` + `open -a
Simulator` FIRST, `pnpm dev-ios-sim` (Next export + cargo sim build + xcodebuild
diff --git a/apps/readest-app/.claude/memory/two-column-spine-curl-6107.md b/apps/readest-app/.claude/memory/two-column-spine-curl-6107.md
new file mode 100644
index 000000000..ca264ae09
--- /dev/null
+++ b/apps/readest-app/.claude/memory/two-column-spine-curl-6107.md
@@ -0,0 +1,25 @@
+---
+name: two-column-spine-curl-6107
+description: "PR #6107 (issue #6106) two-column Page Curl hinged at the spine — review findings, iOS-only cover, deterministic test failure, Xiaomi handoff"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: ce290db5-d7d6-4b12-b726-6e3f5331a846
+ modified: 2026-09-07T08:17:04.736Z
+---
+
+PR #6107 by @kad-air (branch `feat/two-column-spine-curl`, worktree `/Users/chrox/dev/readest-pr-6107`), closes #6106. Reviewed 2026-09-07; CI all green; NOT merged at review time.
+
+**What it does:** with `renderer.columnCount >= 2` the curl turns only the outer column as a leaf hinged at the spine (`uLeaf` open x-interval in the vertex shader; radius decays to 0 so progress 1 is an exact reflection about x=w/2). The leaf's back samples a second texture — a capture of the incoming inner column taken *after* the instant nav, under a native iOS cover view (`cover_webview_region`/`uncover_webview_region`, a `resizableSnapshotView` sibling above the WKWebView that `takeSnapshot` does not render). Android/desktop reject the cover, so the back is theme paper and the canvas fades over the last 20% (`LEAF_FADE_START`).
+
+**Math verified by hand (correct):** a leaf point at `w/2+d` lands at `w/2−d` and samples incoming `u = 2 − 2·vUv.x = 1 − 2d/w`, exactly that point's position inside the captured inner half; the rtl/backward branch (`u = 1 − 2·vUv.x`) matches too. Because both the hinge and `#captureIncoming` use `rect.width/2`, the landed leaf is pixel-exact against the live page **regardless of where the true gutter is** — asymmetric margins / landscape notch only shift the crease line by `(marginLeft − marginRight)/8`, a few px.
+
+**Findings reported to chrox (not posted to the PR):**
+1. `captured-turn.browser.test.ts > commits the incoming column when no overlay interferes` **fails deterministically on this Mac** (4/4, isolated, idle machine) while CI is green. Instrumented cause: the `this.#active !== active` guard after `createImageBitmap` (capturedTurn.ts ~line 844) bails because the turn already ended — headless Chromium's software WebGL makes the two `waitForPaint()` double-rAFs cost ~570 ms vs the 120 ms wait + 450 ms animation. Product-safe (losing the race degrades to paper back + fade) but the test asserts a race the code does not guarantee.
+2. `INCOMING_WAIT_MS = 120` is awaited *before* `#playTo`, so every programmatic two-column turn on iOS freezes on the flat captured page for up to 120 ms before the curl starts. The wait could overlap the early animation (the back is barely visible at low progress).
+3. Late `setIncoming` flips `canvas.style.opacity` from a partly-faded value back to `''` → one-frame brightness pop if the capture lands after progress 0.8.
+4. Fixed-layout (`fixed-layout.js` has no `columnCount`) keeps the old whole-spread curl — PDF/FXL two-page spreads are not covered.
+
+**Device state:** built + installed on the Xiaomi 13 (`pnpm dev-android` from the worktree, md5 `b158c52e21fe4e81308ce8c3cc405d89`, matches the device APK, installed 2026-09-07 16:13). CDP-probed: portrait `columnCount 1`, landscape `columnCount 2`, `captured-turn-style="curl"` — the leaf path IS reachable there, and a `view.next()` turn completes with no JS error. Android therefore exercises the **paper-back + fade** fallback only; the incoming-page-on-the-back half is iOS-only.
+
+**CDP gotcha:** `Page.captureScreenshot` against the Android WebView appears to stall the renderer's rAF loop — five back-to-back shots during a 450 ms turn all came back at progress ~0 (old spread), then the final frame showed the new spread. Mid-turn frames need a held drag (touchMove without release), not screenshot polling; and my synthetic touchMove sequence was consumed as swipe-flips (pages advanced) rather than a scrubbed drag. See [[feedback-always-verify-on-xiaomi]], [[page-turn-styles-viewtransitions-555]].
diff --git a/apps/readest-app/.claude/memory/xml-lang-namespace-selector-6088.md b/apps/readest-app/.claude/memory/xml-lang-namespace-selector-6088.md
new file mode 100644
index 000000000..0af4d0db9
--- /dev/null
+++ b/apps/readest-app/.claude/memory/xml-lang-namespace-selector-6088.md
@@ -0,0 +1,36 @@
+---
+name: xml-lang-namespace-selector-6088
+description: "#6088 book CSS [xml|lang=\"en\"] and :lang() never matched xml:lang in EPUB sections; the srcdoc HTML parse drops the namespace and applyNamespacedAttributes only knew declared prefixes; body{color} loss in the same report is BY DESIGN; getParagraphLayoutStyles is omitted when Use Book Layout is ON, so body{line-height:unset} is not applied and the book's body line-height inherits"
+metadata:
+ type: project
+---
+
+Issue #6088 (email report, Windows 11, 0.12.6, 2026-09-05): a Sigil test book with
+`@namespace xml "http://www.w3.org/XML/1998/namespace"; [xml|lang="en"]{color:red}` and
+`` renders in the theme color. `p.matches(':lang(en)')` was `false` too.
+
+**Root cause:** sections load via `iframe.srcdoc` (HTML parse, see
+[[srcdoc-html-parsing-namespaced-attrs-6038]]), so `xml:lang` is a null-namespace attribute
+named literally `xml:lang`. `applyNamespacedAttributes` (`src/utils/style.ts`) re-attached only
+prefixes declared with `xmlns:*` (+ a fixed `epub` fallback); `xml` is bound by XML itself and is
+NEVER declared, so it was skipped. An older test even asserted that skip as intended.
+
+**Fix (MERGED #6090, squash 04379b8f8 on main, 2026-09-06; UNRELEASED):** `prefix === 'xml'` -> XML namespace twin via
+`setAttributeNS`. Side benefit: `:lang()` (hyphenation, our `:lang(zh|ja|ko)` widows rule) now sees
+`xml:lang`-only books. Playwright dev-web probe: before `rgb(23,23,23)`/false/null, after
+`rgb(255,0,0)`/true/"en". Repro + `probe.cjs` archived in `~/Documents/books/issues/6088/`.
+
+**Same reporter's second pair (body-color.epub / body-color-important.epub) is BY DESIGN
+(chrox decision 2026-09-06):** with override OFF, `body{color:red; line-height:3}` still loses:
+Readest's stylesheet is APPENDED after the book's (`html,body{color}` wins on equal specificity),
+`body{line-height:unset}` is an explicit "dirty hack" in `getPageLayoutStyles`, and
+`p{line-height}` beats inheritance. `!important` in the book flips color only. **Use Book Layout
+did NOT help either** (verified via the settings UI in the probe): `body{line-height:unset}` sat in
+the always-on page-layout chunk, so `
` got `normal`, never the book's 3. FIXED (2nd commit of #6090, in the same squash): the reset moved into `getParagraphLayoutStyles`, so Use Book Layout ON -> 48px at 16px,
+OFF unchanged 22.4px. Color stays theme-controlled BY DESIGN. foliate's fork
+has an UNUSED `setStyles([before, after])` slot ("defaults the book can override") if this is ever
+revisited; moving `html,body{color}` there would make dark-mode books with `body{color:#333}`
+unreadable, which is why it was declined.
+
+Related: [[srcdoc-html-parsing-namespaced-attrs-6038]], [[footnote-aside-namespace-order-4438]],
+[[css-style-fixes]].
diff --git a/apps/readest-app/docs/testing.md b/apps/readest-app/docs/testing.md
index f3734f801..dcebe3c1d 100644
--- a/apps/readest-app/docs/testing.md
+++ b/apps/readest-app/docs/testing.md
@@ -201,6 +201,32 @@ the same port — `chrome://inspect` in a local Chrome, or
parses the same argv for open-with paths and warns on every launch. Prefer the
env var.
+## crengine XPointer Oracle (KOReader sync)
+
+KOReader stores positions as crengine XPointers; `src/utils/xcfi.ts` converts them to and from
+CFIs. The only trustworthy reference for what crengine emits is crengine itself, so
+`apps/readest.koplugin/scripts/xpointer-oracle.lua` runs headlessly inside a KOReader emulator
+build and dumps crengine's XPointer pair and text for every visible word of an EPUB:
+
+```bash
+cd /koreader
+KO_HOME=/tmp/ko-oracle ./luajit /path/to/readest/apps/readest.koplugin/scripts/xpointer-oracle.lua \
+ /path/to/book.epub /path/to/book.crengine.json 40 # keep one word in 40
+```
+
+`src/__tests__/utils/xcfi.crengine-oracle.test.ts` then checks both sync directions for every
+sampled word: crengine's pointers must resolve to exactly that word, and a highlight on that word
+must convert back to exactly crengine's pointers. Committed fixtures live in
+`src/__tests__/fixtures/crengine/` (generated from EPUBs under `fixtures/data/`). To validate a
+local book that cannot be committed:
+
+```bash
+XPOINTER_ORACLE=/path/to/book.crengine.json pnpm test src/__tests__/utils/xcfi.crengine-oracle.test.ts
+```
+
+The JSON's `epub` field is an absolute path or a file next to the JSON. The emulator is a local
+build (`kodev` in a KOReader checkout); nothing in CI needs it.
+
## Test File Naming
| Suffix | Runner | Environment |
diff --git a/apps/readest-app/src/__tests__/fixtures/crengine/sample-alice.json b/apps/readest-app/src/__tests__/fixtures/crengine/sample-alice.json
new file mode 100644
index 000000000..476c43b8e
--- /dev/null
+++ b/apps/readest-app/src/__tests__/fixtures/crengine/sample-alice.json
@@ -0,0 +1,3648 @@
+{
+ "every": 40,
+ "fragments": [
+ { "words": [], "word_count": 0, "index": 0, "docfragment": 1 },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[2]/body/div[1]/h1/text().5",
+ "xp": "/body/DocFragment[2]/body/div[1]/h1/text().0",
+ "text": "Alice"
+ }
+ ],
+ "word_count": 15,
+ "index": 1,
+ "docfragment": 2
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[1]/strong/text().5",
+ "xp": "/body/DocFragment[3]/body/div/div[1]/strong/text().0",
+ "text": "About"
+ },
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[1]/p/text().263",
+ "xp": "/body/DocFragment[3]/body/div/div[1]/p/text().260",
+ "text": "its"
+ },
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[1]/p/text().501",
+ "xp": "/body/DocFragment[3]/body/div/div[1]/p/text().494",
+ "text": "ranging"
+ },
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[1]/p/text().750",
+ "xp": "/body/DocFragment[3]/body/div/div[1]/p/text().748",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[2]/strong/text().14",
+ "xp": "/body/DocFragment[3]/body/div/div[2]/strong/text().5",
+ "text": "available"
+ },
+ {
+ "xp_end": "/body/DocFragment[3]/body/div/div[3]/text()[3].43",
+ "xp": "/body/DocFragment[3]/body/div/div[3]/text()[3].39",
+ "text": "this"
+ }
+ ],
+ "word_count": 205,
+ "index": 2,
+ "docfragment": 3
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[4]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[1]/text().175",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[1]/text().172",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[2]/text().83",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[2]/text().79",
+ "text": "made"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[3]/text().5",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[3]/text().0",
+ "text": "There"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[3]/text().196",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[3]/text().192",
+ "text": "over"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[3]/text().402",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[3]/text().400",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[3]/text().602",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[3]/text().598",
+ "text": "with"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[4]/text().77",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[4]/text().74",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[5]/text().174",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[5]/text().171",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[6]/text().132",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[6]/text().126",
+ "text": "wonder"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[6]/text().326",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[6]/text().322",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[6]/text().535",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[6]/text().533",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[7]/text().32",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[7]/text().25",
+ "text": "herself"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[7]/text().217",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[7]/text().215",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[8]/text().167",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[8]/text().161",
+ "text": "centre"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[8]/text().364",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[8]/text().363",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[8]/text().572",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[8]/text().570",
+ "text": "or"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[9]/text().87",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[9]/text().82",
+ "text": "funny"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[9]/text().293",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[9]/text().289",
+ "text": "word"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[9]/text().502",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[9]/text().495",
+ "text": "through"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[10]/text().23",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[10]/text().18",
+ "text": "There"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[10]/text().214",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[10]/text().210",
+ "text": "time"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[10]/text().391",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[10]/text().388",
+ "text": "eat"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[10]/text().593",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[10]/text().590",
+ "text": "see"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[10]/text().790",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[10]/text().787",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[11]/text().9",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[11]/text().6",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[11]/text().208",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[11]/text().204",
+ "text": "down"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[11]/text().392",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[11]/text().389",
+ "text": "She"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[12]/text().5",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[12]/text().0",
+ "text": "There"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[12]/text().210",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[12]/text().206",
+ "text": "ever"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[13]/text().182",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[13]/text().180",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[13]/text().373",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[13]/text().369",
+ "text": "came"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[14]/text().21",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[14]/text().17",
+ "text": "door"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[14]/text().216",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[14]/text().212",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[14]/text().434",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[14]/text().432",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[14]/text().605",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[14]/text().604",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[15]/text().99",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[15]/text().93",
+ "text": "hoping"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[15]/text().308",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[15]/text().304",
+ "text": "tied"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[16]/text().82",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[16]/text().80",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[16]/text().282",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[16]/text().278",
+ "text": "wild"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[16]/text().490",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[16]/text().487",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[17]/text().20",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[17]/text().14",
+ "text": "bottle"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[17]/text().231",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[17]/text().228",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[22]/text().3",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[22]/text().0",
+ "text": "And"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[22]/text().214",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[22]/text().208",
+ "text": "waited"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[22]/text().416",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[22]/text().415",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[23]/text().13",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[23]/text().8",
+ "text": "while"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[23]/text().225",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[23]/text().223",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[23]/text().416",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[23]/text().413",
+ "text": "too"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[24]/text().113",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[24]/text().109",
+ "text": "this"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[24]/text().345",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[24]/text().339",
+ "text": "having"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[24]/text().556",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[24]/text().550",
+ "text": "people"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[25]/text().124",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[25]/text().122",
+ "text": "on"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[25]/text().324",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[25]/text().319",
+ "text": "under"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[26]/text().111",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[26]/text().108",
+ "text": "her"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[26]/text().313",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[26]/text().309",
+ "text": "into"
+ },
+ {
+ "xp_end": "/body/DocFragment[4]/body/div/div/div/p[27]/text().33",
+ "xp": "/body/DocFragment[4]/body/div/div/div/p[27]/text().29",
+ "text": "soon"
+ }
+ ],
+ "word_count": 2265,
+ "index": 3,
+ "docfragment": 4
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[5]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[1]/text().199",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[1]/text().195",
+ "text": "Good"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[1]/text().396",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[1]/text().387",
+ "text": "stockings"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[1]/text().565",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[1]/text().561",
+ "text": "kind"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[2]/text().43",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[2]/text().40",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[5]/text().18",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[5]/text().12",
+ "text": "Fender"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[8]/text().136",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[8]/text().130",
+ "text": "little"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[9]/text().150",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[9]/text().142",
+ "text": "hopeless"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[10]/text().152",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[10]/text().151",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[11]/text().41",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[11]/text().32",
+ "text": "pattering"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[11]/text().239",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[11]/text().237",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[11]/text().441",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[11]/text().438",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[11]/text().643",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[11]/text().640",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[12]/text().146",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[12]/text().141",
+ "text": "queer"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[12]/text().330",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[12]/text().329",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[12]/text().532",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[12]/text().528",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[13]/text().92",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[13]/text().91",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[13]/text().257",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[13]/text().254",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[13]/text().438",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[13]/text().436",
+ "text": "oh"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[13]/text().644",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[13]/text().643",
+ "text": "s"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[13]/text().838",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[13]/text().836",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[17]/text().24",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[17]/text().19",
+ "text": "scale"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[22]/text().98",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[22]/text().96",
+ "text": "as"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[22]/text().279",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[22]/text().277",
+ "text": "No"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[22]/text().456",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[22]/text().453",
+ "text": "say"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[22]/text().639",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[22]/text().633",
+ "text": "sudden"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[23]/text().84",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[23]/text().81",
+ "text": "had"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[23]/text().276",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[23]/text().269",
+ "text": "measure"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[23]/text().476",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[23]/text().469",
+ "text": "holding"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[24]/text().139",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[24]/text().136",
+ "text": "And"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[24]/text().343",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[24]/text().338",
+ "text": "worse"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[25]/text().76",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[25]/text().73",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[25]/text().260",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[25]/text().257",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[25]/text().468",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[25]/text().462",
+ "text": "wooden"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[26]/text().9",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[26]/text().8",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[26]/text().200",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[26]/text().198",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[27]/text().152",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[27]/text().150",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[28]/text().49",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[28]/text().47",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[28]/text().226",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[28]/text().224",
+ "text": "do"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[28]/text().421",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[28]/text().418",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[28]/text().625",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[28]/text().619",
+ "text": "little"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[29]/text().202",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[29]/text().199",
+ "text": "how"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[29]/text().409",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[29]/text().405",
+ "text": "over"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[30]/text().70",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[30]/text().65",
+ "text": "Would"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[31]/text().151",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[31]/text().146",
+ "text": "fancy"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[31]/text().347",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[31]/text().344",
+ "text": "her"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[31]/text().545",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[31]/text().536",
+ "text": "bristling"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[32]/text().75",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[32]/text().71",
+ "text": "tail"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[33]/text().61",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[33]/text().58",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[33]/text().266",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[33]/text().260",
+ "text": "bright"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[33]/text().445",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[33]/text().444",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[33]/text().633",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[33]/text().624",
+ "text": "sorrowful"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[34]/text().29",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[34]/text().27",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[34]/text().220",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[34]/text().217",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[34]/text().415",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[34]/text().411",
+ "text": "dogs"
+ },
+ {
+ "xp_end": "/body/DocFragment[5]/body/div/div/div/p[35]/text().202",
+ "xp": "/body/DocFragment[5]/body/div/div/div/p[35]/text().193",
+ "text": "creatures"
+ }
+ ],
+ "word_count": 2213,
+ "index": 4,
+ "docfragment": 5
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[6]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[1]/text().171",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[1]/text().168",
+ "text": "wet"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[2]/text().200",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[2]/text().198",
+ "text": "if"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[2]/text().401",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[2]/text().398",
+ "text": "not"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[3]/text().80",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[3]/text().77",
+ "text": "out"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[3]/text().270",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[3]/text().268",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[4]/text().116",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[4]/text().111",
+ "text": "round"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[4]/text().348",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[4]/text().346",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[8]/text().55",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[8]/text().50",
+ "text": "Edwin"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[10]/text().74",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[10]/text().72",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[12]/text().43",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[12]/text().40",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[12]/text().264",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[12]/text().260",
+ "text": "dear"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[14]/text().67",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[14]/text().63",
+ "text": "move"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[15]/text().139",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[15]/text().136",
+ "text": "And"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[16]/text().107",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[16]/text().102",
+ "text": "would"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[17]/text().173",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[17]/text().165",
+ "text": "inclined"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[18]/text().171",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[18]/text().169",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[19]/text().199",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[19]/text().194",
+ "text": "three"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[19]/text().398",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[19]/text().395",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[20]/text().76",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[20]/text().73",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[20]/text().294",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[20]/text().291",
+ "text": "won"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[22]/text().118",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[22]/text().115",
+ "text": "her"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[23]/text().149",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[23]/text().145",
+ "text": "into"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[25]/text().67",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[25]/text().64",
+ "text": "got"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[28]/text().104",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[28]/text().101",
+ "text": "beg"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[29]/text().115",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[29]/text().112",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[30]/text().111",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[30]/text().107",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[30]/text().309",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[30]/text().300",
+ "text": "something"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[32]/text().21",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[32]/text().20",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[33]/text().150",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[33]/text().145",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[43]/text().1",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[43]/text().0",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[63]/text().2",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[63]/text().1",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[79]/text().11",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[79]/text().7",
+ "text": "your"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[81]/text().95",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[81]/text().93",
+ "text": "Oh"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[83]/text().51",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[83]/text().49",
+ "text": "re"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[85]/text().165",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[85]/text().154",
+ "text": "impatiently"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[86]/text().167",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[86]/text().164",
+ "text": "Let"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[87]/text().31",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[87]/text().30",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[89]/text().50",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[89]/text().48",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[89]/text().229",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[89]/text().223",
+ "text": "little"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[90]/text().194",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[90]/text().190",
+ "text": "home"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[90]/text().391",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[90]/text().386",
+ "text": "moved"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[91]/text().153",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[91]/text().150",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[6]/body/div/div/div/p[91]/text().350",
+ "xp": "/body/DocFragment[6]/body/div/div/div/p[91]/text().345",
+ "text": "heard"
+ }
+ ],
+ "word_count": 1751,
+ "index": 5,
+ "docfragment": 6
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[7]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[1]/text().177",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[1]/text().174",
+ "text": "The"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[1]/text().379",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[1]/text().376",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[1]/text().578",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[1]/text().575",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[2]/text().138",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[2]/text().133",
+ "text": "doing"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[2]/text().340",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[2]/text().333",
+ "text": "explain"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[3]/text().158",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[3]/text().152",
+ "text": "gloves"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[3]/text().351",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[3]/text().344",
+ "text": "without"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[4]/text().51",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[4]/text().49",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[4]/text().269",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[4]/text().268",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[4]/text().453",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[4]/text().451",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[5]/text().129",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[5]/text().126",
+ "text": "two"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[5]/text().313",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[5]/text().310",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[5]/text().528",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[5]/text().525",
+ "text": "eat"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[6]/text().21",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[6]/text().18",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[6]/text().239",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[6]/text().237",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[7]/text().24",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[7]/text().22",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[7]/text().229",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[7]/text().222",
+ "text": "against"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[7]/text().423",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[7]/text().422",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[8]/text().155",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[8]/text().153",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[9]/text().129",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[9]/text().124",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[9]/text().313",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[9]/text().309",
+ "text": "When"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[9]/text().498",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[9]/text().497",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[10]/text().55",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[10]/text().50",
+ "text": "older"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[11]/text().55",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[11]/text().52",
+ "text": "can"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[12]/text().93",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[12]/text().81",
+ "text": "conversation"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[13]/text().122",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[13]/text().116",
+ "text": "stairs"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[13]/text().324",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[13]/text().322",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[14]/text().193",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[14]/text().190",
+ "text": "say"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[15]/text().146",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[15]/text().142",
+ "text": "hand"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[15]/text().340",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[15]/text().337",
+ "text": "had"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[16]/text().142",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[16]/text().139",
+ "text": "for"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[19]/text().23",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[19]/text().20",
+ "text": "yer"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[22]/text().27",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[22]/text().19",
+ "text": "business"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[23]/text().151",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[23]/text().149",
+ "text": "Do"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[23]/text().360",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[23]/text().352",
+ "text": "cucumber"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[23]/text().540",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[23]/text().534",
+ "text": "longer"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[24]/text().210",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[24]/text().204",
+ "text": "ladder"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[24]/text().396",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[24]/text().392",
+ "text": "they"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[24]/text().596",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[24]/text().592",
+ "text": "Bill"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[24]/text().758",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[24]/text().755",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[25]/text().186",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[25]/text().184",
+ "text": "is"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[26]/text().133",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[26]/text().131",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[27]/text().46",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[27]/text().40",
+ "text": "chorus"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[27]/text().254",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[27]/text().250",
+ "text": "What"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[28]/text().161",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[28]/text().159",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[30]/text().17",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[30]/text().14",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[31]/text().85",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[31]/text().81",
+ "text": "they"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[32]/text().15",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[32]/text().13",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[32]/text().205",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[32]/text().203",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[33]/text().111",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[33]/text().106",
+ "text": "floor"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[33]/text().293",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[33]/text().289",
+ "text": "make"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[34]/text().182",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[34]/text().179",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[34]/text().390",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[34]/text().386",
+ "text": "made"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[35]/text().63",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[35]/text().60",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[36]/text().32",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[36]/text().30",
+ "text": "no"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[36]/text().251",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[36]/text().248",
+ "text": "her"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[37]/text().171",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[37]/text().167",
+ "text": "tone"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[37]/text().362",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[37]/text().359",
+ "text": "all"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[38]/text().184",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[38]/text().177",
+ "text": "delight"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[38]/text().397",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[38]/text().395",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[38]/text().595",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[38]/text().590",
+ "text": "under"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[38]/text().805",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[38]/text().803",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[39]/text().85",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[39]/text().81",
+ "text": "once"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[40]/text().87",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[40]/text().78",
+ "text": "buttercup"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[40]/text().280",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[40]/text().279",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[41]/text().54",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[41]/text().48",
+ "text": "looked"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[41]/text().266",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[41]/text().263",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[42]/text().3",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[42]/text().0",
+ "text": "She"
+ },
+ {
+ "xp_end": "/body/DocFragment[7]/body/div/div/div/p[42]/text().227",
+ "xp": "/body/DocFragment[7]/body/div/div/div/p[42]/text().224",
+ "text": "and"
+ }
+ ],
+ "word_count": 2772,
+ "index": 6,
+ "docfragment": 7
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[8]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[2]/text().12",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[2]/text().9",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[3]/text().189",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[3]/text().184",
+ "text": "think"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[5]/text().88",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[5]/text().85",
+ "text": "see"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[7]/text().160",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[7]/text().158",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[9]/text().143",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[9]/text().138",
+ "text": "after"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[11]/text().89",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[11]/text().85",
+ "text": "feel"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[13]/text().163",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[13]/text().159",
+ "text": "drew"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[15]/text().108",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[15]/text().97",
+ "text": "Caterpillar"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[18]/text().24",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[18]/text().20",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[21]/text().99",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[21]/text().94",
+ "text": "might"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[22]/text().4",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[22]/text().3",
+ "text": "m"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[24]/text().17",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[24]/text().12",
+ "text": "tried"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[27]/text().28",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[27]/text().21",
+ "text": "William"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[32]/text().15",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[32]/text().13",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[37]/text().14",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[37]/text().8",
+ "text": "turned"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[41]/text().27",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[41]/text().19",
+ "text": "ointment"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[46]/text().7",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[46]/text().3",
+ "text": "Pray"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[50]/text().25",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[50]/text().23",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[55]/text().16",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[55]/text().8",
+ "text": "answered"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[59]/text().23",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[59]/text().18",
+ "text": "right"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[62]/text().19",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[62]/text().16",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[65]/text().24",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[65]/text().21",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[68]/text().42",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[68]/text().36",
+ "text": "larger"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[69]/text().133",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[69]/text().129",
+ "text": "high"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[71]/text().53",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[71]/text().42",
+ "text": "Caterpillar"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[72]/text().148",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[72]/text().144",
+ "text": "once"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[73]/text().4",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[73]/text().1",
+ "text": "One"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[75]/text().38",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[75]/text().36",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[75]/text().246",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[75]/text().243",
+ "text": "far"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[76]/text().117",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[76]/text().113",
+ "text": "next"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[77]/text().127",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[77]/text().118",
+ "text": "shrinking"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[77]/text().317",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[77]/text().314",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[81]/text().42",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[81]/text().37",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[81]/text().256",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[81]/text().254",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[82]/text().117",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[82]/text().115",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[83]/text().52",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[83]/text().47",
+ "text": "hands"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[83]/text().253",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[83]/text().249",
+ "text": "into"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[83]/text().449",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[83]/text().448",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[86]/text().48",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[86]/text().45",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[88]/text().15",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[88]/text().12",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[89]/text().58",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[89]/text().55",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[90]/text().135",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[90]/text().134",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[92]/text().63",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[92]/text().54",
+ "text": "continued"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[93]/text().25",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[93]/text().24",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[95]/text().75",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[95]/text().72",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[96]/text().150",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[96]/text().149",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[97]/text().54",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[97]/text().53",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[98]/text().104",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[98]/text().103",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[99]/text().189",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[99]/text().185",
+ "text": "what"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[100]/text().122",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[100]/text().115",
+ "text": "shouldn"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[101]/text().156",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[101]/text().153",
+ "text": "for"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[101]/text().364",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[101]/text().362",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[102]/text().42",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[102]/text().34",
+ "text": "anything"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[102]/text().242",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[102]/text().237",
+ "text": "these"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[102]/text().429",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[102]/text().427",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[8]/body/div/div/div/p[102]/text().635",
+ "xp": "/body/DocFragment[8]/body/div/div/div/p[102]/text().631",
+ "text": "size"
+ }
+ ],
+ "word_count": 2280,
+ "index": 7,
+ "docfragment": 8
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[9]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[1]/text().181",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[1]/text().174",
+ "text": "footman"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[1]/text().394",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[1]/text().391",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[1]/text().598",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[1]/text().596",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[2]/text().203",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[2]/text().200",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[3]/text().29",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[3]/text().26",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[4]/text().166",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[4]/text().163",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[6]/text().81",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[6]/text().74",
+ "text": "reasons"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[6]/text().296",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[6]/text().290",
+ "text": "within"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[8]/text().29",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[8]/text().27",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[8]/text().238",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[8]/text().235",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[8]/text().432",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[8]/text().429",
+ "text": "any"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[10]/text().76",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[10]/text().68",
+ "text": "skimming"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[11]/text().98",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[11]/text().90",
+ "text": "happened"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[14]/text().44",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[14]/text().42",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[15]/text().97",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[15]/text().96",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[18]/text().50",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[18]/text().45",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[19]/text().127",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[19]/text().126",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[20]/text().75",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[20]/text().73",
+ "text": "as"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[21]/text().188",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[21]/text().182",
+ "text": "things"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[22]/text().73",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[22]/text().70",
+ "text": "not"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[24]/text().39",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[24]/text().33",
+ "text": "sudden"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[25]/text().48",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[25]/text().41",
+ "text": "grinned"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[27]/text().95",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[27]/text().91",
+ "text": "into"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[29]/text().130",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[29]/text().118",
+ "text": "conversation"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[29]/text().325",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[29]/text().321",
+ "text": "came"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[29]/text().544",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[29]/text().539",
+ "text": "blows"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[30]/text().194",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[30]/text().188",
+ "text": "nearly"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[32]/text().68",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[32]/text().66",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[33]/text().8",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[33]/text().1",
+ "text": "Talking"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[34]/text().159",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[34]/text().157",
+ "text": "so"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[35]/text().135",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[35]/text().131",
+ "text": "sort"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[39]/text().13",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[39]/text().11",
+ "text": "he"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[43]/text().134",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[43]/text().128",
+ "text": "howled"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[51]/text().25",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[51]/text().24",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[51]/text().218",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[51]/text().217",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[52]/text().141",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[52]/text().137",
+ "text": "just"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[52]/text().358",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[52]/text().356",
+ "text": "or"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[53]/text().123",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[53]/text().118",
+ "text": "tight"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[53]/text().316",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[53]/text().312",
+ "text": "kill"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[53]/text().505",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[53]/text().500",
+ "text": "grunt"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[54]/text().137",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[54]/text().135",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[54]/text().331",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[54]/text().324",
+ "text": "perhaps"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[55]/text().100",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[55]/text().98",
+ "text": "ll"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[56]/text().44",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[56]/text().37",
+ "text": "herself"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[56]/text().244",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[56]/text().242",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[57]/text().80",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[57]/text().76",
+ "text": "away"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[57]/text().294",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[57]/text().291",
+ "text": "who"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[57]/text().485",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[57]/text().484",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[58]/text().169",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[58]/text().162",
+ "text": "treated"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[59]/text().203",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[59]/text().201",
+ "text": "on"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[62]/text().16",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[62]/text().15",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[65]/text().26",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[65]/text().21",
+ "text": "could"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[66]/text().140",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[66]/text().135",
+ "text": "March"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[68]/text().75",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[68]/text().72",
+ "text": "You"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[71]/text().75",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[71]/text().73",
+ "text": "do"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[74]/text().69",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[74]/text().64",
+ "text": "angry"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[76]/text().22",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[76]/text().18",
+ "text": "like"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[79]/text().9",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[79]/text().6",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[80]/text().43",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[80]/text().39",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[83]/text().12",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[83]/text().6",
+ "text": "waited"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[83]/text().208",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[83]/text().205",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[83]/text().392",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[83]/text().389",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[85]/text().87",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[85]/text().85",
+ "text": "so"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[86]/text().178",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[86]/text().175",
+ "text": "had"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[88]/text().40",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[88]/text().37",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[88]/text().239",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[88]/text().236",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[9]/body/div/div/div/p[88]/text().442",
+ "xp": "/body/DocFragment[9]/body/div/div/div/p[88]/text().435",
+ "text": "herself"
+ }
+ ],
+ "word_count": 2700,
+ "index": 8,
+ "docfragment": 9
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[10]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[1]/text().167",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[1]/text().161",
+ "text": "asleep"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[2]/text().9",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[2]/text().4",
+ "text": "table"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[2]/text().217",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[2]/text().213",
+ "text": "down"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[4]/text().79",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[4]/text().78",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[7]/text().62",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[7]/text().55",
+ "text": "invited"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[9]/text().86",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[9]/text().82",
+ "text": "time"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[11]/text().69",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[11]/text().65",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[13]/text().17",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[13]/text().13",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[16]/text().51",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[16]/text().50",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[17]/text().129",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[17]/text().125",
+ "text": "what"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[19]/text().63",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[19]/text().61",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[20]/text().102",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[20]/text().99",
+ "text": "sat"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[21]/text().85",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[21]/text().81",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[23]/text().4",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[23]/text().1",
+ "text": "Two"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[25]/text().38",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[25]/text().36",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[26]/text().109",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[26]/text().107",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[27]/text().107",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[27]/text().102",
+ "text": "tells"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[29]/text().64",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[29]/text().57",
+ "text": "because"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[31]/text().97",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[31]/text().95",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[33]/text().18",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[33]/text().13",
+ "text": "shook"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[35]/text().25",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[35]/text().20",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[38]/text().111",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[38]/text().105",
+ "text": "asking"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[41]/text().18",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[41]/text().15",
+ "text": "don"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[43]/text().32",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[43]/text().28",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[43]/text().230",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[43]/text().226",
+ "text": "just"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[44]/text().52",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[44]/text().46",
+ "text": "itself"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[46]/text().84",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[46]/text().82",
+ "text": "as"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[48]/text().162",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[48]/text().157",
+ "text": "March"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[52]/text().29",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[52]/text().27",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[57]/text().62",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[57]/text().57",
+ "text": "sleep"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[58]/text().109",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[58]/text().108",
+ "text": "s"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[60]/text().118",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[60]/text().115",
+ "text": "now"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[62]/text().73",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[62]/text().70",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[65]/text().54",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[65]/text().49",
+ "text": "again"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[67]/text().49",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[67]/text().43",
+ "text": "rather"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[69]/text().89",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[69]/text().84",
+ "text": "voice"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[73]/text().5",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[73]/text().1",
+ "text": "Once"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[74]/text().40",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[74]/text().37",
+ "text": "who"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[76]/text().86",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[76]/text().83",
+ "text": "ill"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[78]/text().158",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[78]/text().156",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[81]/text().18",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[81]/text().17",
+ "text": "n"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[84]/text().39",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[84]/text().37",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[85]/text().32",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[85]/text().26",
+ "text": "minute"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[86]/text().141",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[86]/text().133",
+ "text": "remarked"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[88]/text().19",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[88]/text().15",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[90]/text().56",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[90]/text().53",
+ "text": "all"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[92]/text().120",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[92]/text().114",
+ "text": "rather"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[92]/text().324",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[92]/text().320",
+ "text": "milk"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[94]/text().35",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[94]/text().34",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[96]/text().10",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[96]/text().4",
+ "text": "course"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[98]/text().80",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[98]/text().76",
+ "text": "eyes"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[102]/text().32",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[102]/text().28",
+ "text": "eyes"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[102]/text().221",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[102]/text().218",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[103]/text().64",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[103]/text().61",
+ "text": "don"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[105]/text().166",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[105]/text().162",
+ "text": "took"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[106]/text().12",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[106]/text().8",
+ "text": "rate"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[107]/text().43",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[107]/text().40",
+ "text": "one"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[108]/text().13",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[108]/text().10",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[108]/text().224",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[108]/text().221",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[10]/body/div/div/div/p[108]/text().425",
+ "xp": "/body/DocFragment[10]/body/div/div/div/p[108]/text().423",
+ "text": "at"
+ }
+ ],
+ "word_count": 2416,
+ "index": 9,
+ "docfragment": 10
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[11]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[1]/text().177",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[1]/text().176",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[2]/text().11",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[2]/text().10",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[4]/text().54",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[4]/text().49",
+ "text": "Queen"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[7]/text().63",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[7]/text().60",
+ "text": "was"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[8]/text().157",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[8]/text().155",
+ "text": "he"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[10]/text().51",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[10]/text().48",
+ "text": "Two"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[10]/text().224",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[10]/text().222",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[10]/text().434",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[10]/text().429",
+ "text": "Queen"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[11]/text().66",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[11]/text().62",
+ "text": "like"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[11]/text().297",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[11]/text().289",
+ "text": "children"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[11]/text().531",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[11]/text().525",
+ "text": "Rabbit"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[11]/text().754",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[11]/text().751",
+ "text": "all"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[12]/text().152",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[12]/text().151",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[12]/text().359",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[12]/text().353",
+ "text": "waited"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[14]/text().17",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[14]/text().14",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[15]/text().122",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[15]/text().118",
+ "text": "pack"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[16]/text().146",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[16]/text().141",
+ "text": "their"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[17]/text().31",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[17]/text().26",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[19]/text().22",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[19]/text().17",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[21]/text().61",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[21]/text().56",
+ "text": "Knave"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[23]/text().154",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[23]/text().146",
+ "text": "children"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[25]/text().49",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[25]/text().45",
+ "text": "very"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[26]/text().160",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[26]/text().154",
+ "text": "behind"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[27]/text().143",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[27]/text().137",
+ "text": "minute"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[30]/text().33",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[30]/text().28",
+ "text": "Queen"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[33]/text().105",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[33]/text().99",
+ "text": "happen"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[36]/text().18",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[36]/text().14",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[37]/text().16",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[37]/text().12",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[40]/text().53",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[40]/text().48",
+ "text": "Alice"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[41]/text().50",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[41]/text().45",
+ "text": "voice"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[42]/text().56",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[42]/text().55",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[42]/text().277",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[42]/text().271",
+ "text": "arches"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[43]/text().232",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[43]/text().226",
+ "text": "nicely"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[43]/text().447",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[43]/text().443",
+ "text": "when"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[43]/text().656",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[43]/text().651",
+ "text": "ridge"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[43]/text().862",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[43]/text().858",
+ "text": "that"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[44]/text().181",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[44]/text().178",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[45]/text().99",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[45]/text().96",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[46]/text().21",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[46]/text().16",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[46]/text().233",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[46]/text().230",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[47]/text().66",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[47]/text().61",
+ "text": "mouth"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[48]/text().176",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[48]/text().171",
+ "text": "whole"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[48]/text().380",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[48]/text().375",
+ "text": "sight"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[49]/text().169",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[49]/text().165",
+ "text": "seem"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[49]/text().362",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[49]/text().360",
+ "text": "go"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[50]/text().41",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[50]/text().38",
+ "text": "Cat"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[51]/text().192",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[51]/text().189",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[54]/text().44",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[54]/text().40",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[57]/text().28",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[57]/text().24",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[59]/text().5",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[59]/text().1",
+ "text": "Well"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[60]/text().55",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[60]/text().43",
+ "text": "difficulties"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[62]/text().60",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[62]/text().56",
+ "text": "game"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[62]/text().273",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[62]/text().270",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[63]/text().40",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[63]/text().36",
+ "text": "with"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[63]/text().271",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[63]/text().269",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[64]/text().145",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[64]/text().139",
+ "text": "matter"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[64]/text().356",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[64]/text().350",
+ "text": "friend"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[65]/text().207",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[65]/text().205",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[66]/text().154",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[66]/text().152",
+ "text": "at"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[67]/text().126",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[67]/text().121",
+ "text": "never"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[68]/text().101",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[68]/text().97",
+ "text": "talk"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[69]/text().197",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[69]/text().192",
+ "text": "grave"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[71]/text().96",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[71]/text().92",
+ "text": "went"
+ },
+ {
+ "xp_end": "/body/DocFragment[11]/body/div/div/div/p[72]/text().183",
+ "xp": "/body/DocFragment[11]/body/div/div/div/p[72]/text().181",
+ "text": "up"
+ }
+ ],
+ "word_count": 2577,
+ "index": 10,
+ "docfragment": 11
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[12]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[1]/text().168",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[1]/text().160",
+ "text": "together"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[3]/text().19",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[3]/text().12",
+ "text": "Duchess"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[3]/text().217",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[3]/text().214",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[3]/text().425",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[3]/text().417",
+ "text": "tempered"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[4]/text().117",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[4]/text().114",
+ "text": "ear"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[5]/text().18",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[5]/text().17",
+ "text": "t"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[7]/text().23",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[7]/text().19",
+ "text": "like"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[7]/text().246",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[7]/text().243",
+ "text": "did"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[9]/text().37",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[9]/text().34",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[11]/text().58",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[11]/text().51",
+ "text": "Duchess"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[12]/text().44",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[12]/text().38",
+ "text": "things"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[13]/text().178",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[13]/text().173",
+ "text": "Shall"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[15]/text().110",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[15]/text().103",
+ "text": "feather"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[19]/text().35",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[19]/text().28",
+ "text": "Duchess"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[20]/text().11",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[20]/text().7",
+ "text": "know"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[21]/text().82",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[21]/text().79",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[21]/text().283",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[21]/text().279",
+ "text": "what"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[22]/text().138",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[22]/text().136",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[25]/text().40",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[25]/text().37",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[26]/text().131",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[26]/text().129",
+ "text": "it"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[29]/text().36",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[29]/text().33",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[30]/text().161",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[30]/text().156",
+ "text": "began"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[32]/text().21",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[32]/text().17",
+ "text": "fair"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[33]/text().53",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[33]/text().47",
+ "text": "moment"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[35]/text().43",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[35]/text().40",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[36]/text().22",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[36]/text().18",
+ "text": "were"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[36]/text().246",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[36]/text().243",
+ "text": "off"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[36]/text().449",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[36]/text().440",
+ "text": "execution"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[39]/text().32",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[39]/text().28",
+ "text": "Soup"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[42]/text().56",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[42]/text().54",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[43]/text().19",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[43]/text().15",
+ "text": "came"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[43]/text().216",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[43]/text().214",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[43]/text().426",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[43]/text().423",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[44]/text().91",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[44]/text().86",
+ "text": "sight"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[47]/text().10",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[47]/text().1",
+ "text": "Everybody"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[48]/text().82",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[48]/text().79",
+ "text": "sad"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[48]/text().297",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[48]/text().289",
+ "text": "answered"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[49]/text().75",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[49]/text().71",
+ "text": "full"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[51]/text().87",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[51]/text().84",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[52]/text().145",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[52]/text().142",
+ "text": "she"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[54]/text().130",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[54]/text().127",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[54]/text().350",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[54]/text().346",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[55]/text().192",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[55]/text().189",
+ "text": "him"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[58]/text().40",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[58]/text().37",
+ "text": "for"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[58]/text().252",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[58]/text().246",
+ "text": "fellow"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[61]/text().19",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[61]/text().16",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[64]/text().26",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[64]/text().20",
+ "text": "school"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[68]/text().39",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[68]/text().28",
+ "text": "indignantly"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[70]/text().25",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[70]/text().19",
+ "text": "wanted"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[73]/text().25",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[73]/text().23",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[75]/text().38",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[75]/text().36",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[77]/text().68",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[77]/text().62",
+ "text": "uglify"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[79]/text().50",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[79]/text().43",
+ "text": "replied"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[79]/text().280",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[79]/text().278",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[82]/text().47",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[82]/text().44",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[84]/text().40",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[84]/text().33",
+ "text": "Gryphon"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[86]/text().40",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[86]/text().36",
+ "text": "Mock"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[89]/text().39",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[89]/text().36",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[12]/body/div/div/div/p[91]/text().54",
+ "xp": "/body/DocFragment[12]/body/div/div/div/p[91]/text().52",
+ "text": "on"
+ }
+ ],
+ "word_count": 2382,
+ "index": 11,
+ "docfragment": 12
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[13]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[1]/text().177",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[1]/text().175",
+ "text": "as"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[1]/text().378",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[1]/text().376",
+ "text": "he"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[2]/text().205",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[2]/text().201",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[4]/text().62",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[4]/text().59",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[7]/text().19",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[7]/text().14",
+ "text": "twice"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[11]/text().47",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[11]/text().44",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[15]/text().76",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[15]/text().71",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[17]/text().155",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[17]/text().150",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[19]/text().60",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[19]/text().54",
+ "text": "Turtle"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[22]/text().59",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[22]/text().54",
+ "text": "words"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[23]/text().211",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[23]/text().205",
+ "text": "slowly"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[27]/text().8",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[27]/text().5",
+ "text": "are"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[29]/text().42",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[29]/text().39",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[32]/text().39",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[32]/text().36",
+ "text": "far"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[35]/text().49",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[35]/text().44",
+ "text": "could"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[39]/text().27",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[39]/text().20",
+ "text": "beloved"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[41]/text().54",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[41]/text().51",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[43]/text().22",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[43]/text().15",
+ "text": "whiting"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[45]/text().68",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[45]/text().65",
+ "text": "you"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[47]/text().41",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[47]/text().38",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[47]/text().249",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[47]/text().247",
+ "text": "he"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[48]/text().173",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[48]/text().168",
+ "text": "tails"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[50]/text().34",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[50]/text().32",
+ "text": "if"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[53]/text().45",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[53]/text().40",
+ "text": "boots"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[55]/text().84",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[55]/text().80",
+ "text": "They"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[57]/text().59",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[57]/text().54",
+ "text": "great"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[59]/text().107",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[59]/text().104",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[61]/text().37",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[61]/text().36",
+ "text": "a"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[64]/text().2",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[64]/text().1",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[65]/text().95",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[65]/text().92",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[67]/text().110",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[67]/text().106",
+ "text": "time"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[68]/text().204",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[68]/text().201",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[68]/text().425",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[68]/text().422",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[70]/text().49",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[70]/text().41",
+ "text": "repeated"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[71]/text().36",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[71]/text().31",
+ "text": "voice"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[72]/text().178",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[72]/text().174",
+ "text": "head"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[74]/text().31",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[74]/text().30",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[78]/text().37",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[78]/text().34",
+ "text": "gay"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[82]/text().48",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[82]/text().47",
+ "text": "I"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[84]/text().104",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[84]/text().98",
+ "text": "happen"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[87]/text().47",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[87]/text().38",
+ "text": "persisted"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[89]/text().26",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[89]/text().21",
+ "text": "verse"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[91]/text().28",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[91]/text().25",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[95]/text().46",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[95]/text().43",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[100]/text().29",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[100]/text().20",
+ "text": "repeating"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[101]/text().60",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[101]/text().57",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[103]/text().27",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[103]/text().24",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[104]/text().44",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[104]/text().42",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[110]/text().12",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[110]/text().5",
+ "text": "ootiful"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[117]/text().33",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[117]/text().29",
+ "text": "Soup"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[123]/text().88",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[123]/text().84",
+ "text": "when"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[125]/text().32",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[125]/text().26",
+ "text": "panted"
+ },
+ {
+ "xp_end": "/body/DocFragment[13]/body/div/div/div/p[127]/text().25",
+ "xp": "/body/DocFragment[13]/body/div/div/div/p[127]/text().21",
+ "text": "Soup"
+ }
+ ],
+ "word_count": 2121,
+ "index": 12,
+ "docfragment": 13
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[14]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[1]/text().182",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[1]/text().180",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[1]/text().385",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[1]/text().380",
+ "text": "other"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[1]/text().570",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[1]/text().566",
+ "text": "done"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[2]/text().54",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[2]/text().51",
+ "text": "but"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[3]/text().17",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[3]/text().14",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[4]/text().4",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[4]/text().1",
+ "text": "And"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[4]/text().226",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[4]/text().222",
+ "text": "last"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[4]/text().436",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[4]/text().432",
+ "text": "just"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[6]/text().21",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[6]/text().17",
+ "text": "down"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[7]/text().125",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[7]/text().123",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[8]/text().115",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[8]/text().109",
+ "text": "Stupid"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[8]/text().308",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[8]/text().306",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[9]/text().172",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[9]/text().170",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[9]/text().366",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[9]/text().364",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[11]/text().42",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[11]/text().36",
+ "text": "blasts"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[15]/text().24",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[15]/text().20",
+ "text": "away"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[18]/text().79",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[18]/text().73",
+ "text": "blasts"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[19]/text().144",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[19]/text().137",
+ "text": "Majesty"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[21]/text().44",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[21]/text().41",
+ "text": "had"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[24]/text().44",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[24]/text().40",
+ "text": "jury"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[26]/text().21",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[26]/text().17",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[29]/text().4",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[29]/text().0",
+ "text": "Here"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[31]/text().8",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[31]/text().5",
+ "text": "did"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[31]/text().208",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[31]/text().207",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[32]/text().193",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[32]/text().188",
+ "text": "would"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[33]/text().76",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[33]/text().74",
+ "text": "to"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[36]/text().63",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[36]/text().61",
+ "text": "re"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[38]/text().27",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[38]/text().24",
+ "text": "had"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[38]/text().235",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[38]/text().227",
+ "text": "wretched"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[40]/text().52",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[40]/text().50",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[41]/text().33",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[41]/text().30",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[44]/text().47",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[44]/text().43",
+ "text": "most"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[48]/text().41",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[48]/text().38",
+ "text": "out"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[50]/text().47",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[50]/text().43",
+ "text": "some"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[54]/text().13",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[54]/text().4",
+ "text": "miserable"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[56]/text().23",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[56]/text().22",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[56]/text().223",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[56]/text().220",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[57]/text().99",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[57]/text().97",
+ "text": "of"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[58]/text().68",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[58]/text().65",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[62]/text().24",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[62]/text().21",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[64]/text().27",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[64]/text().23",
+ "text": "King"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[65]/text().130",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[65]/text().123",
+ "text": "officer"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[67]/text().135",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[67]/text().131",
+ "text": "into"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[70]/text().65",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[70]/text().62",
+ "text": "low"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[71]/text().155",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[71]/text().150",
+ "text": "sight"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[74]/text().112",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[74]/text().104",
+ "text": "Suppress"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[76]/text().33",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[76]/text().29",
+ "text": "with"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[77]/text().17",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[77]/text().14",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[14]/body/div/div/div/p[77]/text().237",
+ "xp": "/body/DocFragment[14]/body/div/div/div/p[77]/text().233",
+ "text": "read"
+ }
+ ],
+ "word_count": 1973,
+ "index": 13,
+ "docfragment": 14
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/h2/span/span[1]/text().7",
+ "xp": "/body/DocFragment[15]/body/div/div/h2/span/span[1]/text().0",
+ "text": "Chapter"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[1]/text().179",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[1]/text().178",
+ "text": "-"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[1]/text().383",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[1]/text().378",
+ "text": "upset"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[2]/text().181",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[2]/text().177",
+ "text": "head"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[3]/text().72",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[3]/text().67",
+ "text": "until"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[4]/text().73",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[4]/text().70",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[4]/text().284",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[4]/text().280",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[5]/text().94",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[5]/text().91",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[5]/text().305",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[5]/text().300",
+ "text": "mouth"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[10]/text().38",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[10]/text().34",
+ "text": "said"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[10]/text().275",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[10]/text().272",
+ "text": "him"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[12]/text().22",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[12]/text().17",
+ "text": "wrote"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[13]/text().41",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[13]/text().38",
+ "text": "for"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[15]/text().8",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[15]/text().5",
+ "text": "not"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[18]/text().108",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[18]/text().105",
+ "text": "now"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[21]/text().102",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[21]/text().100",
+ "text": "in"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[24]/text().8",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[24]/text().3",
+ "text": "haven"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[25]/text().92",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[25]/text().87",
+ "text": "usual"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[27]/text().146",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[27]/text().141",
+ "text": "added"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[29]/text().78",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[29]/text().73",
+ "text": "about"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[31]/text().80",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[31]/text().75",
+ "text": "prove"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[32]/text().145",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[32]/text().141",
+ "text": "name"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[35]/text().9",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[35]/text().4",
+ "text": "doesn"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[38]/text().6",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[38]/text().1",
+ "text": "Begin"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[40]/text().26",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[40]/text().22",
+ "text": "been"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[46]/text().32",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[46]/text().30",
+ "text": "on"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[53]/text().8",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[53]/text().0",
+ "text": "Involved"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[60]/text().9",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[60]/text().6",
+ "text": "let"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[64]/text().95",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[64]/text().90",
+ "text": "hands"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[65]/text().171",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[65]/text().163",
+ "text": "sixpence"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[66]/text().139",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[66]/text().136",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[67]/text().178",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[67]/text().172",
+ "text": "verses"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[68]/text().9",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[68]/text().4",
+ "text": "Knave"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[69]/text().98",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[69]/text().94",
+ "text": "know"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[69]/text().291",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[69]/text().287",
+ "text": "must"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[71]/text().102",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[71]/text().100",
+ "text": "be"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[72]/text().108",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[72]/text().102",
+ "text": "little"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[73]/text().9",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[73]/text().6",
+ "text": "the"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[74]/text().106",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[74]/text().99",
+ "text": "verdict"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[77]/text().50",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[77]/text().44",
+ "text": "purple"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[80]/text().94",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[80]/text().87",
+ "text": "nothing"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[81]/text().167",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[81]/text().164",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[82]/text().38",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[82]/text().32",
+ "text": "sister"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[83]/text().165",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[83]/text().161",
+ "text": "just"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[83]/text().374",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[83]/text().366",
+ "text": "thinking"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[84]/text().135",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[84]/text().132",
+ "text": "and"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[85]/text().131",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[85]/text().127",
+ "text": "were"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[85]/text().333",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[85]/text().325",
+ "text": "listened"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[86]/text().111",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[86]/text().104",
+ "text": "through"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[86]/text().333",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[86]/text().329",
+ "text": "more"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[86]/text().539",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[86]/text().529",
+ "text": "suppressed"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[87]/text().120",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[87]/text().115",
+ "text": "again"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[87]/text().328",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[87]/text().327",
+ "text": "s"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[87]/text().531",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[87]/text().527",
+ "text": "yard"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[88]/text().104",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[88]/text().97",
+ "text": "herself"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[88]/text().323",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[88]/text().319",
+ "text": "many"
+ },
+ {
+ "xp_end": "/body/DocFragment[15]/body/div/div/div/p[88]/text().534",
+ "xp": "/body/DocFragment[15]/body/div/div/div/p[88]/text().531",
+ "text": "the"
+ }
+ ],
+ "word_count": 2244,
+ "index": 14,
+ "docfragment": 15
+ },
+ {
+ "words": [
+ {
+ "xp_end": "/body/DocFragment[16]/body/div/div[2]/b/text().3",
+ "xp": "/body/DocFragment[16]/body/div/div[2]/b/text().0",
+ "text": "www"
+ }
+ ],
+ "word_count": 7,
+ "index": 15,
+ "docfragment": 16
+ }
+ ],
+ "epub": "sample-alice.epub",
+ "dom_version": 20240114
+}
diff --git a/apps/readest-app/src/__tests__/foliate-epub-malformed-xhtml.test.ts b/apps/readest-app/src/__tests__/foliate-epub-malformed-xhtml.test.ts
index 17a1327d7..3350a74bb 100644
--- a/apps/readest-app/src/__tests__/foliate-epub-malformed-xhtml.test.ts
+++ b/apps/readest-app/src/__tests__/foliate-epub-malformed-xhtml.test.ts
@@ -13,7 +13,8 @@ import { describe, expect, it } from 'vitest';
import { EPUB } from 'foliate-js/epub.js';
import type { BookDoc } from '@/libs/document';
-import { getCFIFromXPointer } from '@/utils/xcfi';
+import * as CFI from 'foliate-js/epubcfi.js';
+import { getCFIFromXPointer, getXPointerFromCFI, XCFI } from '@/utils/xcfi';
const CONTAINER = `
@@ -54,7 +55,7 @@ const malformed = `
`;
-type Section = { createDocument: () => Promise };
+type Section = { createDocument: () => Promise; load: () => Promise };
const openEpub = async (files: Record) => {
const epub = new EPUB({
@@ -122,3 +123,123 @@ describe('createDocument on a section that is not well-formed XML (#5625)', () =
expect(cfi).toMatch(/^epubcfi\(/);
});
});
+
+// #5271: the HTML retry is not a faithful parse of such a file. The HTML
+// parser ignores `/>` on non-void elements and re-opens formatting elements
+// across blocks, so an InDesign page anchor `` at the top of
+// a paragraph swallows every following until the next anchor. Positions
+// then disagree with the book's real structure and with KOReader's crengine,
+// which honours `/>`. When the only fault is unclosed void tags, closing them
+// and re-parsing as XML gives the intended DOM; HTML stays the last resort.
+const inDesign = `
+
+
+Chapter 3
+
+
+ 3
+IT’S NOT YOU, IT’S US
+When someone you love has just died, why does it matter?
+Your personal experience is affected by the wider culture.
+You aren’t crazy. The culture is crazy.
It’s not you.
+
+`;
+
+// Still not XML after closing void tags: a bare ampersand.
+const beyondRepair = `
+Notes
+Rock & roll
+`;
+
+const openInDesignFixture = () =>
+ openEpub({
+ 'META-INF/container.xml': CONTAINER,
+ 'OEBPS/content.opf': opf([
+ { id: 'ch1', href: 'ch1.html' },
+ { id: 'ch3', href: 'ch3.html' },
+ { id: 'notes', href: 'notes.html' },
+ ]),
+ 'OEBPS/ch1.html': wellFormed,
+ 'OEBPS/ch3.html': inDesign,
+ 'OEBPS/notes.html': beyondRepair,
+ });
+
+const XHTML_NS = 'http://www.w3.org/1999/xhtml';
+
+/** Text covered by a range CFI, resolved in `doc` like the reader does. */
+const rangeText = (doc: Document, cfi: string) => {
+ const parts = CFI.parse(cfi);
+ (parts.parent ?? parts).shift(); // drop the spine step
+ return CFI.toRange(doc, parts).toString();
+};
+
+const bodyTags = (doc: Document) =>
+ Array.from(doc.body.children).map((el) => el.tagName.toLowerCase());
+
+describe('a file whose only fault is unclosed void tags is repaired as XML (#5271)', () => {
+ it('createDocument keeps the paragraphs under and the anchors empty', async () => {
+ const sections = await openInDesignFixture();
+ const doc = await sections[1]!.createDocument();
+
+ expect(doc.querySelector('parsererror')).toBeNull();
+ expect(doc.documentElement.namespaceURI).toBe(XHTML_NS);
+ expect(bodyTags(doc)).toEqual(['p', 'p', 'p', 'p', 'p']);
+ expect(doc.getElementById('page_25')!.childNodes.length).toBe(0);
+ expect(doc.getElementById('page_26')!.childNodes.length).toBe(0);
+ expect(doc.querySelector('br')!.namespaceURI).toBe(XHTML_NS);
+ });
+
+ it('converts positions the way crengine numbers this chapter', async () => {
+ const sections = await openInDesignFixture();
+ const bookDoc = { sections } as unknown as BookDoc;
+ const doc = await sections[1]!.createDocument();
+
+ // Readest -> KOReader: a highlight on the third paragraph.
+ const node = doc.body.children[2]!.firstChild as Text;
+ const range = doc.createRange();
+ range.setStart(node, 0);
+ range.setEnd(node, 12);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(1), CFI.fromRange(range));
+ const xp = await getXPointerFromCFI(cfi, undefined, undefined, bookDoc);
+ expect(xp.pos0).toBe('/body/DocFragment[2]/body/p[3]/text().0');
+ expect(xp.pos1).toBe('/body/DocFragment[2]/body/p[3]/text().12');
+
+ // KOReader -> Readest: the text after the mid-paragraph anchor is the
+ // paragraph's second text child in crengine's DOM.
+ const back = new XCFI(doc, 1).xPointerToCFI(
+ '/body/DocFragment[2]/body/p[4]/text()[2].0',
+ '/body/DocFragment[2]/body/p[4]/text()[2].8',
+ );
+ expect(rangeText(doc, back)).toBe('affected');
+ });
+
+ it('renders the repaired XML, not the HTML re-parse', async () => {
+ const originalCreate = URL.createObjectURL;
+ const blobs: Blob[] = [];
+ URL.createObjectURL = (blob: Blob) => {
+ blobs.push(blob);
+ return `blob:test/${blobs.length}`;
+ };
+ try {
+ const sections = await openInDesignFixture();
+ await sections[1]!.load();
+ } finally {
+ URL.createObjectURL = originalCreate;
+ }
+ expect(blobs).toHaveLength(1);
+ const rendered = new DOMParser().parseFromString(
+ await blobs[0]!.text(),
+ 'application/xhtml+xml',
+ );
+ expect(rendered.querySelector('parsererror')).toBeNull();
+ expect(bodyTags(rendered)).toEqual(['p', 'p', 'p', 'p', 'p']);
+ });
+
+ it('still falls back to HTML when the file is broken beyond that repair', async () => {
+ const sections = await openInDesignFixture();
+ const doc = await sections[2]!.createDocument();
+
+ expect(doc.body).not.toBeNull();
+ expect(doc.body.querySelector('p')?.textContent).toBe('Rock & roll');
+ });
+});
diff --git a/apps/readest-app/src/__tests__/utils/xcfi.crengine-oracle.test.ts b/apps/readest-app/src/__tests__/utils/xcfi.crengine-oracle.test.ts
new file mode 100644
index 000000000..a9d3c32b7
--- /dev/null
+++ b/apps/readest-app/src/__tests__/utils/xcfi.crengine-oracle.test.ts
@@ -0,0 +1,119 @@
+// Validates Readest's CFI <-> XPointer conversion against XPointers produced
+// by the real crengine (KOReader's engine), captured with
+// apps/readest.koplugin/scripts/xpointer-oracle.lua.
+//
+// Every fixture under src/__tests__/fixtures/crengine/ lists, for each spine
+// item, crengine's XPointer pair and text for a sample of visible words. Both
+// sync directions are checked for every word:
+// KOReader -> Readest: xPointerToCFI(xp, xp_end) must land on exactly that word
+// Readest -> KOReader: a CFI built on that word must convert back to exactly
+// crengine's xp / xp_end
+//
+// Ad hoc validation of any local EPUB (not committed, e.g. a reporter's book):
+// XPOINTER_ORACLE=/path/to/book.crengine.json pnpm test src/__tests__/utils/xcfi.crengine-oracle.test.ts
+// where the JSON's `epub` field is an absolute path or lives next to the JSON.
+import { describe, expect, it } from 'vitest';
+import { existsSync, readdirSync, readFileSync } from 'fs';
+import { dirname, isAbsolute, join, resolve } from 'path';
+import * as CFI from 'foliate-js/epubcfi.js';
+import { DocumentLoader } from '@/libs/document';
+import type { BookDoc } from '@/libs/document';
+import { XCFI } from '@/utils/xcfi';
+
+type OracleWord = { xp: string; xp_end: string; text: string };
+type Oracle = {
+ epub: string;
+ fragments: { index: number; docfragment: number; word_count: number; words: OracleWord[] }[];
+};
+
+const FIXTURES = resolve(__dirname, '../fixtures/crengine');
+const DATA = resolve(__dirname, '../fixtures/data');
+
+const oracles = (): { name: string; oracle: Oracle; epub: string }[] => {
+ const override = process.env['XPOINTER_ORACLE'];
+ const files = override
+ ? [override]
+ : readdirSync(FIXTURES)
+ .filter((f) => f.endsWith('.json'))
+ .map((f) => join(FIXTURES, f));
+ return files.map((file) => {
+ const oracle = JSON.parse(readFileSync(file, 'utf8')) as Oracle;
+ const candidates = isAbsolute(oracle.epub)
+ ? [oracle.epub]
+ : [join(dirname(file), oracle.epub), join(DATA, oracle.epub)];
+ const epub = candidates.find((p) => existsSync(p));
+ if (!epub) throw new Error(`EPUB for ${file} not found: ${candidates.join(', ')}`);
+ return { name: oracle.epub, oracle, epub };
+ });
+};
+
+const openBook = async (file: string): Promise => {
+ const buffer = readFileSync(file);
+ const f = new File([buffer], file.split('/').pop()!, { type: 'application/epub+zip' });
+ const { book } = await new DocumentLoader(f).open();
+ return book;
+};
+
+// crengine writes /body/DocFragment/... (no index) when the book has a single
+// spine item; it resolves the indexed form too, and Readest always emits it.
+const indexed = (xp: string) => xp.replace(/^\/body\/DocFragment\//, '/body/DocFragment[1]/');
+
+const cases = oracles();
+
+describe.each(cases)('crengine XPointer oracle: $name', ({ oracle, epub }) => {
+ it('converts every sampled word both ways exactly as crengine does', async () => {
+ const book = await openBook(epub);
+ expect(book.sections!.length).toBe(oracle.fragments.length);
+
+ const pull: string[] = [];
+ const push: string[] = [];
+ let words = 0;
+ for (const fragment of oracle.fragments) {
+ const { index } = fragment;
+ const section = book.sections![index]!;
+ const doc = await section.createDocument();
+ const converter = new XCFI(doc, index);
+ const base = section.cfi ?? CFI.fake.fromIndex(index);
+
+ for (const word of fragment.words) {
+ words++;
+ const xp = indexed(word.xp);
+ const xpEnd = indexed(word.xp_end);
+
+ // KOReader -> Readest: the range must cover exactly crengine's word.
+ let range: Range | null = null;
+ try {
+ const cfi = converter.xPointerToCFI(xp, xpEnd);
+ const anchored = book.resolveCFI!(cfi)?.anchor?.(doc);
+ const got =
+ !anchored || typeof anchored === 'number' ? '' : anchored.toString();
+ if (got === word.text) range = anchored as Range;
+ else pull.push(`${xp}: expected "${word.text}", got "${got}"`);
+ } catch (e) {
+ pull.push(`${xp}: ${String(e).split('\n')[0]}`);
+ }
+ if (!range) continue; // no trusted location to push from; reported above
+
+ // Readest -> KOReader: a highlight on that range must come back as
+ // crengine's own pointers.
+ try {
+ const cfi = CFI.joinIndir(base, CFI.fromRange(range));
+ const back = converter.cfiToXPointer(cfi);
+ if (back.pos0 !== xp || back.pos1 !== xpEnd) {
+ push.push(
+ `"${word.text}": expected ${xp} .. ${xpEnd}, got ${back.pos0} .. ${back.pos1}`,
+ );
+ }
+ } catch (e) {
+ push.push(`"${word.text}": ${String(e).split('\n')[0]}`);
+ }
+ }
+ }
+
+ const summarize = (label: string, list: string[]) =>
+ `${label}: ${list.length}/${words} mismatches\n ${list.slice(0, 10).join('\n ')}`;
+ // Report both directions in one run rather than stopping at the first.
+ expect.soft(pull, summarize('KOReader -> Readest', pull)).toEqual([]);
+ expect.soft(push, summarize('Readest -> KOReader', push)).toEqual([]);
+ }, 120000);
+});
diff --git a/apps/readest-app/src/__tests__/utils/xcfi.crengine-semantics.test.ts b/apps/readest-app/src/__tests__/utils/xcfi.crengine-semantics.test.ts
new file mode 100644
index 000000000..985b54e13
--- /dev/null
+++ b/apps/readest-app/src/__tests__/utils/xcfi.crengine-semantics.test.ts
@@ -0,0 +1,188 @@
+// XPointer semantics pinned against the real crengine (KOReader's engine),
+// found with apps/readest.koplugin/scripts/xpointer-oracle.lua. See
+// crengine/src/lvtinydom.cpp ldomXPointer::toString: a text step is
+// `/text()[K]` with K counting the parent's TEXT children only, and the `[K]`
+// is omitted when the parent has a single text child. The offset is within
+// that text node, never cumulative over the element's descendants. Likewise
+// `DocFragment[N]` loses its `[N]` when the book has a single spine item.
+import { describe, expect, it } from 'vitest';
+import * as CFI from 'foliate-js/epubcfi.js';
+import { XCFI } from '@/utils/xcfi';
+
+const html = (body: string) =>
+ new DOMParser().parseFromString(`${body}`, 'text/html');
+
+/** Text covered by a range CFI, resolved in `doc` like the reader does. */
+const rangeText = (doc: Document, cfi: string) => {
+ const parts = CFI.parse(cfi);
+ (parts.parent ?? parts).shift(); // drop the spine step
+ return CFI.toRange(doc, parts).toString();
+};
+
+describe('unindexed /text().N is the element’s only text child, offset N inside it', () => {
+ // sample-alice.epub chapter headings: crengine emits h2/text().1 for the
+ // "D" of "Down", i.e. offset 1 of the "\nDown the Rabbit Hole" text node.
+ // Cumulative-over-descendants reading lands on "hapter" instead.
+ const doc = html(
+ '\nDown the Rabbit Hole
',
+ );
+ const converter = new XCFI(doc, 3);
+
+ it('resolves a KOReader range after inline children to the right word', () => {
+ const cfi = converter.xPointerToCFI(
+ '/body/DocFragment[4]/body/h2/text().1',
+ '/body/DocFragment[4]/body/h2/text().5',
+ );
+ expect(rangeText(doc, cfi)).toBe('Down');
+ });
+
+ it('round-trips its own XPointer for a selection after an inline child', () => {
+ const doc2 = html('x rest of it
');
+ const c = new XCFI(doc2, 0);
+ const node = doc2.querySelector('p')!.lastChild as Text; // " rest of it"
+ const range = doc2.createRange();
+ range.setStart(node, 1);
+ range.setEnd(node, 5);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(0), CFI.fromRange(range));
+ const xp = c.cfiToXPointer(cfi);
+ expect(xp.pos0).toBe('/body/DocFragment[1]/body/p/text().1');
+ expect(rangeText(doc2, c.xPointerToCFI(xp.pos0!, xp.pos1!))).toBe('rest');
+ });
+
+ it('skips the whitespace-only text node crengine drops at the start of a block', () => {
+ // crengine parses "\ndelta\nepsilon zeta\n
" as
+ // delta epsilon zeta
: the leading whitespace node is gone
+ // and " epsilon zeta " is the ONLY text child, so it writes p/text().1.
+ const doc3 = html('\ndelta\nepsilon zeta\n
');
+ const c = new XCFI(doc3, 0);
+ const cfi = c.xPointerToCFI(
+ '/body/DocFragment[1]/body/p/text().1',
+ '/body/DocFragment[1]/body/p/text().8',
+ );
+ expect(rangeText(doc3, cfi)).toBe('epsilon');
+ });
+});
+
+describe('indexed /text()[K].N counts text children the way crengine keeps them', () => {
+ // crengine: b c d e
— the leading "\n" is dropped,
+ // " c " and " e" survive, so "e" is text()[2].1 (not [3] as raw DOM counting says).
+ const doc = html('\nb c d e
');
+ const converter = new XCFI(doc, 0);
+
+ it('resolves text()[2] to the second surviving text child', () => {
+ const cfi = converter.xPointerToCFI(
+ '/body/DocFragment[1]/body/p/text()[2].1',
+ '/body/DocFragment[1]/body/p/text()[2].2',
+ );
+ expect(rangeText(doc, cfi)).toBe('e');
+ });
+
+ it('emits text()[2] for a selection in that text child', () => {
+ const node = doc.querySelector('p')!.lastChild as Text; // " e"
+ const range = doc.createRange();
+ range.setStart(node, 1);
+ range.setEnd(node, 2);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(0), CFI.fromRange(range));
+ const xp = converter.cfiToXPointer(cfi);
+ expect(xp.pos0).toBe('/body/DocFragment[1]/body/p/text()[2].1');
+ expect(xp.pos1).toBe('/body/DocFragment[1]/body/p/text()[2].2');
+ });
+});
+
+describe('dropped leading whitespace stays at the block start', () => {
+ it.each([
+ '',
+ '\nepsilon',
+ '\nepsilonzetaeta',
+ ])('preserves a selection starting before inline content with trailing text %j', (trailingText) => {
+ const doc = html(`\ndelta${trailingText}
`);
+ const converter = new XCFI(doc, 0);
+ const range = doc.createRange();
+ range.setStart(doc.querySelector('p')!.firstChild!, 0);
+ range.setEnd(doc.querySelector('em')!.firstChild!, 5);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(0), CFI.fromRange(range));
+
+ const xp = converter.cfiToXPointer(cfi);
+
+ expect(xp.pos0).toBe('/body/DocFragment[1]/body/p');
+ expect(rangeText(doc, converter.xPointerToCFI(xp.pos0!, xp.pos1!)).trim()).toBe('delta');
+ });
+});
+
+describe('offsets count crengine’s whitespace-collapsed text, not the raw source', () => {
+ // crengine keeps "lambda mu nu" as "lambda mu nu": "mu" is text().7..9
+ // there, while the raw DOM text node has it at 9..11.
+ const doc = html('lambda mu nu
');
+ const converter = new XCFI(doc, 0);
+
+ it('maps a crengine offset back to the raw text node', () => {
+ const cfi = converter.xPointerToCFI(
+ '/body/DocFragment[1]/body/p/text().7',
+ '/body/DocFragment[1]/body/p/text().9',
+ );
+ expect(rangeText(doc, cfi)).toBe('mu');
+ });
+
+ it('emits collapsed offsets for a raw selection', () => {
+ const node = doc.querySelector('p')!.firstChild as Text;
+ const range = doc.createRange();
+ range.setStart(node, 9);
+ range.setEnd(node, 11);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(0), CFI.fromRange(range));
+ const xp = converter.cfiToXPointer(cfi);
+ expect(xp.pos0).toBe('/body/DocFragment[1]/body/p/text().7');
+ expect(xp.pos1).toBe('/body/DocFragment[1]/body/p/text().9');
+ });
+});
+
+describe('preformatted text keeps its whitespace, so offsets are raw there', () => {
+ // crengine: alpha2 beta2
has "beta2" at pre/text()[1].8, and the
+ // same holds inside inline outside any : "eta2" at code/text().8.
+ const doc = html('alpha2 beta2\n gamma2
zeta2 eta2 theta2
');
+
+ it.each([
+ [
+ 'pre',
+ '/body/DocFragment[1]/body/pre/text().8',
+ '/body/DocFragment[1]/body/pre/text().13',
+ 'beta2',
+ ],
+ [
+ 'code',
+ '/body/DocFragment[1]/body/p/code/text().8',
+ '/body/DocFragment[1]/body/p/code/text().12',
+ 'eta2',
+ ],
+ ])('resolves a raw offset inside %s', (_tag, start, end, word) => {
+ const cfi = new XCFI(doc, 0).xPointerToCFI(start, end);
+ expect(rangeText(doc, cfi)).toBe(word);
+ });
+
+ it('emits raw offsets for a selection inside code', () => {
+ const node = doc.querySelector('code')!.firstChild as Text;
+ const range = doc.createRange();
+ range.setStart(node, 8);
+ range.setEnd(node, 12);
+ const cfi = CFI.joinIndir(CFI.fake.fromIndex(0), CFI.fromRange(range));
+ const xp = new XCFI(doc, 0).cfiToXPointer(cfi);
+ expect(xp.pos0).toBe('/body/DocFragment[1]/body/p/code/text().8');
+ expect(xp.pos1).toBe('/body/DocFragment[1]/body/p/code/text().12');
+ });
+});
+
+describe('a single-spine-item book has /body/DocFragment/ with no index', () => {
+ const doc = html('only chapter
');
+
+ it('extracts spine index 0', () => {
+ expect(XCFI.extractSpineIndex('/body/DocFragment/body/p/text().5')).toBe(0);
+ });
+
+ it('converts crengine’s unindexed path to a CFI in section 0', () => {
+ const cfi = new XCFI(doc, 0).xPointerToCFI(
+ '/body/DocFragment/body/p/text().5',
+ '/body/DocFragment/body/p/text().12',
+ );
+ expect(cfi.startsWith('epubcfi(/6/2!')).toBe(true);
+ expect(rangeText(doc, cfi)).toBe('chapter');
+ });
+});
diff --git a/apps/readest-app/src/utils/xcfi.ts b/apps/readest-app/src/utils/xcfi.ts
index 2b67f2731..7ec41d562 100644
--- a/apps/readest-app/src/utils/xcfi.ts
+++ b/apps/readest-app/src/utils/xcfi.ts
@@ -12,6 +12,14 @@ type XPointer = {
pos1?: string;
};
+/** Where an XPointer lands: an exact text node position when the pointer
+ * names one, else a cumulative text offset within `element` (or its start). */
+type XPointerTarget = {
+ element: Element;
+ textOffset?: number;
+ point?: { node: Text; offset: number };
+};
+
export class XCFI {
private document: Document;
private spineItemIndex: number;
@@ -34,12 +42,13 @@ export class XCFI {
// CFI uses even numbers starting from 2: 2, 4, 6, 8, ...
// Convert to 0-based: (step - 2) / 2 = 0, 1, 2, 3, ...
return Math.floor((spineStep - 2) / 2);
- } else if (cfiOrXPath.startsWith('/body/DocFragment[')) {
+ } else if (cfiOrXPath.startsWith('/body/DocFragment')) {
// Note that all indices in XPointer/XPath are 1-based
- // but the text() offsets are 0-based
- const match = cfiOrXPath.match(/DocFragment\[(\d+)\]/);
+ // but the text() offsets are 0-based. crengine omits the [N] when
+ // the book has a single spine item: /body/DocFragment/body/...
+ const match = cfiOrXPath.match(/^\/body\/DocFragment(?:\[(\d+)\])?\//);
if (match) {
- return parseInt(match[1]!, 10) - 1;
+ return match[1] ? parseInt(match[1], 10) - 1 : 0;
}
throw new Error('Cannot extract spine index from XPath');
} else {
@@ -132,64 +141,38 @@ export class XCFI {
* Convert a single point XPointer to CFI
*/
private convertPointXPointerToCFI(xpointer: string): string {
- const { element, textOffset } = this.parseXPointer(xpointer);
-
+ const { node, offset } = this.anchorPoint(this.parseXPointer(xpointer));
const range = this.document.createRange();
- if (textOffset !== undefined) {
- const textNode = this.findTextNodeAtOffset(element, textOffset);
- if (textNode) {
- range.setStart(textNode.node, textNode.offset);
- range.setEnd(textNode.node, textNode.offset);
- } else {
- // Fallback to element positioning
- range.setStart(element, 0);
- range.setEnd(element, 0);
- }
- } else {
- range.setStart(element, 0);
- range.setEnd(element, 0);
- }
-
- const cfi = fromRange(range);
- return this.adjustSpineIndex(cfi);
+ range.setStart(node, offset);
+ range.setEnd(node, offset);
+ return this.adjustSpineIndex(fromRange(range));
}
private convertRangeXPointerToCFI(startXPointer: string, endXPointer: string): string {
- const startInfo = this.parseXPointer(startXPointer);
- const endInfo = this.parseXPointer(endXPointer);
-
+ const start = this.anchorPoint(this.parseXPointer(startXPointer));
+ const end = this.anchorPoint(this.parseXPointer(endXPointer));
const range = this.document.createRange();
- if (startInfo.textOffset !== undefined) {
- const startTextNode = this.findTextNodeAtOffset(startInfo.element, startInfo.textOffset);
- if (startTextNode) {
- range.setStart(startTextNode.node, startTextNode.offset);
- } else {
- range.setStart(startInfo.element, 0);
- }
- } else {
- range.setStart(startInfo.element, 0);
- }
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+ return this.adjustSpineIndex(fromRange(range));
+ }
- if (endInfo.textOffset !== undefined) {
- const endTextNode = this.findTextNodeAtOffset(endInfo.element, endInfo.textOffset);
- if (endTextNode) {
- range.setEnd(endTextNode.node, endTextNode.offset);
- } else {
- range.setEnd(endInfo.element, 0);
- }
- } else {
- range.setEnd(endInfo.element, 0);
+ /** DOM position for a parsed XPointer, falling back to the element start. */
+ private anchorPoint(target: XPointerTarget): { node: Node; offset: number } {
+ if (target.point) return target.point;
+ if (target.textOffset !== undefined) {
+ const found = this.findTextNodeAtOffset(target.element, target.textOffset);
+ if (found) return found;
}
-
- const cfi = fromRange(range);
- return this.adjustSpineIndex(cfi);
+ return { node: target.element, offset: 0 };
}
/**
* Parse XPointer string to extract element and text offset
*
* Supports three KOReader text reference formats:
- * - `/text().N` — cumulative character offset across all text in the element
+ * - `/text().N` — offset N in the element's only text child (crengine
+ * omits the `[K]` when there is a single text child)
* - `/text()[K].N` — Kth direct text node child (1-based), offset N within that node
* - `/tag[idx].N` — offset N directly on the last path element (no explicit
* `text()` step); CREngine emits this when the target
@@ -197,7 +180,7 @@ export class XCFI {
* content, e.g. `div[1].0`. Semantically equivalent to
* `/tag[idx]/text().N`.
*/
- private parseXPointer(xpointer: string): { element: Element; textOffset?: number } {
+ private parseXPointer(xpointer: string): XPointerTarget {
// Format: /text()[K].N — indexed text node with offset
const indexedTextMatch = xpointer.match(/\/text\(\)\[(\d+)\]\.(\d+)$/);
if (indexedTextMatch) {
@@ -210,15 +193,13 @@ export class XCFI {
throw new Error(`Cannot resolve XPointer path: ${elementPath}`);
}
- // Find the Kth direct text node child and compute cumulative offset
- const textOffset = this.resolveIndexedTextNode(element, textNodeIndex, offsetInNode);
- return { element, textOffset };
+ return { element, point: this.textChildPoint(element, textNodeIndex, offsetInNode) };
}
- // Format: /text().N — cumulative character offset
+ // Format: /text().N — the element's only text child, offset N inside it
const textOffsetMatch = xpointer.match(/\/text\(\)\.(\d+)$/);
if (textOffsetMatch) {
- const textOffset = parseInt(textOffsetMatch[1]!, 10);
+ const offsetInNode = parseInt(textOffsetMatch[1]!, 10);
const elementPath = xpointer.replace(/\/text\(\)\.\d+$/, '');
const element = this.resolveXPointerPath(elementPath);
@@ -226,7 +207,11 @@ export class XCFI {
throw new Error(`Cannot resolve XPointer path: ${elementPath}`);
}
- return { element, textOffset };
+ const children = XCFI.crengineTextChildren(element);
+ const node = children.find((t) => t.data.trim().length > 0) ?? children[0];
+ // No direct text at all: keep the cumulative reading as a best effort.
+ if (!node) return { element, textOffset: offsetInNode };
+ return { element, point: { node, offset: XCFI.nodeOffset(node, offsetInNode) } };
}
// Format: /tag[idx].N — offset directly on the last element segment, with
@@ -254,40 +239,146 @@ export class XCFI {
return { element };
}
+ private static readonly WHITESPACE = /[ \t\n\r\f]/;
+ /** Tags whose built-in crengine style is `white-space: pre` (fb2def.h). */
+ private static readonly PRE_TAGS = new Set([
+ 'pre',
+ 'code',
+ 'listing',
+ 'plaintext',
+ 'xmp',
+ 'textarea',
+ ]);
+ /** Block containers, where crengine drops leading whitespace-only text. */
+ private static readonly BLOCK_TAGS = new Set([
+ 'body',
+ 'div',
+ 'p',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'ul',
+ 'ol',
+ 'li',
+ 'dl',
+ 'dt',
+ 'dd',
+ 'blockquote',
+ 'pre',
+ 'section',
+ 'article',
+ 'aside',
+ 'header',
+ 'footer',
+ 'nav',
+ 'main',
+ 'figure',
+ 'figcaption',
+ 'table',
+ 'thead',
+ 'tbody',
+ 'tfoot',
+ 'tr',
+ 'td',
+ 'th',
+ 'caption',
+ 'address',
+ 'details',
+ 'summary',
+ 'form',
+ 'fieldset',
+ 'center',
+ ]);
+
+ /** Inside `pre`/`code` and friends crengine keeps whitespace verbatim. */
+ private static preservesWhitespace(node: Node): boolean {
+ for (let el = node.parentElement; el; el = el.parentElement) {
+ if (XCFI.PRE_TAGS.has(el.tagName.toLowerCase())) return true;
+ }
+ return false;
+ }
+
/**
- * Resolve text()[K].N to a cumulative character offset within the element.
- * K is the 1-based index of direct text node children of the element
- * (counting only Text nodes that are immediate children, skipping element children).
- * N is the character offset within that specific text node.
+ * Direct text children as crengine keeps them (ldomDocumentWriter::OnText):
+ * a whitespace-only text node that opens a block is dropped, whitespace
+ * between or after inline children survives as a single space. Pinned
+ * against the real engine with apps/readest.koplugin/scripts/xpointer-oracle.lua.
*/
- private resolveIndexedTextNode(
+ private static crengineTextChildren(element: Element): Text[] {
+ const dropsLeadingBlank =
+ XCFI.BLOCK_TAGS.has(element.tagName.toLowerCase()) &&
+ !XCFI.preservesWhitespace(element.firstChild ?? element);
+ return Array.from(element.childNodes).filter((node, i): node is Text => {
+ if (node.nodeType !== Node.TEXT_NODE) return false;
+ const text = (node as Text).data;
+ if (text.length === 0) return false;
+ return !(dropsLeadingBlank && i === 0 && text.trim() === '');
+ });
+ }
+
+ /** crengine offset -> raw DOM offset inside `node` (see toRawOffset). */
+ private static nodeOffset(node: Text, crengineOffset: number): number {
+ if (XCFI.preservesWhitespace(node)) return Math.min(crengineOffset, node.data.length);
+ return XCFI.toRawOffset(node.data, crengineOffset);
+ }
+
+ /** raw DOM offset inside `node` -> crengine offset (see toCollapsedOffset). */
+ private static crengineOffset(node: Text, rawOffset: number): number {
+ if (XCFI.preservesWhitespace(node)) return rawOffset;
+ return XCFI.toCollapsedOffset(node.data, rawOffset);
+ }
+
+ /**
+ * crengine collapses each whitespace run inside a text node to one space and
+ * counts offsets in that collapsed text; the DOM counts the raw source.
+ */
+ private static toCollapsedOffset(text: string, rawOffset: number): number {
+ let collapsed = 0;
+ for (let i = 0; i < rawOffset && i < text.length; i++) {
+ if (!XCFI.isRunContinuation(text, i)) collapsed++;
+ }
+ return collapsed + Math.max(0, rawOffset - text.length);
+ }
+
+ private static toRawOffset(text: string, collapsedOffset: number): number {
+ let collapsed = 0;
+ for (let i = 0; i < text.length; i++) {
+ if (XCFI.isRunContinuation(text, i)) continue;
+ if (collapsed === collapsedOffset) return i;
+ collapsed++;
+ }
+ return text.length;
+ }
+
+ private static isRunContinuation(text: string, i: number): boolean {
+ return i > 0 && XCFI.WHITESPACE.test(text[i]!) && XCFI.WHITESPACE.test(text[i - 1]!);
+ }
+
+ /**
+ * Resolve text()[K].N to a DOM position. K is the 1-based index among the
+ * direct text children crengine keeps and N is the offset within that text
+ * node's whitespace-collapsed text.
+ */
+ private textChildPoint(
element: Element,
textNodeIndex: number,
offsetInNode: number,
- ): number {
- let directTextCount = 0;
- let cumulativeOffset = 0;
-
- for (const child of Array.from(element.childNodes)) {
- if (child.nodeType === Node.TEXT_NODE) {
- directTextCount++;
- if (directTextCount === textNodeIndex) {
- return cumulativeOffset + offsetInNode;
- }
- cumulativeOffset += (child.textContent || '').length;
- } else if (child.nodeType === Node.ELEMENT_NODE) {
- // Count text length inside child elements for cumulative offset
- cumulativeOffset += (child.textContent || '').length;
- }
+ ): { node: Text; offset: number } {
+ const children = XCFI.crengineTextChildren(element);
+ const node = children[textNodeIndex - 1];
+ if (!node) {
+ throw new Error(
+ `Text node index ${textNodeIndex} out of bounds (found ${children.length} direct text nodes)`,
+ );
}
-
- throw new Error(
- `Text node index ${textNodeIndex} out of bounds (found ${directTextCount} direct text nodes)`,
- );
+ return { node, offset: XCFI.nodeOffset(node, offsetInNode) };
}
private resolveXPointerPath(path: string): Element | null {
- const pathMatch = path.match(/^\/body\/DocFragment\[\d+\]\/body(.*)$/);
+ const pathMatch = path.match(/^\/body\/DocFragment(?:\[\d+\])?\/body(.*)$/);
if (!pathMatch) {
throw new Error(`Invalid XPointer format: ${path}`);
}
@@ -400,9 +491,7 @@ export class XCFI {
*/
private rangePointToXPointer(container: Node, offset: number): string {
if (container.nodeType === Node.TEXT_NODE) {
- // For text nodes, find the containing element
- const element = container.parentElement || this.document.documentElement;
- return this.handleTextOffsetInElement(element, container as Text, offset);
+ return this.textNodeXPointer(container as Text, offset);
} else if (container.nodeType === Node.ELEMENT_NODE) {
const element = container as Element;
if (offset === 0) {
@@ -421,11 +510,7 @@ export class XCFI {
if (targetChild?.nodeType === Node.ELEMENT_NODE) {
return this.buildXPointerPath(targetChild as Element);
} else if (targetChild?.nodeType === Node.TEXT_NODE) {
- return this.handleTextOffsetInElement(
- element,
- targetChild as Text,
- (targetChild as Text).textContent?.length || 0,
- );
+ return this.textNodeXPointer(targetChild as Text, (targetChild as Text).data.length);
} else {
return this.buildXPointerPath(element);
}
@@ -565,49 +650,29 @@ export class XCFI {
return this.buildXPointerPath(element);
}
- // Use the text node's direct parent for both path and indexing.
- // This produces correct XPointers even when inline elements (like )
- // split text into multiple direct text node children.
- const textParent = targetTextNode.parentElement || element;
- const basePath = this.buildXPointerPath(textParent);
-
- // Count direct text node children and find which one the target is
- let directTextCount = 0;
- let directTextIndex = 0;
- for (const child of Array.from(textParent.childNodes)) {
- if (child.nodeType === Node.TEXT_NODE && (child.textContent || '').length > 0) {
- directTextCount++;
- if (child === targetTextNode) {
- directTextIndex = directTextCount;
- }
- }
- }
-
- // Omit [1] when there is only one direct text node (matches KOReader format)
- if (directTextCount <= 1) {
- return `${basePath}/text().${offsetInNode}`;
- }
- return `${basePath}/text()[${directTextIndex || 1}].${offsetInNode}`;
+ return this.textNodeXPointer(targetTextNode, offsetInNode);
}
/**
- * Handle text offset for a specific text node within an element
+ * XPointer of a position inside a specific text node, in KOReader's form:
+ * the node's direct parent gives the path (correct even when inline
+ * elements split the text), the index counts the text children crengine
+ * keeps and the offset counts its whitespace-collapsed text.
*/
- private handleTextOffsetInElement(element: Element, textNode: Text, offset: number): string {
- // Find all text nodes in the element to calculate cumulative offset
- const textNodes: Text[] = [];
- this.collectTextNodes(element, textNodes);
+ private textNodeXPointer(textNode: Text, offsetInNode: number): string {
+ const textParent = textNode.parentElement || this.document.documentElement;
+ const basePath = this.buildXPointerPath(textParent);
+ const siblings = XCFI.crengineTextChildren(textParent);
+ const index = siblings.indexOf(textNode);
+ // Dropped whitespace belongs at the block start, before any inline content.
+ if (index < 0) return basePath;
+ const collapsed = XCFI.crengineOffset(textNode, offsetInNode);
- let cumulativeOffset = 0;
- for (const node of textNodes) {
- if (node === textNode) {
- cumulativeOffset += offset;
- break;
- }
- cumulativeOffset += (node.textContent || '').length;
+ // Omit [1] when there is only one direct text node (matches KOReader format)
+ if (siblings.length <= 1) {
+ return `${basePath}/text().${collapsed}`;
}
-
- return this.handleTextOffset(element, cumulativeOffset);
+ return `${basePath}/text()[${index + 1}].${collapsed}`;
}
/**
diff --git a/apps/readest.koplugin/scripts/xpointer-oracle.lua b/apps/readest.koplugin/scripts/xpointer-oracle.lua
new file mode 100644
index 000000000..169419bbe
--- /dev/null
+++ b/apps/readest.koplugin/scripts/xpointer-oracle.lua
@@ -0,0 +1,74 @@
+-- XPointer oracle: dump crengine's own XPointer for every visible word of an
+-- EPUB, so Readest's CFI <-> XPointer conversion (src/utils/xcfi.ts) can be
+-- validated against the real KOReader engine instead of assumptions about it.
+--
+-- Runs headlessly inside a KOReader emulator build (no window, no sync server):
+--
+-- cd /koreader
+-- KO_HOME=/tmp/ko-oracle ./luajit \
+-- /path/to/readest/apps/readest.koplugin/scripts/xpointer-oracle.lua \
+-- /path/to/book.epub /path/to/book.crengine.json [every]
+--
+-- `every` keeps one word in N (default 1) to keep committed fixtures small.
+-- KO_HOME keeps the harness settings and cache out of the emulator directory.
+--
+-- Consume the JSON with apps/readest-app/src/__tests__/utils/xcfi.crengine-oracle.test.ts,
+-- either as a committed fixture under src/__tests__/fixtures/crengine/ or ad hoc:
+-- XPOINTER_ORACLE=/path/to/book.crengine.json pnpm test src/__tests__/utils/xcfi.crengine-oracle.test.ts
+require("setupkoenv")
+package.path = "spec/front/unit/?.lua;" .. package.path
+require("commonrequire") -- the spec harness: dummy framebuffer, no SDL window
+local JSON = require("json")
+local InitArray = require("json.util").InitArray -- keep empty lists as [] not {}
+local DocumentRegistry = require("document/documentregistry")
+
+local epub, outpath, every = arg[1], arg[2], tonumber(arg[3] or "1")
+assert(epub and outpath, "usage: xpointer-oracle.lua [every]")
+
+local doc = assert(DocumentRegistry:openDocument(epub), "cannot open " .. epub)
+-- A book KOReader has never seen before gets the latest DOM version
+-- (ReaderRolling:onReadSettings); match that so XPointers are normalized.
+doc:requestDomVersion(doc:getLatestDomVersion())
+doc:render()
+
+local result = {
+ epub = epub:match("([^/]+)$"),
+ dom_version = doc:getLatestDomVersion(),
+ every = every,
+ fragments = InitArray({}),
+}
+
+-- DocFragment[N] is spine item N-1; crengine creates one per . With a
+-- single spine item crengine omits the index and writes /body/DocFragment/...
+local single = not doc:isXPointerInDocument("/body/DocFragment[2]")
+local n = 1
+while doc:isXPointerInDocument("/body/DocFragment[" .. n .. "]") do
+ local prefix = single and "/body/DocFragment/" or ("/body/DocFragment[" .. n .. "]/")
+ local fragment = { index = n - 1, docfragment = n, words = InitArray({}) }
+ local xp = doc:getNextVisibleWordStart(prefix .. "body")
+ local seen = 0
+ while xp and xp:sub(1, #prefix) == prefix do
+ local xp_end = doc:getNextVisibleWordEnd(xp)
+ if xp_end and xp_end:sub(1, #prefix) == prefix then
+ if seen % every == 0 then
+ table.insert(fragment.words, {
+ xp = xp,
+ xp_end = xp_end,
+ text = doc:getTextFromXPointers(xp, xp_end),
+ })
+ end
+ seen = seen + 1
+ end
+ xp = doc:getNextVisibleWordStart(xp)
+ end
+ fragment.word_count = seen
+ table.insert(result.fragments, fragment)
+ n = n + 1
+end
+doc:close()
+
+local out = assert(io.open(outpath, "w"))
+-- luajson escapes "/" as "\/", which is valid JSON but hard to read in fixtures
+out:write((JSON.encode(result):gsub("\\/", "/")))
+out:close()
+print(string.format("wrote %s: %d fragments", outpath, #result.fragments))
diff --git a/packages/foliate-js b/packages/foliate-js
index 8ffd2cffc..cb3896fd5 160000
--- a/packages/foliate-js
+++ b/packages/foliate-js
@@ -1 +1 @@
-Subproject commit 8ffd2cffc36c4be84490c5a1415d11668f37c88f
+Subproject commit cb3896fd52c243f1dc6bfaf09dbf7d96318a4183