@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.
- package/LICENSE +192 -21
- package/dist/index.cjs +363 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -0
- package/dist/index.d.ts +156 -1
- package/dist/index.js +346 -433
- package/dist/index.js.map +1 -1
- package/package.json +21 -57
- package/dist/lib/actions.d.ts +0 -91
- package/dist/lib/index.d.ts +0 -10
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/reducer.d.ts +0 -6
- package/dist/lib/search-plugin.d.ts +0 -32
- package/dist/lib/types.d.ts +0 -165
- package/dist/preact/adapter.d.ts +0 -5
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -107
- package/dist/preact/index.js.map +0 -1
- package/dist/react/adapter.d.ts +0 -2
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -106
- package/dist/react/index.js.map +0 -1
- package/dist/shared/components/index.d.ts +0 -1
- package/dist/shared/components/search-layer.d.ts +0 -11
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/use-search.d.ts +0 -15
- package/dist/shared/index.d.ts +0 -3
- package/dist/shared-preact/components/index.d.ts +0 -1
- package/dist/shared-preact/components/search-layer.d.ts +0 -11
- package/dist/shared-preact/hooks/index.d.ts +0 -1
- package/dist/shared-preact/hooks/use-search.d.ts +0 -15
- package/dist/shared-preact/index.d.ts +0 -3
- package/dist/shared-react/components/index.d.ts +0 -1
- package/dist/shared-react/components/search-layer.d.ts +0 -11
- package/dist/shared-react/hooks/index.d.ts +0 -1
- package/dist/shared-react/hooks/use-search.d.ts +0 -15
- package/dist/shared-react/index.d.ts +0 -3
- package/dist/svelte/components/SearchLayer.svelte.d.ts +0 -11
- package/dist/svelte/components/index.d.ts +0 -1
- package/dist/svelte/hooks/index.d.ts +0 -1
- package/dist/svelte/hooks/use-search.svelte.d.ts +0 -21
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -3
- package/dist/svelte/index.js +0 -129
- package/dist/svelte/index.js.map +0 -1
- package/dist/vue/components/index.d.ts +0 -1
- package/dist/vue/components/search-layer.vue.d.ts +0 -13
- package/dist/vue/hooks/index.d.ts +0 -1
- package/dist/vue/hooks/use-search.d.ts +0 -12
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -3
- package/dist/vue/index.js +0 -131
- package/dist/vue/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,436 +1,349 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
//#
|
|
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
|