wolfy-reader 0.1.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.
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/dist/core/book.d.ts +75 -0
- package/dist/core/book.d.ts.map +1 -0
- package/dist/core/book.js +2 -0
- package/dist/core/book.js.map +1 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +29 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.d.ts +13 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +2 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/graphemes.d.ts +36 -0
- package/dist/core/graphemes.d.ts.map +1 -0
- package/dist/core/graphemes.js +62 -0
- package/dist/core/graphemes.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lookup.d.ts +10 -0
- package/dist/core/lookup.d.ts.map +1 -0
- package/dist/core/lookup.js +16 -0
- package/dist/core/lookup.js.map +1 -0
- package/dist/core/open.d.ts +12 -0
- package/dist/core/open.d.ts.map +1 -0
- package/dist/core/open.js +23 -0
- package/dist/core/open.js.map +1 -0
- package/dist/core/position.d.ts +121 -0
- package/dist/core/position.d.ts.map +1 -0
- package/dist/core/position.js +327 -0
- package/dist/core/position.js.map +1 -0
- package/dist/core/reading-text.d.ts +77 -0
- package/dist/core/reading-text.d.ts.map +1 -0
- package/dist/core/reading-text.js +144 -0
- package/dist/core/reading-text.js.map +1 -0
- package/dist/core/source.d.ts +17 -0
- package/dist/core/source.d.ts.map +1 -0
- package/dist/core/source.js +39 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/storage.d.ts +7 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +2 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/text.d.ts +18 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +32 -0
- package/dist/core/text.js.map +1 -0
- package/dist/formats/epub/href.d.ts +14 -0
- package/dist/formats/epub/href.d.ts.map +1 -0
- package/dist/formats/epub/href.js +52 -0
- package/dist/formats/epub/href.js.map +1 -0
- package/dist/formats/epub/index.d.ts +3 -0
- package/dist/formats/epub/index.d.ts.map +1 -0
- package/dist/formats/epub/index.js +223 -0
- package/dist/formats/epub/index.js.map +1 -0
- package/dist/formats/epub/opf.d.ts +27 -0
- package/dist/formats/epub/opf.d.ts.map +1 -0
- package/dist/formats/epub/opf.js +110 -0
- package/dist/formats/epub/opf.js.map +1 -0
- package/dist/formats/epub/toc.d.ts +6 -0
- package/dist/formats/epub/toc.d.ts.map +1 -0
- package/dist/formats/epub/toc.js +88 -0
- package/dist/formats/epub/toc.js.map +1 -0
- package/dist/formats/fb2/index.d.ts +13 -0
- package/dist/formats/fb2/index.d.ts.map +1 -0
- package/dist/formats/fb2/index.js +317 -0
- package/dist/formats/fb2/index.js.map +1 -0
- package/dist/formats/index.d.ts +4 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/index.js +4 -0
- package/dist/formats/index.js.map +1 -0
- package/dist/formats/text/index.d.ts +11 -0
- package/dist/formats/text/index.d.ts.map +1 -0
- package/dist/formats/text/index.js +201 -0
- package/dist/formats/text/index.js.map +1 -0
- package/dist/formats/xml.d.ts +33 -0
- package/dist/formats/xml.d.ts.map +1 -0
- package/dist/formats/xml.js +321 -0
- package/dist/formats/xml.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/chunk.d.ts +51 -0
- package/dist/layout/chunk.d.ts.map +1 -0
- package/dist/layout/chunk.js +131 -0
- package/dist/layout/chunk.js.map +1 -0
- package/dist/layout/index.d.ts +238 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +476 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/reader/index.d.ts +231 -0
- package/dist/reader/index.d.ts.map +1 -0
- package/dist/reader/index.js +958 -0
- package/dist/reader/index.js.map +1 -0
- package/dist/reader/input.d.ts +69 -0
- package/dist/reader/input.d.ts.map +1 -0
- package/dist/reader/input.js +75 -0
- package/dist/reader/input.js.map +1 -0
- package/dist/search/extract.d.ts +51 -0
- package/dist/search/extract.d.ts.map +1 -0
- package/dist/search/extract.js +401 -0
- package/dist/search/extract.js.map +1 -0
- package/dist/search/index.d.ts +11 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +11 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/matcher.d.ts +56 -0
- package/dist/search/matcher.d.ts.map +1 -0
- package/dist/search/matcher.js +128 -0
- package/dist/search/matcher.js.map +1 -0
- package/dist/search/normalize.d.ts +48 -0
- package/dist/search/normalize.d.ts.map +1 -0
- package/dist/search/normalize.js +103 -0
- package/dist/search/normalize.js.map +1 -0
- package/dist/view/allowlist.d.ts +59 -0
- package/dist/view/allowlist.d.ts.map +1 -0
- package/dist/view/allowlist.js +198 -0
- package/dist/view/allowlist.js.map +1 -0
- package/dist/view/appearance.d.ts +137 -0
- package/dist/view/appearance.d.ts.map +1 -0
- package/dist/view/appearance.js +274 -0
- package/dist/view/appearance.js.map +1 -0
- package/dist/view/css.d.ts +24 -0
- package/dist/view/css.d.ts.map +1 -0
- package/dist/view/css.js +153 -0
- package/dist/view/css.js.map +1 -0
- package/dist/view/frame.d.ts +54 -0
- package/dist/view/frame.d.ts.map +1 -0
- package/dist/view/frame.js +980 -0
- package/dist/view/frame.js.map +1 -0
- package/dist/view/host.d.ts +160 -0
- package/dist/view/host.d.ts.map +1 -0
- package/dist/view/host.js +385 -0
- package/dist/view/host.js.map +1 -0
- package/dist/view/index.d.ts +12 -0
- package/dist/view/index.d.ts.map +1 -0
- package/dist/view/index.js +7 -0
- package/dist/view/index.js.map +1 -0
- package/dist/view/protocol.d.ts +220 -0
- package/dist/view/protocol.d.ts.map +1 -0
- package/dist/view/protocol.js +246 -0
- package/dist/view/protocol.js.map +1 -0
- package/dist/view/reference.d.ts +18 -0
- package/dist/view/reference.d.ts.map +1 -0
- package/dist/view/reference.js +25 -0
- package/dist/view/reference.js.map +1 -0
- package/dist/view/resources.d.ts +51 -0
- package/dist/view/resources.d.ts.map +1 -0
- package/dist/view/resources.js +242 -0
- package/dist/view/resources.js.map +1 -0
- package/dist/view/sanitize.d.ts +32 -0
- package/dist/view/sanitize.d.ts.map +1 -0
- package/dist/view/sanitize.js +226 -0
- package/dist/view/sanitize.js.map +1 -0
- package/dist/zip/central.d.ts +12 -0
- package/dist/zip/central.d.ts.map +1 -0
- package/dist/zip/central.js +72 -0
- package/dist/zip/central.js.map +1 -0
- package/dist/zip/crc32.d.ts +3 -0
- package/dist/zip/crc32.d.ts.map +1 -0
- package/dist/zip/crc32.js +28 -0
- package/dist/zip/crc32.js.map +1 -0
- package/dist/zip/eocd.d.ts +9 -0
- package/dist/zip/eocd.d.ts.map +1 -0
- package/dist/zip/eocd.js +84 -0
- package/dist/zip/eocd.js.map +1 -0
- package/dist/zip/errors.d.ts +19 -0
- package/dist/zip/errors.d.ts.map +1 -0
- package/dist/zip/errors.js +37 -0
- package/dist/zip/errors.js.map +1 -0
- package/dist/zip/index.d.ts +18 -0
- package/dist/zip/index.d.ts.map +1 -0
- package/dist/zip/index.js +78 -0
- package/dist/zip/index.js.map +1 -0
- package/dist/zip/inflate.d.ts +2 -0
- package/dist/zip/inflate.d.ts.map +1 -0
- package/dist/zip/inflate.js +43 -0
- package/dist/zip/inflate.js.map +1 -0
- package/dist/zip/source.d.ts +15 -0
- package/dist/zip/source.d.ts.map +1 -0
- package/dist/zip/source.js +34 -0
- package/dist/zip/source.js.map +1 -0
- package/package.json +75 -0
- package/src/core/book.ts +80 -0
- package/src/core/errors.ts +31 -0
- package/src/core/format.ts +14 -0
- package/src/core/graphemes.ts +73 -0
- package/src/core/index.ts +27 -0
- package/src/core/lookup.ts +16 -0
- package/src/core/open.ts +31 -0
- package/src/core/position.ts +485 -0
- package/src/core/reading-text.ts +200 -0
- package/src/core/source.ts +61 -0
- package/src/core/storage.ts +6 -0
- package/src/core/text.ts +35 -0
- package/src/formats/epub/href.ts +54 -0
- package/src/formats/epub/index.ts +235 -0
- package/src/formats/epub/opf.ts +143 -0
- package/src/formats/epub/toc.ts +120 -0
- package/src/formats/fb2/index.ts +348 -0
- package/src/formats/index.ts +3 -0
- package/src/formats/text/index.ts +208 -0
- package/src/formats/xml.ts +326 -0
- package/src/index.ts +26 -0
- package/src/layout/chunk.ts +169 -0
- package/src/layout/index.ts +573 -0
- package/src/reader/index.ts +1206 -0
- package/src/reader/input.ts +122 -0
- package/src/search/extract.ts +403 -0
- package/src/search/index.ts +10 -0
- package/src/search/matcher.ts +187 -0
- package/src/search/normalize.ts +122 -0
- package/src/view/allowlist.ts +273 -0
- package/src/view/appearance.ts +380 -0
- package/src/view/css.ts +172 -0
- package/src/view/frame.ts +1018 -0
- package/src/view/host.ts +527 -0
- package/src/view/index.ts +24 -0
- package/src/view/protocol.ts +412 -0
- package/src/view/reference.ts +30 -0
- package/src/view/resources.ts +265 -0
- package/src/view/sanitize.ts +266 -0
- package/src/zip/central.ts +85 -0
- package/src/zip/crc32.ts +30 -0
- package/src/zip/eocd.ts +97 -0
- package/src/zip/errors.ts +41 -0
- package/src/zip/index.ts +114 -0
- package/src/zip/inflate.ts +41 -0
- package/src/zip/source.ts +51 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-section text extraction for search. Decodes a section's bytes and strips its
|
|
3
|
+
* markup to a single plain string — the **canonical reading text**, the same text
|
|
4
|
+
* the frame ultimately shows and measures, so a match's captured anchor (quote +
|
|
5
|
+
* context) exists verbatim in the text `goTo` resolves against.
|
|
6
|
+
*
|
|
7
|
+
* Headless by rule: this imports only `src/core` (the shared reading-text policy)
|
|
8
|
+
* and platform primitives (`TextDecoder`) and never touches `document`/`window`, so
|
|
9
|
+
* the whole search stack runs under `node:test`. It deliberately does **not** route
|
|
10
|
+
* through the frame's `Paginator.sectionText()` — that returns only the
|
|
11
|
+
* currently-paginated section and needs a browser. Search scans every section
|
|
12
|
+
* without painting it.
|
|
13
|
+
*
|
|
14
|
+
* Extraction is a small tag-stripping tokenizer rather than `DOMParser`: `DOMParser`
|
|
15
|
+
* is a browser primitive with no headless Node global, and search only needs text
|
|
16
|
+
* content. To line up with the frame it mirrors what the sanitize + resource
|
|
17
|
+
* pipeline does to a section's text, via the one shared policy in
|
|
18
|
+
* `src/core/reading-text.ts`:
|
|
19
|
+
*
|
|
20
|
+
* - Inline markup contributes no characters (`wo<em>rd</em>` is found as `word`) —
|
|
21
|
+
* `textContent` semantics, and unwrapped elements keep their text.
|
|
22
|
+
* - Elements the sanitizer **discards whole** (form controls, `template`, raw-text
|
|
23
|
+
* containers — `DISCARDED_HTML_ELEMENTS` / `DISCARDED_SVG_ELEMENTS`) contribute
|
|
24
|
+
* nothing, exactly as their text never reaches the frame.
|
|
25
|
+
* - An `<img>` contributes what the resource layer will show: nothing when it can
|
|
26
|
+
* be served (or is left for the CSP), its `alt` when the frame substitutes alt
|
|
27
|
+
* text (`imageReadingText`) — which is why extraction takes the section's
|
|
28
|
+
* `resolve` seam.
|
|
29
|
+
* - Whitespace-only text runs at the top level of the body are dropped, because
|
|
30
|
+
* the chunker drops those text nodes before the frame tiles the section text
|
|
31
|
+
* (`chunkElement` in `src/layout/chunk.ts`).
|
|
32
|
+
* - `head`, `script` and `style` content is never reading text (`<style>` survives
|
|
33
|
+
* sanitization, but its CSS is source, not prose — searching it would be noise;
|
|
34
|
+
* see the known-edges note in docs/domains/search.md).
|
|
35
|
+
*/
|
|
36
|
+
import { DISCARDED_HTML_ELEMENTS, DISCARDED_SVG_ELEMENTS, imageReadingText, } from '../core/reading-text.js';
|
|
37
|
+
import { decodeText } from '../core/text.js';
|
|
38
|
+
/** Decodes section bytes honouring the byte-order marks EPUB allows. */
|
|
39
|
+
export { decodeText as decodeSectionBytes } from '../core/text.js';
|
|
40
|
+
/**
|
|
41
|
+
* Elements whose content the HTML parser reads as raw text (a `<` in `1<2` inside
|
|
42
|
+
* `<script>` is not a tag): skipping jumps to the first matching end tag rather
|
|
43
|
+
* than tracking nesting. All but `style` are also discarded by the sanitizer;
|
|
44
|
+
* `style` survives into the frame but its text is source, not prose.
|
|
45
|
+
*/
|
|
46
|
+
const RAW_TEXT = new Set([
|
|
47
|
+
'iframe',
|
|
48
|
+
'noembed',
|
|
49
|
+
'noframes',
|
|
50
|
+
'plaintext',
|
|
51
|
+
'script',
|
|
52
|
+
'style',
|
|
53
|
+
'textarea',
|
|
54
|
+
'title',
|
|
55
|
+
'xmp',
|
|
56
|
+
]);
|
|
57
|
+
/** HTML void elements: no content, no end tag, never on the open-element stack. */
|
|
58
|
+
const VOID = new Set([
|
|
59
|
+
'area',
|
|
60
|
+
'base',
|
|
61
|
+
'br',
|
|
62
|
+
'col',
|
|
63
|
+
'embed',
|
|
64
|
+
'hr',
|
|
65
|
+
'img',
|
|
66
|
+
'input',
|
|
67
|
+
'link',
|
|
68
|
+
'meta',
|
|
69
|
+
'param',
|
|
70
|
+
'source',
|
|
71
|
+
'track',
|
|
72
|
+
'wbr',
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
75
|
+
* Block-level tags whose start tag implies `</p>` in HTML parsing. Without this,
|
|
76
|
+
* the unclosed `<p>` real books ship would keep the stack forever nested and the
|
|
77
|
+
* top-level whitespace rule would never apply again.
|
|
78
|
+
*/
|
|
79
|
+
const P_CLOSERS = new Set([
|
|
80
|
+
'address',
|
|
81
|
+
'article',
|
|
82
|
+
'aside',
|
|
83
|
+
'blockquote',
|
|
84
|
+
'details',
|
|
85
|
+
'div',
|
|
86
|
+
'dl',
|
|
87
|
+
'fieldset',
|
|
88
|
+
'figcaption',
|
|
89
|
+
'figure',
|
|
90
|
+
'footer',
|
|
91
|
+
'form',
|
|
92
|
+
'h1',
|
|
93
|
+
'h2',
|
|
94
|
+
'h3',
|
|
95
|
+
'h4',
|
|
96
|
+
'h5',
|
|
97
|
+
'h6',
|
|
98
|
+
'header',
|
|
99
|
+
'hgroup',
|
|
100
|
+
'hr',
|
|
101
|
+
'main',
|
|
102
|
+
'menu',
|
|
103
|
+
'nav',
|
|
104
|
+
'ol',
|
|
105
|
+
'p',
|
|
106
|
+
'pre',
|
|
107
|
+
'section',
|
|
108
|
+
'table',
|
|
109
|
+
'ul',
|
|
110
|
+
]);
|
|
111
|
+
/**
|
|
112
|
+
* Extracts a section's reading text from its decoded markup — the headless mirror
|
|
113
|
+
* of what the sanitize + resource pipeline leaves for the frame to show (see the
|
|
114
|
+
* module comment for the exact rules). `resolve` is the section's own resolver
|
|
115
|
+
* seam (`Section.resolve`), consulted only to decide whether an `<img>` renders
|
|
116
|
+
* or falls back to its alt text; omit it and every in-book image degrades to alt,
|
|
117
|
+
* matching a section that carries no resolver.
|
|
118
|
+
*/
|
|
119
|
+
export function extractText(markup, resolve) {
|
|
120
|
+
let out = '';
|
|
121
|
+
let index = 0;
|
|
122
|
+
/** Open non-void elements. `head` and skipped containers never land here. */
|
|
123
|
+
const stack = [];
|
|
124
|
+
let svgDepth = 0;
|
|
125
|
+
// The chunker drops whitespace-only text nodes at the top level of the body
|
|
126
|
+
// (they carry no block box), so the frame's tiled text has no inter-paragraph
|
|
127
|
+
// source whitespace. Mirror it: a whitespace-only run emitted while no element
|
|
128
|
+
// (or only html/body) is open contributes nothing.
|
|
129
|
+
const atTopLevel = () => {
|
|
130
|
+
const top = stack[stack.length - 1];
|
|
131
|
+
return top === undefined || top === 'html' || top === 'body';
|
|
132
|
+
};
|
|
133
|
+
const emit = (text) => {
|
|
134
|
+
if (text === '')
|
|
135
|
+
return;
|
|
136
|
+
if (atTopLevel() && text.trim().length === 0)
|
|
137
|
+
return;
|
|
138
|
+
out += text;
|
|
139
|
+
};
|
|
140
|
+
const popTo = (name) => {
|
|
141
|
+
const at = stack.lastIndexOf(name);
|
|
142
|
+
if (at === -1)
|
|
143
|
+
return;
|
|
144
|
+
for (let i = stack.length - 1; i >= at; i -= 1) {
|
|
145
|
+
if (stack[i] === 'svg')
|
|
146
|
+
svgDepth -= 1;
|
|
147
|
+
}
|
|
148
|
+
stack.length = at;
|
|
149
|
+
};
|
|
150
|
+
const length = markup.length;
|
|
151
|
+
while (index < length) {
|
|
152
|
+
const lt = markup.indexOf('<', index);
|
|
153
|
+
if (lt === -1) {
|
|
154
|
+
emit(decodeEntities(markup.slice(index)));
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (lt > index)
|
|
158
|
+
emit(decodeEntities(markup.slice(index, lt)));
|
|
159
|
+
// Comment / CDATA / processing instruction / doctype: skip to its close.
|
|
160
|
+
if (markup.startsWith('<!--', lt)) {
|
|
161
|
+
const close = markup.indexOf('-->', lt + 4);
|
|
162
|
+
index = close === -1 ? length : close + 3;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (markup.startsWith('<![CDATA[', lt)) {
|
|
166
|
+
const close = markup.indexOf(']]>', lt + 9);
|
|
167
|
+
emit(markup.slice(lt + 9, close === -1 ? length : close)); // literal text, no entities
|
|
168
|
+
index = close === -1 ? length : close + 3;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (markup[lt + 1] === '!' || markup[lt + 1] === '?') {
|
|
172
|
+
const close = markup.indexOf('>', lt + 1);
|
|
173
|
+
index = close === -1 ? length : close + 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const gt = markup.indexOf('>', lt);
|
|
177
|
+
if (gt === -1) {
|
|
178
|
+
// A stray '<' with no closing '>': treat the rest as text.
|
|
179
|
+
emit(decodeEntities(markup.slice(lt)));
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
const rawTag = markup.slice(lt + 1, gt);
|
|
183
|
+
index = gt + 1;
|
|
184
|
+
const isClose = rawTag[0] === '/';
|
|
185
|
+
const name = tagName(isClose ? rawTag.slice(1) : rawTag);
|
|
186
|
+
if (name === '')
|
|
187
|
+
continue;
|
|
188
|
+
if (isClose) {
|
|
189
|
+
popTo(name);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const selfClosing = rawTag.endsWith('/');
|
|
193
|
+
const inSvg = svgDepth > 0;
|
|
194
|
+
if (!inSvg) {
|
|
195
|
+
// HTML implied end tags, minimally: a block start tag closes an open <p>,
|
|
196
|
+
// and a list/definition item closes its open sibling. Real books rely on
|
|
197
|
+
// both, and the frame's HTML parse applies them before chunking.
|
|
198
|
+
const top = stack[stack.length - 1];
|
|
199
|
+
if (top === 'p' && P_CLOSERS.has(name))
|
|
200
|
+
stack.pop();
|
|
201
|
+
else if (name === 'li' && top === 'li')
|
|
202
|
+
stack.pop();
|
|
203
|
+
else if ((name === 'dd' || name === 'dt') && (top === 'dd' || top === 'dt'))
|
|
204
|
+
stack.pop();
|
|
205
|
+
}
|
|
206
|
+
// The head never reaches the frame body (only its <link>/<style> move, and
|
|
207
|
+
// those carry no reading text), so its whole subtree is skipped.
|
|
208
|
+
if (!inSvg && name === 'head' && !selfClosing) {
|
|
209
|
+
index = skipBalanced(markup, name, index);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// An <img> renders as an image (zero characters) or becomes its alt text —
|
|
213
|
+
// the one place extraction *adds* text, mirroring the resource layer.
|
|
214
|
+
if (!inSvg && name === 'img') {
|
|
215
|
+
emit(imageReadingText(attributeOf(rawTag, 'src'), attributeOf(rawTag, 'alt'), resolve));
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Discarded-whole elements contribute nothing, exactly like the sanitizer;
|
|
219
|
+
// raw-text containers jump to their first matching end tag, everything else
|
|
220
|
+
// to the end tag balancing same-name nesting.
|
|
221
|
+
const discarded = inSvg ? DISCARDED_SVG_ELEMENTS : DISCARDED_HTML_ELEMENTS;
|
|
222
|
+
if (discarded.has(name) || name === 'style') {
|
|
223
|
+
if (selfClosing || (!inSvg && VOID.has(name)))
|
|
224
|
+
continue;
|
|
225
|
+
index = RAW_TEXT.has(name) ? findEndTag(markup, name, index) : skipBalanced(markup, name, index);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (selfClosing || (!inSvg && VOID.has(name)))
|
|
229
|
+
continue;
|
|
230
|
+
if (name === 'svg')
|
|
231
|
+
svgDepth += 1;
|
|
232
|
+
stack.push(name);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/** Decode + extract a section from its raw bytes. */
|
|
237
|
+
export function extractSectionText(bytes, resolve) {
|
|
238
|
+
return extractText(decodeText(bytes), resolve);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Index just past the matching `</name>` end tag at or after `from` (raw-text
|
|
242
|
+
* containers: the first close wins, nothing inside is a tag), or the markup
|
|
243
|
+
* length when the container is never closed. Case-insensitive on the tag name.
|
|
244
|
+
*/
|
|
245
|
+
function findEndTag(markup, name, from) {
|
|
246
|
+
const lower = markup.toLowerCase();
|
|
247
|
+
const needle = `</${name}`;
|
|
248
|
+
let at = from;
|
|
249
|
+
for (;;) {
|
|
250
|
+
const found = lower.indexOf(needle, at);
|
|
251
|
+
if (found === -1)
|
|
252
|
+
return markup.length;
|
|
253
|
+
const after = markup[found + needle.length];
|
|
254
|
+
// The next char must end the tag name (whitespace or '>'), so </scripting>
|
|
255
|
+
// does not close <script>.
|
|
256
|
+
if (after === undefined || after === '>' || /\s/.test(after)) {
|
|
257
|
+
const gt = markup.indexOf('>', found);
|
|
258
|
+
return gt === -1 ? markup.length : gt + 1;
|
|
259
|
+
}
|
|
260
|
+
at = found + needle.length;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Index just past the end tag that balances an already-open `name` element,
|
|
265
|
+
* counting same-name nesting (a discarded `<select>` may hold another), or the
|
|
266
|
+
* markup length when it never closes.
|
|
267
|
+
*/
|
|
268
|
+
function skipBalanced(markup, name, from) {
|
|
269
|
+
const lower = markup.toLowerCase();
|
|
270
|
+
const open = `<${name}`;
|
|
271
|
+
const close = `</${name}`;
|
|
272
|
+
let depth = 1;
|
|
273
|
+
let at = from;
|
|
274
|
+
while (depth > 0) {
|
|
275
|
+
const lt = lower.indexOf('<', at);
|
|
276
|
+
if (lt === -1)
|
|
277
|
+
return markup.length;
|
|
278
|
+
const gt = markup.indexOf('>', lt);
|
|
279
|
+
if (gt === -1)
|
|
280
|
+
return markup.length;
|
|
281
|
+
if (lower.startsWith(close, lt) && endsTagName(markup, lt + close.length)) {
|
|
282
|
+
depth -= 1;
|
|
283
|
+
}
|
|
284
|
+
else if (lower.startsWith(open, lt) && endsTagName(markup, lt + open.length)) {
|
|
285
|
+
if (markup[gt - 1] !== '/')
|
|
286
|
+
depth += 1; // a self-closing one opens nothing
|
|
287
|
+
}
|
|
288
|
+
at = gt + 1;
|
|
289
|
+
}
|
|
290
|
+
return at;
|
|
291
|
+
}
|
|
292
|
+
/** True when the character at `i` terminates a tag name (`>`, `/`, whitespace, EOF). */
|
|
293
|
+
function endsTagName(markup, i) {
|
|
294
|
+
const char = markup[i];
|
|
295
|
+
return char === undefined || char === '>' || char === '/' || /\s/.test(char);
|
|
296
|
+
}
|
|
297
|
+
/** The lowercased element name from a raw tag body ('em class="x"' → 'em'). */
|
|
298
|
+
function tagName(body) {
|
|
299
|
+
let end = 0;
|
|
300
|
+
while (end < body.length && !/[\s/>]/.test(body[end]))
|
|
301
|
+
end += 1;
|
|
302
|
+
return body.slice(0, end).toLowerCase();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The decoded value of `wanted` in a raw tag body, or `null` when absent.
|
|
306
|
+
* Handles single/double-quoted and unquoted values and a bare attribute name
|
|
307
|
+
* (which the DOM reads as the empty string). Entities in the value are decoded,
|
|
308
|
+
* as an attribute parse would.
|
|
309
|
+
*/
|
|
310
|
+
function attributeOf(rawTag, wanted) {
|
|
311
|
+
let i = 0;
|
|
312
|
+
while (i < rawTag.length && !/[\s/]/.test(rawTag[i]))
|
|
313
|
+
i += 1; // skip the tag name
|
|
314
|
+
while (i < rawTag.length) {
|
|
315
|
+
while (i < rawTag.length && /[\s/]/.test(rawTag[i]))
|
|
316
|
+
i += 1;
|
|
317
|
+
if (i >= rawTag.length)
|
|
318
|
+
break;
|
|
319
|
+
let start = i;
|
|
320
|
+
while (i < rawTag.length && !/[\s=/]/.test(rawTag[i]))
|
|
321
|
+
i += 1;
|
|
322
|
+
const attribute = rawTag.slice(start, i).toLowerCase();
|
|
323
|
+
while (i < rawTag.length && /\s/.test(rawTag[i]))
|
|
324
|
+
i += 1;
|
|
325
|
+
let value = '';
|
|
326
|
+
if (rawTag[i] === '=') {
|
|
327
|
+
i += 1;
|
|
328
|
+
while (i < rawTag.length && /\s/.test(rawTag[i]))
|
|
329
|
+
i += 1;
|
|
330
|
+
const quote = rawTag[i];
|
|
331
|
+
if (quote === '"' || quote === "'") {
|
|
332
|
+
i += 1;
|
|
333
|
+
start = i;
|
|
334
|
+
while (i < rawTag.length && rawTag[i] !== quote)
|
|
335
|
+
i += 1;
|
|
336
|
+
value = rawTag.slice(start, i);
|
|
337
|
+
i += 1;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
start = i;
|
|
341
|
+
while (i < rawTag.length && !/\s/.test(rawTag[i]))
|
|
342
|
+
i += 1;
|
|
343
|
+
value = rawTag.slice(start, i);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (attribute === wanted)
|
|
347
|
+
return decodeEntities(value);
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const NAMED = new Map([
|
|
352
|
+
['amp', '&'],
|
|
353
|
+
['lt', '<'],
|
|
354
|
+
['gt', '>'],
|
|
355
|
+
['quot', '"'],
|
|
356
|
+
['apos', "'"],
|
|
357
|
+
['nbsp', ' '],
|
|
358
|
+
['mdash', '—'],
|
|
359
|
+
['ndash', '–'],
|
|
360
|
+
['hellip', '…'],
|
|
361
|
+
['lsquo', '‘'],
|
|
362
|
+
['rsquo', '’'],
|
|
363
|
+
['ldquo', '“'],
|
|
364
|
+
['rdquo', '”'],
|
|
365
|
+
['copy', '©'],
|
|
366
|
+
['reg', '®'],
|
|
367
|
+
['trade', '™'],
|
|
368
|
+
['deg', '°'],
|
|
369
|
+
['times', '×'],
|
|
370
|
+
['eacute', 'é'],
|
|
371
|
+
['egrave', 'è'],
|
|
372
|
+
['agrave', 'à'],
|
|
373
|
+
['ccedil', 'ç'],
|
|
374
|
+
['uuml', 'ü'],
|
|
375
|
+
[' ', ' '],
|
|
376
|
+
]);
|
|
377
|
+
/**
|
|
378
|
+
* Decodes numeric (`é`, `é`) and the common named HTML entities. An
|
|
379
|
+
* unknown entity is left verbatim — a book's literal `&` in prose stays `&`.
|
|
380
|
+
*/
|
|
381
|
+
function decodeEntities(text) {
|
|
382
|
+
if (!text.includes('&'))
|
|
383
|
+
return text;
|
|
384
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (whole, body) => {
|
|
385
|
+
if (body[0] === '#') {
|
|
386
|
+
const code = body[1] === 'x' || body[1] === 'X'
|
|
387
|
+
? Number.parseInt(body.slice(2), 16)
|
|
388
|
+
: Number.parseInt(body.slice(1), 10);
|
|
389
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff)
|
|
390
|
+
return whole;
|
|
391
|
+
try {
|
|
392
|
+
return String.fromCodePoint(code);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return whole;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return NAMED.get(body.toLowerCase()) ?? whole;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,gBAAgB,GAEjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C,wEAAwE;AACxE,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC;IACvB,QAAQ;IACR,SAAS;IACT,UAAU;IACV,WAAW;IACX,QAAQ;IACR,OAAO;IACP,UAAU;IACV,OAAO;IACP,KAAK;CACN,CAAC,CAAC;AAEH,mFAAmF;AACnF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC;IACnB,MAAM;IACN,MAAM;IACN,IAAI;IACJ,KAAK;IACL,OAAO;IACP,IAAI;IACJ,KAAK;IACL,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;IACP,KAAK;CACN,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,SAAS;IACT,SAAS;IACT,OAAO;IACP,YAAY;IACZ,SAAS;IACT,KAAK;IACL,IAAI;IACJ,UAAU;IACV,YAAY;IACZ,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,QAAQ;IACR,QAAQ;IACR,IAAI;IACJ,MAAM;IACN,MAAM;IACN,KAAK;IACL,IAAI;IACJ,GAAG;IACH,KAAK;IACL,SAAS;IACT,OAAO;IACP,IAAI;CACL,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,OAAyB;IACnE,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,6EAA6E;IAC7E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,mDAAmD;IACnD,MAAM,UAAU,GAAG,GAAY,EAAE;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACpC,OAAO,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,CAAC;IAC/D,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAQ,EAAE;QAClC,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO;QACxB,IAAI,UAAU,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrD,GAAG,IAAI,IAAI,CAAC;IACd,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,IAAY,EAAQ,EAAE;QACnC,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO;QACtB,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/C,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;gBAAE,QAAQ,IAAI,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC;IACpB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,OAAO,KAAK,GAAG,MAAM,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACd,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM;QACR,CAAC;QACD,IAAI,EAAE,GAAG,KAAK;YAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAE9D,yEAAyE;QACzE,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YAC5C,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QACD,IAAI,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,4BAA4B;YACvF,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QACD,IAAI,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YAC1C,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACd,2DAA2D;YAC3D,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QACxC,KAAK,GAAG,EAAE,GAAG,CAAC,CAAC;QACf,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,IAAI,KAAK,EAAE;YAAE,SAAS;QAE1B,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,CAAC;YACZ,SAAS;QACX,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,QAAQ,GAAG,CAAC,CAAC;QAE3B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,0EAA0E;YAC1E,yEAAyE;YACzE,iEAAiE;YACjE,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACpC,IAAI,GAAG,KAAK,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,KAAK,CAAC,GAAG,EAAE,CAAC;iBAC/C,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI;gBAAE,KAAK,CAAC,GAAG,EAAE,CAAC;iBAC/C,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC;gBAAE,KAAK,CAAC,GAAG,EAAE,CAAC;QAC3F,CAAC;QAED,2EAA2E;QAC3E,iEAAiE;QACjE,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YAC9C,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,2EAA2E;QAC3E,sEAAsE;QACtE,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;YACxF,SAAS;QACX,CAAC;QAED,2EAA2E;QAC3E,4EAA4E;QAC5E,8CAA8C;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,uBAAuB,CAAC;QAC3E,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5C,IAAI,WAAW,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAAE,SAAS;YACxD,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACjG,SAAS;QACX,CAAC;QAED,IAAI,WAAW,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QACxD,IAAI,IAAI,KAAK,KAAK;YAAE,QAAQ,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,kBAAkB,CAAC,KAAiB,EAAE,OAAyB;IAC7E,OAAO,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,MAAc,EAAE,IAAY,EAAE,IAAY;IAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE,CAAC;IAC3B,IAAI,EAAE,GAAG,IAAI,CAAC;IACd,SAAS,CAAC;QACR,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,OAAO,MAAM,CAAC,MAAM,CAAC;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,2EAA2E;QAC3E,2BAA2B;QAC3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7D,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACtC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5C,CAAC;QACD,EAAE,GAAG,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,MAAc,EAAE,IAAY,EAAE,IAAY;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE,CAAC;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,EAAE,GAAG,IAAI,CAAC;IACd,OAAO,KAAK,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAClC,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO,MAAM,CAAC,MAAM,CAAC;QACpC,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO,MAAM,CAAC,MAAM,CAAC;QACpC,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1E,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/E,IAAI,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG;gBAAE,KAAK,IAAI,CAAC,CAAC,CAAC,mCAAmC;QAC7E,CAAC;QACD,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IACd,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,wFAAwF;AACxF,SAAS,WAAW,CAAC,MAAc,EAAE,CAAS;IAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACvB,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC;AAED,+EAA+E;AAC/E,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAE,CAAC;QAAE,GAAG,IAAI,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,MAAc,EAAE,MAAc;IACjD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;QAAE,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB;IACnF,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YAAE,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM;YAAE,MAAM;QAC9B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YAAE,CAAC,IAAI,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACvD,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YAAE,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACtB,CAAC,IAAI,CAAC,CAAC;YACP,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;gBAAE,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACxB,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;gBACnC,CAAC,IAAI,CAAC,CAAC;gBACP,KAAK,GAAG,CAAC,CAAC;gBACV,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK;oBAAE,CAAC,IAAI,CAAC,CAAC;gBACxD,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBAC/B,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;iBAAM,CAAC;gBACN,KAAK,GAAG,CAAC,CAAC;gBACV,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;oBAAE,CAAC,IAAI,CAAC,CAAC;gBAC3D,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QACD,IAAI,SAAS,KAAK,MAAM;YAAE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,KAAK,GAAgC,IAAI,GAAG,CAAC;IACjD,CAAC,KAAK,EAAE,GAAG,CAAC;IACZ,CAAC,IAAI,EAAE,GAAG,CAAC;IACX,CAAC,IAAI,EAAE,GAAG,CAAC;IACX,CAAC,MAAM,EAAE,GAAG,CAAC;IACb,CAAC,MAAM,EAAE,GAAG,CAAC;IACb,CAAC,MAAM,EAAE,GAAG,CAAC;IACb,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,QAAQ,EAAE,GAAG,CAAC;IACf,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,MAAM,EAAE,GAAG,CAAC;IACb,CAAC,KAAK,EAAE,GAAG,CAAC;IACZ,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,KAAK,EAAE,GAAG,CAAC;IACZ,CAAC,OAAO,EAAE,GAAG,CAAC;IACd,CAAC,QAAQ,EAAE,GAAG,CAAC;IACf,CAAC,QAAQ,EAAE,GAAG,CAAC;IACf,CAAC,QAAQ,EAAE,GAAG,CAAC;IACf,CAAC,QAAQ,EAAE,GAAG,CAAC;IACf,CAAC,MAAM,EAAE,GAAG,CAAC;IACb,CAAC,GAAG,EAAE,GAAG,CAAC;CACX,CAAC,CAAC;AAEH;;;GAGG;AACH,SAAS,cAAc,CAAC,IAAY;IAClC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,IAAI,CAAC,OAAO,CAAC,mCAAmC,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC/E,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACpB,MAAM,IAAI,GACR,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;gBAChC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACpC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACxE,IAAI,CAAC;gBACH,OAAO,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,KAAK,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search: a headless matcher over decoded section text, yielding jumpable
|
|
3
|
+
* hits. Imports only `src/core` + platform primitives (`DOMParser`, `Intl.Segmenter`,
|
|
4
|
+
* `TextDecoder`), so it runs under `node:test` with no browser and passes
|
|
5
|
+
* `npm run check:core`. The whole-book scan (`searchBook`) is a lazy async generator
|
|
6
|
+
* that never buffers the book whole and cleans up on early `break`.
|
|
7
|
+
*/
|
|
8
|
+
export { searchBook, matchText, type SearchHit, type SearchOptions } from './matcher.ts';
|
|
9
|
+
export { extractText, extractSectionText, decodeSectionBytes, type ReadingResolver } from './extract.ts';
|
|
10
|
+
export { normalizeText, normalizeQuery, type NormalizedText } from './normalize.ts';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search: a headless matcher over decoded section text, yielding jumpable
|
|
3
|
+
* hits. Imports only `src/core` + platform primitives (`DOMParser`, `Intl.Segmenter`,
|
|
4
|
+
* `TextDecoder`), so it runs under `node:test` with no browser and passes
|
|
5
|
+
* `npm run check:core`. The whole-book scan (`searchBook`) is a lazy async generator
|
|
6
|
+
* that never buffers the book whole and cleans up on early `break`.
|
|
7
|
+
*/
|
|
8
|
+
export { searchBook, matchText } from './matcher.js';
|
|
9
|
+
export { extractText, extractSectionText, decodeSectionBytes } from './extract.js';
|
|
10
|
+
export { normalizeText, normalizeQuery } from './normalize.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAsC,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EAAwB,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,cAAc,EAAuB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headless full-text matcher. Normalized literal substring search over a
|
|
3
|
+
* section's extracted text, yielding jumpable hits. Imports only `src/core`
|
|
4
|
+
* (`capturePosition`, `Position`, `Book`, `Section`) and platform primitives, so it
|
|
5
|
+
* runs under `node:test` with no browser.
|
|
6
|
+
*
|
|
7
|
+
* The whole-book scan is a lazy async generator: it decodes and scans one section at
|
|
8
|
+
* a time and yields hits as it goes, so the book is never buffered whole and a
|
|
9
|
+
* consumer that `break`s stops the scan (the generator's cleanup runs on return).
|
|
10
|
+
*/
|
|
11
|
+
import { type Book, type Position } from '../core/index.ts';
|
|
12
|
+
/** One search hit: the matched text, a clean context window, and a jumpable position. */
|
|
13
|
+
export interface SearchHit {
|
|
14
|
+
/** The verbatim matched run, in the section's raw text. */
|
|
15
|
+
readonly text: string;
|
|
16
|
+
/**
|
|
17
|
+
* The match surrounded by context, trimmed to whole words at both ends via
|
|
18
|
+
* `Intl.Segmenter` — no leading/trailing partial word.
|
|
19
|
+
*/
|
|
20
|
+
readonly context: string;
|
|
21
|
+
/**
|
|
22
|
+
* A content-addressed `Position` at the match, built with `capturePosition`. Pass
|
|
23
|
+
* it to `reader.goTo(hit.position)` to land on the hit's page.
|
|
24
|
+
*/
|
|
25
|
+
readonly position: Position;
|
|
26
|
+
/** 0-based index of the section this hit falls in, in reading order. */
|
|
27
|
+
readonly sectionIndex: number;
|
|
28
|
+
}
|
|
29
|
+
export interface SearchOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Characters of surrounding context to aim for on each side of the match before
|
|
32
|
+
* word-boundary trimming. Default 40.
|
|
33
|
+
*/
|
|
34
|
+
readonly contextRadius?: number;
|
|
35
|
+
/** Locale for word segmentation of the context. Defaults to the runtime default. */
|
|
36
|
+
readonly locale?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* All hits for `query` within one section's already-extracted `rawText`. Pure over
|
|
40
|
+
* strings — no I/O — so it is the unit the section scan and the tests both drive.
|
|
41
|
+
* A blank (whitespace-only or empty) query yields nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function matchText(rawText: string, query: string, sectionId: string, sectionIndex: number, options?: SearchOptions): Generator<SearchHit>;
|
|
44
|
+
/**
|
|
45
|
+
* Lazily scans every section of `book` for `query`, one section at a time, yielding
|
|
46
|
+
* each hit as it is found. The book is never buffered whole: each section's bytes are
|
|
47
|
+
* loaded, extracted, matched and released before the next is touched. A consumer that
|
|
48
|
+
* stops early (`break`/`return`) halts the scan — no later section is loaded.
|
|
49
|
+
*
|
|
50
|
+
* `onSection` (test-only instrumentation seam) fires with each section's index the
|
|
51
|
+
* moment before it is decoded, so a test can prove laziness by counting decodes.
|
|
52
|
+
*/
|
|
53
|
+
export declare function searchBook(book: Book, query: string, options?: SearchOptions & {
|
|
54
|
+
onSection?: (index: number) => void;
|
|
55
|
+
}): AsyncGenerator<SearchHit>;
|
|
56
|
+
//# sourceMappingURL=matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/search/matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAmB,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAgB,MAAM,kBAAkB,CAAC;AAM3F,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID;;;;GAIG;AACH,wBAAiB,SAAS,CACxB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,aAAkB,GAC1B,SAAS,CAAC,SAAS,CAAC,CA+CtB;AAqDD;;;;;;;;GAQG;AACH,wBAAuB,UAAU,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAa,GAAG;IAAE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GACpE,cAAc,CAAC,SAAS,CAAC,CAgB3B"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headless full-text matcher. Normalized literal substring search over a
|
|
3
|
+
* section's extracted text, yielding jumpable hits. Imports only `src/core`
|
|
4
|
+
* (`capturePosition`, `Position`, `Book`, `Section`) and platform primitives, so it
|
|
5
|
+
* runs under `node:test` with no browser.
|
|
6
|
+
*
|
|
7
|
+
* The whole-book scan is a lazy async generator: it decodes and scans one section at
|
|
8
|
+
* a time and yields hits as it goes, so the book is never buffered whole and a
|
|
9
|
+
* consumer that `break`s stops the scan (the generator's cleanup runs on return).
|
|
10
|
+
*/
|
|
11
|
+
import { capturePosition } from '../core/index.js';
|
|
12
|
+
import { collapseWhitespace as collapse } from '../core/text.js';
|
|
13
|
+
import { extractSectionText } from './extract.js';
|
|
14
|
+
import { normalizeQuery, normalizeText } from './normalize.js';
|
|
15
|
+
const DEFAULT_CONTEXT_RADIUS = 40;
|
|
16
|
+
/**
|
|
17
|
+
* All hits for `query` within one section's already-extracted `rawText`. Pure over
|
|
18
|
+
* strings — no I/O — so it is the unit the section scan and the tests both drive.
|
|
19
|
+
* A blank (whitespace-only or empty) query yields nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function* matchText(rawText, query, sectionId, sectionIndex, options = {}) {
|
|
22
|
+
const needle = normalizeQuery(query);
|
|
23
|
+
if (needle === '')
|
|
24
|
+
return;
|
|
25
|
+
const { text: haystack, map } = normalizeText(rawText);
|
|
26
|
+
const radius = options.contextRadius ?? DEFAULT_CONTEXT_RADIUS;
|
|
27
|
+
let from = 0;
|
|
28
|
+
for (;;) {
|
|
29
|
+
const at = haystack.indexOf(needle, from);
|
|
30
|
+
if (at === -1)
|
|
31
|
+
return;
|
|
32
|
+
const end = at + needle.length;
|
|
33
|
+
// Map the normalized match span back onto the raw text. `map[end]` points at the
|
|
34
|
+
// start of the character *after* the match, which — when that character is a
|
|
35
|
+
// collapsed space — lies past a run of raw whitespace; trim it so the matched
|
|
36
|
+
// text ends on real content. The anchor still starts at `rawStart`.
|
|
37
|
+
const rawStart = map[at];
|
|
38
|
+
const rawEnd = trimEnd(rawText, rawStart, map[end]);
|
|
39
|
+
const matched = rawText.slice(rawStart, rawEnd);
|
|
40
|
+
// Context window in normalized space, then mapped back and word-trimmed on the
|
|
41
|
+
// raw text so the shown context carries no partial word at either edge.
|
|
42
|
+
const ctxNormStart = Math.max(0, at - radius);
|
|
43
|
+
const ctxNormEnd = Math.min(haystack.length, end + radius);
|
|
44
|
+
const rawCtxStart = map[ctxNormStart];
|
|
45
|
+
const rawCtxEnd = map[ctxNormEnd];
|
|
46
|
+
const context = trimToWords(rawText, rawCtxStart, rawCtxEnd, rawStart, rawEnd, options.locale);
|
|
47
|
+
yield {
|
|
48
|
+
text: matched,
|
|
49
|
+
context,
|
|
50
|
+
position: capturePosition(rawText, rawStart, sectionId),
|
|
51
|
+
sectionIndex,
|
|
52
|
+
};
|
|
53
|
+
// Advance past this match; guard against a zero-length needle (impossible here,
|
|
54
|
+
// an empty needle returned above) so the loop always progresses.
|
|
55
|
+
from = end > at ? end : at + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Trims a raw context window `[start, end)` inward to whole-word boundaries via
|
|
60
|
+
* `Intl.Segmenter`, never cutting into the match span `[matchStart, matchEnd)`. A
|
|
61
|
+
* word straddling the left edge is dropped forward to its end (the next word's
|
|
62
|
+
* start); a word straddling the right edge is dropped back to its start. The result
|
|
63
|
+
* is whitespace-collapsed for a compact one-line context.
|
|
64
|
+
*/
|
|
65
|
+
function trimToWords(text, start, end, matchStart, matchEnd, locale) {
|
|
66
|
+
// Segment a slice wider than the window so a word straddling an edge is visible
|
|
67
|
+
// whole. Offsets are relative to the wide slice, so add `lo`.
|
|
68
|
+
const lo = Math.max(0, Math.min(start, matchStart) - 24);
|
|
69
|
+
const hi = Math.min(text.length, Math.max(end, matchEnd) + 24);
|
|
70
|
+
let head = start;
|
|
71
|
+
let tail = end;
|
|
72
|
+
for (const seg of wordSegmenter(locale).segment(text.slice(lo, hi))) {
|
|
73
|
+
if (!seg.isWordLike)
|
|
74
|
+
continue;
|
|
75
|
+
const segStart = lo + seg.index;
|
|
76
|
+
const segEnd = segStart + seg.segment.length;
|
|
77
|
+
// A word-like segment straddling the left edge: move head to its end (the next
|
|
78
|
+
// word's start), but never past the match start.
|
|
79
|
+
if (segStart < start && segEnd > start)
|
|
80
|
+
head = Math.min(matchStart, segEnd);
|
|
81
|
+
// A word-like segment straddling the right edge: move tail to its start, but
|
|
82
|
+
// never before the match end.
|
|
83
|
+
if (segStart < end && segEnd > end)
|
|
84
|
+
tail = Math.max(matchEnd, segStart);
|
|
85
|
+
}
|
|
86
|
+
return collapse(text.slice(head, tail));
|
|
87
|
+
}
|
|
88
|
+
/** Pull `end` back past any trailing whitespace, never before `start`. */
|
|
89
|
+
function trimEnd(text, start, end) {
|
|
90
|
+
let at = end;
|
|
91
|
+
while (at > start && /\s/.test(text[at - 1]))
|
|
92
|
+
at -= 1;
|
|
93
|
+
return at;
|
|
94
|
+
}
|
|
95
|
+
let cachedWordSegmenter;
|
|
96
|
+
function wordSegmenter(locale) {
|
|
97
|
+
if (cachedWordSegmenter === undefined || cachedWordSegmenter.locale !== locale) {
|
|
98
|
+
cachedWordSegmenter = { locale, segmenter: new Intl.Segmenter(locale, { granularity: 'word' }) };
|
|
99
|
+
}
|
|
100
|
+
return cachedWordSegmenter.segmenter;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Lazily scans every section of `book` for `query`, one section at a time, yielding
|
|
104
|
+
* each hit as it is found. The book is never buffered whole: each section's bytes are
|
|
105
|
+
* loaded, extracted, matched and released before the next is touched. A consumer that
|
|
106
|
+
* stops early (`break`/`return`) halts the scan — no later section is loaded.
|
|
107
|
+
*
|
|
108
|
+
* `onSection` (test-only instrumentation seam) fires with each section's index the
|
|
109
|
+
* moment before it is decoded, so a test can prove laziness by counting decodes.
|
|
110
|
+
*/
|
|
111
|
+
export async function* searchBook(book, query, options = {}) {
|
|
112
|
+
const needle = normalizeQuery(query);
|
|
113
|
+
if (needle === '')
|
|
114
|
+
return;
|
|
115
|
+
const sections = book.sections;
|
|
116
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
117
|
+
const section = sections[index];
|
|
118
|
+
options.onSection?.(index);
|
|
119
|
+
const bytes = await section.load();
|
|
120
|
+
// The section's own resolver rides along so extraction shows an <img> the
|
|
121
|
+
// way the frame will: rendered (no text) or substituted by its alt.
|
|
122
|
+
const rawText = extractSectionText(bytes, section.resolve?.bind(section));
|
|
123
|
+
for (const hit of matchText(rawText, query, section.id, index, options)) {
|
|
124
|
+
yield hit;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/search/matcher.ts"],"names":[],"mappings":"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"}
|