@anaralabs/lector 3.14.13 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -219,6 +219,815 @@ function createDarkModeColorMap(colors) {
219
219
  return map;
220
220
  }
221
221
 
222
+ // src/lib/selection-page.ts
223
+ var SelectionPage = class {
224
+ constructor(element, pageNumber, width, height) {
225
+ this.element = element;
226
+ this.pageNumber = pageNumber;
227
+ this.width = width;
228
+ this.height = height;
229
+ const walker = element.ownerDocument.createTreeWalker(
230
+ element,
231
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT
232
+ );
233
+ const chunks = [];
234
+ const lineBreaks = [];
235
+ let offset3 = 0;
236
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
237
+ if (node.nodeType === Node.TEXT_NODE) {
238
+ const text = node.textContent ?? "";
239
+ if (!text) continue;
240
+ this.nodes.push({
241
+ node,
242
+ start: offset3,
243
+ end: offset3 + text.length
244
+ });
245
+ chunks.push(text);
246
+ offset3 += text.length;
247
+ } else if (node.tagName === "BR") lineBreaks.push(offset3);
248
+ }
249
+ this.content = { text: chunks.join(""), lineBreaks };
250
+ }
251
+ nodes = [];
252
+ content;
253
+ offset(node, offset3) {
254
+ if (!this.element.contains(node)) return null;
255
+ const item = this.nodes.find((item2) => item2.node === node);
256
+ if (item)
257
+ return item.start + Math.max(0, Math.min(offset3, item.end - item.start));
258
+ const range = this.element.ownerDocument.createRange();
259
+ range.selectNodeContents(this.element);
260
+ try {
261
+ range.setEnd(node, offset3);
262
+ } catch {
263
+ return null;
264
+ }
265
+ return Math.min(range.toString().length, this.content.text.length);
266
+ }
267
+ point(offset3, end = false) {
268
+ const index = Math.max(0, Math.min(offset3, this.content.text.length));
269
+ const item = this.nodes.find((item2) => end ? item2.end >= index : item2.end > index) ?? this.nodes.at(-1);
270
+ return item ? { node: item.node, offset: Math.max(0, index - item.start) } : null;
271
+ }
272
+ rects(from, to) {
273
+ const bounds = this.element.getBoundingClientRect();
274
+ const scaleX = bounds.width / this.width || 1;
275
+ const scaleY = bounds.height / this.height || 1;
276
+ const rects = [];
277
+ for (const item of this.nodes) {
278
+ const start = Math.max(from, item.start);
279
+ const end = Math.min(to, item.end);
280
+ if (end <= start || !item.node.textContent?.slice(start - item.start, end - item.start).trim())
281
+ continue;
282
+ const range = this.element.ownerDocument.createRange();
283
+ range.setStart(item.node, start - item.start);
284
+ range.setEnd(item.node, end - item.start);
285
+ for (const rect of range.getClientRects()) {
286
+ if (rect.width <= 0 || rect.height <= 0) continue;
287
+ rects.push({
288
+ pageNumber: this.pageNumber,
289
+ left: (rect.left - bounds.left) / scaleX,
290
+ top: (rect.top - bounds.top) / scaleY,
291
+ width: rect.width / scaleX,
292
+ height: rect.height / scaleY
293
+ });
294
+ }
295
+ }
296
+ return rects;
297
+ }
298
+ };
299
+ async function withSelectionPage(proxy, owner, signal, read) {
300
+ signal.throwIfAborted();
301
+ const { TextLayer: TextLayer2 } = await import('pdfjs-dist/legacy/build/pdf.mjs');
302
+ signal.throwIfAborted();
303
+ const viewport = proxy.getViewport({ scale: 1 });
304
+ const container = owner.createElement("div");
305
+ container.className = "textLayer";
306
+ container.setAttribute("aria-hidden", "true");
307
+ container.style.cssText = `position:fixed;left:-100000px;top:0;visibility:hidden;pointer-events:none;width:${viewport.width}px;height:${viewport.height}px;--scale-factor:1;--total-scale-factor:1;`;
308
+ owner.body.append(container);
309
+ let layer;
310
+ const cancel = () => layer?.cancel();
311
+ signal.addEventListener("abort", cancel, { once: true });
312
+ try {
313
+ layer = new TextLayer2({
314
+ container,
315
+ viewport,
316
+ textContentSource: proxy.streamTextContent()
317
+ });
318
+ await layer.render();
319
+ signal.throwIfAborted();
320
+ const unrotated = proxy.getViewport({ scale: 1, rotation: 0 });
321
+ container.style.width = `${unrotated.width}px`;
322
+ container.style.height = `${unrotated.height}px`;
323
+ return read(
324
+ new SelectionPage(
325
+ container,
326
+ proxy.pageNumber,
327
+ viewport.width,
328
+ viewport.height
329
+ )
330
+ );
331
+ } finally {
332
+ signal.removeEventListener("abort", cancel);
333
+ layer?.cancel();
334
+ container.remove();
335
+ }
336
+ }
337
+
338
+ // src/lib/text-normalization.ts
339
+ function replace(source, replacements2) {
340
+ const chunks = [];
341
+ const edits = [];
342
+ let offset3 = 0;
343
+ let outputLength = 0;
344
+ for (const replacement of replacements2) {
345
+ const { index, length, text } = replacement;
346
+ if (source.text.slice(index, index + length) === text) continue;
347
+ const before = source.text.slice(offset3, index);
348
+ chunks.push(before, text);
349
+ outputLength += before.length;
350
+ edits.push({
351
+ start: outputLength,
352
+ end: outputLength + text.length,
353
+ sourceStart: index,
354
+ sourceEnd: index + length
355
+ });
356
+ outputLength += text.length;
357
+ offset3 = index + length;
358
+ }
359
+ if (!edits.length) return source;
360
+ chunks.push(source.text.slice(offset3));
361
+ return { text: chunks.join(""), maps: [...source.maps, edits] };
362
+ }
363
+ function* replacements(text, pattern, value) {
364
+ for (const match of text.matchAll(pattern)) {
365
+ yield { index: match.index, length: match[0].length, text: value };
366
+ }
367
+ }
368
+ function* insertLineBreaks(text, lineBreaks) {
369
+ let previous = -1;
370
+ for (const index of lineBreaks) {
371
+ if (!Number.isInteger(index) || index <= previous || index < 0 || index > text.length)
372
+ continue;
373
+ previous = index;
374
+ yield { index, length: 0, text: "\n" };
375
+ }
376
+ }
377
+ var ligatures = ["ff", "fi", "fl", "ffi", "ffl", "st", "st"];
378
+ var softHyphen = /\u00ad(?:[^\S\r\n]*\r?\n[^\S\r\n]*(?=\p{L}))?/gu;
379
+ var wrappedHyphen = /(?<=[\p{L}\p{M}])[-\u2010][^\S\r\n]*\r?\n[^\S\r\n]*(?=\p{Ll})/gu;
380
+ var cjkLineBreak = /(?<=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])\r?\n(?=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])/gu;
381
+ function normalizeSelectionText(text, options = {}) {
382
+ let result = normalizeUnicode(text).replace(softHyphen, "");
383
+ if (options.ignoreHyphenation) result = result.replace(wrappedHyphen, "");
384
+ if (options.lineBreaks === "space") {
385
+ result = result.replace(cjkLineBreak, "").replace(/(?<!\n)\n(?!\n)/g, " ");
386
+ }
387
+ return result;
388
+ }
389
+ function normalizeUnicode(text, matchDiacritics = true) {
390
+ let result = text.replace(
391
+ /[\uFB00-\uFB06]/g,
392
+ (char) => ligatures[char.charCodeAt(0) - 64256]
393
+ );
394
+ if (!matchDiacritics) {
395
+ result = result.normalize("NFD").replace(
396
+ /([\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}])\p{M}+/gu,
397
+ "$1"
398
+ );
399
+ }
400
+ return result.normalize("NFC");
401
+ }
402
+ var segmenter;
403
+ function* unicodeReplacements(text, matchDiacritics) {
404
+ segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
405
+ for (const match of text.matchAll(/\p{ASCII}?\P{ASCII}+/gu)) {
406
+ if (normalizeUnicode(match[0], matchDiacritics) === match[0]) continue;
407
+ for (const { segment, index } of segmenter.segment(match[0])) {
408
+ yield {
409
+ index: match.index + index,
410
+ length: segment.length,
411
+ text: normalizeUnicode(segment, matchDiacritics)
412
+ };
413
+ }
414
+ }
415
+ }
416
+ function normalizeSearchText(text, options = {}, lineBreaks = []) {
417
+ let result = { text, maps: [] };
418
+ if (lineBreaks.length)
419
+ result = replace(result, insertLineBreaks(text, lineBreaks));
420
+ const matchDiacritics = options.matchDiacritics !== false;
421
+ if (normalizeUnicode(result.text, matchDiacritics) !== result.text) {
422
+ result = replace(result, unicodeReplacements(result.text, matchDiacritics));
423
+ }
424
+ result = replace(result, replacements(result.text, softHyphen, ""));
425
+ if (options.ignoreHyphenation) {
426
+ result = replace(result, replacements(result.text, wrappedHyphen, ""));
427
+ }
428
+ result = replace(result, replacements(result.text, cjkLineBreak, ""));
429
+ result = replace(result, replacements(result.text, /\s+/gu, " "));
430
+ const lower = result.text.toLowerCase();
431
+ if (lower.length !== result.text.length) {
432
+ function* caseReplacements(value) {
433
+ let index = 0;
434
+ for (const char of value) {
435
+ const folded = char.toLowerCase();
436
+ if (folded.length !== char.length)
437
+ yield { index, length: char.length, text: folded };
438
+ index += char.length;
439
+ }
440
+ }
441
+ result = replace(result, caseReplacements(result.text));
442
+ }
443
+ return { ...result, text: lower };
444
+ }
445
+ function sourceOffset(edits, offset3, end) {
446
+ let low = 0;
447
+ let high = edits.length;
448
+ while (low < high) {
449
+ const middle = low + high >>> 1;
450
+ if (edits[middle].start <= offset3) low = middle + 1;
451
+ else high = middle;
452
+ }
453
+ const edit = edits[low - 1];
454
+ if (!edit) return offset3 + (end ? 1 : 0);
455
+ if (offset3 < edit.end) return end ? edit.sourceEnd : edit.sourceStart;
456
+ return offset3 + edit.sourceEnd - edit.end + (end ? 1 : 0);
457
+ }
458
+ function originalTextRange(normalized, start, end) {
459
+ for (let i = normalized.maps.length - 1; i >= 0; i--) {
460
+ const map = normalized.maps[i];
461
+ start = sourceOffset(map, start, false);
462
+ end = sourceOffset(map, end - 1, true);
463
+ }
464
+ return { start, end };
465
+ }
466
+
467
+ // src/lib/pdf-selection.ts
468
+ var sameAnchor = (a, b) => a.pageNumber === b.pageNumber && a.offset === b.offset;
469
+ var compare = (a, b) => a.pageNumber - b.pageNumber || a.offset - b.offset;
470
+ var samePoint = (a, node, offset3) => a.node === node && a.offset === offset3;
471
+ var selectionOwners = /* @__PURE__ */ new WeakMap();
472
+ var PDFSelectionController = class {
473
+ constructor(options) {
474
+ this.options = options;
475
+ }
476
+ snapshot = null;
477
+ listeners = /* @__PURE__ */ new Set();
478
+ layers = /* @__PURE__ */ new Map();
479
+ pages = /* @__PURE__ */ new Map();
480
+ container = null;
481
+ projection = null;
482
+ frame = null;
483
+ work = null;
484
+ structuralChange = false;
485
+ drag = null;
486
+ connectedCleanup = null;
487
+ generation = 0;
488
+ keyboardWork = null;
489
+ keyboardMoves = [];
490
+ getSnapshot = () => this.snapshot;
491
+ subscribe = (listener) => {
492
+ this.listeners.add(listener);
493
+ return () => {
494
+ this.listeners.delete(listener);
495
+ };
496
+ };
497
+ get isDragging() {
498
+ return this.drag !== null;
499
+ }
500
+ includesPage(page) {
501
+ const range = this.ordered();
502
+ return Boolean(
503
+ range && page >= range.start.pageNumber && page <= range.end.pageNumber
504
+ );
505
+ }
506
+ publish(snapshot) {
507
+ this.snapshot = snapshot;
508
+ for (const listener of this.listeners) listener();
509
+ }
510
+ ordered() {
511
+ if (!this.snapshot) return null;
512
+ const { anchor, focus } = this.snapshot;
513
+ return compare(anchor, focus) <= 0 ? { start: anchor, end: focus } : { start: focus, end: anchor };
514
+ }
515
+ bounds(page, length) {
516
+ const range = this.ordered();
517
+ return {
518
+ from: page === range.start.pageNumber ? Math.min(range.start.offset, length) : 0,
519
+ to: page === range.end.pageNumber ? Math.min(range.end.offset, length) : length
520
+ };
521
+ }
522
+ remember(page) {
523
+ if (!this.includesPage(page.pageNumber)) return;
524
+ for (const point of [this.snapshot.anchor, this.snapshot.focus]) {
525
+ if (point.pageNumber === page.pageNumber && point.offset > page.content.text.length)
526
+ throw new RangeError("Selection offset exceeds page text length");
527
+ }
528
+ const { from, to } = this.bounds(page.pageNumber, page.content.text.length);
529
+ const cached = this.pages.get(page.pageNumber);
530
+ if (cached?.content.text === page.content.text && cached.from === from && cached.to === to)
531
+ return;
532
+ this.pages.set(page.pageNumber, {
533
+ content: page.content,
534
+ from,
535
+ to,
536
+ rects: page.rects(from, to)
537
+ });
538
+ }
539
+ complete() {
540
+ const range = this.ordered();
541
+ if (!range) return false;
542
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++) {
543
+ const page = this.pages.get(n);
544
+ if (!page) return false;
545
+ const bounds = this.bounds(n, page.content.text.length);
546
+ if (page.from !== bounds.from || page.to !== bounds.to) return false;
547
+ }
548
+ return true;
549
+ }
550
+ setSelection = (anchor, focus) => {
551
+ for (const point of [anchor, focus]) {
552
+ const text = this.layers.get(point.pageNumber)?.content.text ?? this.pages.get(point.pageNumber)?.content.text;
553
+ if (text !== void 0 && point.offset > text.length)
554
+ throw new RangeError("Selection offset exceeds page text length");
555
+ if (!Number.isInteger(point.pageNumber) || point.pageNumber < 1 || point.pageNumber > this.options.pageCount || !Number.isInteger(point.offset) || point.offset < 0)
556
+ throw new RangeError(
557
+ "Selection anchors need a valid page and nonnegative UTF-16 offset"
558
+ );
559
+ }
560
+ if (sameAnchor(anchor, focus)) {
561
+ const drag = this.drag;
562
+ this.clear();
563
+ this.drag = drag;
564
+ return;
565
+ }
566
+ if (this.snapshot && this.snapshot.status !== "error" && sameAnchor(this.snapshot.anchor, anchor) && sameAnchor(this.snapshot.focus, focus))
567
+ return;
568
+ if (this.container) {
569
+ const owner = this.container.ownerDocument;
570
+ const previous = selectionOwners.get(owner);
571
+ if (previous !== this) previous?.clear();
572
+ selectionOwners.set(owner, this);
573
+ }
574
+ this.work?.abort();
575
+ this.keyboardWork?.abort();
576
+ this.keyboardWork = null;
577
+ this.generation++;
578
+ this.snapshot = {
579
+ anchor: { ...anchor },
580
+ focus: { ...focus },
581
+ status: "loading"
582
+ };
583
+ for (const n of this.pages.keys())
584
+ if (!this.includesPage(n)) this.pages.delete(n);
585
+ for (const page of this.layers.values()) this.remember(page);
586
+ this.publish({
587
+ ...this.snapshot,
588
+ status: this.complete() ? "ready" : "loading"
589
+ });
590
+ this.project();
591
+ this.schedule();
592
+ this.hydrate();
593
+ };
594
+ clear = () => {
595
+ const owner = this.container?.ownerDocument;
596
+ if (owner && selectionOwners.get(owner) === this)
597
+ selectionOwners.delete(owner);
598
+ this.work?.abort();
599
+ this.keyboardWork?.abort();
600
+ this.keyboardWork = null;
601
+ this.generation++;
602
+ this.pages.clear();
603
+ this.keyboardMoves = [];
604
+ this.drag = null;
605
+ const selection = this.container?.ownerDocument.getSelection();
606
+ if (this.projection && selection && samePoint(
607
+ this.projection.anchor,
608
+ selection.anchorNode,
609
+ selection.anchorOffset
610
+ ) && samePoint(
611
+ this.projection.focus,
612
+ selection.focusNode,
613
+ selection.focusOffset
614
+ ))
615
+ selection.removeAllRanges();
616
+ this.projection = null;
617
+ if (this.snapshot) this.publish(null);
618
+ };
619
+ getText = (options = {}) => {
620
+ const range = this.ordered();
621
+ if (!range || !this.complete()) return null;
622
+ const chunks = [];
623
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++) {
624
+ const page = this.pages.get(n);
625
+ const pieces = [];
626
+ let offset3 = page.from;
627
+ for (const end of page.content.lineBreaks) {
628
+ if (end <= page.from || end >= page.to) continue;
629
+ pieces.push(page.content.text.slice(offset3, end), "\n");
630
+ offset3 = end;
631
+ }
632
+ pieces.push(page.content.text.slice(offset3, page.to));
633
+ chunks.push(pieces.join(""));
634
+ }
635
+ return normalizeSelectionText(chunks.join("\n\n"), options);
636
+ };
637
+ getSelection = (options) => {
638
+ const text = this.getText(options);
639
+ if (text === null || !this.snapshot) return null;
640
+ const range = this.ordered();
641
+ const highlights = [];
642
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++)
643
+ highlights.push(...this.pages.get(n).rects.map((rect) => ({ ...rect })));
644
+ return {
645
+ anchor: { ...this.snapshot.anchor },
646
+ focus: { ...this.snapshot.focus },
647
+ text,
648
+ highlights,
649
+ underlines: highlights.map((rect) => ({
650
+ ...rect,
651
+ top: rect.top + rect.height - 1,
652
+ height: 1
653
+ })),
654
+ isCollapsed: false
655
+ };
656
+ };
657
+ getSelectionAsync = async (options) => {
658
+ const generation = this.generation;
659
+ await new Promise((resolve) => {
660
+ if (this.snapshot?.status !== "loading") {
661
+ resolve();
662
+ return;
663
+ }
664
+ const unsubscribe = this.subscribe(() => {
665
+ if (generation !== this.generation || this.snapshot?.status !== "loading") {
666
+ unsubscribe();
667
+ resolve();
668
+ }
669
+ });
670
+ });
671
+ if (generation !== this.generation)
672
+ throw new DOMException("Selection changed", "AbortError");
673
+ if (this.snapshot?.status === "error") throw this.snapshot.error;
674
+ return this.getSelection(options);
675
+ };
676
+ finishSelection() {
677
+ if (this.snapshot?.status !== "loading" || !this.complete()) return;
678
+ this.work?.abort();
679
+ this.work = null;
680
+ this.publish({ ...this.snapshot, status: "ready" });
681
+ }
682
+ registerLayer(element, pageNumber, width, height) {
683
+ const page = new SelectionPage(element, pageNumber, width, height);
684
+ this.layers.set(pageNumber, page);
685
+ try {
686
+ this.remember(page);
687
+ this.finishSelection();
688
+ } catch (error) {
689
+ this.work?.abort();
690
+ if (this.snapshot)
691
+ this.publish({ ...this.snapshot, status: "error", error });
692
+ }
693
+ this.schedule();
694
+ return () => {
695
+ if (this.layers.get(pageNumber) !== page) return;
696
+ this.structuralChange = true;
697
+ this.layers.delete(pageNumber);
698
+ this.projection = null;
699
+ this.schedule();
700
+ };
701
+ }
702
+ anchorFor(node, offset3) {
703
+ if (!node) return null;
704
+ for (const [pageNumber, page] of this.layers) {
705
+ const index = page.offset(node, offset3);
706
+ if (index !== null) return { pageNumber, offset: index };
707
+ }
708
+ return null;
709
+ }
710
+ capture = () => {
711
+ const owner = this.container?.ownerDocument;
712
+ const active = owner?.activeElement;
713
+ if (active && active !== owner?.body && !this.container?.contains(active) && !active.closest(
714
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
715
+ ))
716
+ return;
717
+ const selection = owner?.getSelection();
718
+ if (!selection || selection.rangeCount > 1) return;
719
+ if (this.projection && samePoint(
720
+ this.projection.anchor,
721
+ selection.anchorNode,
722
+ selection.anchorOffset
723
+ ) && samePoint(
724
+ this.projection.focus,
725
+ selection.focusNode,
726
+ selection.focusOffset
727
+ ))
728
+ return;
729
+ if (this.structuralChange) {
730
+ this.schedule();
731
+ return;
732
+ }
733
+ if (selection.isCollapsed) {
734
+ if (this.structuralChange || this.drag || !this.projection && this.snapshot && (!this.layers.has(this.snapshot.anchor.pageNumber) || !this.layers.has(this.snapshot.focus.pageNumber))) {
735
+ this.schedule();
736
+ return;
737
+ }
738
+ if (this.snapshot) this.clear();
739
+ return;
740
+ }
741
+ const anchor = this.anchorFor(selection.anchorNode, selection.anchorOffset);
742
+ const focus = this.anchorFor(selection.focusNode, selection.focusOffset);
743
+ if (anchor && focus) {
744
+ if (this.drag && !this.drag.started) {
745
+ if (!this.drag.shift && !this.drag.moved) this.drag.anchor = anchor;
746
+ this.drag.started = true;
747
+ }
748
+ const savedAnchor = this.projection && samePoint(
749
+ this.projection.anchor,
750
+ selection.anchorNode,
751
+ selection.anchorOffset
752
+ ) ? this.snapshot?.anchor : void 0;
753
+ this.setSelection(this.drag?.anchor ?? savedAnchor ?? anchor, focus);
754
+ this.projection = {
755
+ anchor: { node: selection.anchorNode, offset: selection.anchorOffset },
756
+ focus: { node: selection.focusNode, offset: selection.focusOffset }
757
+ };
758
+ } else if (!this.structuralChange && !this.drag && (selection.anchorNode || selection.focusNode))
759
+ this.clear();
760
+ };
761
+ schedule() {
762
+ if (!this.container || this.frame !== null) return;
763
+ this.frame = this.container.ownerDocument.defaultView.requestAnimationFrame(() => {
764
+ this.frame = null;
765
+ this.project();
766
+ this.structuralChange = false;
767
+ this.flushKeyboard();
768
+ if (this.drag) this.autoScroll();
769
+ });
770
+ }
771
+ project() {
772
+ const range = this.ordered();
773
+ if (!range || !this.container) return;
774
+ const visible = [...this.layers.values()].filter((page) => this.includesPage(page.pageNumber) && page.nodes.length).sort((a, b) => a.pageNumber - b.pageNumber);
775
+ const first = visible[0];
776
+ const last = visible.at(-1);
777
+ if (!first || !last) return;
778
+ const start = first.point(
779
+ this.bounds(first.pageNumber, first.content.text.length).from
780
+ );
781
+ const end = last.point(
782
+ this.bounds(last.pageNumber, last.content.text.length).to,
783
+ true
784
+ );
785
+ if (!start || !end) return;
786
+ const backwards = compare(this.snapshot.anchor, this.snapshot.focus) > 0;
787
+ const anchor = backwards ? end : start;
788
+ const focus = backwards ? start : end;
789
+ const selection = this.container.ownerDocument.getSelection();
790
+ this.projection = { anchor, focus };
791
+ if (!samePoint(anchor, selection.anchorNode, selection.anchorOffset) || !samePoint(focus, selection.focusNode, selection.focusOffset))
792
+ selection.setBaseAndExtent(
793
+ anchor.node,
794
+ anchor.offset,
795
+ focus.node,
796
+ focus.offset
797
+ );
798
+ }
799
+ hydrate() {
800
+ if (!this.snapshot || this.complete() || !this.container) {
801
+ return;
802
+ }
803
+ const range = this.ordered();
804
+ const owner = this.container.ownerDocument;
805
+ const controller = new AbortController();
806
+ this.work = controller;
807
+ const generation = this.generation;
808
+ let next = range.start.pageNumber;
809
+ const worker = async () => {
810
+ while (next <= range.end.pageNumber) {
811
+ controller.signal.throwIfAborted();
812
+ const n = next++;
813
+ const cached = this.pages.get(n);
814
+ if (cached) {
815
+ const bounds = this.bounds(n, cached.content.text.length);
816
+ if (bounds.from === cached.from && bounds.to === cached.to) continue;
817
+ }
818
+ const page = await this.options.loadPage(n);
819
+ controller.signal.throwIfAborted();
820
+ await withSelectionPage(
821
+ page,
822
+ owner,
823
+ controller.signal,
824
+ (page2) => this.remember(page2)
825
+ );
826
+ }
827
+ };
828
+ void Promise.all([worker(), worker()]).then(() => {
829
+ if (!controller.signal.aborted && generation === this.generation) {
830
+ this.finishSelection();
831
+ this.schedule();
832
+ }
833
+ }).catch((error) => {
834
+ if (!controller.signal.aborted && generation === this.generation && this.snapshot) {
835
+ this.publish({ ...this.snapshot, status: "error", error });
836
+ controller.abort();
837
+ }
838
+ });
839
+ }
840
+ caret(x, y) {
841
+ const owner = this.container.ownerDocument;
842
+ const doc = owner;
843
+ const position = doc.caretPositionFromPoint?.(x, y);
844
+ if (position) return this.anchorFor(position.offsetNode, position.offset);
845
+ const range = doc.caretRangeFromPoint?.(x, y);
846
+ return range ? this.anchorFor(range.startContainer, range.startOffset) : null;
847
+ }
848
+ autoScroll() {
849
+ if (!this.drag?.moved || !this.container) return;
850
+ const bounds = this.container.getBoundingClientRect();
851
+ const { x, y } = this.drag;
852
+ const edge = Math.min(40, bounds.height / 4);
853
+ const delta = y < bounds.top + edge ? -Math.min(24, (bounds.top + edge - y) * 0.5) : y > bounds.bottom - edge ? Math.min(24, (y - bounds.bottom + edge) * 0.5) : 0;
854
+ if (delta) this.container.scrollTop += delta;
855
+ const focus = this.caret(
856
+ Math.max(bounds.left + 1, Math.min(x, bounds.right - 1)),
857
+ Math.max(bounds.top + 1, Math.min(y, bounds.bottom - 1))
858
+ );
859
+ if (focus) this.setSelection(this.drag.anchor, focus);
860
+ if (delta) this.schedule();
861
+ }
862
+ flushKeyboard() {
863
+ if (!this.snapshot || !this.keyboardMoves.length || this.keyboardWork)
864
+ return;
865
+ const page = this.layers.get(this.snapshot.focus.pageNumber);
866
+ if (!page) {
867
+ this.options.revealPage?.(this.snapshot.focus.pageNumber);
868
+ return;
869
+ }
870
+ const move = this.keyboardMoves.shift();
871
+ const native = this.container.ownerDocument.getSelection();
872
+ if (!native?.modify) return;
873
+ const { anchor, focus } = this.snapshot;
874
+ native.modify("extend", move.direction, move.granularity);
875
+ const next = this.anchorFor(native.focusNode, native.focusOffset);
876
+ if (next && !sameAnchor(next, focus)) {
877
+ this.setSelection(anchor, next);
878
+ } else {
879
+ const point = page.point(focus.offset);
880
+ const rtl = point && this.container.ownerDocument.defaultView.getComputedStyle(
881
+ point.node.parentElement
882
+ ).direction === "rtl";
883
+ const backwards = move.direction === "backward" || move.direction === (rtl ? "right" : "left");
884
+ if (move.granularity === "lineboundary" || (backwards ? focus.offset > (move.granularity === "line" ? page.content.lineBreaks[0] ?? 0 : 0) : focus.offset < (move.granularity === "line" ? page.content.lineBreaks.at(-1) ?? page.content.text.length : page.content.text.length))) {
885
+ this.schedule();
886
+ return;
887
+ }
888
+ const n = focus.pageNumber + (backwards ? -1 : 1);
889
+ if (n >= 1 && n <= this.options.pageCount) {
890
+ if (!backwards) {
891
+ this.setSelection(anchor, { pageNumber: n, offset: 0 });
892
+ this.options.revealPage?.(n);
893
+ } else {
894
+ const generation = this.generation;
895
+ const controller = new AbortController();
896
+ this.keyboardWork = controller;
897
+ const owner = this.container.ownerDocument;
898
+ void this.options.loadPage(n).then(
899
+ (proxy) => withSelectionPage(
900
+ proxy,
901
+ owner,
902
+ controller.signal,
903
+ (page2) => page2.content.text.length
904
+ )
905
+ ).then((length) => {
906
+ if (controller.signal.aborted || generation !== this.generation || !this.container)
907
+ return;
908
+ this.setSelection(anchor, { pageNumber: n, offset: length });
909
+ this.options.revealPage?.(n);
910
+ }).catch(() => {
911
+ }).finally(() => {
912
+ if (this.keyboardWork === controller) this.keyboardWork = null;
913
+ this.schedule();
914
+ });
915
+ }
916
+ }
917
+ }
918
+ if (this.keyboardMoves.length) this.schedule();
919
+ }
920
+ connect(container) {
921
+ this.connectedCleanup?.();
922
+ this.container = container;
923
+ const owner = container.ownerDocument;
924
+ const events = new AbortController();
925
+ const { signal } = events;
926
+ const down = (event) => {
927
+ if (event.defaultPrevented) return;
928
+ const target = event.target instanceof Element ? event.target : null;
929
+ if (target?.closest("button, a, [role=button], [data-selection-tooltip]"))
930
+ return;
931
+ if (!container.contains(event.target)) {
932
+ this.clear();
933
+ return;
934
+ }
935
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || !container.contains(event.target))
936
+ return;
937
+ const point = this.caret(event.clientX, event.clientY);
938
+ if (!point) return;
939
+ const anchor = event.shiftKey && this.snapshot ? this.snapshot.anchor : point;
940
+ if (!event.shiftKey) this.clear();
941
+ this.drag = {
942
+ anchor,
943
+ x: event.clientX,
944
+ y: event.clientY,
945
+ pointerId: event.pointerId,
946
+ moved: false,
947
+ started: false,
948
+ shift: event.shiftKey
949
+ };
950
+ this.schedule();
951
+ };
952
+ const move = (event) => {
953
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
954
+ this.drag.moved ||= Math.abs(this.drag.x - event.clientX) + Math.abs(this.drag.y - event.clientY) > 2;
955
+ this.drag.x = event.clientX;
956
+ this.drag.y = event.clientY;
957
+ this.schedule();
958
+ };
959
+ const up = () => {
960
+ this.capture();
961
+ this.drag = null;
962
+ };
963
+ owner.addEventListener("selectionchange", this.capture, { signal });
964
+ owner.addEventListener("pointerdown", down, { signal });
965
+ owner.addEventListener("pointermove", move, { signal });
966
+ owner.addEventListener("pointerup", up, { signal });
967
+ owner.addEventListener("pointercancel", up, { signal });
968
+ owner.defaultView.addEventListener("blur", up, { signal });
969
+ const cancelKeyboard = () => {
970
+ this.keyboardMoves = [];
971
+ this.keyboardWork?.abort();
972
+ this.keyboardWork = null;
973
+ };
974
+ container.addEventListener(
975
+ "focusout",
976
+ (event) => {
977
+ if (!container.contains(event.relatedTarget))
978
+ cancelKeyboard();
979
+ },
980
+ { signal }
981
+ );
982
+ owner.defaultView.addEventListener("blur", cancelKeyboard, { signal });
983
+ container.addEventListener(
984
+ "keydown",
985
+ (event) => {
986
+ if (event.defaultPrevented || event.target instanceof Element && event.target.closest(
987
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
988
+ ))
989
+ return;
990
+ if (event.key === "Escape" && this.snapshot) {
991
+ this.clear();
992
+ return;
993
+ }
994
+ if (!event.shiftKey || !this.snapshot) return;
995
+ const directions = {
996
+ ArrowLeft: "left",
997
+ ArrowRight: "right",
998
+ ArrowUp: "backward",
999
+ ArrowDown: "forward",
1000
+ Home: "backward",
1001
+ End: "forward"
1002
+ };
1003
+ const direction = directions[event.key];
1004
+ if (!direction) return;
1005
+ const native = owner.getSelection();
1006
+ if (!native?.modify) return;
1007
+ event.preventDefault();
1008
+ const boundary = event.key === "Home" || event.key === "End" || event.metaKey && (event.key === "ArrowLeft" || event.key === "ArrowRight");
1009
+ const granularity = boundary ? "lineboundary" : event.key === "ArrowUp" || event.key === "ArrowDown" ? "line" : event.ctrlKey || event.altKey ? "word" : "character";
1010
+ this.keyboardMoves.push({ direction, granularity });
1011
+ this.schedule();
1012
+ },
1013
+ { signal }
1014
+ );
1015
+ const cleanup = () => {
1016
+ events.abort();
1017
+ if (this.frame !== null)
1018
+ owner.defaultView.cancelAnimationFrame(this.frame);
1019
+ this.frame = null;
1020
+ this.clear();
1021
+ this.container = null;
1022
+ this.connectedCleanup = null;
1023
+ };
1024
+ this.connectedCleanup = cleanup;
1025
+ this.hydrate();
1026
+ this.schedule();
1027
+ return cleanup;
1028
+ }
1029
+ };
1030
+
222
1031
  // src/lib/viewport-zoom.ts
223
1032
  var viewportZoomHandlers = /* @__PURE__ */ new WeakMap();
224
1033
  function applyViewportZoom(viewport, zoom) {
@@ -268,6 +1077,11 @@ var PDFStore = createZustandContext(
268
1077
  renderColorMapRef.current = initialColorScheme === "dark" ? createDarkModeColorMap(initialDarkModeColors) : null;
269
1078
  return createStore((set, get) => ({
270
1079
  pdfDocumentProxy: initialState.pdfDocumentProxy,
1080
+ selection: new PDFSelectionController({
1081
+ pageCount: initialState.pdfDocumentProxy.numPages,
1082
+ loadPage: (page) => get().loadPdfPageProxy(page),
1083
+ revealPage: (page) => get().virtualizer?.scrollToIndex(page - 1, { align: "auto" })
1084
+ }),
271
1085
  zoom: initialState.zoom,
272
1086
  isZoomFitWidth: initialState.isZoomFitWidth ?? false,
273
1087
  zoomOptions: {
@@ -3104,6 +3918,114 @@ var mergeTextRuns = (rects) => {
3104
3918
  });
3105
3919
  };
3106
3920
 
3921
+ // src/lib/selection-text.ts
3922
+ function textLayer(node) {
3923
+ const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3924
+ return element?.closest(".textLayer[data-page-number]") ?? null;
3925
+ }
3926
+ function getPdfSelectionText(selection, container, options = {}) {
3927
+ if (!selection || selection.isCollapsed || selection.rangeCount !== 1)
3928
+ return null;
3929
+ const range = selection.getRangeAt(0);
3930
+ const first = textLayer(range.startContainer);
3931
+ const last = textLayer(range.endContainer);
3932
+ if (!first || !last || !container.contains(first) || !container.contains(last))
3933
+ return null;
3934
+ const layers = [
3935
+ ...container.querySelectorAll(".textLayer[data-page-number]")
3936
+ ].filter((layer) => range.intersectsNode(layer));
3937
+ let previousPage;
3938
+ for (const layer of layers) {
3939
+ const page = Number(layer.dataset.pageNumber);
3940
+ if (!Number.isInteger(page) || page < 1 || previousPage !== void 0 && page !== previousPage + 1)
3941
+ return null;
3942
+ previousPage = page;
3943
+ }
3944
+ const selectedLayers = new Set(layers);
3945
+ const root = range.commonAncestorContainer;
3946
+ const walker = container.ownerDocument.createTreeWalker(
3947
+ root,
3948
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
3949
+ {
3950
+ acceptNode(node2) {
3951
+ if (node2.nodeType !== Node.TEXT_NODE && node2.tagName !== "BR")
3952
+ return NodeFilter.FILTER_SKIP;
3953
+ const layer = textLayer(node2);
3954
+ return layer && selectedLayers.has(layer) && range.intersectsNode(node2) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
3955
+ }
3956
+ }
3957
+ );
3958
+ const chunks = [];
3959
+ let previousLayer = null;
3960
+ let trailingLineBreaks = 0;
3961
+ let node = root.nodeType === Node.TEXT_NODE ? root : walker.nextNode();
3962
+ while (node) {
3963
+ const layer = textLayer(node);
3964
+ const start = node === range.startContainer ? range.startOffset : 0;
3965
+ const end = node === range.endContainer ? range.endOffset : node.textContent?.length ?? 0;
3966
+ const text = node.nodeType === Node.TEXT_NODE ? node.textContent.slice(start, end) : "\n";
3967
+ if (text) {
3968
+ if (previousLayer && previousLayer !== layer && trailingLineBreaks < 2) {
3969
+ chunks.push("\n".repeat(2 - trailingLineBreaks));
3970
+ trailingLineBreaks = 2;
3971
+ }
3972
+ chunks.push(text);
3973
+ let trailing = 0;
3974
+ for (let i = text.length - 1; i >= 0 && text[i] === "\n"; i--) trailing++;
3975
+ trailingLineBreaks = trailing === text.length ? trailingLineBreaks + trailing : trailing;
3976
+ previousLayer = layer;
3977
+ }
3978
+ node = walker.nextNode();
3979
+ }
3980
+ return normalizeSelectionText(chunks.join(""), options);
3981
+ }
3982
+ var documents = /* @__PURE__ */ new WeakMap();
3983
+ function registerPdfCopy(container, options, read) {
3984
+ const owner = container.ownerDocument;
3985
+ let binding = documents.get(owner);
3986
+ if (!binding) {
3987
+ const registrations = /* @__PURE__ */ new Set();
3988
+ const listener = (event) => {
3989
+ if (event.defaultPrevented || !event.clipboardData) return;
3990
+ const target = event.target;
3991
+ if (target instanceof Element && target.closest(
3992
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
3993
+ ))
3994
+ return;
3995
+ for (const registration2 of registrations) {
3996
+ const settings = registration2.options();
3997
+ if (settings === false) continue;
3998
+ const persistent = registration2.read?.(settings);
3999
+ if (registration2.read && persistent === void 0) {
4000
+ event.preventDefault();
4001
+ break;
4002
+ }
4003
+ const text = persistent ?? getPdfSelectionText(
4004
+ owner.getSelection(),
4005
+ registration2.container,
4006
+ settings
4007
+ );
4008
+ if (text === null) continue;
4009
+ event.clipboardData.setData("text/plain", text);
4010
+ event.preventDefault();
4011
+ break;
4012
+ }
4013
+ };
4014
+ binding = { registrations, listener };
4015
+ documents.set(owner, binding);
4016
+ owner.addEventListener("copy", listener);
4017
+ }
4018
+ const registration = { container, options, read };
4019
+ binding.registrations.add(registration);
4020
+ return () => {
4021
+ binding.registrations.delete(registration);
4022
+ if (!binding.registrations.size) {
4023
+ owner.removeEventListener("copy", binding.listener);
4024
+ documents.delete(owner);
4025
+ }
4026
+ };
4027
+ }
4028
+
3107
4029
  // src/hooks/useSelectionDimensions.tsx
3108
4030
  var MERGE_THRESHOLD = 2;
3109
4031
  var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
@@ -3151,7 +4073,19 @@ var mapSelectionRectsToLayers = (range) => {
3151
4073
  };
3152
4074
  var useSelectionDimensions = () => {
3153
4075
  const store = PDFStore.useContext();
4076
+ const getText = (options) => {
4077
+ if (store.getState().selection?.getSnapshot())
4078
+ return store.getState().selection.getText(options);
4079
+ const container = store.getState().viewportRef.current;
4080
+ return container ? getPdfSelectionText(
4081
+ container.ownerDocument.getSelection(),
4082
+ container,
4083
+ options
4084
+ ) : null;
4085
+ };
3154
4086
  const getAnnotationDimension = () => {
4087
+ if (store.getState().selection?.getSnapshot())
4088
+ return store.getState().selection.getSelection() ?? void 0;
3155
4089
  const selection = window.getSelection();
3156
4090
  if (!selection || selection.isCollapsed) return;
3157
4091
  const range = selection.getRangeAt(0);
@@ -3289,7 +4223,7 @@ var useSelectionDimensions = () => {
3289
4223
  underlines: consolidateUnderlines(underlineRects).sort(
3290
4224
  (a, b) => a.pageNumber - b.pageNumber
3291
4225
  ),
3292
- text: range.toString().trim(),
4226
+ text: (getText() ?? range.toString()).trim(),
3293
4227
  isCollapsed: false
3294
4228
  };
3295
4229
  };
@@ -3346,6 +4280,8 @@ var useSelectionDimensions = () => {
3346
4280
  return consolidated;
3347
4281
  };
3348
4282
  const getDimension = () => {
4283
+ if (store.getState().selection?.getSnapshot())
4284
+ return store.getState().selection.getSelection() ?? void 0;
3349
4285
  const selection = window.getSelection();
3350
4286
  if (!selection || selection.isCollapsed) return;
3351
4287
  const range = selection.getRangeAt(0);
@@ -3374,12 +4310,12 @@ var useSelectionDimensions = () => {
3374
4310
  });
3375
4311
  return {
3376
4312
  highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber),
3377
- text: range.toString().trim(),
4313
+ text: (getText() ?? range.toString()).trim(),
3378
4314
  isCollapsed: false
3379
4315
  };
3380
4316
  };
3381
4317
  const getSelection = () => getDimension();
3382
- return { getDimension, getSelection, getAnnotationDimension };
4318
+ return { getDimension, getSelection, getAnnotationDimension, getText };
3383
4319
  };
3384
4320
  var SelectionTooltip = ({ children }) => {
3385
4321
  const [isOpen, setIsOpen] = useState(false);
@@ -3514,6 +4450,7 @@ var SelectionTooltip = ({ children }) => {
3514
4450
  return /* @__PURE__ */ jsx(Fragment, { children: isOpen && /* @__PURE__ */ jsx(
3515
4451
  "div",
3516
4452
  {
4453
+ "data-selection-tooltip": "",
3517
4454
  ref: refs.setFloating,
3518
4455
  style: {
3519
4456
  ...floatingStyles
@@ -3929,13 +4866,13 @@ var createTextSelectionManager = () => {
3929
4866
  }
3930
4867
  selectionChangeAbortController = new AbortController();
3931
4868
  const { signal } = selectionChangeAbortController;
3932
- const reset = (endDiv, textLayer) => {
3933
- if (endDiv.parentNode !== textLayer) {
3934
- textLayer.appendChild(endDiv);
4869
+ const reset = (endDiv, textLayer2) => {
4870
+ if (endDiv.parentNode !== textLayer2) {
4871
+ textLayer2.appendChild(endDiv);
3935
4872
  }
3936
4873
  endDiv.style.width = "";
3937
4874
  endDiv.style.height = "";
3938
- textLayer.classList.remove("selecting");
4875
+ textLayer2.classList.remove("selecting");
3939
4876
  };
3940
4877
  document.addEventListener(
3941
4878
  "pointerdown",
@@ -4061,12 +4998,14 @@ var useTextLayer = () => {
4061
4998
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
4062
4999
  const viewportRef = usePdf((state) => state.viewportRef);
4063
5000
  const store = PDFStore.useContext();
5001
+ const selection = store.getState().selection;
4064
5002
  useEffect(() => {
4065
5003
  const textContainer = textContainerRef.current;
4066
5004
  if (!textContainer) {
4067
5005
  return;
4068
5006
  }
4069
5007
  let isCancelled = false;
5008
+ let unregisterSelection;
4070
5009
  let buildTimer = null;
4071
5010
  let unsubscribe = null;
4072
5011
  let built = false;
@@ -4083,13 +5022,13 @@ var useTextLayer = () => {
4083
5022
  void import('pdfjs-dist/legacy/build/pdf.mjs').then(({ TextLayer: TextLayer2 }) => {
4084
5023
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer)
4085
5024
  return;
4086
- const textLayer = new TextLayer2({
5025
+ const textLayer2 = new TextLayer2({
4087
5026
  textContentSource: pdfPageProxy.streamTextContent(),
4088
5027
  container: textContainer,
4089
5028
  viewport: pdfPageProxy.getViewport({ scale: 1 })
4090
5029
  });
4091
- textLayerRef.current = textLayer;
4092
- return textLayer.render();
5030
+ textLayerRef.current = textLayer2;
5031
+ return textLayer2.render();
4093
5032
  }).then(() => {
4094
5033
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer) {
4095
5034
  return;
@@ -4098,6 +5037,16 @@ var useTextLayer = () => {
4098
5037
  endOfContent.className = "endOfContent";
4099
5038
  textContainer.appendChild(endOfContent);
4100
5039
  bindMouseEvents(textContainer, endOfContent);
5040
+ const viewport = pdfPageProxy.getViewport({ scale: 1 });
5041
+ const unrotated = pdfPageProxy.getViewport({ scale: 1, rotation: 0 });
5042
+ textContainer.style.width = `${unrotated.width}px`;
5043
+ textContainer.style.height = `${unrotated.height}px`;
5044
+ unregisterSelection = selection?.registerLayer(
5045
+ textContainer,
5046
+ pageNumber,
5047
+ viewport.width,
5048
+ viewport.height
5049
+ );
4101
5050
  built = true;
4102
5051
  building = false;
4103
5052
  unsubscribe?.();
@@ -4111,6 +5060,12 @@ var useTextLayer = () => {
4111
5060
  };
4112
5061
  const schedule = () => {
4113
5062
  if (built) return;
5063
+ if (selection?.isDragging || selection?.includesPage(pageNumber)) {
5064
+ if (buildTimer) clearTimeout(buildTimer);
5065
+ buildTimer = null;
5066
+ if (!building) build();
5067
+ return;
5068
+ }
4114
5069
  if (building) {
4115
5070
  buildGen++;
4116
5071
  building = false;
@@ -4140,9 +5095,15 @@ var useTextLayer = () => {
4140
5095
  unsubscribe = subscribeToViewportInvalidation(scrollViewport, schedule);
4141
5096
  };
4142
5097
  attachScrollSubscription();
5098
+ const unsubscribeSelection = selection?.subscribe(() => {
5099
+ if (selection.isDragging || selection.includesPage(pageNumber))
5100
+ schedule();
5101
+ });
4143
5102
  schedule();
4144
5103
  return () => {
4145
5104
  isCancelled = true;
5105
+ unregisterSelection?.();
5106
+ unsubscribeSelection?.();
4146
5107
  if (buildTimer) clearTimeout(buildTimer);
4147
5108
  if (attachRaf !== null) cancelAnimationFrame(attachRaf);
4148
5109
  unsubscribe?.();
@@ -4155,7 +5116,13 @@ var useTextLayer = () => {
4155
5116
  delete textContainer._cleanupTextSelection;
4156
5117
  }
4157
5118
  };
4158
- }, [pdfPageProxy.streamTextContent, pdfPageProxy.getViewport, viewportRef]);
5119
+ }, [
5120
+ pdfPageProxy.streamTextContent,
5121
+ pdfPageProxy.getViewport,
5122
+ viewportRef,
5123
+ selection,
5124
+ pageNumber
5125
+ ]);
4159
5126
  useEffect(() => {
4160
5127
  const textContainer = textContainerRef.current;
4161
5128
  if (!textContainer) return;
@@ -4174,7 +5141,10 @@ var useTextLayer = () => {
4174
5141
  show();
4175
5142
  };
4176
5143
  const hideAndDebounce = () => {
4177
- if (selecting) return;
5144
+ if (selecting || selection?.isDragging || selection?.includesPage(pageNumber)) {
5145
+ show();
5146
+ return;
5147
+ }
4178
5148
  textContainer.style.visibility = "hidden";
4179
5149
  if (settleTimer) clearTimeout(settleTimer);
4180
5150
  settleTimer = setTimeout(restoreWhenSettled, TEXT_SCROLL_SETTLE_MS);
@@ -4242,7 +5212,7 @@ var useTextLayer = () => {
4242
5212
  if (settleTimer) clearTimeout(settleTimer);
4243
5213
  show();
4244
5214
  };
4245
- }, [viewportRef, store]);
5215
+ }, [viewportRef, store, selection, pageNumber]);
4246
5216
  return {
4247
5217
  textContainerRef,
4248
5218
  pageNumber: pdfPageProxy.pageNumber
@@ -6988,16 +7958,34 @@ var Pages = ({
6988
7958
  virtualizerOptions = DEFAULT_VIRTUALIZER_OPTIONS,
6989
7959
  initialOffset,
6990
7960
  onOffsetChange,
7961
+ copyOptions,
6991
7962
  ...props
6992
7963
  }) => {
6993
7964
  const [tempItems, setTempItems] = useState([]);
6994
7965
  const viewports = usePdf((state) => state.viewports);
7966
+ const selection = usePdf((state) => state.selection);
6995
7967
  const numPages = usePdf((state) => state.pdfDocumentProxy.numPages);
6996
7968
  const initialPage = usePdf((state) => state.initialPage);
6997
7969
  const isPinching = usePdf((state) => state.isPinching);
6998
7970
  const elementWrapperRef = useRef(null);
6999
7971
  const elementRef = useRef(null);
7000
7972
  const containerRef = useRef(null);
7973
+ const copyOptionsRef = useRef(copyOptions);
7974
+ copyOptionsRef.current = copyOptions;
7975
+ useEffect(() => {
7976
+ const container = containerRef.current;
7977
+ if (!container) return;
7978
+ const disconnect = selection.connect(container);
7979
+ const unregisterCopy = registerPdfCopy(
7980
+ container,
7981
+ () => copyOptionsRef.current ?? {},
7982
+ (options) => selection.getSnapshot() ? selection.getText(options) ?? void 0 : null
7983
+ );
7984
+ return () => {
7985
+ unregisterCopy();
7986
+ disconnect();
7987
+ };
7988
+ }, [selection]);
7001
7989
  useViewportContainer({
7002
7990
  elementRef,
7003
7991
  elementWrapperRef,
@@ -7115,6 +8103,7 @@ var Pages = ({
7115
8103
  Primitive.div,
7116
8104
  {
7117
8105
  ref: containerRef,
8106
+ tabIndex: 0,
7118
8107
  ...props,
7119
8108
  style: {
7120
8109
  display: "flex",
@@ -7759,6 +8748,8 @@ function acquireDocumentText(pages) {
7759
8748
  const page = pages[index];
7760
8749
  const reader = page.streamTextContent().getReader();
7761
8750
  const chunks = [];
8751
+ const lineBreaks = [];
8752
+ let offset3 = 0;
7762
8753
  let complete = false;
7763
8754
  try {
7764
8755
  while (true) {
@@ -7768,9 +8759,13 @@ function acquireDocumentText(pages) {
7768
8759
  complete = true;
7769
8760
  break;
7770
8761
  }
7771
- chunks.push(
7772
- value.items.map((item) => "str" in item ? item.str : "").join("")
7773
- );
8762
+ for (const item of value.items) {
8763
+ if (!("str" in item)) continue;
8764
+ chunks.push(item.str);
8765
+ offset3 += item.str.length;
8766
+ if (item.hasEOL && lineBreaks.at(-1) !== offset3)
8767
+ lineBreaks.push(offset3);
8768
+ }
7774
8769
  }
7775
8770
  } finally {
7776
8771
  if (!complete)
@@ -7778,7 +8773,11 @@ function acquireDocumentText(pages) {
7778
8773
  });
7779
8774
  reader.releaseLock();
7780
8775
  }
7781
- result[index] = { pageNumber: page.pageNumber, text: chunks.join("") };
8776
+ result[index] = {
8777
+ pageNumber: page.pageNumber,
8778
+ text: chunks.join(""),
8779
+ ...lineBreaks.length ? { lineBreaks } : {}
8780
+ };
7782
8781
  }
7783
8782
  };
7784
8783
  const created = {
@@ -8187,33 +9186,31 @@ var CurrentZoom = ({ ...props }) => {
8187
9186
 
8188
9187
  // src/lib/search.ts
8189
9188
  var normalizedPages = /* @__PURE__ */ new WeakMap();
8190
- function normalize(page) {
8191
- const cached = normalizedPages.get(page);
8192
- if (cached?.text === page.text) return cached;
8193
- const lower = page.text.toLowerCase();
8194
- const result = { text: page.text, lower };
8195
- if (lower.length !== page.text.length) {
8196
- result.starts = new Uint32Array(lower.length);
8197
- result.ends = new Uint32Array(lower.length);
8198
- let original = 0;
8199
- let normalized = 0;
8200
- for (const character of page.text) {
8201
- const length = character.toLowerCase().length;
8202
- for (let i = 0; i < length; i++) {
8203
- result.starts[normalized + i] = original + (length === character.length ? i : 0);
8204
- result.ends[normalized + i] = original + (length === character.length ? i + 1 : character.length);
8205
- }
8206
- original += character.length;
8207
- normalized += length;
8208
- }
8209
- }
8210
- normalizedPages.set(page, result);
9189
+ function normalize(page, options) {
9190
+ let cached = normalizedPages.get(page);
9191
+ if (cached?.text !== page.text || cached.lineBreaks !== page.lineBreaks) {
9192
+ cached = {
9193
+ text: page.text,
9194
+ lineBreaks: page.lineBreaks,
9195
+ variants: /* @__PURE__ */ new Map()
9196
+ };
9197
+ normalizedPages.set(page, cached);
9198
+ }
9199
+ const key = (options.matchDiacritics === false ? 1 : 0) | (options.ignoreHyphenation ? 2 : 0);
9200
+ let result = cached.variants.get(key);
9201
+ if (!result) {
9202
+ result = normalizeSearchText(page.text, options, page.lineBreaks);
9203
+ cached.variants.set(key, result);
9204
+ }
8211
9205
  return result;
8212
9206
  }
8213
9207
  function createMatch(page, normalized, start, length, searchText, textSize, score) {
8214
- const end = Math.min(normalized.lower.length, start + length);
8215
- const matchIndex = normalized.starts?.[start] ?? start;
8216
- const matchEnd = normalized.ends?.[end - 1] ?? end;
9208
+ const end = Math.min(normalized.text.length, start + length);
9209
+ const { start: matchIndex, end: matchEnd } = originalTextRange(
9210
+ normalized,
9211
+ start,
9212
+ end
9213
+ );
8217
9214
  const matchLength = matchEnd - matchIndex;
8218
9215
  return {
8219
9216
  pageNumber: page.pageNumber,
@@ -8276,7 +9273,8 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8276
9273
  const threshold = Math.max(0, Math.min(1, options.threshold ?? 0.7));
8277
9274
  const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : 10;
8278
9275
  const textSize = options.textSize ?? 100;
8279
- const query = searchText.toLowerCase();
9276
+ const query = normalizeSearchText(searchText, options).text;
9277
+ if (!query.trim()) return empty;
8280
9278
  const maxDistance = Math.floor(query.length * (1 - threshold));
8281
9279
  const distance = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
8282
9280
  const exactMatches = [];
@@ -8286,10 +9284,12 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8286
9284
  let steps = 0;
8287
9285
  let deadline = cooperative ? performance.now() + budget : 0;
8288
9286
  for (const page of pages) {
8289
- const normalized = normalize(page);
8290
- const lower = normalized.lower;
9287
+ const normalized = normalize(page, options);
9288
+ const lower = normalized.text;
8291
9289
  const exactStarts = [];
8292
9290
  let index = 0;
9291
+ let lastStart = -1;
9292
+ let lastEnd = -1;
8293
9293
  while (true) {
8294
9294
  if (cooperative && (++steps & 127) === 0 && performance.now() >= deadline) {
8295
9295
  yield;
@@ -8297,8 +9297,17 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8297
9297
  }
8298
9298
  const matchIndex = lower.indexOf(query, index);
8299
9299
  if (matchIndex === -1) break;
8300
- exactCount++;
9300
+ const span = originalTextRange(
9301
+ normalized,
9302
+ matchIndex,
9303
+ matchIndex + query.length
9304
+ );
9305
+ index = matchIndex + query.length;
8301
9306
  if (maxDistance > 0) exactStarts.push(matchIndex);
9307
+ if (span.start === lastStart && span.end === lastEnd) continue;
9308
+ lastStart = span.start;
9309
+ lastEnd = span.end;
9310
+ exactCount++;
8302
9311
  if (exactMatches.length < limit) {
8303
9312
  exactMatches.push(
8304
9313
  createMatch(
@@ -8579,5 +9588,24 @@ function mergeAdjacentRects(rects) {
8579
9588
 
8580
9589
  // src/hooks/usePageRendered.ts
8581
9590
  var usePageRendered = (pageNumber) => usePdf((state) => !!state.renderedPages[pageNumber]);
9591
+ function usePdfSelection() {
9592
+ const controller = usePdf((state) => state.selection);
9593
+ const selection = useSyncExternalStore(
9594
+ controller.subscribe,
9595
+ controller.getSnapshot,
9596
+ controller.getSnapshot
9597
+ );
9598
+ return useMemo(
9599
+ () => ({
9600
+ selection,
9601
+ setSelection: controller.setSelection,
9602
+ clearSelection: controller.clear,
9603
+ getText: controller.getText,
9604
+ getSelection: controller.getSelection,
9605
+ getSelectionAsync: controller.getSelectionAsync
9606
+ }),
9607
+ [controller, selection]
9608
+ );
9609
+ }
8582
9610
 
8583
- 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 };
9611
+ 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, usePdfSelection, useSearch, useSelectionDimensions };