@embedpdf/plugin-search 3.0.0-next.13 → 3.0.0-next.15
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/dist/contract-6pr6LBhA.cjs +42 -0
- package/dist/contract-6pr6LBhA.cjs.map +1 -0
- package/dist/contract-B7maCczh.d.ts +214 -0
- package/dist/contract-DqeptOSn.d.cts +214 -0
- package/dist/contract-uWSW5IYM.js +31 -0
- package/dist/contract-uWSW5IYM.js.map +1 -0
- package/dist/contract.cjs +2 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/index.cjs +335 -290
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -8
- package/dist/index.d.ts +36 -8
- package/dist/index.js +336 -294
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/contract-4PZsI7Ty.cjs +0 -13
- package/dist/contract-4PZsI7Ty.cjs.map +0 -1
- package/dist/contract-CSb66POC.d.cts +0 -170
- package/dist/contract-Cj4xgdMj.d.ts +0 -170
- package/dist/contract-YzH14CUn.js +0 -8
- package/dist/contract-YzH14CUn.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,364 +1,409 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_contract = require("./contract-
|
|
2
|
+
const require_contract = require("./contract-6pr6LBhA.cjs");
|
|
3
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
4
4
|
let _embedpdf_plugin_stage_contract = require("@embedpdf/plugin-stage/contract");
|
|
5
5
|
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
6
6
|
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
7
|
-
//#region src/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
//#region src/model.ts
|
|
8
|
+
/**
|
|
9
|
+
* The search session: the query, the hits found so far (also grouped by
|
|
10
|
+
* page), the active hit, and progress. Every function below is a pure
|
|
11
|
+
* transition; the controller applies them with `ctx.state.update`.
|
|
12
|
+
*/
|
|
13
|
+
/** The progress of no search, one object for every session that hasn't searched a page yet. */
|
|
14
|
+
const NO_PROGRESS = Object.freeze({
|
|
15
|
+
pagesSearched: 0,
|
|
16
|
+
pageCount: 0
|
|
17
|
+
});
|
|
18
|
+
const initialSearchState = () => ({
|
|
19
|
+
query: null,
|
|
20
|
+
status: "idle",
|
|
21
|
+
operationId: null,
|
|
22
|
+
hits: [],
|
|
23
|
+
hitsByPage: {},
|
|
24
|
+
activeHitIndex: -1,
|
|
25
|
+
progress: NO_PROGRESS,
|
|
26
|
+
error: null
|
|
27
|
+
});
|
|
28
|
+
/** A new scan starts: everything from the previous session is dropped. */
|
|
29
|
+
const startSession = (_state, query, operationId) => ({
|
|
30
|
+
...initialSearchState(),
|
|
31
|
+
query,
|
|
32
|
+
status: "searching",
|
|
33
|
+
operationId
|
|
34
|
+
});
|
|
35
|
+
const sameProgress = (left, right) => left.pagesSearched === right.pagesSearched && left.pageCount === right.pageCount;
|
|
36
|
+
/**
|
|
37
|
+
* A slice of hits arrived. The first hit becomes active so "1 of N" reads
|
|
38
|
+
* right; moving the camera is navigation's job, not the stream's. Progress
|
|
39
|
+
* with the same counts keeps its object: readers, and `onProgressChanged`,
|
|
40
|
+
* see a change only when the counts change.
|
|
41
|
+
*/
|
|
42
|
+
function appendHits(state, hits, slice) {
|
|
43
|
+
const progress = sameProgress(state.progress, slice) ? state.progress : slice;
|
|
44
|
+
if (hits.length === 0) return progress === state.progress ? state : {
|
|
45
|
+
...state,
|
|
46
|
+
progress
|
|
47
|
+
};
|
|
48
|
+
const hitsByPage = { ...state.hitsByPage };
|
|
49
|
+
for (const hit of hits) {
|
|
50
|
+
const pageObjectNumber = hit.page.objectNumber;
|
|
51
|
+
hitsByPage[pageObjectNumber] = [...hitsByPage[pageObjectNumber] ?? [], hit];
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
hits: [...state.hits, ...hits],
|
|
56
|
+
hitsByPage,
|
|
57
|
+
activeHitIndex: state.activeHitIndex === -1 ? 0 : state.activeHitIndex,
|
|
58
|
+
progress
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const completeSession = (state) => ({
|
|
62
|
+
...state,
|
|
63
|
+
status: "complete"
|
|
64
|
+
});
|
|
65
|
+
const cancelSession = (state) => ({
|
|
66
|
+
...state,
|
|
67
|
+
status: "cancelled"
|
|
68
|
+
});
|
|
69
|
+
const failSession = (state, error) => ({
|
|
70
|
+
...state,
|
|
71
|
+
status: "error",
|
|
72
|
+
error
|
|
73
|
+
});
|
|
74
|
+
const setActiveHit = (state, index) => state.activeHitIndex === index ? state : {
|
|
75
|
+
...state,
|
|
76
|
+
activeHitIndex: index
|
|
13
77
|
};
|
|
14
|
-
|
|
15
|
-
const isPermissionDenied = (err) => err instanceof Error && err.name === "PermissionDenied" || _embedpdf_engine_core_runtime.EngineError.is(err, _embedpdf_engine_core_runtime.EngineErrorCode.Forbidden);
|
|
78
|
+
const clearSession = (state) => state.status === "idle" ? state : initialSearchState();
|
|
16
79
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
80
|
+
* Where `hit` is in `hits`: the same object, or else the hit that covers the
|
|
81
|
+
* same characters of the same page, so a hit kept from before the search ran
|
|
82
|
+
* again still finds its place. -1 when it isn't there.
|
|
83
|
+
*/
|
|
84
|
+
function indexOfHit(hits, hit) {
|
|
85
|
+
const same = hits.indexOf(hit);
|
|
86
|
+
if (same >= 0) return same;
|
|
87
|
+
return hits.findIndex((candidate) => candidate.start === hit.start && candidate.count === hit.count && (0, _embedpdf_core.pageRefsEqual)(candidate.page, hit.page));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The pages that have at least one hit, in document order: `pages` is the
|
|
91
|
+
* document's page list, whose order moving a page changes (object numbers
|
|
92
|
+
* don't follow it).
|
|
93
|
+
*/
|
|
94
|
+
const pagesWithHits = (state, pages) => pages.filter((page) => (state.hitsByPage[page.ref.objectNumber]?.length ?? 0) > 0).map((page) => page.ref);
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/controller.ts
|
|
97
|
+
const EMPTY = Object.freeze([]);
|
|
98
|
+
const EMPTY_PAGES = Object.freeze([]);
|
|
99
|
+
/** Reruns after document mutations are coalesced: a burst of events triggers one rescan. */
|
|
100
|
+
const RERUN_DELAY_MS = 250;
|
|
101
|
+
/**
|
|
102
|
+
* The search controller. `search` runs on a `latest` lane (a newer search
|
|
103
|
+
* supersedes the older one, which can no longer publish); `findAll` runs on
|
|
104
|
+
* its own signal and never touches the session. Both share `collect`, the
|
|
105
|
+
* one cursor loop over the engine's budgeted slices.
|
|
21
106
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
107
|
+
* The scan's own events (started, completed, cancelled, failed) fire where
|
|
108
|
+
* the scan reaches those points. The progress, active-hit and cleared events
|
|
109
|
+
* are derived from state changes, so they always agree with the getters.
|
|
25
110
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the
|
|
29
|
-
* hit's page. Without one, navigation still works; the host scrolls.
|
|
111
|
+
* The settings are read where they're used, never copied, so
|
|
112
|
+
* `updateSettings({ reveal })` applies to the next move at once.
|
|
30
113
|
*/
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
114
|
+
function createSearchController(ctx) {
|
|
115
|
+
const settings = ctx.settings();
|
|
116
|
+
const session = ctx.latest("session");
|
|
117
|
+
const started = ctx.events.source();
|
|
118
|
+
const progressChanged = ctx.events.source();
|
|
119
|
+
const completed = ctx.events.source();
|
|
120
|
+
const cancelled = ctx.events.source();
|
|
121
|
+
const failed = ctx.events.source();
|
|
122
|
+
const activeHitChanged = ctx.events.source();
|
|
123
|
+
const cleared = ctx.events.source();
|
|
124
|
+
const state = () => ctx.state.get();
|
|
125
|
+
ctx.state.onChange(({ previous, next }) => {
|
|
126
|
+
if (previous.progress !== next.progress) progressChanged.emit({
|
|
127
|
+
...next.progress,
|
|
128
|
+
hitCount: next.hits.length
|
|
129
|
+
});
|
|
130
|
+
if (previous.activeHitIndex !== next.activeHitIndex) activeHitChanged.emit({
|
|
131
|
+
index: next.activeHitIndex,
|
|
132
|
+
hit: next.hits[next.activeHitIndex] ?? null
|
|
133
|
+
});
|
|
134
|
+
if (previous.status !== "idle" && next.status === "idle") cleared.emit({});
|
|
135
|
+
});
|
|
34
136
|
function hitsFromSlice(slice) {
|
|
35
|
-
const converters = /* @__PURE__ */ new Map();
|
|
36
|
-
const converterFor = (pon) => {
|
|
37
|
-
let conv = converters.get(pon);
|
|
38
|
-
if (conv !== void 0) return conv;
|
|
39
|
-
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
|
|
40
|
-
if (!layout) conv = null;
|
|
41
|
-
else {
|
|
42
|
-
const { pdfToContent } = (0, _embedpdf_core_geometry.pageGeometry)({
|
|
43
|
-
crop: layout.boxes.crop,
|
|
44
|
-
rotation: layout.rotation,
|
|
45
|
-
userUnit: layout.userUnit
|
|
46
|
-
}, 1);
|
|
47
|
-
const toPoint = (pt) => (0, _embedpdf_core_geometry.applyPoint)(pdfToContent, pt);
|
|
48
|
-
conv = {
|
|
49
|
-
pageIndex: layout.index,
|
|
50
|
-
toQuad: (q) => ({
|
|
51
|
-
upperStart: toPoint(q.p1),
|
|
52
|
-
upperEnd: toPoint(q.p2),
|
|
53
|
-
lowerStart: toPoint(q.p3),
|
|
54
|
-
lowerEnd: toPoint(q.p4)
|
|
55
|
-
})
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
converters.set(pon, conv);
|
|
59
|
-
return conv;
|
|
60
|
-
};
|
|
61
137
|
const hits = [];
|
|
62
138
|
for (const match of slice.matches) {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
const segments = match.segments
|
|
66
|
-
const quad = conv.toQuad(segment.quad);
|
|
67
|
-
return {
|
|
68
|
-
quad,
|
|
69
|
-
rect: (0, _embedpdf_core_geometry.textQuadBounds)(quad),
|
|
70
|
-
advance: segment.advance
|
|
71
|
-
};
|
|
72
|
-
});
|
|
139
|
+
const page = ctx.getPage(match.page);
|
|
140
|
+
if (!page) continue;
|
|
141
|
+
const segments = match.segments;
|
|
73
142
|
hits.push({
|
|
74
|
-
|
|
75
|
-
pageIndex:
|
|
76
|
-
|
|
77
|
-
|
|
143
|
+
page: match.page,
|
|
144
|
+
pageIndex: page.index,
|
|
145
|
+
start: match.start,
|
|
146
|
+
count: match.count,
|
|
78
147
|
segments,
|
|
79
|
-
...segments.length ? { bounds: (0, _embedpdf_core_geometry.boundsOfRects)(segments.map((
|
|
148
|
+
...segments.length ? { bounds: (0, _embedpdf_core_geometry.boundsOfRects)(segments.map((segment) => segment.rect)) ?? void 0 } : {},
|
|
80
149
|
...match.snippet ? { snippet: match.snippet } : {}
|
|
81
150
|
});
|
|
82
151
|
}
|
|
83
152
|
return hits;
|
|
84
153
|
}
|
|
85
154
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* cursor, rect conversion per slice. Resolves true on exhaustion, false
|
|
91
|
-
* when cancelled; throws on error.
|
|
155
|
+
* One cursor loop over budgeted slices: permission fallback (snippets → none
|
|
156
|
+
* when they are denied, unless pinned), restart-once on a stale cursor,
|
|
157
|
+
* projection per slice. Returns true on exhaustion, false when the signal
|
|
158
|
+
* aborted; throws on a real error (already a PluginError: ctx.doc is guarded).
|
|
92
159
|
*/
|
|
93
|
-
async function collect(query,
|
|
94
|
-
const
|
|
95
|
-
if (!doc) return false;
|
|
160
|
+
async function collect(query, options, sink) {
|
|
161
|
+
const { signal } = options;
|
|
96
162
|
sink.onStart();
|
|
97
|
-
let
|
|
98
|
-
const
|
|
163
|
+
let snippets = options.snippets ?? true;
|
|
164
|
+
const snippetsPinned = options.snippets !== void 0;
|
|
99
165
|
let cursor;
|
|
100
166
|
let restarted = false;
|
|
101
167
|
for (;;) {
|
|
168
|
+
if (signal?.aborted) return false;
|
|
102
169
|
let slice;
|
|
170
|
+
const request = {
|
|
171
|
+
...query,
|
|
172
|
+
snippets,
|
|
173
|
+
...cursor !== void 0 ? { cursor } : options.from !== void 0 ? { from: options.from } : {}
|
|
174
|
+
};
|
|
103
175
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const pending = doc.search.query(request);
|
|
110
|
-
sink.setInflight(pending);
|
|
111
|
-
slice = await pending;
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (sink.isCancelled()) return false;
|
|
114
|
-
if (!modePinned && mode === "full" && cursor === void 0 && isPermissionDenied(err)) {
|
|
115
|
-
mode = "rects";
|
|
176
|
+
slice = await ctx.cancellable(signal, ctx.doc.with({ priority: "low" }).search.query(request));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (signal?.aborted) return false;
|
|
179
|
+
if (!snippetsPinned && snippets && cursor === void 0 && (0, _embedpdf_core.isPluginError)(error, "permission-denied")) {
|
|
180
|
+
snippets = false;
|
|
116
181
|
continue;
|
|
117
182
|
}
|
|
118
|
-
if (cursor !== void 0 && !restarted &&
|
|
183
|
+
if (cursor !== void 0 && !restarted && (0, _embedpdf_core.isPluginError)(error, "invalid-input")) {
|
|
119
184
|
restarted = true;
|
|
120
185
|
cursor = void 0;
|
|
121
186
|
sink.onStart();
|
|
122
187
|
continue;
|
|
123
188
|
}
|
|
124
|
-
throw
|
|
125
|
-
}
|
|
126
|
-
if (sink.isCancelled()) return false;
|
|
127
|
-
sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);
|
|
128
|
-
if (slice.nextCursor === null) {
|
|
129
|
-
sink.setInflight(null);
|
|
130
|
-
return true;
|
|
189
|
+
throw error;
|
|
131
190
|
}
|
|
191
|
+
if (signal?.aborted) return false;
|
|
192
|
+
sink.onSlice(hitsFromSlice(slice), slice.pagesSearched, slice.pageCount);
|
|
193
|
+
if (slice.nextCursor === null) return true;
|
|
132
194
|
cursor = slice.nextCursor;
|
|
133
195
|
}
|
|
134
196
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const stage = ctx.tryGet(_embedpdf_plugin_stage_contract.StageToken);
|
|
146
|
-
if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;
|
|
197
|
+
const viewportFirstPage = () => {
|
|
198
|
+
return ctx.tryGet(_embedpdf_plugin_stage_contract.StageToken)?.getCurrentPage() ?? void 0;
|
|
199
|
+
};
|
|
200
|
+
async function search(query, options = {}) {
|
|
201
|
+
if (query.text.length === 0) {
|
|
202
|
+
clear();
|
|
203
|
+
return {
|
|
204
|
+
status: "complete",
|
|
205
|
+
hitCount: 0
|
|
206
|
+
};
|
|
147
207
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
ctx.assertAllowed("doc.text.search", "search");
|
|
209
|
+
const from = options.from === void 0 ? viewportFirstPage() : ctx.pageOf(options.from).ref;
|
|
210
|
+
let operationId = "";
|
|
211
|
+
return session.run(async (run) => {
|
|
212
|
+
operationId = run.id;
|
|
213
|
+
if (!await collect(query, {
|
|
214
|
+
from,
|
|
215
|
+
signal: run.signal
|
|
216
|
+
}, {
|
|
217
|
+
onStart: () => run.commit(() => {
|
|
218
|
+
ctx.state.update(startSession, query, run.id);
|
|
219
|
+
started.emit({ query });
|
|
153
220
|
}),
|
|
154
|
-
onSlice: (hits,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (gen === generation) inflight = pending;
|
|
163
|
-
}
|
|
164
|
-
}) && gen === generation) ctx.dispatch({ type: "COMPLETE" });
|
|
165
|
-
} catch (err) {
|
|
166
|
-
if (gen !== generation) return;
|
|
167
|
-
ctx.dispatch({
|
|
168
|
-
type: "ERROR",
|
|
169
|
-
message: err instanceof Error ? err.message : String(err)
|
|
221
|
+
onSlice: (hits, pagesSearched, pageCount) => run.commit(() => ctx.state.update(appendHits, hits, {
|
|
222
|
+
pagesSearched,
|
|
223
|
+
pageCount
|
|
224
|
+
}))
|
|
225
|
+
})) throw new _embedpdf_core.PluginError("operation-cancelled", "search", "search aborted");
|
|
226
|
+
run.commit(() => {
|
|
227
|
+
ctx.state.update(completeSession);
|
|
228
|
+
completed.emit({ hitCount: state().hits.length });
|
|
170
229
|
});
|
|
171
|
-
|
|
230
|
+
return {
|
|
231
|
+
status: "complete",
|
|
232
|
+
hitCount: state().hits.length
|
|
233
|
+
};
|
|
234
|
+
}, options).catch((error) => {
|
|
235
|
+
if ((0, _embedpdf_core.isPluginError)(error, "instance-closed")) return {
|
|
236
|
+
status: "cancelled",
|
|
237
|
+
hitCount: 0
|
|
238
|
+
};
|
|
239
|
+
if ((0, _embedpdf_core.isPluginError)(error, "operation-cancelled")) {
|
|
240
|
+
const why = error.details?.reason ?? "cancelled";
|
|
241
|
+
const reason = why === "superseded" ? "superseded" : why === "cleared" ? "cleared" : "cancelled";
|
|
242
|
+
if (reason === "cancelled" && state().operationId === operationId) ctx.state.update(cancelSession);
|
|
243
|
+
cancelled.emit({ reason });
|
|
244
|
+
return {
|
|
245
|
+
status: reason === "superseded" ? "superseded" : "cancelled",
|
|
246
|
+
hitCount: state().hits.length
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const info = (0, _embedpdf_core.toPluginErrorInfo)((0, _embedpdf_core.toPluginError)("search", error));
|
|
250
|
+
if (state().operationId === operationId) ctx.state.update(failSession, info);
|
|
251
|
+
failed.emit({ error: info });
|
|
252
|
+
throw error;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function clear() {
|
|
256
|
+
session.cancel("cleared");
|
|
257
|
+
ctx.state.update(clearSession);
|
|
172
258
|
}
|
|
173
|
-
function
|
|
174
|
-
const { hits } =
|
|
259
|
+
function goToHit(target, options) {
|
|
260
|
+
const { hits } = state();
|
|
175
261
|
if (hits.length === 0) return null;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
262
|
+
const index = typeof target === "number" ? (target % hits.length + hits.length) % hits.length : indexOfHit(hits, target);
|
|
263
|
+
if (index < 0) return null;
|
|
264
|
+
ctx.state.update(setActiveHit, index);
|
|
265
|
+
reveal(hits[index], options);
|
|
266
|
+
return hits[index];
|
|
267
|
+
}
|
|
268
|
+
function reveal(hit, options) {
|
|
182
269
|
const arrival = {
|
|
183
|
-
...
|
|
184
|
-
...
|
|
185
|
-
...reveal
|
|
270
|
+
...settings.get().reveal,
|
|
271
|
+
...options
|
|
186
272
|
};
|
|
187
|
-
ctx.tryGet(_embedpdf_plugin_stage_contract.StageToken)?.reveal(hit.
|
|
273
|
+
ctx.tryGet(_embedpdf_plugin_stage_contract.StageToken)?.reveal(hit.page, {
|
|
188
274
|
rect: hit.bounds,
|
|
189
275
|
anchor: arrival.anchor,
|
|
190
276
|
behavior: arrival.behavior
|
|
191
277
|
});
|
|
192
|
-
return hit;
|
|
193
278
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
279
|
+
/**
|
|
280
|
+
* One page's hits, by the page's ref or index. A page that isn't in the
|
|
281
|
+
* document has none: a layer may read while its page is being deleted.
|
|
282
|
+
*/
|
|
283
|
+
const hitsOn = (page) => {
|
|
284
|
+
const found = ctx.getPage(page);
|
|
285
|
+
return found ? state().hitsByPage[found.ref.objectNumber] ?? EMPTY : EMPTY;
|
|
199
286
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
287
|
+
const api = {
|
|
288
|
+
...settings.api,
|
|
289
|
+
canSearch: (options) => ctx.allows("doc.text.search") && (!options?.snippets || ctx.allows("doc.text.copy")),
|
|
290
|
+
search,
|
|
291
|
+
refresh: (options) => {
|
|
292
|
+
const { query, status } = state();
|
|
293
|
+
if (!query || status === "idle") return Promise.resolve({
|
|
294
|
+
status: "complete",
|
|
295
|
+
hitCount: 0
|
|
296
|
+
});
|
|
297
|
+
return search(query, options);
|
|
210
298
|
},
|
|
299
|
+
cancel: () => session.cancel("cancelled"),
|
|
211
300
|
clear,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
const all = [];
|
|
219
|
-
const signal = opts.signal;
|
|
220
|
-
let pending = null;
|
|
221
|
-
const onAbort = () => pending?.abort(signal?.reason);
|
|
222
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
223
|
-
try {
|
|
224
|
-
await collect(query, { mode: opts.mode }, {
|
|
225
|
-
onStart: () => {
|
|
226
|
-
all.length = 0;
|
|
227
|
-
},
|
|
228
|
-
onSlice: (hits) => all.push(...hits),
|
|
229
|
-
isCancelled: () => signal?.aborted ?? false,
|
|
230
|
-
setInflight: (p) => {
|
|
231
|
-
pending = p;
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("findAll aborted");
|
|
235
|
-
return all;
|
|
236
|
-
} finally {
|
|
237
|
-
signal?.removeEventListener("abort", onAbort);
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
status: () => ctx.getState().status,
|
|
241
|
-
hits: () => ctx.getState().hits,
|
|
242
|
-
hitCount: () => ctx.getState().hits.length,
|
|
243
|
-
activeIndex: () => ctx.getState().activeIndex,
|
|
244
|
-
activeHit: () => {
|
|
245
|
-
const { hits, activeIndex } = ctx.getState();
|
|
246
|
-
return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;
|
|
301
|
+
nextHit: (options) => goToHit(state().activeHitIndex + 1, options),
|
|
302
|
+
previousHit: (options) => goToHit(state().activeHitIndex - 1, options),
|
|
303
|
+
goToHit,
|
|
304
|
+
revealActiveHit: (options) => {
|
|
305
|
+
const hit = api.getActiveHit();
|
|
306
|
+
if (hit) reveal(hit, options);
|
|
247
307
|
},
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
308
|
+
getQuery: () => state().query,
|
|
309
|
+
getStatus: () => state().status,
|
|
310
|
+
listHits: (filter) => filter?.page === void 0 ? state().hits : hitsOn(filter.page),
|
|
311
|
+
getHitCount: (page) => (page === void 0 ? state().hits : hitsOn(page)).length,
|
|
312
|
+
listPagesWithHits: () => state().hits.length ? pagesWithHits(state(), ctx.document()?.pages ?? []) : EMPTY_PAGES,
|
|
313
|
+
getActiveHitIndex: () => state().activeHitIndex,
|
|
314
|
+
getActiveHit: () => state().hits[state().activeHitIndex] ?? null,
|
|
315
|
+
getProgress: () => state().progress,
|
|
316
|
+
getError: () => state().error,
|
|
317
|
+
findAll: async (query, options = {}) => {
|
|
318
|
+
ctx.assertAllowed("doc.text.search", "findAll");
|
|
319
|
+
const all = [];
|
|
320
|
+
if (!await collect(query, {
|
|
321
|
+
...options.snippets !== void 0 ? { snippets: options.snippets } : {},
|
|
322
|
+
...options.signal ? { signal: options.signal } : {}
|
|
323
|
+
}, {
|
|
324
|
+
onStart: () => {
|
|
325
|
+
all.length = 0;
|
|
326
|
+
},
|
|
327
|
+
onSlice: (hits) => all.push(...hits)
|
|
328
|
+
})) throw new _embedpdf_core.PluginError("operation-cancelled", "search", "findAll aborted");
|
|
329
|
+
return all;
|
|
252
330
|
},
|
|
253
|
-
|
|
254
|
-
|
|
331
|
+
onStarted: started.on,
|
|
332
|
+
onProgressChanged: progressChanged.on,
|
|
333
|
+
onCompleted: completed.on,
|
|
334
|
+
onCancelled: cancelled.on,
|
|
335
|
+
onFailed: failed.on,
|
|
336
|
+
onActiveHitChanged: activeHitChanged.on,
|
|
337
|
+
onCleared: cleared.on
|
|
255
338
|
};
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
timer = null;
|
|
269
|
-
ctx.get(require_contract.SearchToken).rerun();
|
|
270
|
-
}, RERUN_DELAY_MS);
|
|
271
|
-
});
|
|
272
|
-
ctx.cleanup(() => {
|
|
273
|
-
if (timer !== null) clearTimeout(timer);
|
|
274
|
-
unsubscribe();
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
//#endregion
|
|
278
|
-
//#region src/reducer.ts
|
|
279
|
-
const initialSearchState = () => ({
|
|
280
|
-
query: null,
|
|
281
|
-
status: "idle",
|
|
282
|
-
hits: [],
|
|
283
|
-
hitsByPage: {},
|
|
284
|
-
activeIndex: -1,
|
|
285
|
-
progress: {
|
|
286
|
-
scanned: 0,
|
|
287
|
-
total: 0
|
|
288
|
-
},
|
|
289
|
-
error: null
|
|
290
|
-
});
|
|
291
|
-
const searchReducer = (state, action) => {
|
|
292
|
-
switch (action.type) {
|
|
293
|
-
case "START": return {
|
|
294
|
-
...initialSearchState(),
|
|
295
|
-
query: action.query,
|
|
296
|
-
status: "searching"
|
|
297
|
-
};
|
|
298
|
-
case "APPEND": {
|
|
299
|
-
if (action.hits.length === 0) return {
|
|
300
|
-
...state,
|
|
301
|
-
progress: {
|
|
302
|
-
scanned: action.scanned,
|
|
303
|
-
total: action.total
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
const hits = state.hits.concat(action.hits);
|
|
307
|
-
const hitsByPage = { ...state.hitsByPage };
|
|
308
|
-
for (let i = state.hits.length; i < hits.length; i++) {
|
|
309
|
-
const pon = hits[i].pon;
|
|
310
|
-
hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
...state,
|
|
314
|
-
hits,
|
|
315
|
-
hitsByPage,
|
|
316
|
-
activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,
|
|
317
|
-
progress: {
|
|
318
|
-
scanned: action.scanned,
|
|
319
|
-
total: action.total
|
|
320
|
-
}
|
|
321
|
-
};
|
|
339
|
+
return {
|
|
340
|
+
api,
|
|
341
|
+
connect() {
|
|
342
|
+
let cancelRerun = null;
|
|
343
|
+
ctx.listen(ctx.doc.events, () => {
|
|
344
|
+
if (state().status === "idle") return;
|
|
345
|
+
cancelRerun?.();
|
|
346
|
+
cancelRerun = ctx.clock.after(RERUN_DELAY_MS, () => {
|
|
347
|
+
cancelRerun = null;
|
|
348
|
+
api.refresh().catch(() => {});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
322
351
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
status: "complete"
|
|
326
|
-
};
|
|
327
|
-
case "ERROR": return {
|
|
328
|
-
...state,
|
|
329
|
-
status: "error",
|
|
330
|
-
error: action.message
|
|
331
|
-
};
|
|
332
|
-
case "SET_ACTIVE": return {
|
|
333
|
-
...state,
|
|
334
|
-
activeIndex: action.index
|
|
335
|
-
};
|
|
336
|
-
case "CLEAR": return initialSearchState();
|
|
337
|
-
default: return state;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
352
|
+
};
|
|
353
|
+
}
|
|
340
354
|
//#endregion
|
|
341
355
|
//#region src/search.plugin.ts
|
|
342
356
|
/**
|
|
343
|
-
* Document text search over
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* the hit's page; without it the host owns scrolling.
|
|
357
|
+
* Document text search over the engine's budgeted, resumable slices.
|
|
358
|
+
* Document-scoped; no pointer handling. The stage is optional: with one,
|
|
359
|
+
* scans start at the current page and navigation reveals hits. `config` is
|
|
360
|
+
* the settings the app registers, over {@link SEARCH_DEFAULTS}.
|
|
348
361
|
*/
|
|
349
362
|
const searchPlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
350
363
|
id: "search",
|
|
351
364
|
token: require_contract.SearchToken,
|
|
352
365
|
scope: "document",
|
|
353
366
|
optional: [_embedpdf_plugin_stage_contract.StageToken],
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
367
|
+
state: initialSearchState,
|
|
368
|
+
settings: {
|
|
369
|
+
defaults: require_contract.SEARCH_DEFAULTS,
|
|
370
|
+
registered: config
|
|
371
|
+
},
|
|
372
|
+
create: createSearchController
|
|
373
|
+
});
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/state.ts
|
|
376
|
+
/**
|
|
377
|
+
* The search page's State table as code: what `useSearchState()` returns, and
|
|
378
|
+
* the same fields in every other framework. The matches themselves are not
|
|
379
|
+
* here: `useSearchHits()` reads them, so a component that only shows the
|
|
380
|
+
* count doesn't re-render for every match that arrives.
|
|
381
|
+
*/
|
|
382
|
+
const searchState = (0, _embedpdf_core.defineState)(require_contract.SearchToken, {
|
|
383
|
+
read: (search) => ({
|
|
384
|
+
query: search.getQuery(),
|
|
385
|
+
status: search.getStatus(),
|
|
386
|
+
hitCount: search.getHitCount(),
|
|
387
|
+
activeHitIndex: search.getActiveHitIndex(),
|
|
388
|
+
activeHit: search.getActiveHit(),
|
|
389
|
+
progress: search.getProgress(),
|
|
390
|
+
error: search.getError()
|
|
391
|
+
}),
|
|
392
|
+
empty: {
|
|
393
|
+
query: null,
|
|
394
|
+
status: "idle",
|
|
395
|
+
hitCount: 0,
|
|
396
|
+
activeHitIndex: -1,
|
|
397
|
+
activeHit: null,
|
|
398
|
+
progress: NO_PROGRESS,
|
|
399
|
+
error: null
|
|
400
|
+
}
|
|
358
401
|
});
|
|
359
402
|
//#endregion
|
|
403
|
+
exports.SEARCH_DEFAULTS = require_contract.SEARCH_DEFAULTS;
|
|
360
404
|
exports.SearchToken = require_contract.SearchToken;
|
|
361
405
|
exports.searchPlugin = searchPlugin;
|
|
406
|
+
exports.searchState = searchState;
|
|
362
407
|
Object.defineProperty(exports, "validateSearchQuery", {
|
|
363
408
|
enumerable: true,
|
|
364
409
|
get: function() {
|