@anaralabs/lector 3.14.10 → 3.14.12

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.js CHANGED
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useState, useCallback, useContext, useLayoutEffect, createRef } from 'react';
3
+ import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useState, useCallback, useContext, useLayoutEffect, createRef, useSyncExternalStore, useImperativeHandle } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { useFloating, autoUpdate, offset, flip, shift, useDismiss, useInteractions, inline } from '@floating-ui/react';
6
6
  import { useStore, createStore } from 'zustand';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+ import { useShallow } from 'zustand/react/shallow';
8
9
  import { useDebounce } from 'use-debounce';
9
10
  import { v4 } from 'uuid';
10
11
  import { Slot } from '@radix-ui/react-slot';
@@ -265,12 +266,12 @@ var PDFStore = createZustandContext(
265
266
  updateZoom: (zoom, isZoomFitWidth = false) => {
266
267
  const { minZoom, maxZoom } = get().zoomOptions;
267
268
  set((state) => {
268
- if (typeof zoom === "function") {
269
- const newZoom2 = clamp(zoom(state.zoom), minZoom, maxZoom);
270
- return { zoom: newZoom2, isZoomFitWidth };
271
- }
272
- const newZoom = clamp(zoom, minZoom, maxZoom);
273
- return { zoom: newZoom, isZoomFitWidth };
269
+ const newZoom = clamp(
270
+ typeof zoom === "function" ? zoom(state.zoom) : zoom,
271
+ minZoom,
272
+ maxZoom
273
+ );
274
+ return newZoom === state.zoom && isZoomFitWidth === state.isZoomFitWidth ? state : { zoom: newZoom, isZoomFitWidth };
274
275
  });
275
276
  },
276
277
  zoomFitWidth: () => {
@@ -281,23 +282,20 @@ var PDFStore = createZustandContext(
281
282
  viewports,
282
283
  zoomOptions
283
284
  );
284
- set({
285
- zoom: clampedZoom,
286
- isZoomFitWidth: true
287
- });
285
+ get().updateZoom(clampedZoom, true);
288
286
  return clampedZoom;
289
287
  },
290
288
  currentPage: 1,
291
289
  setCurrentPage: (val) => {
292
- set({
293
- currentPage: val
294
- });
290
+ set(
291
+ (state) => state.currentPage === val ? state : { currentPage: val }
292
+ );
295
293
  },
296
294
  isPinching: false,
297
295
  setIsPinching: (val) => {
298
- set({
299
- isPinching: val
300
- });
296
+ set(
297
+ (state) => state.isPinching === val ? state : { isPinching: val }
298
+ );
301
299
  },
302
300
  isResizing: false,
303
301
  setIsResizing: (val) => {
@@ -307,9 +305,9 @@ var PDFStore = createZustandContext(
307
305
  },
308
306
  virtualizer: null,
309
307
  setVirtualizer: (val) => {
310
- set({
311
- virtualizer: val
312
- });
308
+ set(
309
+ (state) => state.virtualizer === val ? state : { virtualizer: val }
310
+ );
313
311
  },
314
312
  pageProxies: initialState.pageProxies,
315
313
  getPdfPageProxy: (pageNumber) => {
@@ -319,21 +317,21 @@ var PDFStore = createZustandContext(
319
317
  },
320
318
  textContent: [],
321
319
  setTextContent: (val) => {
322
- set({
323
- textContent: val
324
- });
320
+ set(
321
+ (state) => state.textContent === val ? state : { textContent: val }
322
+ );
325
323
  },
326
324
  highlights: [],
327
325
  setHighlight: (val) => {
328
- set({
329
- highlights: val
330
- });
326
+ set(
327
+ (state) => state.highlights === val ? state : { highlights: val }
328
+ );
331
329
  },
332
330
  customSelectionRects: [],
333
331
  setCustomSelectionRects: (val) => {
334
- set({
335
- customSelectionRects: val
336
- });
332
+ set(
333
+ (state) => state.customSelectionRects === val ? state : { customSelectionRects: val }
334
+ );
337
335
  },
338
336
  coloredHighlights: [],
339
337
  addColoredHighlight: (value) => set((prevState) => ({
@@ -700,10 +698,43 @@ var AnnotationsStoreProvider = ({
700
698
  { initialValue: void 0 },
701
699
  children
702
700
  );
703
- var useAnnotations = () => {
701
+ var identity = (state) => state;
702
+ function useAnnotations(selector) {
704
703
  const ctx = AnnotationsStore.useContext();
705
- return useStore(ctx ?? getFallbackStore());
706
- };
704
+ return useStore(
705
+ ctx ?? getFallbackStore(),
706
+ selector ?? identity
707
+ );
708
+ }
709
+ var emptyAnnotations = [];
710
+ var indexes = /* @__PURE__ */ new WeakMap();
711
+ function annotationsByPage(annotations) {
712
+ let index = indexes.get(annotations);
713
+ if (index) return index;
714
+ index = /* @__PURE__ */ new Map();
715
+ for (const annotation of annotations) {
716
+ const pages = /* @__PURE__ */ new Set([annotation.pageNumber]);
717
+ for (const rect of annotation.highlights) pages.add(rect.pageNumber);
718
+ for (const rect of annotation.underlines ?? []) pages.add(rect.pageNumber);
719
+ for (const page of pages) {
720
+ let values = index.get(page);
721
+ if (!values) {
722
+ values = [];
723
+ index.set(page, values);
724
+ }
725
+ values.push(annotation);
726
+ }
727
+ }
728
+ indexes.set(annotations, index);
729
+ return index;
730
+ }
731
+ function usePageAnnotations(pageNumber) {
732
+ return useAnnotations(
733
+ useShallow(
734
+ (state) => annotationsByPage(state.annotations).get(pageNumber) ?? emptyAnnotations
735
+ )
736
+ );
737
+ }
707
738
  var PDFPageNumberContext = createContext(0);
708
739
  var usePDFPageNumber = () => {
709
740
  return useContext(PDFPageNumberContext);
@@ -725,15 +756,9 @@ var AnnotationHighlightLayer = ({
725
756
  onAnnotationTooltipClose,
726
757
  hoverTooltipClassName
727
758
  }) => {
728
- const { annotations } = useAnnotations();
729
759
  const pageNumber = usePDFPageNumber();
730
760
  const isPageRendered = usePdf((state) => !!state.renderedPages[pageNumber]);
731
- const pageAnnotations = useMemo(
732
- () => annotations.filter(
733
- (a) => a.pageNumber === pageNumber || a.highlights.some((h) => h.pageNumber === pageNumber) || a.underlines?.some((u) => u.pageNumber === pageNumber)
734
- ),
735
- [annotations, pageNumber]
736
- );
761
+ const pageAnnotations = usePageAnnotations(pageNumber);
737
762
  if (!isPageRendered) return null;
738
763
  const getCommentIconPosition = (highlights) => {
739
764
  if (!highlights.length) return { top: 0, right: 10 };
@@ -1220,20 +1245,42 @@ var PDFLinkServiceContext = createContext(defaultLinkService);
1220
1245
  var usePDFLinkService = () => {
1221
1246
  return useContext(PDFLinkServiceContext);
1222
1247
  };
1248
+ var targets = /* @__PURE__ */ new Map();
1249
+ var observer;
1223
1250
  var useVisibility = ({
1224
1251
  elementRef
1225
1252
  }) => {
1226
1253
  const [visible, setVisible] = useState(false);
1227
1254
  useEffect(() => {
1228
- if (!elementRef.current) {
1229
- return;
1230
- }
1231
- const observer = new IntersectionObserver(([entry]) => {
1232
- setVisible(entry?.isIntersecting ?? false);
1255
+ const element = elementRef.current;
1256
+ if (!element) return;
1257
+ observer ??= new IntersectionObserver((entries) => {
1258
+ for (const entry of entries) {
1259
+ const target2 = targets.get(entry.target);
1260
+ if (!target2) continue;
1261
+ target2.visible = entry.isIntersecting;
1262
+ for (const listener of target2.listeners) listener(entry.isIntersecting);
1263
+ }
1233
1264
  });
1234
- observer.observe(elementRef.current);
1265
+ let target = targets.get(element);
1266
+ if (!target) {
1267
+ target = { listeners: /* @__PURE__ */ new Set(), visible: false };
1268
+ targets.set(element, target);
1269
+ observer.observe(element);
1270
+ }
1271
+ const { listeners: listeners2 } = target;
1272
+ listeners2.add(setVisible);
1273
+ setVisible(target.visible);
1235
1274
  return () => {
1236
- observer.disconnect();
1275
+ listeners2.delete(setVisible);
1276
+ if (listeners2.size === 0) {
1277
+ observer?.unobserve(element);
1278
+ targets.delete(element);
1279
+ }
1280
+ if (targets.size === 0) {
1281
+ observer?.disconnect();
1282
+ observer = void 0;
1283
+ }
1237
1284
  };
1238
1285
  }, [elementRef]);
1239
1286
  return { visible };
@@ -2061,52 +2108,41 @@ function applyContextRecolor(ctx, map, options) {
2061
2108
  function removeContextRecolor(ctx) {
2062
2109
  ctx[RECOLOR_CLEANUP]?.();
2063
2110
  }
2064
- var DPR_CAP = 3;
2065
- var readDpr = () => typeof window === "undefined" ? 1 : Math.min(window.devicePixelRatio || 1, DPR_CAP);
2066
- var useDpr = () => {
2067
- const [dpr, setDpr] = useState(readDpr);
2068
- useEffect(() => {
2069
- if (typeof window === "undefined") {
2070
- return;
2111
+ var listeners = /* @__PURE__ */ new Set();
2112
+ var media;
2113
+ var getSnapshot = () => Math.min(window.devicePixelRatio || 1, 3);
2114
+ var getServerSnapshot = () => 1;
2115
+ function unlisten() {
2116
+ if (!media) return;
2117
+ if (typeof media.removeEventListener === "function")
2118
+ media.removeEventListener("change", onChange);
2119
+ else media.removeListener(onChange);
2120
+ }
2121
+ function watchResolution() {
2122
+ unlisten();
2123
+ media = window.matchMedia(
2124
+ `(resolution: ${window.devicePixelRatio || 1}dppx), (-webkit-device-pixel-ratio: ${window.devicePixelRatio || 1})`
2125
+ );
2126
+ if (typeof media.addEventListener === "function")
2127
+ media.addEventListener("change", onChange);
2128
+ else media.addListener(onChange);
2129
+ }
2130
+ function onChange() {
2131
+ watchResolution();
2132
+ for (const listener of listeners) listener();
2133
+ }
2134
+ function subscribe(listener) {
2135
+ listeners.add(listener);
2136
+ if (listeners.size === 1) watchResolution();
2137
+ return () => {
2138
+ listeners.delete(listener);
2139
+ if (listeners.size === 0) {
2140
+ unlisten();
2141
+ media = void 0;
2071
2142
  }
2072
- let mediaQuery = null;
2073
- let cancelled = false;
2074
- const handleChange = () => {
2075
- if (cancelled) {
2076
- return;
2077
- }
2078
- setDpr(readDpr());
2079
- subscribe();
2080
- };
2081
- const unlisten = (mql) => {
2082
- if (!mql) return;
2083
- if (typeof mql.removeEventListener === "function") {
2084
- mql.removeEventListener("change", handleChange);
2085
- } else {
2086
- mql.removeListener(handleChange);
2087
- }
2088
- };
2089
- const subscribe = () => {
2090
- unlisten(mediaQuery);
2091
- const ratio = window.devicePixelRatio || 1;
2092
- mediaQuery = window.matchMedia(
2093
- `(resolution: ${ratio}dppx), (-webkit-device-pixel-ratio: ${ratio})`
2094
- );
2095
- if (typeof mediaQuery.addEventListener === "function") {
2096
- mediaQuery.addEventListener("change", handleChange, { once: true });
2097
- } else {
2098
- mediaQuery.addListener(handleChange);
2099
- }
2100
- };
2101
- setDpr(readDpr());
2102
- subscribe();
2103
- return () => {
2104
- cancelled = true;
2105
- unlisten(mediaQuery);
2106
- };
2107
- }, []);
2108
- return dpr;
2109
- };
2143
+ };
2144
+ }
2145
+ var useDpr = () => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
2110
2146
 
2111
2147
  // src/hooks/layers/useCanvasLayer.tsx
2112
2148
  var CACHE_MAX_ENTRIES = 60;
@@ -2858,9 +2894,7 @@ var CanvasLayer = memo(
2858
2894
  );
2859
2895
  CanvasLayer.displayName = "CanvasLayer";
2860
2896
 
2861
- // src/hooks/useSelectionDimensions.tsx
2862
- var MERGE_THRESHOLD = 2;
2863
- var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
2897
+ // src/lib/selection-rects.ts
2864
2898
  var getTextNodeClientRects = (range) => {
2865
2899
  const root = range.commonAncestorContainer;
2866
2900
  const ownerDoc = root.ownerDocument ?? document;
@@ -2912,6 +2946,39 @@ var getTextNodeClientRects = (range) => {
2912
2946
  }
2913
2947
  return results;
2914
2948
  };
2949
+ var connected = (a, b) => {
2950
+ const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
2951
+ const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
2952
+ return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
2953
+ };
2954
+ var mergeTextRuns = (rects) => {
2955
+ const groups = [];
2956
+ for (const rect of rects) {
2957
+ const group = [rect];
2958
+ for (let i = groups.length - 1; i >= 0; i--) {
2959
+ if (groups[i].some((member) => connected(member, rect))) {
2960
+ group.push(...groups[i]);
2961
+ groups.splice(i, 1);
2962
+ }
2963
+ }
2964
+ groups.push(group);
2965
+ }
2966
+ return groups.map((group) => {
2967
+ const left = Math.min(...group.map((rect) => rect.left));
2968
+ const top = Math.min(...group.map((rect) => rect.top));
2969
+ return {
2970
+ ...group[0],
2971
+ left,
2972
+ top,
2973
+ width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
2974
+ height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
2975
+ };
2976
+ });
2977
+ };
2978
+
2979
+ // src/hooks/useSelectionDimensions.tsx
2980
+ var MERGE_THRESHOLD = 2;
2981
+ var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
2915
2982
  var mapSelectionRectsToLayers = (range) => {
2916
2983
  const clientRects = getTextNodeClientRects(range).filter(
2917
2984
  ({ rect }) => rect.width > 2 && rect.height > 2
@@ -2954,122 +3021,6 @@ var mapSelectionRectsToLayers = (range) => {
2954
3021
  }
2955
3022
  return result;
2956
3023
  };
2957
- var shouldMergeRects = (rect1, rect2) => {
2958
- const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
2959
- const horizontallyConnected = Math.abs(rect1.left + rect1.width - rect2.left) <= MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) <= MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
2960
- return verticalOverlap && horizontallyConnected;
2961
- };
2962
- var consolidateHighlightRects = (rects) => {
2963
- if (rects.length <= 1) return rects;
2964
- const consolidated = [];
2965
- const sorted = [...rects].sort((a, b) => {
2966
- const pageCompare = a.pageNumber - b.pageNumber;
2967
- if (pageCompare !== 0) return pageCompare;
2968
- const topDiff = a.top - b.top;
2969
- return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff;
2970
- });
2971
- let current = sorted[0];
2972
- if (!current) return rects;
2973
- for (let i = 1; i < sorted.length; i++) {
2974
- const next = sorted[i];
2975
- if (!next) continue;
2976
- const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < Math.max(current.height, next.height) * 0.5;
2977
- const horizontallyConnected = samePageAndLine && // Adjacent (touching or very close)
2978
- (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || Math.abs(next.left + next.width - current.left) <= MERGE_THRESHOLD || // Overlapping
2979
- current.left < next.left + next.width && next.left < current.left + current.width || // Very close (small gap)
2980
- Math.abs(current.left + current.width - next.left) <= current.height * 0.2);
2981
- if (horizontallyConnected) {
2982
- const newLeft = Math.min(current.left, next.left);
2983
- const newRight = Math.max(
2984
- current.left + current.width,
2985
- next.left + next.width
2986
- );
2987
- const newTop = Math.min(current.top, next.top);
2988
- const newBottom = Math.max(
2989
- current.top + current.height,
2990
- next.top + next.height
2991
- );
2992
- current = {
2993
- left: newLeft,
2994
- top: newTop,
2995
- width: newRight - newLeft,
2996
- height: newBottom - newTop,
2997
- pageNumber: current.pageNumber
2998
- };
2999
- } else {
3000
- consolidated.push(current);
3001
- current = next;
3002
- }
3003
- }
3004
- if (current) {
3005
- consolidated.push(current);
3006
- }
3007
- return consolidated;
3008
- };
3009
- var consolidateRects = (rects) => {
3010
- if (rects.length <= 1) return rects;
3011
- const result = [];
3012
- const visited = /* @__PURE__ */ new Set();
3013
- for (let i = 0; i < rects.length; i++) {
3014
- if (visited.has(i)) continue;
3015
- const currentRect = rects[i];
3016
- if (!currentRect) continue;
3017
- const currentGroup = [currentRect];
3018
- visited.add(i);
3019
- let foundNew = true;
3020
- while (foundNew) {
3021
- foundNew = false;
3022
- for (let j = 0; j < rects.length; j++) {
3023
- if (visited.has(j)) continue;
3024
- const candidateRect = rects[j];
3025
- if (!candidateRect) continue;
3026
- const shouldMergeWithGroup = currentGroup.some(
3027
- (groupRect) => doRectsOverlap(groupRect, candidateRect)
3028
- );
3029
- if (shouldMergeWithGroup) {
3030
- currentGroup.push(candidateRect);
3031
- visited.add(j);
3032
- foundNew = true;
3033
- }
3034
- }
3035
- }
3036
- result.push(mergeRectGroup(currentGroup));
3037
- }
3038
- return result;
3039
- };
3040
- var doRectsOverlap = (rect1, rect2) => {
3041
- const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
3042
- const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height;
3043
- const closeEnough = shouldMergeRects(rect1, rect2);
3044
- return horizontalOverlap && verticalOverlap || closeEnough;
3045
- };
3046
- var mergeRectGroup = (rects) => {
3047
- if (rects.length === 1) {
3048
- const rect = rects[0];
3049
- if (!rect) throw new Error("Invalid rect in group");
3050
- return rect;
3051
- }
3052
- const firstRect = rects[0];
3053
- if (!firstRect) throw new Error("Invalid first rect in group");
3054
- let minLeft = firstRect.left;
3055
- let minTop = firstRect.top;
3056
- let maxRight = firstRect.left + firstRect.width;
3057
- let maxBottom = firstRect.top + firstRect.height;
3058
- rects.forEach((rect) => {
3059
- if (!rect) return;
3060
- minLeft = Math.min(minLeft, rect.left);
3061
- minTop = Math.min(minTop, rect.top);
3062
- maxRight = Math.max(maxRight, rect.left + rect.width);
3063
- maxBottom = Math.max(maxBottom, rect.top + rect.height);
3064
- });
3065
- return {
3066
- left: minLeft,
3067
- top: minTop,
3068
- width: maxRight - minLeft,
3069
- height: maxBottom - minTop,
3070
- pageNumber: firstRect.pageNumber
3071
- };
3072
- };
3073
3024
  var useSelectionDimensions = () => {
3074
3025
  const store = PDFStore.useContext();
3075
3026
  const getAnnotationDimension = () => {
@@ -3142,14 +3093,7 @@ var useSelectionDimensions = () => {
3142
3093
  }
3143
3094
  });
3144
3095
  textLayerMapHighlight.forEach((rects) => {
3145
- if (rects.length > 0) {
3146
- if (rects.length === 1) {
3147
- highlightRects.push(...rects);
3148
- } else {
3149
- const consolidated = consolidateHighlightRects(rects);
3150
- highlightRects.push(...consolidated);
3151
- }
3152
- }
3096
+ highlightRects.push(...mergeTextRuns(rects));
3153
3097
  });
3154
3098
  textLayerMapUnderline.forEach((rects) => {
3155
3099
  if (rects.length > 0) {
@@ -3296,7 +3240,7 @@ var useSelectionDimensions = () => {
3296
3240
  });
3297
3241
  textLayerMap.forEach((rects) => {
3298
3242
  if (rects.length > 0) {
3299
- const consolidated = consolidateRects(rects);
3243
+ const consolidated = mergeTextRuns(rects);
3300
3244
  highlights.push(...consolidated);
3301
3245
  }
3302
3246
  });
@@ -3741,13 +3685,110 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
3741
3685
  }) });
3742
3686
  });
3743
3687
  HighlightLayer.displayName = "HighlightLayer";
3688
+
3689
+ // src/lib/selection-background.ts
3690
+ var SVG_NS = "http://www.w3.org/2000/svg";
3691
+ var createSelectionBackground = () => {
3692
+ const overlays = /* @__PURE__ */ new Map();
3693
+ let style;
3694
+ const clear = (layer) => {
3695
+ overlays.get(layer)?.remove();
3696
+ overlays.delete(layer);
3697
+ layer.removeAttribute("data-lector-selection-active");
3698
+ if (overlays.size === 0) {
3699
+ style?.remove();
3700
+ style = void 0;
3701
+ }
3702
+ };
3703
+ const update = (selection, layers) => {
3704
+ const registered = new Set(layers);
3705
+ const byLayer = /* @__PURE__ */ new Map();
3706
+ if (selection && !selection.isCollapsed) {
3707
+ for (let i = 0; i < selection.rangeCount; i++) {
3708
+ for (const { rect, element } of getTextNodeClientRects(
3709
+ selection.getRangeAt(i)
3710
+ )) {
3711
+ const layer = element?.closest(".textLayer");
3712
+ if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
3713
+ continue;
3714
+ let entry = byLayer.get(layer);
3715
+ if (!entry) {
3716
+ entry = { rects: [], element };
3717
+ byLayer.set(layer, entry);
3718
+ }
3719
+ entry.rects.push(rect);
3720
+ }
3721
+ }
3722
+ }
3723
+ for (const layer of registered) {
3724
+ const entry = byLayer.get(layer);
3725
+ if (!entry) {
3726
+ clear(layer);
3727
+ continue;
3728
+ }
3729
+ layer.removeAttribute("data-lector-selection-active");
3730
+ const color = getComputedStyle(
3731
+ entry.element,
3732
+ "::selection"
3733
+ ).backgroundColor;
3734
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
3735
+ clear(layer);
3736
+ continue;
3737
+ }
3738
+ const bounds = layer.getBoundingClientRect();
3739
+ if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
3740
+ clear(layer);
3741
+ continue;
3742
+ }
3743
+ const scaleX = bounds.width / layer.clientWidth;
3744
+ const scaleY = bounds.height / layer.clientHeight;
3745
+ const rects = mergeTextRuns(
3746
+ entry.rects.map((rect) => ({
3747
+ left: (rect.left - bounds.left) / scaleX,
3748
+ top: (rect.top - bounds.top) / scaleY,
3749
+ width: rect.width / scaleX,
3750
+ height: rect.height / scaleY
3751
+ }))
3752
+ );
3753
+ let overlay = overlays.get(layer);
3754
+ if (!overlay) {
3755
+ overlay = document.createElementNS(SVG_NS, "svg");
3756
+ overlay.dataset.lectorSelection = "";
3757
+ overlay.setAttribute("aria-hidden", "true");
3758
+ overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
3759
+ overlay.append(document.createElementNS(SVG_NS, "path"));
3760
+ layer.append(overlay);
3761
+ overlays.set(layer, overlay);
3762
+ }
3763
+ const path = overlay.firstElementChild;
3764
+ path.setAttribute(
3765
+ "d",
3766
+ rects.map(
3767
+ (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
3768
+ ).join(" ")
3769
+ );
3770
+ path.setAttribute("fill", color);
3771
+ if (!style) {
3772
+ style = document.createElement("style");
3773
+ style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
3774
+ document.head.append(style);
3775
+ }
3776
+ layer.setAttribute("data-lector-selection-active", "");
3777
+ }
3778
+ };
3779
+ return { update, clear };
3780
+ };
3781
+
3782
+ // src/hooks/layers/useTextLayer.tsx
3744
3783
  var createTextSelectionManager = () => {
3745
3784
  const textLayers = /* @__PURE__ */ new Map();
3785
+ const selectionBackground = createSelectionBackground();
3746
3786
  let selectionChangeAbortController = null;
3747
3787
  let isPointerDown = false;
3748
3788
  let prevRange = null;
3749
3789
  let isFirefox;
3750
3790
  const removeGlobalSelectionListener = (textLayerDiv) => {
3791
+ selectionBackground.clear(textLayerDiv);
3751
3792
  textLayers.delete(textLayerDiv);
3752
3793
  if (textLayers.size === 0) {
3753
3794
  selectionChangeAbortController?.abort();
@@ -3804,6 +3845,7 @@ var createTextSelectionManager = () => {
3804
3845
  "selectionchange",
3805
3846
  () => {
3806
3847
  const selection = document.getSelection();
3848
+ selectionBackground.update(selection, textLayers.keys());
3807
3849
  if (!selection || selection.rangeCount === 0) {
3808
3850
  textLayers.forEach(reset);
3809
3851
  return;
@@ -4492,9 +4534,9 @@ var useVisiblePage = ({
4492
4534
  window.addEventListener("resize", measure);
4493
4535
  return () => window.removeEventListener("resize", measure);
4494
4536
  }
4495
- const observer = new ResizeObserver(measure);
4496
- observer.observe(scrollElement);
4497
- return () => observer.disconnect();
4537
+ const observer2 = new ResizeObserver(measure);
4538
+ observer2.observe(scrollElement);
4539
+ return () => observer2.disconnect();
4498
4540
  }, [scrollElement]);
4499
4541
  const calculateVisiblePageIndex = useCallback(
4500
4542
  (virtualItems) => {
@@ -5051,7 +5093,7 @@ var CoordinatesEngine = class extends Engine {
5051
5093
  }
5052
5094
  }
5053
5095
  };
5054
- var identity = (v) => v;
5096
+ var identity2 = (v) => v;
5055
5097
  var DEFAULT_RUBBERBAND = 0.15;
5056
5098
  var commonConfigResolver = {
5057
5099
  enabled(value = true) {
@@ -5084,7 +5126,7 @@ var commonConfigResolver = {
5084
5126
  const transform = value || config.shared.transform;
5085
5127
  this.hasCustomTransform = !!transform;
5086
5128
  if (process.env.NODE_ENV === "development") {
5087
- const originalTransform = transform || identity;
5129
+ const originalTransform = transform || identity2;
5088
5130
  return (v) => {
5089
5131
  const r2 = originalTransform(v);
5090
5132
  if (!isFinite(r2[0]) || !isFinite(r2[1])) {
@@ -5093,7 +5135,7 @@ var commonConfigResolver = {
5093
5135
  return r2;
5094
5136
  };
5095
5137
  }
5096
- return transform || identity;
5138
+ return transform || identity2;
5097
5139
  },
5098
5140
  threshold(value) {
5099
5141
  return V.toVector(value, 0);
@@ -6167,16 +6209,16 @@ var EventStore = class {
6167
6209
  this._gestureKey = gestureKey;
6168
6210
  }
6169
6211
  add(element, device, action, handler, options) {
6170
- const listeners = this._listeners;
6212
+ const listeners2 = this._listeners;
6171
6213
  const type = toDomEventType(device, action);
6172
6214
  const _options = this._gestureKey ? this._ctrl.config[this._gestureKey].eventOptions : {};
6173
6215
  const eventOptions = _objectSpread2(_objectSpread2({}, _options), options);
6174
6216
  element.addEventListener(type, handler, eventOptions);
6175
6217
  const remove = () => {
6176
6218
  element.removeEventListener(type, handler, eventOptions);
6177
- listeners.delete(remove);
6219
+ listeners2.delete(remove);
6178
6220
  };
6179
- listeners.add(remove);
6221
+ listeners2.add(remove);
6180
6222
  return remove;
6181
6223
  }
6182
6224
  clean() {
@@ -6661,7 +6703,6 @@ var useViewportContainer = ({
6661
6703
  origin: originRef.current
6662
6704
  };
6663
6705
  };
6664
- var selectLargestPageWidth = (state) => state.viewports.reduce((max, vp) => Math.max(max, vp.width), 0);
6665
6706
  var DEFAULT_HEIGHT = 600;
6666
6707
  var EXTRA_HEIGHT = 0;
6667
6708
  var DEFAULT_VIRTUALIZER_OPTIONS = { overscan: 1 };
@@ -6776,7 +6817,10 @@ var Pages = ({
6776
6817
  scrollOffset: virtualizer.scrollOffset ?? null
6777
6818
  });
6778
6819
  useFitWidth({ viewportRef: containerRef });
6779
- const largestPageWidth = usePdf(selectLargestPageWidth);
6820
+ const largestPageWidth = useMemo(
6821
+ () => viewports.reduce((max, viewport) => Math.max(max, viewport.width), 0),
6822
+ [viewports]
6823
+ );
6780
6824
  useEffect(() => {
6781
6825
  virtualizer.getOffsetForAlignment = (toOffset, align, itemSize = 0) => {
6782
6826
  const size = virtualizer.getSize();
@@ -6814,7 +6858,9 @@ var Pages = ({
6814
6858
  {
6815
6859
  ref: elementWrapperRef,
6816
6860
  style: {
6817
- width: "max-content",
6861
+ // The absolute page list contributes no intrinsic width. Reserve
6862
+ // its width before any zoom effect runs (including zoom === 1).
6863
+ width: largestPageWidth,
6818
6864
  marginLeft: "auto",
6819
6865
  marginRight: "auto",
6820
6866
  flexShrink: 0
@@ -6855,6 +6901,36 @@ var Pages = ({
6855
6901
  );
6856
6902
  };
6857
6903
 
6904
+ // src/lib/map-concurrent.ts
6905
+ async function mapConcurrent(items, concurrency, map, isCancelled = () => false) {
6906
+ if (!Number.isFinite(concurrency) || concurrency < 1)
6907
+ throw new RangeError("Concurrency must be positive and finite");
6908
+ const results = new Array(items.length);
6909
+ let next = 0;
6910
+ let failed = false;
6911
+ const worker = async () => {
6912
+ while (next < items.length) {
6913
+ if (failed || isCancelled())
6914
+ throw new DOMException("Operation aborted", "AbortError");
6915
+ const index = next++;
6916
+ try {
6917
+ results[index] = await map(items[index], index);
6918
+ } catch (error) {
6919
+ failed = true;
6920
+ throw error;
6921
+ }
6922
+ }
6923
+ };
6924
+ await Promise.all(
6925
+ Array.from(
6926
+ { length: Math.min(Math.max(1, Math.floor(concurrency)), items.length) },
6927
+ worker
6928
+ )
6929
+ );
6930
+ if (isCancelled()) throw new DOMException("Operation aborted", "AbortError");
6931
+ return results;
6932
+ }
6933
+
6858
6934
  // src/lib/pdfjs.ts
6859
6935
  var pdfJsPromise = null;
6860
6936
  var loadPdfJs = () => {
@@ -6987,7 +7063,6 @@ var usePDFDocumentContext = ({
6987
7063
  colorScheme,
6988
7064
  darkModeColors
6989
7065
  }) => {
6990
- const [_, setProgress] = useState(0);
6991
7066
  const [initialState, setInitialState] = useState();
6992
7067
  const [rotation] = useState(initialRotation);
6993
7068
  const [renderColorMapRef] = useState(() => ({
@@ -7002,30 +7077,30 @@ var usePDFDocumentContext = ({
7002
7077
  const darkModeColorsRef = useRef(darkModeColors);
7003
7078
  darkModeColorsRef.current = darkModeColors;
7004
7079
  useEffect(() => {
7080
+ let isDisposed = false;
7005
7081
  const generateViewports = async (pdf) => {
7006
7082
  const pageProxies = [];
7007
- const rotations = [];
7008
- const viewports = await Promise.all(
7009
- Array.from({ length: pdf.numPages }, async (_2, index) => {
7083
+ const viewports = await mapConcurrent(
7084
+ Array.from({ length: pdf.numPages }, (_, index) => index),
7085
+ 16,
7086
+ async (index) => {
7010
7087
  const page = await pdf.getPage(index + 1);
7011
7088
  const deltaRotate = page.rotate || 0;
7012
7089
  const viewport = page.getViewport({
7013
7090
  scale: 1,
7014
7091
  rotation: rotation + deltaRotate
7015
7092
  });
7016
- pageProxies.push(page);
7017
- rotations.push(page.rotate);
7093
+ pageProxies[index] = page;
7018
7094
  return viewport;
7019
- })
7020
- );
7021
- const sortedPageProxies = pageProxies.toSorted(
7022
- (a, b) => a.pageNumber - b.pageNumber
7095
+ },
7096
+ () => isDisposed
7023
7097
  );
7098
+ if (isDisposed) return;
7024
7099
  setInitialState((prev) => ({
7025
7100
  ...prev,
7026
7101
  isZoomFitWidth,
7027
7102
  viewports,
7028
- pageProxies: sortedPageProxies,
7103
+ pageProxies,
7029
7104
  pdfDocumentProxy: pdf,
7030
7105
  zoom,
7031
7106
  zoomOptions,
@@ -7036,9 +7111,7 @@ var usePDFDocumentContext = ({
7036
7111
  };
7037
7112
  const loadDocument = () => {
7038
7113
  setInitialState(null);
7039
- setProgress(0);
7040
7114
  let loadingTask = null;
7041
- let isDisposed = false;
7042
7115
  void loadPdfJs().then(({ getDocument, version }) => {
7043
7116
  if (isDisposed) {
7044
7117
  return;
@@ -7051,18 +7124,11 @@ var usePDFDocumentContext = ({
7051
7124
  documentOptionsRef.current
7052
7125
  )
7053
7126
  );
7054
- loadingTask.onProgress = (progressEvent) => {
7055
- if (progressEvent.loaded === progressEvent.total) {
7056
- return;
7057
- }
7058
- setProgress(progressEvent.loaded / progressEvent.total);
7059
- };
7060
7127
  return loadingTask.promise.then(async (proxy) => {
7061
7128
  if (isDisposed || loadingTask?.destroyed) {
7062
7129
  return;
7063
7130
  }
7064
7131
  onDocumentLoad?.({ proxy, source });
7065
- setProgress(1);
7066
7132
  try {
7067
7133
  await generateViewports(proxy);
7068
7134
  } catch (error) {
@@ -7191,31 +7257,122 @@ var Root = forwardRef(
7191
7257
  }
7192
7258
  );
7193
7259
  Root.displayName = "Root";
7194
- var Search = ({ children, loading = "Loading..." }) => {
7195
- const [isLoading, setIsLoading] = useState(false);
7260
+
7261
+ // src/lib/document-text.ts
7262
+ var jobs = /* @__PURE__ */ new WeakMap();
7263
+ var CONCURRENCY = 4;
7264
+ function acquireDocumentText(pages) {
7265
+ let job = jobs.get(pages);
7266
+ if (!job) {
7267
+ const controller = new AbortController();
7268
+ const { signal } = controller;
7269
+ let next = 0;
7270
+ const result = new Array(pages.length);
7271
+ const worker = async () => {
7272
+ while (next < pages.length) {
7273
+ signal.throwIfAborted();
7274
+ const index = next++;
7275
+ const page = pages[index];
7276
+ const reader = page.streamTextContent().getReader();
7277
+ const chunks = [];
7278
+ let complete = false;
7279
+ try {
7280
+ while (true) {
7281
+ const { value, done } = await reader.read();
7282
+ signal.throwIfAborted();
7283
+ if (done) {
7284
+ complete = true;
7285
+ break;
7286
+ }
7287
+ chunks.push(
7288
+ value.items.map((item) => "str" in item ? item.str : "").join("")
7289
+ );
7290
+ }
7291
+ } finally {
7292
+ if (!complete)
7293
+ void reader.cancel(new Error("Text indexing stopped")).catch(() => {
7294
+ });
7295
+ reader.releaseLock();
7296
+ }
7297
+ result[index] = { pageNumber: page.pageNumber, text: chunks.join("") };
7298
+ }
7299
+ };
7300
+ const created = {
7301
+ controller,
7302
+ users: 0,
7303
+ settled: false,
7304
+ promise: Promise.resolve([])
7305
+ };
7306
+ created.promise = Promise.all(
7307
+ Array.from({ length: Math.min(CONCURRENCY, pages.length) }, worker)
7308
+ ).then(() => result).catch((error) => {
7309
+ controller.abort();
7310
+ if (jobs.get(pages) === created) jobs.delete(pages);
7311
+ throw error;
7312
+ }).finally(() => {
7313
+ created.settled = true;
7314
+ });
7315
+ jobs.set(pages, created);
7316
+ job = created;
7317
+ }
7318
+ job.users++;
7319
+ const acquired = job;
7320
+ let released = false;
7321
+ return {
7322
+ promise: acquired.promise,
7323
+ release() {
7324
+ if (released) return;
7325
+ released = true;
7326
+ acquired.users--;
7327
+ queueMicrotask(() => {
7328
+ if (acquired.users || acquired.settled) return;
7329
+ if (jobs.get(pages) === acquired) jobs.delete(pages);
7330
+ acquired.controller.abort();
7331
+ });
7332
+ }
7333
+ };
7334
+ }
7335
+ var Search = ({
7336
+ children,
7337
+ loading = "Loading...",
7338
+ errorFallback
7339
+ }) => {
7340
+ const [result, setResult] = useState(null);
7341
+ const [attempt, setAttempt] = useState(0);
7196
7342
  const proxies = usePdf((state) => state.pageProxies);
7197
7343
  const setTextContent = usePdf((state) => state.setTextContent);
7198
- const getTextContent = useCallback(
7199
- async (pages) => {
7200
- setIsLoading(true);
7201
- const promises = pages.map(async (proxy) => {
7202
- const content = await proxy.getTextContent();
7203
- const text2 = content.items.map((item) => item?.str || "").join("");
7204
- return Promise.resolve({
7205
- pageNumber: proxy.pageNumber,
7206
- text: text2
7207
- });
7208
- });
7209
- const text = await Promise.all(promises);
7210
- setIsLoading(false);
7211
- setTextContent(text);
7212
- },
7213
- [setTextContent]
7214
- );
7344
+ const retry = useCallback(() => {
7345
+ setResult(null);
7346
+ setAttempt((value) => value + 1);
7347
+ }, []);
7215
7348
  useEffect(() => {
7216
- getTextContent(proxies);
7217
- }, [proxies, getTextContent]);
7218
- return isLoading ? loading : children;
7349
+ let disposed = false;
7350
+ setResult(null);
7351
+ const task = acquireDocumentText(proxies);
7352
+ void task.promise.then((text) => {
7353
+ if (disposed) return;
7354
+ setTextContent(text);
7355
+ setResult({ pages: proxies, status: "ready" });
7356
+ }).catch((error) => {
7357
+ if (disposed) return;
7358
+ console.error("Error extracting PDF text", error);
7359
+ setResult({ pages: proxies, status: "error", error });
7360
+ });
7361
+ return () => {
7362
+ disposed = true;
7363
+ task.release();
7364
+ };
7365
+ }, [proxies, setTextContent, attempt]);
7366
+ if (!result || result.pages !== proxies) return loading;
7367
+ if (result.status === "error") {
7368
+ if (errorFallback) return errorFallback({ error: result.error, retry });
7369
+ return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
7370
+ "Unable to index this document for search.",
7371
+ " ",
7372
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: retry, children: "Retry" })
7373
+ ] });
7374
+ }
7375
+ return children;
7219
7376
  };
7220
7377
  var DEFAULT_CONFIG = {
7221
7378
  maxHeight: 800,
@@ -7308,10 +7465,11 @@ var useThumbnail = (pageNumber, config = {}) => {
7308
7465
  };
7309
7466
  var Thumbnail = ({
7310
7467
  pageNumber = 1,
7468
+ eager = false,
7311
7469
  ...props
7312
7470
  }) => {
7313
7471
  const { canvasRef, containerRef, isVisible } = useThumbnail(pageNumber, {
7314
- isFirstPage: pageNumber < 5
7472
+ isFirstPage: eager || pageNumber < 5
7315
7473
  });
7316
7474
  const { jumpToPage } = usePdfJump();
7317
7475
  return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { minHeight: "150px", minWidth: "10px" }, children: isVisible && /* @__PURE__ */ jsx(
@@ -7338,16 +7496,107 @@ var Thumbnail = ({
7338
7496
  }
7339
7497
  ) });
7340
7498
  };
7499
+ function VirtualizedThumbnails({
7500
+ children,
7501
+ count,
7502
+ virtualize,
7503
+ style,
7504
+ onKeyDown,
7505
+ ref,
7506
+ ...props
7507
+ }) {
7508
+ const containerRef = useRef(null);
7509
+ useImperativeHandle(ref, () => containerRef.current);
7510
+ const [focusPage, setFocusPage] = useState(null);
7511
+ const itemHeight = Number.isFinite(virtualize.itemHeight) ? Math.max(1, virtualize.itemHeight) : 150;
7512
+ const virtualizer = useVirtualizer({
7513
+ count,
7514
+ getScrollElement: () => containerRef.current,
7515
+ estimateSize: () => itemHeight,
7516
+ overscan: virtualize.overscan ?? 2
7517
+ });
7518
+ const items = virtualizer.getVirtualItems();
7519
+ useLayoutEffect(() => {
7520
+ if (focusPage === null) return;
7521
+ const canvas = containerRef.current?.querySelector(
7522
+ `[data-thumbnail-page="${focusPage}"] canvas`
7523
+ );
7524
+ if (canvas) {
7525
+ canvas.focus({ preventScroll: true });
7526
+ setFocusPage(null);
7527
+ }
7528
+ }, [focusPage, items]);
7529
+ return /* @__PURE__ */ jsx(
7530
+ Primitive.div,
7531
+ {
7532
+ ...props,
7533
+ ref: containerRef,
7534
+ style: {
7535
+ height: "100%",
7536
+ overflow: "auto",
7537
+ position: "relative",
7538
+ ...style
7539
+ },
7540
+ onKeyDown: (event) => {
7541
+ onKeyDown?.(event);
7542
+ if (event.defaultPrevented || !count) return;
7543
+ const row = event.target.closest(
7544
+ "[data-thumbnail-page]"
7545
+ );
7546
+ if (!row) return;
7547
+ const current = Number(row.dataset.thumbnailPage) - 1;
7548
+ const next = event.key === "Home" ? 0 : event.key === "End" ? count - 1 : event.key === "ArrowDown" ? Math.min(count - 1, current + 1) : event.key === "ArrowUp" ? Math.max(0, current - 1) : null;
7549
+ if (next === null) return;
7550
+ event.preventDefault();
7551
+ virtualizer.scrollToIndex(next, { align: "auto" });
7552
+ setFocusPage(next + 1);
7553
+ },
7554
+ children: /* @__PURE__ */ jsx("div", { style: { height: virtualizer.getTotalSize(), position: "relative" }, children: items.map((item) => /* @__PURE__ */ jsx(
7555
+ "div",
7556
+ {
7557
+ "data-thumbnail-page": item.index + 1,
7558
+ style: {
7559
+ position: "absolute",
7560
+ top: 0,
7561
+ left: 0,
7562
+ width: "100%",
7563
+ height: itemHeight,
7564
+ transform: `translateY(${item.start}px)`
7565
+ },
7566
+ children: cloneElement(
7567
+ children,
7568
+ { pageNumber: item.index + 1, eager: true }
7569
+ )
7570
+ },
7571
+ item.key
7572
+ )) })
7573
+ }
7574
+ );
7575
+ }
7341
7576
  var Thumbnails = ({
7342
7577
  children,
7578
+ virtualize,
7343
7579
  ...props
7344
7580
  }) => {
7345
7581
  const pageCount = usePdf((state) => state.pdfDocumentProxy.numPages);
7346
- return /* @__PURE__ */ jsx(Primitive.div, { ...props, children: Array.from({
7347
- length: pageCount
7348
- }).map((_, index) => {
7349
- return cloneElement(children, { key: index, pageNumber: index + 1 });
7350
- }) });
7582
+ if (virtualize)
7583
+ return /* @__PURE__ */ jsx(
7584
+ VirtualizedThumbnails,
7585
+ {
7586
+ ...props,
7587
+ virtualize,
7588
+ count: pageCount,
7589
+ children
7590
+ }
7591
+ );
7592
+ return /* @__PURE__ */ jsx(Primitive.div, { ...props, children: Array.from(
7593
+ { length: pageCount },
7594
+ (_, index) => cloneElement(children, {
7595
+ // biome-ignore lint/suspicious/noArrayIndexKey: a PDF page has a stable ordinal within this document
7596
+ key: index,
7597
+ pageNumber: index + 1
7598
+ })
7599
+ ) });
7351
7600
  };
7352
7601
  var ZoomIn = ({ ...props }) => {
7353
7602
  const setZoom = usePdf((state) => state.updateZoom);
@@ -7405,220 +7654,277 @@ var CurrentZoom = ({ ...props }) => {
7405
7654
  }
7406
7655
  );
7407
7656
  };
7408
- function levenshteinDistance(a, b) {
7409
- const matrix = [];
7410
- for (let i = 0; i <= b.length; i++) {
7411
- matrix[i] = [i];
7412
- }
7413
- for (let j = 0; j <= a.length; j++) {
7414
- matrix[0][j] = j;
7415
- }
7416
- for (let i = 1; i <= b.length; i++) {
7417
- for (let j = 1; j <= a.length; j++) {
7418
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
7419
- matrix[i][j] = matrix[i - 1][j - 1];
7420
- } else {
7421
- matrix[i][j] = Math.min(
7422
- matrix[i - 1][j - 1] + 1,
7423
- matrix[i][j - 1] + 1,
7424
- matrix[i - 1][j] + 1
7657
+
7658
+ // src/lib/search.ts
7659
+ var normalizedPages = /* @__PURE__ */ new WeakMap();
7660
+ function normalize(page) {
7661
+ let cached = normalizedPages.get(page);
7662
+ if (!cached || cached.text !== page.text) {
7663
+ cached = { text: page.text, lower: page.text.toLowerCase() };
7664
+ normalizedPages.set(page, cached);
7665
+ }
7666
+ return cached.lower;
7667
+ }
7668
+ function createBoundedDistance(query, maxDistance) {
7669
+ const length = query.length;
7670
+ let previous = new Float64Array(length + 1);
7671
+ let current = new Float64Array(length + 1);
7672
+ const exceeded = maxDistance + 1;
7673
+ return (text, offset3) => {
7674
+ const size = Math.min(length, text.length - offset3);
7675
+ if (length - size > maxDistance) return exceeded;
7676
+ for (let j = 0; j <= length; j++) previous[j] = j;
7677
+ for (let i = 1; i <= size; i++) {
7678
+ const start = Math.max(1, i - maxDistance);
7679
+ const end = Math.min(length, i + maxDistance);
7680
+ current[0] = i;
7681
+ if (start > 1) current[start - 1] = exceeded;
7682
+ let minimum = exceeded;
7683
+ const code = text.charCodeAt(offset3 + i - 1);
7684
+ for (let j = start; j <= end; j++) {
7685
+ const value = Math.min(
7686
+ previous[j] + 1,
7687
+ current[j - 1] + 1,
7688
+ previous[j - 1] + (query.charCodeAt(j - 1) === code ? 0 : 1)
7425
7689
  );
7690
+ current[j] = value;
7691
+ minimum = Math.min(minimum, value);
7426
7692
  }
7693
+ if (end < length) current[end + 1] = exceeded;
7694
+ if (minimum > maxDistance) return exceeded;
7695
+ [previous, current] = [current, previous];
7427
7696
  }
7697
+ return previous[length];
7698
+ };
7699
+ }
7700
+ function insertMatch(matches, match, limit) {
7701
+ if (!limit || matches.length === limit && matches[limit - 1].score >= match.score)
7702
+ return;
7703
+ let low = 0;
7704
+ let high = matches.length;
7705
+ while (low < high) {
7706
+ const mid = low + high >>> 1;
7707
+ if (matches[mid].score >= match.score) low = mid + 1;
7708
+ else high = mid;
7709
+ }
7710
+ matches.splice(low, 0, match);
7711
+ if (matches.length > limit) matches.pop();
7712
+ }
7713
+ function* runSearch(pages, searchText, options = {}, cooperative = false, budget = 8) {
7714
+ const empty = { exactMatches: [], fuzzyMatches: [], hasMoreResults: false };
7715
+ if (!searchText.trim()) return empty;
7716
+ const threshold = Math.max(0, Math.min(1, options.threshold ?? 0.7));
7717
+ const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : 10;
7718
+ const textSize = options.textSize ?? 100;
7719
+ const query = searchText.toLowerCase();
7720
+ const maxDistance = Math.floor(query.length * (1 - threshold));
7721
+ const distance = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
7722
+ const exactMatches = [];
7723
+ const fuzzyMatches = [];
7724
+ let exactCount = 0;
7725
+ let fuzzyCount = 0;
7726
+ let steps = 0;
7727
+ let deadline = cooperative ? performance.now() + budget : 0;
7728
+ for (const page of pages) {
7729
+ const lower = normalize(page);
7730
+ const exactStarts = [];
7731
+ let index = 0;
7732
+ while (true) {
7733
+ if (cooperative && (++steps & 127) === 0 && performance.now() >= deadline) {
7734
+ yield;
7735
+ deadline = performance.now() + budget;
7736
+ }
7737
+ const matchIndex = lower.indexOf(query, index);
7738
+ if (matchIndex === -1) break;
7739
+ exactCount++;
7740
+ if (maxDistance > 0) exactStarts.push(matchIndex);
7741
+ if (exactMatches.length < limit) {
7742
+ exactMatches.push({
7743
+ pageNumber: page.pageNumber,
7744
+ text: page.text.substr(matchIndex, searchText.length + textSize),
7745
+ score: 1,
7746
+ matchIndex,
7747
+ isExactMatch: true,
7748
+ searchText
7749
+ });
7750
+ }
7751
+ index = matchIndex + searchText.length;
7752
+ }
7753
+ if (!(maxDistance > 0)) continue;
7754
+ index = 0;
7755
+ let exactIndex = 0;
7756
+ while (index < lower.length) {
7757
+ if (cooperative && (++steps & 127) === 0 && performance.now() >= deadline) {
7758
+ yield;
7759
+ deadline = performance.now() + budget;
7760
+ }
7761
+ while (exactIndex < exactStarts.length && exactStarts[exactIndex] + searchText.length <= index)
7762
+ exactIndex++;
7763
+ const exactStart = exactStarts[exactIndex];
7764
+ if (exactStart !== void 0 && index >= exactStart) {
7765
+ index = exactStart + searchText.length;
7766
+ continue;
7767
+ }
7768
+ const edits = distance(lower, index);
7769
+ if (edits > 0 && edits <= maxDistance) {
7770
+ fuzzyCount++;
7771
+ const score = 1 - edits / query.length;
7772
+ if (fuzzyMatches.length < limit || score > (fuzzyMatches[limit - 1]?.score ?? Infinity)) {
7773
+ insertMatch(
7774
+ fuzzyMatches,
7775
+ {
7776
+ pageNumber: page.pageNumber,
7777
+ text: page.text.substr(index, query.length + textSize),
7778
+ score,
7779
+ matchIndex: index,
7780
+ isExactMatch: false,
7781
+ searchText
7782
+ },
7783
+ limit
7784
+ );
7785
+ }
7786
+ index += query.length;
7787
+ } else index++;
7788
+ }
7789
+ }
7790
+ const exactLimit = Math.min(
7791
+ exactCount,
7792
+ Math.max(Math.ceil(limit / 2), limit - fuzzyCount)
7793
+ );
7794
+ return {
7795
+ exactMatches: exactMatches.slice(0, exactLimit),
7796
+ fuzzyMatches: fuzzyMatches.slice(0, limit - exactLimit),
7797
+ hasMoreResults: exactCount + fuzzyCount > limit
7798
+ };
7799
+ }
7800
+ function searchDocument(pages, searchText, options = {}) {
7801
+ return runSearch(pages, searchText, options).next().value;
7802
+ }
7803
+ async function searchDocumentAsync(pages, searchText, options = {}) {
7804
+ const budget = Number.isFinite(options.timeSliceMs) ? Math.max(1, options.timeSliceMs) : 8;
7805
+ const iterator = runSearch(pages, searchText, options, true, budget);
7806
+ while (true) {
7807
+ if (options.signal?.aborted)
7808
+ throw new DOMException("Search aborted", "AbortError");
7809
+ const result = iterator.next();
7810
+ if (result.done) return result.value;
7811
+ await new Promise((resolve) => setTimeout(resolve, 0));
7428
7812
  }
7429
- return matrix[b.length][a.length];
7430
7813
  }
7814
+
7815
+ // src/hooks/search/useSearch.tsx
7431
7816
  var useSearch = () => {
7432
7817
  const textContent = usePdf((state) => state.textContent);
7818
+ const activeSearch = useRef(null);
7819
+ const [isSearching, setIsSearching] = useState(false);
7433
7820
  const [keywords] = useState([]);
7434
7821
  const [searchResults, setSearchResults] = useState({
7435
7822
  exactMatches: [],
7436
7823
  fuzzyMatches: [],
7437
7824
  hasMoreResults: false
7438
7825
  });
7439
- const findExactMatches = useCallback(
7440
- (searchText, text, pageNumber, textSize) => {
7441
- const results = [];
7442
- const textLower = text.toLowerCase();
7443
- const searchLower = searchText.toLowerCase();
7444
- let index = 0;
7445
- while (true) {
7446
- const matchIndex = textLower.indexOf(searchLower, index);
7447
- if (matchIndex === -1) break;
7448
- results.push({
7449
- pageNumber,
7450
- text: text.substr(matchIndex, searchText.length + textSize),
7451
- score: 1,
7452
- matchIndex,
7453
- isExactMatch: true,
7454
- searchText
7826
+ useEffect(() => {
7827
+ setIsSearching(false);
7828
+ return () => {
7829
+ activeSearch.current?.abort();
7830
+ activeSearch.current = null;
7831
+ };
7832
+ }, [textContent]);
7833
+ const cancelSearch = useCallback(() => {
7834
+ activeSearch.current?.abort();
7835
+ activeSearch.current = null;
7836
+ setIsSearching(false);
7837
+ }, []);
7838
+ const searchAsync = useCallback(
7839
+ async (searchText, options = {}) => {
7840
+ activeSearch.current?.abort();
7841
+ const controller = new AbortController();
7842
+ activeSearch.current = controller;
7843
+ const abort = () => controller.abort();
7844
+ if (options.signal?.aborted) abort();
7845
+ else options.signal?.addEventListener("abort", abort, { once: true });
7846
+ setIsSearching(true);
7847
+ try {
7848
+ const results = await searchDocumentAsync(textContent, searchText, {
7849
+ ...options,
7850
+ signal: controller.signal
7455
7851
  });
7456
- index = matchIndex + searchText.length;
7457
- }
7458
- return results;
7459
- },
7460
- []
7461
- );
7462
- const findFuzzyMatches = useCallback(
7463
- (searchText, text, pageNumber, threshold, excludeIndices, textSize) => {
7464
- const results = [];
7465
- const textLower = text.toLowerCase();
7466
- const searchLower = searchText.toLowerCase();
7467
- let index = 0;
7468
- while (index < textLower.length) {
7469
- if (excludeIndices.has(index)) {
7470
- index++;
7471
- continue;
7472
- }
7473
- const chunk = textLower.substr(index, searchLower.length + 10);
7474
- const distance = levenshteinDistance(
7475
- searchLower,
7476
- chunk.substr(0, searchLower.length)
7477
- );
7478
- const maxDistance = Math.floor(searchLower.length * (1 - threshold));
7479
- if (distance <= maxDistance && distance > 0) {
7480
- const score = 1 - distance / searchLower.length;
7481
- results.push({
7482
- pageNumber,
7483
- text: text.substr(index, searchLower.length + textSize),
7484
- score,
7485
- matchIndex: index,
7486
- isExactMatch: false,
7487
- searchText
7488
- });
7489
- index += searchLower.length;
7490
- } else {
7491
- index++;
7852
+ if (controller.signal.aborted)
7853
+ throw new DOMException("Search aborted", "AbortError");
7854
+ if (activeSearch.current === controller) setSearchResults(results);
7855
+ return results;
7856
+ } finally {
7857
+ options.signal?.removeEventListener("abort", abort);
7858
+ if (activeSearch.current === controller) {
7859
+ activeSearch.current = null;
7860
+ setIsSearching(false);
7492
7861
  }
7493
7862
  }
7494
- return results;
7495
7863
  },
7496
- []
7864
+ [textContent]
7497
7865
  );
7498
7866
  const search = useCallback(
7499
7867
  (searchText, options = {}) => {
7500
- const { threshold = 0.7, limit = 10, textSize = 100 } = options;
7501
- if (!searchText.trim()) {
7502
- const emptyResults = {
7503
- exactMatches: [],
7504
- fuzzyMatches: [],
7505
- hasMoreResults: false
7506
- };
7507
- setSearchResults(emptyResults);
7508
- return emptyResults;
7509
- }
7510
- let exactMatches = [];
7511
- let fuzzyMatches = [];
7512
- const exactMatchIndices = /* @__PURE__ */ new Set();
7513
- textContent.forEach(({ pageNumber, text }) => {
7514
- const matches = findExactMatches(
7515
- searchText,
7516
- text,
7517
- pageNumber,
7518
- textSize
7519
- );
7520
- exactMatches = [...exactMatches, ...matches];
7521
- matches.forEach((match) => {
7522
- for (let i = 0; i < searchText.length; i++) {
7523
- exactMatchIndices.add(match.matchIndex + i);
7524
- }
7525
- });
7526
- });
7527
- textContent.forEach(({ pageNumber, text }) => {
7528
- const matches = findFuzzyMatches(
7529
- searchText,
7530
- text,
7531
- pageNumber,
7532
- threshold,
7533
- exactMatchIndices,
7534
- textSize
7535
- );
7536
- fuzzyMatches = [...fuzzyMatches, ...matches];
7537
- });
7538
- exactMatches.sort((a, b) => b.score - a.score);
7539
- fuzzyMatches.sort((a, b) => b.score - a.score);
7540
- const exactLimit = Math.min(exactMatches.length, Math.ceil(limit / 2));
7541
- const fuzzyLimit = Math.min(fuzzyMatches.length, limit - exactLimit);
7542
- const limitedResults = {
7543
- exactMatches: exactMatches.slice(0, exactLimit),
7544
- fuzzyMatches: fuzzyMatches.slice(0, fuzzyLimit),
7545
- hasMoreResults: exactMatches.length + fuzzyMatches.length > limit
7546
- };
7547
- setSearchResults(limitedResults);
7548
- return limitedResults;
7868
+ cancelSearch();
7869
+ const results = searchDocument(textContent, searchText, options);
7870
+ setSearchResults(results);
7871
+ return results;
7549
7872
  },
7550
- [textContent, findExactMatches, findFuzzyMatches]
7873
+ [textContent, cancelSearch]
7551
7874
  );
7552
7875
  return {
7553
7876
  textContent,
7554
7877
  keywords,
7555
7878
  searchResults,
7556
- search
7879
+ search,
7880
+ searchAsync,
7881
+ cancelSearch,
7882
+ isSearching
7557
7883
  };
7558
7884
  };
7559
7885
 
7560
7886
  // src/hooks/search/useSearchPosition.tsx
7561
7887
  async function calculateHighlightRects(pageProxy, textMatch) {
7888
+ const matchLength = textMatch.searchText ? textMatch.searchText.length : textMatch.text.length;
7889
+ if (matchLength === 0 || !Number.isFinite(textMatch.matchIndex) || textMatch.matchIndex < 0)
7890
+ return [];
7891
+ const matchEnd = textMatch.matchIndex + matchLength;
7562
7892
  const reader = pageProxy.streamTextContent().getReader();
7563
- const items = [];
7893
+ const matchRects = [];
7894
+ const viewport = pageProxy.getViewport({ scale: 1 });
7895
+ let offset3 = 0;
7896
+ let foundEnd = false;
7564
7897
  try {
7565
- for (; ; ) {
7898
+ read: while (true) {
7566
7899
  const { value, done } = await reader.read();
7567
7900
  if (done) break;
7568
- items.push(
7569
- ...value.items.filter((item) => "str" in item)
7570
- );
7901
+ for (const item of value.items) {
7902
+ if (!("str" in item) || item.str.length === 0) continue;
7903
+ const length = item.str.length;
7904
+ const start = Math.max(0, textMatch.matchIndex - offset3);
7905
+ const end = Math.min(length, matchEnd - offset3);
7906
+ if (end > start) {
7907
+ matchRects.push({
7908
+ pageNumber: textMatch.pageNumber,
7909
+ left: item.transform[4] + start * (item.width / length),
7910
+ top: viewport.height - (item.transform[5] + item.height),
7911
+ width: (end - start) * (item.width / length),
7912
+ height: item.height
7913
+ });
7914
+ }
7915
+ offset3 += length;
7916
+ if (offset3 >= matchEnd) {
7917
+ foundEnd = true;
7918
+ break read;
7919
+ }
7920
+ }
7571
7921
  }
7572
7922
  } finally {
7923
+ if (foundEnd)
7924
+ void reader.cancel(new Error("Highlight range found")).catch(() => {
7925
+ });
7573
7926
  reader.releaseLock();
7574
7927
  }
7575
- const matchLength = textMatch.searchText ? textMatch.searchText.length : textMatch.text.length;
7576
- const matchRects = [];
7577
- let currentIndex = 0;
7578
- let remainingMatchLength = matchLength;
7579
- let foundStart = false;
7580
- const viewport = pageProxy.getViewport({ scale: 1 });
7581
- for (let i = 0; i < items.length; i++) {
7582
- const item = items[i];
7583
- if (!item) continue;
7584
- const itemLength = item.str.length;
7585
- if (!foundStart && currentIndex <= textMatch.matchIndex && textMatch.matchIndex < currentIndex + itemLength) {
7586
- foundStart = true;
7587
- const matchStartInItem = textMatch.matchIndex - currentIndex;
7588
- const matchLengthInItem = Math.min(
7589
- itemLength - matchStartInItem,
7590
- remainingMatchLength
7591
- );
7592
- const transform = item.transform;
7593
- const y = viewport.height - (transform[5] + item.height);
7594
- const rect = {
7595
- pageNumber: textMatch.pageNumber,
7596
- left: transform[4] + matchStartInItem * (item.width / itemLength),
7597
- top: y,
7598
- width: matchLengthInItem * (item.width / itemLength),
7599
- height: item.height
7600
- };
7601
- matchRects.push(rect);
7602
- remainingMatchLength -= matchLengthInItem;
7603
- } else if (foundStart && remainingMatchLength > 0) {
7604
- const matchLengthInItem = Math.min(itemLength, remainingMatchLength);
7605
- const transform = item.transform;
7606
- const y = viewport.height - (transform[5] + item.height);
7607
- const rect = {
7608
- pageNumber: textMatch.pageNumber,
7609
- left: transform[4],
7610
- top: y,
7611
- width: matchLengthInItem * (item.width / itemLength),
7612
- height: item.height
7613
- };
7614
- matchRects.push(rect);
7615
- remainingMatchLength -= matchLengthInItem;
7616
- }
7617
- if (remainingMatchLength <= 0 && foundStart) {
7618
- break;
7619
- }
7620
- currentIndex += itemLength;
7621
- }
7622
7928
  return mergeAdjacentRects(matchRects);
7623
7929
  }
7624
7930
  function mergeAdjacentRects(rects) {
@@ -7646,4 +7952,4 @@ function mergeAdjacentRects(rects) {
7646
7952
  // src/hooks/usePageRendered.ts
7647
7953
  var usePageRendered = (pageNumber) => usePdf((state) => !!state.renderedPages[pageNumber]);
7648
7954
 
7649
- export { AnnotationHighlightLayer, AnnotationLayer, AnnotationTooltip, AnnotationsStoreProvider, CanvasLayer, ColoredHighlightLayer, CurrentPage, CurrentZoom, CustomLayer, DEFAULT_DARK_MODE_COLORS, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, PDFLinkServiceContext, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, createDarkModeColorMap, useAnnotations, useCreatePDFLinkService, usePDFLinkService, usePDFPageNumber, usePageRendered, usePdf, usePdfJump, useSearch, useSelectionDimensions };
7955
+ export { AnnotationHighlightLayer, AnnotationLayer, AnnotationTooltip, AnnotationsStoreProvider, CanvasLayer, ColoredHighlightLayer, CurrentPage, CurrentZoom, CustomLayer, DEFAULT_DARK_MODE_COLORS, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, PDFLinkServiceContext, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, createDarkModeColorMap, useAnnotations, useCreatePDFLinkService, usePDFLinkService, usePDFPageNumber, usePageAnnotations, usePageRendered, usePdf, usePdfJump, useSearch, useSelectionDimensions };