pdf-search-highlight 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -171,7 +209,7 @@ function App() {
171
209
  | Export | Description |
172
210
  |---|---|
173
211
  | `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 }` |
212
+ | `useSearchController(pages, options?)` | Hook: search + highlight, returns `{ search, searchMultiple, next, prev, goTo, clear, current, total }` |
175
213
  | `PDFSearchViewer` | All-in-one component with ref handle for imperative control |
176
214
 
177
215
  ### PDFRenderer
@@ -200,17 +238,28 @@ const search = new SearchController({
200
238
  });
201
239
 
202
240
  search.setPages(pages);
241
+
242
+ // Single search
203
243
  search.search('query', { caseSensitive: false, flexibleWhitespace: true });
204
244
  search.search('query', { fuzzy: true, fuzzyThreshold: 0.6 });
245
+
246
+ // Multi-context search
247
+ search.searchMultiple([
248
+ { query: 'contract' },
249
+ { query: 'payment' },
250
+ { query: 'deadline', options: { fuzzy: true } },
251
+ ]);
252
+
205
253
  search.next();
206
254
  search.prev();
207
255
  search.goTo(5);
208
256
  search.clear();
209
257
  search.onChange = ({ current, total, query }) => {};
210
258
 
211
- search.current // current match index
212
- search.total // total matches
213
- search.query // last query
259
+ search.current // current match index
260
+ search.total // total matches
261
+ search.query // last single query
262
+ search.contexts // last multi-context queries
214
263
  ```
215
264
 
216
265
  ### PDFSearchViewer (Core)
@@ -220,6 +269,13 @@ const viewer = new PDFSearchViewer(container, pdfjsLib, options);
220
269
 
221
270
  await viewer.loadPDF(source);
222
271
  viewer.search('query', { caseSensitive: true });
272
+
273
+ // Multi-context search
274
+ viewer.searchMultiple([
275
+ { query: 'contract' },
276
+ { query: 'payment' },
277
+ ]);
278
+
223
279
  viewer.nextMatch();
224
280
  viewer.prevMatch();
225
281
  viewer.clearSearch();
@@ -233,6 +289,7 @@ await viewer.download('file.pdf'); // Download PDF
233
289
 
234
290
  viewer.on('load', (data) => {}); // { pageCount }
235
291
  viewer.on('search', (data) => {}); // { query, total }
292
+ viewer.on('searchmultiple', (data) => {}); // { contexts, total, totalsPerContext }
236
293
  viewer.on('matchchange', (data) => {}); // { current, total }
237
294
  viewer.on('zoom', (data) => {}); // { scale }
238
295
  viewer.on('error', (data) => {}); // { error, context }
@@ -256,8 +313,42 @@ interface SearchOptions {
256
313
  fuzzy?: boolean; // Default: false — enable approximate matching
257
314
  fuzzyThreshold?: number; // Default: 0.6 — similarity 0.0–1.0
258
315
  }
316
+
317
+ interface SearchContext {
318
+ query: string; // The search query
319
+ options?: SearchOptions; // Optional per-context overrides
320
+ }
259
321
  ```
260
322
 
323
+ ### Multi-Context Search
324
+
325
+ Search for multiple terms simultaneously, each highlighted with a different color:
326
+
327
+ ```js
328
+ // Each context gets an auto-assigned color (highlight-0 through highlight-7, cycles)
329
+ search.searchMultiple([
330
+ { query: 'contract' }, // Yellow
331
+ { query: 'payment' }, // Cyan
332
+ { query: 'deadline' }, // Green
333
+ { query: 'penalty' }, // Orange
334
+ ]);
335
+
336
+ // Per-context options override shared options
337
+ search.searchMultiple(
338
+ [
339
+ { query: 'contract' },
340
+ { query: 'payement', options: { fuzzy: true, fuzzyThreshold: 0.7 } },
341
+ ],
342
+ { caseSensitive: false } // shared options
343
+ );
344
+
345
+ // Navigate through ALL matches in document order
346
+ search.next(); // goes to next match regardless of which context
347
+ search.prev(); // goes to previous match
348
+ ```
349
+
350
+ 8 colors are provided by default (CSS classes `highlight-0` through `highlight-7`). Colors cycle for more than 8 contexts.
351
+
261
352
  ### Custom CSS
262
353
 
263
354
  Override any class name:
@@ -279,12 +370,23 @@ const renderer = new PDFRenderer(container, {
279
370
  Default styles:
280
371
 
281
372
  ```css
373
+ /* Single search */
282
374
  .highlight {
283
375
  background: rgba(255, 230, 0, 0.45) !important;
284
376
  }
285
377
  .highlight.active {
286
378
  background: rgba(233, 69, 96, 0.55) !important;
287
379
  }
380
+
381
+ /* Multi-context search (8 colors) */
382
+ .highlight-0 { /* Yellow */ }
383
+ .highlight-1 { /* Cyan */ }
384
+ .highlight-2 { /* Green */ }
385
+ .highlight-3 { /* Orange */ }
386
+ .highlight-4 { /* Purple */ }
387
+ .highlight-5 { /* Pink */ }
388
+ .highlight-6 { /* Blue */ }
389
+ .highlight-7 { /* Lime */ }
288
390
  ```
289
391
 
290
392
  ## How it works
@@ -294,8 +396,9 @@ Default styles:
294
396
  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
397
  4. **Fuzzy search**: Semi-global Levenshtein alignment finds substrings within edit distance ≤ `queryLength × (1 - threshold)` — handles typos, OCR errors, and garbled text extraction
296
398
  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
399
+ 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`, ...)
400
+ 7. **Navigate**: Prev/next with wrap-around, auto-scroll to active match — in multi-context mode, navigation cycles through all matches across all contexts
401
+ 8. **Zoom**: Re-renders all pages at new scale, search highlights are automatically re-applied
299
402
 
300
403
  ## License
301
404
 
@@ -70,32 +70,6 @@ interface SearchOptions {
70
70
  fuzzyThreshold?: number;
71
71
  }
72
72
 
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
73
  /**
100
74
  * A single search match, potentially spanning multiple spans.
101
75
  * Each match contains an array of <mark> elements that highlight
@@ -125,6 +99,48 @@ interface PageData {
125
99
  /** All text spans on this page. */
126
100
  spans: SpanData[];
127
101
  }
102
+ /**
103
+ * A single search context for multi-context search.
104
+ * Each context represents a separate query highlighted with a distinct color.
105
+ */
106
+ interface SearchContext {
107
+ /** The query string to search for. */
108
+ query: string;
109
+ /** Optional per-context search options (overrides shared options). */
110
+ options?: SearchOptions;
111
+ }
112
+
113
+ type PDFSearchViewerEventMap = {
114
+ /** Fired when PDF finishes loading. */
115
+ load: {
116
+ pageCount: number;
117
+ };
118
+ /** Fired when a search completes. */
119
+ search: {
120
+ query: string;
121
+ total: number;
122
+ };
123
+ /** Fired when a multi-context search completes. */
124
+ searchmultiple: {
125
+ contexts: SearchContext[];
126
+ total: number;
127
+ totalsPerContext: number[];
128
+ };
129
+ /** Fired when active match changes (via next/prev). */
130
+ matchchange: {
131
+ current: number;
132
+ total: number;
133
+ };
134
+ /** Fired when zoom/scale changes. */
135
+ zoom: {
136
+ scale: number;
137
+ };
138
+ /** Fired on error. */
139
+ error: {
140
+ error: Error;
141
+ context: string;
142
+ };
143
+ };
128
144
 
129
145
  type PDFSource = File | ArrayBuffer | Uint8Array | string;
130
146
  /**
@@ -153,6 +169,8 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
153
169
  private pageData;
154
170
  private lastQuery;
155
171
  private lastSearchOptions;
172
+ private lastContexts;
173
+ private lastIsMultiContext;
156
174
  private destroyed;
157
175
  constructor(container: HTMLElement, pdfjsLib: any, options?: PDFSearchViewerOptions);
158
176
  /**
@@ -164,6 +182,13 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
164
182
  * Clears previous highlights and creates new ones.
165
183
  */
166
184
  search(query: string, options?: SearchOptions): number;
185
+ /**
186
+ * Search for multiple query contexts across all pages.
187
+ * Each context is highlighted with a different color (highlight-0, highlight-1, ...).
188
+ * Navigation (nextMatch/prevMatch) cycles through ALL matches in document order.
189
+ * Returns total number of matches across all contexts.
190
+ */
191
+ searchMultiple(contexts: SearchContext[], sharedOptions?: SearchOptions): number;
167
192
  /**
168
193
  * Navigate to next match (wraps around).
169
194
  */
@@ -206,4 +231,4 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
206
231
  destroy(): void;
207
232
  }
208
233
 
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 };
234
+ 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 };
@@ -70,32 +70,6 @@ interface SearchOptions {
70
70
  fuzzyThreshold?: number;
71
71
  }
72
72
 
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
73
  /**
100
74
  * A single search match, potentially spanning multiple spans.
101
75
  * Each match contains an array of <mark> elements that highlight
@@ -125,6 +99,48 @@ interface PageData {
125
99
  /** All text spans on this page. */
126
100
  spans: SpanData[];
127
101
  }
102
+ /**
103
+ * A single search context for multi-context search.
104
+ * Each context represents a separate query highlighted with a distinct color.
105
+ */
106
+ interface SearchContext {
107
+ /** The query string to search for. */
108
+ query: string;
109
+ /** Optional per-context search options (overrides shared options). */
110
+ options?: SearchOptions;
111
+ }
112
+
113
+ type PDFSearchViewerEventMap = {
114
+ /** Fired when PDF finishes loading. */
115
+ load: {
116
+ pageCount: number;
117
+ };
118
+ /** Fired when a search completes. */
119
+ search: {
120
+ query: string;
121
+ total: number;
122
+ };
123
+ /** Fired when a multi-context search completes. */
124
+ searchmultiple: {
125
+ contexts: SearchContext[];
126
+ total: number;
127
+ totalsPerContext: number[];
128
+ };
129
+ /** Fired when active match changes (via next/prev). */
130
+ matchchange: {
131
+ current: number;
132
+ total: number;
133
+ };
134
+ /** Fired when zoom/scale changes. */
135
+ zoom: {
136
+ scale: number;
137
+ };
138
+ /** Fired on error. */
139
+ error: {
140
+ error: Error;
141
+ context: string;
142
+ };
143
+ };
128
144
 
129
145
  type PDFSource = File | ArrayBuffer | Uint8Array | string;
130
146
  /**
@@ -153,6 +169,8 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
153
169
  private pageData;
154
170
  private lastQuery;
155
171
  private lastSearchOptions;
172
+ private lastContexts;
173
+ private lastIsMultiContext;
156
174
  private destroyed;
157
175
  constructor(container: HTMLElement, pdfjsLib: any, options?: PDFSearchViewerOptions);
158
176
  /**
@@ -164,6 +182,13 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
164
182
  * Clears previous highlights and creates new ones.
165
183
  */
166
184
  search(query: string, options?: SearchOptions): number;
185
+ /**
186
+ * Search for multiple query contexts across all pages.
187
+ * Each context is highlighted with a different color (highlight-0, highlight-1, ...).
188
+ * Navigation (nextMatch/prevMatch) cycles through ALL matches in document order.
189
+ * Returns total number of matches across all contexts.
190
+ */
191
+ searchMultiple(contexts: SearchContext[], sharedOptions?: SearchOptions): number;
167
192
  /**
168
193
  * Navigate to next match (wraps around).
169
194
  */
@@ -206,4 +231,4 @@ declare class PDFSearchViewer extends EventEmitter<PDFSearchViewerEventMap> {
206
231
  destroy(): void;
207
232
  }
208
233
 
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 };
234
+ 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 };