@embedpdf/plugin-search 2.14.4 → 3.0.0-next.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.
Files changed (61) hide show
  1. package/LICENSE +192 -21
  2. package/dist/index.cjs +363 -2
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +156 -0
  5. package/dist/index.d.ts +156 -1
  6. package/dist/index.js +346 -433
  7. package/dist/index.js.map +1 -1
  8. package/package.json +21 -57
  9. package/dist/lib/actions.d.ts +0 -91
  10. package/dist/lib/index.d.ts +0 -10
  11. package/dist/lib/manifest.d.ts +0 -4
  12. package/dist/lib/reducer.d.ts +0 -6
  13. package/dist/lib/search-plugin.d.ts +0 -32
  14. package/dist/lib/types.d.ts +0 -165
  15. package/dist/preact/adapter.d.ts +0 -5
  16. package/dist/preact/core.d.ts +0 -1
  17. package/dist/preact/index.cjs +0 -2
  18. package/dist/preact/index.cjs.map +0 -1
  19. package/dist/preact/index.d.ts +0 -1
  20. package/dist/preact/index.js +0 -107
  21. package/dist/preact/index.js.map +0 -1
  22. package/dist/react/adapter.d.ts +0 -2
  23. package/dist/react/core.d.ts +0 -1
  24. package/dist/react/index.cjs +0 -2
  25. package/dist/react/index.cjs.map +0 -1
  26. package/dist/react/index.d.ts +0 -1
  27. package/dist/react/index.js +0 -106
  28. package/dist/react/index.js.map +0 -1
  29. package/dist/shared/components/index.d.ts +0 -1
  30. package/dist/shared/components/search-layer.d.ts +0 -11
  31. package/dist/shared/hooks/index.d.ts +0 -1
  32. package/dist/shared/hooks/use-search.d.ts +0 -15
  33. package/dist/shared/index.d.ts +0 -3
  34. package/dist/shared-preact/components/index.d.ts +0 -1
  35. package/dist/shared-preact/components/search-layer.d.ts +0 -11
  36. package/dist/shared-preact/hooks/index.d.ts +0 -1
  37. package/dist/shared-preact/hooks/use-search.d.ts +0 -15
  38. package/dist/shared-preact/index.d.ts +0 -3
  39. package/dist/shared-react/components/index.d.ts +0 -1
  40. package/dist/shared-react/components/search-layer.d.ts +0 -11
  41. package/dist/shared-react/hooks/index.d.ts +0 -1
  42. package/dist/shared-react/hooks/use-search.d.ts +0 -15
  43. package/dist/shared-react/index.d.ts +0 -3
  44. package/dist/svelte/components/SearchLayer.svelte.d.ts +0 -11
  45. package/dist/svelte/components/index.d.ts +0 -1
  46. package/dist/svelte/hooks/index.d.ts +0 -1
  47. package/dist/svelte/hooks/use-search.svelte.d.ts +0 -21
  48. package/dist/svelte/index.cjs +0 -2
  49. package/dist/svelte/index.cjs.map +0 -1
  50. package/dist/svelte/index.d.ts +0 -3
  51. package/dist/svelte/index.js +0 -129
  52. package/dist/svelte/index.js.map +0 -1
  53. package/dist/vue/components/index.d.ts +0 -1
  54. package/dist/vue/components/search-layer.vue.d.ts +0 -13
  55. package/dist/vue/hooks/index.d.ts +0 -1
  56. package/dist/vue/hooks/use-search.d.ts +0 -12
  57. package/dist/vue/index.cjs +0 -2
  58. package/dist/vue/index.cjs.map +0 -1
  59. package/dist/vue/index.d.ts +0 -3
  60. package/dist/vue/index.js +0 -131
  61. package/dist/vue/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,436 +1,349 @@
1
- import { BasePlugin, createBehaviorEmitter } from "@embedpdf/core";
2
- import { PdfErrorCode, PdfTaskHelper } from "@embedpdf/models";
3
- const INIT_SEARCH_STATE = "SEARCH/INIT_STATE";
4
- const CLEANUP_SEARCH_STATE = "SEARCH/CLEANUP_STATE";
5
- const START_SEARCH_SESSION = "SEARCH/START_SEARCH_SESSION";
6
- const STOP_SEARCH_SESSION = "SEARCH/STOP_SEARCH_SESSION";
7
- const SET_SEARCH_FLAGS = "SEARCH/SET_SEARCH_FLAGS";
8
- const SET_SHOW_ALL_RESULTS = "SEARCH/SET_SHOW_ALL_RESULTS";
9
- const START_SEARCH = "SEARCH/START_SEARCH";
10
- const SET_SEARCH_RESULTS = "SEARCH/SET_SEARCH_RESULTS";
11
- const APPEND_SEARCH_RESULTS = "SEARCH/APPEND_SEARCH_RESULTS";
12
- const SET_ACTIVE_RESULT_INDEX = "SEARCH/SET_ACTIVE_RESULT_INDEX";
13
- function initSearchState(documentId, state) {
14
- return { type: INIT_SEARCH_STATE, payload: { documentId, state } };
15
- }
16
- function cleanupSearchState(documentId) {
17
- return { type: CLEANUP_SEARCH_STATE, payload: documentId };
18
- }
19
- function startSearchSession(documentId) {
20
- return { type: START_SEARCH_SESSION, payload: { documentId } };
21
- }
22
- function stopSearchSession(documentId) {
23
- return { type: STOP_SEARCH_SESSION, payload: { documentId } };
24
- }
25
- function setSearchFlags(documentId, flags) {
26
- return { type: SET_SEARCH_FLAGS, payload: { documentId, flags } };
27
- }
28
- function setShowAllResults(documentId, showAll) {
29
- return { type: SET_SHOW_ALL_RESULTS, payload: { documentId, showAll } };
30
- }
31
- function startSearch(documentId, query) {
32
- return { type: START_SEARCH, payload: { documentId, query } };
33
- }
34
- function setSearchResults(documentId, results, total, activeResultIndex) {
35
- return { type: SET_SEARCH_RESULTS, payload: { documentId, results, total, activeResultIndex } };
36
- }
37
- function appendSearchResults(documentId, results) {
38
- return { type: APPEND_SEARCH_RESULTS, payload: { documentId, results } };
1
+ import { createCapabilityToken, definePlugin } from "@embedpdf/core";
2
+ import { StageToken } from "@embedpdf/plugin-stage";
3
+ import { applyRect, boundsOfRects, pageGeometry } from "@embedpdf/core-geometry";
4
+ import { EngineError, EngineErrorCode, validateSearchQuery, validateSearchRegex } from "@embedpdf/engine-core/runtime";
5
+ //#region src/capability.ts
6
+ const EMPTY = [];
7
+ /** The browser find-bar feel: hit at the top-middle, no zoom change. */
8
+ const DEFAULT_REVEAL = {
9
+ anchor: { y: .35 },
10
+ behavior: "smooth"
11
+ };
12
+ /** Local engines reject a denied scope before the worker; cloud maps 403. */
13
+ const isPermissionDenied = (err) => err instanceof Error && err.name === "PermissionDenied" || EngineError.is(err, EngineErrorCode.Forbidden);
14
+ /**
15
+ * The search capability: a client-driven cursor loop over the engine's
16
+ * budgeted slices. One generation counter arbitrates: a new `search()` (or
17
+ * `clear()`, or a document mutation via `rerun()`) supersedes the running
18
+ * loop, which aborts its in-flight slice and stops dispatching.
19
+ *
20
+ * Rect conversion happens HERE, once per slice: the engine's PDF-space
21
+ * line rects → content space through the same page geometry the selection
22
+ * plugin uses, so search highlights and selection highlights are pixel-twins.
23
+ *
24
+ * Stage integration is optional and additive: with a Stage present the scan
25
+ * starts at the current page (viewport-first — the hits the user is looking
26
+ * at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the
27
+ * hit's page. Without one, navigation still works; the host scrolls.
28
+ */
29
+ function createSearchCapability(ctx, config = {}) {
30
+ let generation = 0;
31
+ let inflight = null;
32
+ const toContent = (m, b) => applyRect(m, {
33
+ x: b.left,
34
+ y: b.bottom,
35
+ width: b.right - b.left,
36
+ height: b.top - b.bottom
37
+ });
38
+ function hitsFromSlice(slice) {
39
+ const converters = /* @__PURE__ */ new Map();
40
+ const converterFor = (pon) => {
41
+ let conv = converters.get(pon);
42
+ if (conv !== void 0) return conv;
43
+ const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
44
+ if (!layout) conv = null;
45
+ else {
46
+ const { pdfToContent } = pageGeometry({
47
+ crop: layout.boxes.crop,
48
+ rotation: layout.rotation,
49
+ userUnit: layout.userUnit
50
+ }, 1);
51
+ conv = {
52
+ pageIndex: layout.index,
53
+ toRect: (b) => toContent(pdfToContent, b)
54
+ };
55
+ }
56
+ converters.set(pon, conv);
57
+ return conv;
58
+ };
59
+ const hits = [];
60
+ for (const match of slice.matches) {
61
+ const conv = converterFor(match.pageObjectNumber);
62
+ if (!conv) continue;
63
+ hits.push({
64
+ pon: match.pageObjectNumber,
65
+ pageIndex: conv.pageIndex,
66
+ charStart: match.charStart,
67
+ charCount: match.charCount,
68
+ rects: match.rects.map(conv.toRect),
69
+ ...match.snippet ? { snippet: match.snippet } : {}
70
+ });
71
+ }
72
+ return hits;
73
+ }
74
+ /**
75
+ * THE MECHANISM — one cursor loop over the engine's budgeted slices,
76
+ * shared by the session (`search`) and the session-free service
77
+ * (`findAll`): permission fallback ('full' → 'rects' when snippets are
78
+ * denied, unless the caller pinned a mode), restart-once on a stale
79
+ * cursor, rect conversion per slice. Resolves true on exhaustion, false
80
+ * when cancelled; throws on error.
81
+ */
82
+ async function collect(query, opts, sink) {
83
+ const doc = ctx.doc;
84
+ if (!doc) return false;
85
+ sink.onStart();
86
+ let mode = opts.mode ?? "full";
87
+ const modePinned = opts.mode !== void 0;
88
+ let cursor;
89
+ let restarted = false;
90
+ for (;;) {
91
+ let slice;
92
+ try {
93
+ const request = {
94
+ query,
95
+ mode,
96
+ ...cursor !== void 0 ? { cursor } : opts.startPage !== void 0 ? { startPage: opts.startPage } : {}
97
+ };
98
+ const pending = doc.search.query(request);
99
+ sink.setInflight(pending);
100
+ slice = await pending;
101
+ } catch (err) {
102
+ if (sink.isCancelled()) return false;
103
+ if (!modePinned && mode === "full" && cursor === void 0 && isPermissionDenied(err)) {
104
+ mode = "rects";
105
+ continue;
106
+ }
107
+ if (cursor !== void 0 && !restarted && EngineError.is(err, EngineErrorCode.InvalidArg)) {
108
+ restarted = true;
109
+ cursor = void 0;
110
+ sink.onStart();
111
+ continue;
112
+ }
113
+ throw err;
114
+ }
115
+ if (sink.isCancelled()) return false;
116
+ sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);
117
+ if (slice.nextCursor === null) {
118
+ sink.setInflight(null);
119
+ return true;
120
+ }
121
+ cursor = slice.nextCursor;
122
+ }
123
+ }
124
+ /** THE SESSION — collect() with state as the sink, generation-guarded. */
125
+ async function runSession(query, startPage) {
126
+ const gen = ++generation;
127
+ inflight?.abort("superseded");
128
+ inflight = null;
129
+ if (!ctx.doc) {
130
+ ctx.dispatch({ type: "CLEAR" });
131
+ return;
132
+ }
133
+ if (startPage === void 0) {
134
+ const stage = ctx.tryGet(StageToken);
135
+ if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;
136
+ }
137
+ try {
138
+ if (await collect(query, { startPage }, {
139
+ onStart: () => ctx.dispatch({
140
+ type: "START",
141
+ query
142
+ }),
143
+ onSlice: (hits, scanned, total) => ctx.dispatch({
144
+ type: "APPEND",
145
+ hits,
146
+ scanned,
147
+ total
148
+ }),
149
+ isCancelled: () => gen !== generation,
150
+ setInflight: (pending) => {
151
+ if (gen === generation) inflight = pending;
152
+ }
153
+ }) && gen === generation) ctx.dispatch({ type: "COMPLETE" });
154
+ } catch (err) {
155
+ if (gen !== generation) return;
156
+ ctx.dispatch({
157
+ type: "ERROR",
158
+ message: err instanceof Error ? err.message : String(err)
159
+ });
160
+ }
161
+ }
162
+ function goTo(index, reveal) {
163
+ const { hits } = ctx.getState();
164
+ if (hits.length === 0) return null;
165
+ const wrapped = (index % hits.length + hits.length) % hits.length;
166
+ ctx.dispatch({
167
+ type: "SET_ACTIVE",
168
+ index: wrapped
169
+ });
170
+ const hit = hits[wrapped];
171
+ const arrival = {
172
+ ...DEFAULT_REVEAL,
173
+ ...config.reveal,
174
+ ...reveal
175
+ };
176
+ ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {
177
+ rect: boundsOfRects(hit.rects) ?? void 0,
178
+ anchor: arrival.anchor,
179
+ behavior: arrival.behavior
180
+ });
181
+ return hit;
182
+ }
183
+ const clear = () => {
184
+ generation++;
185
+ inflight?.abort("cleared");
186
+ inflight = null;
187
+ ctx.dispatch({ type: "CLEAR" });
188
+ };
189
+ return {
190
+ search: (query, exec) => query.text.length === 0 ? clear() : void runSession(query, exec?.startPage),
191
+ rerun: () => {
192
+ const { query, status } = ctx.getState();
193
+ if (query && status !== "idle") runSession(query);
194
+ },
195
+ clear,
196
+ next: () => goTo(ctx.getState().activeIndex + 1),
197
+ prev: () => goTo(ctx.getState().activeIndex - 1),
198
+ goTo,
199
+ query: () => ctx.getState().query,
200
+ findAll: async (query, opts = {}) => {
201
+ if (!ctx.doc) return [];
202
+ const all = [];
203
+ const signal = opts.signal;
204
+ let pending = null;
205
+ const onAbort = () => pending?.abort(signal?.reason);
206
+ signal?.addEventListener("abort", onAbort, { once: true });
207
+ try {
208
+ await collect(query, { mode: opts.mode }, {
209
+ onStart: () => {
210
+ all.length = 0;
211
+ },
212
+ onSlice: (hits) => all.push(...hits),
213
+ isCancelled: () => signal?.aborted ?? false,
214
+ setInflight: (p) => {
215
+ pending = p;
216
+ }
217
+ });
218
+ if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("findAll aborted");
219
+ return all;
220
+ } finally {
221
+ signal?.removeEventListener("abort", onAbort);
222
+ }
223
+ },
224
+ status: () => ctx.getState().status,
225
+ hits: () => ctx.getState().hits,
226
+ hitCount: () => ctx.getState().hits.length,
227
+ activeIndex: () => ctx.getState().activeIndex,
228
+ activeHit: () => {
229
+ const { hits, activeIndex } = ctx.getState();
230
+ return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;
231
+ },
232
+ hitsForPage: (pon) => {
233
+ const { hits, hitsByPage } = ctx.getState();
234
+ const indices = hitsByPage[pon];
235
+ return indices?.length ? indices.map((i) => hits[i]) : EMPTY;
236
+ },
237
+ progress: () => ctx.getState().progress,
238
+ errorMessage: () => ctx.getState().error
239
+ };
39
240
  }
40
- function setActiveResultIndex(documentId, index) {
41
- return { type: SET_ACTIVE_RESULT_INDEX, payload: { documentId, index } };
241
+ //#endregion
242
+ //#region src/types.ts
243
+ const SearchToken = createCapabilityToken("search");
244
+ //#endregion
245
+ //#region src/effects.ts
246
+ const RERUN_DELAY_MS = 250;
247
+ function registerSearchEffects(ctx) {
248
+ const doc = ctx.doc;
249
+ if (!doc) return;
250
+ let timer = null;
251
+ const unsubscribe = doc.events.subscribe(() => {
252
+ if (ctx.getState().status === "idle") return;
253
+ if (timer !== null) clearTimeout(timer);
254
+ timer = setTimeout(() => {
255
+ timer = null;
256
+ ctx.get(SearchToken).rerun();
257
+ }, RERUN_DELAY_MS);
258
+ });
259
+ ctx.cleanup(() => {
260
+ if (timer !== null) clearTimeout(timer);
261
+ unsubscribe();
262
+ });
42
263
  }
43
- const initialSearchDocumentState = {
44
- flags: [],
45
- results: [],
46
- total: 0,
47
- activeResultIndex: -1,
48
- showAllResults: true,
49
- query: "",
50
- loading: false,
51
- active: false
52
- };
53
- const initialState = {
54
- documents: {}
55
- };
56
- const updateDocState = (state, documentId, newDocState) => {
57
- const oldDocState = state.documents[documentId] || initialSearchDocumentState;
58
- return {
59
- ...state,
60
- documents: {
61
- ...state.documents,
62
- [documentId]: {
63
- ...oldDocState,
64
- ...newDocState
65
- }
66
- }
67
- };
68
- };
69
- const searchReducer = (state = initialState, action) => {
70
- switch (action.type) {
71
- case INIT_SEARCH_STATE:
72
- return {
73
- ...state,
74
- documents: {
75
- ...state.documents,
76
- [action.payload.documentId]: action.payload.state
77
- }
78
- };
79
- case CLEANUP_SEARCH_STATE: {
80
- const documentId = action.payload;
81
- const { [documentId]: removed, ...remaining } = state.documents;
82
- return {
83
- ...state,
84
- documents: remaining
85
- };
86
- }
87
- case START_SEARCH_SESSION:
88
- return updateDocState(state, action.payload.documentId, { active: true });
89
- case STOP_SEARCH_SESSION:
90
- return updateDocState(state, action.payload.documentId, {
91
- results: [],
92
- total: 0,
93
- activeResultIndex: -1,
94
- query: "",
95
- loading: false,
96
- active: false
97
- });
98
- case SET_SEARCH_FLAGS:
99
- return updateDocState(state, action.payload.documentId, { flags: action.payload.flags });
100
- case SET_SHOW_ALL_RESULTS:
101
- return updateDocState(state, action.payload.documentId, {
102
- showAllResults: action.payload.showAll
103
- });
104
- case START_SEARCH:
105
- return updateDocState(state, action.payload.documentId, {
106
- loading: true,
107
- query: action.payload.query,
108
- // clear old results on new search start
109
- results: [],
110
- total: 0,
111
- activeResultIndex: -1
112
- });
113
- case APPEND_SEARCH_RESULTS: {
114
- const { documentId, results } = action.payload;
115
- const docState = state.documents[documentId];
116
- if (!docState) return state;
117
- const newResults = [...docState.results, ...results];
118
- const firstHitIndex = docState.activeResultIndex === -1 && newResults.length > 0 ? 0 : docState.activeResultIndex;
119
- return updateDocState(state, documentId, {
120
- results: newResults,
121
- total: newResults.length,
122
- // total-so-far
123
- activeResultIndex: firstHitIndex,
124
- // keep loading true until final SET_SEARCH_RESULTS
125
- loading: true
126
- });
127
- }
128
- case SET_SEARCH_RESULTS: {
129
- const { documentId, results, total, activeResultIndex } = action.payload;
130
- return updateDocState(state, documentId, {
131
- results,
132
- total,
133
- activeResultIndex,
134
- loading: false
135
- });
136
- }
137
- case SET_ACTIVE_RESULT_INDEX:
138
- return updateDocState(state, action.payload.documentId, {
139
- activeResultIndex: action.payload.index
140
- });
141
- default:
142
- return state;
143
- }
144
- };
145
- const _SearchPlugin = class _SearchPlugin extends BasePlugin {
146
- constructor(id, registry, config) {
147
- super(id, registry);
148
- this.searchStop$ = createBehaviorEmitter();
149
- this.searchStart$ = createBehaviorEmitter();
150
- this.searchResult$ = createBehaviorEmitter();
151
- this.searchActiveResultChange$ = createBehaviorEmitter();
152
- this.searchResultState$ = createBehaviorEmitter();
153
- this.searchState$ = createBehaviorEmitter();
154
- this.currentTask = /* @__PURE__ */ new Map();
155
- this.pluginConfig = config;
156
- }
157
- onDocumentLoadingStarted(documentId) {
158
- const initialState2 = {
159
- ...initialSearchDocumentState,
160
- flags: this.pluginConfig.flags || [],
161
- showAllResults: this.pluginConfig.showAllResults ?? true
162
- };
163
- this.dispatch(initSearchState(documentId, initialState2));
164
- }
165
- onDocumentClosed(documentId) {
166
- this.stopSearchSession(documentId);
167
- this.dispatch(cleanupSearchState(documentId));
168
- this.currentTask.delete(documentId);
169
- }
170
- async initialize() {
171
- }
172
- onStoreUpdated(prevState, newState) {
173
- for (const documentId in newState.documents) {
174
- const prevDocState = prevState.documents[documentId];
175
- const newDocState = newState.documents[documentId];
176
- if (prevDocState !== newDocState) {
177
- this.searchState$.emit({ documentId, state: newDocState });
178
- if (!prevDocState || prevDocState.results !== newDocState.results || prevDocState.activeResultIndex !== newDocState.activeResultIndex || prevDocState.showAllResults !== newDocState.showAllResults || prevDocState.active !== newDocState.active) {
179
- this.searchResultState$.emit({
180
- documentId,
181
- state: {
182
- results: newDocState.results,
183
- activeResultIndex: newDocState.activeResultIndex,
184
- showAllResults: newDocState.showAllResults,
185
- active: newDocState.active
186
- }
187
- });
188
- }
189
- }
190
- }
191
- }
192
- buildCapability() {
193
- const getDocId = (documentId) => documentId ?? this.getActiveDocumentId();
194
- const getDocState = (docId) => {
195
- const id = getDocId(docId);
196
- const state = this.state.documents[id];
197
- if (!state) throw new Error(`Search state not found for document ${id}`);
198
- return state;
199
- };
200
- return {
201
- startSearch: (docId) => this.startSearchSession(getDocId(docId)),
202
- stopSearch: (docId) => this.stopSearchSession(getDocId(docId)),
203
- searchAllPages: (keyword, docId) => this.searchAllPages(keyword, getDocId(docId)),
204
- nextResult: (docId) => this.nextResult(getDocId(docId)),
205
- previousResult: (docId) => this.previousResult(getDocId(docId)),
206
- goToResult: (index, docId) => this.goToResult(index, getDocId(docId)),
207
- setShowAllResults: (showAll, docId) => this.dispatch(setShowAllResults(getDocId(docId), showAll)),
208
- getShowAllResults: (docId) => getDocState(docId).showAllResults,
209
- getFlags: (docId) => getDocState(docId).flags,
210
- setFlags: (flags, docId) => this.setFlags(flags, getDocId(docId)),
211
- getState: (docId) => getDocState(docId),
212
- forDocument: this.createSearchScope.bind(this),
213
- onSearchResult: this.searchResult$.on,
214
- onSearchStart: this.searchStart$.on,
215
- onSearchStop: this.searchStop$.on,
216
- onActiveResultChange: this.searchActiveResultChange$.on,
217
- onSearchResultStateChange: this.searchResultState$.on,
218
- onStateChange: this.searchState$.on
219
- };
220
- }
221
- createSearchScope(documentId) {
222
- const getDocState = () => {
223
- const state = this.state.documents[documentId];
224
- if (!state) throw new Error(`Search state not found for document ${documentId}`);
225
- return state;
226
- };
227
- return {
228
- startSearch: () => this.startSearchSession(documentId),
229
- stopSearch: () => this.stopSearchSession(documentId),
230
- searchAllPages: (keyword) => this.searchAllPages(keyword, documentId),
231
- nextResult: () => this.nextResult(documentId),
232
- previousResult: () => this.previousResult(documentId),
233
- goToResult: (index) => this.goToResult(index, documentId),
234
- setShowAllResults: (showAll) => this.dispatch(setShowAllResults(documentId, showAll)),
235
- getShowAllResults: () => getDocState().showAllResults,
236
- getFlags: () => getDocState().flags,
237
- setFlags: (flags) => this.setFlags(flags, documentId),
238
- getState: getDocState,
239
- onSearchResult: (listener) => this.searchResult$.on((event) => {
240
- if (event.documentId === documentId) listener(event.results);
241
- }),
242
- onSearchStart: (listener) => this.searchStart$.on((event) => {
243
- if (event.documentId === documentId) listener();
244
- }),
245
- onSearchStop: (listener) => this.searchStop$.on((event) => {
246
- if (event.documentId === documentId) listener();
247
- }),
248
- onActiveResultChange: (listener) => this.searchActiveResultChange$.on((event) => {
249
- if (event.documentId === documentId) listener(event.index);
250
- }),
251
- onSearchResultStateChange: (listener) => this.searchResultState$.on((event) => {
252
- if (event.documentId === documentId) listener(event.state);
253
- }),
254
- onStateChange: (listener) => this.searchState$.on((event) => {
255
- if (event.documentId === documentId) listener(event.state);
256
- })
257
- };
258
- }
259
- setFlags(flags, documentId) {
260
- this.dispatch(setSearchFlags(documentId, flags));
261
- const docState = this.state.documents[documentId];
262
- if (docState == null ? void 0 : docState.active) {
263
- this.searchAllPages(docState.query, documentId, true);
264
- }
265
- }
266
- notifySearchStart(documentId) {
267
- this.searchStart$.emit({ documentId });
268
- }
269
- notifySearchStop(documentId) {
270
- this.searchStop$.emit({ documentId });
271
- }
272
- notifyActiveResultChange(documentId, index) {
273
- this.searchActiveResultChange$.emit({ documentId, index });
274
- }
275
- startSearchSession(documentId) {
276
- const coreDoc = this.getCoreDocument(documentId);
277
- if (!coreDoc) return;
278
- this.dispatch(startSearchSession(documentId));
279
- this.notifySearchStart(documentId);
280
- }
281
- stopSearchSession(documentId) {
282
- var _a;
283
- const docState = this.state.documents[documentId];
284
- if (!(docState == null ? void 0 : docState.active)) return;
285
- const task = this.currentTask.get(documentId);
286
- if (task) {
287
- try {
288
- (_a = task.abort) == null ? void 0 : _a.call(task, { code: PdfErrorCode.Cancelled, message: "search stopped" });
289
- } catch {
290
- }
291
- this.currentTask.delete(documentId);
292
- }
293
- this.dispatch(stopSearchSession(documentId));
294
- this.notifySearchStop(documentId);
295
- }
296
- searchAllPages(keyword, documentId, force = false) {
297
- var _a;
298
- const docState = this.state.documents[documentId];
299
- if (!docState) {
300
- return PdfTaskHelper.reject({
301
- code: PdfErrorCode.NotFound,
302
- message: "Search state not initialized"
303
- });
304
- }
305
- const coreDoc = this.getCoreDocument(documentId);
306
- if (!(coreDoc == null ? void 0 : coreDoc.document)) {
307
- return PdfTaskHelper.reject({ code: PdfErrorCode.NotFound, message: "Document not loaded" });
308
- }
309
- const trimmedKeyword = keyword.trim();
310
- if (docState.query === trimmedKeyword && !force) {
311
- return PdfTaskHelper.resolve({
312
- results: docState.results,
313
- total: docState.total
314
- });
315
- }
316
- const oldTask = this.currentTask.get(documentId);
317
- if (oldTask) {
318
- try {
319
- (_a = oldTask.abort) == null ? void 0 : _a.call(oldTask, { code: PdfErrorCode.Cancelled, message: "new search" });
320
- } catch {
321
- }
322
- this.currentTask.delete(documentId);
323
- }
324
- this.dispatch(startSearch(documentId, trimmedKeyword));
325
- if (!trimmedKeyword) {
326
- this.dispatch(setSearchResults(documentId, [], 0, -1));
327
- return PdfTaskHelper.resolve({
328
- results: [],
329
- total: 0
330
- });
331
- }
332
- if (!docState.active) {
333
- this.startSearchSession(documentId);
334
- }
335
- const task = this.engine.searchAllPages(coreDoc.document, trimmedKeyword, {
336
- flags: docState.flags
337
- });
338
- this.currentTask.set(documentId, task);
339
- task.onProgress((p) => {
340
- var _a2;
341
- if ((_a2 = p == null ? void 0 : p.results) == null ? void 0 : _a2.length) {
342
- if (this.currentTask.get(documentId) === task) {
343
- this.dispatch(appendSearchResults(documentId, p.results));
344
- if (this.state.documents[documentId].activeResultIndex === -1) {
345
- this.dispatch(setActiveResultIndex(documentId, 0));
346
- this.notifyActiveResultChange(documentId, 0);
347
- }
348
- }
349
- }
350
- });
351
- task.wait(
352
- (results) => {
353
- this.currentTask.delete(documentId);
354
- const activeResultIndex = results.total > 0 ? 0 : -1;
355
- this.dispatch(
356
- setSearchResults(documentId, results.results, results.total, activeResultIndex)
357
- );
358
- this.searchResult$.emit({ documentId, results });
359
- if (results.total > 0) {
360
- this.notifyActiveResultChange(documentId, 0);
361
- }
362
- },
363
- (error) => {
364
- var _a2;
365
- if (((_a2 = error == null ? void 0 : error.reason) == null ? void 0 : _a2.code) !== PdfErrorCode.Cancelled) {
366
- console.error("Error during search:", error);
367
- this.dispatch(setSearchResults(documentId, [], 0, -1));
368
- }
369
- this.currentTask.delete(documentId);
370
- }
371
- );
372
- return task;
373
- }
374
- nextResult(documentId) {
375
- const docState = this.state.documents[documentId];
376
- if (!docState || docState.results.length === 0) return -1;
377
- const nextIndex = docState.activeResultIndex >= docState.results.length - 1 ? 0 : docState.activeResultIndex + 1;
378
- return this.goToResult(nextIndex, documentId);
379
- }
380
- previousResult(documentId) {
381
- const docState = this.state.documents[documentId];
382
- if (!docState || docState.results.length === 0) return -1;
383
- const prevIndex = docState.activeResultIndex <= 0 ? docState.results.length - 1 : docState.activeResultIndex - 1;
384
- return this.goToResult(prevIndex, documentId);
385
- }
386
- goToResult(index, documentId) {
387
- const docState = this.state.documents[documentId];
388
- if (!docState || docState.results.length === 0 || index < 0 || index >= docState.results.length) {
389
- return -1;
390
- }
391
- this.dispatch(setActiveResultIndex(documentId, index));
392
- this.notifyActiveResultChange(documentId, index);
393
- return index;
394
- }
395
- async destroy() {
396
- for (const documentId of Object.keys(this.state.documents)) {
397
- this.stopSearchSession(documentId);
398
- }
399
- this.searchResult$.clear();
400
- this.searchStart$.clear();
401
- this.searchStop$.clear();
402
- this.searchActiveResultChange$.clear();
403
- this.searchResultState$.clear();
404
- this.searchState$.clear();
405
- super.destroy();
406
- }
407
- };
408
- _SearchPlugin.id = "search";
409
- let SearchPlugin = _SearchPlugin;
410
- const SEARCH_PLUGIN_ID = "search";
411
- const manifest = {
412
- id: SEARCH_PLUGIN_ID,
413
- name: "Search Plugin",
414
- version: "1.0.0",
415
- provides: ["search"],
416
- requires: [],
417
- optional: [],
418
- defaultConfig: {
419
- flags: []
420
- }
421
- };
422
- const SearchPluginPackage = {
423
- manifest,
424
- create: (registry, config) => new SearchPlugin(SEARCH_PLUGIN_ID, registry, config),
425
- reducer: searchReducer,
426
- initialState
427
- };
428
- export {
429
- SEARCH_PLUGIN_ID,
430
- SearchPlugin,
431
- SearchPluginPackage,
432
- initialSearchDocumentState,
433
- initialState,
434
- manifest
264
+ //#endregion
265
+ //#region src/reducer.ts
266
+ const initialSearchState = () => ({
267
+ query: null,
268
+ status: "idle",
269
+ hits: [],
270
+ hitsByPage: {},
271
+ activeIndex: -1,
272
+ progress: {
273
+ scanned: 0,
274
+ total: 0
275
+ },
276
+ error: null
277
+ });
278
+ const searchReducer = (state, action) => {
279
+ switch (action.type) {
280
+ case "START": return {
281
+ ...initialSearchState(),
282
+ query: action.query,
283
+ status: "searching"
284
+ };
285
+ case "APPEND": {
286
+ if (action.hits.length === 0) return {
287
+ ...state,
288
+ progress: {
289
+ scanned: action.scanned,
290
+ total: action.total
291
+ }
292
+ };
293
+ const hits = state.hits.concat(action.hits);
294
+ const hitsByPage = { ...state.hitsByPage };
295
+ for (let i = state.hits.length; i < hits.length; i++) {
296
+ const pon = hits[i].pon;
297
+ hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);
298
+ }
299
+ return {
300
+ ...state,
301
+ hits,
302
+ hitsByPage,
303
+ activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,
304
+ progress: {
305
+ scanned: action.scanned,
306
+ total: action.total
307
+ }
308
+ };
309
+ }
310
+ case "COMPLETE": return {
311
+ ...state,
312
+ status: "complete"
313
+ };
314
+ case "ERROR": return {
315
+ ...state,
316
+ status: "error",
317
+ error: action.message
318
+ };
319
+ case "SET_ACTIVE": return {
320
+ ...state,
321
+ activeIndex: action.index
322
+ };
323
+ case "CLEAR": return initialSearchState();
324
+ default: return state;
325
+ }
435
326
  };
436
- //# sourceMappingURL=index.js.map
327
+ //#endregion
328
+ //#region src/search.plugin.ts
329
+ /**
330
+ * Document text search over `doc.search` — the engine's budgeted,
331
+ * cursor-resumable slices. Document-scoped; no pointer handling, so it
332
+ * needs no interaction hub. The Stage is optional: when present, scans
333
+ * start at the current page (viewport-first) and hit navigation reveals
334
+ * the hit's page; without it the host owns scrolling.
335
+ */
336
+ const searchPlugin = (config) => definePlugin({
337
+ id: "search",
338
+ token: SearchToken,
339
+ scope: "document",
340
+ optional: [StageToken],
341
+ initialState: initialSearchState,
342
+ reduce: searchReducer,
343
+ capability: (ctx) => createSearchCapability(ctx, config),
344
+ effects: registerSearchEffects
345
+ });
346
+ //#endregion
347
+ export { SearchToken, searchPlugin, validateSearchQuery, validateSearchRegex };
348
+
349
+ //# sourceMappingURL=index.js.map