pdf-search-highlight 0.2.2 → 0.3.1

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 or fuzzy (approximate) matching, and navigate between highlighted results. Zoom in/out and download PDF files.
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. Supports multi-context search with different highlight colors. 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
 
@@ -15,6 +15,7 @@ npm install pdf-search-highlight pdfjs-dist
15
15
  - Render PDF pages (canvas + text layer)
16
16
  - Search with flexible whitespace matching — handles inconsistent PDF text splitting
17
17
  - Fuzzy (approximate) search — find text even with typos or OCR errors
18
+ - **Multi-context search** — search multiple queries simultaneously, each highlighted with a different color
18
19
  - Cross-span highlight using `<mark>` elements
19
20
  - Navigate between matches (next/prev, auto-scroll)
20
21
  - Zoom in/out with configurable scale
@@ -52,6 +53,19 @@ search.next();
52
53
  search.prev();
53
54
  search.clear();
54
55
 
56
+ // Multi-context search — each query gets a different highlight color
57
+ search.searchMultiple([
58
+ { query: 'contract' },
59
+ { query: 'payment' },
60
+ { query: 'deadline' },
61
+ ]);
62
+
63
+ // With per-context options
64
+ search.searchMultiple([
65
+ { query: 'contract' },
66
+ { query: 'payement', options: { fuzzy: true, fuzzyThreshold: 0.7 } },
67
+ ]);
68
+
55
69
  // Zoom
56
70
  renderer.setScale(1.5);
57
71
  const newPages = await renderer.renderAllPages();
@@ -76,6 +90,14 @@ await viewer.loadPDF(file);
76
90
  viewer.search('query');
77
91
  viewer.nextMatch();
78
92
 
93
+ // Multi-context search
94
+ viewer.searchMultiple([
95
+ { query: 'contract' },
96
+ { query: 'payment' },
97
+ { query: 'deadline' },
98
+ ]);
99
+ viewer.nextMatch(); // navigates through ALL matches in document order
100
+
79
101
  // Zoom
80
102
  await viewer.zoomIn();
81
103
  await viewer.zoomOut();
@@ -87,6 +109,9 @@ await viewer.download('document.pdf');
87
109
  // Events
88
110
  viewer.on('load', ({ pageCount }) => console.log('Pages:', pageCount));
89
111
  viewer.on('search', ({ query, total }) => console.log('Found:', total));
112
+ viewer.on('searchmultiple', ({ contexts, total, totalsPerContext }) => {
113
+ console.log('Multi-search:', total, 'total matches');
114
+ });
90
115
  viewer.on('matchchange', ({ current, total }) => console.log(`${current + 1}/${total}`));
91
116
  viewer.on('zoom', ({ scale }) => console.log('Scale:', scale));
92
117
  viewer.on('error', ({ error, context }) => console.error(context, error));
@@ -101,16 +126,25 @@ import 'pdf-search-highlight/styles.css';
101
126
  function App() {
102
127
  const { containerRef, pages, loadPDF, zoomIn, zoomOut, download, scale } =
103
128
  usePDFRenderer(pdfjsLib);
104
- const { search, next, prev, current, total } = useSearchController(pages);
129
+ const { search, searchMultiple, next, prev, current, total } =
130
+ useSearchController(pages);
105
131
 
106
132
  return (
107
133
  <>
108
- {/* Search UI — anywhere you want */}
134
+ {/* Single search */}
109
135
  <input onChange={e => search(e.target.value)} />
110
136
  <span>{total > 0 ? `${current + 1}/${total}` : ''}</span>
111
137
  <button onClick={prev}>Prev</button>
112
138
  <button onClick={next}>Next</button>
113
139
 
140
+ {/* Multi-context search */}
141
+ <button onClick={() => searchMultiple([
142
+ { query: 'contract' },
143
+ { query: 'payment' },
144
+ ])}>
145
+ Search Multiple
146
+ </button>
147
+
114
148
  {/* Zoom & download */}
115
149
  <button onClick={zoomOut}>-</button>
116
150
  <button onClick={zoomIn}>+</button>
@@ -139,8 +173,11 @@ function App() {
139
173
  pdfjsLib={pdfjsLib}
140
174
  source={file}
141
175
  searchQuery={query}
176
+ // OR multi-context search:
177
+ // searchContexts={[{ query: 'contract' }, { query: 'payment' }]}
142
178
  onLoad={({ pageCount }) => console.log('Pages:', pageCount)}
143
179
  onSearch={({ query, total }) => console.log('Found:', total)}
180
+ onSearchMultiple={({ contexts, total }) => console.log('Multi:', total)}
144
181
  onMatchChange={({ current, total }) => console.log(`${current + 1}/${total}`)}
145
182
  onZoom={({ scale }) => console.log('Scale:', scale)}
146
183
  style={{ height: '80vh', overflow: 'auto' }}
@@ -149,6 +186,7 @@ function App() {
149
186
 
150
187
  // Imperative access via ref
151
188
  // ref.current.nextMatch()
189
+ // ref.current.searchMultiple([{ query: 'a' }, { query: 'b' }])
152
190
  // ref.current.zoomIn()
153
191
  // ref.current.download('doc.pdf')
154
192
  }
@@ -161,18 +199,20 @@ function App() {
161
199
  | Export | Description |
162
200
  |---|---|
163
201
  | `PDFRenderer` | Renders PDF pages into a container (canvas + text layer) |
164
- | `SearchController` | Headless search + highlight controller |
165
- | `PDFSearchViewer` | All-in-one: render + search + highlight + zoom + download |
202
+ | `SearchController` | Headless search + highlight controller. `search()` for single query, `searchMultiple()` for multi-context |
203
+ | `PDFSearchViewer` | All-in-one: render + search + highlight + zoom + download. `search()` + `searchMultiple()` |
166
204
  | `searchPage` | Low-level: search spans with flexible regex |
167
205
  | `HighlightManager` | Low-level: apply/clear highlights on spans |
206
+ | `SearchContext` | Type: `{ query: string; options?: SearchOptions }` — used with `searchMultiple()` |
168
207
 
169
208
  ### React (`pdf-search-highlight/react`)
170
209
 
171
210
  | Export | Description |
172
211
  |---|---|
173
212
  | `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 |
213
+ | `useSearchController(pages, options?)` | Hook: search + highlight, returns `{ search, searchMultiple, next, prev, goTo, clear, current, total }` |
214
+ | `PDFSearchViewer` | All-in-one component. Props: `searchQuery` (single) or `searchContexts` (multi). Ref handle: `nextMatch`, `prevMatch`, `searchMultiple`, `clearSearch`, ... |
215
+ | `SearchContext` | Type re-exported from core |
176
216
 
177
217
  ### PDFRenderer
178
218
 
@@ -200,17 +240,28 @@ const search = new SearchController({
200
240
  });
201
241
 
202
242
  search.setPages(pages);
243
+
244
+ // Single search
203
245
  search.search('query', { caseSensitive: false, flexibleWhitespace: true });
204
246
  search.search('query', { fuzzy: true, fuzzyThreshold: 0.6 });
247
+
248
+ // Multi-context search
249
+ search.searchMultiple([
250
+ { query: 'contract' },
251
+ { query: 'payment' },
252
+ { query: 'deadline', options: { fuzzy: true } },
253
+ ]);
254
+
205
255
  search.next();
206
256
  search.prev();
207
257
  search.goTo(5);
208
258
  search.clear();
209
259
  search.onChange = ({ current, total, query }) => {};
210
260
 
211
- search.current // current match index
212
- search.total // total matches
213
- search.query // last query
261
+ search.current // current match index
262
+ search.total // total matches
263
+ search.query // last single query
264
+ search.contexts // last multi-context queries
214
265
  ```
215
266
 
216
267
  ### PDFSearchViewer (Core)
@@ -219,10 +270,21 @@ search.query // last query
219
270
  const viewer = new PDFSearchViewer(container, pdfjsLib, options);
220
271
 
221
272
  await viewer.loadPDF(source);
273
+
274
+ // Single search
222
275
  viewer.search('query', { caseSensitive: true });
223
- viewer.nextMatch();
224
- viewer.prevMatch();
225
- viewer.clearSearch();
276
+
277
+ // Multi-context search — each context highlighted with a different color
278
+ viewer.searchMultiple([
279
+ { query: 'contract' },
280
+ { query: 'payment' },
281
+ { query: 'deadline', options: { fuzzy: true } },
282
+ ]);
283
+
284
+ // Navigation — works for both single and multi-context
285
+ viewer.nextMatch(); // Next match (all contexts, document order)
286
+ viewer.prevMatch(); // Previous match
287
+ viewer.clearSearch(); // Clear all highlights
226
288
 
227
289
  await viewer.zoomIn(); // Zoom in by 0.25
228
290
  await viewer.zoomOut(); // Zoom out by 0.25
@@ -233,6 +295,7 @@ await viewer.download('file.pdf'); // Download PDF
233
295
 
234
296
  viewer.on('load', (data) => {}); // { pageCount }
235
297
  viewer.on('search', (data) => {}); // { query, total }
298
+ viewer.on('searchmultiple', (data) => {}); // { contexts, total, totalsPerContext }
236
299
  viewer.on('matchchange', (data) => {}); // { current, total }
237
300
  viewer.on('zoom', (data) => {}); // { scale }
238
301
  viewer.on('error', (data) => {}); // { error, context }
@@ -256,8 +319,42 @@ interface SearchOptions {
256
319
  fuzzy?: boolean; // Default: false — enable approximate matching
257
320
  fuzzyThreshold?: number; // Default: 0.6 — similarity 0.0–1.0
258
321
  }
322
+
323
+ interface SearchContext {
324
+ query: string; // The search query
325
+ options?: SearchOptions; // Optional per-context overrides
326
+ }
327
+ ```
328
+
329
+ ### Multi-Context Search
330
+
331
+ Search for multiple terms simultaneously, each highlighted with a different color:
332
+
333
+ ```js
334
+ // Each context gets an auto-assigned color (highlight-0 through highlight-7, cycles)
335
+ search.searchMultiple([
336
+ { query: 'contract' }, // Yellow
337
+ { query: 'payment' }, // Cyan
338
+ { query: 'deadline' }, // Green
339
+ { query: 'penalty' }, // Orange
340
+ ]);
341
+
342
+ // Per-context options override shared options
343
+ search.searchMultiple(
344
+ [
345
+ { query: 'contract' },
346
+ { query: 'payement', options: { fuzzy: true, fuzzyThreshold: 0.7 } },
347
+ ],
348
+ { caseSensitive: false } // shared options
349
+ );
350
+
351
+ // Navigate through ALL matches in document order
352
+ search.next(); // goes to next match regardless of which context
353
+ search.prev(); // goes to previous match
259
354
  ```
260
355
 
356
+ 8 colors are provided by default (CSS classes `highlight-0` through `highlight-7`). Colors cycle for more than 8 contexts.
357
+
261
358
  ### Custom CSS
262
359
 
263
360
  Override any class name:
@@ -279,12 +376,23 @@ const renderer = new PDFRenderer(container, {
279
376
  Default styles:
280
377
 
281
378
  ```css
379
+ /* Single search */
282
380
  .highlight {
283
381
  background: rgba(255, 230, 0, 0.45) !important;
284
382
  }
285
383
  .highlight.active {
286
384
  background: rgba(233, 69, 96, 0.55) !important;
287
385
  }
386
+
387
+ /* Multi-context search (8 colors) */
388
+ .highlight-0 { /* Yellow */ }
389
+ .highlight-1 { /* Cyan */ }
390
+ .highlight-2 { /* Green */ }
391
+ .highlight-3 { /* Orange */ }
392
+ .highlight-4 { /* Purple */ }
393
+ .highlight-5 { /* Pink */ }
394
+ .highlight-6 { /* Blue */ }
395
+ .highlight-7 { /* Lime */ }
288
396
  ```
289
397
 
290
398
  ## How it works
@@ -294,8 +402,9 @@ Default styles:
294
402
  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
295
403
  4. **Fuzzy search**: Semi-global Levenshtein alignment finds substrings within edit distance ≤ `queryLength × (1 - threshold)` — handles typos, OCR errors, and garbled text extraction
296
404
  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
405
+ 6. **Multi-context**: Each context runs independently, matches are sorted by document position, and each context's `<mark>` elements receive a distinct CSS class (`highlight-0`, `highlight-1`, ...)
406
+ 7. **Navigate**: Prev/next with wrap-around, auto-scroll to active match — in multi-context mode, navigation cycles through all matches across all contexts
407
+ 8. **Zoom**: Re-renders all pages at new scale, search highlights are automatically re-applied
299
408
 
300
409
  ## License
301
410
 
@@ -36,6 +36,8 @@ interface PDFSearchViewerOptions {
36
36
  workerSrc?: string;
37
37
  /** Gap in pixels between rendered pages. Defaults to 20. */
38
38
  pageGap?: number;
39
+ /** Auto-scroll to active match on search/next/prev. Defaults to true. */
40
+ autoScroll?: boolean;
39
41
  /**
40
42
  * Custom CSS class names for viewer elements.
41
43
  * Override any or all to apply your own styles.
@@ -70,32 +72,6 @@ interface SearchOptions {
70
72
  fuzzyThreshold?: number;
71
73
  }
72
74
 
73
- type PDFSearchViewerEventMap = {
74
- /** Fired when PDF finishes loading. */
75
- load: {
76
- pageCount: number;
77
- };
78
- /** Fired when a search completes. */
79
- search: {
80
- query: string;
81
- total: number;
82
- };
83
- /** Fired when active match changes (via next/prev). */
84
- matchchange: {
85
- current: number;
86
- total: number;
87
- };
88
- /** Fired when zoom/scale changes. */
89
- zoom: {
90
- scale: number;
91
- };
92
- /** Fired on error. */
93
- error: {
94
- error: Error;
95
- context: string;
96
- };
97
- };
98
-
99
75
  /**
100
76
  * A single search match, potentially spanning multiple spans.
101
77
  * Each match contains an array of <mark> elements that highlight
@@ -125,6 +101,48 @@ interface PageData {
125
101
  /** All text spans on this page. */
126
102
  spans: SpanData[];
127
103
  }
104
+ /**
105
+ * A single search context for multi-context search.
106
+ * Each context represents a separate query highlighted with a distinct color.
107
+ */
108
+ interface SearchContext {
109
+ /** The query string to search for. */
110
+ query: string;
111
+ /** Optional per-context search options (overrides shared options). */
112
+ options?: SearchOptions;
113
+ }
114
+
115
+ type PDFSearchViewerEventMap = {
116
+ /** Fired when PDF finishes loading. */
117
+ load: {
118
+ pageCount: number;
119
+ };
120
+ /** Fired when a search completes. */
121
+ search: {
122
+ query: string;
123
+ total: number;
124
+ };
125
+ /** Fired when a multi-context search completes. */
126
+ searchmultiple: {
127
+ contexts: SearchContext[];
128
+ total: number;
129
+ totalsPerContext: number[];
130
+ };
131
+ /** Fired when active match changes (via next/prev). */
132
+ matchchange: {
133
+ current: number;
134
+ total: number;
135
+ };
136
+ /** Fired when zoom/scale changes. */
137
+ zoom: {
138
+ scale: number;
139
+ };
140
+ /** Fired on error. */
141
+ error: {
142
+ error: Error;
143
+ context: string;
144
+ };
145
+ };
128
146
 
129
147
  type PDFSource = File | ArrayBuffer | Uint8Array | string;
130
148
  /**
@@ -153,6 +171,8 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
153
171
  private pageData;
154
172
  private lastQuery;
155
173
  private lastSearchOptions;
174
+ private lastContexts;
175
+ private lastIsMultiContext;
156
176
  private destroyed;
157
177
  constructor(container: HTMLElement, pdfjsLib: any, options?: PDFSearchViewerOptions);
158
178
  /**
@@ -164,6 +184,13 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
164
184
  * Clears previous highlights and creates new ones.
165
185
  */
166
186
  search(query: string, options?: SearchOptions): number;
187
+ /**
188
+ * Search for multiple query contexts across all pages.
189
+ * Each context is highlighted with a different color (highlight-0, highlight-1, ...).
190
+ * Navigation (nextMatch/prevMatch) cycles through ALL matches in document order.
191
+ * Returns total number of matches across all contexts.
192
+ */
193
+ searchMultiple(contexts: SearchContext[], sharedOptions?: SearchOptions): number;
167
194
  /**
168
195
  * Navigate to next match (wraps around).
169
196
  */
@@ -206,4 +233,4 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
206
233
  destroy(): void;
207
234
  }
208
235
 
209
- export { type ClassNames as C, EventEmitter as E, type PageData as P, type SearchOptions as S, type PDFSearchViewerOptions as a, type SpanData as b, type SearchMatch as c, PDFSearchViewer as d, type PDFSearchViewerEventMap as e, type PDFSource as f };
236
+ export { type ClassNames as C, EventEmitter as E, type PageData as P, type SearchOptions as S, type SearchContext as a, type PDFSearchViewerOptions as b, type SpanData as c, type SearchMatch as d, PDFSearchViewer as e, type PDFSearchViewerEventMap as f, type PDFSource as g };
@@ -36,6 +36,8 @@ interface PDFSearchViewerOptions {
36
36
  workerSrc?: string;
37
37
  /** Gap in pixels between rendered pages. Defaults to 20. */
38
38
  pageGap?: number;
39
+ /** Auto-scroll to active match on search/next/prev. Defaults to true. */
40
+ autoScroll?: boolean;
39
41
  /**
40
42
  * Custom CSS class names for viewer elements.
41
43
  * Override any or all to apply your own styles.
@@ -70,32 +72,6 @@ interface SearchOptions {
70
72
  fuzzyThreshold?: number;
71
73
  }
72
74
 
73
- type PDFSearchViewerEventMap = {
74
- /** Fired when PDF finishes loading. */
75
- load: {
76
- pageCount: number;
77
- };
78
- /** Fired when a search completes. */
79
- search: {
80
- query: string;
81
- total: number;
82
- };
83
- /** Fired when active match changes (via next/prev). */
84
- matchchange: {
85
- current: number;
86
- total: number;
87
- };
88
- /** Fired when zoom/scale changes. */
89
- zoom: {
90
- scale: number;
91
- };
92
- /** Fired on error. */
93
- error: {
94
- error: Error;
95
- context: string;
96
- };
97
- };
98
-
99
75
  /**
100
76
  * A single search match, potentially spanning multiple spans.
101
77
  * Each match contains an array of <mark> elements that highlight
@@ -125,6 +101,48 @@ interface PageData {
125
101
  /** All text spans on this page. */
126
102
  spans: SpanData[];
127
103
  }
104
+ /**
105
+ * A single search context for multi-context search.
106
+ * Each context represents a separate query highlighted with a distinct color.
107
+ */
108
+ interface SearchContext {
109
+ /** The query string to search for. */
110
+ query: string;
111
+ /** Optional per-context search options (overrides shared options). */
112
+ options?: SearchOptions;
113
+ }
114
+
115
+ type PDFSearchViewerEventMap = {
116
+ /** Fired when PDF finishes loading. */
117
+ load: {
118
+ pageCount: number;
119
+ };
120
+ /** Fired when a search completes. */
121
+ search: {
122
+ query: string;
123
+ total: number;
124
+ };
125
+ /** Fired when a multi-context search completes. */
126
+ searchmultiple: {
127
+ contexts: SearchContext[];
128
+ total: number;
129
+ totalsPerContext: number[];
130
+ };
131
+ /** Fired when active match changes (via next/prev). */
132
+ matchchange: {
133
+ current: number;
134
+ total: number;
135
+ };
136
+ /** Fired when zoom/scale changes. */
137
+ zoom: {
138
+ scale: number;
139
+ };
140
+ /** Fired on error. */
141
+ error: {
142
+ error: Error;
143
+ context: string;
144
+ };
145
+ };
128
146
 
129
147
  type PDFSource = File | ArrayBuffer | Uint8Array | string;
130
148
  /**
@@ -153,6 +171,8 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
153
171
  private pageData;
154
172
  private lastQuery;
155
173
  private lastSearchOptions;
174
+ private lastContexts;
175
+ private lastIsMultiContext;
156
176
  private destroyed;
157
177
  constructor(container: HTMLElement, pdfjsLib: any, options?: PDFSearchViewerOptions);
158
178
  /**
@@ -164,6 +184,13 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
164
184
  * Clears previous highlights and creates new ones.
165
185
  */
166
186
  search(query: string, options?: SearchOptions): number;
187
+ /**
188
+ * Search for multiple query contexts across all pages.
189
+ * Each context is highlighted with a different color (highlight-0, highlight-1, ...).
190
+ * Navigation (nextMatch/prevMatch) cycles through ALL matches in document order.
191
+ * Returns total number of matches across all contexts.
192
+ */
193
+ searchMultiple(contexts: SearchContext[], sharedOptions?: SearchOptions): number;
167
194
  /**
168
195
  * Navigate to next match (wraps around).
169
196
  */
@@ -206,4 +233,4 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
206
233
  destroy(): void;
207
234
  }
208
235
 
209
- export { type ClassNames as C, EventEmitter as E, type PageData as P, type SearchOptions as S, type PDFSearchViewerOptions as a, type SpanData as b, type SearchMatch as c, PDFSearchViewer as d, type PDFSearchViewerEventMap as e, type PDFSource as f };
236
+ export { type ClassNames as C, EventEmitter as E, type PageData as P, type SearchOptions as S, type SearchContext as a, type PDFSearchViewerOptions as b, type SpanData as c, type SearchMatch as d, PDFSearchViewer as e, type PDFSearchViewerEventMap as f, type PDFSource as g };