@anaralabs/lector 3.14.13 → 3.15.1

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,825 @@ 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
+ for (const match of text.matchAll(/\p{ASCII}?\P{ASCII}+/gu)) {
405
+ if (normalizeUnicode(match[0], matchDiacritics) === match[0]) continue;
406
+ const prefix = match[0].charCodeAt(0) < 128 ? 1 : 0;
407
+ const code = match[0].charCodeAt(prefix);
408
+ if (match[0].length === prefix + 1 && code >= 64256 && code <= 64262) {
409
+ yield {
410
+ index: match.index + prefix,
411
+ length: 1,
412
+ text: ligatures[code - 64256]
413
+ };
414
+ continue;
415
+ }
416
+ segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
417
+ for (const { segment, index } of segmenter.segment(match[0])) {
418
+ yield {
419
+ index: match.index + index,
420
+ length: segment.length,
421
+ text: normalizeUnicode(segment, matchDiacritics)
422
+ };
423
+ }
424
+ }
425
+ }
426
+ function normalizeSearchText(text, options = {}, lineBreaks = []) {
427
+ let result = { text, maps: [] };
428
+ if (lineBreaks.length)
429
+ result = replace(result, insertLineBreaks(text, lineBreaks));
430
+ const matchDiacritics = options.matchDiacritics !== false;
431
+ if (normalizeUnicode(result.text, matchDiacritics) !== result.text) {
432
+ result = replace(result, unicodeReplacements(result.text, matchDiacritics));
433
+ }
434
+ result = replace(result, replacements(result.text, softHyphen, ""));
435
+ if (options.ignoreHyphenation) {
436
+ result = replace(result, replacements(result.text, wrappedHyphen, ""));
437
+ }
438
+ result = replace(result, replacements(result.text, cjkLineBreak, ""));
439
+ result = replace(result, replacements(result.text, /\s+/gu, " "));
440
+ const lower = result.text.toLowerCase();
441
+ if (lower.length !== result.text.length) {
442
+ function* caseReplacements(value) {
443
+ let index = 0;
444
+ for (const char of value) {
445
+ const folded = char.toLowerCase();
446
+ if (folded.length !== char.length)
447
+ yield { index, length: char.length, text: folded };
448
+ index += char.length;
449
+ }
450
+ }
451
+ result = replace(result, caseReplacements(result.text));
452
+ }
453
+ return { ...result, text: lower };
454
+ }
455
+ function sourceOffset(edits, offset3, end) {
456
+ let low = 0;
457
+ let high = edits.length;
458
+ while (low < high) {
459
+ const middle = low + high >>> 1;
460
+ if (edits[middle].start <= offset3) low = middle + 1;
461
+ else high = middle;
462
+ }
463
+ const edit = edits[low - 1];
464
+ if (!edit) return offset3 + (end ? 1 : 0);
465
+ if (offset3 < edit.end) return end ? edit.sourceEnd : edit.sourceStart;
466
+ return offset3 + edit.sourceEnd - edit.end + (end ? 1 : 0);
467
+ }
468
+ function originalTextRange(normalized, start, end) {
469
+ for (let i = normalized.maps.length - 1; i >= 0; i--) {
470
+ const map = normalized.maps[i];
471
+ start = sourceOffset(map, start, false);
472
+ end = sourceOffset(map, end - 1, true);
473
+ }
474
+ return { start, end };
475
+ }
476
+
477
+ // src/lib/pdf-selection.ts
478
+ var sameAnchor = (a, b) => a.pageNumber === b.pageNumber && a.offset === b.offset;
479
+ var compare = (a, b) => a.pageNumber - b.pageNumber || a.offset - b.offset;
480
+ var samePoint = (a, node, offset3) => a.node === node && a.offset === offset3;
481
+ var selectionOwners = /* @__PURE__ */ new WeakMap();
482
+ var PDFSelectionController = class {
483
+ constructor(options) {
484
+ this.options = options;
485
+ }
486
+ snapshot = null;
487
+ listeners = /* @__PURE__ */ new Set();
488
+ layers = /* @__PURE__ */ new Map();
489
+ pages = /* @__PURE__ */ new Map();
490
+ container = null;
491
+ projection = null;
492
+ frame = null;
493
+ work = null;
494
+ structuralChange = false;
495
+ drag = null;
496
+ connectedCleanup = null;
497
+ generation = 0;
498
+ keyboardWork = null;
499
+ keyboardMoves = [];
500
+ getSnapshot = () => this.snapshot;
501
+ subscribe = (listener) => {
502
+ this.listeners.add(listener);
503
+ return () => {
504
+ this.listeners.delete(listener);
505
+ };
506
+ };
507
+ get isDragging() {
508
+ return this.drag !== null;
509
+ }
510
+ includesPage(page) {
511
+ const range = this.ordered();
512
+ return Boolean(
513
+ range && page >= range.start.pageNumber && page <= range.end.pageNumber
514
+ );
515
+ }
516
+ publish(snapshot) {
517
+ this.snapshot = snapshot;
518
+ for (const listener of this.listeners) listener();
519
+ }
520
+ ordered() {
521
+ if (!this.snapshot) return null;
522
+ const { anchor, focus } = this.snapshot;
523
+ return compare(anchor, focus) <= 0 ? { start: anchor, end: focus } : { start: focus, end: anchor };
524
+ }
525
+ bounds(page, length) {
526
+ const range = this.ordered();
527
+ return {
528
+ from: page === range.start.pageNumber ? Math.min(range.start.offset, length) : 0,
529
+ to: page === range.end.pageNumber ? Math.min(range.end.offset, length) : length
530
+ };
531
+ }
532
+ remember(page) {
533
+ if (!this.includesPage(page.pageNumber)) return;
534
+ for (const point of [this.snapshot.anchor, this.snapshot.focus]) {
535
+ if (point.pageNumber === page.pageNumber && point.offset > page.content.text.length)
536
+ throw new RangeError("Selection offset exceeds page text length");
537
+ }
538
+ const { from, to } = this.bounds(page.pageNumber, page.content.text.length);
539
+ const cached = this.pages.get(page.pageNumber);
540
+ if (cached?.content.text === page.content.text && cached.from === from && cached.to === to)
541
+ return;
542
+ this.pages.set(page.pageNumber, {
543
+ content: page.content,
544
+ from,
545
+ to,
546
+ rects: page.rects(from, to)
547
+ });
548
+ }
549
+ complete() {
550
+ const range = this.ordered();
551
+ if (!range) return false;
552
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++) {
553
+ const page = this.pages.get(n);
554
+ if (!page) return false;
555
+ const bounds = this.bounds(n, page.content.text.length);
556
+ if (page.from !== bounds.from || page.to !== bounds.to) return false;
557
+ }
558
+ return true;
559
+ }
560
+ setSelection = (anchor, focus) => {
561
+ for (const point of [anchor, focus]) {
562
+ const text = this.layers.get(point.pageNumber)?.content.text ?? this.pages.get(point.pageNumber)?.content.text;
563
+ if (text !== void 0 && point.offset > text.length)
564
+ throw new RangeError("Selection offset exceeds page text length");
565
+ if (!Number.isInteger(point.pageNumber) || point.pageNumber < 1 || point.pageNumber > this.options.pageCount || !Number.isInteger(point.offset) || point.offset < 0)
566
+ throw new RangeError(
567
+ "Selection anchors need a valid page and nonnegative UTF-16 offset"
568
+ );
569
+ }
570
+ if (sameAnchor(anchor, focus)) {
571
+ const drag = this.drag;
572
+ this.clear();
573
+ this.drag = drag;
574
+ return;
575
+ }
576
+ if (this.snapshot && this.snapshot.status !== "error" && sameAnchor(this.snapshot.anchor, anchor) && sameAnchor(this.snapshot.focus, focus))
577
+ return;
578
+ if (this.container) {
579
+ const owner = this.container.ownerDocument;
580
+ const previous = selectionOwners.get(owner);
581
+ if (previous !== this) previous?.clear();
582
+ selectionOwners.set(owner, this);
583
+ }
584
+ this.work?.abort();
585
+ this.keyboardWork?.abort();
586
+ this.keyboardWork = null;
587
+ this.generation++;
588
+ this.snapshot = {
589
+ anchor: { ...anchor },
590
+ focus: { ...focus },
591
+ status: "loading"
592
+ };
593
+ for (const n of this.pages.keys())
594
+ if (!this.includesPage(n)) this.pages.delete(n);
595
+ for (const page of this.layers.values()) this.remember(page);
596
+ this.publish({
597
+ ...this.snapshot,
598
+ status: this.complete() ? "ready" : "loading"
599
+ });
600
+ this.project();
601
+ this.schedule();
602
+ this.hydrate();
603
+ };
604
+ clear = () => {
605
+ const owner = this.container?.ownerDocument;
606
+ if (owner && selectionOwners.get(owner) === this)
607
+ selectionOwners.delete(owner);
608
+ this.work?.abort();
609
+ this.keyboardWork?.abort();
610
+ this.keyboardWork = null;
611
+ this.generation++;
612
+ this.pages.clear();
613
+ this.keyboardMoves = [];
614
+ this.drag = null;
615
+ const selection = this.container?.ownerDocument.getSelection();
616
+ if (this.projection && selection && samePoint(
617
+ this.projection.anchor,
618
+ selection.anchorNode,
619
+ selection.anchorOffset
620
+ ) && samePoint(
621
+ this.projection.focus,
622
+ selection.focusNode,
623
+ selection.focusOffset
624
+ ))
625
+ selection.removeAllRanges();
626
+ this.projection = null;
627
+ if (this.snapshot) this.publish(null);
628
+ };
629
+ getText = (options = {}) => {
630
+ const range = this.ordered();
631
+ if (!range || !this.complete()) return null;
632
+ const chunks = [];
633
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++) {
634
+ const page = this.pages.get(n);
635
+ const pieces = [];
636
+ let offset3 = page.from;
637
+ for (const end of page.content.lineBreaks) {
638
+ if (end <= page.from || end >= page.to) continue;
639
+ pieces.push(page.content.text.slice(offset3, end), "\n");
640
+ offset3 = end;
641
+ }
642
+ pieces.push(page.content.text.slice(offset3, page.to));
643
+ chunks.push(pieces.join(""));
644
+ }
645
+ return normalizeSelectionText(chunks.join("\n\n"), options);
646
+ };
647
+ getSelection = (options) => {
648
+ const text = this.getText(options);
649
+ if (text === null || !this.snapshot) return null;
650
+ const range = this.ordered();
651
+ const highlights = [];
652
+ for (let n = range.start.pageNumber; n <= range.end.pageNumber; n++)
653
+ highlights.push(...this.pages.get(n).rects.map((rect) => ({ ...rect })));
654
+ return {
655
+ anchor: { ...this.snapshot.anchor },
656
+ focus: { ...this.snapshot.focus },
657
+ text,
658
+ highlights,
659
+ underlines: highlights.map((rect) => ({
660
+ ...rect,
661
+ top: rect.top + rect.height - 1,
662
+ height: 1
663
+ })),
664
+ isCollapsed: false
665
+ };
666
+ };
667
+ getSelectionAsync = async (options) => {
668
+ const generation = this.generation;
669
+ await new Promise((resolve) => {
670
+ if (this.snapshot?.status !== "loading") {
671
+ resolve();
672
+ return;
673
+ }
674
+ const unsubscribe = this.subscribe(() => {
675
+ if (generation !== this.generation || this.snapshot?.status !== "loading") {
676
+ unsubscribe();
677
+ resolve();
678
+ }
679
+ });
680
+ });
681
+ if (generation !== this.generation)
682
+ throw new DOMException("Selection changed", "AbortError");
683
+ if (this.snapshot?.status === "error") throw this.snapshot.error;
684
+ return this.getSelection(options);
685
+ };
686
+ finishSelection() {
687
+ if (this.snapshot?.status !== "loading" || !this.complete()) return;
688
+ this.work?.abort();
689
+ this.work = null;
690
+ this.publish({ ...this.snapshot, status: "ready" });
691
+ }
692
+ registerLayer(element, pageNumber, width, height) {
693
+ const page = new SelectionPage(element, pageNumber, width, height);
694
+ this.layers.set(pageNumber, page);
695
+ try {
696
+ this.remember(page);
697
+ this.finishSelection();
698
+ } catch (error) {
699
+ this.work?.abort();
700
+ if (this.snapshot)
701
+ this.publish({ ...this.snapshot, status: "error", error });
702
+ }
703
+ this.schedule();
704
+ return () => {
705
+ if (this.layers.get(pageNumber) !== page) return;
706
+ this.structuralChange = true;
707
+ this.layers.delete(pageNumber);
708
+ this.projection = null;
709
+ this.schedule();
710
+ };
711
+ }
712
+ anchorFor(node, offset3) {
713
+ if (!node) return null;
714
+ for (const [pageNumber, page] of this.layers) {
715
+ const index = page.offset(node, offset3);
716
+ if (index !== null) return { pageNumber, offset: index };
717
+ }
718
+ return null;
719
+ }
720
+ capture = () => {
721
+ const owner = this.container?.ownerDocument;
722
+ const active = owner?.activeElement;
723
+ if (active && active !== owner?.body && !this.container?.contains(active) && !active.closest(
724
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
725
+ ))
726
+ return;
727
+ const selection = owner?.getSelection();
728
+ if (!selection || selection.rangeCount > 1) return;
729
+ if (this.projection && samePoint(
730
+ this.projection.anchor,
731
+ selection.anchorNode,
732
+ selection.anchorOffset
733
+ ) && samePoint(
734
+ this.projection.focus,
735
+ selection.focusNode,
736
+ selection.focusOffset
737
+ ))
738
+ return;
739
+ if (this.structuralChange) {
740
+ this.schedule();
741
+ return;
742
+ }
743
+ if (selection.isCollapsed) {
744
+ if (this.structuralChange || this.drag || !this.projection && this.snapshot && (!this.layers.has(this.snapshot.anchor.pageNumber) || !this.layers.has(this.snapshot.focus.pageNumber))) {
745
+ this.schedule();
746
+ return;
747
+ }
748
+ if (this.snapshot) this.clear();
749
+ return;
750
+ }
751
+ const anchor = this.anchorFor(selection.anchorNode, selection.anchorOffset);
752
+ const focus = this.anchorFor(selection.focusNode, selection.focusOffset);
753
+ if (anchor && focus) {
754
+ if (this.drag && !this.drag.started) {
755
+ if (!this.drag.shift && !this.drag.moved) this.drag.anchor = anchor;
756
+ this.drag.started = true;
757
+ }
758
+ const savedAnchor = this.projection && samePoint(
759
+ this.projection.anchor,
760
+ selection.anchorNode,
761
+ selection.anchorOffset
762
+ ) ? this.snapshot?.anchor : void 0;
763
+ this.setSelection(this.drag?.anchor ?? savedAnchor ?? anchor, focus);
764
+ this.projection = {
765
+ anchor: { node: selection.anchorNode, offset: selection.anchorOffset },
766
+ focus: { node: selection.focusNode, offset: selection.focusOffset }
767
+ };
768
+ } else if (!this.structuralChange && !this.drag && (selection.anchorNode || selection.focusNode))
769
+ this.clear();
770
+ };
771
+ schedule() {
772
+ if (!this.container || this.frame !== null) return;
773
+ this.frame = this.container.ownerDocument.defaultView.requestAnimationFrame(() => {
774
+ this.frame = null;
775
+ this.project();
776
+ this.structuralChange = false;
777
+ this.flushKeyboard();
778
+ if (this.drag) this.autoScroll();
779
+ });
780
+ }
781
+ project() {
782
+ const range = this.ordered();
783
+ if (!range || !this.container) return;
784
+ const visible = [...this.layers.values()].filter((page) => this.includesPage(page.pageNumber) && page.nodes.length).sort((a, b) => a.pageNumber - b.pageNumber);
785
+ const first = visible[0];
786
+ const last = visible.at(-1);
787
+ if (!first || !last) return;
788
+ const start = first.point(
789
+ this.bounds(first.pageNumber, first.content.text.length).from
790
+ );
791
+ const end = last.point(
792
+ this.bounds(last.pageNumber, last.content.text.length).to,
793
+ true
794
+ );
795
+ if (!start || !end) return;
796
+ const backwards = compare(this.snapshot.anchor, this.snapshot.focus) > 0;
797
+ const anchor = backwards ? end : start;
798
+ const focus = backwards ? start : end;
799
+ const selection = this.container.ownerDocument.getSelection();
800
+ this.projection = { anchor, focus };
801
+ if (!samePoint(anchor, selection.anchorNode, selection.anchorOffset) || !samePoint(focus, selection.focusNode, selection.focusOffset))
802
+ selection.setBaseAndExtent(
803
+ anchor.node,
804
+ anchor.offset,
805
+ focus.node,
806
+ focus.offset
807
+ );
808
+ }
809
+ hydrate() {
810
+ if (!this.snapshot || this.complete() || !this.container) {
811
+ return;
812
+ }
813
+ const range = this.ordered();
814
+ const owner = this.container.ownerDocument;
815
+ const controller = new AbortController();
816
+ this.work = controller;
817
+ const generation = this.generation;
818
+ let next = range.start.pageNumber;
819
+ const worker = async () => {
820
+ while (next <= range.end.pageNumber) {
821
+ controller.signal.throwIfAborted();
822
+ const n = next++;
823
+ const cached = this.pages.get(n);
824
+ if (cached) {
825
+ const bounds = this.bounds(n, cached.content.text.length);
826
+ if (bounds.from === cached.from && bounds.to === cached.to) continue;
827
+ }
828
+ const page = await this.options.loadPage(n);
829
+ controller.signal.throwIfAborted();
830
+ await withSelectionPage(
831
+ page,
832
+ owner,
833
+ controller.signal,
834
+ (page2) => this.remember(page2)
835
+ );
836
+ }
837
+ };
838
+ void Promise.all([worker(), worker()]).then(() => {
839
+ if (!controller.signal.aborted && generation === this.generation) {
840
+ this.finishSelection();
841
+ this.schedule();
842
+ }
843
+ }).catch((error) => {
844
+ if (!controller.signal.aborted && generation === this.generation && this.snapshot) {
845
+ this.publish({ ...this.snapshot, status: "error", error });
846
+ controller.abort();
847
+ }
848
+ });
849
+ }
850
+ caret(x, y) {
851
+ const owner = this.container.ownerDocument;
852
+ const doc = owner;
853
+ const position = doc.caretPositionFromPoint?.(x, y);
854
+ if (position) return this.anchorFor(position.offsetNode, position.offset);
855
+ const range = doc.caretRangeFromPoint?.(x, y);
856
+ return range ? this.anchorFor(range.startContainer, range.startOffset) : null;
857
+ }
858
+ autoScroll() {
859
+ if (!this.drag?.moved || !this.container) return;
860
+ const bounds = this.container.getBoundingClientRect();
861
+ const { x, y } = this.drag;
862
+ const edge = Math.min(40, bounds.height / 4);
863
+ 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;
864
+ if (delta) this.container.scrollTop += delta;
865
+ const focus = this.caret(
866
+ Math.max(bounds.left + 1, Math.min(x, bounds.right - 1)),
867
+ Math.max(bounds.top + 1, Math.min(y, bounds.bottom - 1))
868
+ );
869
+ if (focus) this.setSelection(this.drag.anchor, focus);
870
+ if (delta) this.schedule();
871
+ }
872
+ flushKeyboard() {
873
+ if (!this.snapshot || !this.keyboardMoves.length || this.keyboardWork)
874
+ return;
875
+ const page = this.layers.get(this.snapshot.focus.pageNumber);
876
+ if (!page) {
877
+ this.options.revealPage?.(this.snapshot.focus.pageNumber);
878
+ return;
879
+ }
880
+ const move = this.keyboardMoves.shift();
881
+ const native = this.container.ownerDocument.getSelection();
882
+ if (!native?.modify) return;
883
+ const { anchor, focus } = this.snapshot;
884
+ native.modify("extend", move.direction, move.granularity);
885
+ const next = this.anchorFor(native.focusNode, native.focusOffset);
886
+ if (next && !sameAnchor(next, focus)) {
887
+ this.setSelection(anchor, next);
888
+ } else {
889
+ const point = page.point(focus.offset);
890
+ const rtl = point && this.container.ownerDocument.defaultView.getComputedStyle(
891
+ point.node.parentElement
892
+ ).direction === "rtl";
893
+ const backwards = move.direction === "backward" || move.direction === (rtl ? "right" : "left");
894
+ 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))) {
895
+ this.schedule();
896
+ return;
897
+ }
898
+ const n = focus.pageNumber + (backwards ? -1 : 1);
899
+ if (n >= 1 && n <= this.options.pageCount) {
900
+ if (!backwards) {
901
+ this.setSelection(anchor, { pageNumber: n, offset: 0 });
902
+ this.options.revealPage?.(n);
903
+ } else {
904
+ const generation = this.generation;
905
+ const controller = new AbortController();
906
+ this.keyboardWork = controller;
907
+ const owner = this.container.ownerDocument;
908
+ void this.options.loadPage(n).then(
909
+ (proxy) => withSelectionPage(
910
+ proxy,
911
+ owner,
912
+ controller.signal,
913
+ (page2) => page2.content.text.length
914
+ )
915
+ ).then((length) => {
916
+ if (controller.signal.aborted || generation !== this.generation || !this.container)
917
+ return;
918
+ this.setSelection(anchor, { pageNumber: n, offset: length });
919
+ this.options.revealPage?.(n);
920
+ }).catch(() => {
921
+ }).finally(() => {
922
+ if (this.keyboardWork === controller) this.keyboardWork = null;
923
+ this.schedule();
924
+ });
925
+ }
926
+ }
927
+ }
928
+ if (this.keyboardMoves.length) this.schedule();
929
+ }
930
+ connect(container) {
931
+ this.connectedCleanup?.();
932
+ this.container = container;
933
+ const owner = container.ownerDocument;
934
+ const events = new AbortController();
935
+ const { signal } = events;
936
+ const down = (event) => {
937
+ if (event.defaultPrevented) return;
938
+ const target = event.target instanceof Element ? event.target : null;
939
+ if (target?.closest("button, a, [role=button], [data-selection-tooltip]"))
940
+ return;
941
+ if (!container.contains(event.target)) {
942
+ this.clear();
943
+ return;
944
+ }
945
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || !container.contains(event.target))
946
+ return;
947
+ const point = this.caret(event.clientX, event.clientY);
948
+ if (!point) return;
949
+ const anchor = event.shiftKey && this.snapshot ? this.snapshot.anchor : point;
950
+ if (!event.shiftKey) this.clear();
951
+ this.drag = {
952
+ anchor,
953
+ x: event.clientX,
954
+ y: event.clientY,
955
+ pointerId: event.pointerId,
956
+ moved: false,
957
+ started: false,
958
+ shift: event.shiftKey
959
+ };
960
+ this.schedule();
961
+ };
962
+ const move = (event) => {
963
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
964
+ this.drag.moved ||= Math.abs(this.drag.x - event.clientX) + Math.abs(this.drag.y - event.clientY) > 2;
965
+ this.drag.x = event.clientX;
966
+ this.drag.y = event.clientY;
967
+ this.schedule();
968
+ };
969
+ const up = () => {
970
+ this.capture();
971
+ this.drag = null;
972
+ };
973
+ owner.addEventListener("selectionchange", this.capture, { signal });
974
+ owner.addEventListener("pointerdown", down, { signal });
975
+ owner.addEventListener("pointermove", move, { signal });
976
+ owner.addEventListener("pointerup", up, { signal });
977
+ owner.addEventListener("pointercancel", up, { signal });
978
+ owner.defaultView.addEventListener("blur", up, { signal });
979
+ const cancelKeyboard = () => {
980
+ this.keyboardMoves = [];
981
+ this.keyboardWork?.abort();
982
+ this.keyboardWork = null;
983
+ };
984
+ container.addEventListener(
985
+ "focusout",
986
+ (event) => {
987
+ if (!container.contains(event.relatedTarget))
988
+ cancelKeyboard();
989
+ },
990
+ { signal }
991
+ );
992
+ owner.defaultView.addEventListener("blur", cancelKeyboard, { signal });
993
+ container.addEventListener(
994
+ "keydown",
995
+ (event) => {
996
+ if (event.defaultPrevented || event.target instanceof Element && event.target.closest(
997
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
998
+ ))
999
+ return;
1000
+ if (event.key === "Escape" && this.snapshot) {
1001
+ this.clear();
1002
+ return;
1003
+ }
1004
+ if (!event.shiftKey || !this.snapshot) return;
1005
+ const directions = {
1006
+ ArrowLeft: "left",
1007
+ ArrowRight: "right",
1008
+ ArrowUp: "backward",
1009
+ ArrowDown: "forward",
1010
+ Home: "backward",
1011
+ End: "forward"
1012
+ };
1013
+ const direction = directions[event.key];
1014
+ if (!direction) return;
1015
+ const native = owner.getSelection();
1016
+ if (!native?.modify) return;
1017
+ event.preventDefault();
1018
+ const boundary = event.key === "Home" || event.key === "End" || event.metaKey && (event.key === "ArrowLeft" || event.key === "ArrowRight");
1019
+ const granularity = boundary ? "lineboundary" : event.key === "ArrowUp" || event.key === "ArrowDown" ? "line" : event.ctrlKey || event.altKey ? "word" : "character";
1020
+ this.keyboardMoves.push({ direction, granularity });
1021
+ this.schedule();
1022
+ },
1023
+ { signal }
1024
+ );
1025
+ const cleanup = () => {
1026
+ events.abort();
1027
+ if (this.frame !== null)
1028
+ owner.defaultView.cancelAnimationFrame(this.frame);
1029
+ this.frame = null;
1030
+ this.clear();
1031
+ this.container = null;
1032
+ this.connectedCleanup = null;
1033
+ };
1034
+ this.connectedCleanup = cleanup;
1035
+ this.hydrate();
1036
+ this.schedule();
1037
+ return cleanup;
1038
+ }
1039
+ };
1040
+
222
1041
  // src/lib/viewport-zoom.ts
223
1042
  var viewportZoomHandlers = /* @__PURE__ */ new WeakMap();
224
1043
  function applyViewportZoom(viewport, zoom) {
@@ -268,6 +1087,11 @@ var PDFStore = createZustandContext(
268
1087
  renderColorMapRef.current = initialColorScheme === "dark" ? createDarkModeColorMap(initialDarkModeColors) : null;
269
1088
  return createStore((set, get) => ({
270
1089
  pdfDocumentProxy: initialState.pdfDocumentProxy,
1090
+ selection: new PDFSelectionController({
1091
+ pageCount: initialState.pdfDocumentProxy.numPages,
1092
+ loadPage: (page) => get().loadPdfPageProxy(page),
1093
+ revealPage: (page) => get().virtualizer?.scrollToIndex(page - 1, { align: "auto" })
1094
+ }),
271
1095
  zoom: initialState.zoom,
272
1096
  isZoomFitWidth: initialState.isZoomFitWidth ?? false,
273
1097
  zoomOptions: {
@@ -3104,6 +3928,114 @@ var mergeTextRuns = (rects) => {
3104
3928
  });
3105
3929
  };
3106
3930
 
3931
+ // src/lib/selection-text.ts
3932
+ function textLayer(node) {
3933
+ const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3934
+ return element?.closest(".textLayer[data-page-number]") ?? null;
3935
+ }
3936
+ function getPdfSelectionText(selection, container, options = {}) {
3937
+ if (!selection || selection.isCollapsed || selection.rangeCount !== 1)
3938
+ return null;
3939
+ const range = selection.getRangeAt(0);
3940
+ const first = textLayer(range.startContainer);
3941
+ const last = textLayer(range.endContainer);
3942
+ if (!first || !last || !container.contains(first) || !container.contains(last))
3943
+ return null;
3944
+ const layers = [
3945
+ ...container.querySelectorAll(".textLayer[data-page-number]")
3946
+ ].filter((layer) => range.intersectsNode(layer));
3947
+ let previousPage;
3948
+ for (const layer of layers) {
3949
+ const page = Number(layer.dataset.pageNumber);
3950
+ if (!Number.isInteger(page) || page < 1 || previousPage !== void 0 && page !== previousPage + 1)
3951
+ return null;
3952
+ previousPage = page;
3953
+ }
3954
+ const selectedLayers = new Set(layers);
3955
+ const root = range.commonAncestorContainer;
3956
+ const walker = container.ownerDocument.createTreeWalker(
3957
+ root,
3958
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
3959
+ {
3960
+ acceptNode(node2) {
3961
+ if (node2.nodeType !== Node.TEXT_NODE && node2.tagName !== "BR")
3962
+ return NodeFilter.FILTER_SKIP;
3963
+ const layer = textLayer(node2);
3964
+ return layer && selectedLayers.has(layer) && range.intersectsNode(node2) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
3965
+ }
3966
+ }
3967
+ );
3968
+ const chunks = [];
3969
+ let previousLayer = null;
3970
+ let trailingLineBreaks = 0;
3971
+ let node = root.nodeType === Node.TEXT_NODE ? root : walker.nextNode();
3972
+ while (node) {
3973
+ const layer = textLayer(node);
3974
+ const start = node === range.startContainer ? range.startOffset : 0;
3975
+ const end = node === range.endContainer ? range.endOffset : node.textContent?.length ?? 0;
3976
+ const text = node.nodeType === Node.TEXT_NODE ? node.textContent.slice(start, end) : "\n";
3977
+ if (text) {
3978
+ if (previousLayer && previousLayer !== layer && trailingLineBreaks < 2) {
3979
+ chunks.push("\n".repeat(2 - trailingLineBreaks));
3980
+ trailingLineBreaks = 2;
3981
+ }
3982
+ chunks.push(text);
3983
+ let trailing = 0;
3984
+ for (let i = text.length - 1; i >= 0 && text[i] === "\n"; i--) trailing++;
3985
+ trailingLineBreaks = trailing === text.length ? trailingLineBreaks + trailing : trailing;
3986
+ previousLayer = layer;
3987
+ }
3988
+ node = walker.nextNode();
3989
+ }
3990
+ return normalizeSelectionText(chunks.join(""), options);
3991
+ }
3992
+ var documents = /* @__PURE__ */ new WeakMap();
3993
+ function registerPdfCopy(container, options, read) {
3994
+ const owner = container.ownerDocument;
3995
+ let binding = documents.get(owner);
3996
+ if (!binding) {
3997
+ const registrations = /* @__PURE__ */ new Set();
3998
+ const listener = (event) => {
3999
+ if (event.defaultPrevented || !event.clipboardData) return;
4000
+ const target = event.target;
4001
+ if (target instanceof Element && target.closest(
4002
+ 'input, textarea, [contenteditable]:not([contenteditable="false"])'
4003
+ ))
4004
+ return;
4005
+ for (const registration2 of registrations) {
4006
+ const settings = registration2.options();
4007
+ if (settings === false) continue;
4008
+ const persistent = registration2.read?.(settings);
4009
+ if (registration2.read && persistent === void 0) {
4010
+ event.preventDefault();
4011
+ break;
4012
+ }
4013
+ const text = persistent ?? getPdfSelectionText(
4014
+ owner.getSelection(),
4015
+ registration2.container,
4016
+ settings
4017
+ );
4018
+ if (text === null) continue;
4019
+ event.clipboardData.setData("text/plain", text);
4020
+ event.preventDefault();
4021
+ break;
4022
+ }
4023
+ };
4024
+ binding = { registrations, listener };
4025
+ documents.set(owner, binding);
4026
+ owner.addEventListener("copy", listener);
4027
+ }
4028
+ const registration = { container, options, read };
4029
+ binding.registrations.add(registration);
4030
+ return () => {
4031
+ binding.registrations.delete(registration);
4032
+ if (!binding.registrations.size) {
4033
+ owner.removeEventListener("copy", binding.listener);
4034
+ documents.delete(owner);
4035
+ }
4036
+ };
4037
+ }
4038
+
3107
4039
  // src/hooks/useSelectionDimensions.tsx
3108
4040
  var MERGE_THRESHOLD = 2;
3109
4041
  var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
@@ -3151,7 +4083,19 @@ var mapSelectionRectsToLayers = (range) => {
3151
4083
  };
3152
4084
  var useSelectionDimensions = () => {
3153
4085
  const store = PDFStore.useContext();
4086
+ const getText = (options) => {
4087
+ if (store.getState().selection?.getSnapshot())
4088
+ return store.getState().selection.getText(options);
4089
+ const container = store.getState().viewportRef.current;
4090
+ return container ? getPdfSelectionText(
4091
+ container.ownerDocument.getSelection(),
4092
+ container,
4093
+ options
4094
+ ) : null;
4095
+ };
3154
4096
  const getAnnotationDimension = () => {
4097
+ if (store.getState().selection?.getSnapshot())
4098
+ return store.getState().selection.getSelection() ?? void 0;
3155
4099
  const selection = window.getSelection();
3156
4100
  if (!selection || selection.isCollapsed) return;
3157
4101
  const range = selection.getRangeAt(0);
@@ -3289,7 +4233,7 @@ var useSelectionDimensions = () => {
3289
4233
  underlines: consolidateUnderlines(underlineRects).sort(
3290
4234
  (a, b) => a.pageNumber - b.pageNumber
3291
4235
  ),
3292
- text: range.toString().trim(),
4236
+ text: (getText() ?? range.toString()).trim(),
3293
4237
  isCollapsed: false
3294
4238
  };
3295
4239
  };
@@ -3346,6 +4290,8 @@ var useSelectionDimensions = () => {
3346
4290
  return consolidated;
3347
4291
  };
3348
4292
  const getDimension = () => {
4293
+ if (store.getState().selection?.getSnapshot())
4294
+ return store.getState().selection.getSelection() ?? void 0;
3349
4295
  const selection = window.getSelection();
3350
4296
  if (!selection || selection.isCollapsed) return;
3351
4297
  const range = selection.getRangeAt(0);
@@ -3374,12 +4320,12 @@ var useSelectionDimensions = () => {
3374
4320
  });
3375
4321
  return {
3376
4322
  highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber),
3377
- text: range.toString().trim(),
4323
+ text: (getText() ?? range.toString()).trim(),
3378
4324
  isCollapsed: false
3379
4325
  };
3380
4326
  };
3381
4327
  const getSelection = () => getDimension();
3382
- return { getDimension, getSelection, getAnnotationDimension };
4328
+ return { getDimension, getSelection, getAnnotationDimension, getText };
3383
4329
  };
3384
4330
  var SelectionTooltip = ({ children }) => {
3385
4331
  const [isOpen, setIsOpen] = useState(false);
@@ -3514,6 +4460,7 @@ var SelectionTooltip = ({ children }) => {
3514
4460
  return /* @__PURE__ */ jsx(Fragment, { children: isOpen && /* @__PURE__ */ jsx(
3515
4461
  "div",
3516
4462
  {
4463
+ "data-selection-tooltip": "",
3517
4464
  ref: refs.setFloating,
3518
4465
  style: {
3519
4466
  ...floatingStyles
@@ -3929,13 +4876,13 @@ var createTextSelectionManager = () => {
3929
4876
  }
3930
4877
  selectionChangeAbortController = new AbortController();
3931
4878
  const { signal } = selectionChangeAbortController;
3932
- const reset = (endDiv, textLayer) => {
3933
- if (endDiv.parentNode !== textLayer) {
3934
- textLayer.appendChild(endDiv);
4879
+ const reset = (endDiv, textLayer2) => {
4880
+ if (endDiv.parentNode !== textLayer2) {
4881
+ textLayer2.appendChild(endDiv);
3935
4882
  }
3936
4883
  endDiv.style.width = "";
3937
4884
  endDiv.style.height = "";
3938
- textLayer.classList.remove("selecting");
4885
+ textLayer2.classList.remove("selecting");
3939
4886
  };
3940
4887
  document.addEventListener(
3941
4888
  "pointerdown",
@@ -4061,12 +5008,14 @@ var useTextLayer = () => {
4061
5008
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
4062
5009
  const viewportRef = usePdf((state) => state.viewportRef);
4063
5010
  const store = PDFStore.useContext();
5011
+ const selection = store.getState().selection;
4064
5012
  useEffect(() => {
4065
5013
  const textContainer = textContainerRef.current;
4066
5014
  if (!textContainer) {
4067
5015
  return;
4068
5016
  }
4069
5017
  let isCancelled = false;
5018
+ let unregisterSelection;
4070
5019
  let buildTimer = null;
4071
5020
  let unsubscribe = null;
4072
5021
  let built = false;
@@ -4083,13 +5032,13 @@ var useTextLayer = () => {
4083
5032
  void import('pdfjs-dist/legacy/build/pdf.mjs').then(({ TextLayer: TextLayer2 }) => {
4084
5033
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer)
4085
5034
  return;
4086
- const textLayer = new TextLayer2({
5035
+ const textLayer2 = new TextLayer2({
4087
5036
  textContentSource: pdfPageProxy.streamTextContent(),
4088
5037
  container: textContainer,
4089
5038
  viewport: pdfPageProxy.getViewport({ scale: 1 })
4090
5039
  });
4091
- textLayerRef.current = textLayer;
4092
- return textLayer.render();
5040
+ textLayerRef.current = textLayer2;
5041
+ return textLayer2.render();
4093
5042
  }).then(() => {
4094
5043
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer) {
4095
5044
  return;
@@ -4098,6 +5047,16 @@ var useTextLayer = () => {
4098
5047
  endOfContent.className = "endOfContent";
4099
5048
  textContainer.appendChild(endOfContent);
4100
5049
  bindMouseEvents(textContainer, endOfContent);
5050
+ const viewport = pdfPageProxy.getViewport({ scale: 1 });
5051
+ const unrotated = pdfPageProxy.getViewport({ scale: 1, rotation: 0 });
5052
+ textContainer.style.width = `${unrotated.width}px`;
5053
+ textContainer.style.height = `${unrotated.height}px`;
5054
+ unregisterSelection = selection?.registerLayer(
5055
+ textContainer,
5056
+ pageNumber,
5057
+ viewport.width,
5058
+ viewport.height
5059
+ );
4101
5060
  built = true;
4102
5061
  building = false;
4103
5062
  unsubscribe?.();
@@ -4111,6 +5070,12 @@ var useTextLayer = () => {
4111
5070
  };
4112
5071
  const schedule = () => {
4113
5072
  if (built) return;
5073
+ if (selection?.isDragging || selection?.includesPage(pageNumber)) {
5074
+ if (buildTimer) clearTimeout(buildTimer);
5075
+ buildTimer = null;
5076
+ if (!building) build();
5077
+ return;
5078
+ }
4114
5079
  if (building) {
4115
5080
  buildGen++;
4116
5081
  building = false;
@@ -4140,9 +5105,15 @@ var useTextLayer = () => {
4140
5105
  unsubscribe = subscribeToViewportInvalidation(scrollViewport, schedule);
4141
5106
  };
4142
5107
  attachScrollSubscription();
5108
+ const unsubscribeSelection = selection?.subscribe(() => {
5109
+ if (selection.isDragging || selection.includesPage(pageNumber))
5110
+ schedule();
5111
+ });
4143
5112
  schedule();
4144
5113
  return () => {
4145
5114
  isCancelled = true;
5115
+ unregisterSelection?.();
5116
+ unsubscribeSelection?.();
4146
5117
  if (buildTimer) clearTimeout(buildTimer);
4147
5118
  if (attachRaf !== null) cancelAnimationFrame(attachRaf);
4148
5119
  unsubscribe?.();
@@ -4155,7 +5126,13 @@ var useTextLayer = () => {
4155
5126
  delete textContainer._cleanupTextSelection;
4156
5127
  }
4157
5128
  };
4158
- }, [pdfPageProxy.streamTextContent, pdfPageProxy.getViewport, viewportRef]);
5129
+ }, [
5130
+ pdfPageProxy.streamTextContent,
5131
+ pdfPageProxy.getViewport,
5132
+ viewportRef,
5133
+ selection,
5134
+ pageNumber
5135
+ ]);
4159
5136
  useEffect(() => {
4160
5137
  const textContainer = textContainerRef.current;
4161
5138
  if (!textContainer) return;
@@ -4174,7 +5151,10 @@ var useTextLayer = () => {
4174
5151
  show();
4175
5152
  };
4176
5153
  const hideAndDebounce = () => {
4177
- if (selecting) return;
5154
+ if (selecting || selection?.isDragging || selection?.includesPage(pageNumber)) {
5155
+ show();
5156
+ return;
5157
+ }
4178
5158
  textContainer.style.visibility = "hidden";
4179
5159
  if (settleTimer) clearTimeout(settleTimer);
4180
5160
  settleTimer = setTimeout(restoreWhenSettled, TEXT_SCROLL_SETTLE_MS);
@@ -4242,7 +5222,7 @@ var useTextLayer = () => {
4242
5222
  if (settleTimer) clearTimeout(settleTimer);
4243
5223
  show();
4244
5224
  };
4245
- }, [viewportRef, store]);
5225
+ }, [viewportRef, store, selection, pageNumber]);
4246
5226
  return {
4247
5227
  textContainerRef,
4248
5228
  pageNumber: pdfPageProxy.pageNumber
@@ -6829,6 +7809,10 @@ var useViewportContainer = ({
6829
7809
  if (!st.active) {
6830
7810
  return;
6831
7811
  }
7812
+ if (Math.abs(event.deltaX) > abs) {
7813
+ st.active = false;
7814
+ return;
7815
+ }
6832
7816
  if (now - st.lastTime > WHEEL_INERTIA_GAP_MS) {
6833
7817
  st.active = false;
6834
7818
  return;
@@ -6888,12 +7872,11 @@ var useViewportContainer = ({
6888
7872
  contentPosition,
6889
7873
  containerPosition,
6890
7874
  originZoom: currentZoom,
7875
+ origin: [...origin],
6891
7876
  lastZoom: currentZoom
6892
7877
  };
6893
7878
  });
6894
- if (first) {
6895
- return newMemo;
6896
- }
7879
+ if (first && ms === 1) return newMemo;
6897
7880
  const gestureValuesValid = Number.isFinite(ms) && ms > 0;
6898
7881
  let effectiveScale = ms;
6899
7882
  if (!gestureValuesValid) {
@@ -6912,9 +7895,18 @@ var useViewportContainer = ({
6912
7895
  minZoom,
6913
7896
  maxZoom
6914
7897
  );
7898
+ if (event.type === "wheel") {
7899
+ const previousScale = newMemo.lastZoom / newMemo.originZoom;
7900
+ for (const axis of [0, 1]) {
7901
+ const movement2 = origin[axis] - newMemo.origin[axis];
7902
+ newMemo.contentPosition[axis] += movement2 / previousScale;
7903
+ newMemo.containerPosition[axis] += movement2;
7904
+ newMemo.origin[axis] = origin[axis];
7905
+ }
7906
+ }
6915
7907
  const realMs = newZoom / newMemo.originZoom;
6916
- const newTranslateX = newMemo.contentPosition[0] * realMs - newMemo.containerPosition[0];
6917
- const newTranslateY = newMemo.contentPosition[1] * realMs - newMemo.containerPosition[1];
7908
+ const newTranslateX = newMemo.contentPosition[0] * realMs - newMemo.containerPosition[0] - (origin[0] - newMemo.origin[0]);
7909
+ const newTranslateY = newMemo.contentPosition[1] * realMs - newMemo.containerPosition[1] - (origin[1] - newMemo.origin[1]);
6918
7910
  transformations.current = {
6919
7911
  zoom: newZoom,
6920
7912
  translateX: newTranslateX,
@@ -6988,16 +7980,34 @@ var Pages = ({
6988
7980
  virtualizerOptions = DEFAULT_VIRTUALIZER_OPTIONS,
6989
7981
  initialOffset,
6990
7982
  onOffsetChange,
7983
+ copyOptions,
6991
7984
  ...props
6992
7985
  }) => {
6993
7986
  const [tempItems, setTempItems] = useState([]);
6994
7987
  const viewports = usePdf((state) => state.viewports);
7988
+ const selection = usePdf((state) => state.selection);
6995
7989
  const numPages = usePdf((state) => state.pdfDocumentProxy.numPages);
6996
7990
  const initialPage = usePdf((state) => state.initialPage);
6997
7991
  const isPinching = usePdf((state) => state.isPinching);
6998
7992
  const elementWrapperRef = useRef(null);
6999
7993
  const elementRef = useRef(null);
7000
7994
  const containerRef = useRef(null);
7995
+ const copyOptionsRef = useRef(copyOptions);
7996
+ copyOptionsRef.current = copyOptions;
7997
+ useEffect(() => {
7998
+ const container = containerRef.current;
7999
+ if (!container) return;
8000
+ const disconnect = selection.connect(container);
8001
+ const unregisterCopy = registerPdfCopy(
8002
+ container,
8003
+ () => copyOptionsRef.current ?? {},
8004
+ (options) => selection.getSnapshot() ? selection.getText(options) ?? void 0 : null
8005
+ );
8006
+ return () => {
8007
+ unregisterCopy();
8008
+ disconnect();
8009
+ };
8010
+ }, [selection]);
7001
8011
  useViewportContainer({
7002
8012
  elementRef,
7003
8013
  elementWrapperRef,
@@ -7078,7 +8088,14 @@ var Pages = ({
7078
8088
  };
7079
8089
  }, [isPinching, virtualizer?.measure, virtualizer?.getVirtualItems]);
7080
8090
  const virtualizerItems = virtualizer?.getVirtualItems() ?? [];
7081
- const items = tempItems.length ? tempItems : virtualizerItems;
8091
+ const items = useMemo(() => {
8092
+ if (!tempItems.length) return virtualizerItems;
8093
+ const combined = new Map(
8094
+ virtualizerItems.map((item) => [item.index, item])
8095
+ );
8096
+ for (const item of tempItems) combined.set(item.index, item);
8097
+ return [...combined.values()].sort((a, b) => a.index - b.index);
8098
+ }, [tempItems, virtualizerItems]);
7082
8099
  useVisiblePage({
7083
8100
  items,
7084
8101
  // Pass through nullish (pre-measure) rather than coercing to 0, so the
@@ -7115,6 +8132,7 @@ var Pages = ({
7115
8132
  Primitive.div,
7116
8133
  {
7117
8134
  ref: containerRef,
8135
+ tabIndex: 0,
7118
8136
  ...props,
7119
8137
  style: {
7120
8138
  display: "flex",
@@ -7759,6 +8777,8 @@ function acquireDocumentText(pages) {
7759
8777
  const page = pages[index];
7760
8778
  const reader = page.streamTextContent().getReader();
7761
8779
  const chunks = [];
8780
+ const lineBreaks = [];
8781
+ let offset3 = 0;
7762
8782
  let complete = false;
7763
8783
  try {
7764
8784
  while (true) {
@@ -7768,9 +8788,13 @@ function acquireDocumentText(pages) {
7768
8788
  complete = true;
7769
8789
  break;
7770
8790
  }
7771
- chunks.push(
7772
- value.items.map((item) => "str" in item ? item.str : "").join("")
7773
- );
8791
+ for (const item of value.items) {
8792
+ if (!("str" in item)) continue;
8793
+ chunks.push(item.str);
8794
+ offset3 += item.str.length;
8795
+ if (item.hasEOL && lineBreaks.at(-1) !== offset3)
8796
+ lineBreaks.push(offset3);
8797
+ }
7774
8798
  }
7775
8799
  } finally {
7776
8800
  if (!complete)
@@ -7778,7 +8802,11 @@ function acquireDocumentText(pages) {
7778
8802
  });
7779
8803
  reader.releaseLock();
7780
8804
  }
7781
- result[index] = { pageNumber: page.pageNumber, text: chunks.join("") };
8805
+ result[index] = {
8806
+ pageNumber: page.pageNumber,
8807
+ text: chunks.join(""),
8808
+ ...lineBreaks.length ? { lineBreaks } : {}
8809
+ };
7782
8810
  }
7783
8811
  };
7784
8812
  const created = {
@@ -8187,33 +9215,31 @@ var CurrentZoom = ({ ...props }) => {
8187
9215
 
8188
9216
  // src/lib/search.ts
8189
9217
  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);
9218
+ function normalize(page, options) {
9219
+ let cached = normalizedPages.get(page);
9220
+ if (cached?.text !== page.text || cached.lineBreaks !== page.lineBreaks) {
9221
+ cached = {
9222
+ text: page.text,
9223
+ lineBreaks: page.lineBreaks,
9224
+ variants: /* @__PURE__ */ new Map()
9225
+ };
9226
+ normalizedPages.set(page, cached);
9227
+ }
9228
+ const key = (options.matchDiacritics === false ? 1 : 0) | (options.ignoreHyphenation ? 2 : 0);
9229
+ let result = cached.variants.get(key);
9230
+ if (!result) {
9231
+ result = normalizeSearchText(page.text, options, page.lineBreaks);
9232
+ cached.variants.set(key, result);
9233
+ }
8211
9234
  return result;
8212
9235
  }
8213
9236
  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;
9237
+ const end = Math.min(normalized.text.length, start + length);
9238
+ const { start: matchIndex, end: matchEnd } = originalTextRange(
9239
+ normalized,
9240
+ start,
9241
+ end
9242
+ );
8217
9243
  const matchLength = matchEnd - matchIndex;
8218
9244
  return {
8219
9245
  pageNumber: page.pageNumber,
@@ -8276,7 +9302,8 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8276
9302
  const threshold = Math.max(0, Math.min(1, options.threshold ?? 0.7));
8277
9303
  const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : 10;
8278
9304
  const textSize = options.textSize ?? 100;
8279
- const query = searchText.toLowerCase();
9305
+ const query = normalizeSearchText(searchText, options).text;
9306
+ if (!query.trim()) return empty;
8280
9307
  const maxDistance = Math.floor(query.length * (1 - threshold));
8281
9308
  const distance = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
8282
9309
  const exactMatches = [];
@@ -8286,10 +9313,16 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8286
9313
  let steps = 0;
8287
9314
  let deadline = cooperative ? performance.now() + budget : 0;
8288
9315
  for (const page of pages) {
8289
- const normalized = normalize(page);
8290
- const lower = normalized.lower;
9316
+ if (cooperative && performance.now() >= deadline) {
9317
+ yield;
9318
+ deadline = performance.now() + budget;
9319
+ }
9320
+ const normalized = normalize(page, options);
9321
+ const lower = normalized.text;
8291
9322
  const exactStarts = [];
8292
9323
  let index = 0;
9324
+ let lastStart = -1;
9325
+ let lastEnd = -1;
8293
9326
  while (true) {
8294
9327
  if (cooperative && (++steps & 127) === 0 && performance.now() >= deadline) {
8295
9328
  yield;
@@ -8297,8 +9330,20 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
8297
9330
  }
8298
9331
  const matchIndex = lower.indexOf(query, index);
8299
9332
  if (matchIndex === -1) break;
8300
- exactCount++;
9333
+ const span = originalTextRange(
9334
+ normalized,
9335
+ matchIndex,
9336
+ matchIndex + query.length
9337
+ );
9338
+ index = matchIndex + query.length;
8301
9339
  if (maxDistance > 0) exactStarts.push(matchIndex);
9340
+ if (span.start === lastStart && span.end === lastEnd) continue;
9341
+ lastStart = span.start;
9342
+ lastEnd = span.end;
9343
+ exactCount++;
9344
+ if (maxDistance === 0 && exactCount > limit) {
9345
+ return { exactMatches, fuzzyMatches, hasMoreResults: true };
9346
+ }
8302
9347
  if (exactMatches.length < limit) {
8303
9348
  exactMatches.push(
8304
9349
  createMatch(
@@ -8373,7 +9418,19 @@ async function searchDocumentAsync(pages, searchText, options = {}) {
8373
9418
  throw new DOMException("Search aborted", "AbortError");
8374
9419
  const result = iterator.next();
8375
9420
  if (result.done) return result.value;
8376
- await new Promise((resolve) => setTimeout(resolve, 0));
9421
+ await new Promise((resolve) => {
9422
+ if (typeof MessageChannel === "undefined") {
9423
+ setTimeout(resolve, 0);
9424
+ return;
9425
+ }
9426
+ const channel = new MessageChannel();
9427
+ channel.port1.onmessage = () => {
9428
+ channel.port1.close();
9429
+ channel.port2.close();
9430
+ resolve();
9431
+ };
9432
+ channel.port2.postMessage(null);
9433
+ });
8377
9434
  }
8378
9435
  }
8379
9436
 
@@ -8471,10 +9528,16 @@ async function calculateHighlightRects(pageProxy, textMatch) {
8471
9528
  const start = Math.max(0, textMatch.matchIndex - offset3);
8472
9529
  const end = Math.min(length, matchEnd - offset3);
8473
9530
  if (end > start && item.str.slice(start, end).trim()) {
9531
+ let style = styles[item.fontName];
9532
+ if (style?.fontFamily && !style.vertical && (start > 0 || end < length)) {
9533
+ const { TextLayer: TextLayer2 } = await loadPdfJs();
9534
+ const fontFamily = TextLayer2.fontFamilyMap.get(style.fontFamily);
9535
+ if (fontFamily) style = { ...style, fontFamily };
9536
+ }
8474
9537
  matchRects.push(
8475
9538
  getItemRect(
8476
9539
  item,
8477
- styles[item.fontName],
9540
+ style,
8478
9541
  viewport,
8479
9542
  start,
8480
9543
  end,
@@ -8579,5 +9642,24 @@ function mergeAdjacentRects(rects) {
8579
9642
 
8580
9643
  // src/hooks/usePageRendered.ts
8581
9644
  var usePageRendered = (pageNumber) => usePdf((state) => !!state.renderedPages[pageNumber]);
9645
+ function usePdfSelection() {
9646
+ const controller = usePdf((state) => state.selection);
9647
+ const selection = useSyncExternalStore(
9648
+ controller.subscribe,
9649
+ controller.getSnapshot,
9650
+ controller.getSnapshot
9651
+ );
9652
+ return useMemo(
9653
+ () => ({
9654
+ selection,
9655
+ setSelection: controller.setSelection,
9656
+ clearSelection: controller.clear,
9657
+ getText: controller.getText,
9658
+ getSelection: controller.getSelection,
9659
+ getSelectionAsync: controller.getSelectionAsync
9660
+ }),
9661
+ [controller, selection]
9662
+ );
9663
+ }
8582
9664
 
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 };
9665
+ 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 };