docxodus 9.9.0 → 10.0.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.
Files changed (111) hide show
  1. package/README.md +178 -0
  2. package/dist/canonical.d.ts +7 -0
  3. package/dist/canonical.d.ts.map +1 -0
  4. package/dist/canonical.js +56 -0
  5. package/dist/canonical.js.map +1 -0
  6. package/dist/docxodus.worker.d.ts +2 -1
  7. package/dist/docxodus.worker.d.ts.map +1 -1
  8. package/dist/docxodus.worker.js +244 -3
  9. package/dist/docxodus.worker.js.map +1 -1
  10. package/dist/editor.bundle.js +1914 -419
  11. package/dist/editor.d.ts +22 -2
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +94 -9
  14. package/dist/editor.js.map +1 -1
  15. package/dist/embed.bundle.js +2803 -469
  16. package/dist/embed.iife.js +2801 -467
  17. package/dist/export-assets.json +283 -0
  18. package/dist/export-browser.bundle.js +7990 -0
  19. package/dist/export-browser.d.ts +266 -0
  20. package/dist/export-browser.d.ts.map +1 -0
  21. package/dist/export-browser.js +2915 -0
  22. package/dist/export-browser.js.map +1 -0
  23. package/dist/export-resource-limits-v1.json +57 -0
  24. package/dist/font-contract.d.ts +150 -0
  25. package/dist/font-contract.d.ts.map +1 -0
  26. package/dist/font-contract.js +59 -0
  27. package/dist/font-contract.js.map +1 -0
  28. package/dist/font-runtime.d.ts +33 -0
  29. package/dist/font-runtime.d.ts.map +1 -0
  30. package/dist/font-runtime.js +1100 -0
  31. package/dist/font-runtime.js.map +1 -0
  32. package/dist/index.d.ts +62 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +130 -16
  35. package/dist/index.js.map +1 -1
  36. package/dist/page-geometry.d.ts +8 -1
  37. package/dist/page-geometry.d.ts.map +1 -1
  38. package/dist/page-geometry.js +37 -11
  39. package/dist/page-geometry.js.map +1 -1
  40. package/dist/pagination.bundle.js +1827 -410
  41. package/dist/pagination.d.ts +249 -15
  42. package/dist/pagination.d.ts.map +1 -1
  43. package/dist/pagination.js +1942 -453
  44. package/dist/pagination.js.map +1 -1
  45. package/dist/react.d.ts +11 -4
  46. package/dist/react.d.ts.map +1 -1
  47. package/dist/react.js +38 -5
  48. package/dist/react.js.map +1 -1
  49. package/dist/render-report-v2.schema.json +1581 -0
  50. package/dist/ribbon-chrome.d.ts +1 -1
  51. package/dist/ribbon-chrome.d.ts.map +1 -1
  52. package/dist/ribbon-chrome.js +45 -0
  53. package/dist/ribbon-chrome.js.map +1 -1
  54. package/dist/ribbon.js +70 -0
  55. package/dist/ribbon.js.map +1 -1
  56. package/dist/session.bundle.js +695 -29
  57. package/dist/session.d.ts +134 -19
  58. package/dist/session.d.ts.map +1 -1
  59. package/dist/session.js +562 -25
  60. package/dist/session.js.map +1 -1
  61. package/dist/types.d.ts +1207 -16
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js.map +1 -1
  64. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  65. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  66. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  67. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  68. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  69. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  70. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  71. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  72. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  73. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  74. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  75. package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
  76. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  77. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  78. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  79. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  80. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  81. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  82. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  83. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  84. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  85. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  86. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  87. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  88. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  89. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  90. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  91. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  92. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  93. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  94. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  95. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  96. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  97. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  98. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  99. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  100. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  101. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  102. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  103. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  104. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  105. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  106. package/dist/worker-proxy.bundle.js +196 -29
  107. package/dist/worker-proxy.d.ts +36 -3
  108. package/dist/worker-proxy.d.ts.map +1 -1
  109. package/dist/worker-proxy.js +186 -33
  110. package/dist/worker-proxy.js.map +1 -1
  111. package/package.json +22 -6
@@ -362,7 +362,46 @@ function targetSearchInElement(elementId, searchText, occurrence = 1) {
362
362
  }
363
363
 
364
364
  // src/session.ts
365
- var DocxSession = class {
365
+ function mutationBatchChangeSet(before, after, key) {
366
+ const beforeGroups = /* @__PURE__ */ new Map();
367
+ const afterGroups = /* @__PURE__ */ new Map();
368
+ const group = (items, target) => {
369
+ items.forEach((item, index) => {
370
+ const identity = key(item);
371
+ const indices = target.get(identity) ?? [];
372
+ indices.push(index);
373
+ target.set(identity, indices);
374
+ });
375
+ };
376
+ group(before, beforeGroups);
377
+ group(after, afterGroups);
378
+ const beforeMatched = before.map(() => false);
379
+ const afterMatched = after.map(() => false);
380
+ const modified = after.map(() => false);
381
+ for (const [identity, afterIndices] of afterGroups) {
382
+ const beforeIndices = beforeGroups.get(identity) ?? [];
383
+ for (const afterIndex of afterIndices) {
384
+ const beforeIndex = beforeIndices.find((index) => !beforeMatched[index] && JSON.stringify(before[index]) === JSON.stringify(after[afterIndex]));
385
+ if (beforeIndex === void 0) continue;
386
+ beforeMatched[beforeIndex] = true;
387
+ afterMatched[afterIndex] = true;
388
+ }
389
+ const remainingBefore = beforeIndices.filter((index) => !beforeMatched[index]);
390
+ const remainingAfter = afterIndices.filter((index) => !afterMatched[index]);
391
+ const modifiedCount = Math.min(remainingBefore.length, remainingAfter.length);
392
+ for (let index = 0; index < modifiedCount; index++) {
393
+ beforeMatched[remainingBefore[index]] = true;
394
+ afterMatched[remainingAfter[index]] = true;
395
+ modified[remainingAfter[index]] = true;
396
+ }
397
+ }
398
+ return {
399
+ added: after.filter((_, index) => !afterMatched[index]),
400
+ removed: before.filter((_, index) => !beforeMatched[index]),
401
+ modified: after.filter((_, index) => modified[index])
402
+ };
403
+ }
404
+ var DocxSession = class _DocxSession {
366
405
  /** @internal */
367
406
  constructor(handle, wasm) {
368
407
  // ─── Raw escape hatch ────────────────────────────────────────────────
@@ -378,6 +417,336 @@ var DocxSession = class {
378
417
  project() {
379
418
  return JSON.parse(this.wasm.Project(this.handle));
380
419
  }
420
+ /** Monotonic document version (0 at open; +1 per committed mutation/undo/redo). */
421
+ getVersion() {
422
+ return JSON.parse(this.wasm.GetVersion(this.handle)).version;
423
+ }
424
+ /**
425
+ * Verification manifest for the current logical checkpoint. Unsaved edits are included and
426
+ * the read does not mutate package bytes, caches, history, or the document version.
427
+ */
428
+ getPackageManifest() {
429
+ return JSON.parse(this.wasm.GetPackageManifest(this.handle));
430
+ }
431
+ /** Register a browser-materialized PageMap without changing the document version. */
432
+ registerPageMap(pageMap, expectedRendererFingerprint) {
433
+ return JSON.parse(this.wasm.RegisterPageMap(
434
+ this.handle,
435
+ JSON.stringify(pageMap),
436
+ expectedRendererFingerprint ?? ""
437
+ ));
438
+ }
439
+ getPageMapStatus(request) {
440
+ return JSON.parse(this.wasm.GetPageMapStatus(
441
+ this.handle,
442
+ request ? JSON.stringify(request) : ""
443
+ ));
444
+ }
445
+ getPageCitation(anchorId, request) {
446
+ return JSON.parse(this.wasm.GetPageCitation(
447
+ this.handle,
448
+ anchorId,
449
+ JSON.stringify(request)
450
+ ));
451
+ }
452
+ /** Evaluate optimistic guards without mutating or advancing the version. */
453
+ checkPreconditions(preconditions) {
454
+ return JSON.parse(
455
+ this.wasm.CheckPreconditions(this.handle, JSON.stringify(preconditions))
456
+ );
457
+ }
458
+ /**
459
+ * Guard any synchronous mutation. WASM calls are synchronous and single-threaded, so the
460
+ * check and callback form one uninterrupted client-side operation. Prefer a method's native
461
+ * `preconditions` option where it has one (notably replaceTextRange's match-count guard).
462
+ */
463
+ runWithPreconditions(preconditions, mutation) {
464
+ const checked = this.checkPreconditions(preconditions);
465
+ return checked.success ? mutation() : checked;
466
+ }
467
+ /**
468
+ * Execute synchronous mutations atomically by default. Atomic success is one undo/version
469
+ * unit; any failed or thrown step restores the exact package and history checkpoint.
470
+ */
471
+ executeBatch(steps, mode = "atomic") {
472
+ if (mode !== "atomic" && mode !== "best_effort") {
473
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
474
+ }
475
+ const baseVersion = this.getVersion();
476
+ const observationWarnings = [];
477
+ const inspect = (label, read, fallback) => {
478
+ try {
479
+ return read();
480
+ } catch (error) {
481
+ observationWarnings.push(`${label} unavailable: ${error instanceof Error ? error.message : String(error)}`);
482
+ return fallback;
483
+ }
484
+ };
485
+ const beforeRevisions = inspect("Revision delta inspection", () => this.listRevisions(), []);
486
+ const beforeComments = inspect("Comment delta inspection", () => this.listComments(), []);
487
+ const beforeAnnotations = inspect("Annotation delta inspection", () => this.listAnnotations(), []);
488
+ const complete = (result) => {
489
+ try {
490
+ const revisionChanges = mutationBatchChangeSet(
491
+ beforeRevisions,
492
+ inspect("Revision delta inspection", () => this.listRevisions(), beforeRevisions),
493
+ (revision) => revision.id
494
+ );
495
+ const commentChanges = mutationBatchChangeSet(
496
+ beforeComments,
497
+ inspect("Comment delta inspection", () => this.listComments(), beforeComments),
498
+ (comment) => comment.anchorId
499
+ );
500
+ const annotationChanges = mutationBatchChangeSet(
501
+ beforeAnnotations,
502
+ inspect("Annotation delta inspection", () => this.listAnnotations(), beforeAnnotations),
503
+ (annotation) => annotation.id ?? ""
504
+ );
505
+ const resultVersion = inspect(
506
+ "Result version inspection",
507
+ () => this.getVersion(),
508
+ baseVersion
509
+ );
510
+ const warnings = [...observationWarnings];
511
+ if ([...revisionChanges.added, ...revisionChanges.modified].some((revision) => revision.date !== void 0 && revision.date !== null)) {
512
+ warnings.push("Tracked-revision date attributes may use the execution clock; compare revision ids, authors, types, text, and anchors across separate executions.");
513
+ }
514
+ if ([...commentChanges.added, ...commentChanges.modified].some((comment) => comment.date !== void 0 && comment.date !== null)) {
515
+ warnings.push("Comment date attributes may be generated from the execution clock; supply dates explicitly when byte-identical replay is required.");
516
+ }
517
+ if (annotationChanges.added.some((annotation) => annotation.created !== void 0 && annotation.created !== null)) {
518
+ warnings.push("Auto-generated annotation ids or creation timestamps are execution metadata; supply id and created explicitly when byte-identical replay is required.");
519
+ }
520
+ if (result.steps.some((step) => step.results.some((edit) => edit.created.length > 0))) {
521
+ warnings.push("Created anchors and related OOXML ids may be generated independently on replay; preview/apply equivalence is semantic and packageHash or anchor ids may differ.");
522
+ }
523
+ if (mode === "best_effort" && !result.success) {
524
+ warnings.push("Best-effort execution retains every successful step despite later failures.");
525
+ }
526
+ let packageHash = null;
527
+ if (!this.wasm.GetPackageContentHash) {
528
+ warnings.push("This WASM bundle predates package equivalence hashes; packageHash is unavailable.");
529
+ } else {
530
+ try {
531
+ packageHash = this.wasm.GetPackageContentHash(this.handle);
532
+ } catch (error) {
533
+ warnings.push(`Package equivalence hash unavailable: ${error instanceof Error ? error.message : String(error)}`);
534
+ }
535
+ }
536
+ return {
537
+ ...result,
538
+ preview: false,
539
+ baseVersion,
540
+ resultVersion,
541
+ packageHash,
542
+ revisionChanges,
543
+ commentChanges,
544
+ annotationChanges,
545
+ warnings,
546
+ html: null
547
+ };
548
+ } catch (error) {
549
+ return {
550
+ ...result,
551
+ preview: false,
552
+ baseVersion,
553
+ resultVersion: inspect("Result version inspection", () => this.getVersion(), baseVersion),
554
+ packageHash: null,
555
+ revisionChanges: { added: [], removed: [], modified: [] },
556
+ commentChanges: { added: [], removed: [], modified: [] },
557
+ annotationChanges: { added: [], removed: [], modified: [] },
558
+ warnings: [
559
+ ...observationWarnings,
560
+ `Batch receipt enrichment unavailable: ${error instanceof Error ? error.message : String(error)}`
561
+ ],
562
+ html: null
563
+ };
564
+ }
565
+ };
566
+ const internalFailure = (value) => ({
567
+ success: false,
568
+ error: { code: "internal_error", message: value instanceof Error ? value.message : String(value) },
569
+ created: [],
570
+ removed: [],
571
+ modified: []
572
+ });
573
+ const run = (step) => {
574
+ try {
575
+ const value = step.mutation();
576
+ const results = Array.isArray(value) ? value : [value];
577
+ if (results.some((result) => result === null || typeof result !== "object" || typeof result.success !== "boolean")) {
578
+ return [internalFailure("batch mutation returned invalid edit results")];
579
+ }
580
+ return results;
581
+ } catch (error) {
582
+ return [internalFailure(error)];
583
+ }
584
+ };
585
+ const failureOf = (step, rolledBack) => ({
586
+ index: step.index,
587
+ tool: step.tool,
588
+ action: step.action,
589
+ error: step.results.find((result) => !result.success)?.error ?? { code: "internal_error", message: "batch step failed without an error" },
590
+ rolledBack
591
+ });
592
+ const preflightOne = (step) => {
593
+ try {
594
+ return step.preflight?.();
595
+ } catch (error) {
596
+ return internalFailure(error).error;
597
+ }
598
+ };
599
+ if (mode === "atomic") {
600
+ const preflight = steps.map(preflightOne);
601
+ const failedPreflight = preflight.findIndex((error) => error !== void 0);
602
+ if (failedPreflight >= 0) {
603
+ const source = steps[failedPreflight];
604
+ const failed2 = {
605
+ index: failedPreflight,
606
+ tool: source.tool,
607
+ action: source.action,
608
+ success: false,
609
+ rolledBack: true,
610
+ results: [{ success: false, error: preflight[failedPreflight], created: [], removed: [], modified: [] }]
611
+ };
612
+ return complete({
613
+ mode,
614
+ status: "failed",
615
+ success: false,
616
+ rolledBack: true,
617
+ steps: [failed2],
618
+ failure: failureOf(failed2, true)
619
+ });
620
+ }
621
+ const transaction = this.wasm.BeginTransaction(this.handle);
622
+ const completed2 = [];
623
+ try {
624
+ for (let index = 0; index < steps.length; index++) {
625
+ const source = steps[index];
626
+ const results = run(source);
627
+ const step = {
628
+ index,
629
+ tool: source.tool,
630
+ action: source.action,
631
+ success: results.every((result) => result.success),
632
+ rolledBack: false,
633
+ results
634
+ };
635
+ completed2.push(step);
636
+ if (!step.success) {
637
+ this.wasm.RollbackTransaction(transaction);
638
+ const rolledBack = completed2.map((value) => ({ ...value, rolledBack: true }));
639
+ const failed2 = rolledBack[rolledBack.length - 1];
640
+ return complete({
641
+ mode,
642
+ status: "failed",
643
+ success: false,
644
+ rolledBack: true,
645
+ steps: rolledBack,
646
+ failure: failureOf(failed2, true)
647
+ });
648
+ }
649
+ }
650
+ this.wasm.CommitTransaction(transaction);
651
+ return complete({ mode, status: "ok", success: true, rolledBack: false, steps: completed2 });
652
+ } catch (error) {
653
+ try {
654
+ this.wasm.RollbackTransaction(transaction);
655
+ } catch {
656
+ }
657
+ throw error;
658
+ }
659
+ }
660
+ const completed = steps.map((source, index) => {
661
+ const preflight = preflightOne(source);
662
+ const results = preflight ? [{ success: false, error: preflight, created: [], removed: [], modified: [] }] : run(source);
663
+ return {
664
+ index,
665
+ tool: source.tool,
666
+ action: source.action,
667
+ success: results.every((result) => result.success),
668
+ rolledBack: false,
669
+ results
670
+ };
671
+ });
672
+ const failed = completed.find((step) => !step.success);
673
+ return complete({
674
+ mode,
675
+ status: failed ? completed.some((step) => step.success) ? "partial" : "failed" : "ok",
676
+ success: failed === void 0,
677
+ rolledBack: false,
678
+ steps: completed,
679
+ failure: failed ? failureOf(failed, false) : void 0
680
+ });
681
+ }
682
+ /**
683
+ * Execute the same callback batch algorithm against a complete isolated package clone.
684
+ * Callbacks receive the shadow session explicitly; mutate that argument. The live session's
685
+ * package, caches, version, configuration, and undo/redo history are never execution targets.
686
+ */
687
+ previewBatch(steps, mode = "atomic", options) {
688
+ if (mode !== "atomic" && mode !== "best_effort") {
689
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
690
+ }
691
+ const htmlMode = options?.html ?? "none";
692
+ if (htmlMode !== "none" && htmlMode !== "scoped" && htmlMode !== "full") {
693
+ throw new RangeError(`unknown preview HTML mode: ${String(htmlMode)}`);
694
+ }
695
+ if (!this.wasm.OpenPreviewSession) {
696
+ throw new Error("This WASM bundle does not support isolated mutation previews.");
697
+ }
698
+ const shadow = new _DocxSession(this.wasm.OpenPreviewSession(this.handle), this.wasm);
699
+ try {
700
+ const result = shadow.executeBatch(
701
+ steps.map((step) => ({
702
+ tool: step.tool,
703
+ action: step.action,
704
+ mutation: () => step.mutation(shadow),
705
+ preflight: step.preflight ? () => step.preflight(shadow) : void 0
706
+ })),
707
+ mode
708
+ );
709
+ const warnings = [...result.warnings];
710
+ let html = null;
711
+ const unwrapRendered = (rendered) => {
712
+ if (rendered.trimStart().startsWith("{")) {
713
+ const envelope = JSON.parse(rendered);
714
+ if (envelope.error) {
715
+ warnings.push(`Preview HTML could not be generated: ${envelope.error}`);
716
+ return null;
717
+ }
718
+ }
719
+ return rendered;
720
+ };
721
+ const legacyProfileWarning = "This WASM bundle predates the shared preview HTML profile; preview HTML omits comments, annotations and headers/footers and may differ from other surfaces.";
722
+ try {
723
+ if (htmlMode === "scoped") {
724
+ if (!options?.htmlAnchorId) {
725
+ warnings.push("Scoped HTML was requested without htmlAnchorId; no HTML was generated.");
726
+ } else if (this.wasm.RenderPreviewBlockHtml) {
727
+ html = unwrapRendered(
728
+ this.wasm.RenderPreviewBlockHtml(shadow.handle, options.htmlAnchorId)
729
+ );
730
+ } else {
731
+ warnings.push(legacyProfileWarning);
732
+ html = shadow.renderBlock(options.htmlAnchorId);
733
+ }
734
+ } else if (htmlMode === "full") {
735
+ if (this.wasm.RenderPreviewHtml) {
736
+ html = unwrapRendered(this.wasm.RenderPreviewHtml(shadow.handle));
737
+ } else {
738
+ warnings.push(legacyProfileWarning);
739
+ html = unwrapRendered(this.wasm.RenderHtmlForReview ? this.wasm.RenderHtmlForReview(shadow.handle, "docx-", false, false, 1, true) : this.wasm.RenderHtml(shadow.handle, "docx-", false, false, 1));
740
+ }
741
+ }
742
+ } catch (error) {
743
+ warnings.push(`Preview HTML could not be generated: ${error instanceof Error ? error.message : String(error)}`);
744
+ }
745
+ return { ...result, preview: true, warnings, html };
746
+ } finally {
747
+ shadow.close();
748
+ }
749
+ }
381
750
  /**
382
751
  * Project a slice of the document keyed off an anchor — useful for showing
383
752
  * one section to an LLM at a time without paying the cost of projecting the
@@ -393,9 +762,9 @@ var DocxSession = class {
393
762
  *
394
763
  * @see docs/architecture/docx_mutation_api.md
395
764
  */
396
- projectAnchor(anchorId, depth = 2 /* SubtreeAndFollowingSiblings */) {
765
+ projectAnchor(anchorId, depth = 2 /* SubtreeAndFollowingSiblings */, citation) {
397
766
  return JSON.parse(
398
- this.wasm.ProjectAnchor(this.handle, anchorId, depth)
767
+ citation ? this.wasm.ProjectAnchorWithCitations(this.handle, anchorId, depth, JSON.stringify(citation)) : this.wasm.ProjectAnchor(this.handle, anchorId, depth)
399
768
  );
400
769
  }
401
770
  /**
@@ -419,11 +788,21 @@ var DocxSession = class {
419
788
  return html;
420
789
  }
421
790
  // ─── Tier A: text CRUD ───────────────────────────────────────────────
422
- replaceText(anchorId, markdown) {
423
- return JSON.parse(this.wasm.ReplaceText(this.handle, anchorId, markdown));
424
- }
425
- deleteBlock(anchorId) {
426
- return JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
791
+ replaceText(anchorId, markdown, preconditions) {
792
+ const apply = () => JSON.parse(
793
+ this.wasm.ReplaceText(this.handle, anchorId, markdown)
794
+ );
795
+ return preconditions ? this.runWithPreconditions(
796
+ { ...preconditions, anchorId: preconditions.anchorId ?? anchorId },
797
+ apply
798
+ ) : apply();
799
+ }
800
+ deleteBlock(anchorId, preconditions) {
801
+ const apply = () => JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
802
+ return preconditions ? this.runWithPreconditions(
803
+ { ...preconditions, anchorId: preconditions.anchorId ?? anchorId },
804
+ apply
805
+ ) : apply();
427
806
  }
428
807
  /** Reorder one top-level paragraph/heading/list/table block relative to another. */
429
808
  moveBlock(sourceAnchorId, targetAnchorId, position) {
@@ -478,7 +857,7 @@ var DocxSession = class {
478
857
  }
479
858
  /**
480
859
  * Insert a `rows`×`cols` table before/after the block. `options` controls borders, row-major
481
- * cell markdown, and cell alignment. The returned `EditResult.created` lists the cell-paragraph
860
+ * cell markdown, and cell alignment. The returned `EditResult.created` lists canonical `tc`
482
861
  * anchors (row-major), so each cell can then be addressed to fill/format.
483
862
  */
484
863
  insertTable(anchorId, position, rows, cols, options) {
@@ -487,10 +866,26 @@ var DocxSession = class {
487
866
  this.wasm.InsertTable(this.handle, anchorId, position, rows, cols, optionsJson)
488
867
  );
489
868
  }
869
+ /** Resolve a canonical `tbl` anchor to explicit table/row/column/cell identities. */
870
+ getTableMetadata(tableAnchorId) {
871
+ return JSON.parse(this.wasm.GetTableMetadata(this.handle, tableAnchorId));
872
+ }
873
+ /** Resolve a canonical `tc` anchor to its zero-based table-grid coordinate and spans. */
874
+ resolveTableCellAnchor(cellAnchorId) {
875
+ return JSON.parse(
876
+ this.wasm.ResolveTableCellAnchor(this.handle, cellAnchorId)
877
+ );
878
+ }
879
+ /** Resolve a zero-based table-grid coordinate to the physical `tc` covering it. */
880
+ resolveTableCellCoordinate(tableAnchorId, rowIndex, columnIndex) {
881
+ return JSON.parse(
882
+ this.wasm.ResolveTableCellCoordinate(this.handle, tableAnchorId, rowIndex, columnIndex)
883
+ );
884
+ }
490
885
  /**
491
- * Table row/column editing, addressed by a cell-paragraph anchor (e.g. one returned from
492
- * {@link insertTable}'s `created`). Insert clones the reference row/column's widths and starts
493
- * empty (`created` lists the new cell-paragraph anchors); delete of the last row/column removes
886
+ * Table row/column editing, addressed by the canonical `tc` anchor returned from
887
+ * {@link insertTable}'s `created` or table metadata. Insert clones the reference row/column's
888
+ * widths and starts empty (`created` lists new `tc` anchors); delete of the last row/column removes
494
889
  * the whole table. All four are grid-aware: inserting across a merge extends it, deleting
495
890
  * through one narrows it, and deleting a vertical merge's lead row promotes the next row to
496
891
  * carry it — the grid is never left ragged.
@@ -530,7 +925,7 @@ var DocxSession = class {
530
925
  return JSON.parse(this.wasm.UnmergeCells(this.handle, cellAnchorId));
531
926
  }
532
927
  /**
533
- * Table styling, addressed by a cell-paragraph anchor — the post-insert counterpart of
928
+ * Table styling, addressed by a canonical `tc` anchor — the post-insert counterpart of
534
929
  * {@link insertTable}'s options (issue #315 Stage A). `setColumnWidths` retunes `w:tblGrid` +
535
930
  * every row's cell width (one positive twip value per column) and pins the table to fixed
536
931
  * layout, exactly as inserting with explicit `columnWidths` would.
@@ -570,6 +965,19 @@ var DocxSession = class {
570
965
  this.wasm.SetRepeatHeaderRow(this.handle, cellAnchorId, repeat)
571
966
  );
572
967
  }
968
+ /** Apply row layout options to the row containing the canonical cell anchor. */
969
+ setTableRowOptions(cellAnchorId, options) {
970
+ return JSON.parse(
971
+ this.wasm.SetTableRowOptions(
972
+ this.handle,
973
+ cellAnchorId,
974
+ options.repeatHeader ?? null,
975
+ options.allowBreakAcrossPages ?? null,
976
+ options.heightTwips ?? null,
977
+ options.heightRule ?? "atLeast"
978
+ )
979
+ );
980
+ }
573
981
  // ─── Headers / footers / page numbers ────────────────────────────────
574
982
  /**
575
983
  * Set the running header story for the section that owns `anchorId` (any body block in that
@@ -664,6 +1072,25 @@ var DocxSession = class {
664
1072
  this.wasm.InsertFootnote(this.handle, anchorId, characterOffset, markdown)
665
1073
  );
666
1074
  }
1075
+ /**
1076
+ * Insert a Word-faithful internal cross-reference — a `REF` field targeting an existing
1077
+ * bookmark — at a character offset (issue #545). The field carries a cached result run
1078
+ * (the bookmarked text, or the target's auto-number under `referenceNumber`), so
1079
+ * renderers that do not recompute fields show a faithful snapshot and Word updates it
1080
+ * like a hand-authored cross-reference. A missing or incoherent bookmark fails with
1081
+ * `missing_bookmark_target`.
1082
+ */
1083
+ insertCrossReference(anchorId, characterOffset, bookmarkName, options) {
1084
+ return JSON.parse(
1085
+ this.wasm.InsertCrossReference(
1086
+ this.handle,
1087
+ anchorId,
1088
+ characterOffset,
1089
+ bookmarkName,
1090
+ options ? JSON.stringify(options) : ""
1091
+ )
1092
+ );
1093
+ }
667
1094
  /** Create an endnote — see {@link insertFootnote}; writes the endnotes part and a
668
1095
  * `w:endnoteReference`, and the created definition anchor has kind `en`. */
669
1096
  insertEndnote(anchorId, characterOffset, markdown) {
@@ -755,6 +1182,170 @@ var DocxSession = class {
755
1182
  listComments() {
756
1183
  return JSON.parse(this.wasm.ListComments(this.handle));
757
1184
  }
1185
+ listHyperlinks(scopes = 63 /* All */) {
1186
+ return JSON.parse(this.wasm.ListHyperlinks(this.handle, scopes));
1187
+ }
1188
+ addHyperlink(anchorId, span, kind, target) {
1189
+ return JSON.parse(this.wasm.AddHyperlink(
1190
+ this.handle,
1191
+ anchorId,
1192
+ span.start,
1193
+ span.length,
1194
+ kind,
1195
+ target
1196
+ ));
1197
+ }
1198
+ updateHyperlink(hyperlinkId, kind, target) {
1199
+ return JSON.parse(this.wasm.UpdateHyperlink(this.handle, hyperlinkId, kind, target));
1200
+ }
1201
+ removeHyperlink(hyperlinkId) {
1202
+ return JSON.parse(this.wasm.RemoveHyperlink(this.handle, hyperlinkId));
1203
+ }
1204
+ /** Versioned operational facts for native image inspection/mutation in this runtime. */
1205
+ getImageCapabilities() {
1206
+ return JSON.parse(this.wasm.GetImageCapabilities());
1207
+ }
1208
+ listImages(scopes = 63 /* All */) {
1209
+ return JSON.parse(this.wasm.ListImages(this.handle, scopes));
1210
+ }
1211
+ insertImage(anchorId, characterOffset, bytes, options = {}) {
1212
+ return JSON.parse(this.wasm.InsertImage(
1213
+ this.handle,
1214
+ anchorId,
1215
+ characterOffset,
1216
+ imageBytesToBase64(bytes),
1217
+ JSON.stringify(options)
1218
+ ));
1219
+ }
1220
+ replaceImage(imageId, bytes) {
1221
+ return JSON.parse(this.wasm.ReplaceImage(
1222
+ this.handle,
1223
+ imageId,
1224
+ imageBytesToBase64(bytes)
1225
+ ));
1226
+ }
1227
+ setImageDimensions(imageId, dimensions) {
1228
+ return JSON.parse(this.wasm.SetImageDimensions(
1229
+ this.handle,
1230
+ imageId,
1231
+ JSON.stringify(dimensions)
1232
+ ));
1233
+ }
1234
+ setImageMetadata(imageId, altText, title) {
1235
+ return JSON.parse(this.wasm.SetImageMetadata(
1236
+ this.handle,
1237
+ imageId,
1238
+ altText,
1239
+ title
1240
+ ));
1241
+ }
1242
+ setImageFloatingLayout(imageId, layout) {
1243
+ return JSON.parse(this.wasm.SetImageFloatingLayout(
1244
+ this.handle,
1245
+ imageId,
1246
+ JSON.stringify(layout)
1247
+ ));
1248
+ }
1249
+ removeImage(imageId) {
1250
+ return JSON.parse(this.wasm.RemoveImage(this.handle, imageId));
1251
+ }
1252
+ /** Native Word structured-document tags, in outer-before-inner story order. */
1253
+ listContentControls(scopes = 63 /* All */) {
1254
+ return JSON.parse(this.wasm.ListContentControls(this.handle, scopes));
1255
+ }
1256
+ fillContentControlText(anchorId, text, options = {}) {
1257
+ return JSON.parse(this.wasm.FillContentControlText(
1258
+ this.handle,
1259
+ anchorId,
1260
+ text,
1261
+ JSON.stringify(options)
1262
+ ));
1263
+ }
1264
+ fillContentControlRichText(anchorId, markdown, options = {}) {
1265
+ return JSON.parse(this.wasm.FillContentControlRichText(
1266
+ this.handle,
1267
+ anchorId,
1268
+ markdown,
1269
+ JSON.stringify(options)
1270
+ ));
1271
+ }
1272
+ setContentControlChecked(anchorId, isChecked, options = {}) {
1273
+ return JSON.parse(this.wasm.SetContentControlChecked(
1274
+ this.handle,
1275
+ anchorId,
1276
+ isChecked,
1277
+ JSON.stringify(options)
1278
+ ));
1279
+ }
1280
+ setContentControlDate(anchorId, value, displayText, options = {}) {
1281
+ const timestamp = value instanceof Date ? value.toISOString() : value;
1282
+ return JSON.parse(this.wasm.SetContentControlDate(
1283
+ this.handle,
1284
+ anchorId,
1285
+ timestamp,
1286
+ displayText ?? null,
1287
+ JSON.stringify(options)
1288
+ ));
1289
+ }
1290
+ selectContentControlItem(anchorId, value, options = {}) {
1291
+ return JSON.parse(this.wasm.SelectContentControlItem(
1292
+ this.handle,
1293
+ anchorId,
1294
+ value,
1295
+ JSON.stringify(options)
1296
+ ));
1297
+ }
1298
+ fillContentControlPicture(anchorId, bytes, options = {}) {
1299
+ return JSON.parse(this.wasm.FillContentControlPicture(
1300
+ this.handle,
1301
+ anchorId,
1302
+ imageBytesToBase64(bytes),
1303
+ JSON.stringify(options)
1304
+ ));
1305
+ }
1306
+ addRepeatingSectionItem(sectionAnchorId, afterItemAnchorId, options = {}) {
1307
+ return JSON.parse(this.wasm.AddRepeatingSectionItem(
1308
+ this.handle,
1309
+ sectionAnchorId,
1310
+ afterItemAnchorId ?? "",
1311
+ JSON.stringify(options)
1312
+ ));
1313
+ }
1314
+ removeRepeatingSectionItem(itemAnchorId) {
1315
+ return JSON.parse(this.wasm.RemoveRepeatingSectionItem(
1316
+ this.handle,
1317
+ itemAnchorId
1318
+ ));
1319
+ }
1320
+ listBookmarks(scopes = 63 /* All */) {
1321
+ return JSON.parse(this.wasm.ListBookmarks(this.handle, scopes));
1322
+ }
1323
+ addBookmark(name, range) {
1324
+ return JSON.parse(this.wasm.AddBookmark(
1325
+ this.handle,
1326
+ name,
1327
+ range.startAnchorId,
1328
+ range.startOffset,
1329
+ range.endAnchorId,
1330
+ range.endOffset
1331
+ ));
1332
+ }
1333
+ renameBookmark(name, newName) {
1334
+ return JSON.parse(this.wasm.RenameBookmark(this.handle, name, newName));
1335
+ }
1336
+ moveBookmark(name, range) {
1337
+ return JSON.parse(this.wasm.MoveBookmark(
1338
+ this.handle,
1339
+ name,
1340
+ range.startAnchorId,
1341
+ range.startOffset,
1342
+ range.endAnchorId,
1343
+ range.endOffset
1344
+ ));
1345
+ }
1346
+ removeBookmark(name) {
1347
+ return JSON.parse(this.wasm.RemoveBookmark(this.handle, name));
1348
+ }
758
1349
  // ─── Tracked revisions (issue #318) ──────────────────────────────────
759
1350
  /** Markup-native tracked-revision listing, in document order across body, headers,
760
1351
  * footers, footnotes, and endnotes. Ids are stable while the underlying markup
@@ -775,6 +1366,22 @@ var DocxSession = class {
775
1366
  rejectRevision(revisionId) {
776
1367
  return JSON.parse(this.wasm.RejectRevision(this.handle, revisionId));
777
1368
  }
1369
+ /**
1370
+ * Accept every live revision as one undoable session mutation.
1371
+ *
1372
+ * Fails closed: an unsupported, malformed, or ambiguous registry entry aborts the whole
1373
+ * operation (`revisionUnsupported`/`revisionMalformed`/`revisionAmbiguous`) and nothing is
1374
+ * mutated. There is no force mode — call {@link listRevisions} and read each entry's
1375
+ * `diagnostic` to see what blocks it.
1376
+ */
1377
+ acceptAllRevisions() {
1378
+ return JSON.parse(this.wasm.AcceptAllRevisions(this.handle));
1379
+ }
1380
+ /** Reject every live revision as one undoable session mutation. Fails closed exactly like
1381
+ * {@link acceptAllRevisions}. */
1382
+ rejectAllRevisions() {
1383
+ return JSON.parse(this.wasm.RejectAllRevisions(this.handle));
1384
+ }
778
1385
  // ─── Tier C: formatting ──────────────────────────────────────────────
779
1386
  applyFormat(anchorId, span, op) {
780
1387
  const spanJson = span ? JSON.stringify(span) : "";
@@ -880,6 +1487,10 @@ var DocxSession = class {
880
1487
  * replaces it with `replace`, preserving the surrounding run formatting that
881
1488
  * the match didn't touch. Returns one `EditResult` per attempted match.
882
1489
  *
1490
+ * A `find` that matches nothing fails with a single `text_not_found` result
1491
+ * naming the anchor and the needle — never an empty array. Pass
1492
+ * `expectedMatchCount: 0` to assert absence as a successful no-op instead.
1493
+ *
883
1494
  * Run-formatting contract: the replacement text inherits the formatting of
884
1495
  * the FIRST run the match spanned. Middle/trailing runs keep their `w:rPr`
885
1496
  * but lose the slice of text the match consumed.
@@ -1056,10 +1667,16 @@ var DocxSession = class {
1056
1667
  *
1057
1668
  * @see docs/architecture/docx_mutation_api.md#findplaceholders
1058
1669
  */
1059
- findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char) {
1060
- return JSON.parse(
1061
- this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary)
1062
- );
1670
+ findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char, citation) {
1671
+ const json = citation ? this.wasm.FindPlaceholdersWithCitations(
1672
+ this.handle,
1673
+ kinds,
1674
+ scope,
1675
+ contextChars,
1676
+ boundary,
1677
+ JSON.stringify(citation)
1678
+ ) : this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary);
1679
+ return JSON.parse(json);
1063
1680
  }
1064
1681
  /**
1065
1682
  * Returns a snapshot of edit-state introspection signals — placeholder counts,
@@ -1082,6 +1699,23 @@ var DocxSession = class {
1082
1699
  }
1083
1700
  return raw;
1084
1701
  }
1702
+ /**
1703
+ * Return stable semantic changes from the package opened for this session to
1704
+ * its current state. Requires `captureInitialProjection` (enabled by default).
1705
+ */
1706
+ getSemanticChanges() {
1707
+ return JSON.parse(this.wasm.GetSemanticChanges(this.handle));
1708
+ }
1709
+ /**
1710
+ * Run the default deliverable gate on this session's normal clean-save checkpoint.
1711
+ * With initial projection capture enabled (the default), exact opening bytes are the
1712
+ * baseline used for dispositions and semantic/package deltas.
1713
+ */
1714
+ verifyDeliverable() {
1715
+ return JSON.parse(
1716
+ this.wasm.VerifyDeliverable(this.handle)
1717
+ );
1718
+ }
1085
1719
  // ─── Annotation-based anchor discovery (#132) ────────────────────────
1086
1720
  /**
1087
1721
  * Resolves an annotation's range to the block-level markdown anchors covering
@@ -1097,8 +1731,9 @@ var DocxSession = class {
1097
1731
  *
1098
1732
  * @see docs/architecture/docx_mutation_api.md#findbyannotation
1099
1733
  */
1100
- findByAnnotation(annotationId) {
1101
- return JSON.parse(this.wasm.FindByAnnotation(this.handle, annotationId));
1734
+ findByAnnotation(annotationId, citation) {
1735
+ const json = citation ? this.wasm.FindByAnnotationWithCitations(this.handle, annotationId, JSON.stringify(citation)) : this.wasm.FindByAnnotation(this.handle, annotationId);
1736
+ return JSON.parse(json);
1102
1737
  }
1103
1738
  /**
1104
1739
  * Finds every annotation whose `labelId` matches and resolves each of their
@@ -1107,8 +1742,9 @@ var DocxSession = class {
1107
1742
  * annotations on different paragraphs become three entries). Annotations
1108
1743
  * whose bookmark resolves to no anchors are omitted from the result.
1109
1744
  */
1110
- findByLabel(labelId) {
1111
- return JSON.parse(this.wasm.FindByLabel(this.handle, labelId));
1745
+ findByLabel(labelId, citation) {
1746
+ const json = citation ? this.wasm.FindByLabelWithCitations(this.handle, labelId, JSON.stringify(citation)) : this.wasm.FindByLabel(this.handle, labelId);
1747
+ return JSON.parse(json);
1112
1748
  }
1113
1749
  /**
1114
1750
  * Resolves any bookmark in the main document part (Docxodus-managed or
@@ -1116,8 +1752,9 @@ var DocxSession = class {
1116
1752
  * order. Empty when the bookmark name is unknown. Use this for raw bookmark
1117
1753
  * names that didn't come from the annotation system.
1118
1754
  */
1119
- findByBookmark(bookmarkName) {
1120
- return JSON.parse(this.wasm.FindByBookmark(this.handle, bookmarkName));
1755
+ findByBookmark(bookmarkName, citation) {
1756
+ const json = citation ? this.wasm.FindByBookmarkWithCitations(this.handle, bookmarkName, JSON.stringify(citation)) : this.wasm.FindByBookmark(this.handle, bookmarkName);
1757
+ return JSON.parse(json);
1121
1758
  }
1122
1759
  // ─── Text/kind-based anchor discovery (#171) ─────────────────────────
1123
1760
  /**
@@ -1162,15 +1799,25 @@ var DocxSession = class {
1162
1799
  );
1163
1800
  }
1164
1801
  /**
1165
- * Return every anchor of the given `kind` (`"p"`, `"h"`, `"li"`, `"tbl"`,
1166
- * `"row"`, `"cell"`, …), in document order. Reads the projection's anchor
1802
+ * Return every anchor of the given `kind` — one of `"p"`, `"h"`, `"li"`,
1803
+ * `"tbl"`, `"tr"`, `"tc"`, `"col"`, `"sdt"`, `"sec"`, `"fn"`, `"en"`,
1804
+ * `"cmt"`, `"unk"` — in document order. Matching is an exact string comparison;
1805
+ * in particular the row and cell tokens are `"tr"` and `"tc"`, never
1806
+ * `"row"`/`"cell"`. Unknown strings are not parsed or rejected: they simply
1807
+ * return an empty array. `"img"` and `"drw"` likewise return empty because the
1808
+ * projection never assigns those reserved kinds; address images through the
1809
+ * image surface instead. Reads the projection's anchor
1167
1810
  * index directly — no text scan. Pass `scope` (e.g. `"body"`) to restrict to
1168
1811
  * a single part; omit it to span all scopes.
1169
1812
  */
1170
- findByKind(kind, scope) {
1171
- return JSON.parse(
1172
- this.wasm.FindByKind(this.handle, kind, scope ?? "")
1173
- );
1813
+ findByKind(kind, scope, citation) {
1814
+ const json = citation ? this.wasm.FindByKindWithCitations(
1815
+ this.handle,
1816
+ kind,
1817
+ scope ?? "",
1818
+ JSON.stringify(citation)
1819
+ ) : this.wasm.FindByKind(this.handle, kind, scope ?? "");
1820
+ return JSON.parse(json);
1174
1821
  }
1175
1822
  /**
1176
1823
  * Look up a single anchor's preview info — `{ id, kind, scope, textPreview }`.
@@ -1224,6 +1871,18 @@ var DocxSession = class {
1224
1871
  const raw = this.wasm.GetSectionInfo(this.handle, anchorId);
1225
1872
  return JSON.parse(raw);
1226
1873
  }
1874
+ /** Enumerate the document's explicit style catalog with resolved high-signal properties. */
1875
+ listStyles() {
1876
+ return JSON.parse(this.wasm.ListStyles(this.handle));
1877
+ }
1878
+ /** Inspect direct and effective paragraph/run formatting for one paragraph anchor. */
1879
+ getFormatting(anchorId) {
1880
+ return JSON.parse(this.wasm.GetFormatting(this.handle, anchorId));
1881
+ }
1882
+ /** Enumerate text-bearing runs as mutation-compatible anchor/span pairs. */
1883
+ listInlineSpans(anchorId) {
1884
+ return JSON.parse(this.wasm.ListInlineSpans(this.handle, anchorId));
1885
+ }
1227
1886
  /**
1228
1887
  * Enumerates every annotation persisted in the document. Lets an agent prime
1229
1888
  * itself with "here are the labeled regions you can target" before committing
@@ -1288,6 +1947,13 @@ var DocxSession = class {
1288
1947
  this.close();
1289
1948
  }
1290
1949
  };
1950
+ function imageBytesToBase64(bytes) {
1951
+ let binary = "";
1952
+ for (let offset = 0; offset < bytes.length; offset += 32768) {
1953
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
1954
+ }
1955
+ return globalThis.btoa(binary);
1956
+ }
1291
1957
  function openDocxSession(bytes, wasmExports2, settings) {
1292
1958
  const bridge = wasmExports2.DocxSessionBridge;
1293
1959
  const handle = bridge.OpenSession(bytes, settings ? JSON.stringify(settings) : "");
@@ -1339,17 +2005,41 @@ function pxToPt(px) {
1339
2005
  function ptToPx(pt) {
1340
2006
  return pt / 0.75;
1341
2007
  }
2008
+ function finiteDataNumber(value, fallback) {
2009
+ if (value === void 0 || value.trim() === "") return fallback;
2010
+ const parsed = Number(value);
2011
+ return Number.isFinite(parsed) ? parsed : fallback;
2012
+ }
2013
+ function positiveDataNumber(value, fallback) {
2014
+ const parsed = finiteDataNumber(value, fallback);
2015
+ return parsed > 0 ? parsed : fallback;
2016
+ }
2017
+ function nonNegativeDataNumber(value, fallback) {
2018
+ const parsed = finiteDataNumber(value, fallback);
2019
+ return parsed >= 0 ? parsed : fallback;
2020
+ }
2021
+ function positiveFinite(value, fallback) {
2022
+ return Number.isFinite(value) && value > 0 ? value : fallback;
2023
+ }
1342
2024
  function parseSectionDimensions(section) {
1343
- const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
1344
- const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
1345
- const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
1346
- const contentHeight = parseFloat(section.dataset.contentHeight || "") || pageHeight - 2 * DEFAULT_MARGIN;
1347
- const marginTop = parseFloat(section.dataset.marginTop || "") || DEFAULT_MARGIN;
1348
- const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
1349
- const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
1350
- const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
1351
- const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1352
- const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
2025
+ const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
2026
+ const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
2027
+ const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
2028
+ const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
2029
+ const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
2030
+ const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
2031
+ const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
2032
+ const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
2033
+ const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
2034
+ const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
2035
+ const headerDistance = nonNegativeDataNumber(
2036
+ section.dataset.headerHeight,
2037
+ DEFAULT_HEADER_FOOTER_DISTANCE
2038
+ );
2039
+ const footerDistance = nonNegativeDataNumber(
2040
+ section.dataset.footerHeight,
2041
+ DEFAULT_HEADER_FOOTER_DISTANCE
2042
+ );
1353
2043
  return {
1354
2044
  pageWidth,
1355
2045
  pageHeight,
@@ -1429,8 +2119,93 @@ function applyZoom(el, scale, naturalPt) {
1429
2119
  function samePageBox(a, b) {
1430
2120
  return a.pageWidth === b.pageWidth && a.pageHeight === b.pageHeight && a.marginTop === b.marginTop && a.marginRight === b.marginRight && a.marginBottom === b.marginBottom && a.marginLeft === b.marginLeft;
1431
2121
  }
2122
+ function createUnavailablePageMap(documentVersion, rendererFingerprint, mode = "continuous") {
2123
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
2124
+ return {
2125
+ schemaVersion: 1,
2126
+ mode,
2127
+ availability: "unavailable",
2128
+ documentVersion,
2129
+ rendererFingerprint,
2130
+ pages: [],
2131
+ fragments: []
2132
+ };
2133
+ }
2134
+ var activePageCitationHighlights = /* @__PURE__ */ new WeakMap();
2135
+ function clearPageCitationHighlight(root) {
2136
+ const active = activePageCitationHighlights.get(root);
2137
+ if (!active) return;
2138
+ if (active.highlightClass && active.addedClass) {
2139
+ active.target.classList.remove(active.highlightClass);
2140
+ }
2141
+ for (const style of active.inlineStyles ?? []) {
2142
+ if (style.value) {
2143
+ active.target.style.setProperty(style.name, style.value, style.priority);
2144
+ } else {
2145
+ active.target.style.removeProperty(style.name);
2146
+ }
2147
+ }
2148
+ activePageCitationHighlights.delete(root);
2149
+ }
2150
+ function navigateToPageCitation(root, citation, options = {}) {
2151
+ clearPageCitationHighlight(root);
2152
+ if (citation.availability !== "available" || citation.fragments.length === 0) {
2153
+ return { navigated: false, unavailableReason: "citation_unavailable" };
2154
+ }
2155
+ const byAttribute = (name, value, within = root) => {
2156
+ for (const node of Array.from(within.querySelectorAll(`[${name}]`))) {
2157
+ if (node.getAttribute(name) === value) return node;
2158
+ }
2159
+ return null;
2160
+ };
2161
+ const fragment = citation.fragments[0];
2162
+ let target = byAttribute("data-page-fragment-id", fragment.fragmentId);
2163
+ if (!target) {
2164
+ const page = byAttribute("data-page-number", String(fragment.pageNumber));
2165
+ if (page) target = byAttribute("data-source-anchor-id", citation.anchorId, page) ?? page;
2166
+ }
2167
+ if (!target) {
2168
+ return {
2169
+ navigated: false,
2170
+ pageNumber: fragment.pageNumber,
2171
+ fragmentId: fragment.fragmentId,
2172
+ unavailableReason: "fragment_not_found"
2173
+ };
2174
+ }
2175
+ if (options.highlightClass) {
2176
+ const addedClass = !target.classList.contains(options.highlightClass);
2177
+ target.classList.add(options.highlightClass);
2178
+ activePageCitationHighlights.set(root, {
2179
+ target,
2180
+ highlightClass: options.highlightClass,
2181
+ addedClass
2182
+ });
2183
+ } else if (options.highlight !== false) {
2184
+ const names = ["outline", "outline-offset", "background-color"];
2185
+ const inlineStyles = names.map((name) => ({
2186
+ name,
2187
+ value: target.style.getPropertyValue(name),
2188
+ priority: target.style.getPropertyPriority(name)
2189
+ }));
2190
+ target.style.setProperty("outline", "3px solid #f4b400", "important");
2191
+ target.style.setProperty("outline-offset", "2px", "important");
2192
+ target.style.setProperty("background-color", "rgba(255, 235, 59, .18)", "important");
2193
+ activePageCitationHighlights.set(root, { target, inlineStyles });
2194
+ }
2195
+ target.scrollIntoView({
2196
+ behavior: options.behavior ?? "smooth",
2197
+ block: options.block ?? "center"
2198
+ });
2199
+ return {
2200
+ navigated: true,
2201
+ target,
2202
+ pageNumber: fragment.pageNumber,
2203
+ fragmentId: fragment.fragmentId
2204
+ };
2205
+ }
1432
2206
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
1433
- var MIN_BODY_CONTENT_HEIGHT = 72;
2207
+ var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
2208
+ var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
1434
2209
  var PaginationEngine = class {
1435
2210
  /**
1436
2211
  * Creates a new pagination engine.
@@ -1440,24 +2215,46 @@ var PaginationEngine = class {
1440
2215
  * @param options - Pagination options
1441
2216
  */
1442
2217
  constructor(staging, container, options = {}) {
2218
+ this.createdPageCount = 0;
2219
+ this.footnoteSeparator = null;
2220
+ this.footnoteContinuationSeparator = null;
2221
+ this.footnoteLayoutLikeWord8 = false;
1443
2222
  this.pendingFootnoteContinuation = null;
1444
2223
  /** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
1445
2224
  this.pageNumbering = /* @__PURE__ */ new Map();
1446
- this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
1447
- this.containerElement = typeof container === "string" ? document.getElementById(container) : container;
2225
+ this.lastPages = [];
2226
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
2227
+ this.state = "ready";
2228
+ const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
2229
+ this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
2230
+ this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
1448
2231
  if (!this.stagingElement) {
1449
2232
  throw new Error("Staging element not found");
1450
2233
  }
1451
2234
  if (!this.containerElement) {
1452
2235
  throw new Error("Container element not found");
1453
2236
  }
2237
+ if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
2238
+ throw new Error("Staging and container elements must belong to the same document");
2239
+ }
2240
+ const view = ownerDocument.defaultView;
2241
+ if (!view) {
2242
+ throw new Error("Pagination requires an attached document with a defaultView");
2243
+ }
2244
+ this.document = ownerDocument;
2245
+ this.view = view;
1454
2246
  this.scale = options.scale ?? 1;
1455
2247
  this.cssPrefix = options.cssPrefix ?? "page-";
1456
2248
  this.showPageNumbers = options.showPageNumbers ?? true;
1457
2249
  this.pageGap = options.pageGap ?? 20;
1458
2250
  this.fragmentParagraphs = options.fragmentParagraphs ?? false;
2251
+ this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
2252
+ this.cancellationCheckpoint = options.checkCancellation;
2253
+ this.pageCountCheckpoint = options.checkPageCount;
2254
+ this.layoutToken = options.layoutToken;
1459
2255
  this.hfRegistry = /* @__PURE__ */ new Map();
1460
2256
  this.footnoteRegistry = /* @__PURE__ */ new Map();
2257
+ this.commentMarginRegistry = /* @__PURE__ */ new Map();
1461
2258
  }
1462
2259
  /**
1463
2260
  * Runs the pagination process.
@@ -1465,58 +2262,469 @@ var PaginationEngine = class {
1465
2262
  * @returns PaginationResult with page information
1466
2263
  */
1467
2264
  paginate() {
1468
- const pages = [];
1469
- let pageNumber = 1;
1470
- this.hfRegistry = this.parseHeaderFooterRegistry();
1471
- this.footnoteRegistry = this.parseFootnoteRegistry();
1472
- const sections = this.stagingElement.querySelectorAll(
1473
- "[data-section-index]"
1474
- );
1475
- this.pageNumbering = this.parsePageNumbering(sections);
1476
- const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
1477
- const runs = [];
1478
- for (const section of sectionsToProcess) {
1479
- const dims = parseSectionDimensions(section);
1480
- const previous = runs[runs.length - 1];
1481
- if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
1482
- previous.sections.push(section);
1483
- } else {
1484
- runs.push({
1485
- sections: [section],
1486
- sectionIndex: parseInt(section.dataset.sectionIndex || "0", 10),
1487
- dims
2265
+ this.checkpoint();
2266
+ if (this.state !== "ready") {
2267
+ throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
2268
+ }
2269
+ if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
2270
+ throw new Error("Pagination requires staging and container elements attached to their document");
2271
+ }
2272
+ if (this.document.documentElement.getBoundingClientRect().width <= 0) {
2273
+ throw new Error("Pagination requires a browsing context with non-zero layout");
2274
+ }
2275
+ this.state = "running";
2276
+ try {
2277
+ const pages = [];
2278
+ let pageNumber = 1;
2279
+ this.hfRegistry = this.parseHeaderFooterRegistry();
2280
+ this.footnoteRegistry = this.parseFootnoteRegistry();
2281
+ ({
2282
+ normal: this.footnoteSeparator,
2283
+ continuation: this.footnoteContinuationSeparator
2284
+ } = this.parseFootnoteSeparators());
2285
+ this.commentMarginRegistry = this.parseCommentMarginRegistry();
2286
+ this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
2287
+ const sections = this.stagingElement.querySelectorAll(
2288
+ "[data-section-index]"
2289
+ );
2290
+ this.pageNumbering = this.parsePageNumbering(sections);
2291
+ const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
2292
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
2293
+ const referencedFootnoteIds = /* @__PURE__ */ new Set();
2294
+ const referencedCommentIds = /* @__PURE__ */ new Set();
2295
+ for (const section of sectionsToProcess) {
2296
+ this.checkpoint();
2297
+ this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
2298
+ for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
2299
+ this.checkpoint();
2300
+ if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
2301
+ const id = reference.dataset.footnoteId;
2302
+ if (id) referencedFootnoteIds.add(id);
2303
+ }
2304
+ for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
2305
+ this.checkpoint();
2306
+ if (reference.closest(
2307
+ "#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
2308
+ )) continue;
2309
+ const id = reference.dataset.commentId;
2310
+ if (id) referencedCommentIds.add(id);
2311
+ }
2312
+ }
2313
+ for (const id of referencedFootnoteIds) {
2314
+ this.checkpoint();
2315
+ const source = this.footnoteRegistry.get(id);
2316
+ if (!source) continue;
2317
+ this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
2318
+ for (const reference of Array.from(
2319
+ source.querySelectorAll("[data-comment-id]")
2320
+ )) {
2321
+ this.checkpoint();
2322
+ const commentId = reference.dataset.commentId;
2323
+ if (commentId) referencedCommentIds.add(commentId);
2324
+ }
2325
+ }
2326
+ for (const id of referencedCommentIds) {
2327
+ this.checkpoint();
2328
+ const source = this.commentMarginRegistry.get(id);
2329
+ if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
2330
+ }
2331
+ const runs = [];
2332
+ for (const section of sectionsToProcess) {
2333
+ this.checkpoint();
2334
+ const dims = parseSectionDimensions(section);
2335
+ const previous = runs[runs.length - 1];
2336
+ if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
2337
+ previous.sections.push(section);
2338
+ previous.sectionDimensions.set(
2339
+ parseInt(section.dataset.sectionIndex || "0", 10),
2340
+ dims
2341
+ );
2342
+ } else {
2343
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
2344
+ runs.push({
2345
+ sections: [section],
2346
+ sectionIndex,
2347
+ sectionType: section.dataset.sectionType ?? "nextPage",
2348
+ dims,
2349
+ sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
2350
+ });
2351
+ }
2352
+ }
2353
+ for (const run of runs) {
2354
+ this.checkpoint();
2355
+ const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
2356
+ if (needsParityFiller) {
2357
+ const precedingPage = pages[pages.length - 1];
2358
+ const precedingPageInSection = parseInt(
2359
+ precedingPage.element.dataset.pageInSection ?? "1",
2360
+ 10
2361
+ );
2362
+ const precedingDisplayedPageNumber = parseInt(
2363
+ precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
2364
+ 10
2365
+ );
2366
+ const filler = this.createPage(
2367
+ precedingPage.dimensions,
2368
+ pageNumber,
2369
+ precedingPage.sectionIndex,
2370
+ precedingDisplayedPageNumber + 1,
2371
+ [],
2372
+ precedingPageInSection + 1,
2373
+ [],
2374
+ 0,
2375
+ null,
2376
+ void 0,
2377
+ true
2378
+ );
2379
+ pages.push(filler);
2380
+ pageNumber++;
2381
+ }
2382
+ this.stagingElement.style.visibility = "hidden";
2383
+ this.stagingElement.style.position = "absolute";
2384
+ this.stagingElement.style.left = "-9999px";
2385
+ this.stagingElement.style.display = "block";
2386
+ const blocks = [];
2387
+ for (const section of run.sections) {
2388
+ this.checkpoint();
2389
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
2390
+ const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
2391
+ section.style.width = `${sectionDims.contentWidth}pt`;
2392
+ const columnCount = parseInt(section.dataset.cols || "1", 10);
2393
+ if (columnCount > 1) {
2394
+ const gap = parseFloat(section.dataset.colGap || "");
2395
+ blocks.push(...this.buildColumnBlocks(
2396
+ section,
2397
+ sectionDims,
2398
+ sectionIndex,
2399
+ columnCount,
2400
+ Number.isFinite(gap) ? gap : 36
2401
+ ));
2402
+ } else {
2403
+ blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
2404
+ }
2405
+ }
2406
+ const sectionPages = this.flowToPages(
2407
+ blocks,
2408
+ run.dims,
2409
+ pageNumber,
2410
+ run.sectionIndex,
2411
+ run.sectionDimensions
2412
+ );
2413
+ this.checkpoint();
2414
+ pages.push(...sectionPages);
2415
+ pageNumber += sectionPages.length;
2416
+ }
2417
+ this.stagingElement.style.display = "none";
2418
+ this.substitutePageNumberFields(pages.length);
2419
+ for (const page of pages) {
2420
+ this.checkpoint();
2421
+ if (page.element.dataset.sectionFiller === "true") continue;
2422
+ const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
2423
+ const displayedPageNumber = parseInt(
2424
+ page.element.dataset.displayedPageNumber || String(page.pageNumber),
2425
+ 10
2426
+ );
2427
+ const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
2428
+ const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
2429
+ if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
2430
+ if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
2431
+ }
2432
+ this.qualifyPageFragments(pages);
2433
+ this.transferVisibleFragmentTargets();
2434
+ if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
2435
+ this.lastPages = pages;
2436
+ const result = {
2437
+ totalPages: pages.length,
2438
+ pages,
2439
+ pageMap: this.layoutToken ? this.materializePageMap(
2440
+ this.layoutToken.documentVersion,
2441
+ this.layoutToken.rendererFingerprint
2442
+ ) : void 0
2443
+ };
2444
+ this.state = "complete";
2445
+ return result;
2446
+ } catch (error) {
2447
+ this.state = "failed";
2448
+ throw error;
2449
+ }
2450
+ }
2451
+ checkpoint() {
2452
+ this.cancellationCheckpoint?.();
2453
+ }
2454
+ /**
2455
+ * Normalize visible fragment identities after a caller applies final standalone styles. This
2456
+ * deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
2457
+ * measurement cannot mutate a tree after it was declared stable.
2458
+ */
2459
+ normalizePageMapFragmentIdentities() {
2460
+ if (this.lastPages.length === 0) {
2461
+ throw new Error("paginate() must complete before fragment identities can be normalized");
2462
+ }
2463
+ this.normalizeVisiblePageFragments(this.lastPages);
2464
+ }
2465
+ /**
2466
+ * Materialize the last completed browser layout as portable page-relative point geometry.
2467
+ * The caller supplies both invalidation tokens; this engine never guesses a document version
2468
+ * or renderer fingerprint.
2469
+ */
2470
+ materializePageMap(documentVersion, rendererFingerprint) {
2471
+ if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
2472
+ throw new Error("documentVersion must be a non-negative safe integer");
2473
+ }
2474
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
2475
+ if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
2476
+ const pages = this.lastPages.map((page) => ({
2477
+ pageNumber: page.pageNumber,
2478
+ pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
2479
+ width: page.dimensions.pageWidth,
2480
+ height: page.dimensions.pageHeight,
2481
+ sectionIndex: page.sectionIndex,
2482
+ pageName: `docxodus-section-${page.sectionIndex}`
2483
+ }));
2484
+ const fragments = [];
2485
+ const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
2486
+ if (requiredAnchorIds.size === 0) {
2487
+ throw new Error("cannot publish an available PageMap without canonical source inventory");
2488
+ }
2489
+ const measuredAnchorIds = /* @__PURE__ */ new Set();
2490
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
2491
+ for (const page of this.lastPages) {
2492
+ this.checkpoint();
2493
+ const pageRect = page.element.getBoundingClientRect();
2494
+ if (pageRect.width <= 0 || pageRect.height <= 0) {
2495
+ throw new Error(`page ${page.pageNumber} has no measurable geometry`);
2496
+ }
2497
+ const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
2498
+ const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
2499
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
2500
+ for (const element of Array.from(nodes)) {
2501
+ this.checkpoint();
2502
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
2503
+ const anchorId = element.dataset.sourceAnchorId;
2504
+ if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
2505
+ throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
2506
+ }
2507
+ const rect = element.getBoundingClientRect();
2508
+ const style = this.view.getComputedStyle(element);
2509
+ const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
2510
+ if (deliberatelyHidden) continue;
2511
+ requiredAnchorIds.add(anchorId);
2512
+ const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
2513
+ const left2 = visibleRect.left;
2514
+ const top2 = visibleRect.top;
2515
+ const right2 = visibleRect.right;
2516
+ const bottom2 = visibleRect.bottom;
2517
+ if (rect.width <= 0 || rect.height <= 0 || right2 <= left2 || bottom2 <= top2) {
2518
+ continue;
2519
+ }
2520
+ measuredAnchorIds.add(anchorId);
2521
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
2522
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
2523
+ const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2524
+ if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
2525
+ throw new Error(
2526
+ `page ${page.pageNumber} fragment identity changed after final-tree normalization`
2527
+ );
2528
+ }
2529
+ fragments.push({
2530
+ fragmentId,
2531
+ anchorId,
2532
+ fragmentIndex,
2533
+ pageNumber: page.pageNumber,
2534
+ geometry: {
2535
+ x: (left2 - pageRect.left) * pointPerRenderedX,
2536
+ y: (top2 - pageRect.top) * pointPerRenderedY,
2537
+ width: (right2 - left2) * pointPerRenderedX,
2538
+ height: (bottom2 - top2) * pointPerRenderedY
2539
+ },
2540
+ story: this.storyForCanonicalAnchor(anchorId),
2541
+ inTableCell: element.matches("td,th") || element.closest("td,th") !== null
1488
2542
  });
1489
2543
  }
1490
2544
  }
1491
- for (const run of runs) {
1492
- this.stagingElement.style.visibility = "hidden";
1493
- this.stagingElement.style.position = "absolute";
1494
- this.stagingElement.style.left = "-9999px";
1495
- this.stagingElement.style.display = "block";
1496
- const blocks = [];
1497
- for (const section of run.sections) {
1498
- section.style.width = `${run.dims.contentWidth}pt`;
1499
- const columnCount = parseInt(section.dataset.cols || "1", 10);
1500
- if (columnCount > 1) {
1501
- const gap = parseFloat(section.dataset.colGap || "");
1502
- blocks.push(...this.buildColumnBlocks(
1503
- section,
1504
- run.dims,
1505
- run.sectionIndex,
1506
- columnCount,
1507
- Number.isFinite(gap) ? gap : 36
1508
- ));
1509
- } else {
1510
- blocks.push(...this.measureBlocks(section, run.dims));
2545
+ const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
2546
+ if (missingAnchor) {
2547
+ throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
2548
+ }
2549
+ return {
2550
+ schemaVersion: 1,
2551
+ mode: "paginated",
2552
+ availability: "available",
2553
+ documentVersion,
2554
+ rendererFingerprint,
2555
+ pages,
2556
+ fragments
2557
+ };
2558
+ }
2559
+ normalizeVisiblePageFragments(pages) {
2560
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
2561
+ for (const page of pages) {
2562
+ this.checkpoint();
2563
+ const pageRect = page.element.getBoundingClientRect();
2564
+ for (const element of Array.from(
2565
+ page.element.querySelectorAll("[data-source-anchor-id]")
2566
+ )) {
2567
+ this.checkpoint();
2568
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
2569
+ const anchorId = element.dataset.sourceAnchorId;
2570
+ if (!anchorId) continue;
2571
+ const style = this.view.getComputedStyle(element);
2572
+ if (style.display === "none" || style.visibility === "hidden") continue;
2573
+ const rect = element.getBoundingClientRect();
2574
+ const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
2575
+ if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
2576
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
2577
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
2578
+ element.dataset.pageNumber = String(page.pageNumber);
2579
+ element.dataset.fragmentIndex = String(fragmentIndex);
2580
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2581
+ }
2582
+ }
2583
+ }
2584
+ /**
2585
+ * Intersect an element with every ancestor that establishes an overflow clip before the page
2586
+ * root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
2587
+ * therefore must not satisfy PageMap completeness or inflate portable geometry.
2588
+ */
2589
+ intersectWithClippingAncestors(element, page, pageRect, rect) {
2590
+ let left2 = Math.max(rect.left, pageRect.left);
2591
+ let top2 = Math.max(rect.top, pageRect.top);
2592
+ let right2 = Math.min(rect.right, pageRect.right);
2593
+ let bottom2 = Math.min(rect.bottom, pageRect.bottom);
2594
+ const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
2595
+ for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
2596
+ const style = this.view.getComputedStyle(ancestor);
2597
+ const clipsX = clips(style.overflowX);
2598
+ const clipsY = clips(style.overflowY);
2599
+ if (!clipsX && !clipsY) continue;
2600
+ const ancestorRect = ancestor.getBoundingClientRect();
2601
+ if (clipsX) {
2602
+ left2 = Math.max(left2, ancestorRect.left);
2603
+ right2 = Math.min(right2, ancestorRect.right);
2604
+ }
2605
+ if (clipsY) {
2606
+ top2 = Math.max(top2, ancestorRect.top);
2607
+ bottom2 = Math.min(bottom2, ancestorRect.bottom);
2608
+ }
2609
+ }
2610
+ return { left: left2, top: top2, right: right2, bottom: bottom2 };
2611
+ }
2612
+ storyForCanonicalAnchor(anchorId) {
2613
+ const first = anchorId.indexOf(":");
2614
+ const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
2615
+ const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
2616
+ if (scope.startsWith("hdr")) return "header";
2617
+ if (scope.startsWith("ftr")) return "footer";
2618
+ if (scope === "fn") return "footnote";
2619
+ if (scope === "en") return "endnote";
2620
+ if (scope === "cmt") return "comment";
2621
+ return "body";
2622
+ }
2623
+ /**
2624
+ * Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
2625
+ * Registry wrappers are excluded when scanning staging because selectable registry contents are
2626
+ * inventoried separately. Producers may explicitly mark content that has no visual substrate
2627
+ * with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
2628
+ */
2629
+ collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
2630
+ const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
2631
+ candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
2632
+ for (const element of candidates) {
2633
+ if (excludeRegistries && element.closest(
2634
+ "#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
2635
+ )) {
2636
+ continue;
2637
+ }
2638
+ if (this.isZeroHeightExplicitBreakCarrier(element)) {
2639
+ element.dataset.pageMapExclude = "true";
2640
+ }
2641
+ if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
2642
+ const anchorId = element.dataset.sourceAnchorId;
2643
+ if (anchorId) destination.add(anchorId);
2644
+ }
2645
+ }
2646
+ isZeroHeightExplicitBreakCarrier(element) {
2647
+ const next = element.nextElementSibling;
2648
+ const bounds = element.getBoundingClientRect();
2649
+ return element.hasAttribute("data-source-anchor-id") && (element.textContent ?? "").replace(/\u00a0/g, "").trim() === "" && bounds.height <= 0.01 && !element.querySelector("[data-source-anchor-id]") && !element.querySelector("img,svg,canvas,table,hr,input,textarea,select") && (next?.dataset.pageBreak === "true" || next?.classList.contains(`${this.cssPrefix}break`) === true);
2650
+ }
2651
+ isDeliberatelyUnrenderedSource(element, sourceRoot) {
2652
+ for (let current = element; current; current = current.parentElement) {
2653
+ const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
2654
+ if (current.dataset.pageMapExclude === "true" || current.dataset.pageBreak === "true" || current.classList.contains(`${this.cssPrefix}break`) || zeroHeightExplicitBreakCarrier || current.hidden || current.getAttribute("aria-hidden") === "true" || current.style.display === "none" || current.style.visibility === "hidden") {
2655
+ return true;
2656
+ }
2657
+ if (current === sourceRoot) break;
2658
+ }
2659
+ return false;
2660
+ }
2661
+ /**
2662
+ * Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
2663
+ * canonical source identity plus page/fragment qualification instead.
2664
+ */
2665
+ qualifyPageFragments(pages) {
2666
+ const fragmentCounts = /* @__PURE__ */ new Map();
2667
+ const activeCanonicalIds = /* @__PURE__ */ new Set();
2668
+ const activeBareAnchorIds = /* @__PURE__ */ new Set();
2669
+ const makeInactive = (element) => {
2670
+ element.removeAttribute("data-anchor");
2671
+ element.removeAttribute("data-committed-text");
2672
+ if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
2673
+ };
2674
+ for (const page of pages) {
2675
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
2676
+ for (const element of Array.from(nodes)) {
2677
+ const anchorId = element.dataset.sourceAnchorId;
2678
+ if (!anchorId) continue;
2679
+ const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
2680
+ fragmentCounts.set(anchorId, fragmentIndex + 1);
2681
+ element.dataset.pageNumber = String(page.pageNumber);
2682
+ element.dataset.fragmentIndex = String(fragmentIndex);
2683
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2684
+ const story = this.storyForCanonicalAnchor(anchorId);
2685
+ const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
2686
+ if (element.hasAttribute("data-anchor")) {
2687
+ const bareAnchorId = element.dataset.anchor;
2688
+ if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
2689
+ activeCanonicalIds.add(anchorId);
2690
+ activeBareAnchorIds.add(bareAnchorId);
2691
+ } else {
2692
+ makeInactive(element);
2693
+ }
1511
2694
  }
1512
2695
  }
1513
- const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
1514
- pages.push(...sectionPages);
1515
- pageNumber += sectionPages.length;
1516
2696
  }
1517
- this.stagingElement.style.display = "none";
1518
- this.substitutePageNumberFields(pages.length);
1519
- return { totalPages: pages.length, pages };
2697
+ const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
2698
+ for (const element of Array.from(
2699
+ this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
2700
+ )) {
2701
+ const anchorId = element.dataset.sourceAnchorId;
2702
+ if (!anchorId) continue;
2703
+ const bareAnchorId = element.dataset.anchor;
2704
+ if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
2705
+ makeInactive(element);
2706
+ } else {
2707
+ activeStagingCanonicalIds.add(anchorId);
2708
+ activeBareAnchorIds.add(bareAnchorId);
2709
+ }
2710
+ }
2711
+ }
2712
+ /**
2713
+ * Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
2714
+ * fragment target copied into a visible page would therefore resolve to its earlier hidden
2715
+ * source. Transfer target ownership to the page presentation after flow is complete; registry
2716
+ * and wrapper IDs that have no visible counterpart remain available to pagination internals.
2717
+ */
2718
+ transferVisibleFragmentTargets() {
2719
+ const visibleIds = new Set(Array.from(
2720
+ this.containerElement.querySelectorAll("[id]")
2721
+ ).map((element) => element.id).filter(Boolean));
2722
+ if (visibleIds.size === 0) return;
2723
+ for (const source of Array.from(
2724
+ this.stagingElement.querySelectorAll("[id]")
2725
+ )) {
2726
+ if (visibleIds.has(source.id)) source.removeAttribute("id");
2727
+ }
1520
2728
  }
1521
2729
  /** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
1522
2730
  parsePageNumbering(sections) {
@@ -1560,9 +2768,8 @@ var PaginationEngine = class {
1560
2768
  if (markers.length === 0) continue;
1561
2769
  const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
1562
2770
  const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
1563
- const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
1564
2771
  const numbering = this.pageNumbering.get(sectionIndex) ?? {};
1565
- const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
2772
+ const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
1566
2773
  for (const marker of Array.from(markers)) {
1567
2774
  const kind = marker.dataset.field;
1568
2775
  if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
@@ -1574,17 +2781,26 @@ var PaginationEngine = class {
1574
2781
  /**
1575
2782
  * Measures all content blocks in a section.
1576
2783
  */
1577
- measureBlocks(section, dims) {
2784
+ measureBlocks(section, dims, sectionIndex) {
1578
2785
  const blocks = [];
1579
2786
  const children = Array.from(section.children);
1580
2787
  for (const child of children) {
2788
+ this.checkpoint();
1581
2789
  if (child.dataset.sectionIndex !== void 0) {
1582
- const nestedBlocks = this.measureBlocks(child, dims);
2790
+ const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
2791
+ const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
1583
2792
  blocks.push(...nestedBlocks);
1584
2793
  continue;
1585
2794
  }
2795
+ if (child.matches("section.endnotes")) {
2796
+ const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
2797
+ if (endnoteBlocks) {
2798
+ blocks.push(...endnoteBlocks);
2799
+ continue;
2800
+ }
2801
+ }
1586
2802
  const rect = child.getBoundingClientRect();
1587
- const style = window.getComputedStyle(child);
2803
+ const style = this.view.getComputedStyle(child);
1588
2804
  const marginTopPx = parseFloat(style.marginTop) || 0;
1589
2805
  const marginBottomPx = parseFloat(style.marginBottom) || 0;
1590
2806
  const heightPt = pxToPt(rect.height);
@@ -1593,6 +2809,7 @@ var PaginationEngine = class {
1593
2809
  const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
1594
2810
  blocks.push({
1595
2811
  element: child,
2812
+ sectionIndex,
1596
2813
  heightPt,
1597
2814
  marginTopPt,
1598
2815
  marginBottomPt,
@@ -1605,6 +2822,95 @@ var PaginationEngine = class {
1605
2822
  }
1606
2823
  return blocks;
1607
2824
  }
2825
+ /**
2826
+ * Flatten the converter's `section.endnotes > ol > li > p` presentation into
2827
+ * ordinary paragraph blocks. This preserves paragraph formatting and canonical
2828
+ * p:en/en:en identities while allowing the existing paragraph fragmenter to
2829
+ * split a long endnote across page boundaries.
2830
+ */
2831
+ measureSafeEndnoteBlocks(section, dims, sectionIndex) {
2832
+ const sectionChildren = Array.from(section.children);
2833
+ const list = sectionChildren.find((child) => child.tagName === "OL");
2834
+ if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
2835
+ return null;
2836
+ }
2837
+ const items = Array.from(list.children);
2838
+ if (items.length === 0 || items.some(
2839
+ (item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
2840
+ )) {
2841
+ return null;
2842
+ }
2843
+ const blocks = [];
2844
+ const sectionStyle = this.view.getComputedStyle(section);
2845
+ for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
2846
+ this.checkpoint();
2847
+ const clonedRule = rule.cloneNode(true);
2848
+ if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
2849
+ blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
2850
+ }
2851
+ const listStyle = this.view.getComputedStyle(list);
2852
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
2853
+ this.checkpoint();
2854
+ const item = items[itemIndex];
2855
+ const ownerAnchorId = item.dataset.sourceAnchorId;
2856
+ const paragraphs = Array.from(item.children);
2857
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
2858
+ this.checkpoint();
2859
+ const paragraph = paragraphs[paragraphIndex];
2860
+ if (!this.hasRangeFragmentSafeLayout(paragraph)) {
2861
+ return null;
2862
+ }
2863
+ const clone = paragraph.cloneNode(true);
2864
+ clone.dataset.paginationSafeEndnote = "true";
2865
+ clone.style.fontSize ||= sectionStyle.fontSize;
2866
+ clone.style.lineHeight ||= sectionStyle.lineHeight;
2867
+ clone.style.paddingLeft ||= listStyle.paddingLeft;
2868
+ if (ownerAnchorId && !Array.from(
2869
+ clone.querySelectorAll("[data-source-anchor-id]")
2870
+ ).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
2871
+ const owner = this.document.createElement("span");
2872
+ owner.dataset.sourceAnchorId = ownerAnchorId;
2873
+ while (clone.firstChild) owner.appendChild(clone.firstChild);
2874
+ clone.appendChild(owner);
2875
+ }
2876
+ if (paragraphIndex === 0) {
2877
+ const itemId = item.id;
2878
+ if (itemId) {
2879
+ if (clone.id && clone.id !== itemId) return null;
2880
+ clone.id = itemId;
2881
+ }
2882
+ const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
2883
+ const marker = this.formatOrderedListMarker(
2884
+ Number.isFinite(value) ? value : itemIndex + 1,
2885
+ listStyle.listStyleType
2886
+ );
2887
+ clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
2888
+ }
2889
+ blocks.push(this.measureElement(clone, dims, sectionIndex));
2890
+ }
2891
+ }
2892
+ return blocks;
2893
+ }
2894
+ /** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
2895
+ formatOrderedListMarker(value, listStyleType) {
2896
+ const format = (() => {
2897
+ switch (listStyleType) {
2898
+ case "lower-roman":
2899
+ return "lowerRoman";
2900
+ case "upper-roman":
2901
+ return "upperRoman";
2902
+ case "lower-alpha":
2903
+ case "lower-latin":
2904
+ return "lowerLetter";
2905
+ case "upper-alpha":
2906
+ case "upper-latin":
2907
+ return "upperLetter";
2908
+ default:
2909
+ return "decimal";
2910
+ }
2911
+ })();
2912
+ return formatPageNumber(value, format);
2913
+ }
1608
2914
  /**
1609
2915
  * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
1610
2916
  * blocks. Word lays such a section out as N columns inside the same body extent;
@@ -1622,7 +2928,7 @@ var PaginationEngine = class {
1622
2928
  const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
1623
2929
  const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
1624
2930
  const makeContainer = (slice) => {
1625
- const container = document.createElement("div");
2931
+ const container = this.document.createElement("div");
1626
2932
  container.style.columnCount = String(columnCount);
1627
2933
  container.style.columnGap = `${columnGapPt}pt`;
1628
2934
  for (const child of slice) {
@@ -1632,17 +2938,19 @@ var PaginationEngine = class {
1632
2938
  };
1633
2939
  let start2 = 0;
1634
2940
  while (start2 < children.length) {
2941
+ this.checkpoint();
1635
2942
  if (isBreak(children[start2])) {
1636
- blocks.push(this.measureElement(children[start2], dims));
2943
+ blocks.push(this.measureElement(children[start2], dims, sectionIndex));
1637
2944
  start2++;
1638
2945
  continue;
1639
2946
  }
1640
2947
  let end = start2 + 1;
1641
2948
  let container = makeContainer(children.slice(start2, end));
1642
- let measured = this.measureElement(container, dims);
2949
+ let measured = this.measureElement(container, dims, sectionIndex);
1643
2950
  while (end < children.length && !isBreak(children[end])) {
2951
+ this.checkpoint();
1644
2952
  const candidate = makeContainer(children.slice(start2, end + 1));
1645
- const candidateMeasured = this.measureElement(candidate, dims);
2953
+ const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
1646
2954
  if (candidateMeasured.heightPt > maxFragmentHeight) break;
1647
2955
  container = candidate;
1648
2956
  measured = candidateMeasured;
@@ -1650,6 +2958,7 @@ var PaginationEngine = class {
1650
2958
  }
1651
2959
  blocks.push({
1652
2960
  element: container,
2961
+ sectionIndex,
1653
2962
  heightPt: measured.heightPt,
1654
2963
  marginTopPt: measured.marginTopPt,
1655
2964
  marginBottomPt: measured.marginBottomPt,
@@ -1668,8 +2977,8 @@ var PaginationEngine = class {
1668
2977
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
1669
2978
  * rows because wrapping and collapsed borders change the height of a fragment.
1670
2979
  */
1671
- measureElement(element, dims) {
1672
- const measurementHost = document.createElement("div");
2980
+ measureElement(element, dims, sectionIndex) {
2981
+ const measurementHost = this.document.createElement("div");
1673
2982
  measurementHost.style.position = "absolute";
1674
2983
  measurementHost.style.visibility = "hidden";
1675
2984
  measurementHost.style.left = "-9999px";
@@ -1678,9 +2987,10 @@ var PaginationEngine = class {
1678
2987
  measurementHost.appendChild(measuredElement);
1679
2988
  this.stagingElement.appendChild(measurementHost);
1680
2989
  const rect = measuredElement.getBoundingClientRect();
1681
- const style = window.getComputedStyle(measuredElement);
2990
+ const style = this.view.getComputedStyle(measuredElement);
1682
2991
  const measured = {
1683
2992
  element,
2993
+ sectionIndex,
1684
2994
  heightPt: pxToPt(rect.height),
1685
2995
  marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
1686
2996
  marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
@@ -1774,6 +3084,7 @@ var PaginationEngine = class {
1774
3084
  const knownIds = new Set(existingFootnoteIds);
1775
3085
  const newIds = [];
1776
3086
  for (const block of blocks) {
3087
+ this.checkpoint();
1777
3088
  for (const id of this.extractFootnoteRefs(block.element)) {
1778
3089
  if (!knownIds.has(id)) {
1779
3090
  knownIds.add(id);
@@ -1791,8 +3102,8 @@ var PaginationEngine = class {
1791
3102
  smallestEffectiveContentHeight(dims, sectionIndex) {
1792
3103
  return Math.min(
1793
3104
  this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
1794
- this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
1795
- this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
3105
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
3106
+ this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
1796
3107
  );
1797
3108
  }
1798
3109
  /**
@@ -1830,10 +3141,11 @@ var PaginationEngine = class {
1830
3141
  if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
1831
3142
  return null;
1832
3143
  }
1833
- const table = wrapper.firstElementChild;
1834
- if (!(table instanceof HTMLTableElement)) {
3144
+ const tableElement = wrapper.firstElementChild;
3145
+ if (!tableElement || tableElement.localName !== "table") {
1835
3146
  return null;
1836
3147
  }
3148
+ const table = tableElement;
1837
3149
  const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
1838
3150
  if (!body || table.tHead || table.tFoot || body.rows.length < 2 || Array.from(table.children).some((child) => child !== body && child.tagName !== "COLGROUP") || table.querySelector("table, [rowspan], [colspan], [data-footnote-id]") || wrapper.querySelector("[data-footnote-id]")) {
1839
3151
  return null;
@@ -1847,8 +3159,10 @@ var PaginationEngine = class {
1847
3159
  const groups = [];
1848
3160
  let start2 = 0;
1849
3161
  while (start2 < rows.length) {
3162
+ this.checkpoint();
1850
3163
  let end = start2;
1851
3164
  while (end < rows.length) {
3165
+ this.checkpoint();
1852
3166
  const candidate = this.createSimpleTableFragment(
1853
3167
  wrapper,
1854
3168
  table,
@@ -1856,7 +3170,7 @@ var PaginationEngine = class {
1856
3170
  rows.slice(start2, end + 1),
1857
3171
  start2 === 0
1858
3172
  );
1859
- const measured = this.measureElement(candidate, dims);
3173
+ const measured = this.measureElement(candidate, dims, block.sectionIndex);
1860
3174
  if (measured.heightPt > maximumFragmentHeight) {
1861
3175
  break;
1862
3176
  }
@@ -1888,7 +3202,7 @@ var PaginationEngine = class {
1888
3202
  if (!isLast) {
1889
3203
  fragment.style.setProperty("margin-bottom", "0", "important");
1890
3204
  }
1891
- const measured = this.measureElement(fragment, dims);
3205
+ const measured = this.measureElement(fragment, dims, block.sectionIndex);
1892
3206
  if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
1893
3207
  return null;
1894
3208
  }
@@ -1903,35 +3217,198 @@ var PaginationEngine = class {
1903
3217
  return fragments;
1904
3218
  }
1905
3219
  /**
1906
- * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
1907
- * after whitespace or at a run boundary so the paginator never deliberately
1908
- * cuts through an ordinary word merely to fill a little more of a page.
1909
- */
1910
- paragraphFragmentEndpoints(paragraph) {
1911
- const endpoints = [];
1912
- const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
3220
+ * DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
3221
+ * offsets are checked against the browser's Unicode grapheme segmenter, so a
3222
+ * formatting-run boundary can never bisect a surrogate pair, combining
3223
+ * sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
3224
+ * PAGE/NUMPAGES field results are atomic because splitting their marker would
3225
+ * make later substitution duplicate or replace only half of the field.
3226
+ */
3227
+ paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
3228
+ const textNodes = [];
3229
+ const textStarts = /* @__PURE__ */ new Map();
3230
+ const lastTextByField = /* @__PURE__ */ new Map();
3231
+ const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
3232
+ const flattenedChunks = [];
3233
+ let flattenedLength = 0;
1913
3234
  let textNode;
1914
3235
  while (textNode = walker.nextNode()) {
1915
- const text = textNode.data;
1916
- if (text.length === 0) continue;
1917
- const whitespace = /\s+/g;
3236
+ this.checkpoint();
3237
+ textStarts.set(textNode, flattenedLength);
3238
+ textNodes.push(textNode);
3239
+ flattenedChunks.push(textNode.data);
3240
+ flattenedLength += textNode.data.length;
3241
+ let field = textNode.parentElement?.closest("[data-field]") ?? null;
3242
+ while (field?.parentElement?.closest("[data-field]")) {
3243
+ field = field.parentElement.closest("[data-field]");
3244
+ }
3245
+ if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
3246
+ }
3247
+ const flattenedText = flattenedChunks.join("");
3248
+ if (!this.hasVisibleText(flattenedText)) return [];
3249
+ const invisible = /[\s\u200B-\u200F\uFEFF]/;
3250
+ let firstVisibleOffset = -1;
3251
+ let lastVisibleOffset = -1;
3252
+ for (let index = 0; index < flattenedText.length; index++) {
3253
+ if (index % 4096 === 0) this.checkpoint();
3254
+ if (invisible.test(flattenedText[index])) continue;
3255
+ if (firstVisibleOffset < 0) firstVisibleOffset = index;
3256
+ lastVisibleOffset = index;
3257
+ }
3258
+ const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
3259
+ const candidates = [];
3260
+ const addCandidate = (candidate) => {
3261
+ const { textOffset } = candidate;
3262
+ if (textOffset <= 0 || textOffset >= flattenedText.length) return;
3263
+ if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
3264
+ if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
3265
+ if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
3266
+ if (graphemeBoundaries) {
3267
+ if (!graphemeBoundaries.has(textOffset)) return;
3268
+ } else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
3269
+ return;
3270
+ }
3271
+ candidates.push(candidate);
3272
+ };
3273
+ for (const node of textNodes) {
3274
+ this.checkpoint();
3275
+ const start2 = textStarts.get(node);
3276
+ const atomic = node.parentElement?.closest(
3277
+ "[data-field], [data-list-marker], a[data-comment-id]"
3278
+ );
3279
+ if (atomic && paragraph.contains(atomic)) continue;
3280
+ const whitespace = /[\u0009-\u000D\u0020]+/g;
1918
3281
  let match;
1919
- while ((match = whitespace.exec(text)) !== null) {
1920
- endpoints.push({ node: textNode, offset: match.index + match[0].length });
3282
+ let matchesSinceCheckpoint = 0;
3283
+ while ((match = whitespace.exec(node.data)) !== null) {
3284
+ if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
3285
+ const offset = match.index + match[0].length;
3286
+ addCandidate({ node, offset, textOffset: start2 + offset, priority: 0 });
3287
+ }
3288
+ addCandidate({
3289
+ node,
3290
+ offset: node.data.length,
3291
+ textOffset: start2 + node.data.length,
3292
+ priority: 0
3293
+ });
3294
+ if (emergencyGraphemeBreaks) {
3295
+ for (let offset = 1; offset < node.data.length; offset++) {
3296
+ if (offset % 256 === 0) this.checkpoint();
3297
+ addCandidate({
3298
+ node,
3299
+ offset,
3300
+ textOffset: start2 + offset,
3301
+ priority: -1
3302
+ });
3303
+ }
1921
3304
  }
1922
- if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
1923
- endpoints.push({ node: textNode, offset: text.length });
3305
+ }
3306
+ for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
3307
+ this.checkpoint();
3308
+ if (field.parentElement?.closest("[data-field]")) continue;
3309
+ const last = lastTextByField.get(field);
3310
+ const parent = field.parentNode;
3311
+ if (!last || !parent) continue;
3312
+ const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
3313
+ if (childIndex < 0) continue;
3314
+ addCandidate({
3315
+ node: parent,
3316
+ offset: childIndex + 1,
3317
+ textOffset: textStarts.get(last) + last.data.length,
3318
+ priority: 1
3319
+ });
3320
+ }
3321
+ candidates.sort((left2, right2) => left2.textOffset - right2.textOffset || right2.priority - left2.priority);
3322
+ const endpoints = [];
3323
+ for (const candidate of candidates) {
3324
+ if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
3325
+ endpoints.push(candidate);
1924
3326
  }
1925
3327
  }
1926
3328
  return endpoints;
1927
3329
  }
3330
+ /** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
3331
+ graphemeBoundaryOffsets(text) {
3332
+ const SegmenterConstructor = this.view.Intl.Segmenter;
3333
+ if (!SegmenterConstructor) return null;
3334
+ const boundaries = /* @__PURE__ */ new Set([0, text.length]);
3335
+ let segmentCount = 0;
3336
+ for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
3337
+ if (++segmentCount % 256 === 0) this.checkpoint();
3338
+ boundaries.add(part.index);
3339
+ boundaries.add(part.index + part.segment.length);
3340
+ }
3341
+ return boundaries;
3342
+ }
3343
+ /**
3344
+ * Conservative UAX #14/CSS wrapping opportunities used for synthetic page
3345
+ * boundaries. In particular, a formatting-run boundary is not itself a word
3346
+ * boundary, and Japanese opening/closing punctuation stays with its pair.
3347
+ * Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
3348
+ * the browser to wrap anywhere.
3349
+ */
3350
+ isLegalParagraphLineBoundary(paragraph, text, offset) {
3351
+ const lastCodeUnit = text.charCodeAt(offset - 1);
3352
+ const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
3353
+ const beforeCodePoint = text.codePointAt(beforeOffset);
3354
+ const afterCodePoint = text.codePointAt(offset);
3355
+ const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
3356
+ const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
3357
+ if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
3358
+ if (/[-\u00AD\u2010]$/u.test(before)) return true;
3359
+ const style = this.view.getComputedStyle(paragraph);
3360
+ if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
3361
+ const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
3362
+ if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
3363
+ const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
3364
+ const closing = ")]}>\u2019\u201D\u3001\u3002\u3009\u300B\u300D\u300F\u3011\u3015\u3017\u3019\u301B\uFF01\uFF05\uFF09\uFF0C\uFF0E\uFF1A\uFF1B\uFF1F\uFF3D\uFF5D";
3365
+ return !opening.includes(before) && !closing.includes(after);
3366
+ }
3367
+ /** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
3368
+ isNonBreakingTextBoundary(text, offset) {
3369
+ const codePointBefore = (index) => {
3370
+ if (index <= 0) return void 0;
3371
+ const last = text.charCodeAt(index - 1);
3372
+ const start2 = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
3373
+ return text.codePointAt(Math.max(0, start2));
3374
+ };
3375
+ const nonBreaking = /* @__PURE__ */ new Set([
3376
+ 160,
3377
+ 8206,
3378
+ 8207,
3379
+ 8209,
3380
+ 8234,
3381
+ 8235,
3382
+ 8236,
3383
+ 8237,
3384
+ 8238,
3385
+ 8239,
3386
+ 8288,
3387
+ 8294,
3388
+ 8295,
3389
+ 8296,
3390
+ 8297,
3391
+ 65279
3392
+ ]);
3393
+ return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
3394
+ }
3395
+ /**
3396
+ * Without Intl.Segmenter, admit only an all-ASCII boundary. This still
3397
+ * fragments ordinary prose while refusing to guess about Unicode clusters.
3398
+ */
3399
+ isConservativeFallbackTextBoundary(text, offset) {
3400
+ return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
3401
+ }
1928
3402
  /**
1929
3403
  * Whether a range contains visible text after ignoring bidi/zero-width marks.
1930
3404
  * Paragraph fragmentation deliberately excludes non-textual descendants, so
1931
3405
  * this is enough to reject empty head or tail fragments.
1932
3406
  */
1933
3407
  hasVisibleFragmentText(fragment) {
1934
- return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
3408
+ return this.hasVisibleText(fragment.textContent || "");
3409
+ }
3410
+ hasVisibleText(text) {
3411
+ return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
1935
3412
  }
1936
3413
  /**
1937
3414
  * Phase one only handles text-like paragraph descendants. Objects, explicit
@@ -1940,13 +3417,25 @@ var PaginationEngine = class {
1940
3417
  * fallback instead of risking broken content or duplicate anchors.
1941
3418
  */
1942
3419
  canFragmentParagraph(block) {
1943
- if (!this.fragmentParagraphs || block.element.tagName !== "P") {
3420
+ if (!this.fragmentParagraphs) {
1944
3421
  return false;
1945
3422
  }
1946
3423
  const paragraph = block.element;
1947
3424
  if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
1948
3425
  return false;
1949
3426
  }
3427
+ return this.canRangeFragmentParagraph(paragraph);
3428
+ }
3429
+ /**
3430
+ * Shared structural gate for body and note paragraph fragmentation. Footnote
3431
+ * first paragraphs are inline beside their marker, so that one known layout
3432
+ * context may opt into an inline root while retaining every descendant and
3433
+ * break-safety restriction used for body text.
3434
+ */
3435
+ canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
3436
+ if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
3437
+ return false;
3438
+ }
1950
3439
  const unsupportedDescendants = [
1951
3440
  "br",
1952
3441
  "img",
@@ -1977,21 +3466,27 @@ var PaginationEngine = class {
1977
3466
  "fieldset",
1978
3467
  "details",
1979
3468
  "[data-footnote-id]",
1980
- "[data-list-marker]",
1981
- "[data-anchor]",
1982
- "[id]",
1983
3469
  "[contenteditable]"
1984
3470
  ].join(", ");
1985
3471
  if (paragraph.querySelector(unsupportedDescendants)) {
1986
3472
  return false;
1987
3473
  }
1988
- const paragraphStyle = window.getComputedStyle(paragraph);
1989
- if (paragraphStyle.display !== "block" || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
3474
+ const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
3475
+ return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
3476
+ }
3477
+ /**
3478
+ * A range clone preserves nested inline formatting exactly. Anything that establishes its own
3479
+ * box/layout context is deferred until a future fragmenter can model it accurately. Callers
3480
+ * must invoke this while the paragraph is attached to the styled document.
3481
+ */
3482
+ hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
3483
+ const paragraphStyle = this.view.getComputedStyle(paragraph);
3484
+ if (paragraphStyle.display !== "block" && !(allowInlineRoot && paragraphStyle.display === "inline") || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" && paragraphStyle.whiteSpace !== "pre-wrap" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
1990
3485
  return false;
1991
3486
  }
1992
3487
  for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
1993
- const style = window.getComputedStyle(descendant);
1994
- if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
3488
+ const style = this.view.getComputedStyle(descendant);
3489
+ if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
1995
3490
  return false;
1996
3491
  }
1997
3492
  }
@@ -2018,58 +3513,122 @@ var PaginationEngine = class {
2018
3513
  return fragment;
2019
3514
  }
2020
3515
  /**
2021
- * Splits a simple paragraph at the largest DOM Range endpoint that fits the
2022
- * currently available body space. The caller then processes the tail normally,
2023
- * allowing it to fragment again on later pages when necessary.
3516
+ * Range clones repeat an inline ancestor when the split lands inside it. Keep
3517
+ * semantic wrappers (links, comments, formatting) on both sides, but retain a
3518
+ * duplicated HTML/editor identity only on the leading fragment. Targets that
3519
+ * occur wholly after the split are absent from `head` and remain on `tail`.
2024
3520
  */
2025
- tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
2026
- if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
2027
- return null;
2028
- }
2029
- const paragraph = block.element;
2030
- const endpoints = this.paragraphFragmentEndpoints(paragraph);
2031
- if (endpoints.length < 2) {
2032
- return null;
2033
- }
2034
- let low = 0;
2035
- let high = endpoints.length - 2;
2036
- let best = null;
2037
- while (low <= high) {
2038
- const middle = Math.floor((low + high) / 2);
2039
- const endpoint = endpoints[middle];
2040
- const headRange = document.createRange();
2041
- headRange.setStart(paragraph, 0);
2042
- headRange.setEnd(endpoint.node, endpoint.offset);
2043
- const headContents = headRange.cloneContents();
2044
- if (!this.hasVisibleFragmentText(headContents)) {
2045
- low = middle + 1;
2046
- continue;
3521
+ reconcileParagraphFragmentIdentities(head, tail) {
3522
+ const elements = (root, selector2) => [
3523
+ ...root.matches(selector2) ? [root] : [],
3524
+ ...Array.from(root.querySelectorAll(selector2))
3525
+ ];
3526
+ const headIds = new Set(elements(head, "[id]").map((element) => element.id));
3527
+ for (const element of elements(tail, "[id]")) {
3528
+ if (headIds.has(element.id)) element.removeAttribute("id");
3529
+ }
3530
+ const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
3531
+ for (const element of elements(tail, "[data-anchor]")) {
3532
+ if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
3533
+ element.removeAttribute("data-anchor");
3534
+ element.removeAttribute("data-committed-text");
3535
+ }
3536
+ }
3537
+ /**
3538
+ * Range-clone the largest safe prefix accepted by `fits`. The measurement
3539
+ * policy stays with the caller, allowing body blocks and note bands to share
3540
+ * one DOM fragmenter while measuring in their respective layout contexts.
3541
+ */
3542
+ splitParagraphAtLargestFit(paragraph, fits, options = {}) {
3543
+ const largestFit = (endpoints2) => {
3544
+ const lastEndpoint = endpoints2[endpoints2.length - 1];
3545
+ if (lastEndpoint) {
3546
+ const lastRange = this.document.createRange();
3547
+ lastRange.setStart(paragraph, 0);
3548
+ lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
3549
+ const lastContents = lastRange.cloneContents();
3550
+ if (this.hasVisibleFragmentText(lastContents)) {
3551
+ const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
3552
+ if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
3553
+ }
2047
3554
  }
2048
- const head = this.createParagraphFragment(paragraph, headRange, true, false);
2049
- const measured = this.measureElement(head, dims);
2050
- if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
2051
- best = { endpoint, element: head, measured };
2052
- low = middle + 1;
2053
- } else {
2054
- high = middle - 1;
3555
+ let low = 0;
3556
+ let high = endpoints2.length - 2;
3557
+ let best2 = null;
3558
+ while (low <= high) {
3559
+ this.checkpoint();
3560
+ const middle = Math.floor((low + high) / 2);
3561
+ const endpoint = endpoints2[middle];
3562
+ const headRange = this.document.createRange();
3563
+ headRange.setStart(paragraph, 0);
3564
+ headRange.setEnd(endpoint.node, endpoint.offset);
3565
+ const headContents = headRange.cloneContents();
3566
+ if (!this.hasVisibleFragmentText(headContents)) {
3567
+ low = middle + 1;
3568
+ continue;
3569
+ }
3570
+ const head = this.createParagraphFragment(paragraph, headRange, true, false);
3571
+ if (fits(head)) {
3572
+ best2 = { endpoint, head };
3573
+ low = middle + 1;
3574
+ } else {
3575
+ high = middle - 1;
3576
+ }
2055
3577
  }
3578
+ return best2;
3579
+ };
3580
+ let endpoints = this.paragraphFragmentEndpoints(paragraph);
3581
+ let best = largestFit(endpoints);
3582
+ if (!best && options.emergencyGraphemeBreaks) {
3583
+ endpoints = this.paragraphFragmentEndpoints(paragraph, true);
3584
+ best = largestFit(endpoints);
3585
+ }
3586
+ if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
3587
+ const endpoint = endpoints[0];
3588
+ const headRange = this.document.createRange();
3589
+ headRange.setStart(paragraph, 0);
3590
+ headRange.setEnd(endpoint.node, endpoint.offset);
3591
+ best = {
3592
+ endpoint,
3593
+ head: this.createParagraphFragment(paragraph, headRange, true, false)
3594
+ };
2056
3595
  }
2057
3596
  if (!best) {
2058
- return null;
3597
+ return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
2059
3598
  }
2060
- const tailRange = document.createRange();
3599
+ const tailRange = this.document.createRange();
2061
3600
  tailRange.setStart(best.endpoint.node, best.endpoint.offset);
2062
3601
  tailRange.setEnd(paragraph, paragraph.childNodes.length);
2063
3602
  const tailContents = tailRange.cloneContents();
2064
- if (!this.hasVisibleFragmentText(tailContents)) {
3603
+ if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
3604
+ const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
3605
+ this.reconcileParagraphFragmentIdentities(best.head, tail);
3606
+ return {
3607
+ kind: "split",
3608
+ head: best.head,
3609
+ tail
3610
+ };
3611
+ }
3612
+ /**
3613
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
3614
+ * currently available body space. The caller then processes the tail normally,
3615
+ * allowing it to fragment again on later pages when necessary.
3616
+ */
3617
+ tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
3618
+ if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
2065
3619
  return null;
2066
3620
  }
2067
- const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
2068
- const tailMeasured = this.measureElement(tail, dims);
3621
+ const split = this.splitParagraphAtLargestFit(block.element, (head) => {
3622
+ const measured = this.measureElement(head, dims, block.sectionIndex);
3623
+ return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
3624
+ });
3625
+ if (split.kind !== "split") return null;
3626
+ const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
3627
+ const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
2069
3628
  return [
2070
3629
  {
2071
- ...best.measured,
2072
- element: best.element,
3630
+ ...headMeasured,
3631
+ element: split.head,
2073
3632
  keepWithNext: false,
2074
3633
  keepLines: false,
2075
3634
  pageBreakBefore: false,
@@ -2077,7 +3636,7 @@ var PaginationEngine = class {
2077
3636
  },
2078
3637
  {
2079
3638
  ...tailMeasured,
2080
- element: tail,
3639
+ element: split.tail,
2081
3640
  keepWithNext: false,
2082
3641
  keepLines: false,
2083
3642
  pageBreakBefore: false,
@@ -2148,7 +3707,7 @@ var PaginationEngine = class {
2148
3707
  const registry = /* @__PURE__ */ new Map();
2149
3708
  const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
2150
3709
  if (!registryEl) return registry;
2151
- const entries = Array.from(registryEl.querySelectorAll("[data-footnote-id]"));
3710
+ const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
2152
3711
  for (const entry of entries) {
2153
3712
  const footnoteId = entry.dataset.footnoteId;
2154
3713
  if (footnoteId) {
@@ -2157,6 +3716,68 @@ var PaginationEngine = class {
2157
3716
  }
2158
3717
  return registry;
2159
3718
  }
3719
+ /** Read Word's optional normal and continuation separator stories. */
3720
+ parseFootnoteSeparators() {
3721
+ const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
3722
+ const clone = (kind) => {
3723
+ const source = Array.from(registry?.children ?? []).find(
3724
+ (entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
3725
+ );
3726
+ return source?.cloneNode(true);
3727
+ };
3728
+ return {
3729
+ normal: clone("normal") ?? null,
3730
+ continuation: clone("continuation") ?? null
3731
+ };
3732
+ }
3733
+ /** Append the exact separator story selected for this initial/continued note band. */
3734
+ appendFootnoteSeparator(container, continuation) {
3735
+ const kind = continuation ? "continuation" : "normal";
3736
+ const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
3737
+ if (!source) {
3738
+ const fallback = this.document.createElement("hr");
3739
+ fallback.dataset.footnoteSeparator = kind;
3740
+ container.appendChild(fallback);
3741
+ return;
3742
+ }
3743
+ const clone = source.cloneNode(true);
3744
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
3745
+ element.removeAttribute("id");
3746
+ element.removeAttribute("data-anchor");
3747
+ element.removeAttribute("data-committed-text");
3748
+ element.removeAttribute("data-source-anchor-id");
3749
+ element.removeAttribute("data-page-fragment-id");
3750
+ element.removeAttribute("data-fragment-index");
3751
+ element.removeAttribute("data-footnote-id");
3752
+ element.removeAttribute("data-comment-id");
3753
+ element.removeAttribute("name");
3754
+ if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
3755
+ element.removeAttribute("href");
3756
+ }
3757
+ element.setAttribute("contenteditable", "false");
3758
+ }
3759
+ container.appendChild(clone);
3760
+ }
3761
+ /** Parses the hidden source notes used to render paginated margin comments. */
3762
+ parseCommentMarginRegistry() {
3763
+ const registry = /* @__PURE__ */ new Map();
3764
+ const registryEl = this.stagingElement.querySelector(
3765
+ "#pagination-comment-margin-registry"
3766
+ );
3767
+ if (!registryEl) return registry;
3768
+ for (const entry of Array.from(
3769
+ registryEl.querySelectorAll("[data-comment-id]")
3770
+ )) {
3771
+ const commentId = entry.dataset.commentId;
3772
+ if (!commentId) continue;
3773
+ let root = entry;
3774
+ for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
3775
+ root = parent;
3776
+ }
3777
+ registry.set(commentId, root.cloneNode(true));
3778
+ }
3779
+ return registry;
3780
+ }
2160
3781
  /**
2161
3782
  * Extracts footnote reference IDs from an element.
2162
3783
  */
@@ -2171,6 +3792,92 @@ var PaginationEngine = class {
2171
3792
  }
2172
3793
  return ids;
2173
3794
  }
3795
+ /** Clone a note child for the continuation queue with its original selector position. */
3796
+ cloneFootnoteElementForContinuation(element, sourceIndex) {
3797
+ const clone = element.cloneNode(true);
3798
+ clone.setAttribute(
3799
+ FOOTNOTE_SOURCE_POSITION_ATTR,
3800
+ sourceIndex === 0 ? "first" : "later"
3801
+ );
3802
+ return clone;
3803
+ }
3804
+ /** Remove identities that belong only to the initial registry presentation shell. */
3805
+ makeContinuationShellInert(element) {
3806
+ element.removeAttribute("id");
3807
+ element.removeAttribute("data-anchor");
3808
+ element.removeAttribute("data-committed-text");
3809
+ }
3810
+ /**
3811
+ * Build the exact continuation shape shared by measurement and paint.
3812
+ *
3813
+ * Keep the source `.footnote-item > .footnote-content` ancestry: document
3814
+ * CSS, inherited direction/language, and custom classes frequently target
3815
+ * those shells. Only the number and initial HTML/editor identities are
3816
+ * omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
3817
+ * whose first carried element was a later source paragraph.
3818
+ */
3819
+ createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
3820
+ const source = this.footnoteRegistry.get(continuation.footnoteId);
3821
+ const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
3822
+ this.makeContinuationShellInert(wrapper);
3823
+ wrapper.classList.add("footnote-continuation");
3824
+ wrapper.dataset.footnoteId = continuation.footnoteId;
3825
+ if (continuation.sourceAnchorId) {
3826
+ wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
3827
+ }
3828
+ const sourceContent = source?.querySelector(".footnote-content");
3829
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
3830
+ this.makeContinuationShellInert(content);
3831
+ if (!sourceContent) content.className = "footnote-content";
3832
+ const first = elements[0];
3833
+ if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
3834
+ const sentinel = this.document.createElement("p");
3835
+ sentinel.className = "footnote-continuation-position-sentinel";
3836
+ sentinel.setAttribute("aria-hidden", "true");
3837
+ sentinel.style.setProperty("display", "none", "important");
3838
+ content.appendChild(sentinel);
3839
+ }
3840
+ for (let index = 0; index < elements.length; index++) {
3841
+ this.checkpoint();
3842
+ const element = elements[index];
3843
+ const clone = element.cloneNode(true);
3844
+ clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
3845
+ if (index === 0 && clone.tagName === "P") {
3846
+ clone.style.setProperty("display", "block", "important");
3847
+ }
3848
+ content.appendChild(clone);
3849
+ }
3850
+ wrapper.appendChild(content);
3851
+ return wrapper;
3852
+ }
3853
+ /** Build the exact partial-note item shape shared by measurement and paint. */
3854
+ createPartialFootnoteItem(footnote, fittingElements) {
3855
+ const item = footnote.cloneNode(false);
3856
+ const number = footnote.querySelector(".footnote-number");
3857
+ if (number) item.appendChild(number.cloneNode(true));
3858
+ const sourceContent = footnote.querySelector(".footnote-content");
3859
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
3860
+ if (!sourceContent) content.className = "footnote-content";
3861
+ for (const element of fittingElements) {
3862
+ this.checkpoint();
3863
+ content.appendChild(element.cloneNode(true));
3864
+ }
3865
+ item.appendChild(content);
3866
+ return item;
3867
+ }
3868
+ /**
3869
+ * Where a note measurement tree is attached.
3870
+ *
3871
+ * A note band is reserved from a measurement and then painted; if the two
3872
+ * happen under different inherited typography the painted notes overflow the
3873
+ * reserve, which is the invisible clipping this engine exists to avoid. The
3874
+ * registry lives in the staging tree but a band paints inside the output
3875
+ * container, so measure there. The host is never a page box, so a page's
3876
+ * `zoom` cannot scale a reserve that is accounted for in unscaled points.
3877
+ */
3878
+ noteMeasurementHost() {
3879
+ return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
3880
+ }
2174
3881
  /**
2175
3882
  * Measures the height of footnotes for given IDs (in points).
2176
3883
  * Creates a temporary container to measure the footnotes.
@@ -2178,140 +3885,246 @@ var PaginationEngine = class {
2178
3885
  * @param contentWidth - Width for measurement
2179
3886
  * @param continuation - Optional continuation content to include first
2180
3887
  */
2181
- measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
3888
+ measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
2182
3889
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2183
- if (footnoteIds.length === 0 && !hasContinuation || this.footnoteRegistry.size === 0) {
3890
+ if (footnoteIds.length === 0 && !hasContinuation) {
3891
+ return 0;
3892
+ }
3893
+ if (this.footnoteRegistry.size === 0 && !hasContinuation) {
2184
3894
  return 0;
2185
3895
  }
2186
- const measureContainer = document.createElement("div");
3896
+ const measureContainer = this.document.createElement("div");
2187
3897
  measureContainer.style.position = "absolute";
2188
3898
  measureContainer.style.visibility = "hidden";
2189
3899
  measureContainer.style.width = `${contentWidth}pt`;
2190
3900
  measureContainer.style.left = "-9999px";
2191
3901
  measureContainer.className = this.cssPrefix + "footnotes";
2192
- const hr = document.createElement("hr");
2193
- measureContainer.appendChild(hr);
3902
+ this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
2194
3903
  if (hasContinuation) {
2195
- const contWrapper = document.createElement("div");
2196
- contWrapper.className = "footnote-continuation";
2197
- for (const el of continuation.remainingElements) {
2198
- contWrapper.appendChild(el.cloneNode(true));
2199
- }
2200
- measureContainer.appendChild(contWrapper);
3904
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
2201
3905
  }
3906
+ const partialById = new Map(
3907
+ partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
3908
+ );
2202
3909
  for (const id of footnoteIds) {
3910
+ this.checkpoint();
2203
3911
  const footnote = this.footnoteRegistry.get(id);
2204
3912
  if (footnote) {
2205
- measureContainer.appendChild(footnote.cloneNode(true));
3913
+ const partial = partialById.get(id);
3914
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
2206
3915
  }
2207
3916
  }
2208
- this.stagingElement.appendChild(measureContainer);
2209
- const rect = measureContainer.getBoundingClientRect();
2210
- const heightPt = pxToPt(rect.height);
2211
- this.stagingElement.removeChild(measureContainer);
2212
- return heightPt;
3917
+ this.noteMeasurementHost().appendChild(measureContainer);
3918
+ try {
3919
+ return pxToPt(measureContainer.getBoundingClientRect().height);
3920
+ } finally {
3921
+ measureContainer.remove();
3922
+ }
2213
3923
  }
2214
3924
  /**
2215
- * Measures the height of just the continuation content (in points).
3925
+ * Partition a continuation for one page's note band, preferring complete children
3926
+ * and range-fragmenting an eligible paragraph when necessary. Always advances by
3927
+ * at least one element so an indivisible oversized paragraph follows the established
3928
+ * clipped fallback without trapping pagination in a loop.
2216
3929
  */
2217
- measureContinuationHeight(continuation, contentWidth) {
2218
- if (!continuation || continuation.remainingElements.length === 0) {
2219
- return 0;
2220
- }
2221
- const measureContainer = document.createElement("div");
3930
+ splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
3931
+ const fitting = [];
3932
+ let remaining = [];
3933
+ const measureContainer = this.document.createElement("div");
2222
3934
  measureContainer.style.position = "absolute";
2223
3935
  measureContainer.style.visibility = "hidden";
2224
3936
  measureContainer.style.width = `${contentWidth}pt`;
2225
- measureContainer.style.left = "-9999px";
2226
- measureContainer.className = this.cssPrefix + "footnotes";
2227
- const hr = document.createElement("hr");
2228
- measureContainer.appendChild(hr);
2229
- for (const el of continuation.remainingElements) {
2230
- measureContainer.appendChild(el.cloneNode(true));
2231
- }
2232
- this.stagingElement.appendChild(measureContainer);
2233
- const rect = measureContainer.getBoundingClientRect();
2234
- const heightPt = pxToPt(rect.height);
2235
- this.stagingElement.removeChild(measureContainer);
2236
- return heightPt;
2237
- }
2238
- /**
2239
- * Splits a footnote element into parts that fit within the available height.
2240
- * Returns the elements that fit and the elements that need to continue.
2241
- */
2242
- splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
2243
- const footnoteContent = footnoteElement.querySelector(".footnote-content");
2244
- if (!footnoteContent) {
2245
- return {
2246
- fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
2247
- overflow: []
2248
- };
2249
- }
2250
- const children = Array.from(footnoteContent.children);
2251
- if (children.length <= 1) {
2252
- return {
2253
- fits: children.map((el) => el.cloneNode(true)),
2254
- overflow: []
2255
- };
2256
- }
2257
- const fits = [];
2258
- const overflow = [];
2259
- let currentHeight = 0;
2260
- const hrMeasure = document.createElement("div");
2261
- hrMeasure.style.position = "absolute";
2262
- hrMeasure.style.visibility = "hidden";
2263
- hrMeasure.style.width = `${contentWidth}pt`;
2264
- hrMeasure.style.left = "-9999px";
2265
- hrMeasure.className = this.cssPrefix + "footnotes";
2266
- const hr = document.createElement("hr");
2267
- hrMeasure.appendChild(hr);
2268
- this.stagingElement.appendChild(hrMeasure);
2269
- const hrHeight = pxToPt(hrMeasure.getBoundingClientRect().height);
2270
- this.stagingElement.removeChild(hrMeasure);
2271
- currentHeight = hrHeight;
2272
- const footnoteNumber = footnoteElement.querySelector(".footnote-number");
2273
- for (let i = 0; i < children.length; i++) {
2274
- const child = children[i];
2275
- const measureContainer = document.createElement("div");
2276
- measureContainer.style.position = "absolute";
2277
- measureContainer.style.visibility = "hidden";
2278
- measureContainer.style.width = `${contentWidth}pt`;
2279
- measureContainer.style.left = "-9999px";
2280
- measureContainer.className = this.cssPrefix + "footnotes";
2281
- measureContainer.appendChild(child.cloneNode(true));
2282
- this.stagingElement.appendChild(measureContainer);
2283
- const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
2284
- this.stagingElement.removeChild(measureContainer);
2285
- if (currentHeight + childHeight <= availableHeightPt) {
2286
- fits.push(child.cloneNode(true));
2287
- currentHeight += childHeight;
2288
- } else {
2289
- for (let j = i; j < children.length; j++) {
2290
- overflow.push(children[j].cloneNode(true));
3937
+ measureContainer.style.left = "-9999px";
3938
+ measureContainer.className = this.cssPrefix + "footnotes";
3939
+ this.appendFootnoteSeparator(measureContainer, true);
3940
+ const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
3941
+ const content = wrapper.querySelector(":scope > .footnote-content");
3942
+ if (!content) {
3943
+ throw new Error("Footnote continuation is missing its content shell");
3944
+ }
3945
+ measureContainer.appendChild(wrapper);
3946
+ this.noteMeasurementHost().appendChild(measureContainer);
3947
+ try {
3948
+ for (let index = 0; index < continuation.remainingElements.length; index++) {
3949
+ this.checkpoint();
3950
+ const element = continuation.remainingElements[index];
3951
+ const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
3952
+ if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
3953
+ const sentinel = this.document.createElement("p");
3954
+ sentinel.className = "footnote-continuation-position-sentinel";
3955
+ sentinel.setAttribute("aria-hidden", "true");
3956
+ sentinel.style.setProperty("display", "none", "important");
3957
+ content.appendChild(sentinel);
3958
+ }
3959
+ const candidate = element.cloneNode(true);
3960
+ candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
3961
+ if (fitting.length === 0 && candidate.tagName === "P") {
3962
+ candidate.style.setProperty("display", "block", "important");
3963
+ }
3964
+ content.appendChild(candidate);
3965
+ const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
3966
+ if (candidateHeight <= availableHeightPt) {
3967
+ fitting.push(element);
3968
+ continue;
3969
+ }
3970
+ const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
3971
+ candidate.remove();
3972
+ let split = null;
3973
+ if (canSplitCandidate) {
3974
+ split = this.splitParagraphAtLargestFit(candidate, (head) => {
3975
+ content.appendChild(head);
3976
+ try {
3977
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
3978
+ } finally {
3979
+ head.remove();
3980
+ }
3981
+ }, {
3982
+ // Both fallbacks below cut where ordinary line breaking would not,
3983
+ // so they are only ever right for an element that owns the whole
3984
+ // band: if this page already carries earlier content, a paragraph
3985
+ // that will not start here belongs intact in the next note band.
3986
+ emergencyGraphemeBreaks: fitting.length === 0,
3987
+ // A continuation page owns the full note band. If even one legal
3988
+ // grapheme is taller than it, clip only that unit and keep draining.
3989
+ forceFirstOnNoFit: fitting.length === 0
3990
+ });
3991
+ }
3992
+ if (split?.kind === "split") {
3993
+ if (sourcePosition) {
3994
+ split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
3995
+ split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
3996
+ }
3997
+ fitting.push(split.head);
3998
+ remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
3999
+ } else if (fitting.length === 0) {
4000
+ fitting.push(element);
4001
+ remaining = continuation.remainingElements.slice(index + 1);
4002
+ } else {
4003
+ remaining = continuation.remainingElements.slice(index);
2291
4004
  }
2292
4005
  break;
2293
4006
  }
4007
+ } finally {
4008
+ measureContainer.remove();
2294
4009
  }
2295
- return { fits, overflow };
4010
+ return {
4011
+ current: {
4012
+ footnoteId: continuation.footnoteId,
4013
+ sourceAnchorId: continuation.sourceAnchorId,
4014
+ remainingElements: fitting
4015
+ },
4016
+ overflow: remaining.length > 0 ? {
4017
+ footnoteId: continuation.footnoteId,
4018
+ sourceAnchorId: continuation.sourceAnchorId,
4019
+ remainingElements: remaining
4020
+ } : null
4021
+ };
2296
4022
  }
2297
4023
  /**
2298
- * Measures a single footnote's height.
4024
+ * Splits a footnote element into parts that fit within the available height.
4025
+ * Returns the elements that fit and the elements that need to continue.
2299
4026
  */
2300
- measureSingleFootnoteHeight(footnoteId, contentWidth) {
2301
- const footnote = this.footnoteRegistry.get(footnoteId);
2302
- if (!footnote) return 0;
2303
- const measureContainer = document.createElement("div");
4027
+ splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
4028
+ const layoutContext = this.document.createElement("div");
4029
+ layoutContext.style.position = "absolute";
4030
+ layoutContext.style.visibility = "hidden";
4031
+ layoutContext.style.width = `${contentWidth}pt`;
4032
+ layoutContext.style.left = "-9999px";
4033
+ layoutContext.className = this.cssPrefix + "footnotes";
4034
+ this.appendFootnoteSeparator(layoutContext, false);
4035
+ const attachedFootnote = footnoteElement.cloneNode(true);
4036
+ layoutContext.appendChild(attachedFootnote);
4037
+ this.noteMeasurementHost().appendChild(layoutContext);
4038
+ const measureContainer = this.document.createElement("div");
2304
4039
  measureContainer.style.position = "absolute";
2305
4040
  measureContainer.style.visibility = "hidden";
2306
4041
  measureContainer.style.width = `${contentWidth}pt`;
2307
4042
  measureContainer.style.left = "-9999px";
2308
4043
  measureContainer.className = this.cssPrefix + "footnotes";
2309
- measureContainer.appendChild(footnote.cloneNode(true));
2310
- this.stagingElement.appendChild(measureContainer);
2311
- const rect = measureContainer.getBoundingClientRect();
2312
- const heightPt = pxToPt(rect.height);
2313
- this.stagingElement.removeChild(measureContainer);
2314
- return heightPt;
4044
+ const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
4045
+ this.appendFootnoteSeparator(measureContainer, hasContinuation);
4046
+ if (hasContinuation) {
4047
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(
4048
+ existingPayload.continuation
4049
+ ));
4050
+ }
4051
+ const partialById = new Map(
4052
+ existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
4053
+ );
4054
+ for (const id of existingPayload?.footnoteIds ?? []) {
4055
+ this.checkpoint();
4056
+ const source = this.footnoteRegistry.get(id);
4057
+ if (!source) continue;
4058
+ const partial = partialById.get(id);
4059
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
4060
+ }
4061
+ const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
4062
+ const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
4063
+ if (!measuredContent) {
4064
+ layoutContext.remove();
4065
+ throw new Error("Footnote is missing its content shell");
4066
+ }
4067
+ measureContainer.appendChild(measuredPartial);
4068
+ this.noteMeasurementHost().appendChild(measureContainer);
4069
+ try {
4070
+ const footnoteContent = attachedFootnote.querySelector(".footnote-content");
4071
+ if (!footnoteContent) {
4072
+ return {
4073
+ fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
4074
+ overflow: []
4075
+ };
4076
+ }
4077
+ const children = Array.from(footnoteContent.children);
4078
+ const fits = [];
4079
+ let overflow = [];
4080
+ for (let i = 0; i < children.length; i++) {
4081
+ this.checkpoint();
4082
+ const child = children[i];
4083
+ const measuredCandidate = child.cloneNode(true);
4084
+ measuredContent.appendChild(measuredCandidate);
4085
+ const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
4086
+ if (candidateFits) {
4087
+ fits.push(child.cloneNode(true));
4088
+ continue;
4089
+ }
4090
+ measuredCandidate.remove();
4091
+ const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
4092
+ measuredContent.appendChild(head);
4093
+ try {
4094
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
4095
+ } finally {
4096
+ head.remove();
4097
+ }
4098
+ }, {
4099
+ // Both fallbacks cut where ordinary line breaking would not, so they
4100
+ // are only ever right for an element that owns the whole band: with
4101
+ // earlier siblings already packed here, a paragraph that will not
4102
+ // start belongs intact in the next note band.
4103
+ emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
4104
+ forceFirstOnNoFit: fits.length === 0 && forceProgress
4105
+ }) : null;
4106
+ if (split?.kind === "split") {
4107
+ fits.push(split.head);
4108
+ split.tail.setAttribute(
4109
+ FOOTNOTE_SOURCE_POSITION_ATTR,
4110
+ i === 0 ? "first" : "later"
4111
+ );
4112
+ overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
4113
+ } else if (split?.kind === "no-fit") {
4114
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
4115
+ } else if (fits.length === 0 && forceProgress) {
4116
+ fits.push(child.cloneNode(true));
4117
+ overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
4118
+ } else {
4119
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
4120
+ }
4121
+ break;
4122
+ }
4123
+ return { fits, overflow };
4124
+ } finally {
4125
+ measureContainer.remove();
4126
+ layoutContext.remove();
4127
+ }
2315
4128
  }
2316
4129
  /**
2317
4130
  * Adds footnotes to a page container, including continuation content.
@@ -2324,12 +4137,11 @@ var PaginationEngine = class {
2324
4137
  if (this.footnoteRegistry.size === 0 && !hasContinuation) {
2325
4138
  return;
2326
4139
  }
2327
- const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
2328
4140
  const maxFootnoteHeight = Math.min(
2329
4141
  footnoteHeight,
2330
4142
  bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
2331
4143
  );
2332
- const footnotesDiv = document.createElement("div");
4144
+ const footnotesDiv = this.document.createElement("div");
2333
4145
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
2334
4146
  footnotesDiv.style.position = "absolute";
2335
4147
  footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
@@ -2338,35 +4150,18 @@ var PaginationEngine = class {
2338
4150
  footnotesDiv.style.boxSizing = "border-box";
2339
4151
  footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
2340
4152
  footnotesDiv.style.overflow = "hidden";
2341
- const hr = document.createElement("hr");
2342
- footnotesDiv.appendChild(hr);
4153
+ this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
2343
4154
  if (hasContinuation) {
2344
- const contWrapper = document.createElement("div");
2345
- contWrapper.className = "footnote-continuation";
2346
- for (const el of continuation.remainingElements) {
2347
- contWrapper.appendChild(el.cloneNode(true));
2348
- }
2349
- footnotesDiv.appendChild(contWrapper);
4155
+ footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
2350
4156
  }
2351
4157
  for (const id of footnoteIds) {
2352
4158
  const partial = partialFootnotes?.find((p) => p.footnoteId === id);
2353
4159
  if (partial) {
2354
4160
  const footnote = this.footnoteRegistry.get(id);
2355
4161
  if (footnote) {
2356
- const partialDiv = document.createElement("div");
2357
- partialDiv.className = "footnote-item";
2358
- partialDiv.dataset.footnoteId = id;
2359
- const numberSpan = footnote.querySelector(".footnote-number");
2360
- if (numberSpan) {
2361
- partialDiv.appendChild(numberSpan.cloneNode(true));
2362
- }
2363
- const contentSpan = document.createElement("span");
2364
- contentSpan.className = "footnote-content";
2365
- for (const el of partial.fittingElements) {
2366
- contentSpan.appendChild(el.cloneNode(true));
2367
- }
2368
- partialDiv.appendChild(contentSpan);
2369
- footnotesDiv.appendChild(partialDiv);
4162
+ footnotesDiv.appendChild(
4163
+ this.createPartialFootnoteItem(footnote, partial.fittingElements)
4164
+ );
2370
4165
  }
2371
4166
  } else {
2372
4167
  const footnote = this.footnoteRegistry.get(id);
@@ -2377,30 +4172,26 @@ var PaginationEngine = class {
2377
4172
  }
2378
4173
  pageBox.appendChild(footnotesDiv);
2379
4174
  }
2380
- /**
2381
- * Selects the appropriate header for a page based on section, page position, and page number.
2382
- */
2383
- selectHeader(sectionIndex, pageInSection, globalPageNumber) {
4175
+ /** Select the section's first/odd/even header from its one-based page position. */
4176
+ selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
2384
4177
  const sectionHf = this.hfRegistry.get(sectionIndex);
2385
4178
  if (!sectionHf) return void 0;
2386
4179
  if (pageInSection === 1 && sectionHf.headerFirst) {
2387
4180
  return sectionHf.headerFirst;
2388
4181
  }
2389
- if (globalPageNumber % 2 === 0 && sectionHf.headerEven) {
4182
+ if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
2390
4183
  return sectionHf.headerEven;
2391
4184
  }
2392
4185
  return sectionHf.headerDefault;
2393
4186
  }
2394
- /**
2395
- * Selects the appropriate footer for a page based on section, page position, and page number.
2396
- */
2397
- selectFooter(sectionIndex, pageInSection, globalPageNumber) {
4187
+ /** Select the section's first/odd/even footer from its one-based page position. */
4188
+ selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
2398
4189
  const sectionHf = this.hfRegistry.get(sectionIndex);
2399
4190
  if (!sectionHf) return void 0;
2400
4191
  if (pageInSection === 1 && sectionHf.footerFirst) {
2401
4192
  return sectionHf.footerFirst;
2402
4193
  }
2403
- if (globalPageNumber % 2 === 0 && sectionHf.footerEven) {
4194
+ if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
2404
4195
  return sectionHf.footerEven;
2405
4196
  }
2406
4197
  return sectionHf.footerDefault;
@@ -2415,7 +4206,7 @@ var PaginationEngine = class {
2415
4206
  * Deterministic: it depends only on the section's page setup and the registry's pre-measured
2416
4207
  * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
2417
4208
  */
2418
- getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
4209
+ getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
2419
4210
  const sectionHf = this.hfRegistry.get(sectionIndex);
2420
4211
  return resolvePageBands(
2421
4212
  dims,
@@ -2424,14 +4215,14 @@ var PaginationEngine = class {
2424
4215
  sectionHf?.headerEvenHeight,
2425
4216
  sectionHf?.headerDefaultHeight,
2426
4217
  pageInSection,
2427
- globalPageNumber
4218
+ displayedPageNumber
2428
4219
  ),
2429
4220
  this.selectStoryHeight(
2430
4221
  sectionHf?.footerFirstHeight,
2431
4222
  sectionHf?.footerEvenHeight,
2432
4223
  sectionHf?.footerDefaultHeight,
2433
4224
  pageInSection,
2434
- globalPageNumber
4225
+ displayedPageNumber
2435
4226
  )
2436
4227
  );
2437
4228
  }
@@ -2439,9 +4230,9 @@ var PaginationEngine = class {
2439
4230
  * The measured height of the running story this page position selects, mirroring
2440
4231
  * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
2441
4232
  */
2442
- selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
4233
+ selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
2443
4234
  if (pageInSection === 1 && first != null) return first;
2444
- if (globalPageNumber % 2 === 0 && even != null) return even;
4235
+ if (displayedPageNumber % 2 === 0 && even != null) return even;
2445
4236
  return fallback ?? 0;
2446
4237
  }
2447
4238
  /**
@@ -2453,7 +4244,7 @@ var PaginationEngine = class {
2453
4244
  * silently starts overlapping body text.
2454
4245
  */
2455
4246
  measureHeaderFooterHeight(source, contentWidth) {
2456
- const measureContainer = document.createElement("div");
4247
+ const measureContainer = this.document.createElement("div");
2457
4248
  measureContainer.style.position = "absolute";
2458
4249
  measureContainer.style.visibility = "hidden";
2459
4250
  measureContainer.style.width = `${contentWidth}pt`;
@@ -2472,79 +4263,275 @@ var PaginationEngine = class {
2472
4263
  * Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
2473
4264
  * Supports footnote continuation - long footnotes can split across pages.
2474
4265
  */
2475
- flowToPages(blocks, dims, startPageNumber, sectionIndex) {
4266
+ flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
2476
4267
  const pages = [];
2477
4268
  let currentContent = [];
2478
4269
  let pageNumber = startPageNumber;
2479
- let pageInSection = 1;
4270
+ let pageSectionIndex = sectionIndex;
4271
+ const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
4272
+ const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
4273
+ const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
4274
+ const markSectionPlaced = (owner) => {
4275
+ if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
4276
+ };
4277
+ const previousBox = this.containerElement.querySelector(
4278
+ `.${this.cssPrefix}box:last-of-type`
4279
+ );
4280
+ let precedingDisplayedPageNumber = parseInt(
4281
+ previousBox?.dataset.displayedPageNumber ?? "0",
4282
+ 10
4283
+ );
4284
+ const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
4285
+ const numbering = this.pageNumbering.get(owner) ?? {};
4286
+ if (numbering.start !== void 0) {
4287
+ return numbering.start + ownerPageInSection - 1;
4288
+ }
4289
+ const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
4290
+ const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
4291
+ return currentPhysicalNumber + physicalPage - pageNumber;
4292
+ };
2480
4293
  let { bodyHeight: effectiveContentHeight } = this.getPageBands(
2481
- dims,
2482
- sectionIndex,
2483
- pageInSection,
2484
- pageNumber
4294
+ dimensionsFor(),
4295
+ pageSectionIndex,
4296
+ pageInSection(),
4297
+ displayedPageNumber()
2485
4298
  );
2486
4299
  let remainingHeight = effectiveContentHeight;
2487
4300
  let prevMarginBottomPt = 0;
2488
4301
  let currentFootnoteIds = [];
4302
+ let currentPageHasFootnoteReference = false;
2489
4303
  let currentFootnoteHeight = 0;
2490
4304
  let currentContinuation = this.pendingFootnoteContinuation;
2491
4305
  let nextPageContinuation = null;
4306
+ let currentContinuationPartitioned = false;
4307
+ let currentPageAdmitted = false;
2492
4308
  let deferredFootnoteIds = [];
2493
4309
  let currentPartialFootnotes = [];
2494
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
2495
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
2496
- }
2497
- const finishPage = () => {
4310
+ const adoptEmptyPageOwner = (owner) => {
4311
+ pageSectionIndex = owner;
4312
+ const bands = this.getPageBands(
4313
+ dimensionsFor(owner),
4314
+ owner,
4315
+ pageInSection(owner),
4316
+ displayedPageNumber(owner)
4317
+ );
4318
+ effectiveContentHeight = bands.bodyHeight;
4319
+ remainingHeight = effectiveContentHeight;
4320
+ };
4321
+ const admitCurrentPage = () => {
4322
+ if (currentPageAdmitted) return;
4323
+ this.admitPageAllocation();
4324
+ currentPageAdmitted = true;
4325
+ };
4326
+ const prepareCurrentContinuation = () => {
4327
+ if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
4328
+ admitCurrentPage();
4329
+ const ownedDimensions = dimensionsFor();
4330
+ const pageBands = this.getPageBands(
4331
+ ownedDimensions,
4332
+ pageSectionIndex,
4333
+ pageInSection(),
4334
+ displayedPageNumber()
4335
+ );
4336
+ const partition = this.splitContinuationForPage(
4337
+ currentContinuation,
4338
+ pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
4339
+ ownedDimensions.contentWidth
4340
+ );
4341
+ currentContinuation = partition.current;
4342
+ if (partition.overflow) nextPageContinuation = partition.overflow;
4343
+ currentContinuationPartitioned = true;
4344
+ currentFootnoteHeight = this.measureFootnotesHeight(
4345
+ currentFootnoteIds,
4346
+ ownedDimensions.contentWidth,
4347
+ currentContinuation,
4348
+ currentPartialFootnotes
4349
+ );
4350
+ };
4351
+ const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
2498
4352
  const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
2499
- if (currentContent.length === 0 && !hasCurrentContinuation) return;
4353
+ if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
4354
+ adoptEmptyPageOwner(nextPageSectionIndex);
4355
+ return;
4356
+ }
4357
+ prepareCurrentContinuation();
4358
+ admitCurrentPage();
4359
+ let pageContinuation = currentContinuation;
4360
+ const ownedPageInSection = pageInSection();
4361
+ const ownedDimensions = dimensionsFor();
4362
+ const ownedDisplayedPageNumber = displayedPageNumber(
4363
+ pageSectionIndex,
4364
+ ownedPageInSection
4365
+ );
4366
+ const pageBands = this.getPageBands(
4367
+ ownedDimensions,
4368
+ pageSectionIndex,
4369
+ ownedPageInSection,
4370
+ ownedDisplayedPageNumber
4371
+ );
4372
+ const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4373
+ pageContinuation = currentContinuation;
4374
+ currentFootnoteHeight = this.measureFootnotesHeight(
4375
+ currentFootnoteIds,
4376
+ ownedDimensions.contentWidth,
4377
+ pageContinuation,
4378
+ currentPartialFootnotes
4379
+ );
4380
+ if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
4381
+ const fittingIds = [];
4382
+ for (let index = 0; index < currentFootnoteIds.length; index++) {
4383
+ const footnoteId = currentFootnoteIds[index];
4384
+ const candidateIds = [...fittingIds, footnoteId];
4385
+ const candidateHeight = this.measureFootnotesHeight(
4386
+ candidateIds,
4387
+ ownedDimensions.contentWidth,
4388
+ pageContinuation
4389
+ );
4390
+ const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
4391
+ if (guardedCandidateHeight <= maxFootnoteHeight) {
4392
+ fittingIds.push(footnoteId);
4393
+ currentFootnoteHeight = guardedCandidateHeight;
4394
+ continue;
4395
+ }
4396
+ const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
4397
+ if (fittingIds.length === 0 && !hasPageContinuation) {
4398
+ const source = this.footnoteRegistry.get(footnoteId);
4399
+ const split = source ? this.splitFootnoteToFit(
4400
+ source,
4401
+ maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
4402
+ ownedDimensions.contentWidth,
4403
+ true
4404
+ ) : null;
4405
+ fittingIds.push(footnoteId);
4406
+ if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
4407
+ currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
4408
+ nextPageContinuation = {
4409
+ footnoteId,
4410
+ sourceAnchorId: source.dataset.sourceAnchorId,
4411
+ remainingElements: split.overflow
4412
+ };
4413
+ currentFootnoteHeight = maxFootnoteHeight;
4414
+ } else {
4415
+ currentFootnoteHeight = guardedCandidateHeight;
4416
+ }
4417
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
4418
+ } else {
4419
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
4420
+ }
4421
+ break;
4422
+ }
4423
+ currentFootnoteIds = fittingIds;
4424
+ }
2500
4425
  const page = this.createPage(
2501
- dims,
4426
+ ownedDimensions,
2502
4427
  pageNumber,
2503
- sectionIndex,
4428
+ pageSectionIndex,
4429
+ ownedDisplayedPageNumber,
2504
4430
  currentContent,
2505
- pageInSection,
4431
+ ownedPageInSection,
2506
4432
  currentFootnoteIds,
2507
4433
  currentFootnoteHeight,
2508
- currentContinuation,
2509
- currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
4434
+ pageContinuation,
4435
+ currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
4436
+ false,
4437
+ currentPageAdmitted
2510
4438
  );
2511
4439
  pages.push(page);
4440
+ precedingDisplayedPageNumber = ownedDisplayedPageNumber;
2512
4441
  pageNumber++;
2513
- pageInSection++;
2514
4442
  currentContent = [];
2515
- const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
4443
+ pageSectionIndex = nextPageSectionIndex;
4444
+ if (!sectionStartPages.has(pageSectionIndex)) {
4445
+ sectionStartPages.set(pageSectionIndex, pageNumber);
4446
+ }
4447
+ const newBands = this.getPageBands(
4448
+ dimensionsFor(),
4449
+ pageSectionIndex,
4450
+ pageInSection(),
4451
+ displayedPageNumber()
4452
+ );
2516
4453
  effectiveContentHeight = newBands.bodyHeight;
2517
4454
  remainingHeight = effectiveContentHeight;
2518
4455
  prevMarginBottomPt = 0;
2519
4456
  currentFootnoteIds = [];
4457
+ currentPageHasFootnoteReference = false;
2520
4458
  currentPartialFootnotes = [];
2521
4459
  currentContinuation = nextPageContinuation;
2522
4460
  nextPageContinuation = null;
4461
+ currentContinuationPartitioned = false;
4462
+ currentPageAdmitted = false;
2523
4463
  if (deferredFootnoteIds.length > 0) {
2524
4464
  currentFootnoteIds = [...deferredFootnoteIds];
2525
4465
  deferredFootnoteIds = [];
2526
4466
  }
2527
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
2528
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
2529
- } else {
2530
- currentFootnoteHeight = 0;
2531
- }
2532
- if (currentFootnoteIds.length > 0) {
2533
- currentFootnoteHeight += this.measureFootnotesHeight(
2534
- currentFootnoteIds,
2535
- dims.contentWidth,
2536
- null
2537
- );
2538
- }
4467
+ const nextDimensions = dimensionsFor();
4468
+ currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
2539
4469
  };
4470
+ const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
2540
4471
  for (let i = 0; i < blocks.length; i++) {
4472
+ this.checkpoint();
2541
4473
  const block = blocks[i];
4474
+ const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
4475
+ if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
4476
+ finishPage(block.sectionIndex);
4477
+ }
4478
+ const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
4479
+ if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
4480
+ adoptEmptyPageOwner(block.sectionIndex);
4481
+ }
4482
+ prepareCurrentContinuation();
4483
+ if (currentFootnoteHeight > 0) {
4484
+ const ownerDimensions = dimensionsFor();
4485
+ const ownerBands = this.getPageBands(
4486
+ ownerDimensions,
4487
+ pageSectionIndex,
4488
+ pageInSection(),
4489
+ displayedPageNumber()
4490
+ );
4491
+ const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4492
+ if (currentFootnoteHeight > ownerMaxFootnoteArea) {
4493
+ const fittingIds = [];
4494
+ let fittingHeight = 0;
4495
+ if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
4496
+ for (const footnoteId of currentFootnoteIds) {
4497
+ this.checkpoint();
4498
+ const candidateHeight = this.measureFootnotesHeight(
4499
+ [...fittingIds, footnoteId],
4500
+ ownerDimensions.contentWidth
4501
+ );
4502
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
4503
+ fittingIds.push(footnoteId);
4504
+ fittingHeight = candidateHeight;
4505
+ }
4506
+ }
4507
+ if (fittingIds.length > 0) {
4508
+ deferredFootnoteIds = [
4509
+ ...currentFootnoteIds.slice(fittingIds.length),
4510
+ ...deferredFootnoteIds
4511
+ ];
4512
+ currentFootnoteIds = fittingIds;
4513
+ currentFootnoteHeight = fittingHeight;
4514
+ } else {
4515
+ finishPage(block.sectionIndex);
4516
+ i--;
4517
+ continue;
4518
+ }
4519
+ }
4520
+ }
4521
+ const blockDimensions = dimensionsFor(block.sectionIndex);
4522
+ const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
4523
+ const freshBlockPageBodyHeight = this.getPageBands(
4524
+ blockDimensions,
4525
+ block.sectionIndex,
4526
+ nextBlockPageInSection,
4527
+ displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
4528
+ ).bodyHeight;
2542
4529
  if (block.isPageBreak) {
2543
- finishPage();
4530
+ finishPage(block.sectionIndex);
2544
4531
  continue;
2545
4532
  }
2546
4533
  if (block.pageBreakBefore && currentContent.length > 0) {
2547
- finishPage();
4534
+ finishPage(block.sectionIndex);
2548
4535
  }
2549
4536
  const previousBlock = blocks[i - 1];
2550
4537
  const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
@@ -2560,7 +4547,7 @@ var PaginationEngine = class {
2560
4547
  if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
2561
4548
  const totalChainFootnoteHeight = this.measureFootnotesHeight(
2562
4549
  [...currentFootnoteIds, ...newChainFootnoteIds],
2563
- dims.contentWidth,
4550
+ blockDimensions.contentWidth,
2564
4551
  currentContinuation
2565
4552
  );
2566
4553
  additionalChainFootnoteHeight = Math.max(
@@ -2572,61 +4559,68 @@ var PaginationEngine = class {
2572
4559
  keepChain,
2573
4560
  prevMarginBottomPt,
2574
4561
  currentContent.length === 0,
2575
- pageInSection
4562
+ pageInSection(block.sectionIndex)
2576
4563
  ) + additionalChainFootnoteHeight;
2577
4564
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
2578
4565
  if (currentChainHeight > currentAvailableHeight) {
2579
4566
  const nextPageBands = this.getPageBands(
2580
- dims,
2581
- sectionIndex,
2582
- pageInSection + 1,
2583
- pageNumber + 1
4567
+ dimensionsFor(block.sectionIndex),
4568
+ block.sectionIndex,
4569
+ pageInSection(block.sectionIndex, pageNumber + 1),
4570
+ displayedPageNumber(
4571
+ block.sectionIndex,
4572
+ pageInSection(block.sectionIndex, pageNumber + 1),
4573
+ pageNumber + 1
4574
+ )
2584
4575
  );
2585
4576
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
2586
4577
  keepChain,
2587
4578
  0,
2588
4579
  true,
2589
- pageInSection + 1
4580
+ pageInSection(block.sectionIndex, pageNumber + 1)
2590
4581
  );
2591
4582
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
2592
4583
  newChainFootnoteIds,
2593
- dims.contentWidth,
4584
+ blockDimensions.contentWidth,
2594
4585
  nextPageContinuation
2595
4586
  );
2596
4587
  if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
2597
- finishPage();
4588
+ finishPage(block.sectionIndex);
2598
4589
  }
2599
4590
  }
2600
4591
  }
2601
4592
  }
2602
- const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
2603
4593
  const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
4594
+ let combinedFootnoteHeight = currentFootnoteHeight;
2604
4595
  let additionalFootnoteHeight = 0;
2605
4596
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
2606
4597
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
2607
- const totalFootnoteHeight = this.measureFootnotesHeight(
4598
+ combinedFootnoteHeight = this.measureFootnotesHeight(
2608
4599
  combinedFootnoteIds,
2609
- dims.contentWidth,
2610
- currentContinuation
4600
+ blockDimensions.contentWidth,
4601
+ currentContinuation,
4602
+ currentPartialFootnotes
4603
+ );
4604
+ additionalFootnoteHeight = Math.max(
4605
+ 0,
4606
+ combinedFootnoteHeight - currentFootnoteHeight
2611
4607
  );
2612
- additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
2613
4608
  }
2614
4609
  const isFirstOnPage = currentContent.length === 0;
2615
4610
  const effectiveMarginTop = this.effectiveBlockMarginTop(
2616
4611
  block,
2617
4612
  prevMarginBottomPt,
2618
4613
  isFirstOnPage,
2619
- pageInSection
4614
+ pageInSection(block.sectionIndex)
2620
4615
  );
2621
4616
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
2622
4617
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
2623
4618
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
2624
4619
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
2625
- const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
2626
4620
  if (blockSpace > effectiveRemainingHeight) {
2627
4621
  const paragraphFragments = this.tryFragmentParagraph(
2628
4622
  block,
2629
- dims,
4623
+ blockDimensions,
2630
4624
  effectiveRemainingHeight,
2631
4625
  effectiveMarginTop
2632
4626
  );
@@ -2636,40 +4630,74 @@ var PaginationEngine = class {
2636
4630
  continue;
2637
4631
  }
2638
4632
  }
2639
- if (blockSpace <= effectiveRemainingHeight) {
2640
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
4633
+ if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
4634
+ markSectionPlaced(block.sectionIndex);
4635
+ currentContent.push(this.cloneBlockForPage(
4636
+ block,
4637
+ isFirstOnPage,
4638
+ pageInSection(block.sectionIndex)
4639
+ ));
2641
4640
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
2642
4641
  prevMarginBottomPt = block.marginBottomPt;
2643
4642
  if (newFootnoteIds.length > 0) {
2644
4643
  currentFootnoteIds.push(...newFootnoteIds);
2645
- currentFootnoteHeight += additionalFootnoteHeight;
4644
+ currentFootnoteHeight = combinedFootnoteHeight;
2646
4645
  }
2647
- } else if (block.heightPt + this.effectiveBlockMarginTop(block, 0, true, pageInSection + 1) <= effectiveContentHeight) {
4646
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
4647
+ } else if (block.heightPt + this.effectiveBlockMarginTop(
4648
+ block,
4649
+ 0,
4650
+ true,
4651
+ pageInSection(block.sectionIndex, pageNumber + 1)
4652
+ ) <= freshBlockPageBodyHeight) {
2648
4653
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
2649
- const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
2650
- const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
2651
4654
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
2652
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
4655
+ markSectionPlaced(block.sectionIndex);
4656
+ currentContent.push(this.cloneBlockForPage(
4657
+ block,
4658
+ isFirstOnPage,
4659
+ pageInSection(block.sectionIndex)
4660
+ ));
2653
4661
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
2654
4662
  prevMarginBottomPt = block.marginBottomPt;
2655
4663
  const availableForFootnotes = Math.min(
2656
4664
  maxFootnoteArea,
2657
4665
  effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
2658
4666
  );
2659
- for (const footnoteId of newFootnoteIds) {
4667
+ const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4668
+ const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
4669
+ for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
4670
+ this.checkpoint();
4671
+ const footnoteId = newFootnoteIds[noteIndex];
2660
4672
  const footnote = this.footnoteRegistry.get(footnoteId);
2661
4673
  if (!footnote) continue;
2662
- const footnoteHeight = this.measureSingleFootnoteHeight(footnoteId, dims.contentWidth);
4674
+ if (nextPageContinuation) {
4675
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4676
+ break;
4677
+ }
4678
+ const candidateIds = [...currentFootnoteIds, footnoteId];
4679
+ const candidateHeight = this.measureFootnotesHeight(
4680
+ candidateIds,
4681
+ blockDimensions.contentWidth,
4682
+ currentContinuation,
4683
+ currentPartialFootnotes
4684
+ );
2663
4685
  const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
2664
- if (footnoteHeight <= spaceLeftForFootnotes) {
4686
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
2665
4687
  currentFootnoteIds.push(footnoteId);
2666
- currentFootnoteHeight += footnoteHeight;
4688
+ currentFootnoteHeight = candidateHeight;
2667
4689
  } else {
2668
4690
  if (spaceLeftForFootnotes > 20) {
2669
4691
  const { fits, overflow } = this.splitFootnoteToFit(
2670
4692
  footnote,
2671
- spaceLeftForFootnotes,
2672
- dims.contentWidth
4693
+ availableForFootnotes,
4694
+ blockDimensions.contentWidth,
4695
+ deferralCannotHelp,
4696
+ {
4697
+ footnoteIds: currentFootnoteIds,
4698
+ continuation: currentContinuation,
4699
+ partialFootnotes: currentPartialFootnotes
4700
+ }
2673
4701
  );
2674
4702
  if (fits.length > 0) {
2675
4703
  currentFootnoteIds.push(footnoteId);
@@ -2680,78 +4708,70 @@ var PaginationEngine = class {
2680
4708
  if (overflow.length > 0) {
2681
4709
  nextPageContinuation = {
2682
4710
  footnoteId,
4711
+ sourceAnchorId: footnote.dataset.sourceAnchorId,
2683
4712
  remainingElements: overflow
2684
4713
  };
2685
4714
  }
2686
- currentFootnoteHeight = availableForFootnotes;
4715
+ currentFootnoteHeight = this.measureFootnotesHeight(
4716
+ currentFootnoteIds,
4717
+ blockDimensions.contentWidth,
4718
+ currentContinuation,
4719
+ currentPartialFootnotes
4720
+ );
4721
+ if (overflow.length > 0) {
4722
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
4723
+ break;
4724
+ }
2687
4725
  } else {
2688
- deferredFootnoteIds.push(footnoteId);
4726
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4727
+ break;
2689
4728
  }
2690
4729
  } else {
2691
- deferredFootnoteIds.push(footnoteId);
4730
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4731
+ break;
2692
4732
  }
2693
4733
  }
2694
4734
  }
2695
- } else if (canExpandFootnotes && newFootnoteIds.length > 0) {
2696
- const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
2697
- const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
2698
- if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
2699
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
2700
- remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
2701
- prevMarginBottomPt = block.marginBottomPt;
2702
- currentFootnoteIds.push(...newFootnoteIds);
2703
- currentFootnoteHeight = expandedFootnoteSpace;
2704
- } else {
2705
- finishPage();
2706
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
2707
- const newPageMarginTop = this.effectiveBlockMarginTop(
2708
- block,
2709
- 0,
2710
- true,
2711
- pageInSection
2712
- );
2713
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
2714
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2715
- remainingHeight = effectiveContentHeight - newPageSpace;
2716
- prevMarginBottomPt = block.marginBottomPt;
2717
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2718
- currentFootnoteHeight = newPageFootnoteHeight;
2719
- }
4735
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2720
4736
  } else {
2721
- finishPage();
2722
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
2723
- const newPageMarginTop = this.effectiveBlockMarginTop(
2724
- block,
2725
- 0,
2726
- true,
2727
- pageInSection
2728
- );
2729
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
2730
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2731
- remainingHeight = effectiveContentHeight - newPageSpace;
2732
- prevMarginBottomPt = block.marginBottomPt;
2733
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2734
- currentFootnoteHeight = newPageFootnoteHeight;
4737
+ finishPage(block.sectionIndex, !pageHasPayload());
4738
+ i--;
4739
+ continue;
2735
4740
  }
2736
4741
  } else {
2737
- const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
4742
+ const tableFragments = this.trySplitSimpleOversizedTable(
4743
+ block,
4744
+ dimensionsFor(block.sectionIndex),
4745
+ block.sectionIndex
4746
+ );
2738
4747
  if (tableFragments) {
2739
4748
  if (currentContent.length > 0 || currentContinuation) {
2740
- finishPage();
4749
+ finishPage(block.sectionIndex);
2741
4750
  }
2742
4751
  blocks.splice(i, 1, ...tableFragments);
2743
4752
  i--;
2744
4753
  continue;
2745
4754
  }
2746
- if (currentContent.length > 0) {
2747
- finishPage();
4755
+ if (pageHasPayload()) {
4756
+ finishPage(block.sectionIndex);
4757
+ i--;
4758
+ continue;
2748
4759
  }
2749
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2750
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2751
- finishPage();
4760
+ markSectionPlaced(block.sectionIndex);
4761
+ currentContent.push(this.cloneBlockForPage(
4762
+ block,
4763
+ true,
4764
+ pageInSection(block.sectionIndex)
4765
+ ));
4766
+ deferredFootnoteIds.push(...newFootnoteIds);
4767
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
4768
+ finishPage(block.sectionIndex);
2752
4769
  }
2753
4770
  }
2754
4771
  finishPage();
4772
+ while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
4773
+ finishPage();
4774
+ }
2755
4775
  this.pendingFootnoteContinuation = nextPageContinuation;
2756
4776
  return pages;
2757
4777
  }
@@ -2776,6 +4796,19 @@ var PaginationEngine = class {
2776
4796
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
2777
4797
  }
2778
4798
  }
4799
+ /** A repeated margin note is presentation, not a second bookmark/link target. */
4800
+ makeClonedMarginCommentInert(root) {
4801
+ this.makeClonedStoryInert(root);
4802
+ const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
4803
+ for (const element of nodes) {
4804
+ element.removeAttribute("id");
4805
+ if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
4806
+ element.removeAttribute("href");
4807
+ element.setAttribute("aria-disabled", "true");
4808
+ element.tabIndex = -1;
4809
+ }
4810
+ }
4811
+ }
2779
4812
  /**
2780
4813
  * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
2781
4814
  *
@@ -2801,7 +4834,7 @@ var PaginationEngine = class {
2801
4834
  const context = {
2802
4835
  staticLeft: toPageX(staticRect.left),
2803
4836
  staticTop: toPageY(staticRect.top),
2804
- lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
4837
+ lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2805
4838
  paragraphLeft: toPageX(paragraphRect.left),
2806
4839
  paragraphTop: toPageY(paragraphRect.top),
2807
4840
  paragraphWidth: paragraphRect.width / pixelsPerPoint,
@@ -2931,36 +4964,48 @@ var PaginationEngine = class {
2931
4964
  else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
2932
4965
  return reference.start + (reference.size - objectSize) * fraction;
2933
4966
  }
4967
+ /** Charge a physical page before any page-owned partitioning or DOM allocation. */
4968
+ admitPageAllocation() {
4969
+ const prospectivePageCount = this.createdPageCount + 1;
4970
+ this.pageCountCheckpoint?.(prospectivePageCount);
4971
+ this.createdPageCount = prospectivePageCount;
4972
+ }
2934
4973
  /**
2935
4974
  * Creates a page container element.
2936
4975
  */
2937
- createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
2938
- const pageBox = document.createElement("div");
4976
+ createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
4977
+ if (!pageAlreadyAdmitted) this.admitPageAllocation();
4978
+ const pageBox = this.document.createElement("div");
2939
4979
  pageBox.className = `${this.cssPrefix}box`;
2940
4980
  pageBox.style.width = `${dims.pageWidth}pt`;
2941
4981
  pageBox.style.height = `${dims.pageHeight}pt`;
2942
4982
  pageBox.style.overflow = "hidden";
2943
4983
  pageBox.style.position = "relative";
2944
4984
  if (this.scale !== 1) {
2945
- pageBox.style.zoom = String(this.scale);
2946
- pageBox.style.transform = `scale(${this.scale})`;
2947
- pageBox.style.transformOrigin = "top left";
2948
- const heightReductionPt = dims.pageHeight * (1 - this.scale);
2949
- const widthReductionPt = dims.pageWidth * (1 - this.scale);
2950
- const heightReductionPx = ptToPx(heightReductionPt);
2951
- const widthReductionPx = ptToPx(widthReductionPt);
2952
- pageBox.style.marginRight = `-${widthReductionPx}px`;
2953
- pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
4985
+ if (this.view.CSS?.supports("zoom", "1")) {
4986
+ pageBox.style.zoom = String(this.scale);
4987
+ } else {
4988
+ pageBox.style.transform = `scale(${this.scale})`;
4989
+ pageBox.style.transformOrigin = "top left";
4990
+ const heightReductionPt = dims.pageHeight * (1 - this.scale);
4991
+ const widthReductionPt = dims.pageWidth * (1 - this.scale);
4992
+ const heightReductionPx = ptToPx(heightReductionPt);
4993
+ const widthReductionPx = ptToPx(widthReductionPt);
4994
+ pageBox.style.marginRight = `-${widthReductionPx}px`;
4995
+ pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
4996
+ }
2954
4997
  }
2955
4998
  pageBox.style.willChange = "transform";
2956
4999
  pageBox.style.contain = "layout paint";
2957
5000
  pageBox.dataset.pageNumber = String(pageNumber);
2958
5001
  pageBox.dataset.sectionIndex = String(sectionIndex);
5002
+ pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
5003
+ if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
2959
5004
  pageBox.dataset.pageInSection = String(pageInSection);
2960
- const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2961
- const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
5005
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
5006
+ const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
2962
5007
  if (headerSource) {
2963
- const headerDiv = document.createElement("div");
5008
+ const headerDiv = this.document.createElement("div");
2964
5009
  headerDiv.className = `${this.cssPrefix}header`;
2965
5010
  headerDiv.style.position = "absolute";
2966
5011
  headerDiv.style.top = `${bands.headerTop}pt`;
@@ -2982,7 +5027,7 @@ var PaginationEngine = class {
2982
5027
  }
2983
5028
  const contentAreaTop = bands.bodyTop;
2984
5029
  const contentAreaHeight = bands.bodyHeight;
2985
- const contentArea = document.createElement("div");
5030
+ const contentArea = this.document.createElement("div");
2986
5031
  contentArea.className = `${this.cssPrefix}content`;
2987
5032
  contentArea.style.position = "absolute";
2988
5033
  contentArea.style.top = `${contentAreaTop}pt`;
@@ -2995,12 +5040,46 @@ var PaginationEngine = class {
2995
5040
  }
2996
5041
  pageBox.appendChild(contentArea);
2997
5042
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2998
- if (footnoteIds.length > 0 || hasContinuation) {
5043
+ if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
2999
5044
  this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
3000
5045
  }
3001
- const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
5046
+ const pageCommentIds = [];
5047
+ for (const marker of Array.from(
5048
+ pageBox.querySelectorAll("[data-comment-id]")
5049
+ )) {
5050
+ if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
5051
+ const id = marker.dataset.commentId;
5052
+ if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
5053
+ pageCommentIds.push(id);
5054
+ }
5055
+ }
5056
+ if (pageCommentIds.length > 0) {
5057
+ const marginColumn = this.document.createElement("aside");
5058
+ marginColumn.className = `${this.cssPrefix}comment-margin`;
5059
+ marginColumn.style.position = "absolute";
5060
+ marginColumn.style.top = `${contentAreaTop}pt`;
5061
+ marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
5062
+ marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
5063
+ marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
5064
+ marginColumn.style.overflow = "hidden";
5065
+ marginColumn.style.boxSizing = "border-box";
5066
+ const placedThreadRoots = /* @__PURE__ */ new Set();
5067
+ for (const id of pageCommentIds) {
5068
+ const source = this.commentMarginRegistry.get(id);
5069
+ if (source) {
5070
+ const rootId = source.dataset.commentId ?? id;
5071
+ if (placedThreadRoots.has(rootId)) continue;
5072
+ placedThreadRoots.add(rootId);
5073
+ const clone = source.cloneNode(true);
5074
+ this.makeClonedMarginCommentInert(clone);
5075
+ marginColumn.appendChild(clone);
5076
+ }
5077
+ }
5078
+ pageBox.appendChild(marginColumn);
5079
+ }
5080
+ const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
3002
5081
  if (footerSource) {
3003
- const footerDiv = document.createElement("div");
5082
+ const footerDiv = this.document.createElement("div");
3004
5083
  footerDiv.className = `${this.cssPrefix}footer`;
3005
5084
  footerDiv.style.position = "absolute";
3006
5085
  footerDiv.style.top = "auto";
@@ -3020,8 +5099,8 @@ var PaginationEngine = class {
3020
5099
  }
3021
5100
  pageBox.appendChild(footerDiv);
3022
5101
  }
3023
- if (this.showPageNumbers) {
3024
- const pageNum = document.createElement("div");
5102
+ if (this.showPageNumbers && !isSectionFiller) {
5103
+ const pageNum = this.document.createElement("div");
3025
5104
  pageNum.className = `${this.cssPrefix}number`;
3026
5105
  pageNum.textContent = String(pageNumber);
3027
5106
  pageBox.appendChild(pageNum);
@@ -3044,7 +5123,8 @@ var PaginationEngine = class {
3044
5123
  }
3045
5124
  };
3046
5125
  function paginateHtml(html, container, options = {}) {
3047
- const containerEl = typeof container === "string" ? document.getElementById(container) : container;
5126
+ const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
5127
+ const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
3048
5128
  if (!containerEl) {
3049
5129
  throw new Error("Container element not found");
3050
5130
  }
@@ -6346,6 +8426,13 @@ function fontWeightIsBold(w) {
6346
8426
  function escapeInlineMarkdown(text) {
6347
8427
  return text.replace(/([\\`*_[\]])/g, "\\$1");
6348
8428
  }
8429
+ function escapeLeadingBlockMarkers(md) {
8430
+ return md.split("\n").map((line) => {
8431
+ if (/^[#>|]/.test(line) || /^[-+] /.test(line)) return "\\" + line;
8432
+ if (/^\d+\. /.test(line)) return line.replace(".", "\\.");
8433
+ return line;
8434
+ }).join("\n");
8435
+ }
6349
8436
  function collectInlineSegments(node, out) {
6350
8437
  node.childNodes.forEach((child) => {
6351
8438
  if (isGeneratedChrome(child)) return;
@@ -6397,7 +8484,7 @@ function serializeInlineMarkdown(block) {
6397
8484
  merged.push({ ...s });
6398
8485
  }
6399
8486
  }
6400
- return merged.map(segToMarkdown).join("").trim();
8487
+ return escapeLeadingBlockMarkers(merged.map(segToMarkdown).join("").trim());
6401
8488
  }
6402
8489
  function isListBlock(block) {
6403
8490
  return !!block.querySelector(":scope > [data-list-marker]");
@@ -8432,17 +10519,19 @@ var DocxEditor = class _DocxEditor {
8432
10519
  this.refreshAfter(block, idx, false);
8433
10520
  }
8434
10521
  // ─── Table row / column editing (active block must be inside a table cell) ──────────
8435
- /** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */
10522
+ /** Run a table-structure op on the active cell's canonical tc anchor and re-render. */
8436
10523
  tableEdit(run) {
8437
10524
  const block = this.activeBlock;
8438
- if (this.closed || !block || !block.closest("table")) return;
10525
+ const cell = block?.closest("td, th");
10526
+ if (this.closed || !block || !cell || !block.closest("table")) return;
8439
10527
  const unid = block.getAttribute("data-anchor");
8440
10528
  if (!unid) return;
8441
- let fullId = this.anchorIdOf(block);
8442
- if (!fullId) return;
10529
+ const paragraphId = this.anchorIdOf(block);
10530
+ const cellId = this.anchorIdOf(cell);
10531
+ if (!paragraphId || !cellId) return;
8443
10532
  const idx = this.blockIndex(block);
8444
- fullId = this.syncBlock(block, fullId);
8445
- const res = this.parseEdit(run(fullId));
10533
+ this.syncBlock(block, paragraphId);
10534
+ const res = this.parseEdit(run(cellId));
8446
10535
  if (!res.success) return;
8447
10536
  this.refreshAfter(block, idx, false);
8448
10537
  }
@@ -8595,6 +10684,59 @@ var DocxEditor = class _DocxEditor {
8595
10684
  if (!res.success) return;
8596
10685
  this.reconcile(idx, false);
8597
10686
  }
10687
+ /**
10688
+ * Add a native Word comment on the current selection in the active block — or on the whole
10689
+ * block when the selection is collapsed (a null span comments the paragraph). The definition,
10690
+ * threading parts, and body-side range markers are written by the session's `AddComment`
10691
+ * (issue #300); the re-render shows the marker chrome the converter already draws. The
10692
+ * annotation type legal review runs on, finally authorable from the shipped surface
10693
+ * (issue #580).
10694
+ */
10695
+ addComment(markdown = "New comment.", author = "Reviewer") {
10696
+ const block = this.activeBlock;
10697
+ if (this.closed || !block) return;
10698
+ const bridge = this.exports.DocxSessionBridge;
10699
+ if (!bridge.AddComment) return;
10700
+ let fullId = this.anchorIdOf(block);
10701
+ if (!fullId) return;
10702
+ const idx = this.blockIndex(block);
10703
+ const span = selectionSpanIn(block);
10704
+ fullId = this.syncBlock(block, fullId);
10705
+ const res = this.parseEdit(
10706
+ bridge.AddComment(
10707
+ this.handle,
10708
+ fullId,
10709
+ span ? JSON.stringify(span) : "",
10710
+ author,
10711
+ "",
10712
+ "",
10713
+ markdown
10714
+ )
10715
+ );
10716
+ if (!res.success) return;
10717
+ this.reconcile(idx, false);
10718
+ }
10719
+ /** The document's native comment threads (session truth), for review UIs — flat entries
10720
+ * with `parentAnchorId` linking replies and `resolved` carrying thread state. */
10721
+ listComments() {
10722
+ if (this.closed) return [];
10723
+ const bridge = this.exports.DocxSessionBridge;
10724
+ if (!bridge.ListComments) return [];
10725
+ try {
10726
+ return JSON.parse(bridge.ListComments(this.handle));
10727
+ } catch {
10728
+ return [];
10729
+ }
10730
+ }
10731
+ /** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}).
10732
+ * Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is
10733
+ * needed — a review UI re-reads {@link listComments} for the new state. */
10734
+ setCommentResolved(commentAnchorId, resolved) {
10735
+ if (this.closed) return;
10736
+ const bridge = this.exports.DocxSessionBridge;
10737
+ if (!bridge.SetCommentResolved) return;
10738
+ this.parseEdit(bridge.SetCommentResolved(this.handle, commentAnchorId, resolved));
10739
+ }
8598
10740
  applyParagraphFormat(op) {
8599
10741
  const block = this.activeBlock;
8600
10742
  if (this.closed || !block) return;
@@ -9122,8 +11264,8 @@ var DocxEditor = class _DocxEditor {
9122
11264
  const backref = document.createElement("a");
9123
11265
  backref.setAttribute("class", `${prefix}-backref`);
9124
11266
  backref.setAttribute("contenteditable", "false");
9125
- backref.textContent = "\u21A9";
9126
- last.append(" ", backref);
11267
+ backref.textContent = " \u21A9";
11268
+ last.append(backref);
9127
11269
  }
9128
11270
  li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
9129
11271
  return li;
@@ -9680,6 +11822,25 @@ var RIBBON_CSS = `
9680
11822
  box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
9681
11823
  }
9682
11824
 
11825
+ /* \u2500\u2500 Review: comment threads \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
11826
+ .dxr-threads {
11827
+ display: flex; flex-direction: column; gap: 4px;
11828
+ max-height: 92px; overflow-y: auto; min-width: 260px; padding-right: 2px;
11829
+ }
11830
+ .dxr-thread {
11831
+ display: flex; align-items: center; gap: 6px;
11832
+ font-size: 11px; line-height: 1.3; padding: 3px 6px;
11833
+ border: 1px solid var(--dxr-rule); border-radius: 4px; background: var(--dxr-chrome-sunk);
11834
+ }
11835
+ .dxr-thread[data-resolved] { opacity: 0.55; }
11836
+ .dxr-thread[data-reply] { margin-left: 14px; }
11837
+ .dxr-thread .dxr-tauthor { font-weight: 600; white-space: nowrap; }
11838
+ .dxr-thread .dxr-ttext {
11839
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
11840
+ }
11841
+ .dxr-thread button { flex: none; }
11842
+ .dxr-group.dxr-grow { flex: 1; min-width: 0; }
11843
+
9683
11844
  /* \u2500\u2500 Table size picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
9684
11845
  .dxr-pop {
9685
11846
  display: none;
@@ -9998,6 +12159,7 @@ var RIBBON_HTML = `
9998
12159
  <button type="button" class="dxr-tab" role="tab" data-tab="home" aria-selected="true">Home</button>
9999
12160
  <button type="button" class="dxr-tab" role="tab" data-tab="insert" aria-selected="false">Insert</button>
10000
12161
  <button type="button" class="dxr-tab" role="tab" data-tab="layout" aria-selected="false">Layout</button>
12162
+ <button type="button" class="dxr-tab" role="tab" data-tab="review" aria-selected="false">Review</button>
10001
12163
  <button type="button" class="dxr-tab" role="tab" data-tab="table" data-contextual aria-selected="false" hidden>Table</button>
10002
12164
  </div>
10003
12165
 
@@ -10163,6 +12325,31 @@ var RIBBON_HTML = `
10163
12325
  </div>
10164
12326
  </div>
10165
12327
 
12328
+ <!-- REVIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
12329
+ Comment authoring over the session's native comment family (issue #580):
12330
+ the button comments the selection (the whole block when collapsed), and
12331
+ the thread list is live session truth with resolve/reopen per thread. -->
12332
+ <div class="dxr-panel" data-panel="review">
12333
+ <div class="dxr-group">
12334
+ <span class="dxr-glabel">Comments</span>
12335
+ <div class="dxr-row">
12336
+ <input data-dxr="commenttext" type="text" placeholder="Comment text&#8230;"
12337
+ title="The comment body; markdown is honored" />
12338
+ <button type="button" data-dxr="comment"
12339
+ title="Comment on the selection, or on the whole block when nothing is selected">&#128172; Comment</button>
12340
+ </div>
12341
+ <div class="dxr-row">
12342
+ <span class="dxr-note">Comments the selected text; the whole block when the caret is collapsed.</span>
12343
+ </div>
12344
+ </div>
12345
+ <div class="dxr-group dxr-grow">
12346
+ <span class="dxr-glabel">Threads</span>
12347
+ <div class="dxr-threads" data-dxr="threads" aria-live="polite">
12348
+ <span class="dxr-note">No comments yet.</span>
12349
+ </div>
12350
+ </div>
12351
+ </div>
12352
+
10166
12353
  <!-- TABLE (contextual) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
10167
12354
  Row/column editing lives here rather than in a floating toolbar: a docked
10168
12355
  contextual tab cannot overlap the cell you are editing. -->
@@ -10670,6 +12857,18 @@ var RibbonSurface = class {
10670
12857
  if (this.live && style.value) this.run("style", () => this.live.setParagraphStyle(style.value));
10671
12858
  style.value = "";
10672
12859
  });
12860
+ const commentButton = this.control("comment");
12861
+ if (commentButton) {
12862
+ this.keepSelection(commentButton);
12863
+ commentButton.addEventListener("click", () => {
12864
+ if (!this.live) return;
12865
+ const input = this.control("commenttext");
12866
+ const text = input?.value.trim() || "New comment.";
12867
+ this.run("comment", () => this.live.addComment(text, this.options.revisionAuthor ?? "Reviewer"));
12868
+ if (input) input.value = "";
12869
+ this.renderThreads();
12870
+ });
12871
+ }
10673
12872
  this.wireFileActions();
10674
12873
  this.wireLayout();
10675
12874
  this.wirePicker();
@@ -10792,6 +12991,52 @@ var RibbonSurface = class {
10792
12991
  else panel.removeAttribute("data-active");
10793
12992
  }
10794
12993
  if (name === "layout") this.syncPageNumbering();
12994
+ if (name === "review") this.renderThreads();
12995
+ }
12996
+ /** Re-render the Review tab's thread list from session truth ({@link DocxEditor.listComments}):
12997
+ * one row per comment, replies indented under their thread root, resolve/reopen per root. */
12998
+ renderThreads() {
12999
+ const host = this.control("threads");
13000
+ if (!host || !this.live) return;
13001
+ const doc = this.element.ownerDocument ?? document;
13002
+ host.textContent = "";
13003
+ const comments = this.live.listComments();
13004
+ if (comments.length === 0) {
13005
+ const empty = doc.createElement("span");
13006
+ empty.className = "dxr-note";
13007
+ empty.textContent = "No comments yet.";
13008
+ host.appendChild(empty);
13009
+ return;
13010
+ }
13011
+ for (const entry of comments) {
13012
+ const row = doc.createElement("div");
13013
+ row.className = "dxr-thread";
13014
+ row.setAttribute("data-thread", entry.anchorId);
13015
+ if (entry.resolved) row.setAttribute("data-resolved", "");
13016
+ if (entry.parentAnchorId) row.setAttribute("data-reply", "");
13017
+ const author = doc.createElement("span");
13018
+ author.className = "dxr-tauthor";
13019
+ author.textContent = entry.author || "\u2014";
13020
+ row.appendChild(author);
13021
+ const text = doc.createElement("span");
13022
+ text.className = "dxr-ttext";
13023
+ text.textContent = entry.text;
13024
+ text.title = entry.text;
13025
+ row.appendChild(text);
13026
+ if (!entry.parentAnchorId) {
13027
+ const toggle = doc.createElement("button");
13028
+ toggle.type = "button";
13029
+ toggle.textContent = entry.resolved ? "Reopen" : "Resolve";
13030
+ toggle.title = entry.resolved ? "Reopen this comment thread" : "Mark this comment thread resolved";
13031
+ toggle.addEventListener("click", () => {
13032
+ if (!this.live) return;
13033
+ this.run(entry.resolved ? "reopen comment" : "resolve comment", () => this.live.setCommentResolved(entry.anchorId, !entry.resolved));
13034
+ this.renderThreads();
13035
+ });
13036
+ row.appendChild(toggle);
13037
+ }
13038
+ host.appendChild(row);
13039
+ }
10795
13040
  }
10796
13041
  // ── selection-driven state ──────────────────────────────────────────────────
10797
13042
  selectionElement() {
@@ -11093,6 +13338,55 @@ async function toBytes(input) {
11093
13338
  const buffer = await input.arrayBuffer();
11094
13339
  return new Uint8Array(buffer);
11095
13340
  }
13341
+ async function generatePackageManifest(document2) {
13342
+ const exports = ensureInitialized();
13343
+ const bytes = await toBytes(document2);
13344
+ await yieldToMain();
13345
+ return JSON.parse(
13346
+ exports.DocumentConverter.GeneratePackageManifest(bytes)
13347
+ );
13348
+ }
13349
+ async function verifyDeliverable(document2, baseline) {
13350
+ const exports = ensureInitialized();
13351
+ const bytes = await toBytes(document2);
13352
+ const baselineBytes = baseline === void 0 ? void 0 : await toBytes(baseline);
13353
+ await yieldToMain();
13354
+ return JSON.parse(
13355
+ baselineBytes === void 0 ? exports.DocumentConverter.VerifyDeliverable(bytes) : exports.DocumentConverter.VerifyDeliverableWithBaseline(
13356
+ baselineBytes,
13357
+ bytes
13358
+ )
13359
+ );
13360
+ }
13361
+ async function verifyDeliveryReceipt(receiptJson, artifacts) {
13362
+ const exports = ensureInitialized();
13363
+ const artifactsJson = artifacts === void 0 ? "" : JSON.stringify(
13364
+ Object.fromEntries(
13365
+ Object.entries(artifacts).map(([artifactId, bytes]) => [
13366
+ artifactId,
13367
+ bytesToBase64(bytes)
13368
+ ])
13369
+ )
13370
+ );
13371
+ await yieldToMain();
13372
+ return JSON.parse(
13373
+ exports.DocumentConverter.VerifyDeliveryReceipt(receiptJson, artifactsJson)
13374
+ );
13375
+ }
13376
+ async function proveRedlineReversibility(baseline, intendedFinal, redline) {
13377
+ const exports = ensureInitialized();
13378
+ const baselineBytes = await toBytes(baseline);
13379
+ const intendedFinalBytes = await toBytes(intendedFinal);
13380
+ const redlineBytes = await toBytes(redline);
13381
+ await yieldToMain();
13382
+ return JSON.parse(
13383
+ exports.DocumentConverter.ProveRedlineReversibility(
13384
+ baselineBytes,
13385
+ intendedFinalBytes,
13386
+ redlineBytes
13387
+ )
13388
+ );
13389
+ }
11096
13390
  async function renderBlockHtml(document2, anchorId, options) {
11097
13391
  const exports = ensureInitialized();
11098
13392
  const bytes = await toBytes(document2);
@@ -11353,21 +13647,30 @@ async function docxDiffGetRevisions(left2, right2, settings) {
11353
13647
  throw new Error(`Failed to get DocxDiff revisions: ${error.error}`);
11354
13648
  }
11355
13649
  const parsed = JSON.parse(result);
11356
- return (parsed.revisions || parsed.Revisions || []).map((r) => ({
11357
- revisionType: r.revisionType ?? r.RevisionType,
11358
- text: r.text ?? r.Text,
11359
- author: r.author ?? r.Author,
11360
- date: r.date ?? r.Date,
11361
- moveGroupId: r.moveGroupId ?? r.MoveGroupId ?? void 0,
11362
- isMoveSource: r.isMoveSource ?? r.IsMoveSource ?? void 0,
11363
- formatChange: r.formatChange || r.FormatChange ? {
11364
- oldProperties: r.formatChange?.oldProperties ?? r.FormatChange?.OldProperties,
11365
- newProperties: r.formatChange?.newProperties ?? r.FormatChange?.NewProperties,
11366
- changedPropertyNames: r.formatChange?.changedPropertyNames ?? r.FormatChange?.ChangedPropertyNames
11367
- } : void 0,
11368
- leftAnchor: r.leftAnchor ?? r.LeftAnchor ?? void 0,
11369
- rightAnchor: r.rightAnchor ?? r.RightAnchor ?? void 0
11370
- }));
13650
+ return (parsed.revisions || parsed.Revisions || []).map(mapDocxDiffRevision);
13651
+ }
13652
+ async function docxDiffCompareProducts(left2, right2, settings, products) {
13653
+ const exports = ensureInitialized();
13654
+ const leftBytes = await toBytes(left2);
13655
+ const rightBytes = await toBytes(right2);
13656
+ await yieldToMain();
13657
+ const result = exports.DocxDiffBridge.CompareProductsJson(
13658
+ leftBytes,
13659
+ rightBytes,
13660
+ docxDiffSettingsJson(settings),
13661
+ products ? JSON.stringify(products) : ""
13662
+ );
13663
+ if (isErrorResponse(result)) {
13664
+ const error = parseError(result);
13665
+ throw new Error(`Failed to compare products: ${error.error}`);
13666
+ }
13667
+ const parsed = JSON.parse(result);
13668
+ return {
13669
+ redline: typeof parsed.redlineB64 === "string" ? Uint8Array.from(atob(parsed.redlineB64), (c) => c.charCodeAt(0)) : void 0,
13670
+ revisions: Array.isArray(parsed.revisions) ? parsed.revisions.map(mapDocxDiffRevision) : void 0,
13671
+ editScript: parsed.editScript !== void 0 ? parsed.editScript : void 0,
13672
+ semanticChanges: parsed.semanticChanges !== void 0 ? parsed.semanticChanges : void 0
13673
+ };
11371
13674
  }
11372
13675
  async function docxDiffGetEditScript(left2, right2, settings) {
11373
13676
  const exports = ensureInitialized();
@@ -11385,6 +13688,22 @@ async function docxDiffGetEditScript(left2, right2, settings) {
11385
13688
  }
11386
13689
  return result;
11387
13690
  }
13691
+ async function docxDiffGetSemanticChanges(left2, right2, settings) {
13692
+ const exports = ensureInitialized();
13693
+ const leftBytes = await toBytes(left2);
13694
+ const rightBytes = await toBytes(right2);
13695
+ await yieldToMain();
13696
+ const result = exports.DocxDiffBridge.GetSemanticChangesJson(
13697
+ leftBytes,
13698
+ rightBytes,
13699
+ docxDiffSettingsJson(settings)
13700
+ );
13701
+ if (isErrorResponse(result)) {
13702
+ const error = parseError(result);
13703
+ throw new Error(`Failed to get semantic changes: ${error.error}`);
13704
+ }
13705
+ return JSON.parse(result);
13706
+ }
11388
13707
  async function docxDiffAcceptRevisions(redline) {
11389
13708
  const exports = ensureInitialized();
11390
13709
  const bytes = await toBytes(redline);
@@ -11638,6 +13957,7 @@ async function getDocumentStructure(document2) {
11638
13957
  const parsed = JSON.parse(result);
11639
13958
  const convertElement = (el) => ({
11640
13959
  id: el.Id || el.id,
13960
+ anchorId: el.AnchorId || el.anchorId,
11641
13961
  type: el.Type || el.type,
11642
13962
  textPreview: el.TextPreview || el.textPreview,
11643
13963
  index: el.Index ?? el.index,
@@ -11649,8 +13969,12 @@ async function getDocumentStructure(document2) {
11649
13969
  });
11650
13970
  const convertTableColumn = (col) => ({
11651
13971
  tableId: col.TableId || col.tableId,
13972
+ anchorId: col.AnchorId || col.anchorId,
13973
+ tableAnchorId: col.TableAnchorId || col.tableAnchorId,
13974
+ isVirtual: col.IsVirtual ?? col.isVirtual ?? false,
11652
13975
  columnIndex: col.ColumnIndex ?? col.columnIndex,
11653
13976
  cellIds: col.CellIds || col.cellIds || [],
13977
+ cellAnchorIds: col.CellAnchorIds || col.cellAnchorIds || [],
11654
13978
  rowCount: col.RowCount ?? col.rowCount
11655
13979
  });
11656
13980
  const root = convertElement(parsed.Root || parsed.root);
@@ -11713,7 +14037,8 @@ async function getDocumentMetadata(document2) {
11713
14037
  hasEndnotes: parsed.HasEndnotes ?? parsed.hasEndnotes,
11714
14038
  hasTrackedChanges: parsed.HasTrackedChanges ?? parsed.hasTrackedChanges,
11715
14039
  hasComments: parsed.HasComments ?? parsed.hasComments,
11716
- estimatedPageCount: parsed.EstimatedPageCount ?? parsed.estimatedPageCount
14040
+ estimatedPageCount: parsed.EstimatedPageCount ?? parsed.estimatedPageCount,
14041
+ estimatedPageCountSource: parsed.EstimatedPageCountSource ?? parsed.estimatedPageCountSource ?? "heuristic"
11717
14042
  };
11718
14043
  }
11719
14044
  async function exportToOpenContract(document2) {
@@ -12509,6 +14834,7 @@ export {
12509
14834
  addAnnotation,
12510
14835
  addAnnotationToHtml,
12511
14836
  addAnnotationWithTarget,
14837
+ clearPageCitationHighlight,
12512
14838
  compareDocuments,
12513
14839
  compareDocumentsToHtml,
12514
14840
  compareDocumentsToHtmlWithLog,
@@ -12523,15 +14849,18 @@ export {
12523
14849
  createEditor,
12524
14850
  createExternalAnnotationSet,
12525
14851
  createRibbonEditor,
14852
+ createUnavailablePageMap,
12526
14853
  createViewer,
12527
14854
  docxDiffAcceptRevisions,
12528
14855
  docxDiffCompare,
14856
+ docxDiffCompareProducts,
12529
14857
  docxDiffConsolidate,
12530
14858
  docxDiffGetConflicts,
12531
14859
  docxDiffGetConsolidatedEditScript,
12532
14860
  docxDiffGetConsolidatedRevisions,
12533
14861
  docxDiffGetEditScript,
12534
14862
  docxDiffGetRevisions,
14863
+ docxDiffGetSemanticChanges,
12535
14864
  docxDiffRejectRevisions,
12536
14865
  exportToOpenContract,
12537
14866
  findElementById,
@@ -12541,6 +14870,7 @@ export {
12541
14870
  fitScale,
12542
14871
  generateAnnotationCss,
12543
14872
  generateAnnotationVisibilityCss,
14873
+ generatePackageManifest,
12544
14874
  getAnnotations,
12545
14875
  getDocumentMetadata,
12546
14876
  getDocumentStructure,
@@ -12560,10 +14890,12 @@ export {
12560
14890
  isMoveDestination,
12561
14891
  isMoveSource,
12562
14892
  mountRibbon,
14893
+ navigateToPageCitation,
12563
14894
  openDocxSession2 as openDocxSession,
12564
14895
  paginateHtml,
12565
14896
  parseSectionDimensions,
12566
14897
  projectAnnotationsOntoHtml,
14898
+ proveRedlineReversibility,
12567
14899
  ptToPx,
12568
14900
  pxToPt,
12569
14901
  removeAnnotation,
@@ -12583,5 +14915,7 @@ export {
12583
14915
  targetTableRow,
12584
14916
  toDocumentBytes,
12585
14917
  validateExternalAnnotations,
14918
+ verifyDeliverable,
14919
+ verifyDeliveryReceipt,
12586
14920
  wasmBasePath
12587
14921
  };