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
@@ -154,6 +154,7 @@ var Docxodus = (() => {
154
154
  addAnnotation: () => addAnnotation,
155
155
  addAnnotationToHtml: () => addAnnotationToHtml,
156
156
  addAnnotationWithTarget: () => addAnnotationWithTarget,
157
+ clearPageCitationHighlight: () => clearPageCitationHighlight,
157
158
  compareDocuments: () => compareDocuments,
158
159
  compareDocumentsToHtml: () => compareDocumentsToHtml,
159
160
  compareDocumentsToHtmlWithLog: () => compareDocumentsToHtmlWithLog,
@@ -168,15 +169,18 @@ var Docxodus = (() => {
168
169
  createEditor: () => createEditor,
169
170
  createExternalAnnotationSet: () => createExternalAnnotationSet,
170
171
  createRibbonEditor: () => createRibbonEditor,
172
+ createUnavailablePageMap: () => createUnavailablePageMap,
171
173
  createViewer: () => createViewer,
172
174
  docxDiffAcceptRevisions: () => docxDiffAcceptRevisions,
173
175
  docxDiffCompare: () => docxDiffCompare,
176
+ docxDiffCompareProducts: () => docxDiffCompareProducts,
174
177
  docxDiffConsolidate: () => docxDiffConsolidate,
175
178
  docxDiffGetConflicts: () => docxDiffGetConflicts,
176
179
  docxDiffGetConsolidatedEditScript: () => docxDiffGetConsolidatedEditScript,
177
180
  docxDiffGetConsolidatedRevisions: () => docxDiffGetConsolidatedRevisions,
178
181
  docxDiffGetEditScript: () => docxDiffGetEditScript,
179
182
  docxDiffGetRevisions: () => docxDiffGetRevisions,
183
+ docxDiffGetSemanticChanges: () => docxDiffGetSemanticChanges,
180
184
  docxDiffRejectRevisions: () => docxDiffRejectRevisions,
181
185
  exportToOpenContract: () => exportToOpenContract,
182
186
  findElementById: () => findElementById,
@@ -186,6 +190,7 @@ var Docxodus = (() => {
186
190
  fitScale: () => fitScale,
187
191
  generateAnnotationCss: () => generateAnnotationCss,
188
192
  generateAnnotationVisibilityCss: () => generateAnnotationVisibilityCss,
193
+ generatePackageManifest: () => generatePackageManifest,
189
194
  getAnnotations: () => getAnnotations,
190
195
  getDocumentMetadata: () => getDocumentMetadata,
191
196
  getDocumentStructure: () => getDocumentStructure,
@@ -205,10 +210,12 @@ var Docxodus = (() => {
205
210
  isMoveDestination: () => isMoveDestination,
206
211
  isMoveSource: () => isMoveSource,
207
212
  mountRibbon: () => mountRibbon,
213
+ navigateToPageCitation: () => navigateToPageCitation,
208
214
  openDocxSession: () => openDocxSession2,
209
215
  paginateHtml: () => paginateHtml,
210
216
  parseSectionDimensions: () => parseSectionDimensions,
211
217
  projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
218
+ proveRedlineReversibility: () => proveRedlineReversibility,
212
219
  ptToPx: () => ptToPx,
213
220
  pxToPt: () => pxToPt,
214
221
  removeAnnotation: () => removeAnnotation,
@@ -228,6 +235,8 @@ var Docxodus = (() => {
228
235
  targetTableRow: () => targetTableRow,
229
236
  toDocumentBytes: () => toDocumentBytes,
230
237
  validateExternalAnnotations: () => validateExternalAnnotations,
238
+ verifyDeliverable: () => verifyDeliverable,
239
+ verifyDeliveryReceipt: () => verifyDeliveryReceipt,
231
240
  wasmBasePath: () => wasmBasePath
232
241
  });
233
242
 
@@ -480,7 +489,46 @@ var Docxodus = (() => {
480
489
  }
481
490
 
482
491
  // src/session.ts
483
- var DocxSession = class {
492
+ function mutationBatchChangeSet(before, after, key) {
493
+ const beforeGroups = /* @__PURE__ */ new Map();
494
+ const afterGroups = /* @__PURE__ */ new Map();
495
+ const group = (items, target) => {
496
+ items.forEach((item, index) => {
497
+ const identity = key(item);
498
+ const indices = target.get(identity) ?? [];
499
+ indices.push(index);
500
+ target.set(identity, indices);
501
+ });
502
+ };
503
+ group(before, beforeGroups);
504
+ group(after, afterGroups);
505
+ const beforeMatched = before.map(() => false);
506
+ const afterMatched = after.map(() => false);
507
+ const modified = after.map(() => false);
508
+ for (const [identity, afterIndices] of afterGroups) {
509
+ const beforeIndices = beforeGroups.get(identity) ?? [];
510
+ for (const afterIndex of afterIndices) {
511
+ const beforeIndex = beforeIndices.find((index) => !beforeMatched[index] && JSON.stringify(before[index]) === JSON.stringify(after[afterIndex]));
512
+ if (beforeIndex === void 0) continue;
513
+ beforeMatched[beforeIndex] = true;
514
+ afterMatched[afterIndex] = true;
515
+ }
516
+ const remainingBefore = beforeIndices.filter((index) => !beforeMatched[index]);
517
+ const remainingAfter = afterIndices.filter((index) => !afterMatched[index]);
518
+ const modifiedCount = Math.min(remainingBefore.length, remainingAfter.length);
519
+ for (let index = 0; index < modifiedCount; index++) {
520
+ beforeMatched[remainingBefore[index]] = true;
521
+ afterMatched[remainingAfter[index]] = true;
522
+ modified[remainingAfter[index]] = true;
523
+ }
524
+ }
525
+ return {
526
+ added: after.filter((_, index) => !afterMatched[index]),
527
+ removed: before.filter((_, index) => !beforeMatched[index]),
528
+ modified: after.filter((_, index) => modified[index])
529
+ };
530
+ }
531
+ var DocxSession = class _DocxSession {
484
532
  /** @internal */
485
533
  constructor(handle, wasm) {
486
534
  // ─── Raw escape hatch ────────────────────────────────────────────────
@@ -496,6 +544,336 @@ var Docxodus = (() => {
496
544
  project() {
497
545
  return JSON.parse(this.wasm.Project(this.handle));
498
546
  }
547
+ /** Monotonic document version (0 at open; +1 per committed mutation/undo/redo). */
548
+ getVersion() {
549
+ return JSON.parse(this.wasm.GetVersion(this.handle)).version;
550
+ }
551
+ /**
552
+ * Verification manifest for the current logical checkpoint. Unsaved edits are included and
553
+ * the read does not mutate package bytes, caches, history, or the document version.
554
+ */
555
+ getPackageManifest() {
556
+ return JSON.parse(this.wasm.GetPackageManifest(this.handle));
557
+ }
558
+ /** Register a browser-materialized PageMap without changing the document version. */
559
+ registerPageMap(pageMap, expectedRendererFingerprint) {
560
+ return JSON.parse(this.wasm.RegisterPageMap(
561
+ this.handle,
562
+ JSON.stringify(pageMap),
563
+ expectedRendererFingerprint ?? ""
564
+ ));
565
+ }
566
+ getPageMapStatus(request) {
567
+ return JSON.parse(this.wasm.GetPageMapStatus(
568
+ this.handle,
569
+ request ? JSON.stringify(request) : ""
570
+ ));
571
+ }
572
+ getPageCitation(anchorId, request) {
573
+ return JSON.parse(this.wasm.GetPageCitation(
574
+ this.handle,
575
+ anchorId,
576
+ JSON.stringify(request)
577
+ ));
578
+ }
579
+ /** Evaluate optimistic guards without mutating or advancing the version. */
580
+ checkPreconditions(preconditions) {
581
+ return JSON.parse(
582
+ this.wasm.CheckPreconditions(this.handle, JSON.stringify(preconditions))
583
+ );
584
+ }
585
+ /**
586
+ * Guard any synchronous mutation. WASM calls are synchronous and single-threaded, so the
587
+ * check and callback form one uninterrupted client-side operation. Prefer a method's native
588
+ * `preconditions` option where it has one (notably replaceTextRange's match-count guard).
589
+ */
590
+ runWithPreconditions(preconditions, mutation) {
591
+ const checked = this.checkPreconditions(preconditions);
592
+ return checked.success ? mutation() : checked;
593
+ }
594
+ /**
595
+ * Execute synchronous mutations atomically by default. Atomic success is one undo/version
596
+ * unit; any failed or thrown step restores the exact package and history checkpoint.
597
+ */
598
+ executeBatch(steps, mode = "atomic") {
599
+ if (mode !== "atomic" && mode !== "best_effort") {
600
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
601
+ }
602
+ const baseVersion = this.getVersion();
603
+ const observationWarnings = [];
604
+ const inspect = (label, read, fallback) => {
605
+ try {
606
+ return read();
607
+ } catch (error) {
608
+ observationWarnings.push(`${label} unavailable: ${error instanceof Error ? error.message : String(error)}`);
609
+ return fallback;
610
+ }
611
+ };
612
+ const beforeRevisions = inspect("Revision delta inspection", () => this.listRevisions(), []);
613
+ const beforeComments = inspect("Comment delta inspection", () => this.listComments(), []);
614
+ const beforeAnnotations = inspect("Annotation delta inspection", () => this.listAnnotations(), []);
615
+ const complete = (result) => {
616
+ try {
617
+ const revisionChanges = mutationBatchChangeSet(
618
+ beforeRevisions,
619
+ inspect("Revision delta inspection", () => this.listRevisions(), beforeRevisions),
620
+ (revision) => revision.id
621
+ );
622
+ const commentChanges = mutationBatchChangeSet(
623
+ beforeComments,
624
+ inspect("Comment delta inspection", () => this.listComments(), beforeComments),
625
+ (comment) => comment.anchorId
626
+ );
627
+ const annotationChanges = mutationBatchChangeSet(
628
+ beforeAnnotations,
629
+ inspect("Annotation delta inspection", () => this.listAnnotations(), beforeAnnotations),
630
+ (annotation) => annotation.id ?? ""
631
+ );
632
+ const resultVersion = inspect(
633
+ "Result version inspection",
634
+ () => this.getVersion(),
635
+ baseVersion
636
+ );
637
+ const warnings = [...observationWarnings];
638
+ if ([...revisionChanges.added, ...revisionChanges.modified].some((revision) => revision.date !== void 0 && revision.date !== null)) {
639
+ warnings.push("Tracked-revision date attributes may use the execution clock; compare revision ids, authors, types, text, and anchors across separate executions.");
640
+ }
641
+ if ([...commentChanges.added, ...commentChanges.modified].some((comment) => comment.date !== void 0 && comment.date !== null)) {
642
+ warnings.push("Comment date attributes may be generated from the execution clock; supply dates explicitly when byte-identical replay is required.");
643
+ }
644
+ if (annotationChanges.added.some((annotation) => annotation.created !== void 0 && annotation.created !== null)) {
645
+ warnings.push("Auto-generated annotation ids or creation timestamps are execution metadata; supply id and created explicitly when byte-identical replay is required.");
646
+ }
647
+ if (result.steps.some((step) => step.results.some((edit) => edit.created.length > 0))) {
648
+ 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.");
649
+ }
650
+ if (mode === "best_effort" && !result.success) {
651
+ warnings.push("Best-effort execution retains every successful step despite later failures.");
652
+ }
653
+ let packageHash = null;
654
+ if (!this.wasm.GetPackageContentHash) {
655
+ warnings.push("This WASM bundle predates package equivalence hashes; packageHash is unavailable.");
656
+ } else {
657
+ try {
658
+ packageHash = this.wasm.GetPackageContentHash(this.handle);
659
+ } catch (error) {
660
+ warnings.push(`Package equivalence hash unavailable: ${error instanceof Error ? error.message : String(error)}`);
661
+ }
662
+ }
663
+ return {
664
+ ...result,
665
+ preview: false,
666
+ baseVersion,
667
+ resultVersion,
668
+ packageHash,
669
+ revisionChanges,
670
+ commentChanges,
671
+ annotationChanges,
672
+ warnings,
673
+ html: null
674
+ };
675
+ } catch (error) {
676
+ return {
677
+ ...result,
678
+ preview: false,
679
+ baseVersion,
680
+ resultVersion: inspect("Result version inspection", () => this.getVersion(), baseVersion),
681
+ packageHash: null,
682
+ revisionChanges: { added: [], removed: [], modified: [] },
683
+ commentChanges: { added: [], removed: [], modified: [] },
684
+ annotationChanges: { added: [], removed: [], modified: [] },
685
+ warnings: [
686
+ ...observationWarnings,
687
+ `Batch receipt enrichment unavailable: ${error instanceof Error ? error.message : String(error)}`
688
+ ],
689
+ html: null
690
+ };
691
+ }
692
+ };
693
+ const internalFailure = (value) => ({
694
+ success: false,
695
+ error: { code: "internal_error", message: value instanceof Error ? value.message : String(value) },
696
+ created: [],
697
+ removed: [],
698
+ modified: []
699
+ });
700
+ const run = (step) => {
701
+ try {
702
+ const value = step.mutation();
703
+ const results = Array.isArray(value) ? value : [value];
704
+ if (results.some((result) => result === null || typeof result !== "object" || typeof result.success !== "boolean")) {
705
+ return [internalFailure("batch mutation returned invalid edit results")];
706
+ }
707
+ return results;
708
+ } catch (error) {
709
+ return [internalFailure(error)];
710
+ }
711
+ };
712
+ const failureOf = (step, rolledBack) => ({
713
+ index: step.index,
714
+ tool: step.tool,
715
+ action: step.action,
716
+ error: step.results.find((result) => !result.success)?.error ?? { code: "internal_error", message: "batch step failed without an error" },
717
+ rolledBack
718
+ });
719
+ const preflightOne = (step) => {
720
+ try {
721
+ return step.preflight?.();
722
+ } catch (error) {
723
+ return internalFailure(error).error;
724
+ }
725
+ };
726
+ if (mode === "atomic") {
727
+ const preflight = steps.map(preflightOne);
728
+ const failedPreflight = preflight.findIndex((error) => error !== void 0);
729
+ if (failedPreflight >= 0) {
730
+ const source = steps[failedPreflight];
731
+ const failed2 = {
732
+ index: failedPreflight,
733
+ tool: source.tool,
734
+ action: source.action,
735
+ success: false,
736
+ rolledBack: true,
737
+ results: [{ success: false, error: preflight[failedPreflight], created: [], removed: [], modified: [] }]
738
+ };
739
+ return complete({
740
+ mode,
741
+ status: "failed",
742
+ success: false,
743
+ rolledBack: true,
744
+ steps: [failed2],
745
+ failure: failureOf(failed2, true)
746
+ });
747
+ }
748
+ const transaction = this.wasm.BeginTransaction(this.handle);
749
+ const completed2 = [];
750
+ try {
751
+ for (let index = 0; index < steps.length; index++) {
752
+ const source = steps[index];
753
+ const results = run(source);
754
+ const step = {
755
+ index,
756
+ tool: source.tool,
757
+ action: source.action,
758
+ success: results.every((result) => result.success),
759
+ rolledBack: false,
760
+ results
761
+ };
762
+ completed2.push(step);
763
+ if (!step.success) {
764
+ this.wasm.RollbackTransaction(transaction);
765
+ const rolledBack = completed2.map((value) => ({ ...value, rolledBack: true }));
766
+ const failed2 = rolledBack[rolledBack.length - 1];
767
+ return complete({
768
+ mode,
769
+ status: "failed",
770
+ success: false,
771
+ rolledBack: true,
772
+ steps: rolledBack,
773
+ failure: failureOf(failed2, true)
774
+ });
775
+ }
776
+ }
777
+ this.wasm.CommitTransaction(transaction);
778
+ return complete({ mode, status: "ok", success: true, rolledBack: false, steps: completed2 });
779
+ } catch (error) {
780
+ try {
781
+ this.wasm.RollbackTransaction(transaction);
782
+ } catch {
783
+ }
784
+ throw error;
785
+ }
786
+ }
787
+ const completed = steps.map((source, index) => {
788
+ const preflight = preflightOne(source);
789
+ const results = preflight ? [{ success: false, error: preflight, created: [], removed: [], modified: [] }] : run(source);
790
+ return {
791
+ index,
792
+ tool: source.tool,
793
+ action: source.action,
794
+ success: results.every((result) => result.success),
795
+ rolledBack: false,
796
+ results
797
+ };
798
+ });
799
+ const failed = completed.find((step) => !step.success);
800
+ return complete({
801
+ mode,
802
+ status: failed ? completed.some((step) => step.success) ? "partial" : "failed" : "ok",
803
+ success: failed === void 0,
804
+ rolledBack: false,
805
+ steps: completed,
806
+ failure: failed ? failureOf(failed, false) : void 0
807
+ });
808
+ }
809
+ /**
810
+ * Execute the same callback batch algorithm against a complete isolated package clone.
811
+ * Callbacks receive the shadow session explicitly; mutate that argument. The live session's
812
+ * package, caches, version, configuration, and undo/redo history are never execution targets.
813
+ */
814
+ previewBatch(steps, mode = "atomic", options) {
815
+ if (mode !== "atomic" && mode !== "best_effort") {
816
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
817
+ }
818
+ const htmlMode = options?.html ?? "none";
819
+ if (htmlMode !== "none" && htmlMode !== "scoped" && htmlMode !== "full") {
820
+ throw new RangeError(`unknown preview HTML mode: ${String(htmlMode)}`);
821
+ }
822
+ if (!this.wasm.OpenPreviewSession) {
823
+ throw new Error("This WASM bundle does not support isolated mutation previews.");
824
+ }
825
+ const shadow = new _DocxSession(this.wasm.OpenPreviewSession(this.handle), this.wasm);
826
+ try {
827
+ const result = shadow.executeBatch(
828
+ steps.map((step) => ({
829
+ tool: step.tool,
830
+ action: step.action,
831
+ mutation: () => step.mutation(shadow),
832
+ preflight: step.preflight ? () => step.preflight(shadow) : void 0
833
+ })),
834
+ mode
835
+ );
836
+ const warnings = [...result.warnings];
837
+ let html = null;
838
+ const unwrapRendered = (rendered) => {
839
+ if (rendered.trimStart().startsWith("{")) {
840
+ const envelope = JSON.parse(rendered);
841
+ if (envelope.error) {
842
+ warnings.push(`Preview HTML could not be generated: ${envelope.error}`);
843
+ return null;
844
+ }
845
+ }
846
+ return rendered;
847
+ };
848
+ 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.";
849
+ try {
850
+ if (htmlMode === "scoped") {
851
+ if (!options?.htmlAnchorId) {
852
+ warnings.push("Scoped HTML was requested without htmlAnchorId; no HTML was generated.");
853
+ } else if (this.wasm.RenderPreviewBlockHtml) {
854
+ html = unwrapRendered(
855
+ this.wasm.RenderPreviewBlockHtml(shadow.handle, options.htmlAnchorId)
856
+ );
857
+ } else {
858
+ warnings.push(legacyProfileWarning);
859
+ html = shadow.renderBlock(options.htmlAnchorId);
860
+ }
861
+ } else if (htmlMode === "full") {
862
+ if (this.wasm.RenderPreviewHtml) {
863
+ html = unwrapRendered(this.wasm.RenderPreviewHtml(shadow.handle));
864
+ } else {
865
+ warnings.push(legacyProfileWarning);
866
+ html = unwrapRendered(this.wasm.RenderHtmlForReview ? this.wasm.RenderHtmlForReview(shadow.handle, "docx-", false, false, 1, true) : this.wasm.RenderHtml(shadow.handle, "docx-", false, false, 1));
867
+ }
868
+ }
869
+ } catch (error) {
870
+ warnings.push(`Preview HTML could not be generated: ${error instanceof Error ? error.message : String(error)}`);
871
+ }
872
+ return { ...result, preview: true, warnings, html };
873
+ } finally {
874
+ shadow.close();
875
+ }
876
+ }
499
877
  /**
500
878
  * Project a slice of the document keyed off an anchor — useful for showing
501
879
  * one section to an LLM at a time without paying the cost of projecting the
@@ -511,9 +889,9 @@ var Docxodus = (() => {
511
889
  *
512
890
  * @see docs/architecture/docx_mutation_api.md
513
891
  */
514
- projectAnchor(anchorId, depth = 2 /* SubtreeAndFollowingSiblings */) {
892
+ projectAnchor(anchorId, depth = 2 /* SubtreeAndFollowingSiblings */, citation) {
515
893
  return JSON.parse(
516
- this.wasm.ProjectAnchor(this.handle, anchorId, depth)
894
+ citation ? this.wasm.ProjectAnchorWithCitations(this.handle, anchorId, depth, JSON.stringify(citation)) : this.wasm.ProjectAnchor(this.handle, anchorId, depth)
517
895
  );
518
896
  }
519
897
  /**
@@ -537,11 +915,21 @@ var Docxodus = (() => {
537
915
  return html;
538
916
  }
539
917
  // ─── Tier A: text CRUD ───────────────────────────────────────────────
540
- replaceText(anchorId, markdown) {
541
- return JSON.parse(this.wasm.ReplaceText(this.handle, anchorId, markdown));
542
- }
543
- deleteBlock(anchorId) {
544
- return JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
918
+ replaceText(anchorId, markdown, preconditions) {
919
+ const apply = () => JSON.parse(
920
+ this.wasm.ReplaceText(this.handle, anchorId, markdown)
921
+ );
922
+ return preconditions ? this.runWithPreconditions(
923
+ { ...preconditions, anchorId: preconditions.anchorId ?? anchorId },
924
+ apply
925
+ ) : apply();
926
+ }
927
+ deleteBlock(anchorId, preconditions) {
928
+ const apply = () => JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
929
+ return preconditions ? this.runWithPreconditions(
930
+ { ...preconditions, anchorId: preconditions.anchorId ?? anchorId },
931
+ apply
932
+ ) : apply();
545
933
  }
546
934
  /** Reorder one top-level paragraph/heading/list/table block relative to another. */
547
935
  moveBlock(sourceAnchorId, targetAnchorId, position) {
@@ -596,7 +984,7 @@ var Docxodus = (() => {
596
984
  }
597
985
  /**
598
986
  * Insert a `rows`×`cols` table before/after the block. `options` controls borders, row-major
599
- * cell markdown, and cell alignment. The returned `EditResult.created` lists the cell-paragraph
987
+ * cell markdown, and cell alignment. The returned `EditResult.created` lists canonical `tc`
600
988
  * anchors (row-major), so each cell can then be addressed to fill/format.
601
989
  */
602
990
  insertTable(anchorId, position, rows, cols, options) {
@@ -605,10 +993,26 @@ var Docxodus = (() => {
605
993
  this.wasm.InsertTable(this.handle, anchorId, position, rows, cols, optionsJson)
606
994
  );
607
995
  }
996
+ /** Resolve a canonical `tbl` anchor to explicit table/row/column/cell identities. */
997
+ getTableMetadata(tableAnchorId) {
998
+ return JSON.parse(this.wasm.GetTableMetadata(this.handle, tableAnchorId));
999
+ }
1000
+ /** Resolve a canonical `tc` anchor to its zero-based table-grid coordinate and spans. */
1001
+ resolveTableCellAnchor(cellAnchorId) {
1002
+ return JSON.parse(
1003
+ this.wasm.ResolveTableCellAnchor(this.handle, cellAnchorId)
1004
+ );
1005
+ }
1006
+ /** Resolve a zero-based table-grid coordinate to the physical `tc` covering it. */
1007
+ resolveTableCellCoordinate(tableAnchorId, rowIndex, columnIndex) {
1008
+ return JSON.parse(
1009
+ this.wasm.ResolveTableCellCoordinate(this.handle, tableAnchorId, rowIndex, columnIndex)
1010
+ );
1011
+ }
608
1012
  /**
609
- * Table row/column editing, addressed by a cell-paragraph anchor (e.g. one returned from
610
- * {@link insertTable}'s `created`). Insert clones the reference row/column's widths and starts
611
- * empty (`created` lists the new cell-paragraph anchors); delete of the last row/column removes
1013
+ * Table row/column editing, addressed by the canonical `tc` anchor returned from
1014
+ * {@link insertTable}'s `created` or table metadata. Insert clones the reference row/column's
1015
+ * widths and starts empty (`created` lists new `tc` anchors); delete of the last row/column removes
612
1016
  * the whole table. All four are grid-aware: inserting across a merge extends it, deleting
613
1017
  * through one narrows it, and deleting a vertical merge's lead row promotes the next row to
614
1018
  * carry it — the grid is never left ragged.
@@ -648,7 +1052,7 @@ var Docxodus = (() => {
648
1052
  return JSON.parse(this.wasm.UnmergeCells(this.handle, cellAnchorId));
649
1053
  }
650
1054
  /**
651
- * Table styling, addressed by a cell-paragraph anchor — the post-insert counterpart of
1055
+ * Table styling, addressed by a canonical `tc` anchor — the post-insert counterpart of
652
1056
  * {@link insertTable}'s options (issue #315 Stage A). `setColumnWidths` retunes `w:tblGrid` +
653
1057
  * every row's cell width (one positive twip value per column) and pins the table to fixed
654
1058
  * layout, exactly as inserting with explicit `columnWidths` would.
@@ -688,6 +1092,19 @@ var Docxodus = (() => {
688
1092
  this.wasm.SetRepeatHeaderRow(this.handle, cellAnchorId, repeat)
689
1093
  );
690
1094
  }
1095
+ /** Apply row layout options to the row containing the canonical cell anchor. */
1096
+ setTableRowOptions(cellAnchorId, options) {
1097
+ return JSON.parse(
1098
+ this.wasm.SetTableRowOptions(
1099
+ this.handle,
1100
+ cellAnchorId,
1101
+ options.repeatHeader ?? null,
1102
+ options.allowBreakAcrossPages ?? null,
1103
+ options.heightTwips ?? null,
1104
+ options.heightRule ?? "atLeast"
1105
+ )
1106
+ );
1107
+ }
691
1108
  // ─── Headers / footers / page numbers ────────────────────────────────
692
1109
  /**
693
1110
  * Set the running header story for the section that owns `anchorId` (any body block in that
@@ -782,6 +1199,25 @@ var Docxodus = (() => {
782
1199
  this.wasm.InsertFootnote(this.handle, anchorId, characterOffset, markdown)
783
1200
  );
784
1201
  }
1202
+ /**
1203
+ * Insert a Word-faithful internal cross-reference — a `REF` field targeting an existing
1204
+ * bookmark — at a character offset (issue #545). The field carries a cached result run
1205
+ * (the bookmarked text, or the target's auto-number under `referenceNumber`), so
1206
+ * renderers that do not recompute fields show a faithful snapshot and Word updates it
1207
+ * like a hand-authored cross-reference. A missing or incoherent bookmark fails with
1208
+ * `missing_bookmark_target`.
1209
+ */
1210
+ insertCrossReference(anchorId, characterOffset, bookmarkName, options) {
1211
+ return JSON.parse(
1212
+ this.wasm.InsertCrossReference(
1213
+ this.handle,
1214
+ anchorId,
1215
+ characterOffset,
1216
+ bookmarkName,
1217
+ options ? JSON.stringify(options) : ""
1218
+ )
1219
+ );
1220
+ }
785
1221
  /** Create an endnote — see {@link insertFootnote}; writes the endnotes part and a
786
1222
  * `w:endnoteReference`, and the created definition anchor has kind `en`. */
787
1223
  insertEndnote(anchorId, characterOffset, markdown) {
@@ -873,6 +1309,170 @@ var Docxodus = (() => {
873
1309
  listComments() {
874
1310
  return JSON.parse(this.wasm.ListComments(this.handle));
875
1311
  }
1312
+ listHyperlinks(scopes = 63 /* All */) {
1313
+ return JSON.parse(this.wasm.ListHyperlinks(this.handle, scopes));
1314
+ }
1315
+ addHyperlink(anchorId, span, kind, target) {
1316
+ return JSON.parse(this.wasm.AddHyperlink(
1317
+ this.handle,
1318
+ anchorId,
1319
+ span.start,
1320
+ span.length,
1321
+ kind,
1322
+ target
1323
+ ));
1324
+ }
1325
+ updateHyperlink(hyperlinkId, kind, target) {
1326
+ return JSON.parse(this.wasm.UpdateHyperlink(this.handle, hyperlinkId, kind, target));
1327
+ }
1328
+ removeHyperlink(hyperlinkId) {
1329
+ return JSON.parse(this.wasm.RemoveHyperlink(this.handle, hyperlinkId));
1330
+ }
1331
+ /** Versioned operational facts for native image inspection/mutation in this runtime. */
1332
+ getImageCapabilities() {
1333
+ return JSON.parse(this.wasm.GetImageCapabilities());
1334
+ }
1335
+ listImages(scopes = 63 /* All */) {
1336
+ return JSON.parse(this.wasm.ListImages(this.handle, scopes));
1337
+ }
1338
+ insertImage(anchorId, characterOffset, bytes, options = {}) {
1339
+ return JSON.parse(this.wasm.InsertImage(
1340
+ this.handle,
1341
+ anchorId,
1342
+ characterOffset,
1343
+ imageBytesToBase64(bytes),
1344
+ JSON.stringify(options)
1345
+ ));
1346
+ }
1347
+ replaceImage(imageId, bytes) {
1348
+ return JSON.parse(this.wasm.ReplaceImage(
1349
+ this.handle,
1350
+ imageId,
1351
+ imageBytesToBase64(bytes)
1352
+ ));
1353
+ }
1354
+ setImageDimensions(imageId, dimensions) {
1355
+ return JSON.parse(this.wasm.SetImageDimensions(
1356
+ this.handle,
1357
+ imageId,
1358
+ JSON.stringify(dimensions)
1359
+ ));
1360
+ }
1361
+ setImageMetadata(imageId, altText, title) {
1362
+ return JSON.parse(this.wasm.SetImageMetadata(
1363
+ this.handle,
1364
+ imageId,
1365
+ altText,
1366
+ title
1367
+ ));
1368
+ }
1369
+ setImageFloatingLayout(imageId, layout) {
1370
+ return JSON.parse(this.wasm.SetImageFloatingLayout(
1371
+ this.handle,
1372
+ imageId,
1373
+ JSON.stringify(layout)
1374
+ ));
1375
+ }
1376
+ removeImage(imageId) {
1377
+ return JSON.parse(this.wasm.RemoveImage(this.handle, imageId));
1378
+ }
1379
+ /** Native Word structured-document tags, in outer-before-inner story order. */
1380
+ listContentControls(scopes = 63 /* All */) {
1381
+ return JSON.parse(this.wasm.ListContentControls(this.handle, scopes));
1382
+ }
1383
+ fillContentControlText(anchorId, text, options = {}) {
1384
+ return JSON.parse(this.wasm.FillContentControlText(
1385
+ this.handle,
1386
+ anchorId,
1387
+ text,
1388
+ JSON.stringify(options)
1389
+ ));
1390
+ }
1391
+ fillContentControlRichText(anchorId, markdown, options = {}) {
1392
+ return JSON.parse(this.wasm.FillContentControlRichText(
1393
+ this.handle,
1394
+ anchorId,
1395
+ markdown,
1396
+ JSON.stringify(options)
1397
+ ));
1398
+ }
1399
+ setContentControlChecked(anchorId, isChecked, options = {}) {
1400
+ return JSON.parse(this.wasm.SetContentControlChecked(
1401
+ this.handle,
1402
+ anchorId,
1403
+ isChecked,
1404
+ JSON.stringify(options)
1405
+ ));
1406
+ }
1407
+ setContentControlDate(anchorId, value, displayText, options = {}) {
1408
+ const timestamp = value instanceof Date ? value.toISOString() : value;
1409
+ return JSON.parse(this.wasm.SetContentControlDate(
1410
+ this.handle,
1411
+ anchorId,
1412
+ timestamp,
1413
+ displayText ?? null,
1414
+ JSON.stringify(options)
1415
+ ));
1416
+ }
1417
+ selectContentControlItem(anchorId, value, options = {}) {
1418
+ return JSON.parse(this.wasm.SelectContentControlItem(
1419
+ this.handle,
1420
+ anchorId,
1421
+ value,
1422
+ JSON.stringify(options)
1423
+ ));
1424
+ }
1425
+ fillContentControlPicture(anchorId, bytes, options = {}) {
1426
+ return JSON.parse(this.wasm.FillContentControlPicture(
1427
+ this.handle,
1428
+ anchorId,
1429
+ imageBytesToBase64(bytes),
1430
+ JSON.stringify(options)
1431
+ ));
1432
+ }
1433
+ addRepeatingSectionItem(sectionAnchorId, afterItemAnchorId, options = {}) {
1434
+ return JSON.parse(this.wasm.AddRepeatingSectionItem(
1435
+ this.handle,
1436
+ sectionAnchorId,
1437
+ afterItemAnchorId ?? "",
1438
+ JSON.stringify(options)
1439
+ ));
1440
+ }
1441
+ removeRepeatingSectionItem(itemAnchorId) {
1442
+ return JSON.parse(this.wasm.RemoveRepeatingSectionItem(
1443
+ this.handle,
1444
+ itemAnchorId
1445
+ ));
1446
+ }
1447
+ listBookmarks(scopes = 63 /* All */) {
1448
+ return JSON.parse(this.wasm.ListBookmarks(this.handle, scopes));
1449
+ }
1450
+ addBookmark(name, range) {
1451
+ return JSON.parse(this.wasm.AddBookmark(
1452
+ this.handle,
1453
+ name,
1454
+ range.startAnchorId,
1455
+ range.startOffset,
1456
+ range.endAnchorId,
1457
+ range.endOffset
1458
+ ));
1459
+ }
1460
+ renameBookmark(name, newName) {
1461
+ return JSON.parse(this.wasm.RenameBookmark(this.handle, name, newName));
1462
+ }
1463
+ moveBookmark(name, range) {
1464
+ return JSON.parse(this.wasm.MoveBookmark(
1465
+ this.handle,
1466
+ name,
1467
+ range.startAnchorId,
1468
+ range.startOffset,
1469
+ range.endAnchorId,
1470
+ range.endOffset
1471
+ ));
1472
+ }
1473
+ removeBookmark(name) {
1474
+ return JSON.parse(this.wasm.RemoveBookmark(this.handle, name));
1475
+ }
876
1476
  // ─── Tracked revisions (issue #318) ──────────────────────────────────
877
1477
  /** Markup-native tracked-revision listing, in document order across body, headers,
878
1478
  * footers, footnotes, and endnotes. Ids are stable while the underlying markup
@@ -893,6 +1493,22 @@ var Docxodus = (() => {
893
1493
  rejectRevision(revisionId) {
894
1494
  return JSON.parse(this.wasm.RejectRevision(this.handle, revisionId));
895
1495
  }
1496
+ /**
1497
+ * Accept every live revision as one undoable session mutation.
1498
+ *
1499
+ * Fails closed: an unsupported, malformed, or ambiguous registry entry aborts the whole
1500
+ * operation (`revisionUnsupported`/`revisionMalformed`/`revisionAmbiguous`) and nothing is
1501
+ * mutated. There is no force mode — call {@link listRevisions} and read each entry's
1502
+ * `diagnostic` to see what blocks it.
1503
+ */
1504
+ acceptAllRevisions() {
1505
+ return JSON.parse(this.wasm.AcceptAllRevisions(this.handle));
1506
+ }
1507
+ /** Reject every live revision as one undoable session mutation. Fails closed exactly like
1508
+ * {@link acceptAllRevisions}. */
1509
+ rejectAllRevisions() {
1510
+ return JSON.parse(this.wasm.RejectAllRevisions(this.handle));
1511
+ }
896
1512
  // ─── Tier C: formatting ──────────────────────────────────────────────
897
1513
  applyFormat(anchorId, span, op) {
898
1514
  const spanJson = span ? JSON.stringify(span) : "";
@@ -998,6 +1614,10 @@ var Docxodus = (() => {
998
1614
  * replaces it with `replace`, preserving the surrounding run formatting that
999
1615
  * the match didn't touch. Returns one `EditResult` per attempted match.
1000
1616
  *
1617
+ * A `find` that matches nothing fails with a single `text_not_found` result
1618
+ * naming the anchor and the needle — never an empty array. Pass
1619
+ * `expectedMatchCount: 0` to assert absence as a successful no-op instead.
1620
+ *
1001
1621
  * Run-formatting contract: the replacement text inherits the formatting of
1002
1622
  * the FIRST run the match spanned. Middle/trailing runs keep their `w:rPr`
1003
1623
  * but lose the slice of text the match consumed.
@@ -1174,10 +1794,16 @@ var Docxodus = (() => {
1174
1794
  *
1175
1795
  * @see docs/architecture/docx_mutation_api.md#findplaceholders
1176
1796
  */
1177
- findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char) {
1178
- return JSON.parse(
1179
- this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary)
1180
- );
1797
+ findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char, citation) {
1798
+ const json = citation ? this.wasm.FindPlaceholdersWithCitations(
1799
+ this.handle,
1800
+ kinds,
1801
+ scope,
1802
+ contextChars,
1803
+ boundary,
1804
+ JSON.stringify(citation)
1805
+ ) : this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary);
1806
+ return JSON.parse(json);
1181
1807
  }
1182
1808
  /**
1183
1809
  * Returns a snapshot of edit-state introspection signals — placeholder counts,
@@ -1200,6 +1826,23 @@ var Docxodus = (() => {
1200
1826
  }
1201
1827
  return raw;
1202
1828
  }
1829
+ /**
1830
+ * Return stable semantic changes from the package opened for this session to
1831
+ * its current state. Requires `captureInitialProjection` (enabled by default).
1832
+ */
1833
+ getSemanticChanges() {
1834
+ return JSON.parse(this.wasm.GetSemanticChanges(this.handle));
1835
+ }
1836
+ /**
1837
+ * Run the default deliverable gate on this session's normal clean-save checkpoint.
1838
+ * With initial projection capture enabled (the default), exact opening bytes are the
1839
+ * baseline used for dispositions and semantic/package deltas.
1840
+ */
1841
+ verifyDeliverable() {
1842
+ return JSON.parse(
1843
+ this.wasm.VerifyDeliverable(this.handle)
1844
+ );
1845
+ }
1203
1846
  // ─── Annotation-based anchor discovery (#132) ────────────────────────
1204
1847
  /**
1205
1848
  * Resolves an annotation's range to the block-level markdown anchors covering
@@ -1215,8 +1858,9 @@ var Docxodus = (() => {
1215
1858
  *
1216
1859
  * @see docs/architecture/docx_mutation_api.md#findbyannotation
1217
1860
  */
1218
- findByAnnotation(annotationId) {
1219
- return JSON.parse(this.wasm.FindByAnnotation(this.handle, annotationId));
1861
+ findByAnnotation(annotationId, citation) {
1862
+ const json = citation ? this.wasm.FindByAnnotationWithCitations(this.handle, annotationId, JSON.stringify(citation)) : this.wasm.FindByAnnotation(this.handle, annotationId);
1863
+ return JSON.parse(json);
1220
1864
  }
1221
1865
  /**
1222
1866
  * Finds every annotation whose `labelId` matches and resolves each of their
@@ -1225,8 +1869,9 @@ var Docxodus = (() => {
1225
1869
  * annotations on different paragraphs become three entries). Annotations
1226
1870
  * whose bookmark resolves to no anchors are omitted from the result.
1227
1871
  */
1228
- findByLabel(labelId) {
1229
- return JSON.parse(this.wasm.FindByLabel(this.handle, labelId));
1872
+ findByLabel(labelId, citation) {
1873
+ const json = citation ? this.wasm.FindByLabelWithCitations(this.handle, labelId, JSON.stringify(citation)) : this.wasm.FindByLabel(this.handle, labelId);
1874
+ return JSON.parse(json);
1230
1875
  }
1231
1876
  /**
1232
1877
  * Resolves any bookmark in the main document part (Docxodus-managed or
@@ -1234,8 +1879,9 @@ var Docxodus = (() => {
1234
1879
  * order. Empty when the bookmark name is unknown. Use this for raw bookmark
1235
1880
  * names that didn't come from the annotation system.
1236
1881
  */
1237
- findByBookmark(bookmarkName) {
1238
- return JSON.parse(this.wasm.FindByBookmark(this.handle, bookmarkName));
1882
+ findByBookmark(bookmarkName, citation) {
1883
+ const json = citation ? this.wasm.FindByBookmarkWithCitations(this.handle, bookmarkName, JSON.stringify(citation)) : this.wasm.FindByBookmark(this.handle, bookmarkName);
1884
+ return JSON.parse(json);
1239
1885
  }
1240
1886
  // ─── Text/kind-based anchor discovery (#171) ─────────────────────────
1241
1887
  /**
@@ -1280,15 +1926,25 @@ var Docxodus = (() => {
1280
1926
  );
1281
1927
  }
1282
1928
  /**
1283
- * Return every anchor of the given `kind` (`"p"`, `"h"`, `"li"`, `"tbl"`,
1284
- * `"row"`, `"cell"`, …), in document order. Reads the projection's anchor
1929
+ * Return every anchor of the given `kind` — one of `"p"`, `"h"`, `"li"`,
1930
+ * `"tbl"`, `"tr"`, `"tc"`, `"col"`, `"sdt"`, `"sec"`, `"fn"`, `"en"`,
1931
+ * `"cmt"`, `"unk"` — in document order. Matching is an exact string comparison;
1932
+ * in particular the row and cell tokens are `"tr"` and `"tc"`, never
1933
+ * `"row"`/`"cell"`. Unknown strings are not parsed or rejected: they simply
1934
+ * return an empty array. `"img"` and `"drw"` likewise return empty because the
1935
+ * projection never assigns those reserved kinds; address images through the
1936
+ * image surface instead. Reads the projection's anchor
1285
1937
  * index directly — no text scan. Pass `scope` (e.g. `"body"`) to restrict to
1286
1938
  * a single part; omit it to span all scopes.
1287
1939
  */
1288
- findByKind(kind, scope) {
1289
- return JSON.parse(
1290
- this.wasm.FindByKind(this.handle, kind, scope ?? "")
1291
- );
1940
+ findByKind(kind, scope, citation) {
1941
+ const json = citation ? this.wasm.FindByKindWithCitations(
1942
+ this.handle,
1943
+ kind,
1944
+ scope ?? "",
1945
+ JSON.stringify(citation)
1946
+ ) : this.wasm.FindByKind(this.handle, kind, scope ?? "");
1947
+ return JSON.parse(json);
1292
1948
  }
1293
1949
  /**
1294
1950
  * Look up a single anchor's preview info — `{ id, kind, scope, textPreview }`.
@@ -1342,6 +1998,18 @@ var Docxodus = (() => {
1342
1998
  const raw = this.wasm.GetSectionInfo(this.handle, anchorId);
1343
1999
  return JSON.parse(raw);
1344
2000
  }
2001
+ /** Enumerate the document's explicit style catalog with resolved high-signal properties. */
2002
+ listStyles() {
2003
+ return JSON.parse(this.wasm.ListStyles(this.handle));
2004
+ }
2005
+ /** Inspect direct and effective paragraph/run formatting for one paragraph anchor. */
2006
+ getFormatting(anchorId) {
2007
+ return JSON.parse(this.wasm.GetFormatting(this.handle, anchorId));
2008
+ }
2009
+ /** Enumerate text-bearing runs as mutation-compatible anchor/span pairs. */
2010
+ listInlineSpans(anchorId) {
2011
+ return JSON.parse(this.wasm.ListInlineSpans(this.handle, anchorId));
2012
+ }
1345
2013
  /**
1346
2014
  * Enumerates every annotation persisted in the document. Lets an agent prime
1347
2015
  * itself with "here are the labeled regions you can target" before committing
@@ -1406,6 +2074,13 @@ var Docxodus = (() => {
1406
2074
  this.close();
1407
2075
  }
1408
2076
  };
2077
+ function imageBytesToBase64(bytes) {
2078
+ let binary = "";
2079
+ for (let offset = 0; offset < bytes.length; offset += 32768) {
2080
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
2081
+ }
2082
+ return globalThis.btoa(binary);
2083
+ }
1409
2084
  function openDocxSession(bytes, wasmExports2, settings) {
1410
2085
  const bridge = wasmExports2.DocxSessionBridge;
1411
2086
  const handle = bridge.OpenSession(bytes, settings ? JSON.stringify(settings) : "");
@@ -1457,17 +2132,41 @@ var Docxodus = (() => {
1457
2132
  function ptToPx(pt) {
1458
2133
  return pt / 0.75;
1459
2134
  }
2135
+ function finiteDataNumber(value, fallback) {
2136
+ if (value === void 0 || value.trim() === "") return fallback;
2137
+ const parsed = Number(value);
2138
+ return Number.isFinite(parsed) ? parsed : fallback;
2139
+ }
2140
+ function positiveDataNumber(value, fallback) {
2141
+ const parsed = finiteDataNumber(value, fallback);
2142
+ return parsed > 0 ? parsed : fallback;
2143
+ }
2144
+ function nonNegativeDataNumber(value, fallback) {
2145
+ const parsed = finiteDataNumber(value, fallback);
2146
+ return parsed >= 0 ? parsed : fallback;
2147
+ }
2148
+ function positiveFinite(value, fallback) {
2149
+ return Number.isFinite(value) && value > 0 ? value : fallback;
2150
+ }
1460
2151
  function parseSectionDimensions(section) {
1461
- const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
1462
- const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
1463
- const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
1464
- const contentHeight = parseFloat(section.dataset.contentHeight || "") || pageHeight - 2 * DEFAULT_MARGIN;
1465
- const marginTop = parseFloat(section.dataset.marginTop || "") || DEFAULT_MARGIN;
1466
- const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
1467
- const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
1468
- const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
1469
- const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1470
- const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
2152
+ const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
2153
+ const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
2154
+ const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
2155
+ const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
2156
+ const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
2157
+ const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
2158
+ const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
2159
+ const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
2160
+ const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
2161
+ const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
2162
+ const headerDistance = nonNegativeDataNumber(
2163
+ section.dataset.headerHeight,
2164
+ DEFAULT_HEADER_FOOTER_DISTANCE
2165
+ );
2166
+ const footerDistance = nonNegativeDataNumber(
2167
+ section.dataset.footerHeight,
2168
+ DEFAULT_HEADER_FOOTER_DISTANCE
2169
+ );
1471
2170
  return {
1472
2171
  pageWidth,
1473
2172
  pageHeight,
@@ -1547,8 +2246,93 @@ var Docxodus = (() => {
1547
2246
  function samePageBox(a, b) {
1548
2247
  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;
1549
2248
  }
2249
+ function createUnavailablePageMap(documentVersion, rendererFingerprint, mode = "continuous") {
2250
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
2251
+ return {
2252
+ schemaVersion: 1,
2253
+ mode,
2254
+ availability: "unavailable",
2255
+ documentVersion,
2256
+ rendererFingerprint,
2257
+ pages: [],
2258
+ fragments: []
2259
+ };
2260
+ }
2261
+ var activePageCitationHighlights = /* @__PURE__ */ new WeakMap();
2262
+ function clearPageCitationHighlight(root) {
2263
+ const active = activePageCitationHighlights.get(root);
2264
+ if (!active) return;
2265
+ if (active.highlightClass && active.addedClass) {
2266
+ active.target.classList.remove(active.highlightClass);
2267
+ }
2268
+ for (const style of active.inlineStyles ?? []) {
2269
+ if (style.value) {
2270
+ active.target.style.setProperty(style.name, style.value, style.priority);
2271
+ } else {
2272
+ active.target.style.removeProperty(style.name);
2273
+ }
2274
+ }
2275
+ activePageCitationHighlights.delete(root);
2276
+ }
2277
+ function navigateToPageCitation(root, citation, options = {}) {
2278
+ clearPageCitationHighlight(root);
2279
+ if (citation.availability !== "available" || citation.fragments.length === 0) {
2280
+ return { navigated: false, unavailableReason: "citation_unavailable" };
2281
+ }
2282
+ const byAttribute = (name, value, within = root) => {
2283
+ for (const node of Array.from(within.querySelectorAll(`[${name}]`))) {
2284
+ if (node.getAttribute(name) === value) return node;
2285
+ }
2286
+ return null;
2287
+ };
2288
+ const fragment = citation.fragments[0];
2289
+ let target = byAttribute("data-page-fragment-id", fragment.fragmentId);
2290
+ if (!target) {
2291
+ const page = byAttribute("data-page-number", String(fragment.pageNumber));
2292
+ if (page) target = byAttribute("data-source-anchor-id", citation.anchorId, page) ?? page;
2293
+ }
2294
+ if (!target) {
2295
+ return {
2296
+ navigated: false,
2297
+ pageNumber: fragment.pageNumber,
2298
+ fragmentId: fragment.fragmentId,
2299
+ unavailableReason: "fragment_not_found"
2300
+ };
2301
+ }
2302
+ if (options.highlightClass) {
2303
+ const addedClass = !target.classList.contains(options.highlightClass);
2304
+ target.classList.add(options.highlightClass);
2305
+ activePageCitationHighlights.set(root, {
2306
+ target,
2307
+ highlightClass: options.highlightClass,
2308
+ addedClass
2309
+ });
2310
+ } else if (options.highlight !== false) {
2311
+ const names = ["outline", "outline-offset", "background-color"];
2312
+ const inlineStyles = names.map((name) => ({
2313
+ name,
2314
+ value: target.style.getPropertyValue(name),
2315
+ priority: target.style.getPropertyPriority(name)
2316
+ }));
2317
+ target.style.setProperty("outline", "3px solid #f4b400", "important");
2318
+ target.style.setProperty("outline-offset", "2px", "important");
2319
+ target.style.setProperty("background-color", "rgba(255, 235, 59, .18)", "important");
2320
+ activePageCitationHighlights.set(root, { target, inlineStyles });
2321
+ }
2322
+ target.scrollIntoView({
2323
+ behavior: options.behavior ?? "smooth",
2324
+ block: options.block ?? "center"
2325
+ });
2326
+ return {
2327
+ navigated: true,
2328
+ target,
2329
+ pageNumber: fragment.pageNumber,
2330
+ fragmentId: fragment.fragmentId
2331
+ };
2332
+ }
1550
2333
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
1551
- var MIN_BODY_CONTENT_HEIGHT = 72;
2334
+ var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
2335
+ var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
1552
2336
  var PaginationEngine = class {
1553
2337
  /**
1554
2338
  * Creates a new pagination engine.
@@ -1558,24 +2342,46 @@ var Docxodus = (() => {
1558
2342
  * @param options - Pagination options
1559
2343
  */
1560
2344
  constructor(staging, container, options = {}) {
2345
+ this.createdPageCount = 0;
2346
+ this.footnoteSeparator = null;
2347
+ this.footnoteContinuationSeparator = null;
2348
+ this.footnoteLayoutLikeWord8 = false;
1561
2349
  this.pendingFootnoteContinuation = null;
1562
2350
  /** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
1563
2351
  this.pageNumbering = /* @__PURE__ */ new Map();
1564
- this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
1565
- this.containerElement = typeof container === "string" ? document.getElementById(container) : container;
2352
+ this.lastPages = [];
2353
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
2354
+ this.state = "ready";
2355
+ const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
2356
+ this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
2357
+ this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
1566
2358
  if (!this.stagingElement) {
1567
2359
  throw new Error("Staging element not found");
1568
2360
  }
1569
2361
  if (!this.containerElement) {
1570
2362
  throw new Error("Container element not found");
1571
2363
  }
2364
+ if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
2365
+ throw new Error("Staging and container elements must belong to the same document");
2366
+ }
2367
+ const view = ownerDocument.defaultView;
2368
+ if (!view) {
2369
+ throw new Error("Pagination requires an attached document with a defaultView");
2370
+ }
2371
+ this.document = ownerDocument;
2372
+ this.view = view;
1572
2373
  this.scale = options.scale ?? 1;
1573
2374
  this.cssPrefix = options.cssPrefix ?? "page-";
1574
2375
  this.showPageNumbers = options.showPageNumbers ?? true;
1575
2376
  this.pageGap = options.pageGap ?? 20;
1576
2377
  this.fragmentParagraphs = options.fragmentParagraphs ?? false;
2378
+ this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
2379
+ this.cancellationCheckpoint = options.checkCancellation;
2380
+ this.pageCountCheckpoint = options.checkPageCount;
2381
+ this.layoutToken = options.layoutToken;
1577
2382
  this.hfRegistry = /* @__PURE__ */ new Map();
1578
2383
  this.footnoteRegistry = /* @__PURE__ */ new Map();
2384
+ this.commentMarginRegistry = /* @__PURE__ */ new Map();
1579
2385
  }
1580
2386
  /**
1581
2387
  * Runs the pagination process.
@@ -1583,58 +2389,469 @@ var Docxodus = (() => {
1583
2389
  * @returns PaginationResult with page information
1584
2390
  */
1585
2391
  paginate() {
1586
- const pages = [];
1587
- let pageNumber = 1;
1588
- this.hfRegistry = this.parseHeaderFooterRegistry();
1589
- this.footnoteRegistry = this.parseFootnoteRegistry();
1590
- const sections = this.stagingElement.querySelectorAll(
1591
- "[data-section-index]"
1592
- );
1593
- this.pageNumbering = this.parsePageNumbering(sections);
1594
- const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
1595
- const runs = [];
1596
- for (const section of sectionsToProcess) {
1597
- const dims = parseSectionDimensions(section);
1598
- const previous = runs[runs.length - 1];
1599
- if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
1600
- previous.sections.push(section);
1601
- } else {
1602
- runs.push({
1603
- sections: [section],
1604
- sectionIndex: parseInt(section.dataset.sectionIndex || "0", 10),
1605
- dims
2392
+ this.checkpoint();
2393
+ if (this.state !== "ready") {
2394
+ throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
2395
+ }
2396
+ if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
2397
+ throw new Error("Pagination requires staging and container elements attached to their document");
2398
+ }
2399
+ if (this.document.documentElement.getBoundingClientRect().width <= 0) {
2400
+ throw new Error("Pagination requires a browsing context with non-zero layout");
2401
+ }
2402
+ this.state = "running";
2403
+ try {
2404
+ const pages = [];
2405
+ let pageNumber = 1;
2406
+ this.hfRegistry = this.parseHeaderFooterRegistry();
2407
+ this.footnoteRegistry = this.parseFootnoteRegistry();
2408
+ ({
2409
+ normal: this.footnoteSeparator,
2410
+ continuation: this.footnoteContinuationSeparator
2411
+ } = this.parseFootnoteSeparators());
2412
+ this.commentMarginRegistry = this.parseCommentMarginRegistry();
2413
+ this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
2414
+ const sections = this.stagingElement.querySelectorAll(
2415
+ "[data-section-index]"
2416
+ );
2417
+ this.pageNumbering = this.parsePageNumbering(sections);
2418
+ const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
2419
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
2420
+ const referencedFootnoteIds = /* @__PURE__ */ new Set();
2421
+ const referencedCommentIds = /* @__PURE__ */ new Set();
2422
+ for (const section of sectionsToProcess) {
2423
+ this.checkpoint();
2424
+ this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
2425
+ for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
2426
+ this.checkpoint();
2427
+ if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
2428
+ const id = reference.dataset.footnoteId;
2429
+ if (id) referencedFootnoteIds.add(id);
2430
+ }
2431
+ for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
2432
+ this.checkpoint();
2433
+ if (reference.closest(
2434
+ "#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
2435
+ )) continue;
2436
+ const id = reference.dataset.commentId;
2437
+ if (id) referencedCommentIds.add(id);
2438
+ }
2439
+ }
2440
+ for (const id of referencedFootnoteIds) {
2441
+ this.checkpoint();
2442
+ const source = this.footnoteRegistry.get(id);
2443
+ if (!source) continue;
2444
+ this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
2445
+ for (const reference of Array.from(
2446
+ source.querySelectorAll("[data-comment-id]")
2447
+ )) {
2448
+ this.checkpoint();
2449
+ const commentId = reference.dataset.commentId;
2450
+ if (commentId) referencedCommentIds.add(commentId);
2451
+ }
2452
+ }
2453
+ for (const id of referencedCommentIds) {
2454
+ this.checkpoint();
2455
+ const source = this.commentMarginRegistry.get(id);
2456
+ if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
2457
+ }
2458
+ const runs = [];
2459
+ for (const section of sectionsToProcess) {
2460
+ this.checkpoint();
2461
+ const dims = parseSectionDimensions(section);
2462
+ const previous = runs[runs.length - 1];
2463
+ if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
2464
+ previous.sections.push(section);
2465
+ previous.sectionDimensions.set(
2466
+ parseInt(section.dataset.sectionIndex || "0", 10),
2467
+ dims
2468
+ );
2469
+ } else {
2470
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
2471
+ runs.push({
2472
+ sections: [section],
2473
+ sectionIndex,
2474
+ sectionType: section.dataset.sectionType ?? "nextPage",
2475
+ dims,
2476
+ sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
2477
+ });
2478
+ }
2479
+ }
2480
+ for (const run of runs) {
2481
+ this.checkpoint();
2482
+ const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
2483
+ if (needsParityFiller) {
2484
+ const precedingPage = pages[pages.length - 1];
2485
+ const precedingPageInSection = parseInt(
2486
+ precedingPage.element.dataset.pageInSection ?? "1",
2487
+ 10
2488
+ );
2489
+ const precedingDisplayedPageNumber = parseInt(
2490
+ precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
2491
+ 10
2492
+ );
2493
+ const filler = this.createPage(
2494
+ precedingPage.dimensions,
2495
+ pageNumber,
2496
+ precedingPage.sectionIndex,
2497
+ precedingDisplayedPageNumber + 1,
2498
+ [],
2499
+ precedingPageInSection + 1,
2500
+ [],
2501
+ 0,
2502
+ null,
2503
+ void 0,
2504
+ true
2505
+ );
2506
+ pages.push(filler);
2507
+ pageNumber++;
2508
+ }
2509
+ this.stagingElement.style.visibility = "hidden";
2510
+ this.stagingElement.style.position = "absolute";
2511
+ this.stagingElement.style.left = "-9999px";
2512
+ this.stagingElement.style.display = "block";
2513
+ const blocks = [];
2514
+ for (const section of run.sections) {
2515
+ this.checkpoint();
2516
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
2517
+ const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
2518
+ section.style.width = `${sectionDims.contentWidth}pt`;
2519
+ const columnCount = parseInt(section.dataset.cols || "1", 10);
2520
+ if (columnCount > 1) {
2521
+ const gap = parseFloat(section.dataset.colGap || "");
2522
+ blocks.push(...this.buildColumnBlocks(
2523
+ section,
2524
+ sectionDims,
2525
+ sectionIndex,
2526
+ columnCount,
2527
+ Number.isFinite(gap) ? gap : 36
2528
+ ));
2529
+ } else {
2530
+ blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
2531
+ }
2532
+ }
2533
+ const sectionPages = this.flowToPages(
2534
+ blocks,
2535
+ run.dims,
2536
+ pageNumber,
2537
+ run.sectionIndex,
2538
+ run.sectionDimensions
2539
+ );
2540
+ this.checkpoint();
2541
+ pages.push(...sectionPages);
2542
+ pageNumber += sectionPages.length;
2543
+ }
2544
+ this.stagingElement.style.display = "none";
2545
+ this.substitutePageNumberFields(pages.length);
2546
+ for (const page of pages) {
2547
+ this.checkpoint();
2548
+ if (page.element.dataset.sectionFiller === "true") continue;
2549
+ const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
2550
+ const displayedPageNumber = parseInt(
2551
+ page.element.dataset.displayedPageNumber || String(page.pageNumber),
2552
+ 10
2553
+ );
2554
+ const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
2555
+ const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
2556
+ if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
2557
+ if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
2558
+ }
2559
+ this.qualifyPageFragments(pages);
2560
+ this.transferVisibleFragmentTargets();
2561
+ if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
2562
+ this.lastPages = pages;
2563
+ const result = {
2564
+ totalPages: pages.length,
2565
+ pages,
2566
+ pageMap: this.layoutToken ? this.materializePageMap(
2567
+ this.layoutToken.documentVersion,
2568
+ this.layoutToken.rendererFingerprint
2569
+ ) : void 0
2570
+ };
2571
+ this.state = "complete";
2572
+ return result;
2573
+ } catch (error) {
2574
+ this.state = "failed";
2575
+ throw error;
2576
+ }
2577
+ }
2578
+ checkpoint() {
2579
+ this.cancellationCheckpoint?.();
2580
+ }
2581
+ /**
2582
+ * Normalize visible fragment identities after a caller applies final standalone styles. This
2583
+ * deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
2584
+ * measurement cannot mutate a tree after it was declared stable.
2585
+ */
2586
+ normalizePageMapFragmentIdentities() {
2587
+ if (this.lastPages.length === 0) {
2588
+ throw new Error("paginate() must complete before fragment identities can be normalized");
2589
+ }
2590
+ this.normalizeVisiblePageFragments(this.lastPages);
2591
+ }
2592
+ /**
2593
+ * Materialize the last completed browser layout as portable page-relative point geometry.
2594
+ * The caller supplies both invalidation tokens; this engine never guesses a document version
2595
+ * or renderer fingerprint.
2596
+ */
2597
+ materializePageMap(documentVersion, rendererFingerprint) {
2598
+ if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
2599
+ throw new Error("documentVersion must be a non-negative safe integer");
2600
+ }
2601
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
2602
+ if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
2603
+ const pages = this.lastPages.map((page) => ({
2604
+ pageNumber: page.pageNumber,
2605
+ pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
2606
+ width: page.dimensions.pageWidth,
2607
+ height: page.dimensions.pageHeight,
2608
+ sectionIndex: page.sectionIndex,
2609
+ pageName: `docxodus-section-${page.sectionIndex}`
2610
+ }));
2611
+ const fragments = [];
2612
+ const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
2613
+ if (requiredAnchorIds.size === 0) {
2614
+ throw new Error("cannot publish an available PageMap without canonical source inventory");
2615
+ }
2616
+ const measuredAnchorIds = /* @__PURE__ */ new Set();
2617
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
2618
+ for (const page of this.lastPages) {
2619
+ this.checkpoint();
2620
+ const pageRect = page.element.getBoundingClientRect();
2621
+ if (pageRect.width <= 0 || pageRect.height <= 0) {
2622
+ throw new Error(`page ${page.pageNumber} has no measurable geometry`);
2623
+ }
2624
+ const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
2625
+ const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
2626
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
2627
+ for (const element of Array.from(nodes)) {
2628
+ this.checkpoint();
2629
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
2630
+ const anchorId = element.dataset.sourceAnchorId;
2631
+ if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
2632
+ throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
2633
+ }
2634
+ const rect = element.getBoundingClientRect();
2635
+ const style = this.view.getComputedStyle(element);
2636
+ const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
2637
+ if (deliberatelyHidden) continue;
2638
+ requiredAnchorIds.add(anchorId);
2639
+ const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
2640
+ const left2 = visibleRect.left;
2641
+ const top2 = visibleRect.top;
2642
+ const right2 = visibleRect.right;
2643
+ const bottom2 = visibleRect.bottom;
2644
+ if (rect.width <= 0 || rect.height <= 0 || right2 <= left2 || bottom2 <= top2) {
2645
+ continue;
2646
+ }
2647
+ measuredAnchorIds.add(anchorId);
2648
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
2649
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
2650
+ const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2651
+ if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
2652
+ throw new Error(
2653
+ `page ${page.pageNumber} fragment identity changed after final-tree normalization`
2654
+ );
2655
+ }
2656
+ fragments.push({
2657
+ fragmentId,
2658
+ anchorId,
2659
+ fragmentIndex,
2660
+ pageNumber: page.pageNumber,
2661
+ geometry: {
2662
+ x: (left2 - pageRect.left) * pointPerRenderedX,
2663
+ y: (top2 - pageRect.top) * pointPerRenderedY,
2664
+ width: (right2 - left2) * pointPerRenderedX,
2665
+ height: (bottom2 - top2) * pointPerRenderedY
2666
+ },
2667
+ story: this.storyForCanonicalAnchor(anchorId),
2668
+ inTableCell: element.matches("td,th") || element.closest("td,th") !== null
1606
2669
  });
1607
2670
  }
1608
2671
  }
1609
- for (const run of runs) {
1610
- this.stagingElement.style.visibility = "hidden";
1611
- this.stagingElement.style.position = "absolute";
1612
- this.stagingElement.style.left = "-9999px";
1613
- this.stagingElement.style.display = "block";
1614
- const blocks = [];
1615
- for (const section of run.sections) {
1616
- section.style.width = `${run.dims.contentWidth}pt`;
1617
- const columnCount = parseInt(section.dataset.cols || "1", 10);
1618
- if (columnCount > 1) {
1619
- const gap = parseFloat(section.dataset.colGap || "");
1620
- blocks.push(...this.buildColumnBlocks(
1621
- section,
1622
- run.dims,
1623
- run.sectionIndex,
1624
- columnCount,
1625
- Number.isFinite(gap) ? gap : 36
1626
- ));
1627
- } else {
1628
- blocks.push(...this.measureBlocks(section, run.dims));
2672
+ const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
2673
+ if (missingAnchor) {
2674
+ throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
2675
+ }
2676
+ return {
2677
+ schemaVersion: 1,
2678
+ mode: "paginated",
2679
+ availability: "available",
2680
+ documentVersion,
2681
+ rendererFingerprint,
2682
+ pages,
2683
+ fragments
2684
+ };
2685
+ }
2686
+ normalizeVisiblePageFragments(pages) {
2687
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
2688
+ for (const page of pages) {
2689
+ this.checkpoint();
2690
+ const pageRect = page.element.getBoundingClientRect();
2691
+ for (const element of Array.from(
2692
+ page.element.querySelectorAll("[data-source-anchor-id]")
2693
+ )) {
2694
+ this.checkpoint();
2695
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
2696
+ const anchorId = element.dataset.sourceAnchorId;
2697
+ if (!anchorId) continue;
2698
+ const style = this.view.getComputedStyle(element);
2699
+ if (style.display === "none" || style.visibility === "hidden") continue;
2700
+ const rect = element.getBoundingClientRect();
2701
+ const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
2702
+ if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
2703
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
2704
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
2705
+ element.dataset.pageNumber = String(page.pageNumber);
2706
+ element.dataset.fragmentIndex = String(fragmentIndex);
2707
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2708
+ }
2709
+ }
2710
+ }
2711
+ /**
2712
+ * Intersect an element with every ancestor that establishes an overflow clip before the page
2713
+ * root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
2714
+ * therefore must not satisfy PageMap completeness or inflate portable geometry.
2715
+ */
2716
+ intersectWithClippingAncestors(element, page, pageRect, rect) {
2717
+ let left2 = Math.max(rect.left, pageRect.left);
2718
+ let top2 = Math.max(rect.top, pageRect.top);
2719
+ let right2 = Math.min(rect.right, pageRect.right);
2720
+ let bottom2 = Math.min(rect.bottom, pageRect.bottom);
2721
+ const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
2722
+ for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
2723
+ const style = this.view.getComputedStyle(ancestor);
2724
+ const clipsX = clips(style.overflowX);
2725
+ const clipsY = clips(style.overflowY);
2726
+ if (!clipsX && !clipsY) continue;
2727
+ const ancestorRect = ancestor.getBoundingClientRect();
2728
+ if (clipsX) {
2729
+ left2 = Math.max(left2, ancestorRect.left);
2730
+ right2 = Math.min(right2, ancestorRect.right);
2731
+ }
2732
+ if (clipsY) {
2733
+ top2 = Math.max(top2, ancestorRect.top);
2734
+ bottom2 = Math.min(bottom2, ancestorRect.bottom);
2735
+ }
2736
+ }
2737
+ return { left: left2, top: top2, right: right2, bottom: bottom2 };
2738
+ }
2739
+ storyForCanonicalAnchor(anchorId) {
2740
+ const first = anchorId.indexOf(":");
2741
+ const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
2742
+ const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
2743
+ if (scope.startsWith("hdr")) return "header";
2744
+ if (scope.startsWith("ftr")) return "footer";
2745
+ if (scope === "fn") return "footnote";
2746
+ if (scope === "en") return "endnote";
2747
+ if (scope === "cmt") return "comment";
2748
+ return "body";
2749
+ }
2750
+ /**
2751
+ * Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
2752
+ * Registry wrappers are excluded when scanning staging because selectable registry contents are
2753
+ * inventoried separately. Producers may explicitly mark content that has no visual substrate
2754
+ * with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
2755
+ */
2756
+ collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
2757
+ const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
2758
+ candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
2759
+ for (const element of candidates) {
2760
+ if (excludeRegistries && element.closest(
2761
+ "#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
2762
+ )) {
2763
+ continue;
2764
+ }
2765
+ if (this.isZeroHeightExplicitBreakCarrier(element)) {
2766
+ element.dataset.pageMapExclude = "true";
2767
+ }
2768
+ if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
2769
+ const anchorId = element.dataset.sourceAnchorId;
2770
+ if (anchorId) destination.add(anchorId);
2771
+ }
2772
+ }
2773
+ isZeroHeightExplicitBreakCarrier(element) {
2774
+ const next = element.nextElementSibling;
2775
+ const bounds = element.getBoundingClientRect();
2776
+ 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);
2777
+ }
2778
+ isDeliberatelyUnrenderedSource(element, sourceRoot) {
2779
+ for (let current = element; current; current = current.parentElement) {
2780
+ const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
2781
+ 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") {
2782
+ return true;
2783
+ }
2784
+ if (current === sourceRoot) break;
2785
+ }
2786
+ return false;
2787
+ }
2788
+ /**
2789
+ * Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
2790
+ * canonical source identity plus page/fragment qualification instead.
2791
+ */
2792
+ qualifyPageFragments(pages) {
2793
+ const fragmentCounts = /* @__PURE__ */ new Map();
2794
+ const activeCanonicalIds = /* @__PURE__ */ new Set();
2795
+ const activeBareAnchorIds = /* @__PURE__ */ new Set();
2796
+ const makeInactive = (element) => {
2797
+ element.removeAttribute("data-anchor");
2798
+ element.removeAttribute("data-committed-text");
2799
+ if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
2800
+ };
2801
+ for (const page of pages) {
2802
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
2803
+ for (const element of Array.from(nodes)) {
2804
+ const anchorId = element.dataset.sourceAnchorId;
2805
+ if (!anchorId) continue;
2806
+ const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
2807
+ fragmentCounts.set(anchorId, fragmentIndex + 1);
2808
+ element.dataset.pageNumber = String(page.pageNumber);
2809
+ element.dataset.fragmentIndex = String(fragmentIndex);
2810
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
2811
+ const story = this.storyForCanonicalAnchor(anchorId);
2812
+ const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
2813
+ if (element.hasAttribute("data-anchor")) {
2814
+ const bareAnchorId = element.dataset.anchor;
2815
+ if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
2816
+ activeCanonicalIds.add(anchorId);
2817
+ activeBareAnchorIds.add(bareAnchorId);
2818
+ } else {
2819
+ makeInactive(element);
2820
+ }
1629
2821
  }
1630
2822
  }
1631
- const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
1632
- pages.push(...sectionPages);
1633
- pageNumber += sectionPages.length;
1634
2823
  }
1635
- this.stagingElement.style.display = "none";
1636
- this.substitutePageNumberFields(pages.length);
1637
- return { totalPages: pages.length, pages };
2824
+ const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
2825
+ for (const element of Array.from(
2826
+ this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
2827
+ )) {
2828
+ const anchorId = element.dataset.sourceAnchorId;
2829
+ if (!anchorId) continue;
2830
+ const bareAnchorId = element.dataset.anchor;
2831
+ if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
2832
+ makeInactive(element);
2833
+ } else {
2834
+ activeStagingCanonicalIds.add(anchorId);
2835
+ activeBareAnchorIds.add(bareAnchorId);
2836
+ }
2837
+ }
2838
+ }
2839
+ /**
2840
+ * Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
2841
+ * fragment target copied into a visible page would therefore resolve to its earlier hidden
2842
+ * source. Transfer target ownership to the page presentation after flow is complete; registry
2843
+ * and wrapper IDs that have no visible counterpart remain available to pagination internals.
2844
+ */
2845
+ transferVisibleFragmentTargets() {
2846
+ const visibleIds = new Set(Array.from(
2847
+ this.containerElement.querySelectorAll("[id]")
2848
+ ).map((element) => element.id).filter(Boolean));
2849
+ if (visibleIds.size === 0) return;
2850
+ for (const source of Array.from(
2851
+ this.stagingElement.querySelectorAll("[id]")
2852
+ )) {
2853
+ if (visibleIds.has(source.id)) source.removeAttribute("id");
2854
+ }
1638
2855
  }
1639
2856
  /** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
1640
2857
  parsePageNumbering(sections) {
@@ -1678,9 +2895,8 @@ var Docxodus = (() => {
1678
2895
  if (markers.length === 0) continue;
1679
2896
  const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
1680
2897
  const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
1681
- const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
1682
2898
  const numbering = this.pageNumbering.get(sectionIndex) ?? {};
1683
- const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
2899
+ const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
1684
2900
  for (const marker of Array.from(markers)) {
1685
2901
  const kind = marker.dataset.field;
1686
2902
  if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
@@ -1692,17 +2908,26 @@ var Docxodus = (() => {
1692
2908
  /**
1693
2909
  * Measures all content blocks in a section.
1694
2910
  */
1695
- measureBlocks(section, dims) {
2911
+ measureBlocks(section, dims, sectionIndex) {
1696
2912
  const blocks = [];
1697
2913
  const children = Array.from(section.children);
1698
2914
  for (const child of children) {
2915
+ this.checkpoint();
1699
2916
  if (child.dataset.sectionIndex !== void 0) {
1700
- const nestedBlocks = this.measureBlocks(child, dims);
2917
+ const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
2918
+ const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
1701
2919
  blocks.push(...nestedBlocks);
1702
2920
  continue;
1703
2921
  }
2922
+ if (child.matches("section.endnotes")) {
2923
+ const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
2924
+ if (endnoteBlocks) {
2925
+ blocks.push(...endnoteBlocks);
2926
+ continue;
2927
+ }
2928
+ }
1704
2929
  const rect = child.getBoundingClientRect();
1705
- const style = window.getComputedStyle(child);
2930
+ const style = this.view.getComputedStyle(child);
1706
2931
  const marginTopPx = parseFloat(style.marginTop) || 0;
1707
2932
  const marginBottomPx = parseFloat(style.marginBottom) || 0;
1708
2933
  const heightPt = pxToPt(rect.height);
@@ -1711,6 +2936,7 @@ var Docxodus = (() => {
1711
2936
  const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
1712
2937
  blocks.push({
1713
2938
  element: child,
2939
+ sectionIndex,
1714
2940
  heightPt,
1715
2941
  marginTopPt,
1716
2942
  marginBottomPt,
@@ -1723,6 +2949,95 @@ var Docxodus = (() => {
1723
2949
  }
1724
2950
  return blocks;
1725
2951
  }
2952
+ /**
2953
+ * Flatten the converter's `section.endnotes > ol > li > p` presentation into
2954
+ * ordinary paragraph blocks. This preserves paragraph formatting and canonical
2955
+ * p:en/en:en identities while allowing the existing paragraph fragmenter to
2956
+ * split a long endnote across page boundaries.
2957
+ */
2958
+ measureSafeEndnoteBlocks(section, dims, sectionIndex) {
2959
+ const sectionChildren = Array.from(section.children);
2960
+ const list = sectionChildren.find((child) => child.tagName === "OL");
2961
+ if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
2962
+ return null;
2963
+ }
2964
+ const items = Array.from(list.children);
2965
+ if (items.length === 0 || items.some(
2966
+ (item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
2967
+ )) {
2968
+ return null;
2969
+ }
2970
+ const blocks = [];
2971
+ const sectionStyle = this.view.getComputedStyle(section);
2972
+ for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
2973
+ this.checkpoint();
2974
+ const clonedRule = rule.cloneNode(true);
2975
+ if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
2976
+ blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
2977
+ }
2978
+ const listStyle = this.view.getComputedStyle(list);
2979
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
2980
+ this.checkpoint();
2981
+ const item = items[itemIndex];
2982
+ const ownerAnchorId = item.dataset.sourceAnchorId;
2983
+ const paragraphs = Array.from(item.children);
2984
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
2985
+ this.checkpoint();
2986
+ const paragraph = paragraphs[paragraphIndex];
2987
+ if (!this.hasRangeFragmentSafeLayout(paragraph)) {
2988
+ return null;
2989
+ }
2990
+ const clone = paragraph.cloneNode(true);
2991
+ clone.dataset.paginationSafeEndnote = "true";
2992
+ clone.style.fontSize ||= sectionStyle.fontSize;
2993
+ clone.style.lineHeight ||= sectionStyle.lineHeight;
2994
+ clone.style.paddingLeft ||= listStyle.paddingLeft;
2995
+ if (ownerAnchorId && !Array.from(
2996
+ clone.querySelectorAll("[data-source-anchor-id]")
2997
+ ).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
2998
+ const owner = this.document.createElement("span");
2999
+ owner.dataset.sourceAnchorId = ownerAnchorId;
3000
+ while (clone.firstChild) owner.appendChild(clone.firstChild);
3001
+ clone.appendChild(owner);
3002
+ }
3003
+ if (paragraphIndex === 0) {
3004
+ const itemId = item.id;
3005
+ if (itemId) {
3006
+ if (clone.id && clone.id !== itemId) return null;
3007
+ clone.id = itemId;
3008
+ }
3009
+ const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
3010
+ const marker = this.formatOrderedListMarker(
3011
+ Number.isFinite(value) ? value : itemIndex + 1,
3012
+ listStyle.listStyleType
3013
+ );
3014
+ clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
3015
+ }
3016
+ blocks.push(this.measureElement(clone, dims, sectionIndex));
3017
+ }
3018
+ }
3019
+ return blocks;
3020
+ }
3021
+ /** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
3022
+ formatOrderedListMarker(value, listStyleType) {
3023
+ const format = (() => {
3024
+ switch (listStyleType) {
3025
+ case "lower-roman":
3026
+ return "lowerRoman";
3027
+ case "upper-roman":
3028
+ return "upperRoman";
3029
+ case "lower-alpha":
3030
+ case "lower-latin":
3031
+ return "lowerLetter";
3032
+ case "upper-alpha":
3033
+ case "upper-latin":
3034
+ return "upperLetter";
3035
+ default:
3036
+ return "decimal";
3037
+ }
3038
+ })();
3039
+ return formatPageNumber(value, format);
3040
+ }
1726
3041
  /**
1727
3042
  * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
1728
3043
  * blocks. Word lays such a section out as N columns inside the same body extent;
@@ -1740,7 +3055,7 @@ var Docxodus = (() => {
1740
3055
  const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
1741
3056
  const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
1742
3057
  const makeContainer = (slice) => {
1743
- const container = document.createElement("div");
3058
+ const container = this.document.createElement("div");
1744
3059
  container.style.columnCount = String(columnCount);
1745
3060
  container.style.columnGap = `${columnGapPt}pt`;
1746
3061
  for (const child of slice) {
@@ -1750,17 +3065,19 @@ var Docxodus = (() => {
1750
3065
  };
1751
3066
  let start2 = 0;
1752
3067
  while (start2 < children.length) {
3068
+ this.checkpoint();
1753
3069
  if (isBreak(children[start2])) {
1754
- blocks.push(this.measureElement(children[start2], dims));
3070
+ blocks.push(this.measureElement(children[start2], dims, sectionIndex));
1755
3071
  start2++;
1756
3072
  continue;
1757
3073
  }
1758
3074
  let end = start2 + 1;
1759
3075
  let container = makeContainer(children.slice(start2, end));
1760
- let measured = this.measureElement(container, dims);
3076
+ let measured = this.measureElement(container, dims, sectionIndex);
1761
3077
  while (end < children.length && !isBreak(children[end])) {
3078
+ this.checkpoint();
1762
3079
  const candidate = makeContainer(children.slice(start2, end + 1));
1763
- const candidateMeasured = this.measureElement(candidate, dims);
3080
+ const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
1764
3081
  if (candidateMeasured.heightPt > maxFragmentHeight) break;
1765
3082
  container = candidate;
1766
3083
  measured = candidateMeasured;
@@ -1768,6 +3085,7 @@ var Docxodus = (() => {
1768
3085
  }
1769
3086
  blocks.push({
1770
3087
  element: container,
3088
+ sectionIndex,
1771
3089
  heightPt: measured.heightPt,
1772
3090
  marginTopPt: measured.marginTopPt,
1773
3091
  marginBottomPt: measured.marginBottomPt,
@@ -1786,8 +3104,8 @@ var Docxodus = (() => {
1786
3104
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
1787
3105
  * rows because wrapping and collapsed borders change the height of a fragment.
1788
3106
  */
1789
- measureElement(element, dims) {
1790
- const measurementHost = document.createElement("div");
3107
+ measureElement(element, dims, sectionIndex) {
3108
+ const measurementHost = this.document.createElement("div");
1791
3109
  measurementHost.style.position = "absolute";
1792
3110
  measurementHost.style.visibility = "hidden";
1793
3111
  measurementHost.style.left = "-9999px";
@@ -1796,9 +3114,10 @@ var Docxodus = (() => {
1796
3114
  measurementHost.appendChild(measuredElement);
1797
3115
  this.stagingElement.appendChild(measurementHost);
1798
3116
  const rect = measuredElement.getBoundingClientRect();
1799
- const style = window.getComputedStyle(measuredElement);
3117
+ const style = this.view.getComputedStyle(measuredElement);
1800
3118
  const measured = {
1801
3119
  element,
3120
+ sectionIndex,
1802
3121
  heightPt: pxToPt(rect.height),
1803
3122
  marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
1804
3123
  marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
@@ -1892,6 +3211,7 @@ var Docxodus = (() => {
1892
3211
  const knownIds = new Set(existingFootnoteIds);
1893
3212
  const newIds = [];
1894
3213
  for (const block of blocks) {
3214
+ this.checkpoint();
1895
3215
  for (const id of this.extractFootnoteRefs(block.element)) {
1896
3216
  if (!knownIds.has(id)) {
1897
3217
  knownIds.add(id);
@@ -1909,8 +3229,8 @@ var Docxodus = (() => {
1909
3229
  smallestEffectiveContentHeight(dims, sectionIndex) {
1910
3230
  return Math.min(
1911
3231
  this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
1912
- this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
1913
- this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
3232
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
3233
+ this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
1914
3234
  );
1915
3235
  }
1916
3236
  /**
@@ -1948,10 +3268,11 @@ var Docxodus = (() => {
1948
3268
  if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
1949
3269
  return null;
1950
3270
  }
1951
- const table = wrapper.firstElementChild;
1952
- if (!(table instanceof HTMLTableElement)) {
3271
+ const tableElement = wrapper.firstElementChild;
3272
+ if (!tableElement || tableElement.localName !== "table") {
1953
3273
  return null;
1954
3274
  }
3275
+ const table = tableElement;
1955
3276
  const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
1956
3277
  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]")) {
1957
3278
  return null;
@@ -1965,8 +3286,10 @@ var Docxodus = (() => {
1965
3286
  const groups = [];
1966
3287
  let start2 = 0;
1967
3288
  while (start2 < rows.length) {
3289
+ this.checkpoint();
1968
3290
  let end = start2;
1969
3291
  while (end < rows.length) {
3292
+ this.checkpoint();
1970
3293
  const candidate = this.createSimpleTableFragment(
1971
3294
  wrapper,
1972
3295
  table,
@@ -1974,7 +3297,7 @@ var Docxodus = (() => {
1974
3297
  rows.slice(start2, end + 1),
1975
3298
  start2 === 0
1976
3299
  );
1977
- const measured = this.measureElement(candidate, dims);
3300
+ const measured = this.measureElement(candidate, dims, block.sectionIndex);
1978
3301
  if (measured.heightPt > maximumFragmentHeight) {
1979
3302
  break;
1980
3303
  }
@@ -2006,7 +3329,7 @@ var Docxodus = (() => {
2006
3329
  if (!isLast) {
2007
3330
  fragment.style.setProperty("margin-bottom", "0", "important");
2008
3331
  }
2009
- const measured = this.measureElement(fragment, dims);
3332
+ const measured = this.measureElement(fragment, dims, block.sectionIndex);
2010
3333
  if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
2011
3334
  return null;
2012
3335
  }
@@ -2021,35 +3344,198 @@ var Docxodus = (() => {
2021
3344
  return fragments;
2022
3345
  }
2023
3346
  /**
2024
- * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
2025
- * after whitespace or at a run boundary so the paginator never deliberately
2026
- * cuts through an ordinary word merely to fill a little more of a page.
2027
- */
2028
- paragraphFragmentEndpoints(paragraph) {
2029
- const endpoints = [];
2030
- const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
3347
+ * DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
3348
+ * offsets are checked against the browser's Unicode grapheme segmenter, so a
3349
+ * formatting-run boundary can never bisect a surrogate pair, combining
3350
+ * sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
3351
+ * PAGE/NUMPAGES field results are atomic because splitting their marker would
3352
+ * make later substitution duplicate or replace only half of the field.
3353
+ */
3354
+ paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
3355
+ const textNodes = [];
3356
+ const textStarts = /* @__PURE__ */ new Map();
3357
+ const lastTextByField = /* @__PURE__ */ new Map();
3358
+ const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
3359
+ const flattenedChunks = [];
3360
+ let flattenedLength = 0;
2031
3361
  let textNode;
2032
3362
  while (textNode = walker.nextNode()) {
2033
- const text = textNode.data;
2034
- if (text.length === 0) continue;
2035
- const whitespace = /\s+/g;
3363
+ this.checkpoint();
3364
+ textStarts.set(textNode, flattenedLength);
3365
+ textNodes.push(textNode);
3366
+ flattenedChunks.push(textNode.data);
3367
+ flattenedLength += textNode.data.length;
3368
+ let field = textNode.parentElement?.closest("[data-field]") ?? null;
3369
+ while (field?.parentElement?.closest("[data-field]")) {
3370
+ field = field.parentElement.closest("[data-field]");
3371
+ }
3372
+ if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
3373
+ }
3374
+ const flattenedText = flattenedChunks.join("");
3375
+ if (!this.hasVisibleText(flattenedText)) return [];
3376
+ const invisible = /[\s\u200B-\u200F\uFEFF]/;
3377
+ let firstVisibleOffset = -1;
3378
+ let lastVisibleOffset = -1;
3379
+ for (let index = 0; index < flattenedText.length; index++) {
3380
+ if (index % 4096 === 0) this.checkpoint();
3381
+ if (invisible.test(flattenedText[index])) continue;
3382
+ if (firstVisibleOffset < 0) firstVisibleOffset = index;
3383
+ lastVisibleOffset = index;
3384
+ }
3385
+ const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
3386
+ const candidates = [];
3387
+ const addCandidate = (candidate) => {
3388
+ const { textOffset } = candidate;
3389
+ if (textOffset <= 0 || textOffset >= flattenedText.length) return;
3390
+ if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
3391
+ if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
3392
+ if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
3393
+ if (graphemeBoundaries) {
3394
+ if (!graphemeBoundaries.has(textOffset)) return;
3395
+ } else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
3396
+ return;
3397
+ }
3398
+ candidates.push(candidate);
3399
+ };
3400
+ for (const node of textNodes) {
3401
+ this.checkpoint();
3402
+ const start2 = textStarts.get(node);
3403
+ const atomic = node.parentElement?.closest(
3404
+ "[data-field], [data-list-marker], a[data-comment-id]"
3405
+ );
3406
+ if (atomic && paragraph.contains(atomic)) continue;
3407
+ const whitespace = /[\u0009-\u000D\u0020]+/g;
2036
3408
  let match;
2037
- while ((match = whitespace.exec(text)) !== null) {
2038
- endpoints.push({ node: textNode, offset: match.index + match[0].length });
3409
+ let matchesSinceCheckpoint = 0;
3410
+ while ((match = whitespace.exec(node.data)) !== null) {
3411
+ if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
3412
+ const offset = match.index + match[0].length;
3413
+ addCandidate({ node, offset, textOffset: start2 + offset, priority: 0 });
3414
+ }
3415
+ addCandidate({
3416
+ node,
3417
+ offset: node.data.length,
3418
+ textOffset: start2 + node.data.length,
3419
+ priority: 0
3420
+ });
3421
+ if (emergencyGraphemeBreaks) {
3422
+ for (let offset = 1; offset < node.data.length; offset++) {
3423
+ if (offset % 256 === 0) this.checkpoint();
3424
+ addCandidate({
3425
+ node,
3426
+ offset,
3427
+ textOffset: start2 + offset,
3428
+ priority: -1
3429
+ });
3430
+ }
2039
3431
  }
2040
- if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
2041
- endpoints.push({ node: textNode, offset: text.length });
3432
+ }
3433
+ for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
3434
+ this.checkpoint();
3435
+ if (field.parentElement?.closest("[data-field]")) continue;
3436
+ const last = lastTextByField.get(field);
3437
+ const parent = field.parentNode;
3438
+ if (!last || !parent) continue;
3439
+ const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
3440
+ if (childIndex < 0) continue;
3441
+ addCandidate({
3442
+ node: parent,
3443
+ offset: childIndex + 1,
3444
+ textOffset: textStarts.get(last) + last.data.length,
3445
+ priority: 1
3446
+ });
3447
+ }
3448
+ candidates.sort((left2, right2) => left2.textOffset - right2.textOffset || right2.priority - left2.priority);
3449
+ const endpoints = [];
3450
+ for (const candidate of candidates) {
3451
+ if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
3452
+ endpoints.push(candidate);
2042
3453
  }
2043
3454
  }
2044
3455
  return endpoints;
2045
3456
  }
3457
+ /** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
3458
+ graphemeBoundaryOffsets(text) {
3459
+ const SegmenterConstructor = this.view.Intl.Segmenter;
3460
+ if (!SegmenterConstructor) return null;
3461
+ const boundaries = /* @__PURE__ */ new Set([0, text.length]);
3462
+ let segmentCount = 0;
3463
+ for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
3464
+ if (++segmentCount % 256 === 0) this.checkpoint();
3465
+ boundaries.add(part.index);
3466
+ boundaries.add(part.index + part.segment.length);
3467
+ }
3468
+ return boundaries;
3469
+ }
3470
+ /**
3471
+ * Conservative UAX #14/CSS wrapping opportunities used for synthetic page
3472
+ * boundaries. In particular, a formatting-run boundary is not itself a word
3473
+ * boundary, and Japanese opening/closing punctuation stays with its pair.
3474
+ * Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
3475
+ * the browser to wrap anywhere.
3476
+ */
3477
+ isLegalParagraphLineBoundary(paragraph, text, offset) {
3478
+ const lastCodeUnit = text.charCodeAt(offset - 1);
3479
+ const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
3480
+ const beforeCodePoint = text.codePointAt(beforeOffset);
3481
+ const afterCodePoint = text.codePointAt(offset);
3482
+ const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
3483
+ const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
3484
+ if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
3485
+ if (/[-\u00AD\u2010]$/u.test(before)) return true;
3486
+ const style = this.view.getComputedStyle(paragraph);
3487
+ if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
3488
+ const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
3489
+ if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
3490
+ const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
3491
+ 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";
3492
+ return !opening.includes(before) && !closing.includes(after);
3493
+ }
3494
+ /** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
3495
+ isNonBreakingTextBoundary(text, offset) {
3496
+ const codePointBefore = (index) => {
3497
+ if (index <= 0) return void 0;
3498
+ const last = text.charCodeAt(index - 1);
3499
+ const start2 = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
3500
+ return text.codePointAt(Math.max(0, start2));
3501
+ };
3502
+ const nonBreaking = /* @__PURE__ */ new Set([
3503
+ 160,
3504
+ 8206,
3505
+ 8207,
3506
+ 8209,
3507
+ 8234,
3508
+ 8235,
3509
+ 8236,
3510
+ 8237,
3511
+ 8238,
3512
+ 8239,
3513
+ 8288,
3514
+ 8294,
3515
+ 8295,
3516
+ 8296,
3517
+ 8297,
3518
+ 65279
3519
+ ]);
3520
+ return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
3521
+ }
3522
+ /**
3523
+ * Without Intl.Segmenter, admit only an all-ASCII boundary. This still
3524
+ * fragments ordinary prose while refusing to guess about Unicode clusters.
3525
+ */
3526
+ isConservativeFallbackTextBoundary(text, offset) {
3527
+ return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
3528
+ }
2046
3529
  /**
2047
3530
  * Whether a range contains visible text after ignoring bidi/zero-width marks.
2048
3531
  * Paragraph fragmentation deliberately excludes non-textual descendants, so
2049
3532
  * this is enough to reject empty head or tail fragments.
2050
3533
  */
2051
3534
  hasVisibleFragmentText(fragment) {
2052
- return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
3535
+ return this.hasVisibleText(fragment.textContent || "");
3536
+ }
3537
+ hasVisibleText(text) {
3538
+ return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
2053
3539
  }
2054
3540
  /**
2055
3541
  * Phase one only handles text-like paragraph descendants. Objects, explicit
@@ -2058,13 +3544,25 @@ var Docxodus = (() => {
2058
3544
  * fallback instead of risking broken content or duplicate anchors.
2059
3545
  */
2060
3546
  canFragmentParagraph(block) {
2061
- if (!this.fragmentParagraphs || block.element.tagName !== "P") {
3547
+ if (!this.fragmentParagraphs) {
2062
3548
  return false;
2063
3549
  }
2064
3550
  const paragraph = block.element;
2065
3551
  if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
2066
3552
  return false;
2067
3553
  }
3554
+ return this.canRangeFragmentParagraph(paragraph);
3555
+ }
3556
+ /**
3557
+ * Shared structural gate for body and note paragraph fragmentation. Footnote
3558
+ * first paragraphs are inline beside their marker, so that one known layout
3559
+ * context may opt into an inline root while retaining every descendant and
3560
+ * break-safety restriction used for body text.
3561
+ */
3562
+ canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
3563
+ if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
3564
+ return false;
3565
+ }
2068
3566
  const unsupportedDescendants = [
2069
3567
  "br",
2070
3568
  "img",
@@ -2095,21 +3593,27 @@ var Docxodus = (() => {
2095
3593
  "fieldset",
2096
3594
  "details",
2097
3595
  "[data-footnote-id]",
2098
- "[data-list-marker]",
2099
- "[data-anchor]",
2100
- "[id]",
2101
3596
  "[contenteditable]"
2102
3597
  ].join(", ");
2103
3598
  if (paragraph.querySelector(unsupportedDescendants)) {
2104
3599
  return false;
2105
3600
  }
2106
- const paragraphStyle = window.getComputedStyle(paragraph);
2107
- 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") {
3601
+ const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
3602
+ return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
3603
+ }
3604
+ /**
3605
+ * A range clone preserves nested inline formatting exactly. Anything that establishes its own
3606
+ * box/layout context is deferred until a future fragmenter can model it accurately. Callers
3607
+ * must invoke this while the paragraph is attached to the styled document.
3608
+ */
3609
+ hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
3610
+ const paragraphStyle = this.view.getComputedStyle(paragraph);
3611
+ 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") {
2108
3612
  return false;
2109
3613
  }
2110
3614
  for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
2111
- const style = window.getComputedStyle(descendant);
2112
- if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
3615
+ const style = this.view.getComputedStyle(descendant);
3616
+ if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
2113
3617
  return false;
2114
3618
  }
2115
3619
  }
@@ -2136,58 +3640,122 @@ var Docxodus = (() => {
2136
3640
  return fragment;
2137
3641
  }
2138
3642
  /**
2139
- * Splits a simple paragraph at the largest DOM Range endpoint that fits the
2140
- * currently available body space. The caller then processes the tail normally,
2141
- * allowing it to fragment again on later pages when necessary.
3643
+ * Range clones repeat an inline ancestor when the split lands inside it. Keep
3644
+ * semantic wrappers (links, comments, formatting) on both sides, but retain a
3645
+ * duplicated HTML/editor identity only on the leading fragment. Targets that
3646
+ * occur wholly after the split are absent from `head` and remain on `tail`.
2142
3647
  */
2143
- tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
2144
- if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
2145
- return null;
2146
- }
2147
- const paragraph = block.element;
2148
- const endpoints = this.paragraphFragmentEndpoints(paragraph);
2149
- if (endpoints.length < 2) {
2150
- return null;
2151
- }
2152
- let low = 0;
2153
- let high = endpoints.length - 2;
2154
- let best = null;
2155
- while (low <= high) {
2156
- const middle = Math.floor((low + high) / 2);
2157
- const endpoint = endpoints[middle];
2158
- const headRange = document.createRange();
2159
- headRange.setStart(paragraph, 0);
2160
- headRange.setEnd(endpoint.node, endpoint.offset);
2161
- const headContents = headRange.cloneContents();
2162
- if (!this.hasVisibleFragmentText(headContents)) {
2163
- low = middle + 1;
2164
- continue;
3648
+ reconcileParagraphFragmentIdentities(head, tail) {
3649
+ const elements = (root, selector2) => [
3650
+ ...root.matches(selector2) ? [root] : [],
3651
+ ...Array.from(root.querySelectorAll(selector2))
3652
+ ];
3653
+ const headIds = new Set(elements(head, "[id]").map((element) => element.id));
3654
+ for (const element of elements(tail, "[id]")) {
3655
+ if (headIds.has(element.id)) element.removeAttribute("id");
3656
+ }
3657
+ const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
3658
+ for (const element of elements(tail, "[data-anchor]")) {
3659
+ if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
3660
+ element.removeAttribute("data-anchor");
3661
+ element.removeAttribute("data-committed-text");
3662
+ }
3663
+ }
3664
+ /**
3665
+ * Range-clone the largest safe prefix accepted by `fits`. The measurement
3666
+ * policy stays with the caller, allowing body blocks and note bands to share
3667
+ * one DOM fragmenter while measuring in their respective layout contexts.
3668
+ */
3669
+ splitParagraphAtLargestFit(paragraph, fits, options = {}) {
3670
+ const largestFit = (endpoints2) => {
3671
+ const lastEndpoint = endpoints2[endpoints2.length - 1];
3672
+ if (lastEndpoint) {
3673
+ const lastRange = this.document.createRange();
3674
+ lastRange.setStart(paragraph, 0);
3675
+ lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
3676
+ const lastContents = lastRange.cloneContents();
3677
+ if (this.hasVisibleFragmentText(lastContents)) {
3678
+ const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
3679
+ if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
3680
+ }
2165
3681
  }
2166
- const head = this.createParagraphFragment(paragraph, headRange, true, false);
2167
- const measured = this.measureElement(head, dims);
2168
- if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
2169
- best = { endpoint, element: head, measured };
2170
- low = middle + 1;
2171
- } else {
2172
- high = middle - 1;
3682
+ let low = 0;
3683
+ let high = endpoints2.length - 2;
3684
+ let best2 = null;
3685
+ while (low <= high) {
3686
+ this.checkpoint();
3687
+ const middle = Math.floor((low + high) / 2);
3688
+ const endpoint = endpoints2[middle];
3689
+ const headRange = this.document.createRange();
3690
+ headRange.setStart(paragraph, 0);
3691
+ headRange.setEnd(endpoint.node, endpoint.offset);
3692
+ const headContents = headRange.cloneContents();
3693
+ if (!this.hasVisibleFragmentText(headContents)) {
3694
+ low = middle + 1;
3695
+ continue;
3696
+ }
3697
+ const head = this.createParagraphFragment(paragraph, headRange, true, false);
3698
+ if (fits(head)) {
3699
+ best2 = { endpoint, head };
3700
+ low = middle + 1;
3701
+ } else {
3702
+ high = middle - 1;
3703
+ }
2173
3704
  }
3705
+ return best2;
3706
+ };
3707
+ let endpoints = this.paragraphFragmentEndpoints(paragraph);
3708
+ let best = largestFit(endpoints);
3709
+ if (!best && options.emergencyGraphemeBreaks) {
3710
+ endpoints = this.paragraphFragmentEndpoints(paragraph, true);
3711
+ best = largestFit(endpoints);
3712
+ }
3713
+ if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
3714
+ const endpoint = endpoints[0];
3715
+ const headRange = this.document.createRange();
3716
+ headRange.setStart(paragraph, 0);
3717
+ headRange.setEnd(endpoint.node, endpoint.offset);
3718
+ best = {
3719
+ endpoint,
3720
+ head: this.createParagraphFragment(paragraph, headRange, true, false)
3721
+ };
2174
3722
  }
2175
3723
  if (!best) {
2176
- return null;
3724
+ return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
2177
3725
  }
2178
- const tailRange = document.createRange();
3726
+ const tailRange = this.document.createRange();
2179
3727
  tailRange.setStart(best.endpoint.node, best.endpoint.offset);
2180
3728
  tailRange.setEnd(paragraph, paragraph.childNodes.length);
2181
3729
  const tailContents = tailRange.cloneContents();
2182
- if (!this.hasVisibleFragmentText(tailContents)) {
3730
+ if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
3731
+ const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
3732
+ this.reconcileParagraphFragmentIdentities(best.head, tail);
3733
+ return {
3734
+ kind: "split",
3735
+ head: best.head,
3736
+ tail
3737
+ };
3738
+ }
3739
+ /**
3740
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
3741
+ * currently available body space. The caller then processes the tail normally,
3742
+ * allowing it to fragment again on later pages when necessary.
3743
+ */
3744
+ tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
3745
+ if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
2183
3746
  return null;
2184
3747
  }
2185
- const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
2186
- const tailMeasured = this.measureElement(tail, dims);
3748
+ const split = this.splitParagraphAtLargestFit(block.element, (head) => {
3749
+ const measured = this.measureElement(head, dims, block.sectionIndex);
3750
+ return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
3751
+ });
3752
+ if (split.kind !== "split") return null;
3753
+ const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
3754
+ const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
2187
3755
  return [
2188
3756
  {
2189
- ...best.measured,
2190
- element: best.element,
3757
+ ...headMeasured,
3758
+ element: split.head,
2191
3759
  keepWithNext: false,
2192
3760
  keepLines: false,
2193
3761
  pageBreakBefore: false,
@@ -2195,7 +3763,7 @@ var Docxodus = (() => {
2195
3763
  },
2196
3764
  {
2197
3765
  ...tailMeasured,
2198
- element: tail,
3766
+ element: split.tail,
2199
3767
  keepWithNext: false,
2200
3768
  keepLines: false,
2201
3769
  pageBreakBefore: false,
@@ -2266,7 +3834,7 @@ var Docxodus = (() => {
2266
3834
  const registry = /* @__PURE__ */ new Map();
2267
3835
  const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
2268
3836
  if (!registryEl) return registry;
2269
- const entries = Array.from(registryEl.querySelectorAll("[data-footnote-id]"));
3837
+ const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
2270
3838
  for (const entry of entries) {
2271
3839
  const footnoteId = entry.dataset.footnoteId;
2272
3840
  if (footnoteId) {
@@ -2275,6 +3843,68 @@ var Docxodus = (() => {
2275
3843
  }
2276
3844
  return registry;
2277
3845
  }
3846
+ /** Read Word's optional normal and continuation separator stories. */
3847
+ parseFootnoteSeparators() {
3848
+ const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
3849
+ const clone = (kind) => {
3850
+ const source = Array.from(registry?.children ?? []).find(
3851
+ (entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
3852
+ );
3853
+ return source?.cloneNode(true);
3854
+ };
3855
+ return {
3856
+ normal: clone("normal") ?? null,
3857
+ continuation: clone("continuation") ?? null
3858
+ };
3859
+ }
3860
+ /** Append the exact separator story selected for this initial/continued note band. */
3861
+ appendFootnoteSeparator(container, continuation) {
3862
+ const kind = continuation ? "continuation" : "normal";
3863
+ const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
3864
+ if (!source) {
3865
+ const fallback = this.document.createElement("hr");
3866
+ fallback.dataset.footnoteSeparator = kind;
3867
+ container.appendChild(fallback);
3868
+ return;
3869
+ }
3870
+ const clone = source.cloneNode(true);
3871
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
3872
+ element.removeAttribute("id");
3873
+ element.removeAttribute("data-anchor");
3874
+ element.removeAttribute("data-committed-text");
3875
+ element.removeAttribute("data-source-anchor-id");
3876
+ element.removeAttribute("data-page-fragment-id");
3877
+ element.removeAttribute("data-fragment-index");
3878
+ element.removeAttribute("data-footnote-id");
3879
+ element.removeAttribute("data-comment-id");
3880
+ element.removeAttribute("name");
3881
+ if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
3882
+ element.removeAttribute("href");
3883
+ }
3884
+ element.setAttribute("contenteditable", "false");
3885
+ }
3886
+ container.appendChild(clone);
3887
+ }
3888
+ /** Parses the hidden source notes used to render paginated margin comments. */
3889
+ parseCommentMarginRegistry() {
3890
+ const registry = /* @__PURE__ */ new Map();
3891
+ const registryEl = this.stagingElement.querySelector(
3892
+ "#pagination-comment-margin-registry"
3893
+ );
3894
+ if (!registryEl) return registry;
3895
+ for (const entry of Array.from(
3896
+ registryEl.querySelectorAll("[data-comment-id]")
3897
+ )) {
3898
+ const commentId = entry.dataset.commentId;
3899
+ if (!commentId) continue;
3900
+ let root = entry;
3901
+ for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
3902
+ root = parent;
3903
+ }
3904
+ registry.set(commentId, root.cloneNode(true));
3905
+ }
3906
+ return registry;
3907
+ }
2278
3908
  /**
2279
3909
  * Extracts footnote reference IDs from an element.
2280
3910
  */
@@ -2289,6 +3919,92 @@ var Docxodus = (() => {
2289
3919
  }
2290
3920
  return ids;
2291
3921
  }
3922
+ /** Clone a note child for the continuation queue with its original selector position. */
3923
+ cloneFootnoteElementForContinuation(element, sourceIndex) {
3924
+ const clone = element.cloneNode(true);
3925
+ clone.setAttribute(
3926
+ FOOTNOTE_SOURCE_POSITION_ATTR,
3927
+ sourceIndex === 0 ? "first" : "later"
3928
+ );
3929
+ return clone;
3930
+ }
3931
+ /** Remove identities that belong only to the initial registry presentation shell. */
3932
+ makeContinuationShellInert(element) {
3933
+ element.removeAttribute("id");
3934
+ element.removeAttribute("data-anchor");
3935
+ element.removeAttribute("data-committed-text");
3936
+ }
3937
+ /**
3938
+ * Build the exact continuation shape shared by measurement and paint.
3939
+ *
3940
+ * Keep the source `.footnote-item > .footnote-content` ancestry: document
3941
+ * CSS, inherited direction/language, and custom classes frequently target
3942
+ * those shells. Only the number and initial HTML/editor identities are
3943
+ * omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
3944
+ * whose first carried element was a later source paragraph.
3945
+ */
3946
+ createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
3947
+ const source = this.footnoteRegistry.get(continuation.footnoteId);
3948
+ const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
3949
+ this.makeContinuationShellInert(wrapper);
3950
+ wrapper.classList.add("footnote-continuation");
3951
+ wrapper.dataset.footnoteId = continuation.footnoteId;
3952
+ if (continuation.sourceAnchorId) {
3953
+ wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
3954
+ }
3955
+ const sourceContent = source?.querySelector(".footnote-content");
3956
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
3957
+ this.makeContinuationShellInert(content);
3958
+ if (!sourceContent) content.className = "footnote-content";
3959
+ const first = elements[0];
3960
+ if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
3961
+ const sentinel = this.document.createElement("p");
3962
+ sentinel.className = "footnote-continuation-position-sentinel";
3963
+ sentinel.setAttribute("aria-hidden", "true");
3964
+ sentinel.style.setProperty("display", "none", "important");
3965
+ content.appendChild(sentinel);
3966
+ }
3967
+ for (let index = 0; index < elements.length; index++) {
3968
+ this.checkpoint();
3969
+ const element = elements[index];
3970
+ const clone = element.cloneNode(true);
3971
+ clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
3972
+ if (index === 0 && clone.tagName === "P") {
3973
+ clone.style.setProperty("display", "block", "important");
3974
+ }
3975
+ content.appendChild(clone);
3976
+ }
3977
+ wrapper.appendChild(content);
3978
+ return wrapper;
3979
+ }
3980
+ /** Build the exact partial-note item shape shared by measurement and paint. */
3981
+ createPartialFootnoteItem(footnote, fittingElements) {
3982
+ const item = footnote.cloneNode(false);
3983
+ const number = footnote.querySelector(".footnote-number");
3984
+ if (number) item.appendChild(number.cloneNode(true));
3985
+ const sourceContent = footnote.querySelector(".footnote-content");
3986
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
3987
+ if (!sourceContent) content.className = "footnote-content";
3988
+ for (const element of fittingElements) {
3989
+ this.checkpoint();
3990
+ content.appendChild(element.cloneNode(true));
3991
+ }
3992
+ item.appendChild(content);
3993
+ return item;
3994
+ }
3995
+ /**
3996
+ * Where a note measurement tree is attached.
3997
+ *
3998
+ * A note band is reserved from a measurement and then painted; if the two
3999
+ * happen under different inherited typography the painted notes overflow the
4000
+ * reserve, which is the invisible clipping this engine exists to avoid. The
4001
+ * registry lives in the staging tree but a band paints inside the output
4002
+ * container, so measure there. The host is never a page box, so a page's
4003
+ * `zoom` cannot scale a reserve that is accounted for in unscaled points.
4004
+ */
4005
+ noteMeasurementHost() {
4006
+ return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
4007
+ }
2292
4008
  /**
2293
4009
  * Measures the height of footnotes for given IDs (in points).
2294
4010
  * Creates a temporary container to measure the footnotes.
@@ -2296,140 +4012,246 @@ var Docxodus = (() => {
2296
4012
  * @param contentWidth - Width for measurement
2297
4013
  * @param continuation - Optional continuation content to include first
2298
4014
  */
2299
- measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
4015
+ measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
2300
4016
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2301
- if (footnoteIds.length === 0 && !hasContinuation || this.footnoteRegistry.size === 0) {
4017
+ if (footnoteIds.length === 0 && !hasContinuation) {
2302
4018
  return 0;
2303
4019
  }
2304
- const measureContainer = document.createElement("div");
4020
+ if (this.footnoteRegistry.size === 0 && !hasContinuation) {
4021
+ return 0;
4022
+ }
4023
+ const measureContainer = this.document.createElement("div");
2305
4024
  measureContainer.style.position = "absolute";
2306
4025
  measureContainer.style.visibility = "hidden";
2307
4026
  measureContainer.style.width = `${contentWidth}pt`;
2308
4027
  measureContainer.style.left = "-9999px";
2309
4028
  measureContainer.className = this.cssPrefix + "footnotes";
2310
- const hr = document.createElement("hr");
2311
- measureContainer.appendChild(hr);
4029
+ this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
2312
4030
  if (hasContinuation) {
2313
- const contWrapper = document.createElement("div");
2314
- contWrapper.className = "footnote-continuation";
2315
- for (const el of continuation.remainingElements) {
2316
- contWrapper.appendChild(el.cloneNode(true));
2317
- }
2318
- measureContainer.appendChild(contWrapper);
4031
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
2319
4032
  }
4033
+ const partialById = new Map(
4034
+ partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
4035
+ );
2320
4036
  for (const id of footnoteIds) {
4037
+ this.checkpoint();
2321
4038
  const footnote = this.footnoteRegistry.get(id);
2322
4039
  if (footnote) {
2323
- measureContainer.appendChild(footnote.cloneNode(true));
4040
+ const partial = partialById.get(id);
4041
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
2324
4042
  }
2325
4043
  }
2326
- this.stagingElement.appendChild(measureContainer);
2327
- const rect = measureContainer.getBoundingClientRect();
2328
- const heightPt = pxToPt(rect.height);
2329
- this.stagingElement.removeChild(measureContainer);
2330
- return heightPt;
4044
+ this.noteMeasurementHost().appendChild(measureContainer);
4045
+ try {
4046
+ return pxToPt(measureContainer.getBoundingClientRect().height);
4047
+ } finally {
4048
+ measureContainer.remove();
4049
+ }
2331
4050
  }
2332
4051
  /**
2333
- * Measures the height of just the continuation content (in points).
4052
+ * Partition a continuation for one page's note band, preferring complete children
4053
+ * and range-fragmenting an eligible paragraph when necessary. Always advances by
4054
+ * at least one element so an indivisible oversized paragraph follows the established
4055
+ * clipped fallback without trapping pagination in a loop.
2334
4056
  */
2335
- measureContinuationHeight(continuation, contentWidth) {
2336
- if (!continuation || continuation.remainingElements.length === 0) {
2337
- return 0;
2338
- }
2339
- const measureContainer = document.createElement("div");
4057
+ splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
4058
+ const fitting = [];
4059
+ let remaining = [];
4060
+ const measureContainer = this.document.createElement("div");
2340
4061
  measureContainer.style.position = "absolute";
2341
4062
  measureContainer.style.visibility = "hidden";
2342
4063
  measureContainer.style.width = `${contentWidth}pt`;
2343
4064
  measureContainer.style.left = "-9999px";
2344
4065
  measureContainer.className = this.cssPrefix + "footnotes";
2345
- const hr = document.createElement("hr");
2346
- measureContainer.appendChild(hr);
2347
- for (const el of continuation.remainingElements) {
2348
- measureContainer.appendChild(el.cloneNode(true));
2349
- }
2350
- this.stagingElement.appendChild(measureContainer);
2351
- const rect = measureContainer.getBoundingClientRect();
2352
- const heightPt = pxToPt(rect.height);
2353
- this.stagingElement.removeChild(measureContainer);
2354
- return heightPt;
2355
- }
2356
- /**
2357
- * Splits a footnote element into parts that fit within the available height.
2358
- * Returns the elements that fit and the elements that need to continue.
2359
- */
2360
- splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
2361
- const footnoteContent = footnoteElement.querySelector(".footnote-content");
2362
- if (!footnoteContent) {
2363
- return {
2364
- fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
2365
- overflow: []
2366
- };
2367
- }
2368
- const children = Array.from(footnoteContent.children);
2369
- if (children.length <= 1) {
2370
- return {
2371
- fits: children.map((el) => el.cloneNode(true)),
2372
- overflow: []
2373
- };
2374
- }
2375
- const fits = [];
2376
- const overflow = [];
2377
- let currentHeight = 0;
2378
- const hrMeasure = document.createElement("div");
2379
- hrMeasure.style.position = "absolute";
2380
- hrMeasure.style.visibility = "hidden";
2381
- hrMeasure.style.width = `${contentWidth}pt`;
2382
- hrMeasure.style.left = "-9999px";
2383
- hrMeasure.className = this.cssPrefix + "footnotes";
2384
- const hr = document.createElement("hr");
2385
- hrMeasure.appendChild(hr);
2386
- this.stagingElement.appendChild(hrMeasure);
2387
- const hrHeight = pxToPt(hrMeasure.getBoundingClientRect().height);
2388
- this.stagingElement.removeChild(hrMeasure);
2389
- currentHeight = hrHeight;
2390
- const footnoteNumber = footnoteElement.querySelector(".footnote-number");
2391
- for (let i = 0; i < children.length; i++) {
2392
- const child = children[i];
2393
- const measureContainer = document.createElement("div");
2394
- measureContainer.style.position = "absolute";
2395
- measureContainer.style.visibility = "hidden";
2396
- measureContainer.style.width = `${contentWidth}pt`;
2397
- measureContainer.style.left = "-9999px";
2398
- measureContainer.className = this.cssPrefix + "footnotes";
2399
- measureContainer.appendChild(child.cloneNode(true));
2400
- this.stagingElement.appendChild(measureContainer);
2401
- const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
2402
- this.stagingElement.removeChild(measureContainer);
2403
- if (currentHeight + childHeight <= availableHeightPt) {
2404
- fits.push(child.cloneNode(true));
2405
- currentHeight += childHeight;
2406
- } else {
2407
- for (let j = i; j < children.length; j++) {
2408
- overflow.push(children[j].cloneNode(true));
4066
+ this.appendFootnoteSeparator(measureContainer, true);
4067
+ const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
4068
+ const content = wrapper.querySelector(":scope > .footnote-content");
4069
+ if (!content) {
4070
+ throw new Error("Footnote continuation is missing its content shell");
4071
+ }
4072
+ measureContainer.appendChild(wrapper);
4073
+ this.noteMeasurementHost().appendChild(measureContainer);
4074
+ try {
4075
+ for (let index = 0; index < continuation.remainingElements.length; index++) {
4076
+ this.checkpoint();
4077
+ const element = continuation.remainingElements[index];
4078
+ const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
4079
+ if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
4080
+ const sentinel = this.document.createElement("p");
4081
+ sentinel.className = "footnote-continuation-position-sentinel";
4082
+ sentinel.setAttribute("aria-hidden", "true");
4083
+ sentinel.style.setProperty("display", "none", "important");
4084
+ content.appendChild(sentinel);
4085
+ }
4086
+ const candidate = element.cloneNode(true);
4087
+ candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
4088
+ if (fitting.length === 0 && candidate.tagName === "P") {
4089
+ candidate.style.setProperty("display", "block", "important");
4090
+ }
4091
+ content.appendChild(candidate);
4092
+ const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
4093
+ if (candidateHeight <= availableHeightPt) {
4094
+ fitting.push(element);
4095
+ continue;
4096
+ }
4097
+ const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
4098
+ candidate.remove();
4099
+ let split = null;
4100
+ if (canSplitCandidate) {
4101
+ split = this.splitParagraphAtLargestFit(candidate, (head) => {
4102
+ content.appendChild(head);
4103
+ try {
4104
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
4105
+ } finally {
4106
+ head.remove();
4107
+ }
4108
+ }, {
4109
+ // Both fallbacks below cut where ordinary line breaking would not,
4110
+ // so they are only ever right for an element that owns the whole
4111
+ // band: if this page already carries earlier content, a paragraph
4112
+ // that will not start here belongs intact in the next note band.
4113
+ emergencyGraphemeBreaks: fitting.length === 0,
4114
+ // A continuation page owns the full note band. If even one legal
4115
+ // grapheme is taller than it, clip only that unit and keep draining.
4116
+ forceFirstOnNoFit: fitting.length === 0
4117
+ });
4118
+ }
4119
+ if (split?.kind === "split") {
4120
+ if (sourcePosition) {
4121
+ split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
4122
+ split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
4123
+ }
4124
+ fitting.push(split.head);
4125
+ remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
4126
+ } else if (fitting.length === 0) {
4127
+ fitting.push(element);
4128
+ remaining = continuation.remainingElements.slice(index + 1);
4129
+ } else {
4130
+ remaining = continuation.remainingElements.slice(index);
2409
4131
  }
2410
4132
  break;
2411
4133
  }
4134
+ } finally {
4135
+ measureContainer.remove();
2412
4136
  }
2413
- return { fits, overflow };
4137
+ return {
4138
+ current: {
4139
+ footnoteId: continuation.footnoteId,
4140
+ sourceAnchorId: continuation.sourceAnchorId,
4141
+ remainingElements: fitting
4142
+ },
4143
+ overflow: remaining.length > 0 ? {
4144
+ footnoteId: continuation.footnoteId,
4145
+ sourceAnchorId: continuation.sourceAnchorId,
4146
+ remainingElements: remaining
4147
+ } : null
4148
+ };
2414
4149
  }
2415
4150
  /**
2416
- * Measures a single footnote's height.
4151
+ * Splits a footnote element into parts that fit within the available height.
4152
+ * Returns the elements that fit and the elements that need to continue.
2417
4153
  */
2418
- measureSingleFootnoteHeight(footnoteId, contentWidth) {
2419
- const footnote = this.footnoteRegistry.get(footnoteId);
2420
- if (!footnote) return 0;
2421
- const measureContainer = document.createElement("div");
4154
+ splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
4155
+ const layoutContext = this.document.createElement("div");
4156
+ layoutContext.style.position = "absolute";
4157
+ layoutContext.style.visibility = "hidden";
4158
+ layoutContext.style.width = `${contentWidth}pt`;
4159
+ layoutContext.style.left = "-9999px";
4160
+ layoutContext.className = this.cssPrefix + "footnotes";
4161
+ this.appendFootnoteSeparator(layoutContext, false);
4162
+ const attachedFootnote = footnoteElement.cloneNode(true);
4163
+ layoutContext.appendChild(attachedFootnote);
4164
+ this.noteMeasurementHost().appendChild(layoutContext);
4165
+ const measureContainer = this.document.createElement("div");
2422
4166
  measureContainer.style.position = "absolute";
2423
4167
  measureContainer.style.visibility = "hidden";
2424
4168
  measureContainer.style.width = `${contentWidth}pt`;
2425
4169
  measureContainer.style.left = "-9999px";
2426
4170
  measureContainer.className = this.cssPrefix + "footnotes";
2427
- measureContainer.appendChild(footnote.cloneNode(true));
2428
- this.stagingElement.appendChild(measureContainer);
2429
- const rect = measureContainer.getBoundingClientRect();
2430
- const heightPt = pxToPt(rect.height);
2431
- this.stagingElement.removeChild(measureContainer);
2432
- return heightPt;
4171
+ const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
4172
+ this.appendFootnoteSeparator(measureContainer, hasContinuation);
4173
+ if (hasContinuation) {
4174
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(
4175
+ existingPayload.continuation
4176
+ ));
4177
+ }
4178
+ const partialById = new Map(
4179
+ existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
4180
+ );
4181
+ for (const id of existingPayload?.footnoteIds ?? []) {
4182
+ this.checkpoint();
4183
+ const source = this.footnoteRegistry.get(id);
4184
+ if (!source) continue;
4185
+ const partial = partialById.get(id);
4186
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
4187
+ }
4188
+ const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
4189
+ const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
4190
+ if (!measuredContent) {
4191
+ layoutContext.remove();
4192
+ throw new Error("Footnote is missing its content shell");
4193
+ }
4194
+ measureContainer.appendChild(measuredPartial);
4195
+ this.noteMeasurementHost().appendChild(measureContainer);
4196
+ try {
4197
+ const footnoteContent = attachedFootnote.querySelector(".footnote-content");
4198
+ if (!footnoteContent) {
4199
+ return {
4200
+ fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
4201
+ overflow: []
4202
+ };
4203
+ }
4204
+ const children = Array.from(footnoteContent.children);
4205
+ const fits = [];
4206
+ let overflow = [];
4207
+ for (let i = 0; i < children.length; i++) {
4208
+ this.checkpoint();
4209
+ const child = children[i];
4210
+ const measuredCandidate = child.cloneNode(true);
4211
+ measuredContent.appendChild(measuredCandidate);
4212
+ const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
4213
+ if (candidateFits) {
4214
+ fits.push(child.cloneNode(true));
4215
+ continue;
4216
+ }
4217
+ measuredCandidate.remove();
4218
+ const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
4219
+ measuredContent.appendChild(head);
4220
+ try {
4221
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
4222
+ } finally {
4223
+ head.remove();
4224
+ }
4225
+ }, {
4226
+ // Both fallbacks cut where ordinary line breaking would not, so they
4227
+ // are only ever right for an element that owns the whole band: with
4228
+ // earlier siblings already packed here, a paragraph that will not
4229
+ // start belongs intact in the next note band.
4230
+ emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
4231
+ forceFirstOnNoFit: fits.length === 0 && forceProgress
4232
+ }) : null;
4233
+ if (split?.kind === "split") {
4234
+ fits.push(split.head);
4235
+ split.tail.setAttribute(
4236
+ FOOTNOTE_SOURCE_POSITION_ATTR,
4237
+ i === 0 ? "first" : "later"
4238
+ );
4239
+ overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
4240
+ } else if (split?.kind === "no-fit") {
4241
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
4242
+ } else if (fits.length === 0 && forceProgress) {
4243
+ fits.push(child.cloneNode(true));
4244
+ overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
4245
+ } else {
4246
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
4247
+ }
4248
+ break;
4249
+ }
4250
+ return { fits, overflow };
4251
+ } finally {
4252
+ measureContainer.remove();
4253
+ layoutContext.remove();
4254
+ }
2433
4255
  }
2434
4256
  /**
2435
4257
  * Adds footnotes to a page container, including continuation content.
@@ -2442,12 +4264,11 @@ var Docxodus = (() => {
2442
4264
  if (this.footnoteRegistry.size === 0 && !hasContinuation) {
2443
4265
  return;
2444
4266
  }
2445
- const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
2446
4267
  const maxFootnoteHeight = Math.min(
2447
4268
  footnoteHeight,
2448
4269
  bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
2449
4270
  );
2450
- const footnotesDiv = document.createElement("div");
4271
+ const footnotesDiv = this.document.createElement("div");
2451
4272
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
2452
4273
  footnotesDiv.style.position = "absolute";
2453
4274
  footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
@@ -2456,35 +4277,18 @@ var Docxodus = (() => {
2456
4277
  footnotesDiv.style.boxSizing = "border-box";
2457
4278
  footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
2458
4279
  footnotesDiv.style.overflow = "hidden";
2459
- const hr = document.createElement("hr");
2460
- footnotesDiv.appendChild(hr);
4280
+ this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
2461
4281
  if (hasContinuation) {
2462
- const contWrapper = document.createElement("div");
2463
- contWrapper.className = "footnote-continuation";
2464
- for (const el of continuation.remainingElements) {
2465
- contWrapper.appendChild(el.cloneNode(true));
2466
- }
2467
- footnotesDiv.appendChild(contWrapper);
4282
+ footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
2468
4283
  }
2469
4284
  for (const id of footnoteIds) {
2470
4285
  const partial = partialFootnotes?.find((p) => p.footnoteId === id);
2471
4286
  if (partial) {
2472
4287
  const footnote = this.footnoteRegistry.get(id);
2473
4288
  if (footnote) {
2474
- const partialDiv = document.createElement("div");
2475
- partialDiv.className = "footnote-item";
2476
- partialDiv.dataset.footnoteId = id;
2477
- const numberSpan = footnote.querySelector(".footnote-number");
2478
- if (numberSpan) {
2479
- partialDiv.appendChild(numberSpan.cloneNode(true));
2480
- }
2481
- const contentSpan = document.createElement("span");
2482
- contentSpan.className = "footnote-content";
2483
- for (const el of partial.fittingElements) {
2484
- contentSpan.appendChild(el.cloneNode(true));
2485
- }
2486
- partialDiv.appendChild(contentSpan);
2487
- footnotesDiv.appendChild(partialDiv);
4289
+ footnotesDiv.appendChild(
4290
+ this.createPartialFootnoteItem(footnote, partial.fittingElements)
4291
+ );
2488
4292
  }
2489
4293
  } else {
2490
4294
  const footnote = this.footnoteRegistry.get(id);
@@ -2495,30 +4299,26 @@ var Docxodus = (() => {
2495
4299
  }
2496
4300
  pageBox.appendChild(footnotesDiv);
2497
4301
  }
2498
- /**
2499
- * Selects the appropriate header for a page based on section, page position, and page number.
2500
- */
2501
- selectHeader(sectionIndex, pageInSection, globalPageNumber) {
4302
+ /** Select the section's first/odd/even header from its one-based page position. */
4303
+ selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
2502
4304
  const sectionHf = this.hfRegistry.get(sectionIndex);
2503
4305
  if (!sectionHf) return void 0;
2504
4306
  if (pageInSection === 1 && sectionHf.headerFirst) {
2505
4307
  return sectionHf.headerFirst;
2506
4308
  }
2507
- if (globalPageNumber % 2 === 0 && sectionHf.headerEven) {
4309
+ if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
2508
4310
  return sectionHf.headerEven;
2509
4311
  }
2510
4312
  return sectionHf.headerDefault;
2511
4313
  }
2512
- /**
2513
- * Selects the appropriate footer for a page based on section, page position, and page number.
2514
- */
2515
- selectFooter(sectionIndex, pageInSection, globalPageNumber) {
4314
+ /** Select the section's first/odd/even footer from its one-based page position. */
4315
+ selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
2516
4316
  const sectionHf = this.hfRegistry.get(sectionIndex);
2517
4317
  if (!sectionHf) return void 0;
2518
4318
  if (pageInSection === 1 && sectionHf.footerFirst) {
2519
4319
  return sectionHf.footerFirst;
2520
4320
  }
2521
- if (globalPageNumber % 2 === 0 && sectionHf.footerEven) {
4321
+ if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
2522
4322
  return sectionHf.footerEven;
2523
4323
  }
2524
4324
  return sectionHf.footerDefault;
@@ -2533,7 +4333,7 @@ var Docxodus = (() => {
2533
4333
  * Deterministic: it depends only on the section's page setup and the registry's pre-measured
2534
4334
  * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
2535
4335
  */
2536
- getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
4336
+ getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
2537
4337
  const sectionHf = this.hfRegistry.get(sectionIndex);
2538
4338
  return resolvePageBands(
2539
4339
  dims,
@@ -2542,14 +4342,14 @@ var Docxodus = (() => {
2542
4342
  sectionHf?.headerEvenHeight,
2543
4343
  sectionHf?.headerDefaultHeight,
2544
4344
  pageInSection,
2545
- globalPageNumber
4345
+ displayedPageNumber
2546
4346
  ),
2547
4347
  this.selectStoryHeight(
2548
4348
  sectionHf?.footerFirstHeight,
2549
4349
  sectionHf?.footerEvenHeight,
2550
4350
  sectionHf?.footerDefaultHeight,
2551
4351
  pageInSection,
2552
- globalPageNumber
4352
+ displayedPageNumber
2553
4353
  )
2554
4354
  );
2555
4355
  }
@@ -2557,9 +4357,9 @@ var Docxodus = (() => {
2557
4357
  * The measured height of the running story this page position selects, mirroring
2558
4358
  * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
2559
4359
  */
2560
- selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
4360
+ selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
2561
4361
  if (pageInSection === 1 && first != null) return first;
2562
- if (globalPageNumber % 2 === 0 && even != null) return even;
4362
+ if (displayedPageNumber % 2 === 0 && even != null) return even;
2563
4363
  return fallback ?? 0;
2564
4364
  }
2565
4365
  /**
@@ -2571,7 +4371,7 @@ var Docxodus = (() => {
2571
4371
  * silently starts overlapping body text.
2572
4372
  */
2573
4373
  measureHeaderFooterHeight(source, contentWidth) {
2574
- const measureContainer = document.createElement("div");
4374
+ const measureContainer = this.document.createElement("div");
2575
4375
  measureContainer.style.position = "absolute";
2576
4376
  measureContainer.style.visibility = "hidden";
2577
4377
  measureContainer.style.width = `${contentWidth}pt`;
@@ -2590,79 +4390,275 @@ var Docxodus = (() => {
2590
4390
  * Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
2591
4391
  * Supports footnote continuation - long footnotes can split across pages.
2592
4392
  */
2593
- flowToPages(blocks, dims, startPageNumber, sectionIndex) {
4393
+ flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
2594
4394
  const pages = [];
2595
4395
  let currentContent = [];
2596
4396
  let pageNumber = startPageNumber;
2597
- let pageInSection = 1;
4397
+ let pageSectionIndex = sectionIndex;
4398
+ const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
4399
+ const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
4400
+ const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
4401
+ const markSectionPlaced = (owner) => {
4402
+ if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
4403
+ };
4404
+ const previousBox = this.containerElement.querySelector(
4405
+ `.${this.cssPrefix}box:last-of-type`
4406
+ );
4407
+ let precedingDisplayedPageNumber = parseInt(
4408
+ previousBox?.dataset.displayedPageNumber ?? "0",
4409
+ 10
4410
+ );
4411
+ const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
4412
+ const numbering = this.pageNumbering.get(owner) ?? {};
4413
+ if (numbering.start !== void 0) {
4414
+ return numbering.start + ownerPageInSection - 1;
4415
+ }
4416
+ const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
4417
+ const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
4418
+ return currentPhysicalNumber + physicalPage - pageNumber;
4419
+ };
2598
4420
  let { bodyHeight: effectiveContentHeight } = this.getPageBands(
2599
- dims,
2600
- sectionIndex,
2601
- pageInSection,
2602
- pageNumber
4421
+ dimensionsFor(),
4422
+ pageSectionIndex,
4423
+ pageInSection(),
4424
+ displayedPageNumber()
2603
4425
  );
2604
4426
  let remainingHeight = effectiveContentHeight;
2605
4427
  let prevMarginBottomPt = 0;
2606
4428
  let currentFootnoteIds = [];
4429
+ let currentPageHasFootnoteReference = false;
2607
4430
  let currentFootnoteHeight = 0;
2608
4431
  let currentContinuation = this.pendingFootnoteContinuation;
2609
4432
  let nextPageContinuation = null;
4433
+ let currentContinuationPartitioned = false;
4434
+ let currentPageAdmitted = false;
2610
4435
  let deferredFootnoteIds = [];
2611
4436
  let currentPartialFootnotes = [];
2612
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
2613
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
2614
- }
2615
- const finishPage = () => {
4437
+ const adoptEmptyPageOwner = (owner) => {
4438
+ pageSectionIndex = owner;
4439
+ const bands = this.getPageBands(
4440
+ dimensionsFor(owner),
4441
+ owner,
4442
+ pageInSection(owner),
4443
+ displayedPageNumber(owner)
4444
+ );
4445
+ effectiveContentHeight = bands.bodyHeight;
4446
+ remainingHeight = effectiveContentHeight;
4447
+ };
4448
+ const admitCurrentPage = () => {
4449
+ if (currentPageAdmitted) return;
4450
+ this.admitPageAllocation();
4451
+ currentPageAdmitted = true;
4452
+ };
4453
+ const prepareCurrentContinuation = () => {
4454
+ if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
4455
+ admitCurrentPage();
4456
+ const ownedDimensions = dimensionsFor();
4457
+ const pageBands = this.getPageBands(
4458
+ ownedDimensions,
4459
+ pageSectionIndex,
4460
+ pageInSection(),
4461
+ displayedPageNumber()
4462
+ );
4463
+ const partition = this.splitContinuationForPage(
4464
+ currentContinuation,
4465
+ pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
4466
+ ownedDimensions.contentWidth
4467
+ );
4468
+ currentContinuation = partition.current;
4469
+ if (partition.overflow) nextPageContinuation = partition.overflow;
4470
+ currentContinuationPartitioned = true;
4471
+ currentFootnoteHeight = this.measureFootnotesHeight(
4472
+ currentFootnoteIds,
4473
+ ownedDimensions.contentWidth,
4474
+ currentContinuation,
4475
+ currentPartialFootnotes
4476
+ );
4477
+ };
4478
+ const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
2616
4479
  const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
2617
- if (currentContent.length === 0 && !hasCurrentContinuation) return;
4480
+ if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
4481
+ adoptEmptyPageOwner(nextPageSectionIndex);
4482
+ return;
4483
+ }
4484
+ prepareCurrentContinuation();
4485
+ admitCurrentPage();
4486
+ let pageContinuation = currentContinuation;
4487
+ const ownedPageInSection = pageInSection();
4488
+ const ownedDimensions = dimensionsFor();
4489
+ const ownedDisplayedPageNumber = displayedPageNumber(
4490
+ pageSectionIndex,
4491
+ ownedPageInSection
4492
+ );
4493
+ const pageBands = this.getPageBands(
4494
+ ownedDimensions,
4495
+ pageSectionIndex,
4496
+ ownedPageInSection,
4497
+ ownedDisplayedPageNumber
4498
+ );
4499
+ const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4500
+ pageContinuation = currentContinuation;
4501
+ currentFootnoteHeight = this.measureFootnotesHeight(
4502
+ currentFootnoteIds,
4503
+ ownedDimensions.contentWidth,
4504
+ pageContinuation,
4505
+ currentPartialFootnotes
4506
+ );
4507
+ if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
4508
+ const fittingIds = [];
4509
+ for (let index = 0; index < currentFootnoteIds.length; index++) {
4510
+ const footnoteId = currentFootnoteIds[index];
4511
+ const candidateIds = [...fittingIds, footnoteId];
4512
+ const candidateHeight = this.measureFootnotesHeight(
4513
+ candidateIds,
4514
+ ownedDimensions.contentWidth,
4515
+ pageContinuation
4516
+ );
4517
+ const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
4518
+ if (guardedCandidateHeight <= maxFootnoteHeight) {
4519
+ fittingIds.push(footnoteId);
4520
+ currentFootnoteHeight = guardedCandidateHeight;
4521
+ continue;
4522
+ }
4523
+ const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
4524
+ if (fittingIds.length === 0 && !hasPageContinuation) {
4525
+ const source = this.footnoteRegistry.get(footnoteId);
4526
+ const split = source ? this.splitFootnoteToFit(
4527
+ source,
4528
+ maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
4529
+ ownedDimensions.contentWidth,
4530
+ true
4531
+ ) : null;
4532
+ fittingIds.push(footnoteId);
4533
+ if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
4534
+ currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
4535
+ nextPageContinuation = {
4536
+ footnoteId,
4537
+ sourceAnchorId: source.dataset.sourceAnchorId,
4538
+ remainingElements: split.overflow
4539
+ };
4540
+ currentFootnoteHeight = maxFootnoteHeight;
4541
+ } else {
4542
+ currentFootnoteHeight = guardedCandidateHeight;
4543
+ }
4544
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
4545
+ } else {
4546
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
4547
+ }
4548
+ break;
4549
+ }
4550
+ currentFootnoteIds = fittingIds;
4551
+ }
2618
4552
  const page = this.createPage(
2619
- dims,
4553
+ ownedDimensions,
2620
4554
  pageNumber,
2621
- sectionIndex,
4555
+ pageSectionIndex,
4556
+ ownedDisplayedPageNumber,
2622
4557
  currentContent,
2623
- pageInSection,
4558
+ ownedPageInSection,
2624
4559
  currentFootnoteIds,
2625
4560
  currentFootnoteHeight,
2626
- currentContinuation,
2627
- currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
4561
+ pageContinuation,
4562
+ currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
4563
+ false,
4564
+ currentPageAdmitted
2628
4565
  );
2629
4566
  pages.push(page);
4567
+ precedingDisplayedPageNumber = ownedDisplayedPageNumber;
2630
4568
  pageNumber++;
2631
- pageInSection++;
2632
4569
  currentContent = [];
2633
- const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
4570
+ pageSectionIndex = nextPageSectionIndex;
4571
+ if (!sectionStartPages.has(pageSectionIndex)) {
4572
+ sectionStartPages.set(pageSectionIndex, pageNumber);
4573
+ }
4574
+ const newBands = this.getPageBands(
4575
+ dimensionsFor(),
4576
+ pageSectionIndex,
4577
+ pageInSection(),
4578
+ displayedPageNumber()
4579
+ );
2634
4580
  effectiveContentHeight = newBands.bodyHeight;
2635
4581
  remainingHeight = effectiveContentHeight;
2636
4582
  prevMarginBottomPt = 0;
2637
4583
  currentFootnoteIds = [];
4584
+ currentPageHasFootnoteReference = false;
2638
4585
  currentPartialFootnotes = [];
2639
4586
  currentContinuation = nextPageContinuation;
2640
4587
  nextPageContinuation = null;
4588
+ currentContinuationPartitioned = false;
4589
+ currentPageAdmitted = false;
2641
4590
  if (deferredFootnoteIds.length > 0) {
2642
4591
  currentFootnoteIds = [...deferredFootnoteIds];
2643
4592
  deferredFootnoteIds = [];
2644
4593
  }
2645
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
2646
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
2647
- } else {
2648
- currentFootnoteHeight = 0;
2649
- }
2650
- if (currentFootnoteIds.length > 0) {
2651
- currentFootnoteHeight += this.measureFootnotesHeight(
2652
- currentFootnoteIds,
2653
- dims.contentWidth,
2654
- null
2655
- );
2656
- }
4594
+ const nextDimensions = dimensionsFor();
4595
+ currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
2657
4596
  };
4597
+ const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
2658
4598
  for (let i = 0; i < blocks.length; i++) {
4599
+ this.checkpoint();
2659
4600
  const block = blocks[i];
4601
+ const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
4602
+ if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
4603
+ finishPage(block.sectionIndex);
4604
+ }
4605
+ const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
4606
+ if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
4607
+ adoptEmptyPageOwner(block.sectionIndex);
4608
+ }
4609
+ prepareCurrentContinuation();
4610
+ if (currentFootnoteHeight > 0) {
4611
+ const ownerDimensions = dimensionsFor();
4612
+ const ownerBands = this.getPageBands(
4613
+ ownerDimensions,
4614
+ pageSectionIndex,
4615
+ pageInSection(),
4616
+ displayedPageNumber()
4617
+ );
4618
+ const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4619
+ if (currentFootnoteHeight > ownerMaxFootnoteArea) {
4620
+ const fittingIds = [];
4621
+ let fittingHeight = 0;
4622
+ if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
4623
+ for (const footnoteId of currentFootnoteIds) {
4624
+ this.checkpoint();
4625
+ const candidateHeight = this.measureFootnotesHeight(
4626
+ [...fittingIds, footnoteId],
4627
+ ownerDimensions.contentWidth
4628
+ );
4629
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
4630
+ fittingIds.push(footnoteId);
4631
+ fittingHeight = candidateHeight;
4632
+ }
4633
+ }
4634
+ if (fittingIds.length > 0) {
4635
+ deferredFootnoteIds = [
4636
+ ...currentFootnoteIds.slice(fittingIds.length),
4637
+ ...deferredFootnoteIds
4638
+ ];
4639
+ currentFootnoteIds = fittingIds;
4640
+ currentFootnoteHeight = fittingHeight;
4641
+ } else {
4642
+ finishPage(block.sectionIndex);
4643
+ i--;
4644
+ continue;
4645
+ }
4646
+ }
4647
+ }
4648
+ const blockDimensions = dimensionsFor(block.sectionIndex);
4649
+ const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
4650
+ const freshBlockPageBodyHeight = this.getPageBands(
4651
+ blockDimensions,
4652
+ block.sectionIndex,
4653
+ nextBlockPageInSection,
4654
+ displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
4655
+ ).bodyHeight;
2660
4656
  if (block.isPageBreak) {
2661
- finishPage();
4657
+ finishPage(block.sectionIndex);
2662
4658
  continue;
2663
4659
  }
2664
4660
  if (block.pageBreakBefore && currentContent.length > 0) {
2665
- finishPage();
4661
+ finishPage(block.sectionIndex);
2666
4662
  }
2667
4663
  const previousBlock = blocks[i - 1];
2668
4664
  const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
@@ -2678,7 +4674,7 @@ var Docxodus = (() => {
2678
4674
  if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
2679
4675
  const totalChainFootnoteHeight = this.measureFootnotesHeight(
2680
4676
  [...currentFootnoteIds, ...newChainFootnoteIds],
2681
- dims.contentWidth,
4677
+ blockDimensions.contentWidth,
2682
4678
  currentContinuation
2683
4679
  );
2684
4680
  additionalChainFootnoteHeight = Math.max(
@@ -2690,61 +4686,68 @@ var Docxodus = (() => {
2690
4686
  keepChain,
2691
4687
  prevMarginBottomPt,
2692
4688
  currentContent.length === 0,
2693
- pageInSection
4689
+ pageInSection(block.sectionIndex)
2694
4690
  ) + additionalChainFootnoteHeight;
2695
4691
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
2696
4692
  if (currentChainHeight > currentAvailableHeight) {
2697
4693
  const nextPageBands = this.getPageBands(
2698
- dims,
2699
- sectionIndex,
2700
- pageInSection + 1,
2701
- pageNumber + 1
4694
+ dimensionsFor(block.sectionIndex),
4695
+ block.sectionIndex,
4696
+ pageInSection(block.sectionIndex, pageNumber + 1),
4697
+ displayedPageNumber(
4698
+ block.sectionIndex,
4699
+ pageInSection(block.sectionIndex, pageNumber + 1),
4700
+ pageNumber + 1
4701
+ )
2702
4702
  );
2703
4703
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
2704
4704
  keepChain,
2705
4705
  0,
2706
4706
  true,
2707
- pageInSection + 1
4707
+ pageInSection(block.sectionIndex, pageNumber + 1)
2708
4708
  );
2709
4709
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
2710
4710
  newChainFootnoteIds,
2711
- dims.contentWidth,
4711
+ blockDimensions.contentWidth,
2712
4712
  nextPageContinuation
2713
4713
  );
2714
4714
  if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
2715
- finishPage();
4715
+ finishPage(block.sectionIndex);
2716
4716
  }
2717
4717
  }
2718
4718
  }
2719
4719
  }
2720
- const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
2721
4720
  const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
4721
+ let combinedFootnoteHeight = currentFootnoteHeight;
2722
4722
  let additionalFootnoteHeight = 0;
2723
4723
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
2724
4724
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
2725
- const totalFootnoteHeight = this.measureFootnotesHeight(
4725
+ combinedFootnoteHeight = this.measureFootnotesHeight(
2726
4726
  combinedFootnoteIds,
2727
- dims.contentWidth,
2728
- currentContinuation
4727
+ blockDimensions.contentWidth,
4728
+ currentContinuation,
4729
+ currentPartialFootnotes
4730
+ );
4731
+ additionalFootnoteHeight = Math.max(
4732
+ 0,
4733
+ combinedFootnoteHeight - currentFootnoteHeight
2729
4734
  );
2730
- additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
2731
4735
  }
2732
4736
  const isFirstOnPage = currentContent.length === 0;
2733
4737
  const effectiveMarginTop = this.effectiveBlockMarginTop(
2734
4738
  block,
2735
4739
  prevMarginBottomPt,
2736
4740
  isFirstOnPage,
2737
- pageInSection
4741
+ pageInSection(block.sectionIndex)
2738
4742
  );
2739
4743
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
2740
4744
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
2741
4745
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
2742
4746
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
2743
- const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
2744
4747
  if (blockSpace > effectiveRemainingHeight) {
2745
4748
  const paragraphFragments = this.tryFragmentParagraph(
2746
4749
  block,
2747
- dims,
4750
+ blockDimensions,
2748
4751
  effectiveRemainingHeight,
2749
4752
  effectiveMarginTop
2750
4753
  );
@@ -2754,40 +4757,74 @@ var Docxodus = (() => {
2754
4757
  continue;
2755
4758
  }
2756
4759
  }
2757
- if (blockSpace <= effectiveRemainingHeight) {
2758
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
4760
+ if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
4761
+ markSectionPlaced(block.sectionIndex);
4762
+ currentContent.push(this.cloneBlockForPage(
4763
+ block,
4764
+ isFirstOnPage,
4765
+ pageInSection(block.sectionIndex)
4766
+ ));
2759
4767
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
2760
4768
  prevMarginBottomPt = block.marginBottomPt;
2761
4769
  if (newFootnoteIds.length > 0) {
2762
4770
  currentFootnoteIds.push(...newFootnoteIds);
2763
- currentFootnoteHeight += additionalFootnoteHeight;
4771
+ currentFootnoteHeight = combinedFootnoteHeight;
2764
4772
  }
2765
- } else if (block.heightPt + this.effectiveBlockMarginTop(block, 0, true, pageInSection + 1) <= effectiveContentHeight) {
4773
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
4774
+ } else if (block.heightPt + this.effectiveBlockMarginTop(
4775
+ block,
4776
+ 0,
4777
+ true,
4778
+ pageInSection(block.sectionIndex, pageNumber + 1)
4779
+ ) <= freshBlockPageBodyHeight) {
2766
4780
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
2767
- const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
2768
- const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
2769
4781
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
2770
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
4782
+ markSectionPlaced(block.sectionIndex);
4783
+ currentContent.push(this.cloneBlockForPage(
4784
+ block,
4785
+ isFirstOnPage,
4786
+ pageInSection(block.sectionIndex)
4787
+ ));
2771
4788
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
2772
4789
  prevMarginBottomPt = block.marginBottomPt;
2773
4790
  const availableForFootnotes = Math.min(
2774
4791
  maxFootnoteArea,
2775
4792
  effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
2776
4793
  );
2777
- for (const footnoteId of newFootnoteIds) {
4794
+ const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
4795
+ const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
4796
+ for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
4797
+ this.checkpoint();
4798
+ const footnoteId = newFootnoteIds[noteIndex];
2778
4799
  const footnote = this.footnoteRegistry.get(footnoteId);
2779
4800
  if (!footnote) continue;
2780
- const footnoteHeight = this.measureSingleFootnoteHeight(footnoteId, dims.contentWidth);
4801
+ if (nextPageContinuation) {
4802
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4803
+ break;
4804
+ }
4805
+ const candidateIds = [...currentFootnoteIds, footnoteId];
4806
+ const candidateHeight = this.measureFootnotesHeight(
4807
+ candidateIds,
4808
+ blockDimensions.contentWidth,
4809
+ currentContinuation,
4810
+ currentPartialFootnotes
4811
+ );
2781
4812
  const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
2782
- if (footnoteHeight <= spaceLeftForFootnotes) {
4813
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
2783
4814
  currentFootnoteIds.push(footnoteId);
2784
- currentFootnoteHeight += footnoteHeight;
4815
+ currentFootnoteHeight = candidateHeight;
2785
4816
  } else {
2786
4817
  if (spaceLeftForFootnotes > 20) {
2787
4818
  const { fits, overflow } = this.splitFootnoteToFit(
2788
4819
  footnote,
2789
- spaceLeftForFootnotes,
2790
- dims.contentWidth
4820
+ availableForFootnotes,
4821
+ blockDimensions.contentWidth,
4822
+ deferralCannotHelp,
4823
+ {
4824
+ footnoteIds: currentFootnoteIds,
4825
+ continuation: currentContinuation,
4826
+ partialFootnotes: currentPartialFootnotes
4827
+ }
2791
4828
  );
2792
4829
  if (fits.length > 0) {
2793
4830
  currentFootnoteIds.push(footnoteId);
@@ -2798,78 +4835,70 @@ var Docxodus = (() => {
2798
4835
  if (overflow.length > 0) {
2799
4836
  nextPageContinuation = {
2800
4837
  footnoteId,
4838
+ sourceAnchorId: footnote.dataset.sourceAnchorId,
2801
4839
  remainingElements: overflow
2802
4840
  };
2803
4841
  }
2804
- currentFootnoteHeight = availableForFootnotes;
4842
+ currentFootnoteHeight = this.measureFootnotesHeight(
4843
+ currentFootnoteIds,
4844
+ blockDimensions.contentWidth,
4845
+ currentContinuation,
4846
+ currentPartialFootnotes
4847
+ );
4848
+ if (overflow.length > 0) {
4849
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
4850
+ break;
4851
+ }
2805
4852
  } else {
2806
- deferredFootnoteIds.push(footnoteId);
4853
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4854
+ break;
2807
4855
  }
2808
4856
  } else {
2809
- deferredFootnoteIds.push(footnoteId);
4857
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
4858
+ break;
2810
4859
  }
2811
4860
  }
2812
4861
  }
2813
- } else if (canExpandFootnotes && newFootnoteIds.length > 0) {
2814
- const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
2815
- const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
2816
- if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
2817
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
2818
- remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
2819
- prevMarginBottomPt = block.marginBottomPt;
2820
- currentFootnoteIds.push(...newFootnoteIds);
2821
- currentFootnoteHeight = expandedFootnoteSpace;
2822
- } else {
2823
- finishPage();
2824
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
2825
- const newPageMarginTop = this.effectiveBlockMarginTop(
2826
- block,
2827
- 0,
2828
- true,
2829
- pageInSection
2830
- );
2831
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
2832
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2833
- remainingHeight = effectiveContentHeight - newPageSpace;
2834
- prevMarginBottomPt = block.marginBottomPt;
2835
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2836
- currentFootnoteHeight = newPageFootnoteHeight;
2837
- }
4862
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2838
4863
  } else {
2839
- finishPage();
2840
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
2841
- const newPageMarginTop = this.effectiveBlockMarginTop(
2842
- block,
2843
- 0,
2844
- true,
2845
- pageInSection
2846
- );
2847
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
2848
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2849
- remainingHeight = effectiveContentHeight - newPageSpace;
2850
- prevMarginBottomPt = block.marginBottomPt;
2851
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2852
- currentFootnoteHeight = newPageFootnoteHeight;
4864
+ finishPage(block.sectionIndex, !pageHasPayload());
4865
+ i--;
4866
+ continue;
2853
4867
  }
2854
4868
  } else {
2855
- const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
4869
+ const tableFragments = this.trySplitSimpleOversizedTable(
4870
+ block,
4871
+ dimensionsFor(block.sectionIndex),
4872
+ block.sectionIndex
4873
+ );
2856
4874
  if (tableFragments) {
2857
4875
  if (currentContent.length > 0 || currentContinuation) {
2858
- finishPage();
4876
+ finishPage(block.sectionIndex);
2859
4877
  }
2860
4878
  blocks.splice(i, 1, ...tableFragments);
2861
4879
  i--;
2862
4880
  continue;
2863
4881
  }
2864
- if (currentContent.length > 0) {
2865
- finishPage();
4882
+ if (pageHasPayload()) {
4883
+ finishPage(block.sectionIndex);
4884
+ i--;
4885
+ continue;
2866
4886
  }
2867
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
2868
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
2869
- finishPage();
4887
+ markSectionPlaced(block.sectionIndex);
4888
+ currentContent.push(this.cloneBlockForPage(
4889
+ block,
4890
+ true,
4891
+ pageInSection(block.sectionIndex)
4892
+ ));
4893
+ deferredFootnoteIds.push(...newFootnoteIds);
4894
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
4895
+ finishPage(block.sectionIndex);
2870
4896
  }
2871
4897
  }
2872
4898
  finishPage();
4899
+ while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
4900
+ finishPage();
4901
+ }
2873
4902
  this.pendingFootnoteContinuation = nextPageContinuation;
2874
4903
  return pages;
2875
4904
  }
@@ -2894,6 +4923,19 @@ var Docxodus = (() => {
2894
4923
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
2895
4924
  }
2896
4925
  }
4926
+ /** A repeated margin note is presentation, not a second bookmark/link target. */
4927
+ makeClonedMarginCommentInert(root) {
4928
+ this.makeClonedStoryInert(root);
4929
+ const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
4930
+ for (const element of nodes) {
4931
+ element.removeAttribute("id");
4932
+ if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
4933
+ element.removeAttribute("href");
4934
+ element.setAttribute("aria-disabled", "true");
4935
+ element.tabIndex = -1;
4936
+ }
4937
+ }
4938
+ }
2897
4939
  /**
2898
4940
  * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
2899
4941
  *
@@ -2919,7 +4961,7 @@ var Docxodus = (() => {
2919
4961
  const context = {
2920
4962
  staticLeft: toPageX(staticRect.left),
2921
4963
  staticTop: toPageY(staticRect.top),
2922
- lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
4964
+ lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2923
4965
  paragraphLeft: toPageX(paragraphRect.left),
2924
4966
  paragraphTop: toPageY(paragraphRect.top),
2925
4967
  paragraphWidth: paragraphRect.width / pixelsPerPoint,
@@ -3049,36 +5091,48 @@ var Docxodus = (() => {
3049
5091
  else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
3050
5092
  return reference.start + (reference.size - objectSize) * fraction;
3051
5093
  }
5094
+ /** Charge a physical page before any page-owned partitioning or DOM allocation. */
5095
+ admitPageAllocation() {
5096
+ const prospectivePageCount = this.createdPageCount + 1;
5097
+ this.pageCountCheckpoint?.(prospectivePageCount);
5098
+ this.createdPageCount = prospectivePageCount;
5099
+ }
3052
5100
  /**
3053
5101
  * Creates a page container element.
3054
5102
  */
3055
- createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
3056
- const pageBox = document.createElement("div");
5103
+ createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
5104
+ if (!pageAlreadyAdmitted) this.admitPageAllocation();
5105
+ const pageBox = this.document.createElement("div");
3057
5106
  pageBox.className = `${this.cssPrefix}box`;
3058
5107
  pageBox.style.width = `${dims.pageWidth}pt`;
3059
5108
  pageBox.style.height = `${dims.pageHeight}pt`;
3060
5109
  pageBox.style.overflow = "hidden";
3061
5110
  pageBox.style.position = "relative";
3062
5111
  if (this.scale !== 1) {
3063
- pageBox.style.zoom = String(this.scale);
3064
- pageBox.style.transform = `scale(${this.scale})`;
3065
- pageBox.style.transformOrigin = "top left";
3066
- const heightReductionPt = dims.pageHeight * (1 - this.scale);
3067
- const widthReductionPt = dims.pageWidth * (1 - this.scale);
3068
- const heightReductionPx = ptToPx(heightReductionPt);
3069
- const widthReductionPx = ptToPx(widthReductionPt);
3070
- pageBox.style.marginRight = `-${widthReductionPx}px`;
3071
- pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
5112
+ if (this.view.CSS?.supports("zoom", "1")) {
5113
+ pageBox.style.zoom = String(this.scale);
5114
+ } else {
5115
+ pageBox.style.transform = `scale(${this.scale})`;
5116
+ pageBox.style.transformOrigin = "top left";
5117
+ const heightReductionPt = dims.pageHeight * (1 - this.scale);
5118
+ const widthReductionPt = dims.pageWidth * (1 - this.scale);
5119
+ const heightReductionPx = ptToPx(heightReductionPt);
5120
+ const widthReductionPx = ptToPx(widthReductionPt);
5121
+ pageBox.style.marginRight = `-${widthReductionPx}px`;
5122
+ pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
5123
+ }
3072
5124
  }
3073
5125
  pageBox.style.willChange = "transform";
3074
5126
  pageBox.style.contain = "layout paint";
3075
5127
  pageBox.dataset.pageNumber = String(pageNumber);
3076
5128
  pageBox.dataset.sectionIndex = String(sectionIndex);
5129
+ pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
5130
+ if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
3077
5131
  pageBox.dataset.pageInSection = String(pageInSection);
3078
- const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
3079
- const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
5132
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
5133
+ const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
3080
5134
  if (headerSource) {
3081
- const headerDiv = document.createElement("div");
5135
+ const headerDiv = this.document.createElement("div");
3082
5136
  headerDiv.className = `${this.cssPrefix}header`;
3083
5137
  headerDiv.style.position = "absolute";
3084
5138
  headerDiv.style.top = `${bands.headerTop}pt`;
@@ -3100,7 +5154,7 @@ var Docxodus = (() => {
3100
5154
  }
3101
5155
  const contentAreaTop = bands.bodyTop;
3102
5156
  const contentAreaHeight = bands.bodyHeight;
3103
- const contentArea = document.createElement("div");
5157
+ const contentArea = this.document.createElement("div");
3104
5158
  contentArea.className = `${this.cssPrefix}content`;
3105
5159
  contentArea.style.position = "absolute";
3106
5160
  contentArea.style.top = `${contentAreaTop}pt`;
@@ -3113,12 +5167,46 @@ var Docxodus = (() => {
3113
5167
  }
3114
5168
  pageBox.appendChild(contentArea);
3115
5169
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
3116
- if (footnoteIds.length > 0 || hasContinuation) {
5170
+ if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
3117
5171
  this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
3118
5172
  }
3119
- const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
5173
+ const pageCommentIds = [];
5174
+ for (const marker of Array.from(
5175
+ pageBox.querySelectorAll("[data-comment-id]")
5176
+ )) {
5177
+ if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
5178
+ const id = marker.dataset.commentId;
5179
+ if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
5180
+ pageCommentIds.push(id);
5181
+ }
5182
+ }
5183
+ if (pageCommentIds.length > 0) {
5184
+ const marginColumn = this.document.createElement("aside");
5185
+ marginColumn.className = `${this.cssPrefix}comment-margin`;
5186
+ marginColumn.style.position = "absolute";
5187
+ marginColumn.style.top = `${contentAreaTop}pt`;
5188
+ marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
5189
+ marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
5190
+ marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
5191
+ marginColumn.style.overflow = "hidden";
5192
+ marginColumn.style.boxSizing = "border-box";
5193
+ const placedThreadRoots = /* @__PURE__ */ new Set();
5194
+ for (const id of pageCommentIds) {
5195
+ const source = this.commentMarginRegistry.get(id);
5196
+ if (source) {
5197
+ const rootId = source.dataset.commentId ?? id;
5198
+ if (placedThreadRoots.has(rootId)) continue;
5199
+ placedThreadRoots.add(rootId);
5200
+ const clone = source.cloneNode(true);
5201
+ this.makeClonedMarginCommentInert(clone);
5202
+ marginColumn.appendChild(clone);
5203
+ }
5204
+ }
5205
+ pageBox.appendChild(marginColumn);
5206
+ }
5207
+ const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
3120
5208
  if (footerSource) {
3121
- const footerDiv = document.createElement("div");
5209
+ const footerDiv = this.document.createElement("div");
3122
5210
  footerDiv.className = `${this.cssPrefix}footer`;
3123
5211
  footerDiv.style.position = "absolute";
3124
5212
  footerDiv.style.top = "auto";
@@ -3138,8 +5226,8 @@ var Docxodus = (() => {
3138
5226
  }
3139
5227
  pageBox.appendChild(footerDiv);
3140
5228
  }
3141
- if (this.showPageNumbers) {
3142
- const pageNum = document.createElement("div");
5229
+ if (this.showPageNumbers && !isSectionFiller) {
5230
+ const pageNum = this.document.createElement("div");
3143
5231
  pageNum.className = `${this.cssPrefix}number`;
3144
5232
  pageNum.textContent = String(pageNumber);
3145
5233
  pageBox.appendChild(pageNum);
@@ -3162,7 +5250,8 @@ var Docxodus = (() => {
3162
5250
  }
3163
5251
  };
3164
5252
  function paginateHtml(html, container, options = {}) {
3165
- const containerEl = typeof container === "string" ? document.getElementById(container) : container;
5253
+ const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
5254
+ const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
3166
5255
  if (!containerEl) {
3167
5256
  throw new Error("Container element not found");
3168
5257
  }
@@ -6464,6 +8553,13 @@ var Docxodus = (() => {
6464
8553
  function escapeInlineMarkdown(text) {
6465
8554
  return text.replace(/([\\`*_[\]])/g, "\\$1");
6466
8555
  }
8556
+ function escapeLeadingBlockMarkers(md) {
8557
+ return md.split("\n").map((line) => {
8558
+ if (/^[#>|]/.test(line) || /^[-+] /.test(line)) return "\\" + line;
8559
+ if (/^\d+\. /.test(line)) return line.replace(".", "\\.");
8560
+ return line;
8561
+ }).join("\n");
8562
+ }
6467
8563
  function collectInlineSegments(node, out) {
6468
8564
  node.childNodes.forEach((child) => {
6469
8565
  if (isGeneratedChrome(child)) return;
@@ -6515,7 +8611,7 @@ var Docxodus = (() => {
6515
8611
  merged.push({ ...s });
6516
8612
  }
6517
8613
  }
6518
- return merged.map(segToMarkdown).join("").trim();
8614
+ return escapeLeadingBlockMarkers(merged.map(segToMarkdown).join("").trim());
6519
8615
  }
6520
8616
  function isListBlock(block) {
6521
8617
  return !!block.querySelector(":scope > [data-list-marker]");
@@ -8550,17 +10646,19 @@ var Docxodus = (() => {
8550
10646
  this.refreshAfter(block, idx, false);
8551
10647
  }
8552
10648
  // ─── Table row / column editing (active block must be inside a table cell) ──────────
8553
- /** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */
10649
+ /** Run a table-structure op on the active cell's canonical tc anchor and re-render. */
8554
10650
  tableEdit(run) {
8555
10651
  const block = this.activeBlock;
8556
- if (this.closed || !block || !block.closest("table")) return;
10652
+ const cell = block?.closest("td, th");
10653
+ if (this.closed || !block || !cell || !block.closest("table")) return;
8557
10654
  const unid = block.getAttribute("data-anchor");
8558
10655
  if (!unid) return;
8559
- let fullId = this.anchorIdOf(block);
8560
- if (!fullId) return;
10656
+ const paragraphId = this.anchorIdOf(block);
10657
+ const cellId = this.anchorIdOf(cell);
10658
+ if (!paragraphId || !cellId) return;
8561
10659
  const idx = this.blockIndex(block);
8562
- fullId = this.syncBlock(block, fullId);
8563
- const res = this.parseEdit(run(fullId));
10660
+ this.syncBlock(block, paragraphId);
10661
+ const res = this.parseEdit(run(cellId));
8564
10662
  if (!res.success) return;
8565
10663
  this.refreshAfter(block, idx, false);
8566
10664
  }
@@ -8713,6 +10811,59 @@ var Docxodus = (() => {
8713
10811
  if (!res.success) return;
8714
10812
  this.reconcile(idx, false);
8715
10813
  }
10814
+ /**
10815
+ * Add a native Word comment on the current selection in the active block — or on the whole
10816
+ * block when the selection is collapsed (a null span comments the paragraph). The definition,
10817
+ * threading parts, and body-side range markers are written by the session's `AddComment`
10818
+ * (issue #300); the re-render shows the marker chrome the converter already draws. The
10819
+ * annotation type legal review runs on, finally authorable from the shipped surface
10820
+ * (issue #580).
10821
+ */
10822
+ addComment(markdown = "New comment.", author = "Reviewer") {
10823
+ const block = this.activeBlock;
10824
+ if (this.closed || !block) return;
10825
+ const bridge = this.exports.DocxSessionBridge;
10826
+ if (!bridge.AddComment) return;
10827
+ let fullId = this.anchorIdOf(block);
10828
+ if (!fullId) return;
10829
+ const idx = this.blockIndex(block);
10830
+ const span = selectionSpanIn(block);
10831
+ fullId = this.syncBlock(block, fullId);
10832
+ const res = this.parseEdit(
10833
+ bridge.AddComment(
10834
+ this.handle,
10835
+ fullId,
10836
+ span ? JSON.stringify(span) : "",
10837
+ author,
10838
+ "",
10839
+ "",
10840
+ markdown
10841
+ )
10842
+ );
10843
+ if (!res.success) return;
10844
+ this.reconcile(idx, false);
10845
+ }
10846
+ /** The document's native comment threads (session truth), for review UIs — flat entries
10847
+ * with `parentAnchorId` linking replies and `resolved` carrying thread state. */
10848
+ listComments() {
10849
+ if (this.closed) return [];
10850
+ const bridge = this.exports.DocxSessionBridge;
10851
+ if (!bridge.ListComments) return [];
10852
+ try {
10853
+ return JSON.parse(bridge.ListComments(this.handle));
10854
+ } catch {
10855
+ return [];
10856
+ }
10857
+ }
10858
+ /** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}).
10859
+ * Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is
10860
+ * needed — a review UI re-reads {@link listComments} for the new state. */
10861
+ setCommentResolved(commentAnchorId, resolved) {
10862
+ if (this.closed) return;
10863
+ const bridge = this.exports.DocxSessionBridge;
10864
+ if (!bridge.SetCommentResolved) return;
10865
+ this.parseEdit(bridge.SetCommentResolved(this.handle, commentAnchorId, resolved));
10866
+ }
8716
10867
  applyParagraphFormat(op) {
8717
10868
  const block = this.activeBlock;
8718
10869
  if (this.closed || !block) return;
@@ -9240,8 +11391,8 @@ var Docxodus = (() => {
9240
11391
  const backref = document.createElement("a");
9241
11392
  backref.setAttribute("class", `${prefix}-backref`);
9242
11393
  backref.setAttribute("contenteditable", "false");
9243
- backref.textContent = "\u21A9";
9244
- last.append(" ", backref);
11394
+ backref.textContent = " \u21A9";
11395
+ last.append(backref);
9245
11396
  }
9246
11397
  li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
9247
11398
  return li;
@@ -9798,6 +11949,25 @@ var Docxodus = (() => {
9798
11949
  box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
9799
11950
  }
9800
11951
 
11952
+ /* \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 */
11953
+ .dxr-threads {
11954
+ display: flex; flex-direction: column; gap: 4px;
11955
+ max-height: 92px; overflow-y: auto; min-width: 260px; padding-right: 2px;
11956
+ }
11957
+ .dxr-thread {
11958
+ display: flex; align-items: center; gap: 6px;
11959
+ font-size: 11px; line-height: 1.3; padding: 3px 6px;
11960
+ border: 1px solid var(--dxr-rule); border-radius: 4px; background: var(--dxr-chrome-sunk);
11961
+ }
11962
+ .dxr-thread[data-resolved] { opacity: 0.55; }
11963
+ .dxr-thread[data-reply] { margin-left: 14px; }
11964
+ .dxr-thread .dxr-tauthor { font-weight: 600; white-space: nowrap; }
11965
+ .dxr-thread .dxr-ttext {
11966
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
11967
+ }
11968
+ .dxr-thread button { flex: none; }
11969
+ .dxr-group.dxr-grow { flex: 1; min-width: 0; }
11970
+
9801
11971
  /* \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 */
9802
11972
  .dxr-pop {
9803
11973
  display: none;
@@ -10116,6 +12286,7 @@ var Docxodus = (() => {
10116
12286
  <button type="button" class="dxr-tab" role="tab" data-tab="home" aria-selected="true">Home</button>
10117
12287
  <button type="button" class="dxr-tab" role="tab" data-tab="insert" aria-selected="false">Insert</button>
10118
12288
  <button type="button" class="dxr-tab" role="tab" data-tab="layout" aria-selected="false">Layout</button>
12289
+ <button type="button" class="dxr-tab" role="tab" data-tab="review" aria-selected="false">Review</button>
10119
12290
  <button type="button" class="dxr-tab" role="tab" data-tab="table" data-contextual aria-selected="false" hidden>Table</button>
10120
12291
  </div>
10121
12292
 
@@ -10281,6 +12452,31 @@ var Docxodus = (() => {
10281
12452
  </div>
10282
12453
  </div>
10283
12454
 
12455
+ <!-- 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
12456
+ Comment authoring over the session's native comment family (issue #580):
12457
+ the button comments the selection (the whole block when collapsed), and
12458
+ the thread list is live session truth with resolve/reopen per thread. -->
12459
+ <div class="dxr-panel" data-panel="review">
12460
+ <div class="dxr-group">
12461
+ <span class="dxr-glabel">Comments</span>
12462
+ <div class="dxr-row">
12463
+ <input data-dxr="commenttext" type="text" placeholder="Comment text&#8230;"
12464
+ title="The comment body; markdown is honored" />
12465
+ <button type="button" data-dxr="comment"
12466
+ title="Comment on the selection, or on the whole block when nothing is selected">&#128172; Comment</button>
12467
+ </div>
12468
+ <div class="dxr-row">
12469
+ <span class="dxr-note">Comments the selected text; the whole block when the caret is collapsed.</span>
12470
+ </div>
12471
+ </div>
12472
+ <div class="dxr-group dxr-grow">
12473
+ <span class="dxr-glabel">Threads</span>
12474
+ <div class="dxr-threads" data-dxr="threads" aria-live="polite">
12475
+ <span class="dxr-note">No comments yet.</span>
12476
+ </div>
12477
+ </div>
12478
+ </div>
12479
+
10284
12480
  <!-- 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
10285
12481
  Row/column editing lives here rather than in a floating toolbar: a docked
10286
12482
  contextual tab cannot overlap the cell you are editing. -->
@@ -10788,6 +12984,18 @@ var Docxodus = (() => {
10788
12984
  if (this.live && style.value) this.run("style", () => this.live.setParagraphStyle(style.value));
10789
12985
  style.value = "";
10790
12986
  });
12987
+ const commentButton = this.control("comment");
12988
+ if (commentButton) {
12989
+ this.keepSelection(commentButton);
12990
+ commentButton.addEventListener("click", () => {
12991
+ if (!this.live) return;
12992
+ const input = this.control("commenttext");
12993
+ const text = input?.value.trim() || "New comment.";
12994
+ this.run("comment", () => this.live.addComment(text, this.options.revisionAuthor ?? "Reviewer"));
12995
+ if (input) input.value = "";
12996
+ this.renderThreads();
12997
+ });
12998
+ }
10791
12999
  this.wireFileActions();
10792
13000
  this.wireLayout();
10793
13001
  this.wirePicker();
@@ -10910,6 +13118,52 @@ var Docxodus = (() => {
10910
13118
  else panel.removeAttribute("data-active");
10911
13119
  }
10912
13120
  if (name === "layout") this.syncPageNumbering();
13121
+ if (name === "review") this.renderThreads();
13122
+ }
13123
+ /** Re-render the Review tab's thread list from session truth ({@link DocxEditor.listComments}):
13124
+ * one row per comment, replies indented under their thread root, resolve/reopen per root. */
13125
+ renderThreads() {
13126
+ const host = this.control("threads");
13127
+ if (!host || !this.live) return;
13128
+ const doc = this.element.ownerDocument ?? document;
13129
+ host.textContent = "";
13130
+ const comments = this.live.listComments();
13131
+ if (comments.length === 0) {
13132
+ const empty = doc.createElement("span");
13133
+ empty.className = "dxr-note";
13134
+ empty.textContent = "No comments yet.";
13135
+ host.appendChild(empty);
13136
+ return;
13137
+ }
13138
+ for (const entry of comments) {
13139
+ const row = doc.createElement("div");
13140
+ row.className = "dxr-thread";
13141
+ row.setAttribute("data-thread", entry.anchorId);
13142
+ if (entry.resolved) row.setAttribute("data-resolved", "");
13143
+ if (entry.parentAnchorId) row.setAttribute("data-reply", "");
13144
+ const author = doc.createElement("span");
13145
+ author.className = "dxr-tauthor";
13146
+ author.textContent = entry.author || "\u2014";
13147
+ row.appendChild(author);
13148
+ const text = doc.createElement("span");
13149
+ text.className = "dxr-ttext";
13150
+ text.textContent = entry.text;
13151
+ text.title = entry.text;
13152
+ row.appendChild(text);
13153
+ if (!entry.parentAnchorId) {
13154
+ const toggle = doc.createElement("button");
13155
+ toggle.type = "button";
13156
+ toggle.textContent = entry.resolved ? "Reopen" : "Resolve";
13157
+ toggle.title = entry.resolved ? "Reopen this comment thread" : "Mark this comment thread resolved";
13158
+ toggle.addEventListener("click", () => {
13159
+ if (!this.live) return;
13160
+ this.run(entry.resolved ? "reopen comment" : "resolve comment", () => this.live.setCommentResolved(entry.anchorId, !entry.resolved));
13161
+ this.renderThreads();
13162
+ });
13163
+ row.appendChild(toggle);
13164
+ }
13165
+ host.appendChild(row);
13166
+ }
10913
13167
  }
10914
13168
  // ── selection-driven state ──────────────────────────────────────────────────
10915
13169
  selectionElement() {
@@ -11212,6 +13466,55 @@ var Docxodus = (() => {
11212
13466
  const buffer = await input.arrayBuffer();
11213
13467
  return new Uint8Array(buffer);
11214
13468
  }
13469
+ async function generatePackageManifest(document2) {
13470
+ const exports = ensureInitialized();
13471
+ const bytes = await toBytes(document2);
13472
+ await yieldToMain();
13473
+ return JSON.parse(
13474
+ exports.DocumentConverter.GeneratePackageManifest(bytes)
13475
+ );
13476
+ }
13477
+ async function verifyDeliverable(document2, baseline) {
13478
+ const exports = ensureInitialized();
13479
+ const bytes = await toBytes(document2);
13480
+ const baselineBytes = baseline === void 0 ? void 0 : await toBytes(baseline);
13481
+ await yieldToMain();
13482
+ return JSON.parse(
13483
+ baselineBytes === void 0 ? exports.DocumentConverter.VerifyDeliverable(bytes) : exports.DocumentConverter.VerifyDeliverableWithBaseline(
13484
+ baselineBytes,
13485
+ bytes
13486
+ )
13487
+ );
13488
+ }
13489
+ async function verifyDeliveryReceipt(receiptJson, artifacts) {
13490
+ const exports = ensureInitialized();
13491
+ const artifactsJson = artifacts === void 0 ? "" : JSON.stringify(
13492
+ Object.fromEntries(
13493
+ Object.entries(artifacts).map(([artifactId, bytes]) => [
13494
+ artifactId,
13495
+ bytesToBase64(bytes)
13496
+ ])
13497
+ )
13498
+ );
13499
+ await yieldToMain();
13500
+ return JSON.parse(
13501
+ exports.DocumentConverter.VerifyDeliveryReceipt(receiptJson, artifactsJson)
13502
+ );
13503
+ }
13504
+ async function proveRedlineReversibility(baseline, intendedFinal, redline) {
13505
+ const exports = ensureInitialized();
13506
+ const baselineBytes = await toBytes(baseline);
13507
+ const intendedFinalBytes = await toBytes(intendedFinal);
13508
+ const redlineBytes = await toBytes(redline);
13509
+ await yieldToMain();
13510
+ return JSON.parse(
13511
+ exports.DocumentConverter.ProveRedlineReversibility(
13512
+ baselineBytes,
13513
+ intendedFinalBytes,
13514
+ redlineBytes
13515
+ )
13516
+ );
13517
+ }
11215
13518
  async function renderBlockHtml(document2, anchorId, options) {
11216
13519
  const exports = ensureInitialized();
11217
13520
  const bytes = await toBytes(document2);
@@ -11472,21 +13775,30 @@ var Docxodus = (() => {
11472
13775
  throw new Error(`Failed to get DocxDiff revisions: ${error.error}`);
11473
13776
  }
11474
13777
  const parsed = JSON.parse(result);
11475
- return (parsed.revisions || parsed.Revisions || []).map((r) => ({
11476
- revisionType: r.revisionType ?? r.RevisionType,
11477
- text: r.text ?? r.Text,
11478
- author: r.author ?? r.Author,
11479
- date: r.date ?? r.Date,
11480
- moveGroupId: r.moveGroupId ?? r.MoveGroupId ?? void 0,
11481
- isMoveSource: r.isMoveSource ?? r.IsMoveSource ?? void 0,
11482
- formatChange: r.formatChange || r.FormatChange ? {
11483
- oldProperties: r.formatChange?.oldProperties ?? r.FormatChange?.OldProperties,
11484
- newProperties: r.formatChange?.newProperties ?? r.FormatChange?.NewProperties,
11485
- changedPropertyNames: r.formatChange?.changedPropertyNames ?? r.FormatChange?.ChangedPropertyNames
11486
- } : void 0,
11487
- leftAnchor: r.leftAnchor ?? r.LeftAnchor ?? void 0,
11488
- rightAnchor: r.rightAnchor ?? r.RightAnchor ?? void 0
11489
- }));
13778
+ return (parsed.revisions || parsed.Revisions || []).map(mapDocxDiffRevision);
13779
+ }
13780
+ async function docxDiffCompareProducts(left2, right2, settings, products) {
13781
+ const exports = ensureInitialized();
13782
+ const leftBytes = await toBytes(left2);
13783
+ const rightBytes = await toBytes(right2);
13784
+ await yieldToMain();
13785
+ const result = exports.DocxDiffBridge.CompareProductsJson(
13786
+ leftBytes,
13787
+ rightBytes,
13788
+ docxDiffSettingsJson(settings),
13789
+ products ? JSON.stringify(products) : ""
13790
+ );
13791
+ if (isErrorResponse(result)) {
13792
+ const error = parseError(result);
13793
+ throw new Error(`Failed to compare products: ${error.error}`);
13794
+ }
13795
+ const parsed = JSON.parse(result);
13796
+ return {
13797
+ redline: typeof parsed.redlineB64 === "string" ? Uint8Array.from(atob(parsed.redlineB64), (c) => c.charCodeAt(0)) : void 0,
13798
+ revisions: Array.isArray(parsed.revisions) ? parsed.revisions.map(mapDocxDiffRevision) : void 0,
13799
+ editScript: parsed.editScript !== void 0 ? parsed.editScript : void 0,
13800
+ semanticChanges: parsed.semanticChanges !== void 0 ? parsed.semanticChanges : void 0
13801
+ };
11490
13802
  }
11491
13803
  async function docxDiffGetEditScript(left2, right2, settings) {
11492
13804
  const exports = ensureInitialized();
@@ -11504,6 +13816,22 @@ var Docxodus = (() => {
11504
13816
  }
11505
13817
  return result;
11506
13818
  }
13819
+ async function docxDiffGetSemanticChanges(left2, right2, settings) {
13820
+ const exports = ensureInitialized();
13821
+ const leftBytes = await toBytes(left2);
13822
+ const rightBytes = await toBytes(right2);
13823
+ await yieldToMain();
13824
+ const result = exports.DocxDiffBridge.GetSemanticChangesJson(
13825
+ leftBytes,
13826
+ rightBytes,
13827
+ docxDiffSettingsJson(settings)
13828
+ );
13829
+ if (isErrorResponse(result)) {
13830
+ const error = parseError(result);
13831
+ throw new Error(`Failed to get semantic changes: ${error.error}`);
13832
+ }
13833
+ return JSON.parse(result);
13834
+ }
11507
13835
  async function docxDiffAcceptRevisions(redline) {
11508
13836
  const exports = ensureInitialized();
11509
13837
  const bytes = await toBytes(redline);
@@ -11757,6 +14085,7 @@ var Docxodus = (() => {
11757
14085
  const parsed = JSON.parse(result);
11758
14086
  const convertElement = (el) => ({
11759
14087
  id: el.Id || el.id,
14088
+ anchorId: el.AnchorId || el.anchorId,
11760
14089
  type: el.Type || el.type,
11761
14090
  textPreview: el.TextPreview || el.textPreview,
11762
14091
  index: el.Index ?? el.index,
@@ -11768,8 +14097,12 @@ var Docxodus = (() => {
11768
14097
  });
11769
14098
  const convertTableColumn = (col) => ({
11770
14099
  tableId: col.TableId || col.tableId,
14100
+ anchorId: col.AnchorId || col.anchorId,
14101
+ tableAnchorId: col.TableAnchorId || col.tableAnchorId,
14102
+ isVirtual: col.IsVirtual ?? col.isVirtual ?? false,
11771
14103
  columnIndex: col.ColumnIndex ?? col.columnIndex,
11772
14104
  cellIds: col.CellIds || col.cellIds || [],
14105
+ cellAnchorIds: col.CellAnchorIds || col.cellAnchorIds || [],
11773
14106
  rowCount: col.RowCount ?? col.rowCount
11774
14107
  });
11775
14108
  const root = convertElement(parsed.Root || parsed.root);
@@ -11832,7 +14165,8 @@ var Docxodus = (() => {
11832
14165
  hasEndnotes: parsed.HasEndnotes ?? parsed.hasEndnotes,
11833
14166
  hasTrackedChanges: parsed.HasTrackedChanges ?? parsed.hasTrackedChanges,
11834
14167
  hasComments: parsed.HasComments ?? parsed.hasComments,
11835
- estimatedPageCount: parsed.EstimatedPageCount ?? parsed.estimatedPageCount
14168
+ estimatedPageCount: parsed.EstimatedPageCount ?? parsed.estimatedPageCount,
14169
+ estimatedPageCountSource: parsed.EstimatedPageCountSource ?? parsed.estimatedPageCountSource ?? "heuristic"
11836
14170
  };
11837
14171
  }
11838
14172
  async function exportToOpenContract(document2) {