@embedpdf/plugin-search 3.0.0-next.2 → 3.0.0-next.4
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/index.cjs +18 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +19 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -30,12 +30,6 @@ const isPermissionDenied = (err) => err instanceof Error && err.name === "Permis
|
|
|
30
30
|
function createSearchCapability(ctx, config = {}) {
|
|
31
31
|
let generation = 0;
|
|
32
32
|
let inflight = null;
|
|
33
|
-
const toContent = (m, b) => (0, _embedpdf_core_geometry.applyRect)(m, {
|
|
34
|
-
x: b.left,
|
|
35
|
-
y: b.bottom,
|
|
36
|
-
width: b.right - b.left,
|
|
37
|
-
height: b.top - b.bottom
|
|
38
|
-
});
|
|
39
33
|
function hitsFromSlice(slice) {
|
|
40
34
|
const converters = /* @__PURE__ */ new Map();
|
|
41
35
|
const converterFor = (pon) => {
|
|
@@ -49,9 +43,15 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
49
43
|
rotation: layout.rotation,
|
|
50
44
|
userUnit: layout.userUnit
|
|
51
45
|
}, 1);
|
|
46
|
+
const toPoint = (pt) => (0, _embedpdf_core_geometry.applyPoint)(pdfToContent, pt);
|
|
52
47
|
conv = {
|
|
53
48
|
pageIndex: layout.index,
|
|
54
|
-
|
|
49
|
+
toQuad: (q) => ({
|
|
50
|
+
upperStart: toPoint(q.p1),
|
|
51
|
+
upperEnd: toPoint(q.p2),
|
|
52
|
+
lowerStart: toPoint(q.p3),
|
|
53
|
+
lowerEnd: toPoint(q.p4)
|
|
54
|
+
})
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
converters.set(pon, conv);
|
|
@@ -61,12 +61,21 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
61
61
|
for (const match of slice.matches) {
|
|
62
62
|
const conv = converterFor(match.pageObjectNumber);
|
|
63
63
|
if (!conv) continue;
|
|
64
|
+
const segments = match.segments.map((segment) => {
|
|
65
|
+
const quad = conv.toQuad(segment.quad);
|
|
66
|
+
return {
|
|
67
|
+
quad,
|
|
68
|
+
rect: (0, _embedpdf_core_geometry.textQuadBounds)(quad),
|
|
69
|
+
advance: segment.advance
|
|
70
|
+
};
|
|
71
|
+
});
|
|
64
72
|
hits.push({
|
|
65
73
|
pon: match.pageObjectNumber,
|
|
66
74
|
pageIndex: conv.pageIndex,
|
|
67
75
|
charStart: match.charStart,
|
|
68
76
|
charCount: match.charCount,
|
|
69
|
-
|
|
77
|
+
segments,
|
|
78
|
+
...segments.length ? { bounds: (0, _embedpdf_core_geometry.boundsOfRects)(segments.map((s) => s.rect)) ?? void 0 } : {},
|
|
70
79
|
...match.snippet ? { snippet: match.snippet } : {}
|
|
71
80
|
});
|
|
72
81
|
}
|
|
@@ -175,7 +184,7 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
175
184
|
...reveal
|
|
176
185
|
};
|
|
177
186
|
ctx.tryGet(_embedpdf_plugin_stage.StageToken)?.reveal(hit.pageIndex, {
|
|
178
|
-
rect:
|
|
187
|
+
rect: hit.bounds,
|
|
179
188
|
anchor: arrival.anchor,
|
|
180
189
|
behavior: arrival.behavior
|
|
181
190
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["EngineError","EngineErrorCode","StageToken","StageToken"],"sources":["../src/capability.ts","../src/types.ts","../src/effects.ts","../src/reducer.ts","../src/search.plugin.ts"],"sourcesContent":["import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport {\n applyRect,\n boundsOfRects,\n pageGeometry,\n type Rect,\n type RectIn,\n} from '@embedpdf/core-geometry';\nimport { StageToken } from '@embedpdf/plugin-stage';\nimport { EngineError, EngineErrorCode } from '@embedpdf/engine-core/runtime';\nimport type {\n PdfRect,\n SearchMatch,\n SearchMode,\n SearchQuery,\n SearchSlice,\n} from '@embedpdf/engine-core/runtime';\n\nimport type {\n SearchAction,\n SearchCapability,\n SearchHit,\n SearchPluginConfig,\n SearchRevealOptions,\n SearchState,\n} from './types';\n\nconst EMPTY: SearchHit[] = [];\n\n/** The browser find-bar feel: hit at the top-middle, no zoom change. */\nconst DEFAULT_REVEAL: SearchRevealOptions = { anchor: { y: 0.35 }, behavior: 'smooth' };\n\n/** Local engines reject a denied scope before the worker; cloud maps 403. */\nconst isPermissionDenied = (err: unknown): boolean =>\n (err instanceof Error && err.name === 'PermissionDenied') ||\n EngineError.is(err, EngineErrorCode.Forbidden);\n\n/**\n * The search capability: a client-driven cursor loop over the engine's\n * budgeted slices. One generation counter arbitrates: a new `search()` (or\n * `clear()`, or a document mutation via `rerun()`) supersedes the running\n * loop, which aborts its in-flight slice and stops dispatching.\n *\n * Rect conversion happens HERE, once per slice: the engine's PDF-space\n * line rects → content space through the same page geometry the selection\n * plugin uses, so search highlights and selection highlights are pixel-twins.\n *\n * Stage integration is optional and additive: with a Stage present the scan\n * starts at the current page (viewport-first — the hits the user is looking\n * at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the\n * hit's page. Without one, navigation still works; the host scrolls.\n */\nexport function createSearchCapability(\n ctx: PluginContext<SearchState, SearchAction>,\n config: SearchPluginConfig = {},\n): SearchCapability {\n let generation = 0;\n let inflight: { abort(reason?: unknown): void } | null = null;\n\n const toContent = (m: ReturnType<typeof pageGeometry>['pdfToContent'], b: PdfRect): Rect =>\n applyRect(\n m as never,\n {\n x: b.left,\n y: b.bottom,\n width: b.right - b.left,\n height: b.top - b.bottom,\n } as RectIn<'pdf'>,\n ) as Rect;\n\n interface PageConverter {\n pageIndex: number;\n toRect: (b: PdfRect) => Rect;\n }\n\n function hitsFromSlice(slice: SearchSlice): SearchHit[] {\n // zoom = 1: content space is scale-free; the layer applies the page\n // transform at render time (the selection idiom).\n const converters = new Map<PageObjectNumber, PageConverter | null>();\n const converterFor = (pon: PageObjectNumber): PageConverter | null => {\n let conv = converters.get(pon);\n if (conv !== undefined) return conv;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!layout) {\n conv = null; // page vanished mid-search (delete); drop its hits\n } else {\n const { pdfToContent } = pageGeometry(\n { crop: layout.boxes.crop, rotation: layout.rotation, userUnit: layout.userUnit },\n 1,\n );\n conv = { pageIndex: layout.index, toRect: (b) => toContent(pdfToContent, b) };\n }\n converters.set(pon, conv);\n return conv;\n };\n\n const hits: SearchHit[] = [];\n for (const match of slice.matches) {\n const conv = converterFor(match.pageObjectNumber);\n if (!conv) continue;\n hits.push({\n pon: match.pageObjectNumber,\n pageIndex: conv.pageIndex,\n charStart: match.charStart,\n charCount: match.charCount,\n rects: match.rects.map(conv.toRect),\n ...(match.snippet ? { snippet: match.snippet } : {}),\n });\n }\n return hits;\n }\n\n /** How a collect() consumer observes the loop — the session dispatches\n * into state, findAll accumulates locally. */\n interface CollectSink {\n /** Called before the first slice AND again on a stale-cursor restart —\n * reset any accumulation. */\n onStart: () => void;\n onSlice: (hits: SearchHit[], scanned: number, total: number) => void;\n /** Superseded / aborted — the loop stops without completing. */\n isCancelled: () => boolean;\n setInflight: (pending: { abort(reason?: unknown): void } | null) => void;\n }\n\n /**\n * THE MECHANISM — one cursor loop over the engine's budgeted slices,\n * shared by the session (`search`) and the session-free service\n * (`findAll`): permission fallback ('full' → 'rects' when snippets are\n * denied, unless the caller pinned a mode), restart-once on a stale\n * cursor, rect conversion per slice. Resolves true on exhaustion, false\n * when cancelled; throws on error.\n */\n async function collect(\n query: SearchQuery,\n opts: { startPage?: PageObjectNumber; mode?: SearchMode },\n sink: CollectSink,\n ): Promise<boolean> {\n const doc = ctx.doc;\n if (!doc) return false;\n\n sink.onStart();\n\n let mode: SearchMode = opts.mode ?? 'full';\n const modePinned = opts.mode !== undefined;\n let cursor: string | undefined;\n let restarted = false;\n for (;;) {\n let slice: SearchSlice;\n try {\n const request = {\n query,\n mode,\n ...(cursor !== undefined\n ? { cursor }\n : opts.startPage !== undefined\n ? { startPage: opts.startPage }\n : {}),\n };\n const pending = doc.search.query(request);\n sink.setInflight(pending);\n slice = await pending;\n } catch (err) {\n if (sink.isCancelled()) return false; // superseded — the abort was ours\n // Snippets denied (no doc.text.copy)? Degrade to rect-only matches\n // instead of failing the whole search — find still works.\n if (!modePinned && mode === 'full' && cursor === undefined && isPermissionDenied(err)) {\n mode = 'rects';\n continue;\n }\n // A stale cursor rejects with InvalidArg — the document changed\n // under the loop, or the engine's session was recycled. Position\n // is lost, the query isn't: restart once from scratch.\n if (cursor !== undefined && !restarted && EngineError.is(err, EngineErrorCode.InvalidArg)) {\n restarted = true;\n cursor = undefined;\n sink.onStart();\n continue;\n }\n throw err;\n }\n if (sink.isCancelled()) return false;\n sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);\n if (slice.nextCursor === null) {\n sink.setInflight(null);\n return true;\n }\n cursor = slice.nextCursor;\n }\n }\n\n /** THE SESSION — collect() with state as the sink, generation-guarded. */\n async function runSession(query: SearchQuery, startPage?: PageObjectNumber): Promise<void> {\n const gen = ++generation;\n inflight?.abort('superseded');\n inflight = null;\n\n if (!ctx.doc) {\n ctx.dispatch({ type: 'CLEAR' });\n return;\n }\n\n // Viewport-first: begin scanning where the user is looking.\n if (startPage === undefined) {\n const stage = ctx.tryGet(StageToken);\n if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;\n }\n\n try {\n const complete = await collect(\n query,\n { startPage },\n {\n onStart: () => ctx.dispatch({ type: 'START', query }),\n onSlice: (hits, scanned, total) => ctx.dispatch({ type: 'APPEND', hits, scanned, total }),\n isCancelled: () => gen !== generation,\n setInflight: (pending) => {\n if (gen === generation) inflight = pending;\n },\n },\n );\n if (complete && gen === generation) ctx.dispatch({ type: 'COMPLETE' });\n } catch (err) {\n if (gen !== generation) return;\n ctx.dispatch({ type: 'ERROR', message: err instanceof Error ? err.message : String(err) });\n }\n }\n\n function goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null {\n const { hits } = ctx.getState();\n if (hits.length === 0) return null;\n const wrapped = ((index % hits.length) + hits.length) % hits.length;\n ctx.dispatch({ type: 'SET_ACTIVE', index: wrapped });\n const hit = hits[wrapped];\n // Positioned reveal: the HIT (not just its page) arrives at the anchor —\n // per-call override > plugin config > find-bar default. Zoom never changes.\n const arrival = { ...DEFAULT_REVEAL, ...config.reveal, ...reveal };\n ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {\n rect: boundsOfRects(hit.rects) ?? undefined,\n anchor: arrival.anchor,\n behavior: arrival.behavior,\n });\n return hit;\n }\n\n const clear = () => {\n generation++;\n inflight?.abort('cleared');\n inflight = null;\n ctx.dispatch({ type: 'CLEAR' });\n };\n\n return {\n // An empty text is \"stop searching\", not a query — identical to clear().\n search: (query, exec) =>\n query.text.length === 0 ? clear() : void runSession(query, exec?.startPage),\n\n // Replay the stored query verbatim; the rescan starts viewport-first.\n rerun: () => {\n const { query, status } = ctx.getState();\n if (query && status !== 'idle') void runSession(query);\n },\n\n clear,\n\n next: () => goTo(ctx.getState().activeIndex + 1),\n prev: () => goTo(ctx.getState().activeIndex - 1),\n goTo,\n\n query: () => ctx.getState().query,\n\n findAll: async (query, opts = {}) => {\n if (!ctx.doc) return [];\n const all: SearchHit[] = [];\n const signal = opts.signal;\n // Independent lifecycle: its own abort handle, never the session's\n // generation counter — a findAll can neither supersede the user's\n // visible search nor be superseded by it.\n let pending: { abort(reason?: unknown): void } | null = null;\n const onAbort = () => pending?.abort(signal?.reason);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n await collect(\n query,\n { mode: opts.mode },\n {\n onStart: () => {\n all.length = 0;\n },\n onSlice: (hits) => all.push(...hits),\n isCancelled: () => signal?.aborted ?? false,\n setInflight: (p) => {\n pending = p;\n },\n },\n );\n if (signal?.aborted) throw signal.reason ?? new Error('findAll aborted');\n return all;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n },\n\n status: () => ctx.getState().status,\n hits: () => ctx.getState().hits,\n hitCount: () => ctx.getState().hits.length,\n activeIndex: () => ctx.getState().activeIndex,\n activeHit: () => {\n const { hits, activeIndex } = ctx.getState();\n return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;\n },\n hitsForPage: (pon) => {\n const { hits, hitsByPage } = ctx.getState();\n const indices = hitsByPage[pon];\n return indices?.length ? indices.map((i) => hits[i]) : EMPTY;\n },\n progress: () => ctx.getState().progress,\n errorMessage: () => ctx.getState().error,\n };\n}\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Rect } from '@embedpdf/core-geometry';\nimport type { RevealAnchor, ScrollBehaviorKind } from '@embedpdf/plugin-stage';\nimport type { SearchMode, SearchQuery, SearchSnippet } from '@embedpdf/engine-core/runtime';\n\n/**\n * One match, viewer-shaped: the engine's PDF-space line rects converted to\n * CONTENT space (y-down — the same space selection rects live in), plus the\n * page's display index so navigation can `reveal()` without a lookup.\n * `charStart`/`charCount` keep the engine's text-page offsets, so a hit can\n * later seed a selection or a highlight annotation without re-searching.\n */\nexport interface SearchHit {\n pon: PageObjectNumber;\n pageIndex: number;\n charStart: number;\n charCount: number;\n rects: Rect[];\n snippet?: SearchSnippet;\n}\n\n/**\n * `searching` means the slice loop is still walking pages — results are\n * usable the moment they appear (matches stream in viewport-first order).\n */\nexport type SearchStatus = 'idle' | 'searching' | 'complete' | 'error';\n\n/**\n * Execution options for the session `search()` — HOW the scan runs, never\n * part of what the query MEANS (which is why they're not on `SearchQuery`\n * and are never stored or restored into a search box).\n */\nexport interface SearchExecOptions {\n /** Scan origin override. Defaults to the Stage's current page (viewport-first). */\n startPage?: PageObjectNumber;\n}\n\n/** Options for the session-free `findAll()` service. */\nexport interface SearchFindAllOptions {\n /** Abort the scan; the returned promise rejects with `signal.reason`. */\n signal?: AbortSignal;\n /**\n * Pin the slice mode. Default: `'full'` with automatic `'rects'`\n * fallback when snippets are denied — pass `'rects'` when you only need\n * geometry (e.g. redact-by-term) to skip snippet extraction entirely.\n */\n mode?: SearchMode;\n}\n\nexport interface SearchState {\n /**\n * The query in flight/completed, null when idle/cleared — THE stored\n * form of the search intent, the same flat `SearchQuery` the engine\n * matches on, the wire caches by, and a search box renders. Survives\n * any search UI unmounting (results are document-scoped; so is this).\n */\n query: SearchQuery | null;\n status: SearchStatus;\n hits: SearchHit[];\n /** pon → indices into `hits` — the per-page layer's read model. */\n hitsByPage: Record<number, number[]>;\n /** Index into `hits`, -1 = none. */\n activeIndex: number;\n progress: { scanned: number; total: number };\n error: string | null;\n}\n\nexport type SearchAction =\n | { type: 'START'; query: SearchQuery }\n | { type: 'APPEND'; hits: SearchHit[]; scanned: number; total: number }\n | { type: 'COMPLETE' }\n | { type: 'ERROR'; message: string }\n | { type: 'SET_ACTIVE'; index: number }\n | { type: 'CLEAR' };\n\n/**\n * How hit navigation arrives (forwarded to the Stage's positioned reveal).\n * The default is the browser find-bar feel: the hit lands at the top-middle\n * of the viewport (`anchor: { y: 0.35 }`), horizontal only moves when the\n * hit is off-screen, zoom never changes, smooth tween.\n */\nexport interface SearchRevealOptions {\n anchor?: RevealAnchor;\n behavior?: ScrollBehaviorKind;\n}\n\nexport interface SearchPluginConfig {\n /** Arrival defaults for `next()`/`prev()`/`goTo()`. */\n reveal?: SearchRevealOptions;\n}\n\n/**\n * The search plugin is a find SERVICE plus one user-visible search SESSION\n * per document. `findAll` is the service; `search` is the session; the\n * sidebar, the highlights layer, and next/prev render the session.\n */\nexport interface SearchCapability {\n // ── the session: THE user-visible find, one per document ────────────────\n /**\n * Start a new search (supersedes and aborts any running one). Results\n * stream into state slice by slice; the first hit becomes active but the\n * camera does NOT move until `next()`/`prev()`/`goTo()`. An empty\n * `query.text` is \"stop searching\" — identical to `clear()`.\n */\n search(query: SearchQuery, exec?: SearchExecOptions): void;\n /** Re-run the current query from scratch (used after document mutations). */\n rerun(): void;\n clear(): void;\n\n /** Advance to the next/previous hit (wraps) and reveal it (see {@link SearchRevealOptions}). */\n next(): SearchHit | null;\n prev(): SearchHit | null;\n /** Jump to a specific hit index and reveal it; `reveal` overrides the plugin defaults. */\n goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null;\n\n /** The session's query — what a search box renders. Null when idle. */\n query(): SearchQuery | null;\n status(): SearchStatus;\n hits(): SearchHit[];\n hitCount(): number;\n activeIndex(): number;\n activeHit(): SearchHit | null;\n /** Hits on one page — the layer's input. Element identity is stable. */\n hitsForPage(pon: PageObjectNumber): SearchHit[];\n progress(): { scanned: number; total: number };\n errorMessage(): string | null;\n\n // ── the mechanism: session-free find service ─────────────────────────────\n /**\n * Run a query to completion and return every hit. Touches NO state — no\n * sidebar, no highlights, no camera, no activeIndex; the user's visible\n * search is never superseded. The primitive other features build on\n * (redact-by-term, link detection, occurrence badges). Scans in natural\n * page order; concurrent calls are independent.\n */\n findAll(query: SearchQuery, opts?: SearchFindAllOptions): Promise<SearchHit[]>;\n}\n\nexport const SearchToken = createCapabilityToken<SearchCapability>('search');\n","/**\n * Keeps results honest across document mutations. Every confirmed\n * mutation (own or remote) bumps the engine's mutation sequence, which\n * invalidates search cursors AND can change what's findable (redaction,\n * page delete, import). The correct reaction is always the same: re-run\n * the current query from scratch. Reruns are coalesced — a burst of\n * events (multi-part mutation, SSE catch-up) triggers one rescan.\n */\nimport type { EffectContext } from '@embedpdf/core';\n\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchState } from './types';\n\nconst RERUN_DELAY_MS = 250;\n\nexport function registerSearchEffects(ctx: EffectContext<SearchState, SearchAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n const unsubscribe = doc.events.subscribe(() => {\n if (ctx.getState().status === 'idle') return;\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n ctx.get(SearchToken).rerun();\n }, RERUN_DELAY_MS);\n });\n\n ctx.cleanup(() => {\n if (timer !== null) clearTimeout(timer);\n unsubscribe();\n });\n}\n","import type { SearchAction, SearchState } from './types';\n\nexport const initialSearchState = (): SearchState => ({\n query: null,\n status: 'idle',\n hits: [],\n hitsByPage: {},\n activeIndex: -1,\n progress: { scanned: 0, total: 0 },\n error: null,\n});\n\nexport const searchReducer = (state: SearchState, action: SearchAction): SearchState => {\n switch (action.type) {\n case 'START':\n return { ...initialSearchState(), query: action.query, status: 'searching' };\n case 'APPEND': {\n if (action.hits.length === 0) {\n return { ...state, progress: { scanned: action.scanned, total: action.total } };\n }\n const hits = state.hits.concat(action.hits);\n const hitsByPage = { ...state.hitsByPage };\n for (let i = state.hits.length; i < hits.length; i++) {\n const pon = hits[i].pon;\n hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);\n }\n return {\n ...state,\n hits,\n hitsByPage,\n // The first hit becomes active (so \"1/N\" reads right) — navigation,\n // not the stream, moves the camera.\n activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,\n progress: { scanned: action.scanned, total: action.total },\n };\n }\n case 'COMPLETE':\n return { ...state, status: 'complete' };\n case 'ERROR':\n return { ...state, status: 'error', error: action.message };\n case 'SET_ACTIVE':\n return { ...state, activeIndex: action.index };\n case 'CLEAR':\n return initialSearchState();\n default:\n return state;\n }\n};\n","import { definePlugin } from '@embedpdf/core';\nimport { StageToken } from '@embedpdf/plugin-stage';\n\nimport { createSearchCapability } from './capability';\nimport { registerSearchEffects } from './effects';\nimport { initialSearchState, searchReducer } from './reducer';\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchCapability, SearchPluginConfig, SearchState } from './types';\n\n/**\n * Document text search over `doc.search` — the engine's budgeted,\n * cursor-resumable slices. Document-scoped; no pointer handling, so it\n * needs no interaction hub. The Stage is optional: when present, scans\n * start at the current page (viewport-first) and hit navigation reveals\n * the hit's page; without it the host owns scrolling.\n */\nexport const searchPlugin = (config?: SearchPluginConfig) =>\n definePlugin<SearchState, SearchAction, SearchCapability>({\n id: 'search',\n token: SearchToken,\n scope: 'document',\n optional: [StageToken],\n initialState: initialSearchState,\n reduce: searchReducer,\n capability: (ctx) => createSearchCapability(ctx, config),\n effects: registerSearchEffects,\n });\n"],"mappings":";;;;;;AA2BA,MAAM,QAAqB,CAAC;;AAG5B,MAAM,iBAAsC;CAAE,QAAQ,EAAE,GAAG,IAAK;CAAG,UAAU;AAAS;;AAGtF,MAAM,sBAAsB,QACzB,eAAe,SAAS,IAAI,SAAS,sBACtCA,8BAAAA,YAAY,GAAG,KAAKC,8BAAAA,gBAAgB,SAAS;;;;;;;;;;;;;;;;AAiB/C,SAAgB,uBACd,KACA,SAA6B,CAAC,GACZ;CAClB,IAAI,aAAa;CACjB,IAAI,WAAqD;CAEzD,MAAM,aAAa,GAAoD,OAAA,GAAA,wBAAA,UAAA,CAEnE,GACA;EACE,GAAG,EAAE;EACL,GAAG,EAAE;EACL,OAAO,EAAE,QAAQ,EAAE;EACnB,QAAQ,EAAE,MAAM,EAAE;CACpB,CACF;CAOF,SAAS,cAAc,OAAiC;EAGtD,MAAM,6BAAa,IAAI,IAA4C;EACnE,MAAM,gBAAgB,QAAgD;GACpE,IAAI,OAAO,WAAW,IAAI,GAAG;GAC7B,IAAI,SAAS,KAAA,GAAW,OAAO;GAC/B,MAAM,SAAS,IAAI,SAAS,CAAC,EAAE,MAAM,MAAM,MAAM,EAAE,qBAAqB,GAAG;GAC3E,IAAI,CAAC,QACH,OAAO;QACF;IACL,MAAM,EAAE,kBAAA,GAAA,wBAAA,aAAA,CACN;KAAE,MAAM,OAAO,MAAM;KAAM,UAAU,OAAO;KAAU,UAAU,OAAO;IAAS,GAChF,CACF;IACA,OAAO;KAAE,WAAW,OAAO;KAAO,SAAS,MAAM,UAAU,cAAc,CAAC;IAAE;GAC9E;GACA,WAAW,IAAI,KAAK,IAAI;GACxB,OAAO;EACT;EAEA,MAAM,OAAoB,CAAC;EAC3B,KAAK,MAAM,SAAS,MAAM,SAAS;GACjC,MAAM,OAAO,aAAa,MAAM,gBAAgB;GAChD,IAAI,CAAC,MAAM;GACX,KAAK,KAAK;IACR,KAAK,MAAM;IACX,WAAW,KAAK;IAChB,WAAW,MAAM;IACjB,WAAW,MAAM;IACjB,OAAO,MAAM,MAAM,IAAI,KAAK,MAAM;IAClC,GAAI,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;GACpD,CAAC;EACH;EACA,OAAO;CACT;;;;;;;;;CAsBA,eAAe,QACb,OACA,MACA,MACkB;EAClB,MAAM,MAAM,IAAI;EAChB,IAAI,CAAC,KAAK,OAAO;EAEjB,KAAK,QAAQ;EAEb,IAAI,OAAmB,KAAK,QAAQ;EACpC,MAAM,aAAa,KAAK,SAAS,KAAA;EACjC,IAAI;EACJ,IAAI,YAAY;EAChB,SAAS;GACP,IAAI;GACJ,IAAI;IACF,MAAM,UAAU;KACd;KACA;KACA,GAAI,WAAW,KAAA,IACX,EAAE,OAAO,IACT,KAAK,cAAc,KAAA,IACjB,EAAE,WAAW,KAAK,UAAU,IAC5B,CAAC;IACT;IACA,MAAM,UAAU,IAAI,OAAO,MAAM,OAAO;IACxC,KAAK,YAAY,OAAO;IACxB,QAAQ,MAAM;GAChB,SAAS,KAAK;IACZ,IAAI,KAAK,YAAY,GAAG,OAAO;IAG/B,IAAI,CAAC,cAAc,SAAS,UAAU,WAAW,KAAA,KAAa,mBAAmB,GAAG,GAAG;KACrF,OAAO;KACP;IACF;IAIA,IAAI,WAAW,KAAA,KAAa,CAAC,aAAaD,8BAAAA,YAAY,GAAG,KAAKC,8BAAAA,gBAAgB,UAAU,GAAG;KACzF,YAAY;KACZ,SAAS,KAAA;KACT,KAAK,QAAQ;KACb;IACF;IACA,MAAM;GACR;GACA,IAAI,KAAK,YAAY,GAAG,OAAO;GAC/B,KAAK,QAAQ,cAAc,KAAK,GAAG,MAAM,cAAc,MAAM,UAAU;GACvE,IAAI,MAAM,eAAe,MAAM;IAC7B,KAAK,YAAY,IAAI;IACrB,OAAO;GACT;GACA,SAAS,MAAM;EACjB;CACF;;CAGA,eAAe,WAAW,OAAoB,WAA6C;EACzF,MAAM,MAAM,EAAE;EACd,UAAU,MAAM,YAAY;EAC5B,WAAW;EAEX,IAAI,CAAC,IAAI,KAAK;GACZ,IAAI,SAAS,EAAE,MAAM,QAAQ,CAAC;GAC9B;EACF;EAGA,IAAI,cAAc,KAAA,GAAW;GAC3B,MAAM,QAAQ,IAAI,OAAOC,uBAAAA,UAAU;GACnC,IAAI,OAAO,YAAY,IAAI,SAAS,CAAC,EAAE,MAAM,MAAM,YAAY,EAAE,EAAE;EACrE;EAEA,IAAI;GAaF,IAAI,MAZmB,QACrB,OACA,EAAE,UAAU,GACZ;IACE,eAAe,IAAI,SAAS;KAAE,MAAM;KAAS;IAAM,CAAC;IACpD,UAAU,MAAM,SAAS,UAAU,IAAI,SAAS;KAAE,MAAM;KAAU;KAAM;KAAS;IAAM,CAAC;IACxF,mBAAmB,QAAQ;IAC3B,cAAc,YAAY;KACxB,IAAI,QAAQ,YAAY,WAAW;IACrC;GACF,CACF,KACgB,QAAQ,YAAY,IAAI,SAAS,EAAE,MAAM,WAAW,CAAC;EACvE,SAAS,KAAK;GACZ,IAAI,QAAQ,YAAY;GACxB,IAAI,SAAS;IAAE,MAAM;IAAS,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAAE,CAAC;EAC3F;CACF;CAEA,SAAS,KAAK,OAAe,QAAgD;EAC3E,MAAM,EAAE,SAAS,IAAI,SAAS;EAC9B,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,MAAM,WAAY,QAAQ,KAAK,SAAU,KAAK,UAAU,KAAK;EAC7D,IAAI,SAAS;GAAE,MAAM;GAAc,OAAO;EAAQ,CAAC;EACnD,MAAM,MAAM,KAAK;EAGjB,MAAM,UAAU;GAAE,GAAG;GAAgB,GAAG,OAAO;GAAQ,GAAG;EAAO;EACjE,IAAI,OAAOA,uBAAAA,UAAU,CAAC,EAAE,OAAO,IAAI,WAAW;GAC5C,OAAA,GAAA,wBAAA,cAAA,CAAoB,IAAI,KAAK,KAAK,KAAA;GAClC,QAAQ,QAAQ;GAChB,UAAU,QAAQ;EACpB,CAAC;EACD,OAAO;CACT;CAEA,MAAM,cAAc;EAClB;EACA,UAAU,MAAM,SAAS;EACzB,WAAW;EACX,IAAI,SAAS,EAAE,MAAM,QAAQ,CAAC;CAChC;CAEA,OAAO;EAEL,SAAS,OAAO,SACd,MAAM,KAAK,WAAW,IAAI,MAAM,IAAI,KAAK,WAAW,OAAO,MAAM,SAAS;EAG5E,aAAa;GACX,MAAM,EAAE,OAAO,WAAW,IAAI,SAAS;GACvC,IAAI,SAAS,WAAW,QAAQ,WAAgB,KAAK;EACvD;EAEA;EAEA,YAAY,KAAK,IAAI,SAAS,CAAC,CAAC,cAAc,CAAC;EAC/C,YAAY,KAAK,IAAI,SAAS,CAAC,CAAC,cAAc,CAAC;EAC/C;EAEA,aAAa,IAAI,SAAS,CAAC,CAAC;EAE5B,SAAS,OAAO,OAAO,OAAO,CAAC,MAAM;GACnC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;GACtB,MAAM,MAAmB,CAAC;GAC1B,MAAM,SAAS,KAAK;GAIpB,IAAI,UAAoD;GACxD,MAAM,gBAAgB,SAAS,MAAM,QAAQ,MAAM;GACnD,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GACzD,IAAI;IACF,MAAM,QACJ,OACA,EAAE,MAAM,KAAK,KAAK,GAClB;KACE,eAAe;MACb,IAAI,SAAS;KACf;KACA,UAAU,SAAS,IAAI,KAAK,GAAG,IAAI;KACnC,mBAAmB,QAAQ,WAAW;KACtC,cAAc,MAAM;MAClB,UAAU;KACZ;IACF,CACF;IACA,IAAI,QAAQ,SAAS,MAAM,OAAO,0BAAU,IAAI,MAAM,iBAAiB;IACvE,OAAO;GACT,UAAU;IACR,QAAQ,oBAAoB,SAAS,OAAO;GAC9C;EACF;EAEA,cAAc,IAAI,SAAS,CAAC,CAAC;EAC7B,YAAY,IAAI,SAAS,CAAC,CAAC;EAC3B,gBAAgB,IAAI,SAAS,CAAC,CAAC,KAAK;EACpC,mBAAmB,IAAI,SAAS,CAAC,CAAC;EAClC,iBAAiB;GACf,MAAM,EAAE,MAAM,gBAAgB,IAAI,SAAS;GAC3C,OAAO,eAAe,KAAK,cAAc,KAAK,SAAS,KAAK,eAAe;EAC7E;EACA,cAAc,QAAQ;GACpB,MAAM,EAAE,MAAM,eAAe,IAAI,SAAS;GAC1C,MAAM,UAAU,WAAW;GAC3B,OAAO,SAAS,SAAS,QAAQ,KAAK,MAAM,KAAK,EAAE,IAAI;EACzD;EACA,gBAAgB,IAAI,SAAS,CAAC,CAAC;EAC/B,oBAAoB,IAAI,SAAS,CAAC,CAAC;CACrC;AACF;;;ACpLA,MAAa,eAAA,GAAA,eAAA,sBAAA,CAAsD,QAAQ;;;AC7H3E,MAAM,iBAAiB;AAEvB,SAAgB,sBAAsB,KAAqD;CACzF,MAAM,MAAM,IAAI;CAChB,IAAI,CAAC,KAAK;CAEV,IAAI,QAA8C;CAClD,MAAM,cAAc,IAAI,OAAO,gBAAgB;EAC7C,IAAI,IAAI,SAAS,CAAC,CAAC,WAAW,QAAQ;EACtC,IAAI,UAAU,MAAM,aAAa,KAAK;EACtC,QAAQ,iBAAiB;GACvB,QAAQ;GACR,IAAI,IAAI,WAAW,CAAC,CAAC,MAAM;EAC7B,GAAG,cAAc;CACnB,CAAC;CAED,IAAI,cAAc;EAChB,IAAI,UAAU,MAAM,aAAa,KAAK;EACtC,YAAY;CACd,CAAC;AACH;;;AC/BA,MAAa,4BAAyC;CACpD,OAAO;CACP,QAAQ;CACR,MAAM,CAAC;CACP,YAAY,CAAC;CACb,aAAa;CACb,UAAU;EAAE,SAAS;EAAG,OAAO;CAAE;CACjC,OAAO;AACT;AAEA,MAAa,iBAAiB,OAAoB,WAAsC;CACtF,QAAQ,OAAO,MAAf;EACE,KAAK,SACH,OAAO;GAAE,GAAG,mBAAmB;GAAG,OAAO,OAAO;GAAO,QAAQ;EAAY;EAC7E,KAAK,UAAU;GACb,IAAI,OAAO,KAAK,WAAW,GACzB,OAAO;IAAE,GAAG;IAAO,UAAU;KAAE,SAAS,OAAO;KAAS,OAAO,OAAO;IAAM;GAAE;GAEhF,MAAM,OAAO,MAAM,KAAK,OAAO,OAAO,IAAI;GAC1C,MAAM,aAAa,EAAE,GAAG,MAAM,WAAW;GACzC,KAAK,IAAI,IAAI,MAAM,KAAK,QAAQ,IAAI,KAAK,QAAQ,KAAK;IACpD,MAAM,MAAM,KAAK,EAAE,CAAC;IACpB,WAAW,QAAQ,WAAW,QAAQ,CAAC,EAAA,CAAG,OAAO,CAAC;GACpD;GACA,OAAO;IACL,GAAG;IACH;IACA;IAGA,aAAa,MAAM,gBAAgB,KAAK,IAAI,MAAM;IAClD,UAAU;KAAE,SAAS,OAAO;KAAS,OAAO,OAAO;IAAM;GAC3D;EACF;EACA,KAAK,YACH,OAAO;GAAE,GAAG;GAAO,QAAQ;EAAW;EACxC,KAAK,SACH,OAAO;GAAE,GAAG;GAAO,QAAQ;GAAS,OAAO,OAAO;EAAQ;EAC5D,KAAK,cACH,OAAO;GAAE,GAAG;GAAO,aAAa,OAAO;EAAM;EAC/C,KAAK,SACH,OAAO,mBAAmB;EAC5B,SACE,OAAO;CACX;AACF;;;;;;;;;;AC/BA,MAAa,gBAAgB,YAAA,GAAA,eAAA,aAAA,CAC+B;CACxD,IAAI;CACJ,OAAO;CACP,OAAO;CACP,UAAU,CAACC,uBAAAA,UAAU;CACrB,cAAc;CACd,QAAQ;CACR,aAAa,QAAQ,uBAAuB,KAAK,MAAM;CACvD,SAAS;AACX,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["EngineError","EngineErrorCode","StageToken","StageToken"],"sources":["../src/capability.ts","../src/types.ts","../src/effects.ts","../src/reducer.ts","../src/search.plugin.ts"],"sourcesContent":["import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport { type PointIn, type TextQuad, applyPoint, boundsOfRects, pageGeometry, textQuadBounds } from '@embedpdf/core-geometry';\nimport { StageToken } from '@embedpdf/plugin-stage';\nimport { EngineError, EngineErrorCode } from '@embedpdf/engine-core/runtime';\nimport type {\n PdfQuad,\n PdfRect,\n SearchMatch,\n SearchMode,\n SearchQuery,\n SearchSlice,\n} from '@embedpdf/engine-core/runtime';\n\nimport type {\n SearchAction,\n SearchCapability,\n SearchHit,\n SearchPluginConfig,\n SearchRevealOptions,\n SearchState,\n} from './types';\n\nconst EMPTY: SearchHit[] = [];\n\n/** The browser find-bar feel: hit at the top-middle, no zoom change. */\nconst DEFAULT_REVEAL: SearchRevealOptions = { anchor: { y: 0.35 }, behavior: 'smooth' };\n\n/** Local engines reject a denied scope before the worker; cloud maps 403. */\nconst isPermissionDenied = (err: unknown): boolean =>\n (err instanceof Error && err.name === 'PermissionDenied') ||\n EngineError.is(err, EngineErrorCode.Forbidden);\n\n/**\n * The search capability: a client-driven cursor loop over the engine's\n * budgeted slices. One generation counter arbitrates: a new `search()` (or\n * `clear()`, or a document mutation via `rerun()`) supersedes the running\n * loop, which aborts its in-flight slice and stops dispatching.\n *\n * Rect conversion happens HERE, once per slice: the engine's PDF-space\n * line rects → content space through the same page geometry the selection\n * plugin uses, so search highlights and selection highlights are pixel-twins.\n *\n * Stage integration is optional and additive: with a Stage present the scan\n * starts at the current page (viewport-first — the hits the user is looking\n * at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the\n * hit's page. Without one, navigation still works; the host scrolls.\n */\nexport function createSearchCapability(\n ctx: PluginContext<SearchState, SearchAction>,\n config: SearchPluginConfig = {},\n): SearchCapability {\n let generation = 0;\n let inflight: { abort(reason?: unknown): void } | null = null;\n\n interface PageConverter {\n pageIndex: number;\n toQuad: (q: PdfQuad) => TextQuad;\n }\n\n function hitsFromSlice(slice: SearchSlice): SearchHit[] {\n // zoom = 1: content space is scale-free; the layer applies the page\n // transform at render time (the selection idiom).\n const converters = new Map<PageObjectNumber, PageConverter | null>();\n const converterFor = (pon: PageObjectNumber): PageConverter | null => {\n let conv = converters.get(pon);\n if (conv !== undefined) return conv;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!layout) {\n conv = null; // page vanished mid-search (delete); drop its hits\n } else {\n const { pdfToContent } = pageGeometry(\n { crop: layout.boxes.crop, rotation: layout.rotation, userUnit: layout.userUnit },\n 1,\n );\n // Engine quads carry frame-geometric slot semantics (p1..p4 =\n // upper-start, upper-end, lower-start, lower-end) — a trusted\n // producer, so the y-flip maps corners straight onto their names.\n const toPoint = (pt: { x: number; y: number }) =>\n applyPoint(pdfToContent as never, pt as PointIn<'pdf'>) as { x: number; y: number };\n conv = {\n pageIndex: layout.index,\n toQuad: (q) => ({\n upperStart: toPoint(q.p1),\n upperEnd: toPoint(q.p2),\n lowerStart: toPoint(q.p3),\n lowerEnd: toPoint(q.p4),\n }),\n };\n }\n converters.set(pon, conv);\n return conv;\n };\n\n const hits: SearchHit[] = [];\n for (const match of slice.matches) {\n const conv = converterFor(match.pageObjectNumber);\n if (!conv) continue;\n const segments = match.segments.map((segment) => {\n const quad = conv.toQuad(segment.quad);\n // `rect` is recomputed from the MAPPED quad so the pair can't drift.\n return { quad, rect: textQuadBounds(quad), advance: segment.advance };\n });\n hits.push({\n pon: match.pageObjectNumber,\n pageIndex: conv.pageIndex,\n charStart: match.charStart,\n charCount: match.charCount,\n segments,\n ...(segments.length\n ? { bounds: boundsOfRects(segments.map((s) => s.rect)) ?? undefined }\n : {}),\n ...(match.snippet ? { snippet: match.snippet } : {}),\n });\n }\n return hits;\n }\n\n /** How a collect() consumer observes the loop — the session dispatches\n * into state, findAll accumulates locally. */\n interface CollectSink {\n /** Called before the first slice AND again on a stale-cursor restart —\n * reset any accumulation. */\n onStart: () => void;\n onSlice: (hits: SearchHit[], scanned: number, total: number) => void;\n /** Superseded / aborted — the loop stops without completing. */\n isCancelled: () => boolean;\n setInflight: (pending: { abort(reason?: unknown): void } | null) => void;\n }\n\n /**\n * THE MECHANISM — one cursor loop over the engine's budgeted slices,\n * shared by the session (`search`) and the session-free service\n * (`findAll`): permission fallback ('full' → 'rects' when snippets are\n * denied, unless the caller pinned a mode), restart-once on a stale\n * cursor, rect conversion per slice. Resolves true on exhaustion, false\n * when cancelled; throws on error.\n */\n async function collect(\n query: SearchQuery,\n opts: { startPage?: PageObjectNumber; mode?: SearchMode },\n sink: CollectSink,\n ): Promise<boolean> {\n const doc = ctx.doc;\n if (!doc) return false;\n\n sink.onStart();\n\n let mode: SearchMode = opts.mode ?? 'full';\n const modePinned = opts.mode !== undefined;\n let cursor: string | undefined;\n let restarted = false;\n for (;;) {\n let slice: SearchSlice;\n try {\n const request = {\n query,\n mode,\n ...(cursor !== undefined\n ? { cursor }\n : opts.startPage !== undefined\n ? { startPage: opts.startPage }\n : {}),\n };\n const pending = doc.search.query(request);\n sink.setInflight(pending);\n slice = await pending;\n } catch (err) {\n if (sink.isCancelled()) return false; // superseded — the abort was ours\n // Snippets denied (no doc.text.copy)? Degrade to rect-only matches\n // instead of failing the whole search — find still works.\n if (!modePinned && mode === 'full' && cursor === undefined && isPermissionDenied(err)) {\n mode = 'rects';\n continue;\n }\n // A stale cursor rejects with InvalidArg — the document changed\n // under the loop, or the engine's session was recycled. Position\n // is lost, the query isn't: restart once from scratch.\n if (cursor !== undefined && !restarted && EngineError.is(err, EngineErrorCode.InvalidArg)) {\n restarted = true;\n cursor = undefined;\n sink.onStart();\n continue;\n }\n throw err;\n }\n if (sink.isCancelled()) return false;\n sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);\n if (slice.nextCursor === null) {\n sink.setInflight(null);\n return true;\n }\n cursor = slice.nextCursor;\n }\n }\n\n /** THE SESSION — collect() with state as the sink, generation-guarded. */\n async function runSession(query: SearchQuery, startPage?: PageObjectNumber): Promise<void> {\n const gen = ++generation;\n inflight?.abort('superseded');\n inflight = null;\n\n if (!ctx.doc) {\n ctx.dispatch({ type: 'CLEAR' });\n return;\n }\n\n // Viewport-first: begin scanning where the user is looking.\n if (startPage === undefined) {\n const stage = ctx.tryGet(StageToken);\n if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;\n }\n\n try {\n const complete = await collect(\n query,\n { startPage },\n {\n onStart: () => ctx.dispatch({ type: 'START', query }),\n onSlice: (hits, scanned, total) => ctx.dispatch({ type: 'APPEND', hits, scanned, total }),\n isCancelled: () => gen !== generation,\n setInflight: (pending) => {\n if (gen === generation) inflight = pending;\n },\n },\n );\n if (complete && gen === generation) ctx.dispatch({ type: 'COMPLETE' });\n } catch (err) {\n if (gen !== generation) return;\n ctx.dispatch({ type: 'ERROR', message: err instanceof Error ? err.message : String(err) });\n }\n }\n\n function goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null {\n const { hits } = ctx.getState();\n if (hits.length === 0) return null;\n const wrapped = ((index % hits.length) + hits.length) % hits.length;\n ctx.dispatch({ type: 'SET_ACTIVE', index: wrapped });\n const hit = hits[wrapped];\n // Positioned reveal: the HIT (not just its page) arrives at the anchor —\n // per-call override > plugin config > find-bar default. Zoom never changes.\n const arrival = { ...DEFAULT_REVEAL, ...config.reveal, ...reveal };\n ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {\n rect: hit.bounds,\n anchor: arrival.anchor,\n behavior: arrival.behavior,\n });\n return hit;\n }\n\n const clear = () => {\n generation++;\n inflight?.abort('cleared');\n inflight = null;\n ctx.dispatch({ type: 'CLEAR' });\n };\n\n return {\n // An empty text is \"stop searching\", not a query — identical to clear().\n search: (query, exec) =>\n query.text.length === 0 ? clear() : void runSession(query, exec?.startPage),\n\n // Replay the stored query verbatim; the rescan starts viewport-first.\n rerun: () => {\n const { query, status } = ctx.getState();\n if (query && status !== 'idle') void runSession(query);\n },\n\n clear,\n\n next: () => goTo(ctx.getState().activeIndex + 1),\n prev: () => goTo(ctx.getState().activeIndex - 1),\n goTo,\n\n query: () => ctx.getState().query,\n\n findAll: async (query, opts = {}) => {\n if (!ctx.doc) return [];\n const all: SearchHit[] = [];\n const signal = opts.signal;\n // Independent lifecycle: its own abort handle, never the session's\n // generation counter — a findAll can neither supersede the user's\n // visible search nor be superseded by it.\n let pending: { abort(reason?: unknown): void } | null = null;\n const onAbort = () => pending?.abort(signal?.reason);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n await collect(\n query,\n { mode: opts.mode },\n {\n onStart: () => {\n all.length = 0;\n },\n onSlice: (hits) => all.push(...hits),\n isCancelled: () => signal?.aborted ?? false,\n setInflight: (p) => {\n pending = p;\n },\n },\n );\n if (signal?.aborted) throw signal.reason ?? new Error('findAll aborted');\n return all;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n },\n\n status: () => ctx.getState().status,\n hits: () => ctx.getState().hits,\n hitCount: () => ctx.getState().hits.length,\n activeIndex: () => ctx.getState().activeIndex,\n activeHit: () => {\n const { hits, activeIndex } = ctx.getState();\n return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;\n },\n hitsForPage: (pon) => {\n const { hits, hitsByPage } = ctx.getState();\n const indices = hitsByPage[pon];\n return indices?.length ? indices.map((i) => hits[i]) : EMPTY;\n },\n progress: () => ctx.getState().progress,\n errorMessage: () => ctx.getState().error,\n };\n}\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Rect, TextQuad } from '@embedpdf/core-geometry';\nimport type { RevealAnchor, ScrollBehaviorKind } from '@embedpdf/plugin-stage';\nimport type { SearchMode, SearchQuery, SearchSnippet } from '@embedpdf/engine-core/runtime';\n\n/**\n * One match, viewer-shaped: the engine's PDF-space line rects converted to\n * CONTENT space (y-down — the same space selection rects live in), plus the\n * page's display index so navigation can `reveal()` without a lookup.\n * `charStart`/`charCount` keep the engine's text-page offsets, so a hit can\n * later seed a selection or a highlight annotation without re-searching.\n */\n/**\n * One merged visual line of a match, in content space — the structural twin\n * of selection's `SelectionSegment` and the engine's `PdfTextSegment`.\n * `quad` is the geometric authority (corner-named, frame-geometric); `rect`\n * its AABB; `advance` the reading direction along the baseline.\n */\nexport interface TextSegment {\n quad: TextQuad;\n rect: Rect;\n advance: 1 | -1;\n}\n\nexport interface SearchHit {\n pon: PageObjectNumber;\n pageIndex: number;\n charStart: number;\n charCount: number;\n /** Canonical visual-line segments (content space) — the drawing input. */\n segments: TextSegment[];\n /** Union envelope of the segments — the scroll/reveal target. Absent when\n * a match carries no drawable geometry (degenerate glyphs only). */\n bounds?: Rect;\n snippet?: SearchSnippet;\n}\n\n/**\n * `searching` means the slice loop is still walking pages — results are\n * usable the moment they appear (matches stream in viewport-first order).\n */\nexport type SearchStatus = 'idle' | 'searching' | 'complete' | 'error';\n\n/**\n * Execution options for the session `search()` — HOW the scan runs, never\n * part of what the query MEANS (which is why they're not on `SearchQuery`\n * and are never stored or restored into a search box).\n */\nexport interface SearchExecOptions {\n /** Scan origin override. Defaults to the Stage's current page (viewport-first). */\n startPage?: PageObjectNumber;\n}\n\n/** Options for the session-free `findAll()` service. */\nexport interface SearchFindAllOptions {\n /** Abort the scan; the returned promise rejects with `signal.reason`. */\n signal?: AbortSignal;\n /**\n * Pin the slice mode. Default: `'full'` with automatic `'rects'`\n * fallback when snippets are denied — pass `'rects'` when you only need\n * geometry (e.g. redact-by-term) to skip snippet extraction entirely.\n */\n mode?: SearchMode;\n}\n\nexport interface SearchState {\n /**\n * The query in flight/completed, null when idle/cleared — THE stored\n * form of the search intent, the same flat `SearchQuery` the engine\n * matches on, the wire caches by, and a search box renders. Survives\n * any search UI unmounting (results are document-scoped; so is this).\n */\n query: SearchQuery | null;\n status: SearchStatus;\n hits: SearchHit[];\n /** pon → indices into `hits` — the per-page layer's read model. */\n hitsByPage: Record<number, number[]>;\n /** Index into `hits`, -1 = none. */\n activeIndex: number;\n progress: { scanned: number; total: number };\n error: string | null;\n}\n\nexport type SearchAction =\n | { type: 'START'; query: SearchQuery }\n | { type: 'APPEND'; hits: SearchHit[]; scanned: number; total: number }\n | { type: 'COMPLETE' }\n | { type: 'ERROR'; message: string }\n | { type: 'SET_ACTIVE'; index: number }\n | { type: 'CLEAR' };\n\n/**\n * How hit navigation arrives (forwarded to the Stage's positioned reveal).\n * The default is the browser find-bar feel: the hit lands at the top-middle\n * of the viewport (`anchor: { y: 0.35 }`), horizontal only moves when the\n * hit is off-screen, zoom never changes, smooth tween.\n */\nexport interface SearchRevealOptions {\n anchor?: RevealAnchor;\n behavior?: ScrollBehaviorKind;\n}\n\nexport interface SearchPluginConfig {\n /** Arrival defaults for `next()`/`prev()`/`goTo()`. */\n reveal?: SearchRevealOptions;\n}\n\n/**\n * The search plugin is a find SERVICE plus one user-visible search SESSION\n * per document. `findAll` is the service; `search` is the session; the\n * sidebar, the highlights layer, and next/prev render the session.\n */\nexport interface SearchCapability {\n // ── the session: THE user-visible find, one per document ────────────────\n /**\n * Start a new search (supersedes and aborts any running one). Results\n * stream into state slice by slice; the first hit becomes active but the\n * camera does NOT move until `next()`/`prev()`/`goTo()`. An empty\n * `query.text` is \"stop searching\" — identical to `clear()`.\n */\n search(query: SearchQuery, exec?: SearchExecOptions): void;\n /** Re-run the current query from scratch (used after document mutations). */\n rerun(): void;\n clear(): void;\n\n /** Advance to the next/previous hit (wraps) and reveal it (see {@link SearchRevealOptions}). */\n next(): SearchHit | null;\n prev(): SearchHit | null;\n /** Jump to a specific hit index and reveal it; `reveal` overrides the plugin defaults. */\n goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null;\n\n /** The session's query — what a search box renders. Null when idle. */\n query(): SearchQuery | null;\n status(): SearchStatus;\n hits(): SearchHit[];\n hitCount(): number;\n activeIndex(): number;\n activeHit(): SearchHit | null;\n /** Hits on one page — the layer's input. Element identity is stable. */\n hitsForPage(pon: PageObjectNumber): SearchHit[];\n progress(): { scanned: number; total: number };\n errorMessage(): string | null;\n\n // ── the mechanism: session-free find service ─────────────────────────────\n /**\n * Run a query to completion and return every hit. Touches NO state — no\n * sidebar, no highlights, no camera, no activeIndex; the user's visible\n * search is never superseded. The primitive other features build on\n * (redact-by-term, link detection, occurrence badges). Scans in natural\n * page order; concurrent calls are independent.\n */\n findAll(query: SearchQuery, opts?: SearchFindAllOptions): Promise<SearchHit[]>;\n}\n\nexport const SearchToken = createCapabilityToken<SearchCapability>('search');\n","/**\n * Keeps results honest across document mutations. Every confirmed\n * mutation (own or remote) bumps the engine's mutation sequence, which\n * invalidates search cursors AND can change what's findable (redaction,\n * page delete, import). The correct reaction is always the same: re-run\n * the current query from scratch. Reruns are coalesced — a burst of\n * events (multi-part mutation, SSE catch-up) triggers one rescan.\n */\nimport type { EffectContext } from '@embedpdf/core';\n\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchState } from './types';\n\nconst RERUN_DELAY_MS = 250;\n\nexport function registerSearchEffects(ctx: EffectContext<SearchState, SearchAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n const unsubscribe = doc.events.subscribe(() => {\n if (ctx.getState().status === 'idle') return;\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n ctx.get(SearchToken).rerun();\n }, RERUN_DELAY_MS);\n });\n\n ctx.cleanup(() => {\n if (timer !== null) clearTimeout(timer);\n unsubscribe();\n });\n}\n","import type { SearchAction, SearchState } from './types';\n\nexport const initialSearchState = (): SearchState => ({\n query: null,\n status: 'idle',\n hits: [],\n hitsByPage: {},\n activeIndex: -1,\n progress: { scanned: 0, total: 0 },\n error: null,\n});\n\nexport const searchReducer = (state: SearchState, action: SearchAction): SearchState => {\n switch (action.type) {\n case 'START':\n return { ...initialSearchState(), query: action.query, status: 'searching' };\n case 'APPEND': {\n if (action.hits.length === 0) {\n return { ...state, progress: { scanned: action.scanned, total: action.total } };\n }\n const hits = state.hits.concat(action.hits);\n const hitsByPage = { ...state.hitsByPage };\n for (let i = state.hits.length; i < hits.length; i++) {\n const pon = hits[i].pon;\n hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);\n }\n return {\n ...state,\n hits,\n hitsByPage,\n // The first hit becomes active (so \"1/N\" reads right) — navigation,\n // not the stream, moves the camera.\n activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,\n progress: { scanned: action.scanned, total: action.total },\n };\n }\n case 'COMPLETE':\n return { ...state, status: 'complete' };\n case 'ERROR':\n return { ...state, status: 'error', error: action.message };\n case 'SET_ACTIVE':\n return { ...state, activeIndex: action.index };\n case 'CLEAR':\n return initialSearchState();\n default:\n return state;\n }\n};\n","import { definePlugin } from '@embedpdf/core';\nimport { StageToken } from '@embedpdf/plugin-stage';\n\nimport { createSearchCapability } from './capability';\nimport { registerSearchEffects } from './effects';\nimport { initialSearchState, searchReducer } from './reducer';\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchCapability, SearchPluginConfig, SearchState } from './types';\n\n/**\n * Document text search over `doc.search` — the engine's budgeted,\n * cursor-resumable slices. Document-scoped; no pointer handling, so it\n * needs no interaction hub. The Stage is optional: when present, scans\n * start at the current page (viewport-first) and hit navigation reveals\n * the hit's page; without it the host owns scrolling.\n */\nexport const searchPlugin = (config?: SearchPluginConfig) =>\n definePlugin<SearchState, SearchAction, SearchCapability>({\n id: 'search',\n token: SearchToken,\n scope: 'document',\n optional: [StageToken],\n initialState: initialSearchState,\n reduce: searchReducer,\n capability: (ctx) => createSearchCapability(ctx, config),\n effects: registerSearchEffects,\n });\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PageObjectNumber } from "@embedpdf/core";
|
|
2
|
-
import { Rect } from "@embedpdf/core-geometry";
|
|
2
|
+
import { Rect, TextQuad } from "@embedpdf/core-geometry";
|
|
3
3
|
import { RevealAnchor, ScrollBehaviorKind } from "@embedpdf/plugin-stage";
|
|
4
4
|
import { SearchMode, SearchQuery, SearchQuery as SearchQuery$1, SearchQueryIssue, SearchQueryValidation, SearchRegexValidation, SearchSnippet, SearchSnippet as SearchSnippet$1, validateSearchQuery, validateSearchRegex } from "@embedpdf/engine-core/runtime";
|
|
5
5
|
//#region src/types.d.ts
|
|
@@ -10,12 +10,27 @@ import { SearchMode, SearchQuery, SearchQuery as SearchQuery$1, SearchQueryIssue
|
|
|
10
10
|
* `charStart`/`charCount` keep the engine's text-page offsets, so a hit can
|
|
11
11
|
* later seed a selection or a highlight annotation without re-searching.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* One merged visual line of a match, in content space — the structural twin
|
|
15
|
+
* of selection's `SelectionSegment` and the engine's `PdfTextSegment`.
|
|
16
|
+
* `quad` is the geometric authority (corner-named, frame-geometric); `rect`
|
|
17
|
+
* its AABB; `advance` the reading direction along the baseline.
|
|
18
|
+
*/
|
|
19
|
+
interface TextSegment {
|
|
20
|
+
quad: TextQuad;
|
|
21
|
+
rect: Rect;
|
|
22
|
+
advance: 1 | -1;
|
|
23
|
+
}
|
|
13
24
|
interface SearchHit {
|
|
14
25
|
pon: PageObjectNumber;
|
|
15
26
|
pageIndex: number;
|
|
16
27
|
charStart: number;
|
|
17
28
|
charCount: number;
|
|
18
|
-
|
|
29
|
+
/** Canonical visual-line segments (content space) — the drawing input. */
|
|
30
|
+
segments: TextSegment[];
|
|
31
|
+
/** Union envelope of the segments — the scroll/reveal target. Absent when
|
|
32
|
+
* a match carries no drawable geometry (degenerate glyphs only). */
|
|
33
|
+
bounds?: Rect;
|
|
19
34
|
snippet?: SearchSnippet$1;
|
|
20
35
|
}
|
|
21
36
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PageObjectNumber } from "@embedpdf/core";
|
|
2
2
|
import { RevealAnchor, ScrollBehaviorKind } from "@embedpdf/plugin-stage";
|
|
3
|
-
import { Rect } from "@embedpdf/core-geometry";
|
|
3
|
+
import { Rect, TextQuad } from "@embedpdf/core-geometry";
|
|
4
4
|
import { SearchMode, SearchQuery, SearchQuery as SearchQuery$1, SearchQueryIssue, SearchQueryValidation, SearchRegexValidation, SearchSnippet, SearchSnippet as SearchSnippet$1, validateSearchQuery, validateSearchRegex } from "@embedpdf/engine-core/runtime";
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
/**
|
|
@@ -10,12 +10,27 @@ import { SearchMode, SearchQuery, SearchQuery as SearchQuery$1, SearchQueryIssue
|
|
|
10
10
|
* `charStart`/`charCount` keep the engine's text-page offsets, so a hit can
|
|
11
11
|
* later seed a selection or a highlight annotation without re-searching.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* One merged visual line of a match, in content space — the structural twin
|
|
15
|
+
* of selection's `SelectionSegment` and the engine's `PdfTextSegment`.
|
|
16
|
+
* `quad` is the geometric authority (corner-named, frame-geometric); `rect`
|
|
17
|
+
* its AABB; `advance` the reading direction along the baseline.
|
|
18
|
+
*/
|
|
19
|
+
interface TextSegment {
|
|
20
|
+
quad: TextQuad;
|
|
21
|
+
rect: Rect;
|
|
22
|
+
advance: 1 | -1;
|
|
23
|
+
}
|
|
13
24
|
interface SearchHit {
|
|
14
25
|
pon: PageObjectNumber;
|
|
15
26
|
pageIndex: number;
|
|
16
27
|
charStart: number;
|
|
17
28
|
charCount: number;
|
|
18
|
-
|
|
29
|
+
/** Canonical visual-line segments (content space) — the drawing input. */
|
|
30
|
+
segments: TextSegment[];
|
|
31
|
+
/** Union envelope of the segments — the scroll/reveal target. Absent when
|
|
32
|
+
* a match carries no drawable geometry (degenerate glyphs only). */
|
|
33
|
+
bounds?: Rect;
|
|
19
34
|
snippet?: SearchSnippet$1;
|
|
20
35
|
}
|
|
21
36
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCapabilityToken, definePlugin } from "@embedpdf/core";
|
|
2
2
|
import { StageToken } from "@embedpdf/plugin-stage";
|
|
3
|
-
import {
|
|
3
|
+
import { applyPoint, boundsOfRects, pageGeometry, textQuadBounds } from "@embedpdf/core-geometry";
|
|
4
4
|
import { EngineError, EngineErrorCode, validateSearchQuery, validateSearchRegex } from "@embedpdf/engine-core/runtime";
|
|
5
5
|
//#region src/capability.ts
|
|
6
6
|
const EMPTY = [];
|
|
@@ -29,12 +29,6 @@ const isPermissionDenied = (err) => err instanceof Error && err.name === "Permis
|
|
|
29
29
|
function createSearchCapability(ctx, config = {}) {
|
|
30
30
|
let generation = 0;
|
|
31
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
32
|
function hitsFromSlice(slice) {
|
|
39
33
|
const converters = /* @__PURE__ */ new Map();
|
|
40
34
|
const converterFor = (pon) => {
|
|
@@ -48,9 +42,15 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
48
42
|
rotation: layout.rotation,
|
|
49
43
|
userUnit: layout.userUnit
|
|
50
44
|
}, 1);
|
|
45
|
+
const toPoint = (pt) => applyPoint(pdfToContent, pt);
|
|
51
46
|
conv = {
|
|
52
47
|
pageIndex: layout.index,
|
|
53
|
-
|
|
48
|
+
toQuad: (q) => ({
|
|
49
|
+
upperStart: toPoint(q.p1),
|
|
50
|
+
upperEnd: toPoint(q.p2),
|
|
51
|
+
lowerStart: toPoint(q.p3),
|
|
52
|
+
lowerEnd: toPoint(q.p4)
|
|
53
|
+
})
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
converters.set(pon, conv);
|
|
@@ -60,12 +60,21 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
60
60
|
for (const match of slice.matches) {
|
|
61
61
|
const conv = converterFor(match.pageObjectNumber);
|
|
62
62
|
if (!conv) continue;
|
|
63
|
+
const segments = match.segments.map((segment) => {
|
|
64
|
+
const quad = conv.toQuad(segment.quad);
|
|
65
|
+
return {
|
|
66
|
+
quad,
|
|
67
|
+
rect: textQuadBounds(quad),
|
|
68
|
+
advance: segment.advance
|
|
69
|
+
};
|
|
70
|
+
});
|
|
63
71
|
hits.push({
|
|
64
72
|
pon: match.pageObjectNumber,
|
|
65
73
|
pageIndex: conv.pageIndex,
|
|
66
74
|
charStart: match.charStart,
|
|
67
75
|
charCount: match.charCount,
|
|
68
|
-
|
|
76
|
+
segments,
|
|
77
|
+
...segments.length ? { bounds: boundsOfRects(segments.map((s) => s.rect)) ?? void 0 } : {},
|
|
69
78
|
...match.snippet ? { snippet: match.snippet } : {}
|
|
70
79
|
});
|
|
71
80
|
}
|
|
@@ -174,7 +183,7 @@ function createSearchCapability(ctx, config = {}) {
|
|
|
174
183
|
...reveal
|
|
175
184
|
};
|
|
176
185
|
ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {
|
|
177
|
-
rect:
|
|
186
|
+
rect: hit.bounds,
|
|
178
187
|
anchor: arrival.anchor,
|
|
179
188
|
behavior: arrival.behavior
|
|
180
189
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/capability.ts","../src/types.ts","../src/effects.ts","../src/reducer.ts","../src/search.plugin.ts"],"sourcesContent":["import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport {\n applyRect,\n boundsOfRects,\n pageGeometry,\n type Rect,\n type RectIn,\n} from '@embedpdf/core-geometry';\nimport { StageToken } from '@embedpdf/plugin-stage';\nimport { EngineError, EngineErrorCode } from '@embedpdf/engine-core/runtime';\nimport type {\n PdfRect,\n SearchMatch,\n SearchMode,\n SearchQuery,\n SearchSlice,\n} from '@embedpdf/engine-core/runtime';\n\nimport type {\n SearchAction,\n SearchCapability,\n SearchHit,\n SearchPluginConfig,\n SearchRevealOptions,\n SearchState,\n} from './types';\n\nconst EMPTY: SearchHit[] = [];\n\n/** The browser find-bar feel: hit at the top-middle, no zoom change. */\nconst DEFAULT_REVEAL: SearchRevealOptions = { anchor: { y: 0.35 }, behavior: 'smooth' };\n\n/** Local engines reject a denied scope before the worker; cloud maps 403. */\nconst isPermissionDenied = (err: unknown): boolean =>\n (err instanceof Error && err.name === 'PermissionDenied') ||\n EngineError.is(err, EngineErrorCode.Forbidden);\n\n/**\n * The search capability: a client-driven cursor loop over the engine's\n * budgeted slices. One generation counter arbitrates: a new `search()` (or\n * `clear()`, or a document mutation via `rerun()`) supersedes the running\n * loop, which aborts its in-flight slice and stops dispatching.\n *\n * Rect conversion happens HERE, once per slice: the engine's PDF-space\n * line rects → content space through the same page geometry the selection\n * plugin uses, so search highlights and selection highlights are pixel-twins.\n *\n * Stage integration is optional and additive: with a Stage present the scan\n * starts at the current page (viewport-first — the hits the user is looking\n * at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the\n * hit's page. Without one, navigation still works; the host scrolls.\n */\nexport function createSearchCapability(\n ctx: PluginContext<SearchState, SearchAction>,\n config: SearchPluginConfig = {},\n): SearchCapability {\n let generation = 0;\n let inflight: { abort(reason?: unknown): void } | null = null;\n\n const toContent = (m: ReturnType<typeof pageGeometry>['pdfToContent'], b: PdfRect): Rect =>\n applyRect(\n m as never,\n {\n x: b.left,\n y: b.bottom,\n width: b.right - b.left,\n height: b.top - b.bottom,\n } as RectIn<'pdf'>,\n ) as Rect;\n\n interface PageConverter {\n pageIndex: number;\n toRect: (b: PdfRect) => Rect;\n }\n\n function hitsFromSlice(slice: SearchSlice): SearchHit[] {\n // zoom = 1: content space is scale-free; the layer applies the page\n // transform at render time (the selection idiom).\n const converters = new Map<PageObjectNumber, PageConverter | null>();\n const converterFor = (pon: PageObjectNumber): PageConverter | null => {\n let conv = converters.get(pon);\n if (conv !== undefined) return conv;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!layout) {\n conv = null; // page vanished mid-search (delete); drop its hits\n } else {\n const { pdfToContent } = pageGeometry(\n { crop: layout.boxes.crop, rotation: layout.rotation, userUnit: layout.userUnit },\n 1,\n );\n conv = { pageIndex: layout.index, toRect: (b) => toContent(pdfToContent, b) };\n }\n converters.set(pon, conv);\n return conv;\n };\n\n const hits: SearchHit[] = [];\n for (const match of slice.matches) {\n const conv = converterFor(match.pageObjectNumber);\n if (!conv) continue;\n hits.push({\n pon: match.pageObjectNumber,\n pageIndex: conv.pageIndex,\n charStart: match.charStart,\n charCount: match.charCount,\n rects: match.rects.map(conv.toRect),\n ...(match.snippet ? { snippet: match.snippet } : {}),\n });\n }\n return hits;\n }\n\n /** How a collect() consumer observes the loop — the session dispatches\n * into state, findAll accumulates locally. */\n interface CollectSink {\n /** Called before the first slice AND again on a stale-cursor restart —\n * reset any accumulation. */\n onStart: () => void;\n onSlice: (hits: SearchHit[], scanned: number, total: number) => void;\n /** Superseded / aborted — the loop stops without completing. */\n isCancelled: () => boolean;\n setInflight: (pending: { abort(reason?: unknown): void } | null) => void;\n }\n\n /**\n * THE MECHANISM — one cursor loop over the engine's budgeted slices,\n * shared by the session (`search`) and the session-free service\n * (`findAll`): permission fallback ('full' → 'rects' when snippets are\n * denied, unless the caller pinned a mode), restart-once on a stale\n * cursor, rect conversion per slice. Resolves true on exhaustion, false\n * when cancelled; throws on error.\n */\n async function collect(\n query: SearchQuery,\n opts: { startPage?: PageObjectNumber; mode?: SearchMode },\n sink: CollectSink,\n ): Promise<boolean> {\n const doc = ctx.doc;\n if (!doc) return false;\n\n sink.onStart();\n\n let mode: SearchMode = opts.mode ?? 'full';\n const modePinned = opts.mode !== undefined;\n let cursor: string | undefined;\n let restarted = false;\n for (;;) {\n let slice: SearchSlice;\n try {\n const request = {\n query,\n mode,\n ...(cursor !== undefined\n ? { cursor }\n : opts.startPage !== undefined\n ? { startPage: opts.startPage }\n : {}),\n };\n const pending = doc.search.query(request);\n sink.setInflight(pending);\n slice = await pending;\n } catch (err) {\n if (sink.isCancelled()) return false; // superseded — the abort was ours\n // Snippets denied (no doc.text.copy)? Degrade to rect-only matches\n // instead of failing the whole search — find still works.\n if (!modePinned && mode === 'full' && cursor === undefined && isPermissionDenied(err)) {\n mode = 'rects';\n continue;\n }\n // A stale cursor rejects with InvalidArg — the document changed\n // under the loop, or the engine's session was recycled. Position\n // is lost, the query isn't: restart once from scratch.\n if (cursor !== undefined && !restarted && EngineError.is(err, EngineErrorCode.InvalidArg)) {\n restarted = true;\n cursor = undefined;\n sink.onStart();\n continue;\n }\n throw err;\n }\n if (sink.isCancelled()) return false;\n sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);\n if (slice.nextCursor === null) {\n sink.setInflight(null);\n return true;\n }\n cursor = slice.nextCursor;\n }\n }\n\n /** THE SESSION — collect() with state as the sink, generation-guarded. */\n async function runSession(query: SearchQuery, startPage?: PageObjectNumber): Promise<void> {\n const gen = ++generation;\n inflight?.abort('superseded');\n inflight = null;\n\n if (!ctx.doc) {\n ctx.dispatch({ type: 'CLEAR' });\n return;\n }\n\n // Viewport-first: begin scanning where the user is looking.\n if (startPage === undefined) {\n const stage = ctx.tryGet(StageToken);\n if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;\n }\n\n try {\n const complete = await collect(\n query,\n { startPage },\n {\n onStart: () => ctx.dispatch({ type: 'START', query }),\n onSlice: (hits, scanned, total) => ctx.dispatch({ type: 'APPEND', hits, scanned, total }),\n isCancelled: () => gen !== generation,\n setInflight: (pending) => {\n if (gen === generation) inflight = pending;\n },\n },\n );\n if (complete && gen === generation) ctx.dispatch({ type: 'COMPLETE' });\n } catch (err) {\n if (gen !== generation) return;\n ctx.dispatch({ type: 'ERROR', message: err instanceof Error ? err.message : String(err) });\n }\n }\n\n function goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null {\n const { hits } = ctx.getState();\n if (hits.length === 0) return null;\n const wrapped = ((index % hits.length) + hits.length) % hits.length;\n ctx.dispatch({ type: 'SET_ACTIVE', index: wrapped });\n const hit = hits[wrapped];\n // Positioned reveal: the HIT (not just its page) arrives at the anchor —\n // per-call override > plugin config > find-bar default. Zoom never changes.\n const arrival = { ...DEFAULT_REVEAL, ...config.reveal, ...reveal };\n ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {\n rect: boundsOfRects(hit.rects) ?? undefined,\n anchor: arrival.anchor,\n behavior: arrival.behavior,\n });\n return hit;\n }\n\n const clear = () => {\n generation++;\n inflight?.abort('cleared');\n inflight = null;\n ctx.dispatch({ type: 'CLEAR' });\n };\n\n return {\n // An empty text is \"stop searching\", not a query — identical to clear().\n search: (query, exec) =>\n query.text.length === 0 ? clear() : void runSession(query, exec?.startPage),\n\n // Replay the stored query verbatim; the rescan starts viewport-first.\n rerun: () => {\n const { query, status } = ctx.getState();\n if (query && status !== 'idle') void runSession(query);\n },\n\n clear,\n\n next: () => goTo(ctx.getState().activeIndex + 1),\n prev: () => goTo(ctx.getState().activeIndex - 1),\n goTo,\n\n query: () => ctx.getState().query,\n\n findAll: async (query, opts = {}) => {\n if (!ctx.doc) return [];\n const all: SearchHit[] = [];\n const signal = opts.signal;\n // Independent lifecycle: its own abort handle, never the session's\n // generation counter — a findAll can neither supersede the user's\n // visible search nor be superseded by it.\n let pending: { abort(reason?: unknown): void } | null = null;\n const onAbort = () => pending?.abort(signal?.reason);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n await collect(\n query,\n { mode: opts.mode },\n {\n onStart: () => {\n all.length = 0;\n },\n onSlice: (hits) => all.push(...hits),\n isCancelled: () => signal?.aborted ?? false,\n setInflight: (p) => {\n pending = p;\n },\n },\n );\n if (signal?.aborted) throw signal.reason ?? new Error('findAll aborted');\n return all;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n },\n\n status: () => ctx.getState().status,\n hits: () => ctx.getState().hits,\n hitCount: () => ctx.getState().hits.length,\n activeIndex: () => ctx.getState().activeIndex,\n activeHit: () => {\n const { hits, activeIndex } = ctx.getState();\n return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;\n },\n hitsForPage: (pon) => {\n const { hits, hitsByPage } = ctx.getState();\n const indices = hitsByPage[pon];\n return indices?.length ? indices.map((i) => hits[i]) : EMPTY;\n },\n progress: () => ctx.getState().progress,\n errorMessage: () => ctx.getState().error,\n };\n}\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Rect } from '@embedpdf/core-geometry';\nimport type { RevealAnchor, ScrollBehaviorKind } from '@embedpdf/plugin-stage';\nimport type { SearchMode, SearchQuery, SearchSnippet } from '@embedpdf/engine-core/runtime';\n\n/**\n * One match, viewer-shaped: the engine's PDF-space line rects converted to\n * CONTENT space (y-down — the same space selection rects live in), plus the\n * page's display index so navigation can `reveal()` without a lookup.\n * `charStart`/`charCount` keep the engine's text-page offsets, so a hit can\n * later seed a selection or a highlight annotation without re-searching.\n */\nexport interface SearchHit {\n pon: PageObjectNumber;\n pageIndex: number;\n charStart: number;\n charCount: number;\n rects: Rect[];\n snippet?: SearchSnippet;\n}\n\n/**\n * `searching` means the slice loop is still walking pages — results are\n * usable the moment they appear (matches stream in viewport-first order).\n */\nexport type SearchStatus = 'idle' | 'searching' | 'complete' | 'error';\n\n/**\n * Execution options for the session `search()` — HOW the scan runs, never\n * part of what the query MEANS (which is why they're not on `SearchQuery`\n * and are never stored or restored into a search box).\n */\nexport interface SearchExecOptions {\n /** Scan origin override. Defaults to the Stage's current page (viewport-first). */\n startPage?: PageObjectNumber;\n}\n\n/** Options for the session-free `findAll()` service. */\nexport interface SearchFindAllOptions {\n /** Abort the scan; the returned promise rejects with `signal.reason`. */\n signal?: AbortSignal;\n /**\n * Pin the slice mode. Default: `'full'` with automatic `'rects'`\n * fallback when snippets are denied — pass `'rects'` when you only need\n * geometry (e.g. redact-by-term) to skip snippet extraction entirely.\n */\n mode?: SearchMode;\n}\n\nexport interface SearchState {\n /**\n * The query in flight/completed, null when idle/cleared — THE stored\n * form of the search intent, the same flat `SearchQuery` the engine\n * matches on, the wire caches by, and a search box renders. Survives\n * any search UI unmounting (results are document-scoped; so is this).\n */\n query: SearchQuery | null;\n status: SearchStatus;\n hits: SearchHit[];\n /** pon → indices into `hits` — the per-page layer's read model. */\n hitsByPage: Record<number, number[]>;\n /** Index into `hits`, -1 = none. */\n activeIndex: number;\n progress: { scanned: number; total: number };\n error: string | null;\n}\n\nexport type SearchAction =\n | { type: 'START'; query: SearchQuery }\n | { type: 'APPEND'; hits: SearchHit[]; scanned: number; total: number }\n | { type: 'COMPLETE' }\n | { type: 'ERROR'; message: string }\n | { type: 'SET_ACTIVE'; index: number }\n | { type: 'CLEAR' };\n\n/**\n * How hit navigation arrives (forwarded to the Stage's positioned reveal).\n * The default is the browser find-bar feel: the hit lands at the top-middle\n * of the viewport (`anchor: { y: 0.35 }`), horizontal only moves when the\n * hit is off-screen, zoom never changes, smooth tween.\n */\nexport interface SearchRevealOptions {\n anchor?: RevealAnchor;\n behavior?: ScrollBehaviorKind;\n}\n\nexport interface SearchPluginConfig {\n /** Arrival defaults for `next()`/`prev()`/`goTo()`. */\n reveal?: SearchRevealOptions;\n}\n\n/**\n * The search plugin is a find SERVICE plus one user-visible search SESSION\n * per document. `findAll` is the service; `search` is the session; the\n * sidebar, the highlights layer, and next/prev render the session.\n */\nexport interface SearchCapability {\n // ── the session: THE user-visible find, one per document ────────────────\n /**\n * Start a new search (supersedes and aborts any running one). Results\n * stream into state slice by slice; the first hit becomes active but the\n * camera does NOT move until `next()`/`prev()`/`goTo()`. An empty\n * `query.text` is \"stop searching\" — identical to `clear()`.\n */\n search(query: SearchQuery, exec?: SearchExecOptions): void;\n /** Re-run the current query from scratch (used after document mutations). */\n rerun(): void;\n clear(): void;\n\n /** Advance to the next/previous hit (wraps) and reveal it (see {@link SearchRevealOptions}). */\n next(): SearchHit | null;\n prev(): SearchHit | null;\n /** Jump to a specific hit index and reveal it; `reveal` overrides the plugin defaults. */\n goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null;\n\n /** The session's query — what a search box renders. Null when idle. */\n query(): SearchQuery | null;\n status(): SearchStatus;\n hits(): SearchHit[];\n hitCount(): number;\n activeIndex(): number;\n activeHit(): SearchHit | null;\n /** Hits on one page — the layer's input. Element identity is stable. */\n hitsForPage(pon: PageObjectNumber): SearchHit[];\n progress(): { scanned: number; total: number };\n errorMessage(): string | null;\n\n // ── the mechanism: session-free find service ─────────────────────────────\n /**\n * Run a query to completion and return every hit. Touches NO state — no\n * sidebar, no highlights, no camera, no activeIndex; the user's visible\n * search is never superseded. The primitive other features build on\n * (redact-by-term, link detection, occurrence badges). Scans in natural\n * page order; concurrent calls are independent.\n */\n findAll(query: SearchQuery, opts?: SearchFindAllOptions): Promise<SearchHit[]>;\n}\n\nexport const SearchToken = createCapabilityToken<SearchCapability>('search');\n","/**\n * Keeps results honest across document mutations. Every confirmed\n * mutation (own or remote) bumps the engine's mutation sequence, which\n * invalidates search cursors AND can change what's findable (redaction,\n * page delete, import). The correct reaction is always the same: re-run\n * the current query from scratch. Reruns are coalesced — a burst of\n * events (multi-part mutation, SSE catch-up) triggers one rescan.\n */\nimport type { EffectContext } from '@embedpdf/core';\n\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchState } from './types';\n\nconst RERUN_DELAY_MS = 250;\n\nexport function registerSearchEffects(ctx: EffectContext<SearchState, SearchAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n const unsubscribe = doc.events.subscribe(() => {\n if (ctx.getState().status === 'idle') return;\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n ctx.get(SearchToken).rerun();\n }, RERUN_DELAY_MS);\n });\n\n ctx.cleanup(() => {\n if (timer !== null) clearTimeout(timer);\n unsubscribe();\n });\n}\n","import type { SearchAction, SearchState } from './types';\n\nexport const initialSearchState = (): SearchState => ({\n query: null,\n status: 'idle',\n hits: [],\n hitsByPage: {},\n activeIndex: -1,\n progress: { scanned: 0, total: 0 },\n error: null,\n});\n\nexport const searchReducer = (state: SearchState, action: SearchAction): SearchState => {\n switch (action.type) {\n case 'START':\n return { ...initialSearchState(), query: action.query, status: 'searching' };\n case 'APPEND': {\n if (action.hits.length === 0) {\n return { ...state, progress: { scanned: action.scanned, total: action.total } };\n }\n const hits = state.hits.concat(action.hits);\n const hitsByPage = { ...state.hitsByPage };\n for (let i = state.hits.length; i < hits.length; i++) {\n const pon = hits[i].pon;\n hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);\n }\n return {\n ...state,\n hits,\n hitsByPage,\n // The first hit becomes active (so \"1/N\" reads right) — navigation,\n // not the stream, moves the camera.\n activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,\n progress: { scanned: action.scanned, total: action.total },\n };\n }\n case 'COMPLETE':\n return { ...state, status: 'complete' };\n case 'ERROR':\n return { ...state, status: 'error', error: action.message };\n case 'SET_ACTIVE':\n return { ...state, activeIndex: action.index };\n case 'CLEAR':\n return initialSearchState();\n default:\n return state;\n }\n};\n","import { definePlugin } from '@embedpdf/core';\nimport { StageToken } from '@embedpdf/plugin-stage';\n\nimport { createSearchCapability } from './capability';\nimport { registerSearchEffects } from './effects';\nimport { initialSearchState, searchReducer } from './reducer';\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchCapability, SearchPluginConfig, SearchState } from './types';\n\n/**\n * Document text search over `doc.search` — the engine's budgeted,\n * cursor-resumable slices. Document-scoped; no pointer handling, so it\n * needs no interaction hub. The Stage is optional: when present, scans\n * start at the current page (viewport-first) and hit navigation reveals\n * the hit's page; without it the host owns scrolling.\n */\nexport const searchPlugin = (config?: SearchPluginConfig) =>\n definePlugin<SearchState, SearchAction, SearchCapability>({\n id: 'search',\n token: SearchToken,\n scope: 'document',\n optional: [StageToken],\n initialState: initialSearchState,\n reduce: searchReducer,\n capability: (ctx) => createSearchCapability(ctx, config),\n effects: registerSearchEffects,\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/capability.ts","../src/types.ts","../src/effects.ts","../src/reducer.ts","../src/search.plugin.ts"],"sourcesContent":["import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport { type PointIn, type TextQuad, applyPoint, boundsOfRects, pageGeometry, textQuadBounds } from '@embedpdf/core-geometry';\nimport { StageToken } from '@embedpdf/plugin-stage';\nimport { EngineError, EngineErrorCode } from '@embedpdf/engine-core/runtime';\nimport type {\n PdfQuad,\n PdfRect,\n SearchMatch,\n SearchMode,\n SearchQuery,\n SearchSlice,\n} from '@embedpdf/engine-core/runtime';\n\nimport type {\n SearchAction,\n SearchCapability,\n SearchHit,\n SearchPluginConfig,\n SearchRevealOptions,\n SearchState,\n} from './types';\n\nconst EMPTY: SearchHit[] = [];\n\n/** The browser find-bar feel: hit at the top-middle, no zoom change. */\nconst DEFAULT_REVEAL: SearchRevealOptions = { anchor: { y: 0.35 }, behavior: 'smooth' };\n\n/** Local engines reject a denied scope before the worker; cloud maps 403. */\nconst isPermissionDenied = (err: unknown): boolean =>\n (err instanceof Error && err.name === 'PermissionDenied') ||\n EngineError.is(err, EngineErrorCode.Forbidden);\n\n/**\n * The search capability: a client-driven cursor loop over the engine's\n * budgeted slices. One generation counter arbitrates: a new `search()` (or\n * `clear()`, or a document mutation via `rerun()`) supersedes the running\n * loop, which aborts its in-flight slice and stops dispatching.\n *\n * Rect conversion happens HERE, once per slice: the engine's PDF-space\n * line rects → content space through the same page geometry the selection\n * plugin uses, so search highlights and selection highlights are pixel-twins.\n *\n * Stage integration is optional and additive: with a Stage present the scan\n * starts at the current page (viewport-first — the hits the user is looking\n * at arrive in the first slice) and `next()`/`prev()`/`goTo()` reveal the\n * hit's page. Without one, navigation still works; the host scrolls.\n */\nexport function createSearchCapability(\n ctx: PluginContext<SearchState, SearchAction>,\n config: SearchPluginConfig = {},\n): SearchCapability {\n let generation = 0;\n let inflight: { abort(reason?: unknown): void } | null = null;\n\n interface PageConverter {\n pageIndex: number;\n toQuad: (q: PdfQuad) => TextQuad;\n }\n\n function hitsFromSlice(slice: SearchSlice): SearchHit[] {\n // zoom = 1: content space is scale-free; the layer applies the page\n // transform at render time (the selection idiom).\n const converters = new Map<PageObjectNumber, PageConverter | null>();\n const converterFor = (pon: PageObjectNumber): PageConverter | null => {\n let conv = converters.get(pon);\n if (conv !== undefined) return conv;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!layout) {\n conv = null; // page vanished mid-search (delete); drop its hits\n } else {\n const { pdfToContent } = pageGeometry(\n { crop: layout.boxes.crop, rotation: layout.rotation, userUnit: layout.userUnit },\n 1,\n );\n // Engine quads carry frame-geometric slot semantics (p1..p4 =\n // upper-start, upper-end, lower-start, lower-end) — a trusted\n // producer, so the y-flip maps corners straight onto their names.\n const toPoint = (pt: { x: number; y: number }) =>\n applyPoint(pdfToContent as never, pt as PointIn<'pdf'>) as { x: number; y: number };\n conv = {\n pageIndex: layout.index,\n toQuad: (q) => ({\n upperStart: toPoint(q.p1),\n upperEnd: toPoint(q.p2),\n lowerStart: toPoint(q.p3),\n lowerEnd: toPoint(q.p4),\n }),\n };\n }\n converters.set(pon, conv);\n return conv;\n };\n\n const hits: SearchHit[] = [];\n for (const match of slice.matches) {\n const conv = converterFor(match.pageObjectNumber);\n if (!conv) continue;\n const segments = match.segments.map((segment) => {\n const quad = conv.toQuad(segment.quad);\n // `rect` is recomputed from the MAPPED quad so the pair can't drift.\n return { quad, rect: textQuadBounds(quad), advance: segment.advance };\n });\n hits.push({\n pon: match.pageObjectNumber,\n pageIndex: conv.pageIndex,\n charStart: match.charStart,\n charCount: match.charCount,\n segments,\n ...(segments.length\n ? { bounds: boundsOfRects(segments.map((s) => s.rect)) ?? undefined }\n : {}),\n ...(match.snippet ? { snippet: match.snippet } : {}),\n });\n }\n return hits;\n }\n\n /** How a collect() consumer observes the loop — the session dispatches\n * into state, findAll accumulates locally. */\n interface CollectSink {\n /** Called before the first slice AND again on a stale-cursor restart —\n * reset any accumulation. */\n onStart: () => void;\n onSlice: (hits: SearchHit[], scanned: number, total: number) => void;\n /** Superseded / aborted — the loop stops without completing. */\n isCancelled: () => boolean;\n setInflight: (pending: { abort(reason?: unknown): void } | null) => void;\n }\n\n /**\n * THE MECHANISM — one cursor loop over the engine's budgeted slices,\n * shared by the session (`search`) and the session-free service\n * (`findAll`): permission fallback ('full' → 'rects' when snippets are\n * denied, unless the caller pinned a mode), restart-once on a stale\n * cursor, rect conversion per slice. Resolves true on exhaustion, false\n * when cancelled; throws on error.\n */\n async function collect(\n query: SearchQuery,\n opts: { startPage?: PageObjectNumber; mode?: SearchMode },\n sink: CollectSink,\n ): Promise<boolean> {\n const doc = ctx.doc;\n if (!doc) return false;\n\n sink.onStart();\n\n let mode: SearchMode = opts.mode ?? 'full';\n const modePinned = opts.mode !== undefined;\n let cursor: string | undefined;\n let restarted = false;\n for (;;) {\n let slice: SearchSlice;\n try {\n const request = {\n query,\n mode,\n ...(cursor !== undefined\n ? { cursor }\n : opts.startPage !== undefined\n ? { startPage: opts.startPage }\n : {}),\n };\n const pending = doc.search.query(request);\n sink.setInflight(pending);\n slice = await pending;\n } catch (err) {\n if (sink.isCancelled()) return false; // superseded — the abort was ours\n // Snippets denied (no doc.text.copy)? Degrade to rect-only matches\n // instead of failing the whole search — find still works.\n if (!modePinned && mode === 'full' && cursor === undefined && isPermissionDenied(err)) {\n mode = 'rects';\n continue;\n }\n // A stale cursor rejects with InvalidArg — the document changed\n // under the loop, or the engine's session was recycled. Position\n // is lost, the query isn't: restart once from scratch.\n if (cursor !== undefined && !restarted && EngineError.is(err, EngineErrorCode.InvalidArg)) {\n restarted = true;\n cursor = undefined;\n sink.onStart();\n continue;\n }\n throw err;\n }\n if (sink.isCancelled()) return false;\n sink.onSlice(hitsFromSlice(slice), slice.scannedPages, slice.totalPages);\n if (slice.nextCursor === null) {\n sink.setInflight(null);\n return true;\n }\n cursor = slice.nextCursor;\n }\n }\n\n /** THE SESSION — collect() with state as the sink, generation-guarded. */\n async function runSession(query: SearchQuery, startPage?: PageObjectNumber): Promise<void> {\n const gen = ++generation;\n inflight?.abort('superseded');\n inflight = null;\n\n if (!ctx.doc) {\n ctx.dispatch({ type: 'CLEAR' });\n return;\n }\n\n // Viewport-first: begin scanning where the user is looking.\n if (startPage === undefined) {\n const stage = ctx.tryGet(StageToken);\n if (stage) startPage = ctx.document()?.pages[stage.currentPage()]?.pageObjectNumber;\n }\n\n try {\n const complete = await collect(\n query,\n { startPage },\n {\n onStart: () => ctx.dispatch({ type: 'START', query }),\n onSlice: (hits, scanned, total) => ctx.dispatch({ type: 'APPEND', hits, scanned, total }),\n isCancelled: () => gen !== generation,\n setInflight: (pending) => {\n if (gen === generation) inflight = pending;\n },\n },\n );\n if (complete && gen === generation) ctx.dispatch({ type: 'COMPLETE' });\n } catch (err) {\n if (gen !== generation) return;\n ctx.dispatch({ type: 'ERROR', message: err instanceof Error ? err.message : String(err) });\n }\n }\n\n function goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null {\n const { hits } = ctx.getState();\n if (hits.length === 0) return null;\n const wrapped = ((index % hits.length) + hits.length) % hits.length;\n ctx.dispatch({ type: 'SET_ACTIVE', index: wrapped });\n const hit = hits[wrapped];\n // Positioned reveal: the HIT (not just its page) arrives at the anchor —\n // per-call override > plugin config > find-bar default. Zoom never changes.\n const arrival = { ...DEFAULT_REVEAL, ...config.reveal, ...reveal };\n ctx.tryGet(StageToken)?.reveal(hit.pageIndex, {\n rect: hit.bounds,\n anchor: arrival.anchor,\n behavior: arrival.behavior,\n });\n return hit;\n }\n\n const clear = () => {\n generation++;\n inflight?.abort('cleared');\n inflight = null;\n ctx.dispatch({ type: 'CLEAR' });\n };\n\n return {\n // An empty text is \"stop searching\", not a query — identical to clear().\n search: (query, exec) =>\n query.text.length === 0 ? clear() : void runSession(query, exec?.startPage),\n\n // Replay the stored query verbatim; the rescan starts viewport-first.\n rerun: () => {\n const { query, status } = ctx.getState();\n if (query && status !== 'idle') void runSession(query);\n },\n\n clear,\n\n next: () => goTo(ctx.getState().activeIndex + 1),\n prev: () => goTo(ctx.getState().activeIndex - 1),\n goTo,\n\n query: () => ctx.getState().query,\n\n findAll: async (query, opts = {}) => {\n if (!ctx.doc) return [];\n const all: SearchHit[] = [];\n const signal = opts.signal;\n // Independent lifecycle: its own abort handle, never the session's\n // generation counter — a findAll can neither supersede the user's\n // visible search nor be superseded by it.\n let pending: { abort(reason?: unknown): void } | null = null;\n const onAbort = () => pending?.abort(signal?.reason);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n await collect(\n query,\n { mode: opts.mode },\n {\n onStart: () => {\n all.length = 0;\n },\n onSlice: (hits) => all.push(...hits),\n isCancelled: () => signal?.aborted ?? false,\n setInflight: (p) => {\n pending = p;\n },\n },\n );\n if (signal?.aborted) throw signal.reason ?? new Error('findAll aborted');\n return all;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n },\n\n status: () => ctx.getState().status,\n hits: () => ctx.getState().hits,\n hitCount: () => ctx.getState().hits.length,\n activeIndex: () => ctx.getState().activeIndex,\n activeHit: () => {\n const { hits, activeIndex } = ctx.getState();\n return activeIndex >= 0 && activeIndex < hits.length ? hits[activeIndex] : null;\n },\n hitsForPage: (pon) => {\n const { hits, hitsByPage } = ctx.getState();\n const indices = hitsByPage[pon];\n return indices?.length ? indices.map((i) => hits[i]) : EMPTY;\n },\n progress: () => ctx.getState().progress,\n errorMessage: () => ctx.getState().error,\n };\n}\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Rect, TextQuad } from '@embedpdf/core-geometry';\nimport type { RevealAnchor, ScrollBehaviorKind } from '@embedpdf/plugin-stage';\nimport type { SearchMode, SearchQuery, SearchSnippet } from '@embedpdf/engine-core/runtime';\n\n/**\n * One match, viewer-shaped: the engine's PDF-space line rects converted to\n * CONTENT space (y-down — the same space selection rects live in), plus the\n * page's display index so navigation can `reveal()` without a lookup.\n * `charStart`/`charCount` keep the engine's text-page offsets, so a hit can\n * later seed a selection or a highlight annotation without re-searching.\n */\n/**\n * One merged visual line of a match, in content space — the structural twin\n * of selection's `SelectionSegment` and the engine's `PdfTextSegment`.\n * `quad` is the geometric authority (corner-named, frame-geometric); `rect`\n * its AABB; `advance` the reading direction along the baseline.\n */\nexport interface TextSegment {\n quad: TextQuad;\n rect: Rect;\n advance: 1 | -1;\n}\n\nexport interface SearchHit {\n pon: PageObjectNumber;\n pageIndex: number;\n charStart: number;\n charCount: number;\n /** Canonical visual-line segments (content space) — the drawing input. */\n segments: TextSegment[];\n /** Union envelope of the segments — the scroll/reveal target. Absent when\n * a match carries no drawable geometry (degenerate glyphs only). */\n bounds?: Rect;\n snippet?: SearchSnippet;\n}\n\n/**\n * `searching` means the slice loop is still walking pages — results are\n * usable the moment they appear (matches stream in viewport-first order).\n */\nexport type SearchStatus = 'idle' | 'searching' | 'complete' | 'error';\n\n/**\n * Execution options for the session `search()` — HOW the scan runs, never\n * part of what the query MEANS (which is why they're not on `SearchQuery`\n * and are never stored or restored into a search box).\n */\nexport interface SearchExecOptions {\n /** Scan origin override. Defaults to the Stage's current page (viewport-first). */\n startPage?: PageObjectNumber;\n}\n\n/** Options for the session-free `findAll()` service. */\nexport interface SearchFindAllOptions {\n /** Abort the scan; the returned promise rejects with `signal.reason`. */\n signal?: AbortSignal;\n /**\n * Pin the slice mode. Default: `'full'` with automatic `'rects'`\n * fallback when snippets are denied — pass `'rects'` when you only need\n * geometry (e.g. redact-by-term) to skip snippet extraction entirely.\n */\n mode?: SearchMode;\n}\n\nexport interface SearchState {\n /**\n * The query in flight/completed, null when idle/cleared — THE stored\n * form of the search intent, the same flat `SearchQuery` the engine\n * matches on, the wire caches by, and a search box renders. Survives\n * any search UI unmounting (results are document-scoped; so is this).\n */\n query: SearchQuery | null;\n status: SearchStatus;\n hits: SearchHit[];\n /** pon → indices into `hits` — the per-page layer's read model. */\n hitsByPage: Record<number, number[]>;\n /** Index into `hits`, -1 = none. */\n activeIndex: number;\n progress: { scanned: number; total: number };\n error: string | null;\n}\n\nexport type SearchAction =\n | { type: 'START'; query: SearchQuery }\n | { type: 'APPEND'; hits: SearchHit[]; scanned: number; total: number }\n | { type: 'COMPLETE' }\n | { type: 'ERROR'; message: string }\n | { type: 'SET_ACTIVE'; index: number }\n | { type: 'CLEAR' };\n\n/**\n * How hit navigation arrives (forwarded to the Stage's positioned reveal).\n * The default is the browser find-bar feel: the hit lands at the top-middle\n * of the viewport (`anchor: { y: 0.35 }`), horizontal only moves when the\n * hit is off-screen, zoom never changes, smooth tween.\n */\nexport interface SearchRevealOptions {\n anchor?: RevealAnchor;\n behavior?: ScrollBehaviorKind;\n}\n\nexport interface SearchPluginConfig {\n /** Arrival defaults for `next()`/`prev()`/`goTo()`. */\n reveal?: SearchRevealOptions;\n}\n\n/**\n * The search plugin is a find SERVICE plus one user-visible search SESSION\n * per document. `findAll` is the service; `search` is the session; the\n * sidebar, the highlights layer, and next/prev render the session.\n */\nexport interface SearchCapability {\n // ── the session: THE user-visible find, one per document ────────────────\n /**\n * Start a new search (supersedes and aborts any running one). Results\n * stream into state slice by slice; the first hit becomes active but the\n * camera does NOT move until `next()`/`prev()`/`goTo()`. An empty\n * `query.text` is \"stop searching\" — identical to `clear()`.\n */\n search(query: SearchQuery, exec?: SearchExecOptions): void;\n /** Re-run the current query from scratch (used after document mutations). */\n rerun(): void;\n clear(): void;\n\n /** Advance to the next/previous hit (wraps) and reveal it (see {@link SearchRevealOptions}). */\n next(): SearchHit | null;\n prev(): SearchHit | null;\n /** Jump to a specific hit index and reveal it; `reveal` overrides the plugin defaults. */\n goTo(index: number, reveal?: SearchRevealOptions): SearchHit | null;\n\n /** The session's query — what a search box renders. Null when idle. */\n query(): SearchQuery | null;\n status(): SearchStatus;\n hits(): SearchHit[];\n hitCount(): number;\n activeIndex(): number;\n activeHit(): SearchHit | null;\n /** Hits on one page — the layer's input. Element identity is stable. */\n hitsForPage(pon: PageObjectNumber): SearchHit[];\n progress(): { scanned: number; total: number };\n errorMessage(): string | null;\n\n // ── the mechanism: session-free find service ─────────────────────────────\n /**\n * Run a query to completion and return every hit. Touches NO state — no\n * sidebar, no highlights, no camera, no activeIndex; the user's visible\n * search is never superseded. The primitive other features build on\n * (redact-by-term, link detection, occurrence badges). Scans in natural\n * page order; concurrent calls are independent.\n */\n findAll(query: SearchQuery, opts?: SearchFindAllOptions): Promise<SearchHit[]>;\n}\n\nexport const SearchToken = createCapabilityToken<SearchCapability>('search');\n","/**\n * Keeps results honest across document mutations. Every confirmed\n * mutation (own or remote) bumps the engine's mutation sequence, which\n * invalidates search cursors AND can change what's findable (redaction,\n * page delete, import). The correct reaction is always the same: re-run\n * the current query from scratch. Reruns are coalesced — a burst of\n * events (multi-part mutation, SSE catch-up) triggers one rescan.\n */\nimport type { EffectContext } from '@embedpdf/core';\n\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchState } from './types';\n\nconst RERUN_DELAY_MS = 250;\n\nexport function registerSearchEffects(ctx: EffectContext<SearchState, SearchAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n const unsubscribe = doc.events.subscribe(() => {\n if (ctx.getState().status === 'idle') return;\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n ctx.get(SearchToken).rerun();\n }, RERUN_DELAY_MS);\n });\n\n ctx.cleanup(() => {\n if (timer !== null) clearTimeout(timer);\n unsubscribe();\n });\n}\n","import type { SearchAction, SearchState } from './types';\n\nexport const initialSearchState = (): SearchState => ({\n query: null,\n status: 'idle',\n hits: [],\n hitsByPage: {},\n activeIndex: -1,\n progress: { scanned: 0, total: 0 },\n error: null,\n});\n\nexport const searchReducer = (state: SearchState, action: SearchAction): SearchState => {\n switch (action.type) {\n case 'START':\n return { ...initialSearchState(), query: action.query, status: 'searching' };\n case 'APPEND': {\n if (action.hits.length === 0) {\n return { ...state, progress: { scanned: action.scanned, total: action.total } };\n }\n const hits = state.hits.concat(action.hits);\n const hitsByPage = { ...state.hitsByPage };\n for (let i = state.hits.length; i < hits.length; i++) {\n const pon = hits[i].pon;\n hitsByPage[pon] = (hitsByPage[pon] ?? []).concat(i);\n }\n return {\n ...state,\n hits,\n hitsByPage,\n // The first hit becomes active (so \"1/N\" reads right) — navigation,\n // not the stream, moves the camera.\n activeIndex: state.activeIndex === -1 ? 0 : state.activeIndex,\n progress: { scanned: action.scanned, total: action.total },\n };\n }\n case 'COMPLETE':\n return { ...state, status: 'complete' };\n case 'ERROR':\n return { ...state, status: 'error', error: action.message };\n case 'SET_ACTIVE':\n return { ...state, activeIndex: action.index };\n case 'CLEAR':\n return initialSearchState();\n default:\n return state;\n }\n};\n","import { definePlugin } from '@embedpdf/core';\nimport { StageToken } from '@embedpdf/plugin-stage';\n\nimport { createSearchCapability } from './capability';\nimport { registerSearchEffects } from './effects';\nimport { initialSearchState, searchReducer } from './reducer';\nimport { SearchToken } from './types';\nimport type { SearchAction, SearchCapability, SearchPluginConfig, SearchState } from './types';\n\n/**\n * Document text search over `doc.search` — the engine's budgeted,\n * cursor-resumable slices. Document-scoped; no pointer handling, so it\n * needs no interaction hub. The Stage is optional: when present, scans\n * start at the current page (viewport-first) and hit navigation reveals\n * the hit's page; without it the host owns scrolling.\n */\nexport const searchPlugin = (config?: SearchPluginConfig) =>\n definePlugin<SearchState, SearchAction, SearchCapability>({\n id: 'search',\n token: SearchToken,\n scope: 'document',\n optional: [StageToken],\n initialState: initialSearchState,\n reduce: searchReducer,\n capability: (ctx) => createSearchCapability(ctx, config),\n effects: registerSearchEffects,\n });\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embedpdf/plugin-search",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Document text search: drives the engine's budgeted search slices, converts match rects to content space, and integrates with the Stage for viewport-first scanning and reveal-on-navigate.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@embedpdf/core-geometry": "3.0.0-next.
|
|
22
|
-
"@embedpdf/core": "3.0.0-next.
|
|
23
|
-
"@embedpdf/plugin-stage": "3.0.0-next.
|
|
24
|
-
"@embedpdf/
|
|
21
|
+
"@embedpdf/core-geometry": "3.0.0-next.4",
|
|
22
|
+
"@embedpdf/engine-core": "3.0.0-next.4",
|
|
23
|
+
"@embedpdf/plugin-stage": "3.0.0-next.4",
|
|
24
|
+
"@embedpdf/core": "3.0.0-next.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"typescript": "^5.0.0",
|