dsh-book-reader 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,8 +38,9 @@ export async function getInfo(path)
38
38
  // outline: [{ title, pdf, printed, depth }] }
39
39
 
40
40
  export async function getPageText(path, pdfPage) // -> string
41
- export async function searchPdf(path, query, opts) // opts: {ignoreCase=true, limit=200, contextChars=80}
42
- // -> { hits: [{ pdf, printed, indexOnPage, snippet }], pages: <number of pages with hits> }
41
+ export async function searchPdf(path, query, opts) // opts: {ignoreCase=true, limit=200, contextChars=80, pdfPages?}
42
+ // -> { hits: [{ pdf, printed, indexOnPage, snippet }], pages: <pages with hits>,
43
+ // mode: 'phrase' | 'compact' | 'terms' }
43
44
  export async function closePdf(path) // optional cache eviction -> boolean
44
45
 
45
46
  // lib/core/render.js
@@ -91,26 +92,48 @@ accepted). Invalid pages throw a `RangeError` with
91
92
 
92
93
  ### `searchPdf(path, query, opts)`
93
94
 
94
- Literal, substring search (the query is not a regex). Both the query and each
95
- page's text are whitespace-normalised; `ignoreCase` defaults to `true`.
96
- Matching is non-overlapping, page by page, with an `await` between pages so the
97
- event loop is not held for the whole book.
95
+ Normalized text search (not a regex engine). The query is cleaned first: trimmed,
96
+ a whole-query wrapper pair of `"…"`, `'…'`, `“…”`, `‘…’`, `` `…` `` or `$…$` is
97
+ stripped, and whitespace is collapsed. Both the query and each page's text are
98
+ then NFKC-normalized per code point (math alphanumerics and ligatures fold to
99
+ ASCII), soft hyphens are removed, and all dash variants are deleted (with a
100
+ following space when present), so `com- plex` matches `complex` and
101
+ `finite-dimensional` matches `finite dimensional`.
102
+
103
+ Matching is tried over the whole document in order; the first pass with any hit
104
+ decides the result and `mode`:
105
+
106
+ 1. `phrase` — literal match on the normalized text; `ignoreCase` defaults to
107
+ `true`.
108
+ 2. `compact` — only when no phrase matched: spaces are removed from both sides,
109
+ so `L(V)` finds `ℒ (𝑉)` and `T∈L(V)` finds `𝑆 , 𝑇 ∈ ℒ (𝑉)`.
110
+ 3. `terms` — only when neither matched: every letter/digit token of the query
111
+ (length ≥2, at most 8) must occur somewhere on the page (AND); at most one
112
+ hit per page.
113
+
114
+ Hits rank **body pages first** (numeric `printed`) in page/offset order, then
115
+ front matter (roman labels) in order, with at most 3 front-matter hits while the
116
+ overall `limit` is applied. `pages` counts every matched page in the active pass
117
+ before ranking/limit. `indexOnPage` is a UTF-16 offset into the original
118
+ `getPageText()` text, and snippets are cut from that original text (surrogate
119
+ pairs preserved).
98
120
 
99
- * `hits` are ordered by physical page, then position on the page.
100
- * `printed` is the printed label for that page (`273` for PDF page 293), or a
101
- front-matter string / `null`.
102
- * `indexOnPage` is a UTF-16 offset into the **same normalised text** that
103
- `getPageText()` returns for that page.
104
121
  * `snippet` is at most about `2 * contextChars + query.length` characters, with
105
122
  leading/trailing `…` when the hit is mid-page.
106
- * `limit` caps the number of collected hits (default 200), but the scan
107
- continues to the last page so `pages` always counts **all** pages with at
108
- least one hit. LADR's `"singular value decomposition"`: 56 hits on 28 pages,
109
- including PDF 293 / printed 273.
123
+ * `limit` caps the returned hits (default 200); the scan continues to the last
124
+ page so `pages` is accurate.
125
+ * `pdfPages` (optional) is an array of 1-based **physical** pages to scan; the
126
+ tool maps printed page ranges to it. `pages` then counts matches inside that
127
+ range only. Out-of-document pages throw a `RangeError` with
128
+ `code = 'PDF_PAGE_OUT_OF_RANGE'`.
110
129
  * `contextChars` defaults to 80.
130
+ * The analysis above is one `await` per page, so the event loop stays responsive.
131
+ A 410-page phrase search takes roughly 0.8–1.1 s; a query that also runs the
132
+ fallbacks stays around the same order.
111
133
 
112
- An empty/whitespace-only query throws `TypeError`. Invalid `limit`/`contextChars`
113
- throw `RangeError`.
134
+ An empty query after cleaning (including quotes or `---`) throws `TypeError`;
135
+ queries longer than 1000 characters throw as well. Invalid
136
+ `limit`/`contextChars`/`pdfPages` throw `RangeError`.
114
137
 
115
138
  ### `closePdf(path)`
116
139
 
@@ -193,7 +216,7 @@ particular ranged from ~0.67 s to ~1.18 s across runs).
193
216
  | First PDF call if only `pdf.js` was imported and neither canvas nor pdf.js was loaded yet | **~156 ms** |
194
217
  | Warm `getInfo` | **~0.05 ms** median |
195
218
  | `getPageText(344)` | **~28-50 ms** |
196
- | `searchPdf` over all 410 pages (`"singular value decomposition"`) | **~0.8 s** median; 0.67-1.18 s observed |
219
+ | `searchPdf` over all 410 pages (`"singular value decomposition"`) | **~1.0 s** median; 0.83–1.1 s observed (phrase pass) |
197
220
  | `renderPageToPng(344, scale 2)` | **~26 ms** median warm (22-43 ms) |
198
221
  | `renderPagesToPng([344,345,346], scale 2)` | **~66 ms** median (63-77 ms) |
199
222
  | Cropped render (`0,0,1,0.55`, scale 2) | **~18-27 ms** |
@@ -247,10 +270,12 @@ in one session. Four cached books plus a long render session should stay in the
247
270
  7. **Scale and crop are validated, not clamped.** `scale` outside 1..3 throws;
248
271
  crop fractions outside the page throw. Validate in the tool schema too so
249
272
  the model gets a useful message.
250
- 8. **Search is literal, not regex, and does not unfold hyphenation.** A word
251
- split as `decom-\nposition` will not match `decomposition`. Whitespace and
252
- case are normalized only. Search scans every page even after `limit` is hit
253
- so `pages` is accurate; expect ~0.8 s for a 410-page book.
273
+ 8. **Search is normalized text matching, not regex.** Quotes/`$…$` are stripped,
274
+ NFKC folds math alphanumerics and ligatures, dashes are dropped (so line-break
275
+ hyphenation matches), then a space-insensitive pass and a keyword-AND pass are
276
+ tried only when the literal phrase has no hits (`mode` reports which one won).
277
+ Body pages rank before front matter and front-matter hits are capped at 3, so
278
+ `pages` can exceed `hits.length`. Expect ~1 s for a 410-page book.
254
279
  9. **Page labels come from the PDF, not from visible text.** Some books have
255
280
  duplicate/odd roman labels (LADR's PDF page 4 is labelled `ii` by pdf.js).
256
281
  Only numeric labels drive `printedOffset`; numeric deltas are taken by mode.