pdf-search-highlight 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pdf-search-highlight
2
2
 
3
- PDF viewer with text search and highlight. Render PDF, search text with flexible whitespace matching, and navigate between highlighted results.
3
+ PDF viewer with text search and highlight. Render PDF, search text with flexible whitespace matching or fuzzy (approximate) matching, and navigate between highlighted results. Zoom in/out and download PDF files.
4
4
 
5
5
  Built on [pdf.js](https://mozilla.github.io/pdf.js/). Works with Vanilla JS and React.
6
6
 
@@ -14,11 +14,15 @@ npm install pdf-search-highlight pdfjs-dist
14
14
 
15
15
  - Render PDF pages (canvas + text layer)
16
16
  - Search with flexible whitespace matching — handles inconsistent PDF text splitting
17
+ - Fuzzy (approximate) search — find text even with typos or OCR errors
17
18
  - Cross-span highlight using `<mark>` elements
18
19
  - Navigate between matches (next/prev, auto-scroll)
20
+ - Zoom in/out with configurable scale
21
+ - Download loaded PDF files
19
22
  - Case sensitive toggle
20
23
  - Custom CSS class names
21
24
  - Separate UI and PDF rendering — put search bar anywhere
25
+ - Search highlights preserved across zoom changes
22
26
 
23
27
  ## Usage
24
28
 
@@ -43,19 +47,60 @@ search.onChange = ({ current, total }) => {
43
47
  };
44
48
 
45
49
  search.search('hello world');
50
+ search.search('helo wrld', { fuzzy: true, fuzzyThreshold: 0.6 }); // approximate match
46
51
  search.next();
47
52
  search.prev();
48
53
  search.clear();
54
+
55
+ // Zoom
56
+ renderer.setScale(1.5);
57
+ const newPages = await renderer.renderAllPages();
58
+ search.setPages(newPages); // re-applies search highlights automatically
59
+
60
+ // Download
61
+ await renderer.download('document.pdf');
62
+ ```
63
+
64
+ ### Vanilla JS (All-in-One)
65
+
66
+ ```js
67
+ import { PDFSearchViewer } from 'pdf-search-highlight';
68
+ import 'pdf-search-highlight/styles.css';
69
+
70
+ const viewer = new PDFSearchViewer(container, pdfjsLib, {
71
+ scale: 'auto', // or a number like 1.5
72
+ pageGap: 20,
73
+ });
74
+
75
+ await viewer.loadPDF(file);
76
+ viewer.search('query');
77
+ viewer.nextMatch();
78
+
79
+ // Zoom
80
+ await viewer.zoomIn();
81
+ await viewer.zoomOut();
82
+ await viewer.setScale(2.0);
83
+
84
+ // Download
85
+ await viewer.download('document.pdf');
86
+
87
+ // Events
88
+ viewer.on('load', ({ pageCount }) => console.log('Pages:', pageCount));
89
+ viewer.on('search', ({ query, total }) => console.log('Found:', total));
90
+ viewer.on('matchchange', ({ current, total }) => console.log(`${current + 1}/${total}`));
91
+ viewer.on('zoom', ({ scale }) => console.log('Scale:', scale));
92
+ viewer.on('error', ({ error, context }) => console.error(context, error));
49
93
  ```
50
94
 
51
- ### React
95
+ ### React (Hooks)
52
96
 
53
97
  ```tsx
54
98
  import { usePDFRenderer, useSearchController } from 'pdf-search-highlight/react';
55
99
  import 'pdf-search-highlight/styles.css';
56
100
 
57
101
  function App() {
58
- const { containerRef, pages, loadPDF } = usePDFRenderer(pdfjsLib);
102
+ const { containerRef, pages, loadPDF, zoomIn, zoomOut, download, scale } =
103
+ usePDFRenderer(pdfjsLib);
59
104
  const { search, next, prev, current, total } = useSearchController(pages);
60
105
 
61
106
  return (
@@ -66,13 +111,49 @@ function App() {
66
111
  <button onClick={prev}>Prev</button>
67
112
  <button onClick={next}>Next</button>
68
113
 
69
- {/* PDF container anywhere else */}
114
+ {/* Zoom & download */}
115
+ <button onClick={zoomOut}>-</button>
116
+ <button onClick={zoomIn}>+</button>
117
+ <button onClick={() => download('doc.pdf')}>Download</button>
118
+
119
+ {/* PDF container */}
70
120
  <div ref={containerRef} style={{ height: '80vh', overflow: 'auto' }} />
71
121
  </>
72
122
  );
73
123
  }
74
124
  ```
75
125
 
126
+ ### React (All-in-One Component)
127
+
128
+ ```tsx
129
+ import { useRef } from 'react';
130
+ import { PDFSearchViewer, PDFSearchViewerHandle } from 'pdf-search-highlight/react';
131
+ import 'pdf-search-highlight/styles.css';
132
+
133
+ function App() {
134
+ const ref = useRef<PDFSearchViewerHandle>(null);
135
+
136
+ return (
137
+ <PDFSearchViewer
138
+ ref={ref}
139
+ pdfjsLib={pdfjsLib}
140
+ source={file}
141
+ searchQuery={query}
142
+ onLoad={({ pageCount }) => console.log('Pages:', pageCount)}
143
+ onSearch={({ query, total }) => console.log('Found:', total)}
144
+ onMatchChange={({ current, total }) => console.log(`${current + 1}/${total}`)}
145
+ onZoom={({ scale }) => console.log('Scale:', scale)}
146
+ style={{ height: '80vh', overflow: 'auto' }}
147
+ />
148
+ );
149
+
150
+ // Imperative access via ref
151
+ // ref.current.nextMatch()
152
+ // ref.current.zoomIn()
153
+ // ref.current.download('doc.pdf')
154
+ }
155
+ ```
156
+
76
157
  ## API
77
158
 
78
159
  ### Core (`pdf-search-highlight`)
@@ -81,7 +162,7 @@ function App() {
81
162
  |---|---|
82
163
  | `PDFRenderer` | Renders PDF pages into a container (canvas + text layer) |
83
164
  | `SearchController` | Headless search + highlight controller |
84
- | `PDFSearchViewer` | All-in-one (render + search + highlight) |
165
+ | `PDFSearchViewer` | All-in-one: render + search + highlight + zoom + download |
85
166
  | `searchPage` | Low-level: search spans with flexible regex |
86
167
  | `HighlightManager` | Low-level: apply/clear highlights on spans |
87
168
 
@@ -89,9 +170,27 @@ function App() {
89
170
 
90
171
  | Export | Description |
91
172
  |---|---|
92
- | `usePDFRenderer(pdfjsLib, options?)` | Hook: render PDF, returns `{ containerRef, pages, loadPDF }` |
93
- | `useSearchController(pages, options?)` | Hook: search + highlight, returns `{ search, next, prev, current, total }` |
94
- | `PDFSearchViewer` | All-in-one component |
173
+ | `usePDFRenderer(pdfjsLib, options?)` | Hook: render PDF, returns `{ containerRef, pages, loadPDF, scale, setScale, zoomIn, zoomOut, download, ... }` |
174
+ | `useSearchController(pages, options?)` | Hook: search + highlight, returns `{ search, next, prev, goTo, clear, current, total }` |
175
+ | `PDFSearchViewer` | All-in-one component with ref handle for imperative control |
176
+
177
+ ### PDFRenderer
178
+
179
+ ```js
180
+ const renderer = new PDFRenderer(container, options);
181
+ renderer.setPdfjsLib(pdfjsLib);
182
+
183
+ await renderer.loadDocument(source); // Load PDF (File | ArrayBuffer | Uint8Array | string URL)
184
+ const pages = await renderer.renderAllPages(); // Render all pages
185
+
186
+ renderer.setScale(1.5); // Set zoom level (number or 'auto')
187
+ renderer.getScale(); // Get configured scale
188
+ renderer.getEffectiveScale(); // Get actual numeric scale used
189
+
190
+ await renderer.download('file.pdf'); // Download loaded PDF
191
+ renderer.getPageCount(); // Total page count
192
+ renderer.cleanup(); // Release resources
193
+ ```
95
194
 
96
195
  ### SearchController
97
196
 
@@ -102,6 +201,7 @@ const search = new SearchController({
102
201
 
103
202
  search.setPages(pages);
104
203
  search.search('query', { caseSensitive: false, flexibleWhitespace: true });
204
+ search.search('query', { fuzzy: true, fuzzyThreshold: 0.6 });
105
205
  search.next();
106
206
  search.prev();
107
207
  search.goTo(5);
@@ -113,6 +213,51 @@ search.total // total matches
113
213
  search.query // last query
114
214
  ```
115
215
 
216
+ ### PDFSearchViewer (Core)
217
+
218
+ ```js
219
+ const viewer = new PDFSearchViewer(container, pdfjsLib, options);
220
+
221
+ await viewer.loadPDF(source);
222
+ viewer.search('query', { caseSensitive: true });
223
+ viewer.nextMatch();
224
+ viewer.prevMatch();
225
+ viewer.clearSearch();
226
+
227
+ await viewer.zoomIn(); // Zoom in by 0.25
228
+ await viewer.zoomOut(); // Zoom out by 0.25
229
+ await viewer.setScale(2.0); // Set specific scale
230
+ viewer.getScale(); // Get current scale
231
+
232
+ await viewer.download('file.pdf'); // Download PDF
233
+
234
+ viewer.on('load', (data) => {}); // { pageCount }
235
+ viewer.on('search', (data) => {}); // { query, total }
236
+ viewer.on('matchchange', (data) => {}); // { current, total }
237
+ viewer.on('zoom', (data) => {}); // { scale }
238
+ viewer.on('error', (data) => {}); // { error, context }
239
+
240
+ viewer.destroy();
241
+ ```
242
+
243
+ ### Options
244
+
245
+ ```ts
246
+ interface PDFSearchViewerOptions {
247
+ scale?: number | 'auto'; // Default: 'auto' (fit container width)
248
+ workerSrc?: string; // Path to pdf.js worker
249
+ pageGap?: number; // Gap between pages in px (default: 20)
250
+ classNames?: ClassNames; // Custom CSS class names
251
+ }
252
+
253
+ interface SearchOptions {
254
+ caseSensitive?: boolean; // Default: false
255
+ flexibleWhitespace?: boolean; // Default: true (ignored when fuzzy is true)
256
+ fuzzy?: boolean; // Default: false — enable approximate matching
257
+ fuzzyThreshold?: number; // Default: 0.6 — similarity 0.0–1.0
258
+ }
259
+ ```
260
+
116
261
  ### Custom CSS
117
262
 
118
263
  Override any class name:
@@ -147,8 +292,10 @@ Default styles:
147
292
  1. **Render**: PDF.js renders each page as `<canvas>` + transparent `<span>` text layer overlay
148
293
  2. **Search**: Concatenate all span texts into one string per page, build a `charMap` mapping each character back to its source span
149
294
  3. **Flexible whitespace**: Query `"and expensive"` becomes regex `a\s*n\s*d\s*e\s*x\s*p\s*e\s*n\s*s\s*i\s*v\s*e` — matches regardless of whitespace differences in PDF text
150
- 4. **Highlight**: Regex matches on concatenated text charMap maps back to spans split span DOM into text nodes + `<mark>` elements
151
- 5. **Navigate**: Prev/next with wrap-around, auto-scroll to active match
295
+ 4. **Fuzzy search**: Semi-global Levenshtein alignment finds substrings within edit distance `queryLength × (1 - threshold)` handles typos, OCR errors, and garbled text extraction
296
+ 5. **Highlight**: Regex/fuzzy matches on concatenated text → charMap maps back to spans → split span DOM into text nodes + `<mark>` elements
297
+ 6. **Navigate**: Prev/next with wrap-around, auto-scroll to active match
298
+ 7. **Zoom**: Re-renders all pages at new scale, search highlights are automatically re-applied
152
299
 
153
300
  ## License
154
301
 
@@ -57,8 +57,17 @@ interface SearchOptions {
57
57
  * Flexible whitespace matching: insert \s* between every character.
58
58
  * Handles PDF text split inconsistencies. Defaults to true.
59
59
  * Only applies for queries < 200 chars (performance).
60
+ * Ignored when `fuzzy` is true.
60
61
  */
61
62
  flexibleWhitespace?: boolean;
63
+ /** Enable approximate (fuzzy) matching. Defaults to false. */
64
+ fuzzy?: boolean;
65
+ /**
66
+ * Similarity threshold for fuzzy matching: 0.0–1.0.
67
+ * similarity = 1 - (editDistance / queryLength).
68
+ * Higher values require closer matches. Defaults to 0.6.
69
+ */
70
+ fuzzyThreshold?: number;
62
71
  }
63
72
 
64
73
  type PDFSearchViewerEventMap = {
@@ -57,8 +57,17 @@ interface SearchOptions {
57
57
  * Flexible whitespace matching: insert \s* between every character.
58
58
  * Handles PDF text split inconsistencies. Defaults to true.
59
59
  * Only applies for queries < 200 chars (performance).
60
+ * Ignored when `fuzzy` is true.
60
61
  */
61
62
  flexibleWhitespace?: boolean;
63
+ /** Enable approximate (fuzzy) matching. Defaults to false. */
64
+ fuzzy?: boolean;
65
+ /**
66
+ * Similarity threshold for fuzzy matching: 0.0–1.0.
67
+ * similarity = 1 - (editDistance / queryLength).
68
+ * Higher values require closer matches. Defaults to 0.6.
69
+ */
70
+ fuzzyThreshold?: number;
62
71
  }
63
72
 
64
73
  type PDFSearchViewerEventMap = {
@@ -85,10 +85,14 @@ var PDFRenderer = class {
85
85
  }
86
86
  /**
87
87
  * Render all pages into the container.
88
+ * Preserves scroll position across re-renders (e.g. zoom).
88
89
  * Returns PageData[] for search/highlight.
89
90
  */
90
91
  async renderAllPages() {
91
92
  if (!this.pdfDoc) throw new Error("No PDF document loaded");
93
+ const prevScrollTop = this.container.scrollTop;
94
+ const prevScrollHeight = this.container.scrollHeight || 1;
95
+ const scrollRatio = prevScrollTop / prevScrollHeight;
92
96
  this.container.innerHTML = "";
93
97
  this.container.classList.add(this.cls.container);
94
98
  this.pageData = [];
@@ -98,6 +102,9 @@ var PDFRenderer = class {
98
102
  const pd = await this.renderPage(page, i, numPages);
99
103
  this.pageData.push(pd);
100
104
  }
105
+ if (prevScrollTop > 0) {
106
+ this.container.scrollTop = scrollRatio * this.container.scrollHeight;
107
+ }
101
108
  return this.pageData;
102
109
  }
103
110
  async renderPage(page, pageNum, totalPages) {
@@ -245,9 +252,72 @@ function buildFlexibleRegex(query, options) {
245
252
  const pattern = chars.map((c) => escapeRegex(c)).join("\\s*");
246
253
  return new RegExp(pattern, isCaseSensitive ? "g" : "gi");
247
254
  }
248
- function searchPage(spans, query, options = {}) {
249
- const regex = buildFlexibleRegex(query, options);
250
- if (!regex) return [];
255
+ function fuzzySearchText(text, query, maxErrors) {
256
+ const n = text.length;
257
+ const m = query.length;
258
+ if (m === 0) return [];
259
+ if (n === 0) return [];
260
+ let prev = new Uint32Array(m + 1);
261
+ for (let j = 0; j <= m; j++) prev[j] = j;
262
+ const columns = [prev.slice()];
263
+ const endPositions = [];
264
+ for (let i = 1; i <= n; i++) {
265
+ const curr = new Uint32Array(m + 1);
266
+ curr[0] = 0;
267
+ for (let j = 1; j <= m; j++) {
268
+ const cost = text[i - 1] === query[j - 1] ? 0 : 1;
269
+ curr[j] = Math.min(
270
+ prev[j] + 1,
271
+ // deletion
272
+ curr[j - 1] + 1,
273
+ // insertion
274
+ prev[j - 1] + cost
275
+ // substitution
276
+ );
277
+ }
278
+ columns.push(curr.slice());
279
+ if (curr[m] <= maxErrors) {
280
+ endPositions.push({ col: i, distance: curr[m] });
281
+ }
282
+ prev = curr;
283
+ }
284
+ if (endPositions.length === 0) return [];
285
+ const rawMatches = [];
286
+ for (const { col: endCol, distance } of endPositions) {
287
+ let j = m;
288
+ let i = endCol;
289
+ while (j > 0 && i > 0) {
290
+ const c = columns[i];
291
+ const p = columns[i - 1];
292
+ const cost = text[i - 1] === query[j - 1] ? 0 : 1;
293
+ if (c[j] === p[j - 1] + cost) {
294
+ i--;
295
+ j--;
296
+ } else if (c[j] === p[j] + 1) {
297
+ i--;
298
+ } else {
299
+ j--;
300
+ }
301
+ }
302
+ rawMatches.push({ start: i, end: endCol, distance });
303
+ }
304
+ if (rawMatches.length === 0) return [];
305
+ rawMatches.sort((a, b) => a.start - b.start || a.distance - b.distance);
306
+ const merged = [rawMatches[0]];
307
+ for (let i = 1; i < rawMatches.length; i++) {
308
+ const prev2 = merged[merged.length - 1];
309
+ const curr = rawMatches[i];
310
+ if (curr.start < prev2.end) {
311
+ if (curr.distance < prev2.distance) {
312
+ merged[merged.length - 1] = curr;
313
+ }
314
+ } else {
315
+ merged.push(curr);
316
+ }
317
+ }
318
+ return merged;
319
+ }
320
+ function buildTextAndCharMap(spans) {
251
321
  let fullText = "";
252
322
  const charMap = [];
253
323
  spans.forEach((s, si) => {
@@ -256,27 +326,52 @@ function searchPage(spans, query, options = {}) {
256
326
  fullText += s.text[ci];
257
327
  }
258
328
  });
329
+ return { fullText, charMap };
330
+ }
331
+ function mapToSpanRanges(start, end, charMap) {
332
+ const range = [];
333
+ for (let k = start; k < end; k++) {
334
+ const cm = charMap[k];
335
+ const last = range[range.length - 1];
336
+ if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {
337
+ last.end = cm.charIdx + 1;
338
+ } else {
339
+ range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });
340
+ }
341
+ }
342
+ return range;
343
+ }
344
+ function searchPage(spans, query, options = {}) {
345
+ const trimmed = query.trim();
346
+ if (!trimmed) return [];
347
+ if (options.fuzzy) {
348
+ return fuzzySearchPage(spans, trimmed, options);
349
+ }
350
+ const regex = buildFlexibleRegex(query, options);
351
+ if (!regex) return [];
352
+ const { fullText, charMap } = buildTextAndCharMap(spans);
259
353
  const allMatchRanges = [];
260
354
  let m;
261
355
  regex.lastIndex = 0;
262
356
  while ((m = regex.exec(fullText)) !== null) {
263
- const start = m.index;
264
- const end = start + m[0].length;
265
- const range = [];
266
- for (let k = start; k < end; k++) {
267
- const cm = charMap[k];
268
- const last = range[range.length - 1];
269
- if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {
270
- last.end = cm.charIdx + 1;
271
- } else {
272
- range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });
273
- }
274
- }
275
- allMatchRanges.push(range);
357
+ allMatchRanges.push(mapToSpanRanges(m.index, m.index + m[0].length, charMap));
276
358
  if (m[0].length === 0) regex.lastIndex++;
277
359
  }
278
360
  return allMatchRanges;
279
361
  }
362
+ function fuzzySearchPage(spans, query, options) {
363
+ const { fullText, charMap } = buildTextAndCharMap(spans);
364
+ if (fullText.length === 0) return [];
365
+ const isCaseSensitive = options.caseSensitive ?? false;
366
+ const threshold = options.fuzzyThreshold ?? 0.6;
367
+ const searchText = isCaseSensitive ? fullText : fullText.toLowerCase();
368
+ const searchQuery = isCaseSensitive ? query : query.toLowerCase();
369
+ const strippedQuery = searchQuery.replace(/\s+/g, "");
370
+ if (strippedQuery.length === 0) return [];
371
+ const maxErrors = Math.floor(strippedQuery.length * (1 - threshold));
372
+ const matches = fuzzySearchText(searchText, strippedQuery, maxErrors);
373
+ return matches.map((m) => mapToSpanRanges(m.start, m.end, charMap));
374
+ }
280
375
 
281
376
  // src/core/HighlightManager.ts
282
377
  var HighlightManager = class {
@@ -666,4 +761,4 @@ export {
666
761
  PDFSearchViewer,
667
762
  SearchController
668
763
  };
669
- //# sourceMappingURL=chunk-ABW444BW.js.map
764
+ //# sourceMappingURL=chunk-AA4UECNB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/EventEmitter.ts","../src/core/constants.ts","../src/core/PDFRenderer.ts","../src/core/SearchEngine.ts","../src/core/HighlightManager.ts","../src/core/PDFSearchViewer.ts","../src/core/SearchController.ts"],"sourcesContent":["type Listener<T> = (data: T) => void;\n\nexport class EventEmitter<EventMap extends { [key: string]: unknown }> {\n private listeners = new Map<keyof EventMap, Set<Listener<any>>>();\n\n on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(listener);\n return this;\n }\n\n off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n protected emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {\n this.listeners.get(event)?.forEach((fn) => fn(data));\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","import type { ClassNames } from '../types';\n\nexport const DEFAULT_CLASS_NAMES: Required<ClassNames> = {\n container: 'psh-container',\n page: 'psh-page',\n canvas: 'psh-canvas',\n textLayer: 'psh-text-layer',\n pageLabel: 'psh-page-label',\n highlight: 'highlight',\n activeHighlight: 'active',\n};\n\nexport const DEFAULT_SCALE = 'auto' as number | 'auto';\nexport const DEFAULT_PAGE_GAP = 20;\n\nexport const ZOOM_STEP = 0.25;\nexport const MIN_SCALE = 0.25;\nexport const MAX_SCALE = 5;\n","import type { PDFSearchViewerOptions, ClassNames, PageData, SpanData } from '../types';\nimport { DEFAULT_CLASS_NAMES, DEFAULT_SCALE, DEFAULT_PAGE_GAP } from './constants';\n\n// pdfjs-dist types\ntype PDFDocumentProxy = any;\ntype PDFPageProxy = any;\n\n/**\n * Renders PDF pages into a container using canvas + text layer.\n *\n * Text layer approach (matching demo):\n * - Extract text content from each page\n * - Create absolutely-positioned <span> elements overlaying the canvas\n * - Position spans using the transform matrix from pdf.js\n * - Spans are transparent (for text selection) but allow DOM-based search/highlight\n */\nexport class PDFRenderer {\n private container: HTMLElement;\n private scale: number | 'auto';\n private pageGap: number;\n private cls: Required<ClassNames>;\n private workerSrc?: string;\n private pdfDoc: PDFDocumentProxy | null = null;\n private pageData: PageData[] = [];\n private pdfjsLib: any = null;\n private effectiveScale: number = 1;\n\n constructor(container: HTMLElement, options: PDFSearchViewerOptions) {\n this.container = container;\n this.scale = options.scale ?? DEFAULT_SCALE;\n this.pageGap = options.pageGap ?? DEFAULT_PAGE_GAP;\n this.workerSrc = options.workerSrc;\n this.cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n }\n\n /**\n * Set the pdfjs-dist library reference.\n * Must be called before loadDocument.\n */\n setPdfjsLib(lib: any): void {\n this.pdfjsLib = lib;\n if (this.workerSrc) {\n lib.GlobalWorkerOptions.workerSrc = this.workerSrc;\n }\n }\n\n /**\n * Load a PDF from File, ArrayBuffer, URL string, or Uint8Array.\n */\n async loadDocument(\n source: File | ArrayBuffer | Uint8Array | string\n ): Promise<number> {\n if (!this.pdfjsLib) {\n throw new Error(\n 'pdfjs-dist not set. Call setPdfjsLib(pdfjsLib) before loading a document.'\n );\n }\n\n this.cleanup();\n\n let data: ArrayBuffer | Uint8Array | { url: string };\n if (source instanceof File) {\n data = await source.arrayBuffer();\n } else if (typeof source === 'string') {\n data = { url: source };\n } else {\n data = source;\n }\n\n const loadingTask = this.pdfjsLib.getDocument({ data });\n this.pdfDoc = await loadingTask.promise;\n return this.pdfDoc.numPages;\n }\n\n /**\n * Render all pages into the container.\n * Preserves scroll position across re-renders (e.g. zoom).\n * Returns PageData[] for search/highlight.\n */\n async renderAllPages(): Promise<PageData[]> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n // Save scroll position relative to total content height\n const prevScrollTop = this.container.scrollTop;\n const prevScrollHeight = this.container.scrollHeight || 1;\n const scrollRatio = prevScrollTop / prevScrollHeight;\n\n this.container.innerHTML = '';\n this.container.classList.add(this.cls.container);\n this.pageData = [];\n\n const numPages = this.pdfDoc.numPages;\n\n for (let i = 1; i <= numPages; i++) {\n const page = await this.pdfDoc.getPage(i);\n const pd = await this.renderPage(page, i, numPages);\n this.pageData.push(pd);\n }\n\n // Restore scroll position proportionally\n if (prevScrollTop > 0) {\n this.container.scrollTop = scrollRatio * this.container.scrollHeight;\n }\n\n return this.pageData;\n }\n\n private async renderPage(\n page: PDFPageProxy,\n pageNum: number,\n totalPages: number\n ): Promise<PageData> {\n const scale = this.calculateScale(page);\n if (pageNum === 1) this.effectiveScale = scale;\n const vp = page.getViewport({ scale });\n\n // Page container\n const container = document.createElement('div');\n container.className = this.cls.page;\n container.style.position = 'relative';\n container.style.width = vp.width + 'px';\n container.style.height = vp.height + 'px';\n container.style.margin = '0 auto';\n container.style.marginBottom = this.pageGap + 'px';\n container.style.overflow = 'hidden';\n container.dataset.page = String(pageNum);\n\n // Canvas (2x for retina)\n const canvas = document.createElement('canvas');\n canvas.className = this.cls.canvas;\n canvas.width = vp.width * 2;\n canvas.height = vp.height * 2;\n canvas.style.width = vp.width + 'px';\n canvas.style.height = vp.height + 'px';\n canvas.style.display = 'block';\n const ctx = canvas.getContext('2d')!;\n ctx.scale(2, 2);\n await page.render({ canvasContext: ctx, viewport: vp }).promise;\n\n // Text layer\n const textLayer = document.createElement('div');\n textLayer.className = this.cls.textLayer;\n textLayer.style.position = 'absolute';\n textLayer.style.top = '0';\n textLayer.style.left = '0';\n textLayer.style.right = '0';\n textLayer.style.bottom = '0';\n textLayer.style.overflow = 'hidden';\n textLayer.style.lineHeight = '1';\n\n const tc = await page.getTextContent();\n const spans: SpanData[] = [];\n\n for (const item of tc.items) {\n if (!item.str && !item.hasEOL) continue;\n\n const tx = this.pdfjsLib.Util.transform(vp.transform, item.transform);\n const span = document.createElement('span');\n span.textContent = item.str || '';\n const fh = Math.hypot(tx[2], tx[3]);\n span.style.position = 'absolute';\n span.style.left = tx[4] + 'px';\n span.style.top = (tx[5] - fh) + 'px';\n span.style.fontSize = fh + 'px';\n span.style.color = 'transparent';\n span.style.whiteSpace = 'pre';\n span.style.cursor = 'text';\n span.style.transformOrigin = '0% 0%';\n if (item.fontName) span.style.fontFamily = item.fontName;\n\n const sw = tx[0] / fh;\n if (Math.abs(sw - 1) > 0.01) {\n span.style.transform = `scaleX(${sw})`;\n }\n\n textLayer.appendChild(span);\n spans.push({\n el: span,\n text: item.str || '',\n hasEOL: !!item.hasEOL,\n });\n }\n\n container.appendChild(canvas);\n container.appendChild(textLayer);\n this.container.appendChild(container);\n\n // Page label\n const label = document.createElement('div');\n label.className = this.cls.pageLabel;\n label.textContent = `Page ${pageNum} / ${totalPages}`;\n this.container.appendChild(label);\n\n return { container, spans };\n }\n\n private calculateScale(page: PDFPageProxy): number {\n if (this.scale !== 'auto' && typeof this.scale === 'number') {\n return this.scale;\n }\n const defaultVp = page.getViewport({ scale: 1 });\n const containerWidth = this.container.clientWidth || 800;\n return Math.min(containerWidth / defaultVp.width, 2);\n }\n\n /** Set the scale for subsequent renders. */\n setScale(scale: number | 'auto'): void {\n this.scale = scale;\n }\n\n /** Get the configured scale setting. */\n getScale(): number | 'auto' {\n return this.scale;\n }\n\n /** Get the actual numeric scale used in the last render. */\n getEffectiveScale(): number {\n return this.effectiveScale;\n }\n\n /**\n * Download the currently loaded PDF.\n */\n async download(filename: string = 'document.pdf'): Promise<void> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n const data = await this.pdfDoc.getData();\n const blob = new Blob([data as BlobPart], { type: 'application/pdf' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n\n getClassNames(): Required<ClassNames> {\n return this.cls;\n }\n\n getPageData(): PageData[] {\n return this.pageData;\n }\n\n getPageCount(): number {\n return this.pdfDoc?.numPages ?? 0;\n }\n\n cleanup(): void {\n this.pdfDoc?.destroy();\n this.pdfDoc = null;\n this.pageData = [];\n this.container.innerHTML = '';\n }\n}\n","import type { SearchOptions, SpanData } from '../types';\n\nexport interface CharMapEntry {\n spanIdx: number;\n charIdx: number;\n}\n\nexport interface MatchRange {\n spanIdx: number;\n start: number;\n end: number;\n}\n\nexport interface SearchResult {\n /** Array of span ranges for each match */\n matchRanges: MatchRange[][];\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Build a flexible regex from query.\n *\n * For queries < 200 chars (after removing whitespace):\n * - Strip all whitespace from query\n * - Insert \\s* between every character\n * → \"and expensive\" becomes a\\s*n\\s*d\\s*e\\s*x\\s*p\\s*e\\s*n\\s*s\\s*i\\s*v\\s*e\n * → Matches regardless of whitespace differences in PDF text\n *\n * For queries >= 200 chars:\n * - Split by whitespace, join with \\s+\n */\nfunction buildFlexibleRegex(\n query: string,\n options: SearchOptions\n): RegExp | null {\n const trimmed = query.trim();\n if (!trimmed) return null;\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const flexibleWhitespace = options.flexibleWhitespace ?? true;\n\n if (!flexibleWhitespace) {\n // Simple literal search\n const pattern = escapeRegex(trimmed);\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Remove all whitespace chars from query\n const chars = [...trimmed].filter((c) => !/\\s/.test(c));\n if (chars.length === 0) return null;\n\n if (chars.length > 200) {\n // Fallback: flexible between tokens only\n const tokens = trimmed.split(/\\s+/);\n const pattern = tokens.map((t) => escapeRegex(t)).join('\\\\s+');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Insert \\s* between every character\n const pattern = chars.map((c) => escapeRegex(c)).join('\\\\s*');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n}\n\ninterface FuzzyMatch {\n start: number;\n end: number;\n distance: number;\n}\n\n/**\n * Semi-global Levenshtein alignment for approximate substring matching.\n *\n * Finds all positions in `text` where a substring has edit distance ≤ maxErrors\n * from `query`. Uses O(m) space with single-column DP.\n *\n * Semi-global: first DP column = 0 (match can start anywhere in text),\n * but the full query must be covered.\n */\nfunction fuzzySearchText(\n text: string,\n query: string,\n maxErrors: number\n): FuzzyMatch[] {\n const n = text.length;\n const m = query.length;\n if (m === 0) return [];\n if (n === 0) return [];\n\n // DP: prev[j] = min edit distance to match query[0..j-1] ending at current text pos\n // Semi-global: prev[0] = 0 for all text positions (free start)\n let prev = new Uint32Array(m + 1);\n for (let j = 0; j <= m; j++) prev[j] = j;\n\n // Store full DP matrix columns for traceback\n const columns: Uint32Array[] = [prev.slice()];\n\n // Track match end positions\n const endPositions: Array<{ col: number; distance: number }> = [];\n\n for (let i = 1; i <= n; i++) {\n const curr = new Uint32Array(m + 1);\n curr[0] = 0; // semi-global: free start position\n for (let j = 1; j <= m; j++) {\n const cost = text[i - 1] === query[j - 1] ? 0 : 1;\n curr[j] = Math.min(\n prev[j] + 1, // deletion\n curr[j - 1] + 1, // insertion\n prev[j - 1] + cost // substitution\n );\n }\n columns.push(curr.slice());\n\n if (curr[m] <= maxErrors) {\n endPositions.push({ col: i, distance: curr[m] });\n }\n prev = curr;\n }\n\n if (endPositions.length === 0) return [];\n\n // Traceback to find start position for each match end\n const rawMatches: FuzzyMatch[] = [];\n for (const { col: endCol, distance } of endPositions) {\n // Trace back through the DP matrix to find where the match starts\n let j = m;\n let i = endCol;\n while (j > 0 && i > 0) {\n const c = columns[i];\n const p = columns[i - 1];\n const cost = text[i - 1] === query[j - 1] ? 0 : 1;\n if (c[j] === p[j - 1] + cost) {\n // substitution or match — move diagonally\n i--;\n j--;\n } else if (c[j] === p[j] + 1) {\n // deletion from text — move left in text\n i--;\n } else {\n // insertion into text — move up in query\n j--;\n }\n }\n rawMatches.push({ start: i, end: endCol, distance });\n }\n\n // Merge overlapping matches, keeping the one with lowest distance\n if (rawMatches.length === 0) return [];\n rawMatches.sort((a, b) => a.start - b.start || a.distance - b.distance);\n\n const merged: FuzzyMatch[] = [rawMatches[0]];\n for (let i = 1; i < rawMatches.length; i++) {\n const prev = merged[merged.length - 1];\n const curr = rawMatches[i];\n if (curr.start < prev.end) {\n // Overlapping — keep the one with lower distance\n if (curr.distance < prev.distance) {\n merged[merged.length - 1] = curr;\n }\n } else {\n merged.push(curr);\n }\n }\n\n return merged;\n}\n\n/**\n * Build fullText and charMap from spans.\n */\nfunction buildTextAndCharMap(spans: SpanData[]) {\n let fullText = '';\n const charMap: CharMapEntry[] = [];\n spans.forEach((s, si) => {\n for (let ci = 0; ci < s.text.length; ci++) {\n charMap.push({ spanIdx: si, charIdx: ci });\n fullText += s.text[ci];\n }\n });\n return { fullText, charMap };\n}\n\n/**\n * Map a start/end range in fullText to MatchRange[] via charMap.\n */\nfunction mapToSpanRanges(\n start: number,\n end: number,\n charMap: CharMapEntry[]\n): MatchRange[] {\n const range: MatchRange[] = [];\n for (let k = start; k < end; k++) {\n const cm = charMap[k];\n const last = range[range.length - 1];\n if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {\n last.end = cm.charIdx + 1;\n } else {\n range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });\n }\n }\n return range;\n}\n\n/**\n * Search for text across page spans using charMap-based matching.\n *\n * Algorithm:\n * 1. Concatenate all span texts into one string (fullText)\n * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText\n * 3. Run regex or fuzzy search on fullText\n * 4. Map each match back to span ranges via charMap\n */\nexport function searchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions = {}\n): MatchRange[][] {\n const trimmed = query.trim();\n if (!trimmed) return [];\n\n if (options.fuzzy) {\n return fuzzySearchPage(spans, trimmed, options);\n }\n\n const regex = buildFlexibleRegex(query, options);\n if (!regex) return [];\n\n const { fullText, charMap } = buildTextAndCharMap(spans);\n\n // Find all regex matches\n const allMatchRanges: MatchRange[][] = [];\n let m: RegExpExecArray | null;\n regex.lastIndex = 0;\n\n while ((m = regex.exec(fullText)) !== null) {\n allMatchRanges.push(mapToSpanRanges(m.index, m.index + m[0].length, charMap));\n if (m[0].length === 0) regex.lastIndex++;\n }\n\n return allMatchRanges;\n}\n\nfunction fuzzySearchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions\n): MatchRange[][] {\n const { fullText, charMap } = buildTextAndCharMap(spans);\n if (fullText.length === 0) return [];\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const threshold = options.fuzzyThreshold ?? 0.6;\n\n const searchText = isCaseSensitive ? fullText : fullText.toLowerCase();\n const searchQuery = isCaseSensitive ? query : query.toLowerCase();\n\n // Strip whitespace from query for matching\n const strippedQuery = searchQuery.replace(/\\s+/g, '');\n if (strippedQuery.length === 0) return [];\n\n const maxErrors = Math.floor(strippedQuery.length * (1 - threshold));\n const matches = fuzzySearchText(searchText, strippedQuery, maxErrors);\n\n return matches.map((m) => mapToSpanRanges(m.start, m.end, charMap));\n}\n","import type { SearchMatch, SpanData, PageData } from '../types';\nimport type { MatchRange } from './SearchEngine';\n\n/**\n * Manages cross-span highlighting using the charMap approach.\n *\n * Algorithm (from demo):\n * 1. Group match ranges by spanIdx\n * 2. For each affected span, replace textContent with a DocumentFragment:\n * - Plain text nodes for non-matching parts\n * - <mark> elements for matching parts\n * 3. Collect marks per match for navigation\n */\nexport class HighlightManager {\n private matches: SearchMatch[] = [];\n private currentMatch = -1;\n private highlightClass: string;\n private activeHighlightClass: string;\n\n constructor(highlightClass: string, activeHighlightClass: string) {\n this.highlightClass = highlightClass;\n this.activeHighlightClass = activeHighlightClass;\n }\n\n /**\n * Apply highlights for all matches on a page.\n * Returns the SearchMatch[] (array of mark groups).\n */\n applyHighlights(\n pageSpans: SpanData[],\n matchRanges: MatchRange[][]\n ): SearchMatch[] {\n if (!matchRanges.length) return [];\n\n // Group ranges by spanIdx, keeping track of which match they belong to\n const spanRanges: Record<\n number,\n { start: number; end: number; matchIdx: number }[]\n > = {};\n\n matchRanges.forEach((range, mi) => {\n range.forEach((r) => {\n if (!spanRanges[r.spanIdx]) spanRanges[r.spanIdx] = [];\n spanRanges[r.spanIdx].push({ start: r.start, end: r.end, matchIdx: mi });\n });\n });\n\n // Collect marks per match\n const matchMarks: HTMLElement[][] = matchRanges.map(() => []);\n\n // For each affected span, rebuild DOM with highlights\n for (const siStr of Object.keys(spanRanges)) {\n const si = parseInt(siStr, 10);\n const s = pageSpans[si];\n const ranges = spanRanges[si].sort((a, b) => a.start - b.start);\n\n const frag = document.createDocumentFragment();\n let last = 0;\n\n for (const r of ranges) {\n const actualStart = Math.max(r.start, last);\n\n // Add plain text before highlight\n if (actualStart > last) {\n frag.appendChild(document.createTextNode(s.text.slice(last, actualStart)));\n }\n\n // Add highlight mark\n if (actualStart < r.end) {\n const mark = document.createElement('mark');\n mark.className = this.highlightClass;\n mark.textContent = s.text.slice(actualStart, r.end);\n frag.appendChild(mark);\n matchMarks[r.matchIdx].push(mark);\n }\n\n last = Math.max(last, r.end);\n }\n\n // Add remaining plain text\n if (last < s.text.length) {\n frag.appendChild(document.createTextNode(s.text.slice(last)));\n }\n\n // Replace span content\n s.el.textContent = '';\n s.el.appendChild(frag);\n }\n\n return matchMarks\n .filter((marks) => marks.length > 0)\n .map((marks) => ({ marks }));\n }\n\n /**\n * Add matches to the global list.\n */\n addMatches(newMatches: SearchMatch[]): void {\n this.matches.push(...newMatches);\n }\n\n /**\n * Clear all highlights and restore original span text.\n */\n clearHighlights(allPageData: PageData[]): void {\n allPageData.forEach((pd) => {\n pd.spans.forEach((s) => {\n s.el.textContent = s.text;\n });\n });\n this.matches = [];\n this.currentMatch = -1;\n }\n\n /**\n * Set active match by index. Applies active CSS class and scrolls into view.\n */\n setActiveMatch(index: number): void {\n // Remove active class from previous\n if (this.currentMatch >= 0 && this.currentMatch < this.matches.length) {\n this.matches[this.currentMatch].marks.forEach((m) =>\n m.classList.remove(this.activeHighlightClass)\n );\n }\n\n this.currentMatch = index;\n\n if (index >= 0 && index < this.matches.length) {\n this.matches[index].marks.forEach((m) =>\n m.classList.add(this.activeHighlightClass)\n );\n // Scroll first mark into view\n this.matches[index].marks[0]?.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n });\n }\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n next(): number {\n if (this.matches.length === 0) return -1;\n const newIdx = (this.currentMatch + 1) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prev(): number {\n if (this.matches.length === 0) return -1;\n const newIdx =\n (this.currentMatch - 1 + this.matches.length) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n getCurrentIndex(): number {\n return this.currentMatch;\n }\n\n getTotal(): number {\n return this.matches.length;\n }\n\n getMatches(): SearchMatch[] {\n return this.matches;\n }\n}\n","import { EventEmitter } from './EventEmitter';\nimport { PDFRenderer } from './PDFRenderer';\nimport { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES, ZOOM_STEP, MIN_SCALE, MAX_SCALE } from './constants';\nimport type {\n PDFSearchViewerOptions,\n SearchOptions,\n PDFSearchViewerEventMap,\n PageData,\n} from '../types';\n\nexport type PDFSource = File | ArrayBuffer | Uint8Array | string;\n\n/**\n * Main PDF viewer with search and highlight functionality.\n *\n * Usage:\n * ```js\n * import * as pdfjsLib from 'pdfjs-dist';\n * import { PDFSearchViewer } from 'pdf-search-highlight';\n *\n * const viewer = new PDFSearchViewer(container, pdfjsLib, {\n * classNames: {\n * page: 'my-page',\n * highlight: 'my-highlight',\n * activeHighlight: 'my-active',\n * }\n * });\n * await viewer.loadPDF(file);\n * viewer.search('hello');\n * viewer.nextMatch();\n * ```\n */\nexport class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {\n private renderer: PDFRenderer;\n private highlightManager: HighlightManager;\n private pageData: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n private destroyed = false;\n\n constructor(\n container: HTMLElement,\n pdfjsLib: any,\n options: PDFSearchViewerOptions = {}\n ) {\n super();\n\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n\n this.renderer = new PDFRenderer(container, options);\n this.renderer.setPdfjsLib(pdfjsLib);\n this.highlightManager = new HighlightManager(\n cls.highlight,\n cls.activeHighlight\n );\n }\n\n /**\n * Load and render a PDF document.\n */\n async loadPDF(source: PDFSource): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n try {\n await this.renderer.loadDocument(source);\n this.pageData = await this.renderer.renderAllPages();\n const pageCount = this.renderer.getPageCount();\n this.emit('load', { pageCount });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', { error, context: 'loadPDF' });\n throw error;\n }\n }\n\n /**\n * Search for text across all pages.\n * Clears previous highlights and creates new ones.\n */\n search(query: string, options: SearchOptions = {}): number {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n // Clear previous highlights\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.emit('search', { query, total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n return 0;\n }\n\n // Search each page and apply highlights\n for (const pd of this.pageData) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n\n // Auto-activate first match\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.emit('search', { query, total });\n this.emit('matchchange', {\n current: total > 0 ? 0 : -1,\n total,\n });\n\n return total;\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n nextMatch(): number {\n const idx = this.highlightManager.next();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prevMatch(): number {\n const idx = this.highlightManager.prev();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Clear all search highlights.\n */\n clearSearch(): void {\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = '';\n this.emit('search', { query: '', total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n }\n\n /** Get the current scale setting. */\n getScale(): number | 'auto' {\n return this.renderer.getScale();\n }\n\n /** Set scale and re-render. Preserves current search state. */\n async setScale(scale: number | 'auto'): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n this.renderer.setScale(scale);\n await this.rerender();\n this.emit('zoom', { scale: this.renderer.getEffectiveScale() });\n }\n\n /** Zoom in by one step. */\n async zoomIn(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.min(current + ZOOM_STEP, MAX_SCALE);\n await this.setScale(newScale);\n }\n\n /** Zoom out by one step. */\n async zoomOut(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.max(current - ZOOM_STEP, MIN_SCALE);\n await this.setScale(newScale);\n }\n\n /** Download the currently loaded PDF. */\n async download(filename?: string): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n await this.renderer.download(filename);\n }\n\n private resolveCurrentScale(): number {\n const s = this.renderer.getScale();\n return s === 'auto' ? this.renderer.getEffectiveScale() : s;\n }\n\n private async rerender(): Promise<void> {\n this.highlightManager.clearHighlights(this.pageData);\n this.pageData = await this.renderer.renderAllPages();\n if (this.lastQuery.trim()) {\n this.search(this.lastQuery, this.lastSearchOptions);\n }\n }\n\n /**\n * Get total number of pages.\n */\n getPageCount(): number {\n return this.renderer.getPageCount();\n }\n\n /**\n * Get current active match index (0-based). -1 if none.\n */\n getCurrentMatchIndex(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /**\n * Get total number of matches.\n */\n getMatchCount(): number {\n return this.highlightManager.getTotal();\n }\n\n /**\n * Destroy the viewer, release all resources.\n */\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.highlightManager.clearHighlights(this.pageData);\n this.renderer.cleanup();\n this.removeAllListeners();\n this.pageData = [];\n }\n}\n","import { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES } from './constants';\nimport type { SearchOptions, ClassNames, PageData } from '../types';\n\nexport interface SearchControllerOptions {\n classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;\n}\n\n/**\n * Headless search + highlight controller.\n * Does NOT render PDF — works with any PageData[] you provide.\n *\n * Use this when you want full control over:\n * - Where the PDF is rendered\n * - Where the search UI lives\n * - How search results are displayed\n *\n * Usage:\n * ```js\n * import { PDFRenderer, SearchController } from 'pdf-search-highlight';\n *\n * // Render PDF wherever you want\n * const renderer = new PDFRenderer(pdfContainer, pdfjsLib, {});\n * const pages = await renderer.renderAllPages();\n *\n * // Search controller — no UI, just logic\n * const search = new SearchController();\n * search.setPages(pages);\n *\n * // Wire up your own UI\n * input.oninput = () => search.search(input.value);\n * nextBtn.onclick = () => search.next();\n * prevBtn.onclick = () => search.prev();\n *\n * // React to changes\n * search.onChange = ({ current, total }) => {\n * label.textContent = total > 0 ? `${current + 1}/${total}` : '';\n * };\n * ```\n */\nexport class SearchController {\n private highlightManager: HighlightManager;\n private pages: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n\n /** Callback fired when match state changes (search, next, prev, clear). */\n onChange: ((state: { current: number; total: number; query: string }) => void) | null = null;\n\n constructor(options: SearchControllerOptions = {}) {\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n this.highlightManager = new HighlightManager(cls.highlight, cls.activeHighlight);\n }\n\n /**\n * Set the pages to search on.\n * Call this after rendering PDF pages.\n */\n setPages(pages: PageData[]): void {\n const savedQuery = this.lastQuery;\n const savedOptions = this.lastSearchOptions;\n this.clear();\n this.pages = pages;\n // Re-apply search if there was an active query (e.g. after zoom)\n if (savedQuery.trim()) {\n this.search(savedQuery, savedOptions);\n }\n }\n\n /**\n * Search for text across all pages.\n * Returns total number of matches.\n */\n search(query: string, options: SearchOptions = {}): number {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.notify();\n return 0;\n }\n\n for (const pd of this.pages) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.notify();\n return total;\n }\n\n /** Navigate to next match. Returns new index. */\n next(): number {\n const idx = this.highlightManager.next();\n this.notify();\n return idx;\n }\n\n /** Navigate to previous match. Returns new index. */\n prev(): number {\n const idx = this.highlightManager.prev();\n this.notify();\n return idx;\n }\n\n /** Go to a specific match by index. */\n goTo(index: number): void {\n this.highlightManager.setActiveMatch(index);\n this.notify();\n }\n\n /** Clear all highlights. */\n clear(): void {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = '';\n this.notify();\n }\n\n /** Current match index (0-based). -1 if none. */\n get current(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /** Total number of matches. */\n get total(): number {\n return this.highlightManager.getTotal();\n }\n\n /** Last searched query. */\n get query(): string {\n return this.lastQuery;\n }\n\n private notify(): void {\n this.onChange?.({\n current: this.highlightManager.getCurrentIndex(),\n total: this.highlightManager.getTotal(),\n query: this.lastQuery,\n });\n }\n}\n"],"mappings":";AAEO,IAAM,eAAN,MAAgE;AAAA,EAAhE;AACL,SAAQ,YAAY,oBAAI,IAAwC;AAAA;AAAA,EAEhE,GAA6B,OAAU,UAAuC;AAC5E,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,IAAI,CAAC;AAAA,IACrC;AACA,SAAK,UAAU,IAAI,KAAK,EAAG,IAAI,QAAQ;AACvC,WAAO;AAAA,EACT;AAAA,EAEA,IAA8B,OAAU,UAAuC;AAC7E,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,QAAQ;AAC1C,WAAO;AAAA,EACT;AAAA,EAEU,KAA+B,OAAU,MAAyB;AAC1E,SAAK,UAAU,IAAI,KAAK,GAAG,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,EACrD;AAAA,EAEA,qBAA2B;AACzB,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;;;ACvBO,IAAM,sBAA4C;AAAA,EACvD,WAAW;AAAA,EACX,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,iBAAiB;AACnB;AAEO,IAAM,gBAAgB;AACtB,IAAM,mBAAmB;AAEzB,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,YAAY;;;ACDlB,IAAM,cAAN,MAAkB;AAAA,EAWvB,YAAY,WAAwB,SAAiC;AALrE,SAAQ,SAAkC;AAC1C,SAAQ,WAAuB,CAAC;AAChC,SAAQ,WAAgB;AACxB,SAAQ,iBAAyB;AAG/B,SAAK,YAAY;AACjB,SAAK,QAAQ,QAAQ,SAAS;AAC9B,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,YAAY,QAAQ;AACzB,SAAK,MAAM,EAAE,GAAG,qBAAqB,GAAG,QAAQ,WAAW;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,KAAgB;AAC1B,SAAK,WAAW;AAChB,QAAI,KAAK,WAAW;AAClB,UAAI,oBAAoB,YAAY,KAAK;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aACJ,QACiB;AACjB,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,QAAQ;AAEb,QAAI;AACJ,QAAI,kBAAkB,MAAM;AAC1B,aAAO,MAAM,OAAO,YAAY;AAAA,IAClC,WAAW,OAAO,WAAW,UAAU;AACrC,aAAO,EAAE,KAAK,OAAO;AAAA,IACvB,OAAO;AACL,aAAO;AAAA,IACT;AAEA,UAAM,cAAc,KAAK,SAAS,YAAY,EAAE,KAAK,CAAC;AACtD,SAAK,SAAS,MAAM,YAAY;AAChC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,iBAAsC;AAC1C,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,wBAAwB;AAG1D,UAAM,gBAAgB,KAAK,UAAU;AACrC,UAAM,mBAAmB,KAAK,UAAU,gBAAgB;AACxD,UAAM,cAAc,gBAAgB;AAEpC,SAAK,UAAU,YAAY;AAC3B,SAAK,UAAU,UAAU,IAAI,KAAK,IAAI,SAAS;AAC/C,SAAK,WAAW,CAAC;AAEjB,UAAM,WAAW,KAAK,OAAO;AAE7B,aAAS,IAAI,GAAG,KAAK,UAAU,KAAK;AAClC,YAAM,OAAO,MAAM,KAAK,OAAO,QAAQ,CAAC;AACxC,YAAM,KAAK,MAAM,KAAK,WAAW,MAAM,GAAG,QAAQ;AAClD,WAAK,SAAS,KAAK,EAAE;AAAA,IACvB;AAGA,QAAI,gBAAgB,GAAG;AACrB,WAAK,UAAU,YAAY,cAAc,KAAK,UAAU;AAAA,IAC1D;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,WACZ,MACA,SACA,YACmB;AACnB,UAAM,QAAQ,KAAK,eAAe,IAAI;AACtC,QAAI,YAAY,EAAG,MAAK,iBAAiB;AACzC,UAAM,KAAK,KAAK,YAAY,EAAE,MAAM,CAAC;AAGrC,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY,KAAK,IAAI;AAC/B,cAAU,MAAM,WAAW;AAC3B,cAAU,MAAM,QAAQ,GAAG,QAAQ;AACnC,cAAU,MAAM,SAAS,GAAG,SAAS;AACrC,cAAU,MAAM,SAAS;AACzB,cAAU,MAAM,eAAe,KAAK,UAAU;AAC9C,cAAU,MAAM,WAAW;AAC3B,cAAU,QAAQ,OAAO,OAAO,OAAO;AAGvC,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,YAAY,KAAK,IAAI;AAC5B,WAAO,QAAQ,GAAG,QAAQ;AAC1B,WAAO,SAAS,GAAG,SAAS;AAC5B,WAAO,MAAM,QAAQ,GAAG,QAAQ;AAChC,WAAO,MAAM,SAAS,GAAG,SAAS;AAClC,WAAO,MAAM,UAAU;AACvB,UAAM,MAAM,OAAO,WAAW,IAAI;AAClC,QAAI,MAAM,GAAG,CAAC;AACd,UAAM,KAAK,OAAO,EAAE,eAAe,KAAK,UAAU,GAAG,CAAC,EAAE;AAGxD,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY,KAAK,IAAI;AAC/B,cAAU,MAAM,WAAW;AAC3B,cAAU,MAAM,MAAM;AACtB,cAAU,MAAM,OAAO;AACvB,cAAU,MAAM,QAAQ;AACxB,cAAU,MAAM,SAAS;AACzB,cAAU,MAAM,WAAW;AAC3B,cAAU,MAAM,aAAa;AAE7B,UAAM,KAAK,MAAM,KAAK,eAAe;AACrC,UAAM,QAAoB,CAAC;AAE3B,eAAW,QAAQ,GAAG,OAAO;AAC3B,UAAI,CAAC,KAAK,OAAO,CAAC,KAAK,OAAQ;AAE/B,YAAM,KAAK,KAAK,SAAS,KAAK,UAAU,GAAG,WAAW,KAAK,SAAS;AACpE,YAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,WAAK,cAAc,KAAK,OAAO;AAC/B,YAAM,KAAK,KAAK,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;AAClC,WAAK,MAAM,WAAW;AACtB,WAAK,MAAM,OAAO,GAAG,CAAC,IAAI;AAC1B,WAAK,MAAM,MAAO,GAAG,CAAC,IAAI,KAAM;AAChC,WAAK,MAAM,WAAW,KAAK;AAC3B,WAAK,MAAM,QAAQ;AACnB,WAAK,MAAM,aAAa;AACxB,WAAK,MAAM,SAAS;AACpB,WAAK,MAAM,kBAAkB;AAC7B,UAAI,KAAK,SAAU,MAAK,MAAM,aAAa,KAAK;AAEhD,YAAM,KAAK,GAAG,CAAC,IAAI;AACnB,UAAI,KAAK,IAAI,KAAK,CAAC,IAAI,MAAM;AAC3B,aAAK,MAAM,YAAY,UAAU,EAAE;AAAA,MACrC;AAEA,gBAAU,YAAY,IAAI;AAC1B,YAAM,KAAK;AAAA,QACT,IAAI;AAAA,QACJ,MAAM,KAAK,OAAO;AAAA,QAClB,QAAQ,CAAC,CAAC,KAAK;AAAA,MACjB,CAAC;AAAA,IACH;AAEA,cAAU,YAAY,MAAM;AAC5B,cAAU,YAAY,SAAS;AAC/B,SAAK,UAAU,YAAY,SAAS;AAGpC,UAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,cAAc,QAAQ,OAAO,MAAM,UAAU;AACnD,SAAK,UAAU,YAAY,KAAK;AAEhC,WAAO,EAAE,WAAW,MAAM;AAAA,EAC5B;AAAA,EAEQ,eAAe,MAA4B;AACjD,QAAI,KAAK,UAAU,UAAU,OAAO,KAAK,UAAU,UAAU;AAC3D,aAAO,KAAK;AAAA,IACd;AACA,UAAM,YAAY,KAAK,YAAY,EAAE,OAAO,EAAE,CAAC;AAC/C,UAAM,iBAAiB,KAAK,UAAU,eAAe;AACrD,WAAO,KAAK,IAAI,iBAAiB,UAAU,OAAO,CAAC;AAAA,EACrD;AAAA;AAAA,EAGA,SAAS,OAA8B;AACrC,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,WAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,WAAmB,gBAA+B;AAC/D,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,wBAAwB;AAE1D,UAAM,OAAO,MAAM,KAAK,OAAO,QAAQ;AACvC,UAAM,OAAO,IAAI,KAAK,CAAC,IAAgB,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACrE,UAAM,MAAM,IAAI,gBAAgB,IAAI;AAEpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW;AACb,aAAS,KAAK,YAAY,CAAC;AAC3B,MAAE,MAAM;AACR,aAAS,KAAK,YAAY,CAAC;AAC3B,QAAI,gBAAgB,GAAG;AAAA,EACzB;AAAA,EAEA,gBAAsC;AACpC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,cAA0B;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,eAAuB;AACrB,WAAO,KAAK,QAAQ,YAAY;AAAA,EAClC;AAAA,EAEA,UAAgB;AACd,SAAK,QAAQ,QAAQ;AACrB,SAAK,SAAS;AACd,SAAK,WAAW,CAAC;AACjB,SAAK,UAAU,YAAY;AAAA,EAC7B;AACF;;;AC/OA,SAAS,YAAY,GAAmB;AACtC,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAcA,SAAS,mBACP,OACA,SACe;AACf,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,kBAAkB,QAAQ,iBAAiB;AACjD,QAAM,qBAAqB,QAAQ,sBAAsB;AAEzD,MAAI,CAAC,oBAAoB;AAEvB,UAAMA,WAAU,YAAY,OAAO;AACnC,WAAO,IAAI,OAAOA,UAAS,kBAAkB,MAAM,IAAI;AAAA,EACzD;AAGA,QAAM,QAAQ,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,CAAC;AACtD,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,MAAI,MAAM,SAAS,KAAK;AAEtB,UAAM,SAAS,QAAQ,MAAM,KAAK;AAClC,UAAMA,WAAU,OAAO,IAAI,CAAC,MAAM,YAAY,CAAC,CAAC,EAAE,KAAK,MAAM;AAC7D,WAAO,IAAI,OAAOA,UAAS,kBAAkB,MAAM,IAAI;AAAA,EACzD;AAGA,QAAM,UAAU,MAAM,IAAI,CAAC,MAAM,YAAY,CAAC,CAAC,EAAE,KAAK,MAAM;AAC5D,SAAO,IAAI,OAAO,SAAS,kBAAkB,MAAM,IAAI;AACzD;AAiBA,SAAS,gBACP,MACA,OACA,WACc;AACd,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,MAAM;AAChB,MAAI,MAAM,EAAG,QAAO,CAAC;AACrB,MAAI,MAAM,EAAG,QAAO,CAAC;AAIrB,MAAI,OAAO,IAAI,YAAY,IAAI,CAAC;AAChC,WAAS,IAAI,GAAG,KAAK,GAAG,IAAK,MAAK,CAAC,IAAI;AAGvC,QAAM,UAAyB,CAAC,KAAK,MAAM,CAAC;AAG5C,QAAM,eAAyD,CAAC;AAEhE,WAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,UAAM,OAAO,IAAI,YAAY,IAAI,CAAC;AAClC,SAAK,CAAC,IAAI;AACV,aAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,YAAM,OAAO,KAAK,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,IAAI;AAChD,WAAK,CAAC,IAAI,KAAK;AAAA,QACb,KAAK,CAAC,IAAI;AAAA;AAAA,QACV,KAAK,IAAI,CAAC,IAAI;AAAA;AAAA,QACd,KAAK,IAAI,CAAC,IAAI;AAAA;AAAA,MAChB;AAAA,IACF;AACA,YAAQ,KAAK,KAAK,MAAM,CAAC;AAEzB,QAAI,KAAK,CAAC,KAAK,WAAW;AACxB,mBAAa,KAAK,EAAE,KAAK,GAAG,UAAU,KAAK,CAAC,EAAE,CAAC;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,aAAa,WAAW,EAAG,QAAO,CAAC;AAGvC,QAAM,aAA2B,CAAC;AAClC,aAAW,EAAE,KAAK,QAAQ,SAAS,KAAK,cAAc;AAEpD,QAAI,IAAI;AACR,QAAI,IAAI;AACR,WAAO,IAAI,KAAK,IAAI,GAAG;AACrB,YAAM,IAAI,QAAQ,CAAC;AACnB,YAAM,IAAI,QAAQ,IAAI,CAAC;AACvB,YAAM,OAAO,KAAK,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,IAAI;AAChD,UAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,MAAM;AAE5B;AACA;AAAA,MACF,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,GAAG;AAE5B;AAAA,MACF,OAAO;AAEL;AAAA,MACF;AAAA,IACF;AACA,eAAW,KAAK,EAAE,OAAO,GAAG,KAAK,QAAQ,SAAS,CAAC;AAAA,EACrD;AAGA,MAAI,WAAW,WAAW,EAAG,QAAO,CAAC;AACrC,aAAW,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ;AAEtE,QAAM,SAAuB,CAAC,WAAW,CAAC,CAAC;AAC3C,WAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,UAAMC,QAAO,OAAO,OAAO,SAAS,CAAC;AACrC,UAAM,OAAO,WAAW,CAAC;AACzB,QAAI,KAAK,QAAQA,MAAK,KAAK;AAEzB,UAAI,KAAK,WAAWA,MAAK,UAAU;AACjC,eAAO,OAAO,SAAS,CAAC,IAAI;AAAA,MAC9B;AAAA,IACF,OAAO;AACL,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,oBAAoB,OAAmB;AAC9C,MAAI,WAAW;AACf,QAAM,UAA0B,CAAC;AACjC,QAAM,QAAQ,CAAC,GAAG,OAAO;AACvB,aAAS,KAAK,GAAG,KAAK,EAAE,KAAK,QAAQ,MAAM;AACzC,cAAQ,KAAK,EAAE,SAAS,IAAI,SAAS,GAAG,CAAC;AACzC,kBAAY,EAAE,KAAK,EAAE;AAAA,IACvB;AAAA,EACF,CAAC;AACD,SAAO,EAAE,UAAU,QAAQ;AAC7B;AAKA,SAAS,gBACP,OACA,KACA,SACc;AACd,QAAM,QAAsB,CAAC;AAC7B,WAAS,IAAI,OAAO,IAAI,KAAK,KAAK;AAChC,UAAM,KAAK,QAAQ,CAAC;AACpB,UAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAI,QAAQ,KAAK,YAAY,GAAG,WAAW,KAAK,QAAQ,GAAG,SAAS;AAClE,WAAK,MAAM,GAAG,UAAU;AAAA,IAC1B,OAAO;AACL,YAAM,KAAK,EAAE,SAAS,GAAG,SAAS,OAAO,GAAG,SAAS,KAAK,GAAG,UAAU,EAAE,CAAC;AAAA,IAC5E;AAAA,EACF;AACA,SAAO;AACT;AAWO,SAAS,WACd,OACA,OACA,UAAyB,CAAC,GACV;AAChB,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,MAAI,QAAQ,OAAO;AACjB,WAAO,gBAAgB,OAAO,SAAS,OAAO;AAAA,EAChD;AAEA,QAAM,QAAQ,mBAAmB,OAAO,OAAO;AAC/C,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,EAAE,UAAU,QAAQ,IAAI,oBAAoB,KAAK;AAGvD,QAAM,iBAAiC,CAAC;AACxC,MAAI;AACJ,QAAM,YAAY;AAElB,UAAQ,IAAI,MAAM,KAAK,QAAQ,OAAO,MAAM;AAC1C,mBAAe,KAAK,gBAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,OAAO,CAAC;AAC5E,QAAI,EAAE,CAAC,EAAE,WAAW,EAAG,OAAM;AAAA,EAC/B;AAEA,SAAO;AACT;AAEA,SAAS,gBACP,OACA,OACA,SACgB;AAChB,QAAM,EAAE,UAAU,QAAQ,IAAI,oBAAoB,KAAK;AACvD,MAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAEnC,QAAM,kBAAkB,QAAQ,iBAAiB;AACjD,QAAM,YAAY,QAAQ,kBAAkB;AAE5C,QAAM,aAAa,kBAAkB,WAAW,SAAS,YAAY;AACrE,QAAM,cAAc,kBAAkB,QAAQ,MAAM,YAAY;AAGhE,QAAM,gBAAgB,YAAY,QAAQ,QAAQ,EAAE;AACpD,MAAI,cAAc,WAAW,EAAG,QAAO,CAAC;AAExC,QAAM,YAAY,KAAK,MAAM,cAAc,UAAU,IAAI,UAAU;AACnE,QAAM,UAAU,gBAAgB,YAAY,eAAe,SAAS;AAEpE,SAAO,QAAQ,IAAI,CAAC,MAAM,gBAAgB,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AACpE;;;AC7PO,IAAM,mBAAN,MAAuB;AAAA,EAM5B,YAAY,gBAAwB,sBAA8B;AALlE,SAAQ,UAAyB,CAAC;AAClC,SAAQ,eAAe;AAKrB,SAAK,iBAAiB;AACtB,SAAK,uBAAuB;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBACE,WACA,aACe;AACf,QAAI,CAAC,YAAY,OAAQ,QAAO,CAAC;AAGjC,UAAM,aAGF,CAAC;AAEL,gBAAY,QAAQ,CAAC,OAAO,OAAO;AACjC,YAAM,QAAQ,CAAC,MAAM;AACnB,YAAI,CAAC,WAAW,EAAE,OAAO,EAAG,YAAW,EAAE,OAAO,IAAI,CAAC;AACrD,mBAAW,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,KAAK,UAAU,GAAG,CAAC;AAAA,MACzE,CAAC;AAAA,IACH,CAAC;AAGD,UAAM,aAA8B,YAAY,IAAI,MAAM,CAAC,CAAC;AAG5D,eAAW,SAAS,OAAO,KAAK,UAAU,GAAG;AAC3C,YAAM,KAAK,SAAS,OAAO,EAAE;AAC7B,YAAM,IAAI,UAAU,EAAE;AACtB,YAAM,SAAS,WAAW,EAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAE9D,YAAM,OAAO,SAAS,uBAAuB;AAC7C,UAAI,OAAO;AAEX,iBAAW,KAAK,QAAQ;AACtB,cAAM,cAAc,KAAK,IAAI,EAAE,OAAO,IAAI;AAG1C,YAAI,cAAc,MAAM;AACtB,eAAK,YAAY,SAAS,eAAe,EAAE,KAAK,MAAM,MAAM,WAAW,CAAC,CAAC;AAAA,QAC3E;AAGA,YAAI,cAAc,EAAE,KAAK;AACvB,gBAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,eAAK,YAAY,KAAK;AACtB,eAAK,cAAc,EAAE,KAAK,MAAM,aAAa,EAAE,GAAG;AAClD,eAAK,YAAY,IAAI;AACrB,qBAAW,EAAE,QAAQ,EAAE,KAAK,IAAI;AAAA,QAClC;AAEA,eAAO,KAAK,IAAI,MAAM,EAAE,GAAG;AAAA,MAC7B;AAGA,UAAI,OAAO,EAAE,KAAK,QAAQ;AACxB,aAAK,YAAY,SAAS,eAAe,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC;AAAA,MAC9D;AAGA,QAAE,GAAG,cAAc;AACnB,QAAE,GAAG,YAAY,IAAI;AAAA,IACvB;AAEA,WAAO,WACJ,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC,EAClC,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,YAAiC;AAC1C,SAAK,QAAQ,KAAK,GAAG,UAAU;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,aAA+B;AAC7C,gBAAY,QAAQ,CAAC,OAAO;AAC1B,SAAG,MAAM,QAAQ,CAAC,MAAM;AACtB,UAAE,GAAG,cAAc,EAAE;AAAA,MACvB,CAAC;AAAA,IACH,CAAC;AACD,SAAK,UAAU,CAAC;AAChB,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,OAAqB;AAElC,QAAI,KAAK,gBAAgB,KAAK,KAAK,eAAe,KAAK,QAAQ,QAAQ;AACrE,WAAK,QAAQ,KAAK,YAAY,EAAE,MAAM;AAAA,QAAQ,CAAC,MAC7C,EAAE,UAAU,OAAO,KAAK,oBAAoB;AAAA,MAC9C;AAAA,IACF;AAEA,SAAK,eAAe;AAEpB,QAAI,SAAS,KAAK,QAAQ,KAAK,QAAQ,QAAQ;AAC7C,WAAK,QAAQ,KAAK,EAAE,MAAM;AAAA,QAAQ,CAAC,MACjC,EAAE,UAAU,IAAI,KAAK,oBAAoB;AAAA,MAC3C;AAEA,WAAK,QAAQ,KAAK,EAAE,MAAM,CAAC,GAAG,eAAe;AAAA,QAC3C,UAAU;AAAA,QACV,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe;AACb,QAAI,KAAK,QAAQ,WAAW,EAAG,QAAO;AACtC,UAAM,UAAU,KAAK,eAAe,KAAK,KAAK,QAAQ;AACtD,SAAK,eAAe,MAAM;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe;AACb,QAAI,KAAK,QAAQ,WAAW,EAAG,QAAO;AACtC,UAAM,UACH,KAAK,eAAe,IAAI,KAAK,QAAQ,UAAU,KAAK,QAAQ;AAC/D,SAAK,eAAe,MAAM;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,aAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AACF;;;ACzIO,IAAM,kBAAN,cAA8B,aAAsC;AAAA,EAQzE,YACE,WACA,UACA,UAAkC,CAAC,GACnC;AACA,UAAM;AAVR,SAAQ,WAAuB,CAAC;AAChC,SAAQ,YAAY;AACpB,SAAQ,oBAAmC,CAAC;AAC5C,SAAQ,YAAY;AASlB,UAAM,MAAM,EAAE,GAAG,qBAAqB,GAAG,QAAQ,WAAW;AAE5D,SAAK,WAAW,IAAI,YAAY,WAAW,OAAO;AAClD,SAAK,SAAS,YAAY,QAAQ;AAClC,SAAK,mBAAmB,IAAI;AAAA,MAC1B,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ,QAAkC;AAC9C,QAAI,KAAK,UAAW,OAAM,IAAI,MAAM,oCAAoC;AAExE,QAAI;AACF,YAAM,KAAK,SAAS,aAAa,MAAM;AACvC,WAAK,WAAW,MAAM,KAAK,SAAS,eAAe;AACnD,YAAM,YAAY,KAAK,SAAS,aAAa;AAC7C,WAAK,KAAK,QAAQ,EAAE,UAAU,CAAC;AAAA,IACjC,SAAS,KAAK;AACZ,YAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,KAAK,SAAS,EAAE,OAAO,SAAS,UAAU,CAAC;AAChD,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,OAAe,UAAyB,CAAC,GAAW;AACzD,QAAI,KAAK,UAAW,OAAM,IAAI,MAAM,oCAAoC;AAGxE,SAAK,iBAAiB,gBAAgB,KAAK,QAAQ;AACnD,SAAK,YAAY;AACjB,SAAK,oBAAoB;AAEzB,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,SAAS;AACZ,WAAK,KAAK,UAAU,EAAE,OAAO,OAAO,EAAE,CAAC;AACvC,WAAK,KAAK,eAAe,EAAE,SAAS,IAAI,OAAO,EAAE,CAAC;AAClD,aAAO;AAAA,IACT;AAGA,eAAW,MAAM,KAAK,UAAU;AAC9B,YAAM,cAAc,WAAW,GAAG,OAAO,SAAS,OAAO;AACzD,YAAM,UAAU,KAAK,iBAAiB,gBAAgB,GAAG,OAAO,WAAW;AAC3E,WAAK,iBAAiB,WAAW,OAAO;AAAA,IAC1C;AAEA,UAAM,QAAQ,KAAK,iBAAiB,SAAS;AAG7C,QAAI,QAAQ,GAAG;AACb,WAAK,iBAAiB,eAAe,CAAC;AAAA,IACxC;AAEA,SAAK,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC;AACpC,SAAK,KAAK,eAAe;AAAA,MACvB,SAAS,QAAQ,IAAI,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAoB;AAClB,UAAM,MAAM,KAAK,iBAAiB,KAAK;AACvC,SAAK,KAAK,eAAe;AAAA,MACvB,SAAS;AAAA,MACT,OAAO,KAAK,iBAAiB,SAAS;AAAA,IACxC,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAoB;AAClB,UAAM,MAAM,KAAK,iBAAiB,KAAK;AACvC,SAAK,KAAK,eAAe;AAAA,MACvB,SAAS;AAAA,MACT,OAAO,KAAK,iBAAiB,SAAS;AAAA,IACxC,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,cAAoB;AAClB,SAAK,iBAAiB,gBAAgB,KAAK,QAAQ;AACnD,SAAK,YAAY;AACjB,SAAK,KAAK,UAAU,EAAE,OAAO,IAAI,OAAO,EAAE,CAAC;AAC3C,SAAK,KAAK,eAAe,EAAE,SAAS,IAAI,OAAO,EAAE,CAAC;AAAA,EACpD;AAAA;AAAA,EAGA,WAA4B;AAC1B,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC;AAAA;AAAA,EAGA,MAAM,SAAS,OAAuC;AACpD,QAAI,KAAK,UAAW,OAAM,IAAI,MAAM,oCAAoC;AACxE,SAAK,SAAS,SAAS,KAAK;AAC5B,UAAM,KAAK,SAAS;AACpB,SAAK,KAAK,QAAQ,EAAE,OAAO,KAAK,SAAS,kBAAkB,EAAE,CAAC;AAAA,EAChE;AAAA;AAAA,EAGA,MAAM,SAAwB;AAC5B,UAAM,UAAU,KAAK,oBAAoB;AACzC,UAAM,WAAW,KAAK,IAAI,UAAU,WAAW,SAAS;AACxD,UAAM,KAAK,SAAS,QAAQ;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,UAAM,UAAU,KAAK,oBAAoB;AACzC,UAAM,WAAW,KAAK,IAAI,UAAU,WAAW,SAAS;AACxD,UAAM,KAAK,SAAS,QAAQ;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAM,SAAS,UAAkC;AAC/C,QAAI,KAAK,UAAW,OAAM,IAAI,MAAM,oCAAoC;AACxE,UAAM,KAAK,SAAS,SAAS,QAAQ;AAAA,EACvC;AAAA,EAEQ,sBAA8B;AACpC,UAAM,IAAI,KAAK,SAAS,SAAS;AACjC,WAAO,MAAM,SAAS,KAAK,SAAS,kBAAkB,IAAI;AAAA,EAC5D;AAAA,EAEA,MAAc,WAA0B;AACtC,SAAK,iBAAiB,gBAAgB,KAAK,QAAQ;AACnD,SAAK,WAAW,MAAM,KAAK,SAAS,eAAe;AACnD,QAAI,KAAK,UAAU,KAAK,GAAG;AACzB,WAAK,OAAO,KAAK,WAAW,KAAK,iBAAiB;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAuB;AACrB,WAAO,KAAK,SAAS,aAAa;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,uBAA+B;AAC7B,WAAO,KAAK,iBAAiB,gBAAgB;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAwB;AACtB,WAAO,KAAK,iBAAiB,SAAS;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,iBAAiB,gBAAgB,KAAK,QAAQ;AACnD,SAAK,SAAS,QAAQ;AACtB,SAAK,mBAAmB;AACxB,SAAK,WAAW,CAAC;AAAA,EACnB;AACF;;;AC9LO,IAAM,mBAAN,MAAuB;AAAA,EAS5B,YAAY,UAAmC,CAAC,GAAG;AAPnD,SAAQ,QAAoB,CAAC;AAC7B,SAAQ,YAAY;AACpB,SAAQ,oBAAmC,CAAC;AAG5C;AAAA,oBAAwF;AAGtF,UAAM,MAAM,EAAE,GAAG,qBAAqB,GAAG,QAAQ,WAAW;AAC5D,SAAK,mBAAmB,IAAI,iBAAiB,IAAI,WAAW,IAAI,eAAe;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,OAAyB;AAChC,UAAM,aAAa,KAAK;AACxB,UAAM,eAAe,KAAK;AAC1B,SAAK,MAAM;AACX,SAAK,QAAQ;AAEb,QAAI,WAAW,KAAK,GAAG;AACrB,WAAK,OAAO,YAAY,YAAY;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,OAAe,UAAyB,CAAC,GAAW;AACzD,SAAK,iBAAiB,gBAAgB,KAAK,KAAK;AAChD,SAAK,YAAY;AACjB,SAAK,oBAAoB;AAEzB,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,SAAS;AACZ,WAAK,OAAO;AACZ,aAAO;AAAA,IACT;AAEA,eAAW,MAAM,KAAK,OAAO;AAC3B,YAAM,cAAc,WAAW,GAAG,OAAO,SAAS,OAAO;AACzD,YAAM,UAAU,KAAK,iBAAiB,gBAAgB,GAAG,OAAO,WAAW;AAC3E,WAAK,iBAAiB,WAAW,OAAO;AAAA,IAC1C;AAEA,UAAM,QAAQ,KAAK,iBAAiB,SAAS;AAC7C,QAAI,QAAQ,GAAG;AACb,WAAK,iBAAiB,eAAe,CAAC;AAAA,IACxC;AAEA,SAAK,OAAO;AACZ,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAe;AACb,UAAM,MAAM,KAAK,iBAAiB,KAAK;AACvC,SAAK,OAAO;AACZ,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAe;AACb,UAAM,MAAM,KAAK,iBAAiB,KAAK;AACvC,SAAK,OAAO;AACZ,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,KAAK,OAAqB;AACxB,SAAK,iBAAiB,eAAe,KAAK;AAC1C,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,iBAAiB,gBAAgB,KAAK,KAAK;AAChD,SAAK,YAAY;AACjB,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,UAAkB;AACpB,WAAO,KAAK,iBAAiB,gBAAgB;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,iBAAiB,SAAS;AAAA,EACxC;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,SAAe;AACrB,SAAK,WAAW;AAAA,MACd,SAAS,KAAK,iBAAiB,gBAAgB;AAAA,MAC/C,OAAO,KAAK,iBAAiB,SAAS;AAAA,MACtC,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AACF;","names":["pattern","prev"]}
@@ -85,10 +85,14 @@ var PDFRenderer = class {
85
85
  }
86
86
  /**
87
87
  * Render all pages into the container.
88
+ * Preserves scroll position across re-renders (e.g. zoom).
88
89
  * Returns PageData[] for search/highlight.
89
90
  */
90
91
  async renderAllPages() {
91
92
  if (!this.pdfDoc) throw new Error("No PDF document loaded");
93
+ const prevScrollTop = this.container.scrollTop;
94
+ const prevScrollHeight = this.container.scrollHeight || 1;
95
+ const scrollRatio = prevScrollTop / prevScrollHeight;
92
96
  this.container.innerHTML = "";
93
97
  this.container.classList.add(this.cls.container);
94
98
  this.pageData = [];
@@ -98,6 +102,9 @@ var PDFRenderer = class {
98
102
  const pd = await this.renderPage(page, i, numPages);
99
103
  this.pageData.push(pd);
100
104
  }
105
+ if (prevScrollTop > 0) {
106
+ this.container.scrollTop = scrollRatio * this.container.scrollHeight;
107
+ }
101
108
  return this.pageData;
102
109
  }
103
110
  async renderPage(page, pageNum, totalPages) {
@@ -245,9 +252,72 @@ function buildFlexibleRegex(query, options) {
245
252
  const pattern = chars.map((c) => escapeRegex(c)).join("\\s*");
246
253
  return new RegExp(pattern, isCaseSensitive ? "g" : "gi");
247
254
  }
248
- function searchPage(spans, query, options = {}) {
249
- const regex = buildFlexibleRegex(query, options);
250
- if (!regex) return [];
255
+ function fuzzySearchText(text, query, maxErrors) {
256
+ const n = text.length;
257
+ const m = query.length;
258
+ if (m === 0) return [];
259
+ if (n === 0) return [];
260
+ let prev = new Uint32Array(m + 1);
261
+ for (let j = 0; j <= m; j++) prev[j] = j;
262
+ const columns = [prev.slice()];
263
+ const endPositions = [];
264
+ for (let i = 1; i <= n; i++) {
265
+ const curr = new Uint32Array(m + 1);
266
+ curr[0] = 0;
267
+ for (let j = 1; j <= m; j++) {
268
+ const cost = text[i - 1] === query[j - 1] ? 0 : 1;
269
+ curr[j] = Math.min(
270
+ prev[j] + 1,
271
+ // deletion
272
+ curr[j - 1] + 1,
273
+ // insertion
274
+ prev[j - 1] + cost
275
+ // substitution
276
+ );
277
+ }
278
+ columns.push(curr.slice());
279
+ if (curr[m] <= maxErrors) {
280
+ endPositions.push({ col: i, distance: curr[m] });
281
+ }
282
+ prev = curr;
283
+ }
284
+ if (endPositions.length === 0) return [];
285
+ const rawMatches = [];
286
+ for (const { col: endCol, distance } of endPositions) {
287
+ let j = m;
288
+ let i = endCol;
289
+ while (j > 0 && i > 0) {
290
+ const c = columns[i];
291
+ const p = columns[i - 1];
292
+ const cost = text[i - 1] === query[j - 1] ? 0 : 1;
293
+ if (c[j] === p[j - 1] + cost) {
294
+ i--;
295
+ j--;
296
+ } else if (c[j] === p[j] + 1) {
297
+ i--;
298
+ } else {
299
+ j--;
300
+ }
301
+ }
302
+ rawMatches.push({ start: i, end: endCol, distance });
303
+ }
304
+ if (rawMatches.length === 0) return [];
305
+ rawMatches.sort((a, b) => a.start - b.start || a.distance - b.distance);
306
+ const merged = [rawMatches[0]];
307
+ for (let i = 1; i < rawMatches.length; i++) {
308
+ const prev2 = merged[merged.length - 1];
309
+ const curr = rawMatches[i];
310
+ if (curr.start < prev2.end) {
311
+ if (curr.distance < prev2.distance) {
312
+ merged[merged.length - 1] = curr;
313
+ }
314
+ } else {
315
+ merged.push(curr);
316
+ }
317
+ }
318
+ return merged;
319
+ }
320
+ function buildTextAndCharMap(spans) {
251
321
  let fullText = "";
252
322
  const charMap = [];
253
323
  spans.forEach((s, si) => {
@@ -256,27 +326,52 @@ function searchPage(spans, query, options = {}) {
256
326
  fullText += s.text[ci];
257
327
  }
258
328
  });
329
+ return { fullText, charMap };
330
+ }
331
+ function mapToSpanRanges(start, end, charMap) {
332
+ const range = [];
333
+ for (let k = start; k < end; k++) {
334
+ const cm = charMap[k];
335
+ const last = range[range.length - 1];
336
+ if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {
337
+ last.end = cm.charIdx + 1;
338
+ } else {
339
+ range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });
340
+ }
341
+ }
342
+ return range;
343
+ }
344
+ function searchPage(spans, query, options = {}) {
345
+ const trimmed = query.trim();
346
+ if (!trimmed) return [];
347
+ if (options.fuzzy) {
348
+ return fuzzySearchPage(spans, trimmed, options);
349
+ }
350
+ const regex = buildFlexibleRegex(query, options);
351
+ if (!regex) return [];
352
+ const { fullText, charMap } = buildTextAndCharMap(spans);
259
353
  const allMatchRanges = [];
260
354
  let m;
261
355
  regex.lastIndex = 0;
262
356
  while ((m = regex.exec(fullText)) !== null) {
263
- const start = m.index;
264
- const end = start + m[0].length;
265
- const range = [];
266
- for (let k = start; k < end; k++) {
267
- const cm = charMap[k];
268
- const last = range[range.length - 1];
269
- if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {
270
- last.end = cm.charIdx + 1;
271
- } else {
272
- range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });
273
- }
274
- }
275
- allMatchRanges.push(range);
357
+ allMatchRanges.push(mapToSpanRanges(m.index, m.index + m[0].length, charMap));
276
358
  if (m[0].length === 0) regex.lastIndex++;
277
359
  }
278
360
  return allMatchRanges;
279
361
  }
362
+ function fuzzySearchPage(spans, query, options) {
363
+ const { fullText, charMap } = buildTextAndCharMap(spans);
364
+ if (fullText.length === 0) return [];
365
+ const isCaseSensitive = _nullishCoalesce(options.caseSensitive, () => ( false));
366
+ const threshold = _nullishCoalesce(options.fuzzyThreshold, () => ( 0.6));
367
+ const searchText = isCaseSensitive ? fullText : fullText.toLowerCase();
368
+ const searchQuery = isCaseSensitive ? query : query.toLowerCase();
369
+ const strippedQuery = searchQuery.replace(/\s+/g, "");
370
+ if (strippedQuery.length === 0) return [];
371
+ const maxErrors = Math.floor(strippedQuery.length * (1 - threshold));
372
+ const matches = fuzzySearchText(searchText, strippedQuery, maxErrors);
373
+ return matches.map((m) => mapToSpanRanges(m.start, m.end, charMap));
374
+ }
280
375
 
281
376
  // src/core/HighlightManager.ts
282
377
  var HighlightManager = class {
@@ -666,4 +761,4 @@ var SearchController = class {
666
761
 
667
762
 
668
763
  exports.EventEmitter = EventEmitter; exports.DEFAULT_CLASS_NAMES = DEFAULT_CLASS_NAMES; exports.DEFAULT_SCALE = DEFAULT_SCALE; exports.DEFAULT_PAGE_GAP = DEFAULT_PAGE_GAP; exports.ZOOM_STEP = ZOOM_STEP; exports.MIN_SCALE = MIN_SCALE; exports.MAX_SCALE = MAX_SCALE; exports.PDFRenderer = PDFRenderer; exports.searchPage = searchPage; exports.HighlightManager = HighlightManager; exports.PDFSearchViewer = PDFSearchViewer; exports.SearchController = SearchController;
669
- //# sourceMappingURL=chunk-YH5HZXPG.cjs.map
764
+ //# sourceMappingURL=chunk-Y5LFTIMY.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/hoangnguyen/Desktop/pdf-search-highlight/dist/chunk-Y5LFTIMY.cjs","../src/core/EventEmitter.ts","../src/core/constants.ts","../src/core/PDFRenderer.ts","../src/core/SearchEngine.ts","../src/core/HighlightManager.ts","../src/core/PDFSearchViewer.ts","../src/core/SearchController.ts"],"names":[],"mappings":"AAAA;ACEO,IAAM,aAAA,EAAN,MAAgE;AAAA,EAAhE,WAAA,CAAA,EAAA;AACL,IAAA,IAAA,CAAQ,UAAA,kBAAY,IAAI,GAAA,CAAwC,CAAA;AAAA,EAAA;AAAA,EAEhE,EAAA,CAA6B,KAAA,EAAU,QAAA,EAAuC;AAC5E,IAAA,GAAA,CAAI,CAAC,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAA,kBAAO,IAAI,GAAA,CAAI,CAAC,CAAA;AAAA,IACrC;AACA,IAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA,CAAG,GAAA,CAAI,QAAQ,CAAA;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,GAAA,CAA8B,KAAA,EAAU,QAAA,EAAuC;AAC7E,oBAAA,IAAA,mBAAK,SAAA,qBAAU,GAAA,mBAAI,KAAK,CAAA,6BAAG,MAAA,mBAAO,QAAQ,GAAA;AAC1C,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEU,IAAA,CAA+B,KAAA,EAAU,IAAA,EAAyB;AAC1E,oBAAA,IAAA,qBAAK,SAAA,qBAAU,GAAA,mBAAI,KAAK,CAAA,6BAAG,OAAA,qBAAQ,CAAC,EAAA,EAAA,GAAO,EAAA,CAAG,IAAI,CAAC,GAAA;AAAA,EACrD;AAAA,EAEA,kBAAA,CAAA,EAA2B;AACzB,IAAA,IAAA,CAAK,SAAA,CAAU,KAAA,CAAM,CAAA;AAAA,EACvB;AACF,CAAA;ADFA;AACA;AEtBO,IAAM,oBAAA,EAA4C;AAAA,EACvD,SAAA,EAAW,eAAA;AAAA,EACX,IAAA,EAAM,UAAA;AAAA,EACN,MAAA,EAAQ,YAAA;AAAA,EACR,SAAA,EAAW,gBAAA;AAAA,EACX,SAAA,EAAW,gBAAA;AAAA,EACX,SAAA,EAAW,WAAA;AAAA,EACX,eAAA,EAAiB;AACnB,CAAA;AAEO,IAAM,cAAA,EAAgB,MAAA;AACtB,IAAM,iBAAA,EAAmB,EAAA;AAEzB,IAAM,UAAA,EAAY,IAAA;AAClB,IAAM,UAAA,EAAY,IAAA;AAClB,IAAM,UAAA,EAAY,CAAA;AFsBzB;AACA;AGxBO,IAAM,YAAA,EAAN,MAAkB;AAAA,EAWvB,WAAA,CAAY,SAAA,EAAwB,OAAA,EAAiC;AALrE,IAAA,IAAA,CAAQ,OAAA,EAAkC,IAAA;AAC1C,IAAA,IAAA,CAAQ,SAAA,EAAuB,CAAC,CAAA;AAChC,IAAA,IAAA,CAAQ,SAAA,EAAgB,IAAA;AACxB,IAAA,IAAA,CAAQ,eAAA,EAAyB,CAAA;AAG/B,IAAA,IAAA,CAAK,UAAA,EAAY,SAAA;AACjB,IAAA,IAAA,CAAK,MAAA,mBAAQ,OAAA,CAAQ,KAAA,UAAS,eAAA;AAC9B,IAAA,IAAA,CAAK,QAAA,mBAAU,OAAA,CAAQ,OAAA,UAAW,kBAAA;AAClC,IAAA,IAAA,CAAK,UAAA,EAAY,OAAA,CAAQ,SAAA;AACzB,IAAA,IAAA,CAAK,IAAA,EAAM,EAAE,GAAG,mBAAA,EAAqB,GAAG,OAAA,CAAQ,WAAW,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAA,CAAY,GAAA,EAAgB;AAC1B,IAAA,IAAA,CAAK,SAAA,EAAW,GAAA;AAChB,IAAA,GAAA,CAAI,IAAA,CAAK,SAAA,EAAW;AAClB,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAA,EAAY,IAAA,CAAK,SAAA;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAA,CACJ,MAAA,EACiB;AACjB,IAAA,GAAA,CAAI,CAAC,IAAA,CAAK,QAAA,EAAU;AAClB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,MACF,CAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,OAAA,CAAQ,CAAA;AAEb,IAAA,IAAI,IAAA;AACJ,IAAA,GAAA,CAAI,OAAA,WAAkB,IAAA,EAAM;AAC1B,MAAA,KAAA,EAAO,MAAM,MAAA,CAAO,WAAA,CAAY,CAAA;AAAA,IAClC,EAAA,KAAA,GAAA,CAAW,OAAO,OAAA,IAAW,QAAA,EAAU;AACrC,MAAA,KAAA,EAAO,EAAE,GAAA,EAAK,OAAO,CAAA;AAAA,IACvB,EAAA,KAAO;AACL,MAAA,KAAA,EAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,YAAA,EAAc,IAAA,CAAK,QAAA,CAAS,WAAA,CAAY,EAAE,KAAK,CAAC,CAAA;AACtD,IAAA,IAAA,CAAK,OAAA,EAAS,MAAM,WAAA,CAAY,OAAA;AAChC,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,QAAA;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAA,CAAA,EAAsC;AAC1C,IAAA,GAAA,CAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,MAAM,IAAI,KAAA,CAAM,wBAAwB,CAAA;AAG1D,IAAA,MAAM,cAAA,EAAgB,IAAA,CAAK,SAAA,CAAU,SAAA;AACrC,IAAA,MAAM,iBAAA,EAAmB,IAAA,CAAK,SAAA,CAAU,aAAA,GAAgB,CAAA;AACxD,IAAA,MAAM,YAAA,EAAc,cAAA,EAAgB,gBAAA;AAEpC,IAAA,IAAA,CAAK,SAAA,CAAU,UAAA,EAAY,EAAA;AAC3B,IAAA,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA;AAC/C,IAAA,IAAA,CAAK,SAAA,EAAW,CAAC,CAAA;AAEjB,IAAA,MAAM,SAAA,EAAW,IAAA,CAAK,MAAA,CAAO,QAAA;AAE7B,IAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,GAAK,QAAA,EAAU,CAAA,EAAA,EAAK;AAClC,MAAA,MAAM,KAAA,EAAO,MAAM,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA;AACxC,MAAA,MAAM,GAAA,EAAK,MAAM,IAAA,CAAK,UAAA,CAAW,IAAA,EAAM,CAAA,EAAG,QAAQ,CAAA;AAClD,MAAA,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,EAAE,CAAA;AAAA,IACvB;AAGA,IAAA,GAAA,CAAI,cAAA,EAAgB,CAAA,EAAG;AACrB,MAAA,IAAA,CAAK,SAAA,CAAU,UAAA,EAAY,YAAA,EAAc,IAAA,CAAK,SAAA,CAAU,YAAA;AAAA,IAC1D;AAEA,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAc,UAAA,CACZ,IAAA,EACA,OAAA,EACA,UAAA,EACmB;AACnB,IAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,cAAA,CAAe,IAAI,CAAA;AACtC,IAAA,GAAA,CAAI,QAAA,IAAY,CAAA,EAAG,IAAA,CAAK,eAAA,EAAiB,KAAA;AACzC,IAAA,MAAM,GAAA,EAAK,IAAA,CAAK,WAAA,CAAY,EAAE,MAAM,CAAC,CAAA;AAGrC,IAAA,MAAM,UAAA,EAAY,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC9C,IAAA,SAAA,CAAU,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,IAAA;AAC/B,IAAA,SAAA,CAAU,KAAA,CAAM,SAAA,EAAW,UAAA;AAC3B,IAAA,SAAA,CAAU,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,IAAA;AACnC,IAAA,SAAA,CAAU,KAAA,CAAM,OAAA,EAAS,EAAA,CAAG,OAAA,EAAS,IAAA;AACrC,IAAA,SAAA,CAAU,KAAA,CAAM,OAAA,EAAS,QAAA;AACzB,IAAA,SAAA,CAAU,KAAA,CAAM,aAAA,EAAe,IAAA,CAAK,QAAA,EAAU,IAAA;AAC9C,IAAA,SAAA,CAAU,KAAA,CAAM,SAAA,EAAW,QAAA;AAC3B,IAAA,SAAA,CAAU,OAAA,CAAQ,KAAA,EAAO,MAAA,CAAO,OAAO,CAAA;AAGvC,IAAA,MAAM,OAAA,EAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,MAAA;AAC5B,IAAA,MAAA,CAAO,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAA;AAC1B,IAAA,MAAA,CAAO,OAAA,EAAS,EAAA,CAAG,OAAA,EAAS,CAAA;AAC5B,IAAA,MAAA,CAAO,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,IAAA;AAChC,IAAA,MAAA,CAAO,KAAA,CAAM,OAAA,EAAS,EAAA,CAAG,OAAA,EAAS,IAAA;AAClC,IAAA,MAAA,CAAO,KAAA,CAAM,QAAA,EAAU,OAAA;AACvB,IAAA,MAAM,IAAA,EAAM,MAAA,CAAO,UAAA,CAAW,IAAI,CAAA;AAClC,IAAA,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA;AACd,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,EAAE,aAAA,EAAe,GAAA,EAAK,QAAA,EAAU,GAAG,CAAC,CAAA,CAAE,OAAA;AAGxD,IAAA,MAAM,UAAA,EAAY,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC9C,IAAA,SAAA,CAAU,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,SAAA;AAC/B,IAAA,SAAA,CAAU,KAAA,CAAM,SAAA,EAAW,UAAA;AAC3B,IAAA,SAAA,CAAU,KAAA,CAAM,IAAA,EAAM,GAAA;AACtB,IAAA,SAAA,CAAU,KAAA,CAAM,KAAA,EAAO,GAAA;AACvB,IAAA,SAAA,CAAU,KAAA,CAAM,MAAA,EAAQ,GAAA;AACxB,IAAA,SAAA,CAAU,KAAA,CAAM,OAAA,EAAS,GAAA;AACzB,IAAA,SAAA,CAAU,KAAA,CAAM,SAAA,EAAW,QAAA;AAC3B,IAAA,SAAA,CAAU,KAAA,CAAM,WAAA,EAAa,GAAA;AAE7B,IAAA,MAAM,GAAA,EAAK,MAAM,IAAA,CAAK,cAAA,CAAe,CAAA;AACrC,IAAA,MAAM,MAAA,EAAoB,CAAC,CAAA;AAE3B,IAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,EAAA,CAAG,KAAA,EAAO;AAC3B,MAAA,GAAA,CAAI,CAAC,IAAA,CAAK,IAAA,GAAO,CAAC,IAAA,CAAK,MAAA,EAAQ,QAAA;AAE/B,MAAA,MAAM,GAAA,EAAK,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,EAAA,CAAG,SAAA,EAAW,IAAA,CAAK,SAAS,CAAA;AACpE,MAAA,MAAM,KAAA,EAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,MAAA,IAAA,CAAK,YAAA,EAAc,IAAA,CAAK,IAAA,GAAO,EAAA;AAC/B,MAAA,MAAM,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,EAAA,CAAG,CAAC,CAAA,EAAG,EAAA,CAAG,CAAC,CAAC,CAAA;AAClC,MAAA,IAAA,CAAK,KAAA,CAAM,SAAA,EAAW,UAAA;AACtB,MAAA,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,CAAC,EAAA,EAAI,IAAA;AAC1B,MAAA,IAAA,CAAK,KAAA,CAAM,IAAA,EAAO,EAAA,CAAG,CAAC,EAAA,EAAI,GAAA,EAAM,IAAA;AAChC,MAAA,IAAA,CAAK,KAAA,CAAM,SAAA,EAAW,GAAA,EAAK,IAAA;AAC3B,MAAA,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,aAAA;AACnB,MAAA,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,KAAA;AACxB,MAAA,IAAA,CAAK,KAAA,CAAM,OAAA,EAAS,MAAA;AACpB,MAAA,IAAA,CAAK,KAAA,CAAM,gBAAA,EAAkB,OAAA;AAC7B,MAAA,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,IAAA,CAAK,QAAA;AAEhD,MAAA,MAAM,GAAA,EAAK,EAAA,CAAG,CAAC,EAAA,EAAI,EAAA;AACnB,MAAA,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,CAAC,EAAA,EAAI,IAAA,EAAM;AAC3B,QAAA,IAAA,CAAK,KAAA,CAAM,UAAA,EAAY,CAAA,OAAA,EAAU,EAAE,CAAA,CAAA,CAAA;AAAA,MACrC;AAEA,MAAA,SAAA,CAAU,WAAA,CAAY,IAAI,CAAA;AAC1B,MAAA,KAAA,CAAM,IAAA,CAAK;AAAA,QACT,EAAA,EAAI,IAAA;AAAA,QACJ,IAAA,EAAM,IAAA,CAAK,IAAA,GAAO,EAAA;AAAA,QAClB,MAAA,EAAQ,CAAC,CAAC,IAAA,CAAK;AAAA,MACjB,CAAC,CAAA;AAAA,IACH;AAEA,IAAA,SAAA,CAAU,WAAA,CAAY,MAAM,CAAA;AAC5B,IAAA,SAAA,CAAU,WAAA,CAAY,SAAS,CAAA;AAC/B,IAAA,IAAA,CAAK,SAAA,CAAU,WAAA,CAAY,SAAS,CAAA;AAGpC,IAAA,MAAM,MAAA,EAAQ,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC1C,IAAA,KAAA,CAAM,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,SAAA;AAC3B,IAAA,KAAA,CAAM,YAAA,EAAc,CAAA,KAAA,EAAQ,OAAO,CAAA,GAAA,EAAM,UAAU,CAAA,CAAA;AACnB,IAAA;AAEN,IAAA;AAC5B,EAAA;AAEmD,EAAA;AACE,IAAA;AACrC,MAAA;AACd,IAAA;AAC+C,IAAA;AACT,IAAA;AACa,IAAA;AACrD,EAAA;AAAA;AAGuC,EAAA;AACxB,IAAA;AACf,EAAA;AAAA;AAG4B,EAAA;AACd,IAAA;AACd,EAAA;AAAA;AAG4B,EAAA;AACd,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAKiE,EAAA;AAC7B,IAAA;AAEK,IAAA;AACW,IAAA;AACd,IAAA;AAEA,IAAA;AAC3B,IAAA;AACI,IAAA;AACc,IAAA;AACnB,IAAA;AACmB,IAAA;AACJ,IAAA;AACzB,EAAA;AAEsC,EAAA;AACxB,IAAA;AACd,EAAA;AAE0B,EAAA;AACZ,IAAA;AACd,EAAA;AAEuB,EAAA;AACW,IAAA;AAClC,EAAA;AAEgB,EAAA;AACO,oBAAA;AACP,IAAA;AACG,IAAA;AACU,IAAA;AAC7B,EAAA;AACF;AH3BwD;AACA;AIrNhB;AACQ,EAAA;AAChD;AAiBiB;AACY,EAAA;AACN,EAAA;AAE4B,EAAA;AACd,EAAA;AAEV,EAAA;AAEY,IAAA;AACgB,IAAA;AACrD,EAAA;AAGsD,EAAA;AACvB,EAAA;AAEP,EAAA;AAEY,IAAA;AACgB,IAAA;AACC,IAAA;AACrD,EAAA;AAGsD,EAAA;AACH,EAAA;AACrD;AAqBgB;AACC,EAAA;AACC,EAAA;AACK,EAAA;AACA,EAAA;AAIW,EAAA;AACO,EAAA;AAGK,EAAA;AAGoB,EAAA;AAEnC,EAAA;AACO,IAAA;AACxB,IAAA;AACmB,IAAA;AACqB,MAAA;AACjC,MAAA;AACH,QAAA;AAAA;AACI,QAAA;AAAA;AACA,QAAA;AAAA;AAChB,MAAA;AACF,IAAA;AACyB,IAAA;AAEC,IAAA;AACuB,MAAA;AACjD,IAAA;AACO,IAAA;AACT,EAAA;AAEuC,EAAA;AAGL,EAAA;AACoB,EAAA;AAE5C,IAAA;AACA,IAAA;AACe,IAAA;AACF,MAAA;AACI,MAAA;AACyB,MAAA;AAClB,MAAA;AAE5B,QAAA;AACA,QAAA;AAC4B,MAAA;AAE5B,QAAA;AACK,MAAA;AAEL,QAAA;AACF,MAAA;AACF,IAAA;AACmD,IAAA;AACrD,EAAA;AAGqC,EAAA;AACY,EAAA;AAEN,EAAA;AACC,EAAA;AACL,IAAA;AACZ,IAAA;AACE,IAAA;AAEU,MAAA;AACL,QAAA;AAC9B,MAAA;AACK,IAAA;AACW,MAAA;AAClB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAKgD;AAC/B,EAAA;AACkB,EAAA;AACR,EAAA;AACoB,IAAA;AACA,MAAA;AACpB,MAAA;AACvB,IAAA;AACD,EAAA;AAC0B,EAAA;AAC7B;AASgB;AACe,EAAA;AACK,EAAA;AACZ,IAAA;AACe,IAAA;AACa,IAAA;AACtB,MAAA;AACnB,IAAA;AACuC,MAAA;AAC9C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAekB;AACW,EAAA;AACL,EAAA;AAEH,EAAA;AAC6B,IAAA;AAChD,EAAA;AAE+C,EAAA;AAC3B,EAAA;AAE8B,EAAA;AAGV,EAAA;AACpC,EAAA;AACc,EAAA;AAE0B,EAAA;AACK,IAAA;AAClB,IAAA;AAC/B,EAAA;AAEO,EAAA;AACT;AAMkB;AACkC,EAAA;AACf,EAAA;AAEc,EAAA;AACL,EAAA;AAEI,EAAA;AACI,EAAA;AAGA,EAAA;AACZ,EAAA;AAEa,EAAA;AACT,EAAA;AAES,EAAA;AACvD;AJ4GwD;AACA;AK1W1B;AAMsC,EAAA;AALhC,IAAA;AACX,IAAA;AAKC,IAAA;AACM,IAAA;AAC9B,EAAA;AAAA;AAAA;AAAA;AAAA;AASiB,EAAA;AACkB,IAAA;AAM5B,IAAA;AAE8B,IAAA;AACZ,MAAA;AAC6B,QAAA;AACH,QAAA;AAC9C,MAAA;AACF,IAAA;AAG2D,IAAA;AAGf,IAAA;AACd,MAAA;AACP,MAAA;AACyB,MAAA;AAEF,MAAA;AAClC,MAAA;AAEa,MAAA;AACoB,QAAA;AAGlB,QAAA;AACqB,UAAA;AAC7C,QAAA;AAGyB,QAAA;AACmB,UAAA;AACpB,UAAA;AACuB,UAAA;AACxB,UAAA;AACW,UAAA;AAClC,QAAA;AAE2B,QAAA;AAC7B,MAAA;AAG0B,MAAA;AACwB,QAAA;AAClD,MAAA;AAGmB,MAAA;AACE,MAAA;AACvB,IAAA;AAGqC,IAAA;AAEvC,EAAA;AAAA;AAAA;AAAA;AAK4C,EAAA;AACX,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAK+C,EAAA;AACjB,IAAA;AACF,MAAA;AACD,QAAA;AACtB,MAAA;AACF,IAAA;AACe,IAAA;AACI,IAAA;AACtB,EAAA;AAAA;AAAA;AAAA;AAKoC,EAAA;AAEgB,IAAA;AACV,MAAA;AACZ,QAAA;AAC1B,MAAA;AACF,IAAA;AAEoB,IAAA;AAE2B,IAAA;AACnB,MAAA;AACiB,QAAA;AAC3C,MAAA;AAE6C,sBAAA;AACjC,QAAA;AACH,QAAA;AACR,MAAA;AACH,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKe,EAAA;AACyB,IAAA;AACQ,IAAA;AACpB,IAAA;AACnB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKe,EAAA;AACyB,IAAA;AAEN,IAAA;AACN,IAAA;AACnB,IAAA;AACT,EAAA;AAE0B,EAAA;AACZ,IAAA;AACd,EAAA;AAEmB,EAAA;AACG,IAAA;AACtB,EAAA;AAE4B,EAAA;AACd,IAAA;AACd,EAAA;AACF;ALgUwD;AACA;AM1cmB;AAYvE,EAAA;AACM,IAAA;AAVwB,IAAA;AACZ,IAAA;AACwB,IAAA;AACxB,IAAA;AAS+B,IAAA;AAEC,IAAA;AAChB,IAAA;AACN,IAAA;AACtB,MAAA;AACA,MAAA;AACN,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKgD,EAAA;AACV,IAAA;AAEhC,IAAA;AACqC,MAAA;AACH,MAAA;AACS,MAAA;AACd,MAAA;AACnB,IAAA;AACmC,MAAA;AACC,MAAA;AAC1C,MAAA;AACR,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAM2D,EAAA;AACrB,IAAA;AAGe,IAAA;AAClC,IAAA;AACQ,IAAA;AAEE,IAAA;AACb,IAAA;AAC2B,MAAA;AACW,MAAA;AAC3C,MAAA;AACT,IAAA;AAGgC,IAAA;AACoB,MAAA;AACZ,MAAA;AACE,MAAA;AAC1C,IAAA;AAE6C,IAAA;AAG9B,IAAA;AACyB,MAAA;AACxC,IAAA;AAEoC,IAAA;AACX,IAAA;AACE,MAAA;AACzB,MAAA;AACD,IAAA;AAEM,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AACqB,IAAA;AACd,IAAA;AACd,MAAA;AAC6B,MAAA;AACvC,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AACqB,IAAA;AACd,IAAA;AACd,MAAA;AAC6B,MAAA;AACvC,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AACiC,IAAA;AAClC,IAAA;AAC0B,IAAA;AACO,IAAA;AACpD,EAAA;AAAA;AAG4B,EAAA;AACI,IAAA;AAChC,EAAA;AAAA;AAGsD,EAAA;AAChB,IAAA;AACR,IAAA;AACR,IAAA;AACqB,IAAA;AAC3C,EAAA;AAAA;AAG8B,EAAA;AACa,IAAA;AACM,IAAA;AACnB,IAAA;AAC9B,EAAA;AAAA;AAG+B,EAAA;AACY,IAAA;AACM,IAAA;AACnB,IAAA;AAC9B,EAAA;AAAA;AAGiD,EAAA;AACX,IAAA;AACC,IAAA;AACvC,EAAA;AAEsC,EAAA;AACH,IAAA;AACG,IAAA;AACtC,EAAA;AAEwC,EAAA;AACa,IAAA;AACA,IAAA;AACxB,IAAA;AACyB,MAAA;AACpD,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKuB,EAAA;AACa,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAK+B,EAAA;AACgB,IAAA;AAC/C,EAAA;AAAA;AAAA;AAAA;AAKwB,EAAA;AACgB,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAKgB,EAAA;AACM,IAAA;AACH,IAAA;AACkC,IAAA;AAC7B,IAAA;AACE,IAAA;AACP,IAAA;AACnB,EAAA;AACF;ANwawD;AACA;AOvmB1B;AASuB,EAAA;AAPtB,IAAA;AACT,IAAA;AACwB,IAAA;AAG5C;AAAwF,IAAA;AAGrC,IAAA;AACA,IAAA;AACnD,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkC,EAAA;AACR,IAAA;AACE,IAAA;AACf,IAAA;AACE,IAAA;AAEU,IAAA;AACe,MAAA;AACtC,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAM2D,EAAA;AACT,IAAA;AAC/B,IAAA;AACQ,IAAA;AAEE,IAAA;AACb,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AAE6B,IAAA;AACuB,MAAA;AACZ,MAAA;AACE,MAAA;AAC1C,IAAA;AAE6C,IAAA;AAC9B,IAAA;AACyB,MAAA;AACxC,IAAA;AAEY,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGe,EAAA;AAC0B,IAAA;AAC3B,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGe,EAAA;AAC0B,IAAA;AAC3B,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG0B,EAAA;AACkB,IAAA;AAC9B,IAAA;AACd,EAAA;AAAA;AAGc,EAAA;AACoC,IAAA;AAC/B,IAAA;AACL,IAAA;AACd,EAAA;AAAA;AAGsB,EAAA;AACyB,IAAA;AAC/C,EAAA;AAAA;AAGoB,EAAA;AACoB,IAAA;AACxC,EAAA;AAAA;AAGoB,EAAA;AACN,IAAA;AACd,EAAA;AAEuB,EAAA;AACL,oBAAA;AACiC,MAAA;AACT,MAAA;AAC1B,MAAA;AACb,IAAA;AACH,EAAA;AACF;APulBwD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/hoangnguyen/Desktop/pdf-search-highlight/dist/chunk-Y5LFTIMY.cjs","sourcesContent":[null,"type Listener<T> = (data: T) => void;\n\nexport class EventEmitter<EventMap extends { [key: string]: unknown }> {\n private listeners = new Map<keyof EventMap, Set<Listener<any>>>();\n\n on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(listener);\n return this;\n }\n\n off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n protected emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {\n this.listeners.get(event)?.forEach((fn) => fn(data));\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","import type { ClassNames } from '../types';\n\nexport const DEFAULT_CLASS_NAMES: Required<ClassNames> = {\n container: 'psh-container',\n page: 'psh-page',\n canvas: 'psh-canvas',\n textLayer: 'psh-text-layer',\n pageLabel: 'psh-page-label',\n highlight: 'highlight',\n activeHighlight: 'active',\n};\n\nexport const DEFAULT_SCALE = 'auto' as number | 'auto';\nexport const DEFAULT_PAGE_GAP = 20;\n\nexport const ZOOM_STEP = 0.25;\nexport const MIN_SCALE = 0.25;\nexport const MAX_SCALE = 5;\n","import type { PDFSearchViewerOptions, ClassNames, PageData, SpanData } from '../types';\nimport { DEFAULT_CLASS_NAMES, DEFAULT_SCALE, DEFAULT_PAGE_GAP } from './constants';\n\n// pdfjs-dist types\ntype PDFDocumentProxy = any;\ntype PDFPageProxy = any;\n\n/**\n * Renders PDF pages into a container using canvas + text layer.\n *\n * Text layer approach (matching demo):\n * - Extract text content from each page\n * - Create absolutely-positioned <span> elements overlaying the canvas\n * - Position spans using the transform matrix from pdf.js\n * - Spans are transparent (for text selection) but allow DOM-based search/highlight\n */\nexport class PDFRenderer {\n private container: HTMLElement;\n private scale: number | 'auto';\n private pageGap: number;\n private cls: Required<ClassNames>;\n private workerSrc?: string;\n private pdfDoc: PDFDocumentProxy | null = null;\n private pageData: PageData[] = [];\n private pdfjsLib: any = null;\n private effectiveScale: number = 1;\n\n constructor(container: HTMLElement, options: PDFSearchViewerOptions) {\n this.container = container;\n this.scale = options.scale ?? DEFAULT_SCALE;\n this.pageGap = options.pageGap ?? DEFAULT_PAGE_GAP;\n this.workerSrc = options.workerSrc;\n this.cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n }\n\n /**\n * Set the pdfjs-dist library reference.\n * Must be called before loadDocument.\n */\n setPdfjsLib(lib: any): void {\n this.pdfjsLib = lib;\n if (this.workerSrc) {\n lib.GlobalWorkerOptions.workerSrc = this.workerSrc;\n }\n }\n\n /**\n * Load a PDF from File, ArrayBuffer, URL string, or Uint8Array.\n */\n async loadDocument(\n source: File | ArrayBuffer | Uint8Array | string\n ): Promise<number> {\n if (!this.pdfjsLib) {\n throw new Error(\n 'pdfjs-dist not set. Call setPdfjsLib(pdfjsLib) before loading a document.'\n );\n }\n\n this.cleanup();\n\n let data: ArrayBuffer | Uint8Array | { url: string };\n if (source instanceof File) {\n data = await source.arrayBuffer();\n } else if (typeof source === 'string') {\n data = { url: source };\n } else {\n data = source;\n }\n\n const loadingTask = this.pdfjsLib.getDocument({ data });\n this.pdfDoc = await loadingTask.promise;\n return this.pdfDoc.numPages;\n }\n\n /**\n * Render all pages into the container.\n * Preserves scroll position across re-renders (e.g. zoom).\n * Returns PageData[] for search/highlight.\n */\n async renderAllPages(): Promise<PageData[]> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n // Save scroll position relative to total content height\n const prevScrollTop = this.container.scrollTop;\n const prevScrollHeight = this.container.scrollHeight || 1;\n const scrollRatio = prevScrollTop / prevScrollHeight;\n\n this.container.innerHTML = '';\n this.container.classList.add(this.cls.container);\n this.pageData = [];\n\n const numPages = this.pdfDoc.numPages;\n\n for (let i = 1; i <= numPages; i++) {\n const page = await this.pdfDoc.getPage(i);\n const pd = await this.renderPage(page, i, numPages);\n this.pageData.push(pd);\n }\n\n // Restore scroll position proportionally\n if (prevScrollTop > 0) {\n this.container.scrollTop = scrollRatio * this.container.scrollHeight;\n }\n\n return this.pageData;\n }\n\n private async renderPage(\n page: PDFPageProxy,\n pageNum: number,\n totalPages: number\n ): Promise<PageData> {\n const scale = this.calculateScale(page);\n if (pageNum === 1) this.effectiveScale = scale;\n const vp = page.getViewport({ scale });\n\n // Page container\n const container = document.createElement('div');\n container.className = this.cls.page;\n container.style.position = 'relative';\n container.style.width = vp.width + 'px';\n container.style.height = vp.height + 'px';\n container.style.margin = '0 auto';\n container.style.marginBottom = this.pageGap + 'px';\n container.style.overflow = 'hidden';\n container.dataset.page = String(pageNum);\n\n // Canvas (2x for retina)\n const canvas = document.createElement('canvas');\n canvas.className = this.cls.canvas;\n canvas.width = vp.width * 2;\n canvas.height = vp.height * 2;\n canvas.style.width = vp.width + 'px';\n canvas.style.height = vp.height + 'px';\n canvas.style.display = 'block';\n const ctx = canvas.getContext('2d')!;\n ctx.scale(2, 2);\n await page.render({ canvasContext: ctx, viewport: vp }).promise;\n\n // Text layer\n const textLayer = document.createElement('div');\n textLayer.className = this.cls.textLayer;\n textLayer.style.position = 'absolute';\n textLayer.style.top = '0';\n textLayer.style.left = '0';\n textLayer.style.right = '0';\n textLayer.style.bottom = '0';\n textLayer.style.overflow = 'hidden';\n textLayer.style.lineHeight = '1';\n\n const tc = await page.getTextContent();\n const spans: SpanData[] = [];\n\n for (const item of tc.items) {\n if (!item.str && !item.hasEOL) continue;\n\n const tx = this.pdfjsLib.Util.transform(vp.transform, item.transform);\n const span = document.createElement('span');\n span.textContent = item.str || '';\n const fh = Math.hypot(tx[2], tx[3]);\n span.style.position = 'absolute';\n span.style.left = tx[4] + 'px';\n span.style.top = (tx[5] - fh) + 'px';\n span.style.fontSize = fh + 'px';\n span.style.color = 'transparent';\n span.style.whiteSpace = 'pre';\n span.style.cursor = 'text';\n span.style.transformOrigin = '0% 0%';\n if (item.fontName) span.style.fontFamily = item.fontName;\n\n const sw = tx[0] / fh;\n if (Math.abs(sw - 1) > 0.01) {\n span.style.transform = `scaleX(${sw})`;\n }\n\n textLayer.appendChild(span);\n spans.push({\n el: span,\n text: item.str || '',\n hasEOL: !!item.hasEOL,\n });\n }\n\n container.appendChild(canvas);\n container.appendChild(textLayer);\n this.container.appendChild(container);\n\n // Page label\n const label = document.createElement('div');\n label.className = this.cls.pageLabel;\n label.textContent = `Page ${pageNum} / ${totalPages}`;\n this.container.appendChild(label);\n\n return { container, spans };\n }\n\n private calculateScale(page: PDFPageProxy): number {\n if (this.scale !== 'auto' && typeof this.scale === 'number') {\n return this.scale;\n }\n const defaultVp = page.getViewport({ scale: 1 });\n const containerWidth = this.container.clientWidth || 800;\n return Math.min(containerWidth / defaultVp.width, 2);\n }\n\n /** Set the scale for subsequent renders. */\n setScale(scale: number | 'auto'): void {\n this.scale = scale;\n }\n\n /** Get the configured scale setting. */\n getScale(): number | 'auto' {\n return this.scale;\n }\n\n /** Get the actual numeric scale used in the last render. */\n getEffectiveScale(): number {\n return this.effectiveScale;\n }\n\n /**\n * Download the currently loaded PDF.\n */\n async download(filename: string = 'document.pdf'): Promise<void> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n const data = await this.pdfDoc.getData();\n const blob = new Blob([data as BlobPart], { type: 'application/pdf' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n\n getClassNames(): Required<ClassNames> {\n return this.cls;\n }\n\n getPageData(): PageData[] {\n return this.pageData;\n }\n\n getPageCount(): number {\n return this.pdfDoc?.numPages ?? 0;\n }\n\n cleanup(): void {\n this.pdfDoc?.destroy();\n this.pdfDoc = null;\n this.pageData = [];\n this.container.innerHTML = '';\n }\n}\n","import type { SearchOptions, SpanData } from '../types';\n\nexport interface CharMapEntry {\n spanIdx: number;\n charIdx: number;\n}\n\nexport interface MatchRange {\n spanIdx: number;\n start: number;\n end: number;\n}\n\nexport interface SearchResult {\n /** Array of span ranges for each match */\n matchRanges: MatchRange[][];\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Build a flexible regex from query.\n *\n * For queries < 200 chars (after removing whitespace):\n * - Strip all whitespace from query\n * - Insert \\s* between every character\n * → \"and expensive\" becomes a\\s*n\\s*d\\s*e\\s*x\\s*p\\s*e\\s*n\\s*s\\s*i\\s*v\\s*e\n * → Matches regardless of whitespace differences in PDF text\n *\n * For queries >= 200 chars:\n * - Split by whitespace, join with \\s+\n */\nfunction buildFlexibleRegex(\n query: string,\n options: SearchOptions\n): RegExp | null {\n const trimmed = query.trim();\n if (!trimmed) return null;\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const flexibleWhitespace = options.flexibleWhitespace ?? true;\n\n if (!flexibleWhitespace) {\n // Simple literal search\n const pattern = escapeRegex(trimmed);\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Remove all whitespace chars from query\n const chars = [...trimmed].filter((c) => !/\\s/.test(c));\n if (chars.length === 0) return null;\n\n if (chars.length > 200) {\n // Fallback: flexible between tokens only\n const tokens = trimmed.split(/\\s+/);\n const pattern = tokens.map((t) => escapeRegex(t)).join('\\\\s+');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Insert \\s* between every character\n const pattern = chars.map((c) => escapeRegex(c)).join('\\\\s*');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n}\n\ninterface FuzzyMatch {\n start: number;\n end: number;\n distance: number;\n}\n\n/**\n * Semi-global Levenshtein alignment for approximate substring matching.\n *\n * Finds all positions in `text` where a substring has edit distance ≤ maxErrors\n * from `query`. Uses O(m) space with single-column DP.\n *\n * Semi-global: first DP column = 0 (match can start anywhere in text),\n * but the full query must be covered.\n */\nfunction fuzzySearchText(\n text: string,\n query: string,\n maxErrors: number\n): FuzzyMatch[] {\n const n = text.length;\n const m = query.length;\n if (m === 0) return [];\n if (n === 0) return [];\n\n // DP: prev[j] = min edit distance to match query[0..j-1] ending at current text pos\n // Semi-global: prev[0] = 0 for all text positions (free start)\n let prev = new Uint32Array(m + 1);\n for (let j = 0; j <= m; j++) prev[j] = j;\n\n // Store full DP matrix columns for traceback\n const columns: Uint32Array[] = [prev.slice()];\n\n // Track match end positions\n const endPositions: Array<{ col: number; distance: number }> = [];\n\n for (let i = 1; i <= n; i++) {\n const curr = new Uint32Array(m + 1);\n curr[0] = 0; // semi-global: free start position\n for (let j = 1; j <= m; j++) {\n const cost = text[i - 1] === query[j - 1] ? 0 : 1;\n curr[j] = Math.min(\n prev[j] + 1, // deletion\n curr[j - 1] + 1, // insertion\n prev[j - 1] + cost // substitution\n );\n }\n columns.push(curr.slice());\n\n if (curr[m] <= maxErrors) {\n endPositions.push({ col: i, distance: curr[m] });\n }\n prev = curr;\n }\n\n if (endPositions.length === 0) return [];\n\n // Traceback to find start position for each match end\n const rawMatches: FuzzyMatch[] = [];\n for (const { col: endCol, distance } of endPositions) {\n // Trace back through the DP matrix to find where the match starts\n let j = m;\n let i = endCol;\n while (j > 0 && i > 0) {\n const c = columns[i];\n const p = columns[i - 1];\n const cost = text[i - 1] === query[j - 1] ? 0 : 1;\n if (c[j] === p[j - 1] + cost) {\n // substitution or match — move diagonally\n i--;\n j--;\n } else if (c[j] === p[j] + 1) {\n // deletion from text — move left in text\n i--;\n } else {\n // insertion into text — move up in query\n j--;\n }\n }\n rawMatches.push({ start: i, end: endCol, distance });\n }\n\n // Merge overlapping matches, keeping the one with lowest distance\n if (rawMatches.length === 0) return [];\n rawMatches.sort((a, b) => a.start - b.start || a.distance - b.distance);\n\n const merged: FuzzyMatch[] = [rawMatches[0]];\n for (let i = 1; i < rawMatches.length; i++) {\n const prev = merged[merged.length - 1];\n const curr = rawMatches[i];\n if (curr.start < prev.end) {\n // Overlapping — keep the one with lower distance\n if (curr.distance < prev.distance) {\n merged[merged.length - 1] = curr;\n }\n } else {\n merged.push(curr);\n }\n }\n\n return merged;\n}\n\n/**\n * Build fullText and charMap from spans.\n */\nfunction buildTextAndCharMap(spans: SpanData[]) {\n let fullText = '';\n const charMap: CharMapEntry[] = [];\n spans.forEach((s, si) => {\n for (let ci = 0; ci < s.text.length; ci++) {\n charMap.push({ spanIdx: si, charIdx: ci });\n fullText += s.text[ci];\n }\n });\n return { fullText, charMap };\n}\n\n/**\n * Map a start/end range in fullText to MatchRange[] via charMap.\n */\nfunction mapToSpanRanges(\n start: number,\n end: number,\n charMap: CharMapEntry[]\n): MatchRange[] {\n const range: MatchRange[] = [];\n for (let k = start; k < end; k++) {\n const cm = charMap[k];\n const last = range[range.length - 1];\n if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {\n last.end = cm.charIdx + 1;\n } else {\n range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });\n }\n }\n return range;\n}\n\n/**\n * Search for text across page spans using charMap-based matching.\n *\n * Algorithm:\n * 1. Concatenate all span texts into one string (fullText)\n * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText\n * 3. Run regex or fuzzy search on fullText\n * 4. Map each match back to span ranges via charMap\n */\nexport function searchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions = {}\n): MatchRange[][] {\n const trimmed = query.trim();\n if (!trimmed) return [];\n\n if (options.fuzzy) {\n return fuzzySearchPage(spans, trimmed, options);\n }\n\n const regex = buildFlexibleRegex(query, options);\n if (!regex) return [];\n\n const { fullText, charMap } = buildTextAndCharMap(spans);\n\n // Find all regex matches\n const allMatchRanges: MatchRange[][] = [];\n let m: RegExpExecArray | null;\n regex.lastIndex = 0;\n\n while ((m = regex.exec(fullText)) !== null) {\n allMatchRanges.push(mapToSpanRanges(m.index, m.index + m[0].length, charMap));\n if (m[0].length === 0) regex.lastIndex++;\n }\n\n return allMatchRanges;\n}\n\nfunction fuzzySearchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions\n): MatchRange[][] {\n const { fullText, charMap } = buildTextAndCharMap(spans);\n if (fullText.length === 0) return [];\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const threshold = options.fuzzyThreshold ?? 0.6;\n\n const searchText = isCaseSensitive ? fullText : fullText.toLowerCase();\n const searchQuery = isCaseSensitive ? query : query.toLowerCase();\n\n // Strip whitespace from query for matching\n const strippedQuery = searchQuery.replace(/\\s+/g, '');\n if (strippedQuery.length === 0) return [];\n\n const maxErrors = Math.floor(strippedQuery.length * (1 - threshold));\n const matches = fuzzySearchText(searchText, strippedQuery, maxErrors);\n\n return matches.map((m) => mapToSpanRanges(m.start, m.end, charMap));\n}\n","import type { SearchMatch, SpanData, PageData } from '../types';\nimport type { MatchRange } from './SearchEngine';\n\n/**\n * Manages cross-span highlighting using the charMap approach.\n *\n * Algorithm (from demo):\n * 1. Group match ranges by spanIdx\n * 2. For each affected span, replace textContent with a DocumentFragment:\n * - Plain text nodes for non-matching parts\n * - <mark> elements for matching parts\n * 3. Collect marks per match for navigation\n */\nexport class HighlightManager {\n private matches: SearchMatch[] = [];\n private currentMatch = -1;\n private highlightClass: string;\n private activeHighlightClass: string;\n\n constructor(highlightClass: string, activeHighlightClass: string) {\n this.highlightClass = highlightClass;\n this.activeHighlightClass = activeHighlightClass;\n }\n\n /**\n * Apply highlights for all matches on a page.\n * Returns the SearchMatch[] (array of mark groups).\n */\n applyHighlights(\n pageSpans: SpanData[],\n matchRanges: MatchRange[][]\n ): SearchMatch[] {\n if (!matchRanges.length) return [];\n\n // Group ranges by spanIdx, keeping track of which match they belong to\n const spanRanges: Record<\n number,\n { start: number; end: number; matchIdx: number }[]\n > = {};\n\n matchRanges.forEach((range, mi) => {\n range.forEach((r) => {\n if (!spanRanges[r.spanIdx]) spanRanges[r.spanIdx] = [];\n spanRanges[r.spanIdx].push({ start: r.start, end: r.end, matchIdx: mi });\n });\n });\n\n // Collect marks per match\n const matchMarks: HTMLElement[][] = matchRanges.map(() => []);\n\n // For each affected span, rebuild DOM with highlights\n for (const siStr of Object.keys(spanRanges)) {\n const si = parseInt(siStr, 10);\n const s = pageSpans[si];\n const ranges = spanRanges[si].sort((a, b) => a.start - b.start);\n\n const frag = document.createDocumentFragment();\n let last = 0;\n\n for (const r of ranges) {\n const actualStart = Math.max(r.start, last);\n\n // Add plain text before highlight\n if (actualStart > last) {\n frag.appendChild(document.createTextNode(s.text.slice(last, actualStart)));\n }\n\n // Add highlight mark\n if (actualStart < r.end) {\n const mark = document.createElement('mark');\n mark.className = this.highlightClass;\n mark.textContent = s.text.slice(actualStart, r.end);\n frag.appendChild(mark);\n matchMarks[r.matchIdx].push(mark);\n }\n\n last = Math.max(last, r.end);\n }\n\n // Add remaining plain text\n if (last < s.text.length) {\n frag.appendChild(document.createTextNode(s.text.slice(last)));\n }\n\n // Replace span content\n s.el.textContent = '';\n s.el.appendChild(frag);\n }\n\n return matchMarks\n .filter((marks) => marks.length > 0)\n .map((marks) => ({ marks }));\n }\n\n /**\n * Add matches to the global list.\n */\n addMatches(newMatches: SearchMatch[]): void {\n this.matches.push(...newMatches);\n }\n\n /**\n * Clear all highlights and restore original span text.\n */\n clearHighlights(allPageData: PageData[]): void {\n allPageData.forEach((pd) => {\n pd.spans.forEach((s) => {\n s.el.textContent = s.text;\n });\n });\n this.matches = [];\n this.currentMatch = -1;\n }\n\n /**\n * Set active match by index. Applies active CSS class and scrolls into view.\n */\n setActiveMatch(index: number): void {\n // Remove active class from previous\n if (this.currentMatch >= 0 && this.currentMatch < this.matches.length) {\n this.matches[this.currentMatch].marks.forEach((m) =>\n m.classList.remove(this.activeHighlightClass)\n );\n }\n\n this.currentMatch = index;\n\n if (index >= 0 && index < this.matches.length) {\n this.matches[index].marks.forEach((m) =>\n m.classList.add(this.activeHighlightClass)\n );\n // Scroll first mark into view\n this.matches[index].marks[0]?.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n });\n }\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n next(): number {\n if (this.matches.length === 0) return -1;\n const newIdx = (this.currentMatch + 1) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prev(): number {\n if (this.matches.length === 0) return -1;\n const newIdx =\n (this.currentMatch - 1 + this.matches.length) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n getCurrentIndex(): number {\n return this.currentMatch;\n }\n\n getTotal(): number {\n return this.matches.length;\n }\n\n getMatches(): SearchMatch[] {\n return this.matches;\n }\n}\n","import { EventEmitter } from './EventEmitter';\nimport { PDFRenderer } from './PDFRenderer';\nimport { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES, ZOOM_STEP, MIN_SCALE, MAX_SCALE } from './constants';\nimport type {\n PDFSearchViewerOptions,\n SearchOptions,\n PDFSearchViewerEventMap,\n PageData,\n} from '../types';\n\nexport type PDFSource = File | ArrayBuffer | Uint8Array | string;\n\n/**\n * Main PDF viewer with search and highlight functionality.\n *\n * Usage:\n * ```js\n * import * as pdfjsLib from 'pdfjs-dist';\n * import { PDFSearchViewer } from 'pdf-search-highlight';\n *\n * const viewer = new PDFSearchViewer(container, pdfjsLib, {\n * classNames: {\n * page: 'my-page',\n * highlight: 'my-highlight',\n * activeHighlight: 'my-active',\n * }\n * });\n * await viewer.loadPDF(file);\n * viewer.search('hello');\n * viewer.nextMatch();\n * ```\n */\nexport class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {\n private renderer: PDFRenderer;\n private highlightManager: HighlightManager;\n private pageData: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n private destroyed = false;\n\n constructor(\n container: HTMLElement,\n pdfjsLib: any,\n options: PDFSearchViewerOptions = {}\n ) {\n super();\n\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n\n this.renderer = new PDFRenderer(container, options);\n this.renderer.setPdfjsLib(pdfjsLib);\n this.highlightManager = new HighlightManager(\n cls.highlight,\n cls.activeHighlight\n );\n }\n\n /**\n * Load and render a PDF document.\n */\n async loadPDF(source: PDFSource): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n try {\n await this.renderer.loadDocument(source);\n this.pageData = await this.renderer.renderAllPages();\n const pageCount = this.renderer.getPageCount();\n this.emit('load', { pageCount });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', { error, context: 'loadPDF' });\n throw error;\n }\n }\n\n /**\n * Search for text across all pages.\n * Clears previous highlights and creates new ones.\n */\n search(query: string, options: SearchOptions = {}): number {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n // Clear previous highlights\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.emit('search', { query, total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n return 0;\n }\n\n // Search each page and apply highlights\n for (const pd of this.pageData) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n\n // Auto-activate first match\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.emit('search', { query, total });\n this.emit('matchchange', {\n current: total > 0 ? 0 : -1,\n total,\n });\n\n return total;\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n nextMatch(): number {\n const idx = this.highlightManager.next();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prevMatch(): number {\n const idx = this.highlightManager.prev();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Clear all search highlights.\n */\n clearSearch(): void {\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = '';\n this.emit('search', { query: '', total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n }\n\n /** Get the current scale setting. */\n getScale(): number | 'auto' {\n return this.renderer.getScale();\n }\n\n /** Set scale and re-render. Preserves current search state. */\n async setScale(scale: number | 'auto'): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n this.renderer.setScale(scale);\n await this.rerender();\n this.emit('zoom', { scale: this.renderer.getEffectiveScale() });\n }\n\n /** Zoom in by one step. */\n async zoomIn(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.min(current + ZOOM_STEP, MAX_SCALE);\n await this.setScale(newScale);\n }\n\n /** Zoom out by one step. */\n async zoomOut(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.max(current - ZOOM_STEP, MIN_SCALE);\n await this.setScale(newScale);\n }\n\n /** Download the currently loaded PDF. */\n async download(filename?: string): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n await this.renderer.download(filename);\n }\n\n private resolveCurrentScale(): number {\n const s = this.renderer.getScale();\n return s === 'auto' ? this.renderer.getEffectiveScale() : s;\n }\n\n private async rerender(): Promise<void> {\n this.highlightManager.clearHighlights(this.pageData);\n this.pageData = await this.renderer.renderAllPages();\n if (this.lastQuery.trim()) {\n this.search(this.lastQuery, this.lastSearchOptions);\n }\n }\n\n /**\n * Get total number of pages.\n */\n getPageCount(): number {\n return this.renderer.getPageCount();\n }\n\n /**\n * Get current active match index (0-based). -1 if none.\n */\n getCurrentMatchIndex(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /**\n * Get total number of matches.\n */\n getMatchCount(): number {\n return this.highlightManager.getTotal();\n }\n\n /**\n * Destroy the viewer, release all resources.\n */\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.highlightManager.clearHighlights(this.pageData);\n this.renderer.cleanup();\n this.removeAllListeners();\n this.pageData = [];\n }\n}\n","import { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES } from './constants';\nimport type { SearchOptions, ClassNames, PageData } from '../types';\n\nexport interface SearchControllerOptions {\n classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;\n}\n\n/**\n * Headless search + highlight controller.\n * Does NOT render PDF — works with any PageData[] you provide.\n *\n * Use this when you want full control over:\n * - Where the PDF is rendered\n * - Where the search UI lives\n * - How search results are displayed\n *\n * Usage:\n * ```js\n * import { PDFRenderer, SearchController } from 'pdf-search-highlight';\n *\n * // Render PDF wherever you want\n * const renderer = new PDFRenderer(pdfContainer, pdfjsLib, {});\n * const pages = await renderer.renderAllPages();\n *\n * // Search controller — no UI, just logic\n * const search = new SearchController();\n * search.setPages(pages);\n *\n * // Wire up your own UI\n * input.oninput = () => search.search(input.value);\n * nextBtn.onclick = () => search.next();\n * prevBtn.onclick = () => search.prev();\n *\n * // React to changes\n * search.onChange = ({ current, total }) => {\n * label.textContent = total > 0 ? `${current + 1}/${total}` : '';\n * };\n * ```\n */\nexport class SearchController {\n private highlightManager: HighlightManager;\n private pages: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n\n /** Callback fired when match state changes (search, next, prev, clear). */\n onChange: ((state: { current: number; total: number; query: string }) => void) | null = null;\n\n constructor(options: SearchControllerOptions = {}) {\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n this.highlightManager = new HighlightManager(cls.highlight, cls.activeHighlight);\n }\n\n /**\n * Set the pages to search on.\n * Call this after rendering PDF pages.\n */\n setPages(pages: PageData[]): void {\n const savedQuery = this.lastQuery;\n const savedOptions = this.lastSearchOptions;\n this.clear();\n this.pages = pages;\n // Re-apply search if there was an active query (e.g. after zoom)\n if (savedQuery.trim()) {\n this.search(savedQuery, savedOptions);\n }\n }\n\n /**\n * Search for text across all pages.\n * Returns total number of matches.\n */\n search(query: string, options: SearchOptions = {}): number {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.notify();\n return 0;\n }\n\n for (const pd of this.pages) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.notify();\n return total;\n }\n\n /** Navigate to next match. Returns new index. */\n next(): number {\n const idx = this.highlightManager.next();\n this.notify();\n return idx;\n }\n\n /** Navigate to previous match. Returns new index. */\n prev(): number {\n const idx = this.highlightManager.prev();\n this.notify();\n return idx;\n }\n\n /** Go to a specific match by index. */\n goTo(index: number): void {\n this.highlightManager.setActiveMatch(index);\n this.notify();\n }\n\n /** Clear all highlights. */\n clear(): void {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = '';\n this.notify();\n }\n\n /** Current match index (0-based). -1 if none. */\n get current(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /** Total number of matches. */\n get total(): number {\n return this.highlightManager.getTotal();\n }\n\n /** Last searched query. */\n get query(): string {\n return this.lastQuery;\n }\n\n private notify(): void {\n this.onChange?.({\n current: this.highlightManager.getCurrentIndex(),\n total: this.highlightManager.getTotal(),\n query: this.lastQuery,\n });\n }\n}\n"]}
@@ -11,7 +11,7 @@
11
11
 
12
12
 
13
13
 
14
- var _chunkYH5HZXPGcjs = require('../chunk-YH5HZXPG.cjs');
14
+ var _chunkY5LFTIMYcjs = require('../chunk-Y5LFTIMY.cjs');
15
15
 
16
16
 
17
17
 
@@ -25,5 +25,5 @@ var _chunkYH5HZXPGcjs = require('../chunk-YH5HZXPG.cjs');
25
25
 
26
26
 
27
27
 
28
- exports.DEFAULT_CLASS_NAMES = _chunkYH5HZXPGcjs.DEFAULT_CLASS_NAMES; exports.DEFAULT_PAGE_GAP = _chunkYH5HZXPGcjs.DEFAULT_PAGE_GAP; exports.DEFAULT_SCALE = _chunkYH5HZXPGcjs.DEFAULT_SCALE; exports.EventEmitter = _chunkYH5HZXPGcjs.EventEmitter; exports.HighlightManager = _chunkYH5HZXPGcjs.HighlightManager; exports.MAX_SCALE = _chunkYH5HZXPGcjs.MAX_SCALE; exports.MIN_SCALE = _chunkYH5HZXPGcjs.MIN_SCALE; exports.PDFRenderer = _chunkYH5HZXPGcjs.PDFRenderer; exports.PDFSearchViewer = _chunkYH5HZXPGcjs.PDFSearchViewer; exports.SearchController = _chunkYH5HZXPGcjs.SearchController; exports.ZOOM_STEP = _chunkYH5HZXPGcjs.ZOOM_STEP; exports.searchPage = _chunkYH5HZXPGcjs.searchPage;
28
+ exports.DEFAULT_CLASS_NAMES = _chunkY5LFTIMYcjs.DEFAULT_CLASS_NAMES; exports.DEFAULT_PAGE_GAP = _chunkY5LFTIMYcjs.DEFAULT_PAGE_GAP; exports.DEFAULT_SCALE = _chunkY5LFTIMYcjs.DEFAULT_SCALE; exports.EventEmitter = _chunkY5LFTIMYcjs.EventEmitter; exports.HighlightManager = _chunkY5LFTIMYcjs.HighlightManager; exports.MAX_SCALE = _chunkY5LFTIMYcjs.MAX_SCALE; exports.MIN_SCALE = _chunkY5LFTIMYcjs.MIN_SCALE; exports.PDFRenderer = _chunkY5LFTIMYcjs.PDFRenderer; exports.PDFSearchViewer = _chunkY5LFTIMYcjs.PDFSearchViewer; exports.SearchController = _chunkY5LFTIMYcjs.SearchController; exports.ZOOM_STEP = _chunkY5LFTIMYcjs.ZOOM_STEP; exports.searchPage = _chunkY5LFTIMYcjs.searchPage;
29
29
  //# sourceMappingURL=index.cjs.map
@@ -1,5 +1,5 @@
1
- import { C as ClassNames, P as PageData, S as SearchOptions, a as PDFSearchViewerOptions, b as SpanData, c as SearchMatch } from '../PDFSearchViewer-CQaAFXQs.cjs';
2
- export { E as EventEmitter, d as PDFSearchViewer, e as PDFSearchViewerEventMap, f as PDFSource } from '../PDFSearchViewer-CQaAFXQs.cjs';
1
+ import { C as ClassNames, P as PageData, S as SearchOptions, a as PDFSearchViewerOptions, b as SpanData, c as SearchMatch } from '../PDFSearchViewer-rNp5NirR.cjs';
2
+ export { E as EventEmitter, d as PDFSearchViewer, e as PDFSearchViewerEventMap, f as PDFSource } from '../PDFSearchViewer-rNp5NirR.cjs';
3
3
 
4
4
  interface SearchControllerOptions {
5
5
  classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;
@@ -106,6 +106,7 @@ declare class PDFRenderer {
106
106
  loadDocument(source: File | ArrayBuffer | Uint8Array | string): Promise<number>;
107
107
  /**
108
108
  * Render all pages into the container.
109
+ * Preserves scroll position across re-renders (e.g. zoom).
109
110
  * Returns PageData[] for search/highlight.
110
111
  */
111
112
  renderAllPages(): Promise<PageData[]>;
@@ -138,7 +139,7 @@ interface MatchRange {
138
139
  * Algorithm:
139
140
  * 1. Concatenate all span texts into one string (fullText)
140
141
  * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText
141
- * 3. Run regex on fullText
142
+ * 3. Run regex or fuzzy search on fullText
142
143
  * 4. Map each match back to span ranges via charMap
143
144
  */
144
145
  declare function searchPage(spans: SpanData[], query: string, options?: SearchOptions): MatchRange[][];
@@ -1,5 +1,5 @@
1
- import { C as ClassNames, P as PageData, S as SearchOptions, a as PDFSearchViewerOptions, b as SpanData, c as SearchMatch } from '../PDFSearchViewer-CQaAFXQs.js';
2
- export { E as EventEmitter, d as PDFSearchViewer, e as PDFSearchViewerEventMap, f as PDFSource } from '../PDFSearchViewer-CQaAFXQs.js';
1
+ import { C as ClassNames, P as PageData, S as SearchOptions, a as PDFSearchViewerOptions, b as SpanData, c as SearchMatch } from '../PDFSearchViewer-rNp5NirR.js';
2
+ export { E as EventEmitter, d as PDFSearchViewer, e as PDFSearchViewerEventMap, f as PDFSource } from '../PDFSearchViewer-rNp5NirR.js';
3
3
 
4
4
  interface SearchControllerOptions {
5
5
  classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;
@@ -106,6 +106,7 @@ declare class PDFRenderer {
106
106
  loadDocument(source: File | ArrayBuffer | Uint8Array | string): Promise<number>;
107
107
  /**
108
108
  * Render all pages into the container.
109
+ * Preserves scroll position across re-renders (e.g. zoom).
109
110
  * Returns PageData[] for search/highlight.
110
111
  */
111
112
  renderAllPages(): Promise<PageData[]>;
@@ -138,7 +139,7 @@ interface MatchRange {
138
139
  * Algorithm:
139
140
  * 1. Concatenate all span texts into one string (fullText)
140
141
  * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText
141
- * 3. Run regex on fullText
142
+ * 3. Run regex or fuzzy search on fullText
142
143
  * 4. Map each match back to span ranges via charMap
143
144
  */
144
145
  declare function searchPage(spans: SpanData[], query: string, options?: SearchOptions): MatchRange[][];
@@ -11,7 +11,7 @@ import {
11
11
  SearchController,
12
12
  ZOOM_STEP,
13
13
  searchPage
14
- } from "../chunk-ABW444BW.js";
14
+ } from "../chunk-AA4UECNB.js";
15
15
  export {
16
16
  DEFAULT_CLASS_NAMES,
17
17
  DEFAULT_PAGE_GAP,
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunkYH5HZXPGcjs = require('../chunk-YH5HZXPG.cjs');
8
+ var _chunkY5LFTIMYcjs = require('../chunk-Y5LFTIMY.cjs');
9
9
 
10
10
  // src/react/usePDFRenderer.ts
11
11
  var _react = require('react');
@@ -27,7 +27,7 @@ function usePDFRenderer(pdfjsLib, options = {}) {
27
27
  const getRenderer = _react.useCallback.call(void 0, () => {
28
28
  if (!containerRef.current) throw new Error("Container ref not attached");
29
29
  if (!rendererRef.current) {
30
- const r = new (0, _chunkYH5HZXPGcjs.PDFRenderer)(containerRef.current, optionsRef.current);
30
+ const r = new (0, _chunkY5LFTIMYcjs.PDFRenderer)(containerRef.current, optionsRef.current);
31
31
  r.setPdfjsLib(pdfjsLib);
32
32
  rendererRef.current = r;
33
33
  }
@@ -64,13 +64,13 @@ function usePDFRenderer(pdfjsLib, options = {}) {
64
64
  const zoomIn = _react.useCallback.call(void 0, async () => {
65
65
  const renderer = getRenderer();
66
66
  const current = renderer.getScale() === "auto" ? renderer.getEffectiveScale() : renderer.getScale();
67
- const newScale = Math.min(current + _chunkYH5HZXPGcjs.ZOOM_STEP, _chunkYH5HZXPGcjs.MAX_SCALE);
67
+ const newScale = Math.min(current + _chunkY5LFTIMYcjs.ZOOM_STEP, _chunkY5LFTIMYcjs.MAX_SCALE);
68
68
  return setScale(newScale);
69
69
  }, [getRenderer, setScale]);
70
70
  const zoomOut = _react.useCallback.call(void 0, async () => {
71
71
  const renderer = getRenderer();
72
72
  const current = renderer.getScale() === "auto" ? renderer.getEffectiveScale() : renderer.getScale();
73
- const newScale = Math.max(current - _chunkYH5HZXPGcjs.ZOOM_STEP, _chunkYH5HZXPGcjs.MIN_SCALE);
73
+ const newScale = Math.max(current - _chunkY5LFTIMYcjs.ZOOM_STEP, _chunkY5LFTIMYcjs.MIN_SCALE);
74
74
  return setScale(newScale);
75
75
  }, [getRenderer, setScale]);
76
76
  const download = _react.useCallback.call(void 0,
@@ -108,7 +108,7 @@ function useSearchController(pages, options = {}) {
108
108
  const [current, setCurrent] = _react.useState.call(void 0, -1);
109
109
  const [total, setTotal] = _react.useState.call(void 0, 0);
110
110
  if (!controllerRef.current) {
111
- controllerRef.current = new (0, _chunkYH5HZXPGcjs.SearchController)(options);
111
+ controllerRef.current = new (0, _chunkY5LFTIMYcjs.SearchController)(options);
112
112
  }
113
113
  _react.useEffect.call(void 0, () => {
114
114
  const ctrl = controllerRef.current;
@@ -170,7 +170,7 @@ var PDFSearchViewer2 = _react.forwardRef.call(void 0, function PDFSearchViewer3(
170
170
  callbackRefs.current = { onLoad, onSearch, onMatchChange, onZoom, onError };
171
171
  _react.useEffect.call(void 0, () => {
172
172
  if (!containerRef.current || !pdfjsLib) return;
173
- const core = new (0, _chunkYH5HZXPGcjs.PDFSearchViewer)(
173
+ const core = new (0, _chunkY5LFTIMYcjs.PDFSearchViewer)(
174
174
  containerRef.current,
175
175
  pdfjsLib,
176
176
  _nullishCoalesce(viewerOptions, () => ( {}))
@@ -1,5 +1,5 @@
1
- import { P as PageData, f as PDFSource, a as PDFSearchViewerOptions, C as ClassNames, S as SearchOptions, d as PDFSearchViewer$1 } from '../PDFSearchViewer-CQaAFXQs.cjs';
2
- export { c as SearchMatch } from '../PDFSearchViewer-CQaAFXQs.cjs';
1
+ import { P as PageData, f as PDFSource, a as PDFSearchViewerOptions, C as ClassNames, S as SearchOptions, d as PDFSearchViewer$1 } from '../PDFSearchViewer-rNp5NirR.cjs';
2
+ export { c as SearchMatch } from '../PDFSearchViewer-rNp5NirR.cjs';
3
3
  import * as react from 'react';
4
4
  import { CSSProperties } from 'react';
5
5
 
@@ -1,5 +1,5 @@
1
- import { P as PageData, f as PDFSource, a as PDFSearchViewerOptions, C as ClassNames, S as SearchOptions, d as PDFSearchViewer$1 } from '../PDFSearchViewer-CQaAFXQs.js';
2
- export { c as SearchMatch } from '../PDFSearchViewer-CQaAFXQs.js';
1
+ import { P as PageData, f as PDFSource, a as PDFSearchViewerOptions, C as ClassNames, S as SearchOptions, d as PDFSearchViewer$1 } from '../PDFSearchViewer-rNp5NirR.js';
2
+ export { c as SearchMatch } from '../PDFSearchViewer-rNp5NirR.js';
3
3
  import * as react from 'react';
4
4
  import { CSSProperties } from 'react';
5
5
 
@@ -5,7 +5,7 @@ import {
5
5
  PDFSearchViewer,
6
6
  SearchController,
7
7
  ZOOM_STEP
8
- } from "../chunk-ABW444BW.js";
8
+ } from "../chunk-AA4UECNB.js";
9
9
 
10
10
  // src/react/usePDFRenderer.ts
11
11
  import { useRef, useEffect, useCallback, useState } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-search-highlight",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Drop-in PDF viewer with text search and highlight. Vanilla JS core + React wrapper.",
5
5
  "type": "module",
6
6
  "main": "./dist/core/index.cjs",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/EventEmitter.ts","../src/core/constants.ts","../src/core/PDFRenderer.ts","../src/core/SearchEngine.ts","../src/core/HighlightManager.ts","../src/core/PDFSearchViewer.ts","../src/core/SearchController.ts"],"sourcesContent":["type Listener<T> = (data: T) => void;\n\nexport class EventEmitter<EventMap extends { [key: string]: unknown }> {\n private listeners = new Map<keyof EventMap, Set<Listener<any>>>();\n\n on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(listener);\n return this;\n }\n\n off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n protected emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {\n this.listeners.get(event)?.forEach((fn) => fn(data));\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","import type { ClassNames } from '../types';\n\nexport const DEFAULT_CLASS_NAMES: Required<ClassNames> = {\n container: 'psh-container',\n page: 'psh-page',\n canvas: 'psh-canvas',\n textLayer: 'psh-text-layer',\n pageLabel: 'psh-page-label',\n highlight: 'highlight',\n activeHighlight: 'active',\n};\n\nexport const DEFAULT_SCALE = 'auto' as number | 'auto';\nexport const DEFAULT_PAGE_GAP = 20;\n\nexport const ZOOM_STEP = 0.25;\nexport const MIN_SCALE = 0.25;\nexport const MAX_SCALE = 5;\n","import type { PDFSearchViewerOptions, ClassNames, PageData, SpanData } from '../types';\nimport { DEFAULT_CLASS_NAMES, DEFAULT_SCALE, DEFAULT_PAGE_GAP } from './constants';\n\n// pdfjs-dist types\ntype PDFDocumentProxy = any;\ntype PDFPageProxy = any;\n\n/**\n * Renders PDF pages into a container using canvas + text layer.\n *\n * Text layer approach (matching demo):\n * - Extract text content from each page\n * - Create absolutely-positioned <span> elements overlaying the canvas\n * - Position spans using the transform matrix from pdf.js\n * - Spans are transparent (for text selection) but allow DOM-based search/highlight\n */\nexport class PDFRenderer {\n private container: HTMLElement;\n private scale: number | 'auto';\n private pageGap: number;\n private cls: Required<ClassNames>;\n private workerSrc?: string;\n private pdfDoc: PDFDocumentProxy | null = null;\n private pageData: PageData[] = [];\n private pdfjsLib: any = null;\n private effectiveScale: number = 1;\n\n constructor(container: HTMLElement, options: PDFSearchViewerOptions) {\n this.container = container;\n this.scale = options.scale ?? DEFAULT_SCALE;\n this.pageGap = options.pageGap ?? DEFAULT_PAGE_GAP;\n this.workerSrc = options.workerSrc;\n this.cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n }\n\n /**\n * Set the pdfjs-dist library reference.\n * Must be called before loadDocument.\n */\n setPdfjsLib(lib: any): void {\n this.pdfjsLib = lib;\n if (this.workerSrc) {\n lib.GlobalWorkerOptions.workerSrc = this.workerSrc;\n }\n }\n\n /**\n * Load a PDF from File, ArrayBuffer, URL string, or Uint8Array.\n */\n async loadDocument(\n source: File | ArrayBuffer | Uint8Array | string\n ): Promise<number> {\n if (!this.pdfjsLib) {\n throw new Error(\n 'pdfjs-dist not set. Call setPdfjsLib(pdfjsLib) before loading a document.'\n );\n }\n\n this.cleanup();\n\n let data: ArrayBuffer | Uint8Array | { url: string };\n if (source instanceof File) {\n data = await source.arrayBuffer();\n } else if (typeof source === 'string') {\n data = { url: source };\n } else {\n data = source;\n }\n\n const loadingTask = this.pdfjsLib.getDocument({ data });\n this.pdfDoc = await loadingTask.promise;\n return this.pdfDoc.numPages;\n }\n\n /**\n * Render all pages into the container.\n * Returns PageData[] for search/highlight.\n */\n async renderAllPages(): Promise<PageData[]> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n this.container.innerHTML = '';\n this.container.classList.add(this.cls.container);\n this.pageData = [];\n\n const numPages = this.pdfDoc.numPages;\n\n for (let i = 1; i <= numPages; i++) {\n const page = await this.pdfDoc.getPage(i);\n const pd = await this.renderPage(page, i, numPages);\n this.pageData.push(pd);\n }\n\n return this.pageData;\n }\n\n private async renderPage(\n page: PDFPageProxy,\n pageNum: number,\n totalPages: number\n ): Promise<PageData> {\n const scale = this.calculateScale(page);\n if (pageNum === 1) this.effectiveScale = scale;\n const vp = page.getViewport({ scale });\n\n // Page container\n const container = document.createElement('div');\n container.className = this.cls.page;\n container.style.position = 'relative';\n container.style.width = vp.width + 'px';\n container.style.height = vp.height + 'px';\n container.style.margin = '0 auto';\n container.style.marginBottom = this.pageGap + 'px';\n container.style.overflow = 'hidden';\n container.dataset.page = String(pageNum);\n\n // Canvas (2x for retina)\n const canvas = document.createElement('canvas');\n canvas.className = this.cls.canvas;\n canvas.width = vp.width * 2;\n canvas.height = vp.height * 2;\n canvas.style.width = vp.width + 'px';\n canvas.style.height = vp.height + 'px';\n canvas.style.display = 'block';\n const ctx = canvas.getContext('2d')!;\n ctx.scale(2, 2);\n await page.render({ canvasContext: ctx, viewport: vp }).promise;\n\n // Text layer\n const textLayer = document.createElement('div');\n textLayer.className = this.cls.textLayer;\n textLayer.style.position = 'absolute';\n textLayer.style.top = '0';\n textLayer.style.left = '0';\n textLayer.style.right = '0';\n textLayer.style.bottom = '0';\n textLayer.style.overflow = 'hidden';\n textLayer.style.lineHeight = '1';\n\n const tc = await page.getTextContent();\n const spans: SpanData[] = [];\n\n for (const item of tc.items) {\n if (!item.str && !item.hasEOL) continue;\n\n const tx = this.pdfjsLib.Util.transform(vp.transform, item.transform);\n const span = document.createElement('span');\n span.textContent = item.str || '';\n const fh = Math.hypot(tx[2], tx[3]);\n span.style.position = 'absolute';\n span.style.left = tx[4] + 'px';\n span.style.top = (tx[5] - fh) + 'px';\n span.style.fontSize = fh + 'px';\n span.style.color = 'transparent';\n span.style.whiteSpace = 'pre';\n span.style.cursor = 'text';\n span.style.transformOrigin = '0% 0%';\n if (item.fontName) span.style.fontFamily = item.fontName;\n\n const sw = tx[0] / fh;\n if (Math.abs(sw - 1) > 0.01) {\n span.style.transform = `scaleX(${sw})`;\n }\n\n textLayer.appendChild(span);\n spans.push({\n el: span,\n text: item.str || '',\n hasEOL: !!item.hasEOL,\n });\n }\n\n container.appendChild(canvas);\n container.appendChild(textLayer);\n this.container.appendChild(container);\n\n // Page label\n const label = document.createElement('div');\n label.className = this.cls.pageLabel;\n label.textContent = `Page ${pageNum} / ${totalPages}`;\n this.container.appendChild(label);\n\n return { container, spans };\n }\n\n private calculateScale(page: PDFPageProxy): number {\n if (this.scale !== 'auto' && typeof this.scale === 'number') {\n return this.scale;\n }\n const defaultVp = page.getViewport({ scale: 1 });\n const containerWidth = this.container.clientWidth || 800;\n return Math.min(containerWidth / defaultVp.width, 2);\n }\n\n /** Set the scale for subsequent renders. */\n setScale(scale: number | 'auto'): void {\n this.scale = scale;\n }\n\n /** Get the configured scale setting. */\n getScale(): number | 'auto' {\n return this.scale;\n }\n\n /** Get the actual numeric scale used in the last render. */\n getEffectiveScale(): number {\n return this.effectiveScale;\n }\n\n /**\n * Download the currently loaded PDF.\n */\n async download(filename: string = 'document.pdf'): Promise<void> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n const data = await this.pdfDoc.getData();\n const blob = new Blob([data as BlobPart], { type: 'application/pdf' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n\n getClassNames(): Required<ClassNames> {\n return this.cls;\n }\n\n getPageData(): PageData[] {\n return this.pageData;\n }\n\n getPageCount(): number {\n return this.pdfDoc?.numPages ?? 0;\n }\n\n cleanup(): void {\n this.pdfDoc?.destroy();\n this.pdfDoc = null;\n this.pageData = [];\n this.container.innerHTML = '';\n }\n}\n","import type { SearchOptions, SpanData } from '../types';\n\nexport interface CharMapEntry {\n spanIdx: number;\n charIdx: number;\n}\n\nexport interface MatchRange {\n spanIdx: number;\n start: number;\n end: number;\n}\n\nexport interface SearchResult {\n /** Array of span ranges for each match */\n matchRanges: MatchRange[][];\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Build a flexible regex from query.\n *\n * For queries < 200 chars (after removing whitespace):\n * - Strip all whitespace from query\n * - Insert \\s* between every character\n * → \"and expensive\" becomes a\\s*n\\s*d\\s*e\\s*x\\s*p\\s*e\\s*n\\s*s\\s*i\\s*v\\s*e\n * → Matches regardless of whitespace differences in PDF text\n *\n * For queries >= 200 chars:\n * - Split by whitespace, join with \\s+\n */\nfunction buildFlexibleRegex(\n query: string,\n options: SearchOptions\n): RegExp | null {\n const trimmed = query.trim();\n if (!trimmed) return null;\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const flexibleWhitespace = options.flexibleWhitespace ?? true;\n\n if (!flexibleWhitespace) {\n // Simple literal search\n const pattern = escapeRegex(trimmed);\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Remove all whitespace chars from query\n const chars = [...trimmed].filter((c) => !/\\s/.test(c));\n if (chars.length === 0) return null;\n\n if (chars.length > 200) {\n // Fallback: flexible between tokens only\n const tokens = trimmed.split(/\\s+/);\n const pattern = tokens.map((t) => escapeRegex(t)).join('\\\\s+');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Insert \\s* between every character\n const pattern = chars.map((c) => escapeRegex(c)).join('\\\\s*');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n}\n\n/**\n * Search for text across page spans using charMap-based matching.\n *\n * Algorithm:\n * 1. Concatenate all span texts into one string (fullText)\n * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText\n * 3. Run regex on fullText\n * 4. Map each match back to span ranges via charMap\n */\nexport function searchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions = {}\n): MatchRange[][] {\n const regex = buildFlexibleRegex(query, options);\n if (!regex) return [];\n\n // Build fullText and charMap\n let fullText = '';\n const charMap: CharMapEntry[] = [];\n\n spans.forEach((s, si) => {\n for (let ci = 0; ci < s.text.length; ci++) {\n charMap.push({ spanIdx: si, charIdx: ci });\n fullText += s.text[ci];\n }\n });\n\n // Find all regex matches\n const allMatchRanges: MatchRange[][] = [];\n let m: RegExpExecArray | null;\n regex.lastIndex = 0;\n\n while ((m = regex.exec(fullText)) !== null) {\n const start = m.index;\n const end = start + m[0].length;\n\n // Map to span ranges\n const range: MatchRange[] = [];\n for (let k = start; k < end; k++) {\n const cm = charMap[k];\n const last = range[range.length - 1];\n if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {\n last.end = cm.charIdx + 1;\n } else {\n range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });\n }\n }\n allMatchRanges.push(range);\n\n // Prevent infinite loop for zero-length matches\n if (m[0].length === 0) regex.lastIndex++;\n }\n\n return allMatchRanges;\n}\n","import type { SearchMatch, SpanData, PageData } from '../types';\nimport type { MatchRange } from './SearchEngine';\n\n/**\n * Manages cross-span highlighting using the charMap approach.\n *\n * Algorithm (from demo):\n * 1. Group match ranges by spanIdx\n * 2. For each affected span, replace textContent with a DocumentFragment:\n * - Plain text nodes for non-matching parts\n * - <mark> elements for matching parts\n * 3. Collect marks per match for navigation\n */\nexport class HighlightManager {\n private matches: SearchMatch[] = [];\n private currentMatch = -1;\n private highlightClass: string;\n private activeHighlightClass: string;\n\n constructor(highlightClass: string, activeHighlightClass: string) {\n this.highlightClass = highlightClass;\n this.activeHighlightClass = activeHighlightClass;\n }\n\n /**\n * Apply highlights for all matches on a page.\n * Returns the SearchMatch[] (array of mark groups).\n */\n applyHighlights(\n pageSpans: SpanData[],\n matchRanges: MatchRange[][]\n ): SearchMatch[] {\n if (!matchRanges.length) return [];\n\n // Group ranges by spanIdx, keeping track of which match they belong to\n const spanRanges: Record<\n number,\n { start: number; end: number; matchIdx: number }[]\n > = {};\n\n matchRanges.forEach((range, mi) => {\n range.forEach((r) => {\n if (!spanRanges[r.spanIdx]) spanRanges[r.spanIdx] = [];\n spanRanges[r.spanIdx].push({ start: r.start, end: r.end, matchIdx: mi });\n });\n });\n\n // Collect marks per match\n const matchMarks: HTMLElement[][] = matchRanges.map(() => []);\n\n // For each affected span, rebuild DOM with highlights\n for (const siStr of Object.keys(spanRanges)) {\n const si = parseInt(siStr, 10);\n const s = pageSpans[si];\n const ranges = spanRanges[si].sort((a, b) => a.start - b.start);\n\n const frag = document.createDocumentFragment();\n let last = 0;\n\n for (const r of ranges) {\n const actualStart = Math.max(r.start, last);\n\n // Add plain text before highlight\n if (actualStart > last) {\n frag.appendChild(document.createTextNode(s.text.slice(last, actualStart)));\n }\n\n // Add highlight mark\n if (actualStart < r.end) {\n const mark = document.createElement('mark');\n mark.className = this.highlightClass;\n mark.textContent = s.text.slice(actualStart, r.end);\n frag.appendChild(mark);\n matchMarks[r.matchIdx].push(mark);\n }\n\n last = Math.max(last, r.end);\n }\n\n // Add remaining plain text\n if (last < s.text.length) {\n frag.appendChild(document.createTextNode(s.text.slice(last)));\n }\n\n // Replace span content\n s.el.textContent = '';\n s.el.appendChild(frag);\n }\n\n return matchMarks\n .filter((marks) => marks.length > 0)\n .map((marks) => ({ marks }));\n }\n\n /**\n * Add matches to the global list.\n */\n addMatches(newMatches: SearchMatch[]): void {\n this.matches.push(...newMatches);\n }\n\n /**\n * Clear all highlights and restore original span text.\n */\n clearHighlights(allPageData: PageData[]): void {\n allPageData.forEach((pd) => {\n pd.spans.forEach((s) => {\n s.el.textContent = s.text;\n });\n });\n this.matches = [];\n this.currentMatch = -1;\n }\n\n /**\n * Set active match by index. Applies active CSS class and scrolls into view.\n */\n setActiveMatch(index: number): void {\n // Remove active class from previous\n if (this.currentMatch >= 0 && this.currentMatch < this.matches.length) {\n this.matches[this.currentMatch].marks.forEach((m) =>\n m.classList.remove(this.activeHighlightClass)\n );\n }\n\n this.currentMatch = index;\n\n if (index >= 0 && index < this.matches.length) {\n this.matches[index].marks.forEach((m) =>\n m.classList.add(this.activeHighlightClass)\n );\n // Scroll first mark into view\n this.matches[index].marks[0]?.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n });\n }\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n next(): number {\n if (this.matches.length === 0) return -1;\n const newIdx = (this.currentMatch + 1) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prev(): number {\n if (this.matches.length === 0) return -1;\n const newIdx =\n (this.currentMatch - 1 + this.matches.length) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n getCurrentIndex(): number {\n return this.currentMatch;\n }\n\n getTotal(): number {\n return this.matches.length;\n }\n\n getMatches(): SearchMatch[] {\n return this.matches;\n }\n}\n","import { EventEmitter } from './EventEmitter';\nimport { PDFRenderer } from './PDFRenderer';\nimport { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES, ZOOM_STEP, MIN_SCALE, MAX_SCALE } from './constants';\nimport type {\n PDFSearchViewerOptions,\n SearchOptions,\n PDFSearchViewerEventMap,\n PageData,\n} from '../types';\n\nexport type PDFSource = File | ArrayBuffer | Uint8Array | string;\n\n/**\n * Main PDF viewer with search and highlight functionality.\n *\n * Usage:\n * ```js\n * import * as pdfjsLib from 'pdfjs-dist';\n * import { PDFSearchViewer } from 'pdf-search-highlight';\n *\n * const viewer = new PDFSearchViewer(container, pdfjsLib, {\n * classNames: {\n * page: 'my-page',\n * highlight: 'my-highlight',\n * activeHighlight: 'my-active',\n * }\n * });\n * await viewer.loadPDF(file);\n * viewer.search('hello');\n * viewer.nextMatch();\n * ```\n */\nexport class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {\n private renderer: PDFRenderer;\n private highlightManager: HighlightManager;\n private pageData: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n private destroyed = false;\n\n constructor(\n container: HTMLElement,\n pdfjsLib: any,\n options: PDFSearchViewerOptions = {}\n ) {\n super();\n\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n\n this.renderer = new PDFRenderer(container, options);\n this.renderer.setPdfjsLib(pdfjsLib);\n this.highlightManager = new HighlightManager(\n cls.highlight,\n cls.activeHighlight\n );\n }\n\n /**\n * Load and render a PDF document.\n */\n async loadPDF(source: PDFSource): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n try {\n await this.renderer.loadDocument(source);\n this.pageData = await this.renderer.renderAllPages();\n const pageCount = this.renderer.getPageCount();\n this.emit('load', { pageCount });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', { error, context: 'loadPDF' });\n throw error;\n }\n }\n\n /**\n * Search for text across all pages.\n * Clears previous highlights and creates new ones.\n */\n search(query: string, options: SearchOptions = {}): number {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n // Clear previous highlights\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.emit('search', { query, total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n return 0;\n }\n\n // Search each page and apply highlights\n for (const pd of this.pageData) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n\n // Auto-activate first match\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.emit('search', { query, total });\n this.emit('matchchange', {\n current: total > 0 ? 0 : -1,\n total,\n });\n\n return total;\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n nextMatch(): number {\n const idx = this.highlightManager.next();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prevMatch(): number {\n const idx = this.highlightManager.prev();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Clear all search highlights.\n */\n clearSearch(): void {\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = '';\n this.emit('search', { query: '', total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n }\n\n /** Get the current scale setting. */\n getScale(): number | 'auto' {\n return this.renderer.getScale();\n }\n\n /** Set scale and re-render. Preserves current search state. */\n async setScale(scale: number | 'auto'): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n this.renderer.setScale(scale);\n await this.rerender();\n this.emit('zoom', { scale: this.renderer.getEffectiveScale() });\n }\n\n /** Zoom in by one step. */\n async zoomIn(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.min(current + ZOOM_STEP, MAX_SCALE);\n await this.setScale(newScale);\n }\n\n /** Zoom out by one step. */\n async zoomOut(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.max(current - ZOOM_STEP, MIN_SCALE);\n await this.setScale(newScale);\n }\n\n /** Download the currently loaded PDF. */\n async download(filename?: string): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n await this.renderer.download(filename);\n }\n\n private resolveCurrentScale(): number {\n const s = this.renderer.getScale();\n return s === 'auto' ? this.renderer.getEffectiveScale() : s;\n }\n\n private async rerender(): Promise<void> {\n this.highlightManager.clearHighlights(this.pageData);\n this.pageData = await this.renderer.renderAllPages();\n if (this.lastQuery.trim()) {\n this.search(this.lastQuery, this.lastSearchOptions);\n }\n }\n\n /**\n * Get total number of pages.\n */\n getPageCount(): number {\n return this.renderer.getPageCount();\n }\n\n /**\n * Get current active match index (0-based). -1 if none.\n */\n getCurrentMatchIndex(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /**\n * Get total number of matches.\n */\n getMatchCount(): number {\n return this.highlightManager.getTotal();\n }\n\n /**\n * Destroy the viewer, release all resources.\n */\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.highlightManager.clearHighlights(this.pageData);\n this.renderer.cleanup();\n this.removeAllListeners();\n this.pageData = [];\n }\n}\n","import { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES } from './constants';\nimport type { SearchOptions, ClassNames, PageData } from '../types';\n\nexport interface SearchControllerOptions {\n classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;\n}\n\n/**\n * Headless search + highlight controller.\n * Does NOT render PDF — works with any PageData[] you provide.\n *\n * Use this when you want full control over:\n * - Where the PDF is rendered\n * - Where the search UI lives\n * - How search results are displayed\n *\n * Usage:\n * ```js\n * import { PDFRenderer, SearchController } from 'pdf-search-highlight';\n *\n * // Render PDF wherever you want\n * const renderer = new PDFRenderer(pdfContainer, pdfjsLib, {});\n * const pages = await renderer.renderAllPages();\n *\n * // Search controller — no UI, just logic\n * const search = new SearchController();\n * search.setPages(pages);\n *\n * // Wire up your own UI\n * input.oninput = () => search.search(input.value);\n * nextBtn.onclick = () => search.next();\n * prevBtn.onclick = () => search.prev();\n *\n * // React to changes\n * search.onChange = ({ current, total }) => {\n * label.textContent = total > 0 ? `${current + 1}/${total}` : '';\n * };\n * ```\n */\nexport class SearchController {\n private highlightManager: HighlightManager;\n private pages: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n\n /** Callback fired when match state changes (search, next, prev, clear). */\n onChange: ((state: { current: number; total: number; query: string }) => void) | null = null;\n\n constructor(options: SearchControllerOptions = {}) {\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n this.highlightManager = new HighlightManager(cls.highlight, cls.activeHighlight);\n }\n\n /**\n * Set the pages to search on.\n * Call this after rendering PDF pages.\n */\n setPages(pages: PageData[]): void {\n const savedQuery = this.lastQuery;\n const savedOptions = this.lastSearchOptions;\n this.clear();\n this.pages = pages;\n // Re-apply search if there was an active query (e.g. after zoom)\n if (savedQuery.trim()) {\n this.search(savedQuery, savedOptions);\n }\n }\n\n /**\n * Search for text across all pages.\n * Returns total number of matches.\n */\n search(query: string, options: SearchOptions = {}): number {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.notify();\n return 0;\n }\n\n for (const pd of this.pages) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.notify();\n return total;\n }\n\n /** Navigate to next match. Returns new index. */\n next(): number {\n const idx = this.highlightManager.next();\n this.notify();\n return idx;\n }\n\n /** Navigate to previous match. Returns new index. */\n prev(): number {\n const idx = this.highlightManager.prev();\n this.notify();\n return idx;\n }\n\n /** Go to a specific match by index. */\n goTo(index: number): void {\n this.highlightManager.setActiveMatch(index);\n this.notify();\n }\n\n /** Clear all highlights. */\n clear(): void {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = '';\n this.notify();\n }\n\n /** Current match index (0-based). -1 if none. */\n get current(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /** Total number of matches. */\n get total(): number {\n return this.highlightManager.getTotal();\n }\n\n /** Last searched query. */\n get query(): string {\n return this.lastQuery;\n }\n\n private notify(): void {\n this.onChange?.({\n current: this.highlightManager.getCurrentIndex(),\n total: this.highlightManager.getTotal(),\n query: this.lastQuery,\n });\n }\n}\n"],"mappings":"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","names":["pattern"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/hoangnguyen/Desktop/pdf-search-highlight/dist/chunk-YH5HZXPG.cjs","../src/core/EventEmitter.ts","../src/core/constants.ts","../src/core/PDFRenderer.ts","../src/core/SearchEngine.ts","../src/core/HighlightManager.ts","../src/core/PDFSearchViewer.ts","../src/core/SearchController.ts"],"names":[],"mappings":"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","file":"/Users/hoangnguyen/Desktop/pdf-search-highlight/dist/chunk-YH5HZXPG.cjs","sourcesContent":[null,"type Listener<T> = (data: T) => void;\n\nexport class EventEmitter<EventMap extends { [key: string]: unknown }> {\n private listeners = new Map<keyof EventMap, Set<Listener<any>>>();\n\n on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(listener);\n return this;\n }\n\n off<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n protected emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {\n this.listeners.get(event)?.forEach((fn) => fn(data));\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","import type { ClassNames } from '../types';\n\nexport const DEFAULT_CLASS_NAMES: Required<ClassNames> = {\n container: 'psh-container',\n page: 'psh-page',\n canvas: 'psh-canvas',\n textLayer: 'psh-text-layer',\n pageLabel: 'psh-page-label',\n highlight: 'highlight',\n activeHighlight: 'active',\n};\n\nexport const DEFAULT_SCALE = 'auto' as number | 'auto';\nexport const DEFAULT_PAGE_GAP = 20;\n\nexport const ZOOM_STEP = 0.25;\nexport const MIN_SCALE = 0.25;\nexport const MAX_SCALE = 5;\n","import type { PDFSearchViewerOptions, ClassNames, PageData, SpanData } from '../types';\nimport { DEFAULT_CLASS_NAMES, DEFAULT_SCALE, DEFAULT_PAGE_GAP } from './constants';\n\n// pdfjs-dist types\ntype PDFDocumentProxy = any;\ntype PDFPageProxy = any;\n\n/**\n * Renders PDF pages into a container using canvas + text layer.\n *\n * Text layer approach (matching demo):\n * - Extract text content from each page\n * - Create absolutely-positioned <span> elements overlaying the canvas\n * - Position spans using the transform matrix from pdf.js\n * - Spans are transparent (for text selection) but allow DOM-based search/highlight\n */\nexport class PDFRenderer {\n private container: HTMLElement;\n private scale: number | 'auto';\n private pageGap: number;\n private cls: Required<ClassNames>;\n private workerSrc?: string;\n private pdfDoc: PDFDocumentProxy | null = null;\n private pageData: PageData[] = [];\n private pdfjsLib: any = null;\n private effectiveScale: number = 1;\n\n constructor(container: HTMLElement, options: PDFSearchViewerOptions) {\n this.container = container;\n this.scale = options.scale ?? DEFAULT_SCALE;\n this.pageGap = options.pageGap ?? DEFAULT_PAGE_GAP;\n this.workerSrc = options.workerSrc;\n this.cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n }\n\n /**\n * Set the pdfjs-dist library reference.\n * Must be called before loadDocument.\n */\n setPdfjsLib(lib: any): void {\n this.pdfjsLib = lib;\n if (this.workerSrc) {\n lib.GlobalWorkerOptions.workerSrc = this.workerSrc;\n }\n }\n\n /**\n * Load a PDF from File, ArrayBuffer, URL string, or Uint8Array.\n */\n async loadDocument(\n source: File | ArrayBuffer | Uint8Array | string\n ): Promise<number> {\n if (!this.pdfjsLib) {\n throw new Error(\n 'pdfjs-dist not set. Call setPdfjsLib(pdfjsLib) before loading a document.'\n );\n }\n\n this.cleanup();\n\n let data: ArrayBuffer | Uint8Array | { url: string };\n if (source instanceof File) {\n data = await source.arrayBuffer();\n } else if (typeof source === 'string') {\n data = { url: source };\n } else {\n data = source;\n }\n\n const loadingTask = this.pdfjsLib.getDocument({ data });\n this.pdfDoc = await loadingTask.promise;\n return this.pdfDoc.numPages;\n }\n\n /**\n * Render all pages into the container.\n * Returns PageData[] for search/highlight.\n */\n async renderAllPages(): Promise<PageData[]> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n this.container.innerHTML = '';\n this.container.classList.add(this.cls.container);\n this.pageData = [];\n\n const numPages = this.pdfDoc.numPages;\n\n for (let i = 1; i <= numPages; i++) {\n const page = await this.pdfDoc.getPage(i);\n const pd = await this.renderPage(page, i, numPages);\n this.pageData.push(pd);\n }\n\n return this.pageData;\n }\n\n private async renderPage(\n page: PDFPageProxy,\n pageNum: number,\n totalPages: number\n ): Promise<PageData> {\n const scale = this.calculateScale(page);\n if (pageNum === 1) this.effectiveScale = scale;\n const vp = page.getViewport({ scale });\n\n // Page container\n const container = document.createElement('div');\n container.className = this.cls.page;\n container.style.position = 'relative';\n container.style.width = vp.width + 'px';\n container.style.height = vp.height + 'px';\n container.style.margin = '0 auto';\n container.style.marginBottom = this.pageGap + 'px';\n container.style.overflow = 'hidden';\n container.dataset.page = String(pageNum);\n\n // Canvas (2x for retina)\n const canvas = document.createElement('canvas');\n canvas.className = this.cls.canvas;\n canvas.width = vp.width * 2;\n canvas.height = vp.height * 2;\n canvas.style.width = vp.width + 'px';\n canvas.style.height = vp.height + 'px';\n canvas.style.display = 'block';\n const ctx = canvas.getContext('2d')!;\n ctx.scale(2, 2);\n await page.render({ canvasContext: ctx, viewport: vp }).promise;\n\n // Text layer\n const textLayer = document.createElement('div');\n textLayer.className = this.cls.textLayer;\n textLayer.style.position = 'absolute';\n textLayer.style.top = '0';\n textLayer.style.left = '0';\n textLayer.style.right = '0';\n textLayer.style.bottom = '0';\n textLayer.style.overflow = 'hidden';\n textLayer.style.lineHeight = '1';\n\n const tc = await page.getTextContent();\n const spans: SpanData[] = [];\n\n for (const item of tc.items) {\n if (!item.str && !item.hasEOL) continue;\n\n const tx = this.pdfjsLib.Util.transform(vp.transform, item.transform);\n const span = document.createElement('span');\n span.textContent = item.str || '';\n const fh = Math.hypot(tx[2], tx[3]);\n span.style.position = 'absolute';\n span.style.left = tx[4] + 'px';\n span.style.top = (tx[5] - fh) + 'px';\n span.style.fontSize = fh + 'px';\n span.style.color = 'transparent';\n span.style.whiteSpace = 'pre';\n span.style.cursor = 'text';\n span.style.transformOrigin = '0% 0%';\n if (item.fontName) span.style.fontFamily = item.fontName;\n\n const sw = tx[0] / fh;\n if (Math.abs(sw - 1) > 0.01) {\n span.style.transform = `scaleX(${sw})`;\n }\n\n textLayer.appendChild(span);\n spans.push({\n el: span,\n text: item.str || '',\n hasEOL: !!item.hasEOL,\n });\n }\n\n container.appendChild(canvas);\n container.appendChild(textLayer);\n this.container.appendChild(container);\n\n // Page label\n const label = document.createElement('div');\n label.className = this.cls.pageLabel;\n label.textContent = `Page ${pageNum} / ${totalPages}`;\n this.container.appendChild(label);\n\n return { container, spans };\n }\n\n private calculateScale(page: PDFPageProxy): number {\n if (this.scale !== 'auto' && typeof this.scale === 'number') {\n return this.scale;\n }\n const defaultVp = page.getViewport({ scale: 1 });\n const containerWidth = this.container.clientWidth || 800;\n return Math.min(containerWidth / defaultVp.width, 2);\n }\n\n /** Set the scale for subsequent renders. */\n setScale(scale: number | 'auto'): void {\n this.scale = scale;\n }\n\n /** Get the configured scale setting. */\n getScale(): number | 'auto' {\n return this.scale;\n }\n\n /** Get the actual numeric scale used in the last render. */\n getEffectiveScale(): number {\n return this.effectiveScale;\n }\n\n /**\n * Download the currently loaded PDF.\n */\n async download(filename: string = 'document.pdf'): Promise<void> {\n if (!this.pdfDoc) throw new Error('No PDF document loaded');\n\n const data = await this.pdfDoc.getData();\n const blob = new Blob([data as BlobPart], { type: 'application/pdf' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n }\n\n getClassNames(): Required<ClassNames> {\n return this.cls;\n }\n\n getPageData(): PageData[] {\n return this.pageData;\n }\n\n getPageCount(): number {\n return this.pdfDoc?.numPages ?? 0;\n }\n\n cleanup(): void {\n this.pdfDoc?.destroy();\n this.pdfDoc = null;\n this.pageData = [];\n this.container.innerHTML = '';\n }\n}\n","import type { SearchOptions, SpanData } from '../types';\n\nexport interface CharMapEntry {\n spanIdx: number;\n charIdx: number;\n}\n\nexport interface MatchRange {\n spanIdx: number;\n start: number;\n end: number;\n}\n\nexport interface SearchResult {\n /** Array of span ranges for each match */\n matchRanges: MatchRange[][];\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Build a flexible regex from query.\n *\n * For queries < 200 chars (after removing whitespace):\n * - Strip all whitespace from query\n * - Insert \\s* between every character\n * → \"and expensive\" becomes a\\s*n\\s*d\\s*e\\s*x\\s*p\\s*e\\s*n\\s*s\\s*i\\s*v\\s*e\n * → Matches regardless of whitespace differences in PDF text\n *\n * For queries >= 200 chars:\n * - Split by whitespace, join with \\s+\n */\nfunction buildFlexibleRegex(\n query: string,\n options: SearchOptions\n): RegExp | null {\n const trimmed = query.trim();\n if (!trimmed) return null;\n\n const isCaseSensitive = options.caseSensitive ?? false;\n const flexibleWhitespace = options.flexibleWhitespace ?? true;\n\n if (!flexibleWhitespace) {\n // Simple literal search\n const pattern = escapeRegex(trimmed);\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Remove all whitespace chars from query\n const chars = [...trimmed].filter((c) => !/\\s/.test(c));\n if (chars.length === 0) return null;\n\n if (chars.length > 200) {\n // Fallback: flexible between tokens only\n const tokens = trimmed.split(/\\s+/);\n const pattern = tokens.map((t) => escapeRegex(t)).join('\\\\s+');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n }\n\n // Insert \\s* between every character\n const pattern = chars.map((c) => escapeRegex(c)).join('\\\\s*');\n return new RegExp(pattern, isCaseSensitive ? 'g' : 'gi');\n}\n\n/**\n * Search for text across page spans using charMap-based matching.\n *\n * Algorithm:\n * 1. Concatenate all span texts into one string (fullText)\n * 2. Build charMap: charMap[i] = { spanIdx, charIdx } for each char in fullText\n * 3. Run regex on fullText\n * 4. Map each match back to span ranges via charMap\n */\nexport function searchPage(\n spans: SpanData[],\n query: string,\n options: SearchOptions = {}\n): MatchRange[][] {\n const regex = buildFlexibleRegex(query, options);\n if (!regex) return [];\n\n // Build fullText and charMap\n let fullText = '';\n const charMap: CharMapEntry[] = [];\n\n spans.forEach((s, si) => {\n for (let ci = 0; ci < s.text.length; ci++) {\n charMap.push({ spanIdx: si, charIdx: ci });\n fullText += s.text[ci];\n }\n });\n\n // Find all regex matches\n const allMatchRanges: MatchRange[][] = [];\n let m: RegExpExecArray | null;\n regex.lastIndex = 0;\n\n while ((m = regex.exec(fullText)) !== null) {\n const start = m.index;\n const end = start + m[0].length;\n\n // Map to span ranges\n const range: MatchRange[] = [];\n for (let k = start; k < end; k++) {\n const cm = charMap[k];\n const last = range[range.length - 1];\n if (last && last.spanIdx === cm.spanIdx && last.end === cm.charIdx) {\n last.end = cm.charIdx + 1;\n } else {\n range.push({ spanIdx: cm.spanIdx, start: cm.charIdx, end: cm.charIdx + 1 });\n }\n }\n allMatchRanges.push(range);\n\n // Prevent infinite loop for zero-length matches\n if (m[0].length === 0) regex.lastIndex++;\n }\n\n return allMatchRanges;\n}\n","import type { SearchMatch, SpanData, PageData } from '../types';\nimport type { MatchRange } from './SearchEngine';\n\n/**\n * Manages cross-span highlighting using the charMap approach.\n *\n * Algorithm (from demo):\n * 1. Group match ranges by spanIdx\n * 2. For each affected span, replace textContent with a DocumentFragment:\n * - Plain text nodes for non-matching parts\n * - <mark> elements for matching parts\n * 3. Collect marks per match for navigation\n */\nexport class HighlightManager {\n private matches: SearchMatch[] = [];\n private currentMatch = -1;\n private highlightClass: string;\n private activeHighlightClass: string;\n\n constructor(highlightClass: string, activeHighlightClass: string) {\n this.highlightClass = highlightClass;\n this.activeHighlightClass = activeHighlightClass;\n }\n\n /**\n * Apply highlights for all matches on a page.\n * Returns the SearchMatch[] (array of mark groups).\n */\n applyHighlights(\n pageSpans: SpanData[],\n matchRanges: MatchRange[][]\n ): SearchMatch[] {\n if (!matchRanges.length) return [];\n\n // Group ranges by spanIdx, keeping track of which match they belong to\n const spanRanges: Record<\n number,\n { start: number; end: number; matchIdx: number }[]\n > = {};\n\n matchRanges.forEach((range, mi) => {\n range.forEach((r) => {\n if (!spanRanges[r.spanIdx]) spanRanges[r.spanIdx] = [];\n spanRanges[r.spanIdx].push({ start: r.start, end: r.end, matchIdx: mi });\n });\n });\n\n // Collect marks per match\n const matchMarks: HTMLElement[][] = matchRanges.map(() => []);\n\n // For each affected span, rebuild DOM with highlights\n for (const siStr of Object.keys(spanRanges)) {\n const si = parseInt(siStr, 10);\n const s = pageSpans[si];\n const ranges = spanRanges[si].sort((a, b) => a.start - b.start);\n\n const frag = document.createDocumentFragment();\n let last = 0;\n\n for (const r of ranges) {\n const actualStart = Math.max(r.start, last);\n\n // Add plain text before highlight\n if (actualStart > last) {\n frag.appendChild(document.createTextNode(s.text.slice(last, actualStart)));\n }\n\n // Add highlight mark\n if (actualStart < r.end) {\n const mark = document.createElement('mark');\n mark.className = this.highlightClass;\n mark.textContent = s.text.slice(actualStart, r.end);\n frag.appendChild(mark);\n matchMarks[r.matchIdx].push(mark);\n }\n\n last = Math.max(last, r.end);\n }\n\n // Add remaining plain text\n if (last < s.text.length) {\n frag.appendChild(document.createTextNode(s.text.slice(last)));\n }\n\n // Replace span content\n s.el.textContent = '';\n s.el.appendChild(frag);\n }\n\n return matchMarks\n .filter((marks) => marks.length > 0)\n .map((marks) => ({ marks }));\n }\n\n /**\n * Add matches to the global list.\n */\n addMatches(newMatches: SearchMatch[]): void {\n this.matches.push(...newMatches);\n }\n\n /**\n * Clear all highlights and restore original span text.\n */\n clearHighlights(allPageData: PageData[]): void {\n allPageData.forEach((pd) => {\n pd.spans.forEach((s) => {\n s.el.textContent = s.text;\n });\n });\n this.matches = [];\n this.currentMatch = -1;\n }\n\n /**\n * Set active match by index. Applies active CSS class and scrolls into view.\n */\n setActiveMatch(index: number): void {\n // Remove active class from previous\n if (this.currentMatch >= 0 && this.currentMatch < this.matches.length) {\n this.matches[this.currentMatch].marks.forEach((m) =>\n m.classList.remove(this.activeHighlightClass)\n );\n }\n\n this.currentMatch = index;\n\n if (index >= 0 && index < this.matches.length) {\n this.matches[index].marks.forEach((m) =>\n m.classList.add(this.activeHighlightClass)\n );\n // Scroll first mark into view\n this.matches[index].marks[0]?.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n });\n }\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n next(): number {\n if (this.matches.length === 0) return -1;\n const newIdx = (this.currentMatch + 1) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prev(): number {\n if (this.matches.length === 0) return -1;\n const newIdx =\n (this.currentMatch - 1 + this.matches.length) % this.matches.length;\n this.setActiveMatch(newIdx);\n return newIdx;\n }\n\n getCurrentIndex(): number {\n return this.currentMatch;\n }\n\n getTotal(): number {\n return this.matches.length;\n }\n\n getMatches(): SearchMatch[] {\n return this.matches;\n }\n}\n","import { EventEmitter } from './EventEmitter';\nimport { PDFRenderer } from './PDFRenderer';\nimport { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES, ZOOM_STEP, MIN_SCALE, MAX_SCALE } from './constants';\nimport type {\n PDFSearchViewerOptions,\n SearchOptions,\n PDFSearchViewerEventMap,\n PageData,\n} from '../types';\n\nexport type PDFSource = File | ArrayBuffer | Uint8Array | string;\n\n/**\n * Main PDF viewer with search and highlight functionality.\n *\n * Usage:\n * ```js\n * import * as pdfjsLib from 'pdfjs-dist';\n * import { PDFSearchViewer } from 'pdf-search-highlight';\n *\n * const viewer = new PDFSearchViewer(container, pdfjsLib, {\n * classNames: {\n * page: 'my-page',\n * highlight: 'my-highlight',\n * activeHighlight: 'my-active',\n * }\n * });\n * await viewer.loadPDF(file);\n * viewer.search('hello');\n * viewer.nextMatch();\n * ```\n */\nexport class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {\n private renderer: PDFRenderer;\n private highlightManager: HighlightManager;\n private pageData: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n private destroyed = false;\n\n constructor(\n container: HTMLElement,\n pdfjsLib: any,\n options: PDFSearchViewerOptions = {}\n ) {\n super();\n\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n\n this.renderer = new PDFRenderer(container, options);\n this.renderer.setPdfjsLib(pdfjsLib);\n this.highlightManager = new HighlightManager(\n cls.highlight,\n cls.activeHighlight\n );\n }\n\n /**\n * Load and render a PDF document.\n */\n async loadPDF(source: PDFSource): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n try {\n await this.renderer.loadDocument(source);\n this.pageData = await this.renderer.renderAllPages();\n const pageCount = this.renderer.getPageCount();\n this.emit('load', { pageCount });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', { error, context: 'loadPDF' });\n throw error;\n }\n }\n\n /**\n * Search for text across all pages.\n * Clears previous highlights and creates new ones.\n */\n search(query: string, options: SearchOptions = {}): number {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n\n // Clear previous highlights\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.emit('search', { query, total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n return 0;\n }\n\n // Search each page and apply highlights\n for (const pd of this.pageData) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n\n // Auto-activate first match\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.emit('search', { query, total });\n this.emit('matchchange', {\n current: total > 0 ? 0 : -1,\n total,\n });\n\n return total;\n }\n\n /**\n * Navigate to next match (wraps around).\n */\n nextMatch(): number {\n const idx = this.highlightManager.next();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Navigate to previous match (wraps around).\n */\n prevMatch(): number {\n const idx = this.highlightManager.prev();\n this.emit('matchchange', {\n current: idx,\n total: this.highlightManager.getTotal(),\n });\n return idx;\n }\n\n /**\n * Clear all search highlights.\n */\n clearSearch(): void {\n this.highlightManager.clearHighlights(this.pageData);\n this.lastQuery = '';\n this.emit('search', { query: '', total: 0 });\n this.emit('matchchange', { current: -1, total: 0 });\n }\n\n /** Get the current scale setting. */\n getScale(): number | 'auto' {\n return this.renderer.getScale();\n }\n\n /** Set scale and re-render. Preserves current search state. */\n async setScale(scale: number | 'auto'): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n this.renderer.setScale(scale);\n await this.rerender();\n this.emit('zoom', { scale: this.renderer.getEffectiveScale() });\n }\n\n /** Zoom in by one step. */\n async zoomIn(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.min(current + ZOOM_STEP, MAX_SCALE);\n await this.setScale(newScale);\n }\n\n /** Zoom out by one step. */\n async zoomOut(): Promise<void> {\n const current = this.resolveCurrentScale();\n const newScale = Math.max(current - ZOOM_STEP, MIN_SCALE);\n await this.setScale(newScale);\n }\n\n /** Download the currently loaded PDF. */\n async download(filename?: string): Promise<void> {\n if (this.destroyed) throw new Error('PDFSearchViewer has been destroyed');\n await this.renderer.download(filename);\n }\n\n private resolveCurrentScale(): number {\n const s = this.renderer.getScale();\n return s === 'auto' ? this.renderer.getEffectiveScale() : s;\n }\n\n private async rerender(): Promise<void> {\n this.highlightManager.clearHighlights(this.pageData);\n this.pageData = await this.renderer.renderAllPages();\n if (this.lastQuery.trim()) {\n this.search(this.lastQuery, this.lastSearchOptions);\n }\n }\n\n /**\n * Get total number of pages.\n */\n getPageCount(): number {\n return this.renderer.getPageCount();\n }\n\n /**\n * Get current active match index (0-based). -1 if none.\n */\n getCurrentMatchIndex(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /**\n * Get total number of matches.\n */\n getMatchCount(): number {\n return this.highlightManager.getTotal();\n }\n\n /**\n * Destroy the viewer, release all resources.\n */\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.highlightManager.clearHighlights(this.pageData);\n this.renderer.cleanup();\n this.removeAllListeners();\n this.pageData = [];\n }\n}\n","import { searchPage } from './SearchEngine';\nimport { HighlightManager } from './HighlightManager';\nimport { DEFAULT_CLASS_NAMES } from './constants';\nimport type { SearchOptions, ClassNames, PageData } from '../types';\n\nexport interface SearchControllerOptions {\n classNames?: Pick<ClassNames, 'highlight' | 'activeHighlight'>;\n}\n\n/**\n * Headless search + highlight controller.\n * Does NOT render PDF — works with any PageData[] you provide.\n *\n * Use this when you want full control over:\n * - Where the PDF is rendered\n * - Where the search UI lives\n * - How search results are displayed\n *\n * Usage:\n * ```js\n * import { PDFRenderer, SearchController } from 'pdf-search-highlight';\n *\n * // Render PDF wherever you want\n * const renderer = new PDFRenderer(pdfContainer, pdfjsLib, {});\n * const pages = await renderer.renderAllPages();\n *\n * // Search controller — no UI, just logic\n * const search = new SearchController();\n * search.setPages(pages);\n *\n * // Wire up your own UI\n * input.oninput = () => search.search(input.value);\n * nextBtn.onclick = () => search.next();\n * prevBtn.onclick = () => search.prev();\n *\n * // React to changes\n * search.onChange = ({ current, total }) => {\n * label.textContent = total > 0 ? `${current + 1}/${total}` : '';\n * };\n * ```\n */\nexport class SearchController {\n private highlightManager: HighlightManager;\n private pages: PageData[] = [];\n private lastQuery = '';\n private lastSearchOptions: SearchOptions = {};\n\n /** Callback fired when match state changes (search, next, prev, clear). */\n onChange: ((state: { current: number; total: number; query: string }) => void) | null = null;\n\n constructor(options: SearchControllerOptions = {}) {\n const cls = { ...DEFAULT_CLASS_NAMES, ...options.classNames };\n this.highlightManager = new HighlightManager(cls.highlight, cls.activeHighlight);\n }\n\n /**\n * Set the pages to search on.\n * Call this after rendering PDF pages.\n */\n setPages(pages: PageData[]): void {\n const savedQuery = this.lastQuery;\n const savedOptions = this.lastSearchOptions;\n this.clear();\n this.pages = pages;\n // Re-apply search if there was an active query (e.g. after zoom)\n if (savedQuery.trim()) {\n this.search(savedQuery, savedOptions);\n }\n }\n\n /**\n * Search for text across all pages.\n * Returns total number of matches.\n */\n search(query: string, options: SearchOptions = {}): number {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = query;\n this.lastSearchOptions = options;\n\n const trimmed = query.trim();\n if (!trimmed) {\n this.notify();\n return 0;\n }\n\n for (const pd of this.pages) {\n const matchRanges = searchPage(pd.spans, trimmed, options);\n const matches = this.highlightManager.applyHighlights(pd.spans, matchRanges);\n this.highlightManager.addMatches(matches);\n }\n\n const total = this.highlightManager.getTotal();\n if (total > 0) {\n this.highlightManager.setActiveMatch(0);\n }\n\n this.notify();\n return total;\n }\n\n /** Navigate to next match. Returns new index. */\n next(): number {\n const idx = this.highlightManager.next();\n this.notify();\n return idx;\n }\n\n /** Navigate to previous match. Returns new index. */\n prev(): number {\n const idx = this.highlightManager.prev();\n this.notify();\n return idx;\n }\n\n /** Go to a specific match by index. */\n goTo(index: number): void {\n this.highlightManager.setActiveMatch(index);\n this.notify();\n }\n\n /** Clear all highlights. */\n clear(): void {\n this.highlightManager.clearHighlights(this.pages);\n this.lastQuery = '';\n this.notify();\n }\n\n /** Current match index (0-based). -1 if none. */\n get current(): number {\n return this.highlightManager.getCurrentIndex();\n }\n\n /** Total number of matches. */\n get total(): number {\n return this.highlightManager.getTotal();\n }\n\n /** Last searched query. */\n get query(): string {\n return this.lastQuery;\n }\n\n private notify(): void {\n this.onChange?.({\n current: this.highlightManager.getCurrentIndex(),\n total: this.highlightManager.getTotal(),\n query: this.lastQuery,\n });\n }\n}\n"]}