docxodus 9.8.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 (151) 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 +2024 -388
  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 +2912 -437
  16. package/dist/embed.iife.js +2911 -436
  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 +1937 -380
  41. package/dist/pagination.d.ts +277 -15
  42. package/dist/pagination.d.ts.map +1 -1
  43. package/dist/pagination.js +2055 -438
  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 +46 -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.NonGeneric.wasm +0 -0
  77. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
  78. package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
  79. package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
  80. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  81. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  82. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
  83. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
  84. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
  85. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
  86. package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
  87. package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
  88. package/dist/wasm/_framework/System.Console.wasm +0 -0
  89. package/dist/wasm/_framework/System.Console.wasm.br +0 -0
  90. package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
  91. package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
  92. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  93. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  94. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  95. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  96. package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
  97. package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
  98. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  99. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  100. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  101. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  102. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  103. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  104. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  105. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  106. package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
  107. package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
  108. package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
  109. package/dist/wasm/_framework/System.ObjectModel.wasm.br +0 -0
  110. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  111. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  112. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  113. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  114. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  115. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  116. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  117. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  118. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  119. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  120. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  121. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  122. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  123. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  124. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
  125. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
  126. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  127. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  128. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  129. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  130. package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
  131. package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
  132. package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
  133. package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
  134. package/dist/wasm/_framework/System.wasm +0 -0
  135. package/dist/wasm/_framework/System.wasm.br +0 -0
  136. package/dist/wasm/_framework/dotnet.boot.js +38 -38
  137. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  138. package/dist/wasm/_framework/dotnet.js +1 -1
  139. package/dist/wasm/_framework/dotnet.js.br +0 -0
  140. package/dist/wasm/_framework/dotnet.native.js +3 -3
  141. package/dist/wasm/_framework/dotnet.native.js.br +0 -0
  142. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  143. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  144. package/dist/wasm/_framework/dotnet.runtime.js +1 -1
  145. package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
  146. package/dist/worker-proxy.bundle.js +196 -29
  147. package/dist/worker-proxy.d.ts +36 -3
  148. package/dist/worker-proxy.d.ts.map +1 -1
  149. package/dist/worker-proxy.js +186 -33
  150. package/dist/worker-proxy.js.map +1 -1
  151. package/package.json +24 -7
@@ -22,6 +22,9 @@ var DocxodusPagination = (() => {
22
22
  var pagination_exports = {};
23
23
  __export(pagination_exports, {
24
24
  PaginationEngine: () => PaginationEngine,
25
+ clearPageCitationHighlight: () => clearPageCitationHighlight,
26
+ createUnavailablePageMap: () => createUnavailablePageMap,
27
+ navigateToPageCitation: () => navigateToPageCitation,
25
28
  paginateHtml: () => paginateHtml
26
29
  });
27
30
 
@@ -70,17 +73,41 @@ var DocxodusPagination = (() => {
70
73
  function ptToPx(pt) {
71
74
  return pt / 0.75;
72
75
  }
76
+ function finiteDataNumber(value, fallback) {
77
+ if (value === void 0 || value.trim() === "") return fallback;
78
+ const parsed = Number(value);
79
+ return Number.isFinite(parsed) ? parsed : fallback;
80
+ }
81
+ function positiveDataNumber(value, fallback) {
82
+ const parsed = finiteDataNumber(value, fallback);
83
+ return parsed > 0 ? parsed : fallback;
84
+ }
85
+ function nonNegativeDataNumber(value, fallback) {
86
+ const parsed = finiteDataNumber(value, fallback);
87
+ return parsed >= 0 ? parsed : fallback;
88
+ }
89
+ function positiveFinite(value, fallback) {
90
+ return Number.isFinite(value) && value > 0 ? value : fallback;
91
+ }
73
92
  function parseSectionDimensions(section) {
74
- const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
75
- const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
76
- const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
77
- const contentHeight = parseFloat(section.dataset.contentHeight || "") || pageHeight - 2 * DEFAULT_MARGIN;
78
- const marginTop = parseFloat(section.dataset.marginTop || "") || DEFAULT_MARGIN;
79
- const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
80
- const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
81
- const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
82
- const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
83
- const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
93
+ const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
94
+ const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
95
+ const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
96
+ const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
97
+ const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
98
+ const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
99
+ const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
100
+ const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
101
+ const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
102
+ const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
103
+ const headerDistance = nonNegativeDataNumber(
104
+ section.dataset.headerHeight,
105
+ DEFAULT_HEADER_FOOTER_DISTANCE
106
+ );
107
+ const footerDistance = nonNegativeDataNumber(
108
+ section.dataset.footerHeight,
109
+ DEFAULT_HEADER_FOOTER_DISTANCE
110
+ );
84
111
  return {
85
112
  pageWidth,
86
113
  pageHeight,
@@ -125,8 +152,96 @@ var DocxodusPagination = (() => {
125
152
  }
126
153
 
127
154
  // src/pagination.ts
155
+ function samePageBox(a, b) {
156
+ 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;
157
+ }
158
+ function createUnavailablePageMap(documentVersion, rendererFingerprint, mode = "continuous") {
159
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
160
+ return {
161
+ schemaVersion: 1,
162
+ mode,
163
+ availability: "unavailable",
164
+ documentVersion,
165
+ rendererFingerprint,
166
+ pages: [],
167
+ fragments: []
168
+ };
169
+ }
170
+ var activePageCitationHighlights = /* @__PURE__ */ new WeakMap();
171
+ function clearPageCitationHighlight(root) {
172
+ const active = activePageCitationHighlights.get(root);
173
+ if (!active) return;
174
+ if (active.highlightClass && active.addedClass) {
175
+ active.target.classList.remove(active.highlightClass);
176
+ }
177
+ for (const style of active.inlineStyles ?? []) {
178
+ if (style.value) {
179
+ active.target.style.setProperty(style.name, style.value, style.priority);
180
+ } else {
181
+ active.target.style.removeProperty(style.name);
182
+ }
183
+ }
184
+ activePageCitationHighlights.delete(root);
185
+ }
186
+ function navigateToPageCitation(root, citation, options = {}) {
187
+ clearPageCitationHighlight(root);
188
+ if (citation.availability !== "available" || citation.fragments.length === 0) {
189
+ return { navigated: false, unavailableReason: "citation_unavailable" };
190
+ }
191
+ const byAttribute = (name, value, within = root) => {
192
+ for (const node of Array.from(within.querySelectorAll(`[${name}]`))) {
193
+ if (node.getAttribute(name) === value) return node;
194
+ }
195
+ return null;
196
+ };
197
+ const fragment = citation.fragments[0];
198
+ let target = byAttribute("data-page-fragment-id", fragment.fragmentId);
199
+ if (!target) {
200
+ const page = byAttribute("data-page-number", String(fragment.pageNumber));
201
+ if (page) target = byAttribute("data-source-anchor-id", citation.anchorId, page) ?? page;
202
+ }
203
+ if (!target) {
204
+ return {
205
+ navigated: false,
206
+ pageNumber: fragment.pageNumber,
207
+ fragmentId: fragment.fragmentId,
208
+ unavailableReason: "fragment_not_found"
209
+ };
210
+ }
211
+ if (options.highlightClass) {
212
+ const addedClass = !target.classList.contains(options.highlightClass);
213
+ target.classList.add(options.highlightClass);
214
+ activePageCitationHighlights.set(root, {
215
+ target,
216
+ highlightClass: options.highlightClass,
217
+ addedClass
218
+ });
219
+ } else if (options.highlight !== false) {
220
+ const names = ["outline", "outline-offset", "background-color"];
221
+ const inlineStyles = names.map((name) => ({
222
+ name,
223
+ value: target.style.getPropertyValue(name),
224
+ priority: target.style.getPropertyPriority(name)
225
+ }));
226
+ target.style.setProperty("outline", "3px solid #f4b400", "important");
227
+ target.style.setProperty("outline-offset", "2px", "important");
228
+ target.style.setProperty("background-color", "rgba(255, 235, 59, .18)", "important");
229
+ activePageCitationHighlights.set(root, { target, inlineStyles });
230
+ }
231
+ target.scrollIntoView({
232
+ behavior: options.behavior ?? "smooth",
233
+ block: options.block ?? "center"
234
+ });
235
+ return {
236
+ navigated: true,
237
+ target,
238
+ pageNumber: fragment.pageNumber,
239
+ fragmentId: fragment.fragmentId
240
+ };
241
+ }
128
242
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
129
- var MIN_BODY_CONTENT_HEIGHT = 72;
243
+ var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
244
+ var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
130
245
  var PaginationEngine = class {
131
246
  /**
132
247
  * Creates a new pagination engine.
@@ -136,24 +251,46 @@ var DocxodusPagination = (() => {
136
251
  * @param options - Pagination options
137
252
  */
138
253
  constructor(staging, container, options = {}) {
254
+ this.createdPageCount = 0;
255
+ this.footnoteSeparator = null;
256
+ this.footnoteContinuationSeparator = null;
257
+ this.footnoteLayoutLikeWord8 = false;
139
258
  this.pendingFootnoteContinuation = null;
140
259
  /** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
141
260
  this.pageNumbering = /* @__PURE__ */ new Map();
142
- this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
143
- this.containerElement = typeof container === "string" ? document.getElementById(container) : container;
261
+ this.lastPages = [];
262
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
263
+ this.state = "ready";
264
+ const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
265
+ this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
266
+ this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
144
267
  if (!this.stagingElement) {
145
268
  throw new Error("Staging element not found");
146
269
  }
147
270
  if (!this.containerElement) {
148
271
  throw new Error("Container element not found");
149
272
  }
273
+ if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
274
+ throw new Error("Staging and container elements must belong to the same document");
275
+ }
276
+ const view = ownerDocument.defaultView;
277
+ if (!view) {
278
+ throw new Error("Pagination requires an attached document with a defaultView");
279
+ }
280
+ this.document = ownerDocument;
281
+ this.view = view;
150
282
  this.scale = options.scale ?? 1;
151
283
  this.cssPrefix = options.cssPrefix ?? "page-";
152
284
  this.showPageNumbers = options.showPageNumbers ?? true;
153
285
  this.pageGap = options.pageGap ?? 20;
154
286
  this.fragmentParagraphs = options.fragmentParagraphs ?? false;
287
+ this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
288
+ this.cancellationCheckpoint = options.checkCancellation;
289
+ this.pageCountCheckpoint = options.checkPageCount;
290
+ this.layoutToken = options.layoutToken;
155
291
  this.hfRegistry = /* @__PURE__ */ new Map();
156
292
  this.footnoteRegistry = /* @__PURE__ */ new Map();
293
+ this.commentMarginRegistry = /* @__PURE__ */ new Map();
157
294
  }
158
295
  /**
159
296
  * Runs the pagination process.
@@ -161,31 +298,469 @@ var DocxodusPagination = (() => {
161
298
  * @returns PaginationResult with page information
162
299
  */
163
300
  paginate() {
164
- const pages = [];
165
- let pageNumber = 1;
166
- this.hfRegistry = this.parseHeaderFooterRegistry();
167
- this.footnoteRegistry = this.parseFootnoteRegistry();
168
- const sections = this.stagingElement.querySelectorAll(
169
- "[data-section-index]"
170
- );
171
- this.pageNumbering = this.parsePageNumbering(sections);
172
- const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
173
- for (const section of sectionsToProcess) {
174
- const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
175
- const dims = parseSectionDimensions(section);
176
- this.stagingElement.style.visibility = "hidden";
177
- this.stagingElement.style.position = "absolute";
178
- this.stagingElement.style.left = "-9999px";
179
- this.stagingElement.style.display = "block";
180
- section.style.width = `${dims.contentWidth}pt`;
181
- const blocks = this.measureBlocks(section, dims);
182
- const sectionPages = this.flowToPages(blocks, dims, pageNumber, sectionIndex);
183
- pages.push(...sectionPages);
184
- pageNumber += sectionPages.length;
185
- }
186
- this.stagingElement.style.display = "none";
187
- this.substitutePageNumberFields(pages.length);
188
- return { totalPages: pages.length, pages };
301
+ this.checkpoint();
302
+ if (this.state !== "ready") {
303
+ throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
304
+ }
305
+ if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
306
+ throw new Error("Pagination requires staging and container elements attached to their document");
307
+ }
308
+ if (this.document.documentElement.getBoundingClientRect().width <= 0) {
309
+ throw new Error("Pagination requires a browsing context with non-zero layout");
310
+ }
311
+ this.state = "running";
312
+ try {
313
+ const pages = [];
314
+ let pageNumber = 1;
315
+ this.hfRegistry = this.parseHeaderFooterRegistry();
316
+ this.footnoteRegistry = this.parseFootnoteRegistry();
317
+ ({
318
+ normal: this.footnoteSeparator,
319
+ continuation: this.footnoteContinuationSeparator
320
+ } = this.parseFootnoteSeparators());
321
+ this.commentMarginRegistry = this.parseCommentMarginRegistry();
322
+ this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
323
+ const sections = this.stagingElement.querySelectorAll(
324
+ "[data-section-index]"
325
+ );
326
+ this.pageNumbering = this.parsePageNumbering(sections);
327
+ const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
328
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
329
+ const referencedFootnoteIds = /* @__PURE__ */ new Set();
330
+ const referencedCommentIds = /* @__PURE__ */ new Set();
331
+ for (const section of sectionsToProcess) {
332
+ this.checkpoint();
333
+ this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
334
+ for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
335
+ this.checkpoint();
336
+ if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
337
+ const id = reference.dataset.footnoteId;
338
+ if (id) referencedFootnoteIds.add(id);
339
+ }
340
+ for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
341
+ this.checkpoint();
342
+ if (reference.closest(
343
+ "#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
344
+ )) continue;
345
+ const id = reference.dataset.commentId;
346
+ if (id) referencedCommentIds.add(id);
347
+ }
348
+ }
349
+ for (const id of referencedFootnoteIds) {
350
+ this.checkpoint();
351
+ const source = this.footnoteRegistry.get(id);
352
+ if (!source) continue;
353
+ this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
354
+ for (const reference of Array.from(
355
+ source.querySelectorAll("[data-comment-id]")
356
+ )) {
357
+ this.checkpoint();
358
+ const commentId = reference.dataset.commentId;
359
+ if (commentId) referencedCommentIds.add(commentId);
360
+ }
361
+ }
362
+ for (const id of referencedCommentIds) {
363
+ this.checkpoint();
364
+ const source = this.commentMarginRegistry.get(id);
365
+ if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
366
+ }
367
+ const runs = [];
368
+ for (const section of sectionsToProcess) {
369
+ this.checkpoint();
370
+ const dims = parseSectionDimensions(section);
371
+ const previous = runs[runs.length - 1];
372
+ if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
373
+ previous.sections.push(section);
374
+ previous.sectionDimensions.set(
375
+ parseInt(section.dataset.sectionIndex || "0", 10),
376
+ dims
377
+ );
378
+ } else {
379
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
380
+ runs.push({
381
+ sections: [section],
382
+ sectionIndex,
383
+ sectionType: section.dataset.sectionType ?? "nextPage",
384
+ dims,
385
+ sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
386
+ });
387
+ }
388
+ }
389
+ for (const run of runs) {
390
+ this.checkpoint();
391
+ const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
392
+ if (needsParityFiller) {
393
+ const precedingPage = pages[pages.length - 1];
394
+ const precedingPageInSection = parseInt(
395
+ precedingPage.element.dataset.pageInSection ?? "1",
396
+ 10
397
+ );
398
+ const precedingDisplayedPageNumber = parseInt(
399
+ precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
400
+ 10
401
+ );
402
+ const filler = this.createPage(
403
+ precedingPage.dimensions,
404
+ pageNumber,
405
+ precedingPage.sectionIndex,
406
+ precedingDisplayedPageNumber + 1,
407
+ [],
408
+ precedingPageInSection + 1,
409
+ [],
410
+ 0,
411
+ null,
412
+ void 0,
413
+ true
414
+ );
415
+ pages.push(filler);
416
+ pageNumber++;
417
+ }
418
+ this.stagingElement.style.visibility = "hidden";
419
+ this.stagingElement.style.position = "absolute";
420
+ this.stagingElement.style.left = "-9999px";
421
+ this.stagingElement.style.display = "block";
422
+ const blocks = [];
423
+ for (const section of run.sections) {
424
+ this.checkpoint();
425
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
426
+ const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
427
+ section.style.width = `${sectionDims.contentWidth}pt`;
428
+ const columnCount = parseInt(section.dataset.cols || "1", 10);
429
+ if (columnCount > 1) {
430
+ const gap = parseFloat(section.dataset.colGap || "");
431
+ blocks.push(...this.buildColumnBlocks(
432
+ section,
433
+ sectionDims,
434
+ sectionIndex,
435
+ columnCount,
436
+ Number.isFinite(gap) ? gap : 36
437
+ ));
438
+ } else {
439
+ blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
440
+ }
441
+ }
442
+ const sectionPages = this.flowToPages(
443
+ blocks,
444
+ run.dims,
445
+ pageNumber,
446
+ run.sectionIndex,
447
+ run.sectionDimensions
448
+ );
449
+ this.checkpoint();
450
+ pages.push(...sectionPages);
451
+ pageNumber += sectionPages.length;
452
+ }
453
+ this.stagingElement.style.display = "none";
454
+ this.substitutePageNumberFields(pages.length);
455
+ for (const page of pages) {
456
+ this.checkpoint();
457
+ if (page.element.dataset.sectionFiller === "true") continue;
458
+ const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
459
+ const displayedPageNumber = parseInt(
460
+ page.element.dataset.displayedPageNumber || String(page.pageNumber),
461
+ 10
462
+ );
463
+ const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
464
+ const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
465
+ if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
466
+ if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
467
+ }
468
+ this.qualifyPageFragments(pages);
469
+ this.transferVisibleFragmentTargets();
470
+ if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
471
+ this.lastPages = pages;
472
+ const result = {
473
+ totalPages: pages.length,
474
+ pages,
475
+ pageMap: this.layoutToken ? this.materializePageMap(
476
+ this.layoutToken.documentVersion,
477
+ this.layoutToken.rendererFingerprint
478
+ ) : void 0
479
+ };
480
+ this.state = "complete";
481
+ return result;
482
+ } catch (error) {
483
+ this.state = "failed";
484
+ throw error;
485
+ }
486
+ }
487
+ checkpoint() {
488
+ this.cancellationCheckpoint?.();
489
+ }
490
+ /**
491
+ * Normalize visible fragment identities after a caller applies final standalone styles. This
492
+ * deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
493
+ * measurement cannot mutate a tree after it was declared stable.
494
+ */
495
+ normalizePageMapFragmentIdentities() {
496
+ if (this.lastPages.length === 0) {
497
+ throw new Error("paginate() must complete before fragment identities can be normalized");
498
+ }
499
+ this.normalizeVisiblePageFragments(this.lastPages);
500
+ }
501
+ /**
502
+ * Materialize the last completed browser layout as portable page-relative point geometry.
503
+ * The caller supplies both invalidation tokens; this engine never guesses a document version
504
+ * or renderer fingerprint.
505
+ */
506
+ materializePageMap(documentVersion, rendererFingerprint) {
507
+ if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
508
+ throw new Error("documentVersion must be a non-negative safe integer");
509
+ }
510
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
511
+ if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
512
+ const pages = this.lastPages.map((page) => ({
513
+ pageNumber: page.pageNumber,
514
+ pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
515
+ width: page.dimensions.pageWidth,
516
+ height: page.dimensions.pageHeight,
517
+ sectionIndex: page.sectionIndex,
518
+ pageName: `docxodus-section-${page.sectionIndex}`
519
+ }));
520
+ const fragments = [];
521
+ const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
522
+ if (requiredAnchorIds.size === 0) {
523
+ throw new Error("cannot publish an available PageMap without canonical source inventory");
524
+ }
525
+ const measuredAnchorIds = /* @__PURE__ */ new Set();
526
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
527
+ for (const page of this.lastPages) {
528
+ this.checkpoint();
529
+ const pageRect = page.element.getBoundingClientRect();
530
+ if (pageRect.width <= 0 || pageRect.height <= 0) {
531
+ throw new Error(`page ${page.pageNumber} has no measurable geometry`);
532
+ }
533
+ const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
534
+ const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
535
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
536
+ for (const element of Array.from(nodes)) {
537
+ this.checkpoint();
538
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
539
+ const anchorId = element.dataset.sourceAnchorId;
540
+ if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
541
+ throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
542
+ }
543
+ const rect = element.getBoundingClientRect();
544
+ const style = this.view.getComputedStyle(element);
545
+ const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
546
+ if (deliberatelyHidden) continue;
547
+ requiredAnchorIds.add(anchorId);
548
+ const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
549
+ const left = visibleRect.left;
550
+ const top = visibleRect.top;
551
+ const right = visibleRect.right;
552
+ const bottom = visibleRect.bottom;
553
+ if (rect.width <= 0 || rect.height <= 0 || right <= left || bottom <= top) {
554
+ continue;
555
+ }
556
+ measuredAnchorIds.add(anchorId);
557
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
558
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
559
+ const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
560
+ if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
561
+ throw new Error(
562
+ `page ${page.pageNumber} fragment identity changed after final-tree normalization`
563
+ );
564
+ }
565
+ fragments.push({
566
+ fragmentId,
567
+ anchorId,
568
+ fragmentIndex,
569
+ pageNumber: page.pageNumber,
570
+ geometry: {
571
+ x: (left - pageRect.left) * pointPerRenderedX,
572
+ y: (top - pageRect.top) * pointPerRenderedY,
573
+ width: (right - left) * pointPerRenderedX,
574
+ height: (bottom - top) * pointPerRenderedY
575
+ },
576
+ story: this.storyForCanonicalAnchor(anchorId),
577
+ inTableCell: element.matches("td,th") || element.closest("td,th") !== null
578
+ });
579
+ }
580
+ }
581
+ const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
582
+ if (missingAnchor) {
583
+ throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
584
+ }
585
+ return {
586
+ schemaVersion: 1,
587
+ mode: "paginated",
588
+ availability: "available",
589
+ documentVersion,
590
+ rendererFingerprint,
591
+ pages,
592
+ fragments
593
+ };
594
+ }
595
+ normalizeVisiblePageFragments(pages) {
596
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
597
+ for (const page of pages) {
598
+ this.checkpoint();
599
+ const pageRect = page.element.getBoundingClientRect();
600
+ for (const element of Array.from(
601
+ page.element.querySelectorAll("[data-source-anchor-id]")
602
+ )) {
603
+ this.checkpoint();
604
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
605
+ const anchorId = element.dataset.sourceAnchorId;
606
+ if (!anchorId) continue;
607
+ const style = this.view.getComputedStyle(element);
608
+ if (style.display === "none" || style.visibility === "hidden") continue;
609
+ const rect = element.getBoundingClientRect();
610
+ const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
611
+ if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
612
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
613
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
614
+ element.dataset.pageNumber = String(page.pageNumber);
615
+ element.dataset.fragmentIndex = String(fragmentIndex);
616
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Intersect an element with every ancestor that establishes an overflow clip before the page
622
+ * root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
623
+ * therefore must not satisfy PageMap completeness or inflate portable geometry.
624
+ */
625
+ intersectWithClippingAncestors(element, page, pageRect, rect) {
626
+ let left = Math.max(rect.left, pageRect.left);
627
+ let top = Math.max(rect.top, pageRect.top);
628
+ let right = Math.min(rect.right, pageRect.right);
629
+ let bottom = Math.min(rect.bottom, pageRect.bottom);
630
+ const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
631
+ for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
632
+ const style = this.view.getComputedStyle(ancestor);
633
+ const clipsX = clips(style.overflowX);
634
+ const clipsY = clips(style.overflowY);
635
+ if (!clipsX && !clipsY) continue;
636
+ const ancestorRect = ancestor.getBoundingClientRect();
637
+ if (clipsX) {
638
+ left = Math.max(left, ancestorRect.left);
639
+ right = Math.min(right, ancestorRect.right);
640
+ }
641
+ if (clipsY) {
642
+ top = Math.max(top, ancestorRect.top);
643
+ bottom = Math.min(bottom, ancestorRect.bottom);
644
+ }
645
+ }
646
+ return { left, top, right, bottom };
647
+ }
648
+ storyForCanonicalAnchor(anchorId) {
649
+ const first = anchorId.indexOf(":");
650
+ const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
651
+ const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
652
+ if (scope.startsWith("hdr")) return "header";
653
+ if (scope.startsWith("ftr")) return "footer";
654
+ if (scope === "fn") return "footnote";
655
+ if (scope === "en") return "endnote";
656
+ if (scope === "cmt") return "comment";
657
+ return "body";
658
+ }
659
+ /**
660
+ * Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
661
+ * Registry wrappers are excluded when scanning staging because selectable registry contents are
662
+ * inventoried separately. Producers may explicitly mark content that has no visual substrate
663
+ * with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
664
+ */
665
+ collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
666
+ const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
667
+ candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
668
+ for (const element of candidates) {
669
+ if (excludeRegistries && element.closest(
670
+ "#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
671
+ )) {
672
+ continue;
673
+ }
674
+ if (this.isZeroHeightExplicitBreakCarrier(element)) {
675
+ element.dataset.pageMapExclude = "true";
676
+ }
677
+ if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
678
+ const anchorId = element.dataset.sourceAnchorId;
679
+ if (anchorId) destination.add(anchorId);
680
+ }
681
+ }
682
+ isZeroHeightExplicitBreakCarrier(element) {
683
+ const next = element.nextElementSibling;
684
+ const bounds = element.getBoundingClientRect();
685
+ 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);
686
+ }
687
+ isDeliberatelyUnrenderedSource(element, sourceRoot) {
688
+ for (let current = element; current; current = current.parentElement) {
689
+ const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
690
+ 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") {
691
+ return true;
692
+ }
693
+ if (current === sourceRoot) break;
694
+ }
695
+ return false;
696
+ }
697
+ /**
698
+ * Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
699
+ * canonical source identity plus page/fragment qualification instead.
700
+ */
701
+ qualifyPageFragments(pages) {
702
+ const fragmentCounts = /* @__PURE__ */ new Map();
703
+ const activeCanonicalIds = /* @__PURE__ */ new Set();
704
+ const activeBareAnchorIds = /* @__PURE__ */ new Set();
705
+ const makeInactive = (element) => {
706
+ element.removeAttribute("data-anchor");
707
+ element.removeAttribute("data-committed-text");
708
+ if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
709
+ };
710
+ for (const page of pages) {
711
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
712
+ for (const element of Array.from(nodes)) {
713
+ const anchorId = element.dataset.sourceAnchorId;
714
+ if (!anchorId) continue;
715
+ const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
716
+ fragmentCounts.set(anchorId, fragmentIndex + 1);
717
+ element.dataset.pageNumber = String(page.pageNumber);
718
+ element.dataset.fragmentIndex = String(fragmentIndex);
719
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
720
+ const story = this.storyForCanonicalAnchor(anchorId);
721
+ const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
722
+ if (element.hasAttribute("data-anchor")) {
723
+ const bareAnchorId = element.dataset.anchor;
724
+ if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
725
+ activeCanonicalIds.add(anchorId);
726
+ activeBareAnchorIds.add(bareAnchorId);
727
+ } else {
728
+ makeInactive(element);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
734
+ for (const element of Array.from(
735
+ this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
736
+ )) {
737
+ const anchorId = element.dataset.sourceAnchorId;
738
+ if (!anchorId) continue;
739
+ const bareAnchorId = element.dataset.anchor;
740
+ if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
741
+ makeInactive(element);
742
+ } else {
743
+ activeStagingCanonicalIds.add(anchorId);
744
+ activeBareAnchorIds.add(bareAnchorId);
745
+ }
746
+ }
747
+ }
748
+ /**
749
+ * Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
750
+ * fragment target copied into a visible page would therefore resolve to its earlier hidden
751
+ * source. Transfer target ownership to the page presentation after flow is complete; registry
752
+ * and wrapper IDs that have no visible counterpart remain available to pagination internals.
753
+ */
754
+ transferVisibleFragmentTargets() {
755
+ const visibleIds = new Set(Array.from(
756
+ this.containerElement.querySelectorAll("[id]")
757
+ ).map((element) => element.id).filter(Boolean));
758
+ if (visibleIds.size === 0) return;
759
+ for (const source of Array.from(
760
+ this.stagingElement.querySelectorAll("[id]")
761
+ )) {
762
+ if (visibleIds.has(source.id)) source.removeAttribute("id");
763
+ }
189
764
  }
190
765
  /** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
191
766
  parsePageNumbering(sections) {
@@ -229,9 +804,8 @@ var DocxodusPagination = (() => {
229
804
  if (markers.length === 0) continue;
230
805
  const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
231
806
  const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
232
- const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
233
807
  const numbering = this.pageNumbering.get(sectionIndex) ?? {};
234
- const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
808
+ const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
235
809
  for (const marker of Array.from(markers)) {
236
810
  const kind = marker.dataset.field;
237
811
  if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
@@ -243,17 +817,26 @@ var DocxodusPagination = (() => {
243
817
  /**
244
818
  * Measures all content blocks in a section.
245
819
  */
246
- measureBlocks(section, dims) {
820
+ measureBlocks(section, dims, sectionIndex) {
247
821
  const blocks = [];
248
822
  const children = Array.from(section.children);
249
823
  for (const child of children) {
824
+ this.checkpoint();
250
825
  if (child.dataset.sectionIndex !== void 0) {
251
- const nestedBlocks = this.measureBlocks(child, dims);
826
+ const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
827
+ const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
252
828
  blocks.push(...nestedBlocks);
253
829
  continue;
254
830
  }
831
+ if (child.matches("section.endnotes")) {
832
+ const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
833
+ if (endnoteBlocks) {
834
+ blocks.push(...endnoteBlocks);
835
+ continue;
836
+ }
837
+ }
255
838
  const rect = child.getBoundingClientRect();
256
- const style = window.getComputedStyle(child);
839
+ const style = this.view.getComputedStyle(child);
257
840
  const marginTopPx = parseFloat(style.marginTop) || 0;
258
841
  const marginBottomPx = parseFloat(style.marginBottom) || 0;
259
842
  const heightPt = pxToPt(rect.height);
@@ -262,24 +845,176 @@ var DocxodusPagination = (() => {
262
845
  const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
263
846
  blocks.push({
264
847
  element: child,
848
+ sectionIndex,
265
849
  heightPt,
266
850
  marginTopPt,
267
851
  marginBottomPt,
268
852
  keepWithNext: child.dataset.keepWithNext === "true",
269
853
  keepLines: child.dataset.keepLines === "true",
270
854
  pageBreakBefore: child.dataset.pageBreakBefore === "true",
271
- isPageBreak
855
+ isPageBreak,
856
+ isWordParagraph: this.isWordParagraphElement(child)
272
857
  });
273
858
  }
274
859
  return blocks;
275
860
  }
861
+ /**
862
+ * Flatten the converter's `section.endnotes > ol > li > p` presentation into
863
+ * ordinary paragraph blocks. This preserves paragraph formatting and canonical
864
+ * p:en/en:en identities while allowing the existing paragraph fragmenter to
865
+ * split a long endnote across page boundaries.
866
+ */
867
+ measureSafeEndnoteBlocks(section, dims, sectionIndex) {
868
+ const sectionChildren = Array.from(section.children);
869
+ const list = sectionChildren.find((child) => child.tagName === "OL");
870
+ if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
871
+ return null;
872
+ }
873
+ const items = Array.from(list.children);
874
+ if (items.length === 0 || items.some(
875
+ (item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
876
+ )) {
877
+ return null;
878
+ }
879
+ const blocks = [];
880
+ const sectionStyle = this.view.getComputedStyle(section);
881
+ for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
882
+ this.checkpoint();
883
+ const clonedRule = rule.cloneNode(true);
884
+ if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
885
+ blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
886
+ }
887
+ const listStyle = this.view.getComputedStyle(list);
888
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
889
+ this.checkpoint();
890
+ const item = items[itemIndex];
891
+ const ownerAnchorId = item.dataset.sourceAnchorId;
892
+ const paragraphs = Array.from(item.children);
893
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
894
+ this.checkpoint();
895
+ const paragraph = paragraphs[paragraphIndex];
896
+ if (!this.hasRangeFragmentSafeLayout(paragraph)) {
897
+ return null;
898
+ }
899
+ const clone = paragraph.cloneNode(true);
900
+ clone.dataset.paginationSafeEndnote = "true";
901
+ clone.style.fontSize ||= sectionStyle.fontSize;
902
+ clone.style.lineHeight ||= sectionStyle.lineHeight;
903
+ clone.style.paddingLeft ||= listStyle.paddingLeft;
904
+ if (ownerAnchorId && !Array.from(
905
+ clone.querySelectorAll("[data-source-anchor-id]")
906
+ ).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
907
+ const owner = this.document.createElement("span");
908
+ owner.dataset.sourceAnchorId = ownerAnchorId;
909
+ while (clone.firstChild) owner.appendChild(clone.firstChild);
910
+ clone.appendChild(owner);
911
+ }
912
+ if (paragraphIndex === 0) {
913
+ const itemId = item.id;
914
+ if (itemId) {
915
+ if (clone.id && clone.id !== itemId) return null;
916
+ clone.id = itemId;
917
+ }
918
+ const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
919
+ const marker = this.formatOrderedListMarker(
920
+ Number.isFinite(value) ? value : itemIndex + 1,
921
+ listStyle.listStyleType
922
+ );
923
+ clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
924
+ }
925
+ blocks.push(this.measureElement(clone, dims, sectionIndex));
926
+ }
927
+ }
928
+ return blocks;
929
+ }
930
+ /** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
931
+ formatOrderedListMarker(value, listStyleType) {
932
+ const format = (() => {
933
+ switch (listStyleType) {
934
+ case "lower-roman":
935
+ return "lowerRoman";
936
+ case "upper-roman":
937
+ return "upperRoman";
938
+ case "lower-alpha":
939
+ case "lower-latin":
940
+ return "lowerLetter";
941
+ case "upper-alpha":
942
+ case "upper-latin":
943
+ return "upperLetter";
944
+ default:
945
+ return "decimal";
946
+ }
947
+ })();
948
+ return formatPageNumber(value, format);
949
+ }
950
+ /**
951
+ * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
952
+ * blocks. Word lays such a section out as N columns inside the same body extent;
953
+ * a balanced `column-count` container reproduces that geometry, and the paginator
954
+ * then places each container as one ordinary measured block. A container grows
955
+ * greedily until its balanced height would exceed the smallest page body available
956
+ * to the section, so a long columned section still splits across pages at block
957
+ * boundaries. Each child lands in exactly one container, so anchors never
958
+ * duplicate. An explicit page break child passes through as its own block, which
959
+ * ends the current container and lets the normal flow logic turn the page.
960
+ */
961
+ buildColumnBlocks(section, dims, sectionIndex, columnCount, columnGapPt) {
962
+ const children = Array.from(section.children);
963
+ const blocks = [];
964
+ const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
965
+ const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
966
+ const makeContainer = (slice) => {
967
+ const container = this.document.createElement("div");
968
+ container.style.columnCount = String(columnCount);
969
+ container.style.columnGap = `${columnGapPt}pt`;
970
+ for (const child of slice) {
971
+ container.appendChild(child.cloneNode(true));
972
+ }
973
+ return container;
974
+ };
975
+ let start = 0;
976
+ while (start < children.length) {
977
+ this.checkpoint();
978
+ if (isBreak(children[start])) {
979
+ blocks.push(this.measureElement(children[start], dims, sectionIndex));
980
+ start++;
981
+ continue;
982
+ }
983
+ let end = start + 1;
984
+ let container = makeContainer(children.slice(start, end));
985
+ let measured = this.measureElement(container, dims, sectionIndex);
986
+ while (end < children.length && !isBreak(children[end])) {
987
+ this.checkpoint();
988
+ const candidate = makeContainer(children.slice(start, end + 1));
989
+ const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
990
+ if (candidateMeasured.heightPt > maxFragmentHeight) break;
991
+ container = candidate;
992
+ measured = candidateMeasured;
993
+ end++;
994
+ }
995
+ blocks.push({
996
+ element: container,
997
+ sectionIndex,
998
+ heightPt: measured.heightPt,
999
+ marginTopPt: measured.marginTopPt,
1000
+ marginBottomPt: measured.marginBottomPt,
1001
+ keepWithNext: false,
1002
+ keepLines: false,
1003
+ pageBreakBefore: false,
1004
+ isPageBreak: false,
1005
+ isWordParagraph: false
1006
+ });
1007
+ start = end;
1008
+ }
1009
+ return blocks;
1010
+ }
276
1011
  /**
277
1012
  * Measures one element in the same hidden staging context used for the source blocks.
278
1013
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
279
1014
  * rows because wrapping and collapsed borders change the height of a fragment.
280
1015
  */
281
- measureElement(element, dims) {
282
- const measurementHost = document.createElement("div");
1016
+ measureElement(element, dims, sectionIndex) {
1017
+ const measurementHost = this.document.createElement("div");
283
1018
  measurementHost.style.position = "absolute";
284
1019
  measurementHost.style.visibility = "hidden";
285
1020
  measurementHost.style.left = "-9999px";
@@ -288,16 +1023,18 @@ var DocxodusPagination = (() => {
288
1023
  measurementHost.appendChild(measuredElement);
289
1024
  this.stagingElement.appendChild(measurementHost);
290
1025
  const rect = measuredElement.getBoundingClientRect();
291
- const style = window.getComputedStyle(measuredElement);
1026
+ const style = this.view.getComputedStyle(measuredElement);
292
1027
  const measured = {
293
1028
  element,
1029
+ sectionIndex,
294
1030
  heightPt: pxToPt(rect.height),
295
1031
  marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
296
1032
  marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
297
1033
  keepWithNext: element.dataset.keepWithNext === "true",
298
1034
  keepLines: element.dataset.keepLines === "true",
299
1035
  pageBreakBefore: element.dataset.pageBreakBefore === "true",
300
- isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
1036
+ isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`),
1037
+ isWordParagraph: this.isWordParagraphElement(element)
301
1038
  };
302
1039
  this.stagingElement.removeChild(measurementHost);
303
1040
  return measured;
@@ -329,10 +1066,15 @@ var DocxodusPagination = (() => {
329
1066
  * collapsed-margin rules as normal block placement. The trailing margin is
330
1067
  * intentionally excluded, matching the individual block fit check.
331
1068
  */
332
- measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
1069
+ measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage, pageInSection) {
333
1070
  const firstBlock = chain[0];
334
1071
  if (!firstBlock) return 0;
335
- const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
1072
+ const firstMarginTop = this.effectiveBlockMarginTop(
1073
+ firstBlock,
1074
+ previousMarginBottomPt,
1075
+ isFirstOnPage,
1076
+ pageInSection
1077
+ );
336
1078
  let bodyHeight = firstMarginTop + firstBlock.heightPt;
337
1079
  for (let index = 1; index < chain.length; index++) {
338
1080
  const previousBlock = chain[index - 1];
@@ -341,6 +1083,35 @@ var DocxodusPagination = (() => {
341
1083
  }
342
1084
  return bodyHeight;
343
1085
  }
1086
+ /** Word paragraphs render as `p`, or as `h1`–`h6` when their style has an outline level. */
1087
+ isWordParagraphElement(element) {
1088
+ return element.tagName === "P" || /^H[1-6]$/.test(element.tagName);
1089
+ }
1090
+ shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection) {
1091
+ return isFirstOnPage && pageInSection > 1 && block.isWordParagraph === true;
1092
+ }
1093
+ /**
1094
+ * Resolves the part of a block's top margin that consumes the current page.
1095
+ *
1096
+ * In Word's native DOCX layout, paragraph space-before is suppressed when a paragraph is the
1097
+ * first body block on a later page of the SAME section. The first page of a document/section is
1098
+ * the exception and keeps its spacing. Tables and other block margins are not paragraph spacing,
1099
+ * so they continue to use the ordinary CSS collapsing rule.
1100
+ */
1101
+ effectiveBlockMarginTop(block, previousMarginBottomPt, isFirstOnPage, pageInSection) {
1102
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
1103
+ return 0;
1104
+ }
1105
+ return isFirstOnPage ? block.marginTopPt : Math.max(block.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
1106
+ }
1107
+ /** Clone a source block with the same page-top spacing decision used by the height budget. */
1108
+ cloneBlockForPage(block, isFirstOnPage, pageInSection) {
1109
+ const clone = block.element.cloneNode(true);
1110
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
1111
+ clone.style.setProperty("margin-top", "0", "important");
1112
+ }
1113
+ return clone;
1114
+ }
344
1115
  /**
345
1116
  * Finds footnote references introduced by a sequence of blocks, preserving
346
1117
  * document order and excluding references already assigned to the page.
@@ -349,6 +1120,7 @@ var DocxodusPagination = (() => {
349
1120
  const knownIds = new Set(existingFootnoteIds);
350
1121
  const newIds = [];
351
1122
  for (const block of blocks) {
1123
+ this.checkpoint();
352
1124
  for (const id of this.extractFootnoteRefs(block.element)) {
353
1125
  if (!knownIds.has(id)) {
354
1126
  knownIds.add(id);
@@ -366,8 +1138,8 @@ var DocxodusPagination = (() => {
366
1138
  smallestEffectiveContentHeight(dims, sectionIndex) {
367
1139
  return Math.min(
368
1140
  this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
369
- this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
370
- this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
1141
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
1142
+ this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
371
1143
  );
372
1144
  }
373
1145
  /**
@@ -405,10 +1177,11 @@ var DocxodusPagination = (() => {
405
1177
  if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
406
1178
  return null;
407
1179
  }
408
- const table = wrapper.firstElementChild;
409
- if (!(table instanceof HTMLTableElement)) {
1180
+ const tableElement = wrapper.firstElementChild;
1181
+ if (!tableElement || tableElement.localName !== "table") {
410
1182
  return null;
411
1183
  }
1184
+ const table = tableElement;
412
1185
  const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
413
1186
  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]")) {
414
1187
  return null;
@@ -422,8 +1195,10 @@ var DocxodusPagination = (() => {
422
1195
  const groups = [];
423
1196
  let start = 0;
424
1197
  while (start < rows.length) {
1198
+ this.checkpoint();
425
1199
  let end = start;
426
1200
  while (end < rows.length) {
1201
+ this.checkpoint();
427
1202
  const candidate = this.createSimpleTableFragment(
428
1203
  wrapper,
429
1204
  table,
@@ -431,7 +1206,7 @@ var DocxodusPagination = (() => {
431
1206
  rows.slice(start, end + 1),
432
1207
  start === 0
433
1208
  );
434
- const measured = this.measureElement(candidate, dims);
1209
+ const measured = this.measureElement(candidate, dims, block.sectionIndex);
435
1210
  if (measured.heightPt > maximumFragmentHeight) {
436
1211
  break;
437
1212
  }
@@ -463,7 +1238,7 @@ var DocxodusPagination = (() => {
463
1238
  if (!isLast) {
464
1239
  fragment.style.setProperty("margin-bottom", "0", "important");
465
1240
  }
466
- const measured = this.measureElement(fragment, dims);
1241
+ const measured = this.measureElement(fragment, dims, block.sectionIndex);
467
1242
  if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
468
1243
  return null;
469
1244
  }
@@ -478,35 +1253,198 @@ var DocxodusPagination = (() => {
478
1253
  return fragments;
479
1254
  }
480
1255
  /**
481
- * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
482
- * after whitespace or at a run boundary so the paginator never deliberately
483
- * cuts through an ordinary word merely to fill a little more of a page.
1256
+ * DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
1257
+ * offsets are checked against the browser's Unicode grapheme segmenter, so a
1258
+ * formatting-run boundary can never bisect a surrogate pair, combining
1259
+ * sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
1260
+ * PAGE/NUMPAGES field results are atomic because splitting their marker would
1261
+ * make later substitution duplicate or replace only half of the field.
484
1262
  */
485
- paragraphFragmentEndpoints(paragraph) {
486
- const endpoints = [];
487
- const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
1263
+ paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
1264
+ const textNodes = [];
1265
+ const textStarts = /* @__PURE__ */ new Map();
1266
+ const lastTextByField = /* @__PURE__ */ new Map();
1267
+ const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
1268
+ const flattenedChunks = [];
1269
+ let flattenedLength = 0;
488
1270
  let textNode;
489
1271
  while (textNode = walker.nextNode()) {
490
- const text = textNode.data;
491
- if (text.length === 0) continue;
492
- const whitespace = /\s+/g;
1272
+ this.checkpoint();
1273
+ textStarts.set(textNode, flattenedLength);
1274
+ textNodes.push(textNode);
1275
+ flattenedChunks.push(textNode.data);
1276
+ flattenedLength += textNode.data.length;
1277
+ let field = textNode.parentElement?.closest("[data-field]") ?? null;
1278
+ while (field?.parentElement?.closest("[data-field]")) {
1279
+ field = field.parentElement.closest("[data-field]");
1280
+ }
1281
+ if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
1282
+ }
1283
+ const flattenedText = flattenedChunks.join("");
1284
+ if (!this.hasVisibleText(flattenedText)) return [];
1285
+ const invisible = /[\s\u200B-\u200F\uFEFF]/;
1286
+ let firstVisibleOffset = -1;
1287
+ let lastVisibleOffset = -1;
1288
+ for (let index = 0; index < flattenedText.length; index++) {
1289
+ if (index % 4096 === 0) this.checkpoint();
1290
+ if (invisible.test(flattenedText[index])) continue;
1291
+ if (firstVisibleOffset < 0) firstVisibleOffset = index;
1292
+ lastVisibleOffset = index;
1293
+ }
1294
+ const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
1295
+ const candidates = [];
1296
+ const addCandidate = (candidate) => {
1297
+ const { textOffset } = candidate;
1298
+ if (textOffset <= 0 || textOffset >= flattenedText.length) return;
1299
+ if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
1300
+ if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
1301
+ if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
1302
+ if (graphemeBoundaries) {
1303
+ if (!graphemeBoundaries.has(textOffset)) return;
1304
+ } else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
1305
+ return;
1306
+ }
1307
+ candidates.push(candidate);
1308
+ };
1309
+ for (const node of textNodes) {
1310
+ this.checkpoint();
1311
+ const start = textStarts.get(node);
1312
+ const atomic = node.parentElement?.closest(
1313
+ "[data-field], [data-list-marker], a[data-comment-id]"
1314
+ );
1315
+ if (atomic && paragraph.contains(atomic)) continue;
1316
+ const whitespace = /[\u0009-\u000D\u0020]+/g;
493
1317
  let match;
494
- while ((match = whitespace.exec(text)) !== null) {
495
- endpoints.push({ node: textNode, offset: match.index + match[0].length });
1318
+ let matchesSinceCheckpoint = 0;
1319
+ while ((match = whitespace.exec(node.data)) !== null) {
1320
+ if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
1321
+ const offset = match.index + match[0].length;
1322
+ addCandidate({ node, offset, textOffset: start + offset, priority: 0 });
496
1323
  }
497
- if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
498
- endpoints.push({ node: textNode, offset: text.length });
1324
+ addCandidate({
1325
+ node,
1326
+ offset: node.data.length,
1327
+ textOffset: start + node.data.length,
1328
+ priority: 0
1329
+ });
1330
+ if (emergencyGraphemeBreaks) {
1331
+ for (let offset = 1; offset < node.data.length; offset++) {
1332
+ if (offset % 256 === 0) this.checkpoint();
1333
+ addCandidate({
1334
+ node,
1335
+ offset,
1336
+ textOffset: start + offset,
1337
+ priority: -1
1338
+ });
1339
+ }
1340
+ }
1341
+ }
1342
+ for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
1343
+ this.checkpoint();
1344
+ if (field.parentElement?.closest("[data-field]")) continue;
1345
+ const last = lastTextByField.get(field);
1346
+ const parent = field.parentNode;
1347
+ if (!last || !parent) continue;
1348
+ const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
1349
+ if (childIndex < 0) continue;
1350
+ addCandidate({
1351
+ node: parent,
1352
+ offset: childIndex + 1,
1353
+ textOffset: textStarts.get(last) + last.data.length,
1354
+ priority: 1
1355
+ });
1356
+ }
1357
+ candidates.sort((left, right) => left.textOffset - right.textOffset || right.priority - left.priority);
1358
+ const endpoints = [];
1359
+ for (const candidate of candidates) {
1360
+ if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
1361
+ endpoints.push(candidate);
499
1362
  }
500
1363
  }
501
1364
  return endpoints;
502
1365
  }
1366
+ /** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
1367
+ graphemeBoundaryOffsets(text) {
1368
+ const SegmenterConstructor = this.view.Intl.Segmenter;
1369
+ if (!SegmenterConstructor) return null;
1370
+ const boundaries = /* @__PURE__ */ new Set([0, text.length]);
1371
+ let segmentCount = 0;
1372
+ for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
1373
+ if (++segmentCount % 256 === 0) this.checkpoint();
1374
+ boundaries.add(part.index);
1375
+ boundaries.add(part.index + part.segment.length);
1376
+ }
1377
+ return boundaries;
1378
+ }
1379
+ /**
1380
+ * Conservative UAX #14/CSS wrapping opportunities used for synthetic page
1381
+ * boundaries. In particular, a formatting-run boundary is not itself a word
1382
+ * boundary, and Japanese opening/closing punctuation stays with its pair.
1383
+ * Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
1384
+ * the browser to wrap anywhere.
1385
+ */
1386
+ isLegalParagraphLineBoundary(paragraph, text, offset) {
1387
+ const lastCodeUnit = text.charCodeAt(offset - 1);
1388
+ const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
1389
+ const beforeCodePoint = text.codePointAt(beforeOffset);
1390
+ const afterCodePoint = text.codePointAt(offset);
1391
+ const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
1392
+ const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
1393
+ if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
1394
+ if (/[-\u00AD\u2010]$/u.test(before)) return true;
1395
+ const style = this.view.getComputedStyle(paragraph);
1396
+ if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
1397
+ const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
1398
+ if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
1399
+ const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
1400
+ 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";
1401
+ return !opening.includes(before) && !closing.includes(after);
1402
+ }
1403
+ /** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
1404
+ isNonBreakingTextBoundary(text, offset) {
1405
+ const codePointBefore = (index) => {
1406
+ if (index <= 0) return void 0;
1407
+ const last = text.charCodeAt(index - 1);
1408
+ const start = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
1409
+ return text.codePointAt(Math.max(0, start));
1410
+ };
1411
+ const nonBreaking = /* @__PURE__ */ new Set([
1412
+ 160,
1413
+ 8206,
1414
+ 8207,
1415
+ 8209,
1416
+ 8234,
1417
+ 8235,
1418
+ 8236,
1419
+ 8237,
1420
+ 8238,
1421
+ 8239,
1422
+ 8288,
1423
+ 8294,
1424
+ 8295,
1425
+ 8296,
1426
+ 8297,
1427
+ 65279
1428
+ ]);
1429
+ return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
1430
+ }
1431
+ /**
1432
+ * Without Intl.Segmenter, admit only an all-ASCII boundary. This still
1433
+ * fragments ordinary prose while refusing to guess about Unicode clusters.
1434
+ */
1435
+ isConservativeFallbackTextBoundary(text, offset) {
1436
+ return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
1437
+ }
503
1438
  /**
504
1439
  * Whether a range contains visible text after ignoring bidi/zero-width marks.
505
1440
  * Paragraph fragmentation deliberately excludes non-textual descendants, so
506
1441
  * this is enough to reject empty head or tail fragments.
507
1442
  */
508
1443
  hasVisibleFragmentText(fragment) {
509
- return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
1444
+ return this.hasVisibleText(fragment.textContent || "");
1445
+ }
1446
+ hasVisibleText(text) {
1447
+ return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
510
1448
  }
511
1449
  /**
512
1450
  * Phase one only handles text-like paragraph descendants. Objects, explicit
@@ -515,13 +1453,25 @@ var DocxodusPagination = (() => {
515
1453
  * fallback instead of risking broken content or duplicate anchors.
516
1454
  */
517
1455
  canFragmentParagraph(block) {
518
- if (!this.fragmentParagraphs || block.element.tagName !== "P") {
1456
+ if (!this.fragmentParagraphs) {
519
1457
  return false;
520
1458
  }
521
1459
  const paragraph = block.element;
522
1460
  if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
523
1461
  return false;
524
1462
  }
1463
+ return this.canRangeFragmentParagraph(paragraph);
1464
+ }
1465
+ /**
1466
+ * Shared structural gate for body and note paragraph fragmentation. Footnote
1467
+ * first paragraphs are inline beside their marker, so that one known layout
1468
+ * context may opt into an inline root while retaining every descendant and
1469
+ * break-safety restriction used for body text.
1470
+ */
1471
+ canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
1472
+ if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
1473
+ return false;
1474
+ }
525
1475
  const unsupportedDescendants = [
526
1476
  "br",
527
1477
  "img",
@@ -552,21 +1502,27 @@ var DocxodusPagination = (() => {
552
1502
  "fieldset",
553
1503
  "details",
554
1504
  "[data-footnote-id]",
555
- "[data-list-marker]",
556
- "[data-anchor]",
557
- "[id]",
558
1505
  "[contenteditable]"
559
1506
  ].join(", ");
560
1507
  if (paragraph.querySelector(unsupportedDescendants)) {
561
1508
  return false;
562
1509
  }
563
- const paragraphStyle = window.getComputedStyle(paragraph);
564
- 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") {
1510
+ const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
1511
+ return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
1512
+ }
1513
+ /**
1514
+ * A range clone preserves nested inline formatting exactly. Anything that establishes its own
1515
+ * box/layout context is deferred until a future fragmenter can model it accurately. Callers
1516
+ * must invoke this while the paragraph is attached to the styled document.
1517
+ */
1518
+ hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
1519
+ const paragraphStyle = this.view.getComputedStyle(paragraph);
1520
+ 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") {
565
1521
  return false;
566
1522
  }
567
1523
  for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
568
- const style = window.getComputedStyle(descendant);
569
- if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
1524
+ const style = this.view.getComputedStyle(descendant);
1525
+ if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
570
1526
  return false;
571
1527
  }
572
1528
  }
@@ -593,58 +1549,122 @@ var DocxodusPagination = (() => {
593
1549
  return fragment;
594
1550
  }
595
1551
  /**
596
- * Splits a simple paragraph at the largest DOM Range endpoint that fits the
597
- * currently available body space. The caller then processes the tail normally,
598
- * allowing it to fragment again on later pages when necessary.
1552
+ * Range clones repeat an inline ancestor when the split lands inside it. Keep
1553
+ * semantic wrappers (links, comments, formatting) on both sides, but retain a
1554
+ * duplicated HTML/editor identity only on the leading fragment. Targets that
1555
+ * occur wholly after the split are absent from `head` and remain on `tail`.
599
1556
  */
600
- tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
601
- if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
602
- return null;
1557
+ reconcileParagraphFragmentIdentities(head, tail) {
1558
+ const elements = (root, selector) => [
1559
+ ...root.matches(selector) ? [root] : [],
1560
+ ...Array.from(root.querySelectorAll(selector))
1561
+ ];
1562
+ const headIds = new Set(elements(head, "[id]").map((element) => element.id));
1563
+ for (const element of elements(tail, "[id]")) {
1564
+ if (headIds.has(element.id)) element.removeAttribute("id");
603
1565
  }
604
- const paragraph = block.element;
605
- const endpoints = this.paragraphFragmentEndpoints(paragraph);
606
- if (endpoints.length < 2) {
607
- return null;
1566
+ const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
1567
+ for (const element of elements(tail, "[data-anchor]")) {
1568
+ if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
1569
+ element.removeAttribute("data-anchor");
1570
+ element.removeAttribute("data-committed-text");
608
1571
  }
609
- let low = 0;
610
- let high = endpoints.length - 2;
611
- let best = null;
612
- while (low <= high) {
613
- const middle = Math.floor((low + high) / 2);
614
- const endpoint = endpoints[middle];
615
- const headRange = document.createRange();
616
- headRange.setStart(paragraph, 0);
617
- headRange.setEnd(endpoint.node, endpoint.offset);
618
- const headContents = headRange.cloneContents();
619
- if (!this.hasVisibleFragmentText(headContents)) {
620
- low = middle + 1;
621
- continue;
1572
+ }
1573
+ /**
1574
+ * Range-clone the largest safe prefix accepted by `fits`. The measurement
1575
+ * policy stays with the caller, allowing body blocks and note bands to share
1576
+ * one DOM fragmenter while measuring in their respective layout contexts.
1577
+ */
1578
+ splitParagraphAtLargestFit(paragraph, fits, options = {}) {
1579
+ const largestFit = (endpoints2) => {
1580
+ const lastEndpoint = endpoints2[endpoints2.length - 1];
1581
+ if (lastEndpoint) {
1582
+ const lastRange = this.document.createRange();
1583
+ lastRange.setStart(paragraph, 0);
1584
+ lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
1585
+ const lastContents = lastRange.cloneContents();
1586
+ if (this.hasVisibleFragmentText(lastContents)) {
1587
+ const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
1588
+ if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
1589
+ }
622
1590
  }
623
- const head = this.createParagraphFragment(paragraph, headRange, true, false);
624
- const measured = this.measureElement(head, dims);
625
- if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
626
- best = { endpoint, element: head, measured };
627
- low = middle + 1;
628
- } else {
629
- high = middle - 1;
1591
+ let low = 0;
1592
+ let high = endpoints2.length - 2;
1593
+ let best2 = null;
1594
+ while (low <= high) {
1595
+ this.checkpoint();
1596
+ const middle = Math.floor((low + high) / 2);
1597
+ const endpoint = endpoints2[middle];
1598
+ const headRange = this.document.createRange();
1599
+ headRange.setStart(paragraph, 0);
1600
+ headRange.setEnd(endpoint.node, endpoint.offset);
1601
+ const headContents = headRange.cloneContents();
1602
+ if (!this.hasVisibleFragmentText(headContents)) {
1603
+ low = middle + 1;
1604
+ continue;
1605
+ }
1606
+ const head = this.createParagraphFragment(paragraph, headRange, true, false);
1607
+ if (fits(head)) {
1608
+ best2 = { endpoint, head };
1609
+ low = middle + 1;
1610
+ } else {
1611
+ high = middle - 1;
1612
+ }
630
1613
  }
1614
+ return best2;
1615
+ };
1616
+ let endpoints = this.paragraphFragmentEndpoints(paragraph);
1617
+ let best = largestFit(endpoints);
1618
+ if (!best && options.emergencyGraphemeBreaks) {
1619
+ endpoints = this.paragraphFragmentEndpoints(paragraph, true);
1620
+ best = largestFit(endpoints);
1621
+ }
1622
+ if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
1623
+ const endpoint = endpoints[0];
1624
+ const headRange = this.document.createRange();
1625
+ headRange.setStart(paragraph, 0);
1626
+ headRange.setEnd(endpoint.node, endpoint.offset);
1627
+ best = {
1628
+ endpoint,
1629
+ head: this.createParagraphFragment(paragraph, headRange, true, false)
1630
+ };
631
1631
  }
632
1632
  if (!best) {
633
- return null;
1633
+ return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
634
1634
  }
635
- const tailRange = document.createRange();
1635
+ const tailRange = this.document.createRange();
636
1636
  tailRange.setStart(best.endpoint.node, best.endpoint.offset);
637
1637
  tailRange.setEnd(paragraph, paragraph.childNodes.length);
638
1638
  const tailContents = tailRange.cloneContents();
639
- if (!this.hasVisibleFragmentText(tailContents)) {
1639
+ if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
1640
+ const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
1641
+ this.reconcileParagraphFragmentIdentities(best.head, tail);
1642
+ return {
1643
+ kind: "split",
1644
+ head: best.head,
1645
+ tail
1646
+ };
1647
+ }
1648
+ /**
1649
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
1650
+ * currently available body space. The caller then processes the tail normally,
1651
+ * allowing it to fragment again on later pages when necessary.
1652
+ */
1653
+ tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
1654
+ if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
640
1655
  return null;
641
1656
  }
642
- const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
643
- const tailMeasured = this.measureElement(tail, dims);
1657
+ const split = this.splitParagraphAtLargestFit(block.element, (head) => {
1658
+ const measured = this.measureElement(head, dims, block.sectionIndex);
1659
+ return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
1660
+ });
1661
+ if (split.kind !== "split") return null;
1662
+ const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
1663
+ const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
644
1664
  return [
645
1665
  {
646
- ...best.measured,
647
- element: best.element,
1666
+ ...headMeasured,
1667
+ element: split.head,
648
1668
  keepWithNext: false,
649
1669
  keepLines: false,
650
1670
  pageBreakBefore: false,
@@ -652,7 +1672,7 @@ var DocxodusPagination = (() => {
652
1672
  },
653
1673
  {
654
1674
  ...tailMeasured,
655
- element: tail,
1675
+ element: split.tail,
656
1676
  keepWithNext: false,
657
1677
  keepLines: false,
658
1678
  pageBreakBefore: false,
@@ -723,7 +1743,7 @@ var DocxodusPagination = (() => {
723
1743
  const registry = /* @__PURE__ */ new Map();
724
1744
  const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
725
1745
  if (!registryEl) return registry;
726
- const entries = Array.from(registryEl.querySelectorAll("[data-footnote-id]"));
1746
+ const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
727
1747
  for (const entry of entries) {
728
1748
  const footnoteId = entry.dataset.footnoteId;
729
1749
  if (footnoteId) {
@@ -732,6 +1752,68 @@ var DocxodusPagination = (() => {
732
1752
  }
733
1753
  return registry;
734
1754
  }
1755
+ /** Read Word's optional normal and continuation separator stories. */
1756
+ parseFootnoteSeparators() {
1757
+ const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
1758
+ const clone = (kind) => {
1759
+ const source = Array.from(registry?.children ?? []).find(
1760
+ (entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
1761
+ );
1762
+ return source?.cloneNode(true);
1763
+ };
1764
+ return {
1765
+ normal: clone("normal") ?? null,
1766
+ continuation: clone("continuation") ?? null
1767
+ };
1768
+ }
1769
+ /** Append the exact separator story selected for this initial/continued note band. */
1770
+ appendFootnoteSeparator(container, continuation) {
1771
+ const kind = continuation ? "continuation" : "normal";
1772
+ const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
1773
+ if (!source) {
1774
+ const fallback = this.document.createElement("hr");
1775
+ fallback.dataset.footnoteSeparator = kind;
1776
+ container.appendChild(fallback);
1777
+ return;
1778
+ }
1779
+ const clone = source.cloneNode(true);
1780
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
1781
+ element.removeAttribute("id");
1782
+ element.removeAttribute("data-anchor");
1783
+ element.removeAttribute("data-committed-text");
1784
+ element.removeAttribute("data-source-anchor-id");
1785
+ element.removeAttribute("data-page-fragment-id");
1786
+ element.removeAttribute("data-fragment-index");
1787
+ element.removeAttribute("data-footnote-id");
1788
+ element.removeAttribute("data-comment-id");
1789
+ element.removeAttribute("name");
1790
+ if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
1791
+ element.removeAttribute("href");
1792
+ }
1793
+ element.setAttribute("contenteditable", "false");
1794
+ }
1795
+ container.appendChild(clone);
1796
+ }
1797
+ /** Parses the hidden source notes used to render paginated margin comments. */
1798
+ parseCommentMarginRegistry() {
1799
+ const registry = /* @__PURE__ */ new Map();
1800
+ const registryEl = this.stagingElement.querySelector(
1801
+ "#pagination-comment-margin-registry"
1802
+ );
1803
+ if (!registryEl) return registry;
1804
+ for (const entry of Array.from(
1805
+ registryEl.querySelectorAll("[data-comment-id]")
1806
+ )) {
1807
+ const commentId = entry.dataset.commentId;
1808
+ if (!commentId) continue;
1809
+ let root = entry;
1810
+ for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
1811
+ root = parent;
1812
+ }
1813
+ registry.set(commentId, root.cloneNode(true));
1814
+ }
1815
+ return registry;
1816
+ }
735
1817
  /**
736
1818
  * Extracts footnote reference IDs from an element.
737
1819
  */
@@ -746,6 +1828,92 @@ var DocxodusPagination = (() => {
746
1828
  }
747
1829
  return ids;
748
1830
  }
1831
+ /** Clone a note child for the continuation queue with its original selector position. */
1832
+ cloneFootnoteElementForContinuation(element, sourceIndex) {
1833
+ const clone = element.cloneNode(true);
1834
+ clone.setAttribute(
1835
+ FOOTNOTE_SOURCE_POSITION_ATTR,
1836
+ sourceIndex === 0 ? "first" : "later"
1837
+ );
1838
+ return clone;
1839
+ }
1840
+ /** Remove identities that belong only to the initial registry presentation shell. */
1841
+ makeContinuationShellInert(element) {
1842
+ element.removeAttribute("id");
1843
+ element.removeAttribute("data-anchor");
1844
+ element.removeAttribute("data-committed-text");
1845
+ }
1846
+ /**
1847
+ * Build the exact continuation shape shared by measurement and paint.
1848
+ *
1849
+ * Keep the source `.footnote-item > .footnote-content` ancestry: document
1850
+ * CSS, inherited direction/language, and custom classes frequently target
1851
+ * those shells. Only the number and initial HTML/editor identities are
1852
+ * omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
1853
+ * whose first carried element was a later source paragraph.
1854
+ */
1855
+ createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
1856
+ const source = this.footnoteRegistry.get(continuation.footnoteId);
1857
+ const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
1858
+ this.makeContinuationShellInert(wrapper);
1859
+ wrapper.classList.add("footnote-continuation");
1860
+ wrapper.dataset.footnoteId = continuation.footnoteId;
1861
+ if (continuation.sourceAnchorId) {
1862
+ wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
1863
+ }
1864
+ const sourceContent = source?.querySelector(".footnote-content");
1865
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
1866
+ this.makeContinuationShellInert(content);
1867
+ if (!sourceContent) content.className = "footnote-content";
1868
+ const first = elements[0];
1869
+ if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
1870
+ const sentinel = this.document.createElement("p");
1871
+ sentinel.className = "footnote-continuation-position-sentinel";
1872
+ sentinel.setAttribute("aria-hidden", "true");
1873
+ sentinel.style.setProperty("display", "none", "important");
1874
+ content.appendChild(sentinel);
1875
+ }
1876
+ for (let index = 0; index < elements.length; index++) {
1877
+ this.checkpoint();
1878
+ const element = elements[index];
1879
+ const clone = element.cloneNode(true);
1880
+ clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
1881
+ if (index === 0 && clone.tagName === "P") {
1882
+ clone.style.setProperty("display", "block", "important");
1883
+ }
1884
+ content.appendChild(clone);
1885
+ }
1886
+ wrapper.appendChild(content);
1887
+ return wrapper;
1888
+ }
1889
+ /** Build the exact partial-note item shape shared by measurement and paint. */
1890
+ createPartialFootnoteItem(footnote, fittingElements) {
1891
+ const item = footnote.cloneNode(false);
1892
+ const number = footnote.querySelector(".footnote-number");
1893
+ if (number) item.appendChild(number.cloneNode(true));
1894
+ const sourceContent = footnote.querySelector(".footnote-content");
1895
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
1896
+ if (!sourceContent) content.className = "footnote-content";
1897
+ for (const element of fittingElements) {
1898
+ this.checkpoint();
1899
+ content.appendChild(element.cloneNode(true));
1900
+ }
1901
+ item.appendChild(content);
1902
+ return item;
1903
+ }
1904
+ /**
1905
+ * Where a note measurement tree is attached.
1906
+ *
1907
+ * A note band is reserved from a measurement and then painted; if the two
1908
+ * happen under different inherited typography the painted notes overflow the
1909
+ * reserve, which is the invisible clipping this engine exists to avoid. The
1910
+ * registry lives in the staging tree but a band paints inside the output
1911
+ * container, so measure there. The host is never a page box, so a page's
1912
+ * `zoom` cannot scale a reserve that is accounted for in unscaled points.
1913
+ */
1914
+ noteMeasurementHost() {
1915
+ return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
1916
+ }
749
1917
  /**
750
1918
  * Measures the height of footnotes for given IDs (in points).
751
1919
  * Creates a temporary container to measure the footnotes.
@@ -753,140 +1921,246 @@ var DocxodusPagination = (() => {
753
1921
  * @param contentWidth - Width for measurement
754
1922
  * @param continuation - Optional continuation content to include first
755
1923
  */
756
- measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
1924
+ measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
757
1925
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
758
- if (footnoteIds.length === 0 && !hasContinuation || this.footnoteRegistry.size === 0) {
1926
+ if (footnoteIds.length === 0 && !hasContinuation) {
759
1927
  return 0;
760
1928
  }
761
- const measureContainer = document.createElement("div");
1929
+ if (this.footnoteRegistry.size === 0 && !hasContinuation) {
1930
+ return 0;
1931
+ }
1932
+ const measureContainer = this.document.createElement("div");
762
1933
  measureContainer.style.position = "absolute";
763
1934
  measureContainer.style.visibility = "hidden";
764
1935
  measureContainer.style.width = `${contentWidth}pt`;
765
1936
  measureContainer.style.left = "-9999px";
766
1937
  measureContainer.className = this.cssPrefix + "footnotes";
767
- const hr = document.createElement("hr");
768
- measureContainer.appendChild(hr);
1938
+ this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
769
1939
  if (hasContinuation) {
770
- const contWrapper = document.createElement("div");
771
- contWrapper.className = "footnote-continuation";
772
- for (const el of continuation.remainingElements) {
773
- contWrapper.appendChild(el.cloneNode(true));
774
- }
775
- measureContainer.appendChild(contWrapper);
1940
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
776
1941
  }
1942
+ const partialById = new Map(
1943
+ partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
1944
+ );
777
1945
  for (const id of footnoteIds) {
1946
+ this.checkpoint();
778
1947
  const footnote = this.footnoteRegistry.get(id);
779
1948
  if (footnote) {
780
- measureContainer.appendChild(footnote.cloneNode(true));
1949
+ const partial = partialById.get(id);
1950
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
781
1951
  }
782
1952
  }
783
- this.stagingElement.appendChild(measureContainer);
784
- const rect = measureContainer.getBoundingClientRect();
785
- const heightPt = pxToPt(rect.height);
786
- this.stagingElement.removeChild(measureContainer);
787
- return heightPt;
1953
+ this.noteMeasurementHost().appendChild(measureContainer);
1954
+ try {
1955
+ return pxToPt(measureContainer.getBoundingClientRect().height);
1956
+ } finally {
1957
+ measureContainer.remove();
1958
+ }
788
1959
  }
789
1960
  /**
790
- * Measures the height of just the continuation content (in points).
1961
+ * Partition a continuation for one page's note band, preferring complete children
1962
+ * and range-fragmenting an eligible paragraph when necessary. Always advances by
1963
+ * at least one element so an indivisible oversized paragraph follows the established
1964
+ * clipped fallback without trapping pagination in a loop.
791
1965
  */
792
- measureContinuationHeight(continuation, contentWidth) {
793
- if (!continuation || continuation.remainingElements.length === 0) {
794
- return 0;
795
- }
796
- const measureContainer = document.createElement("div");
1966
+ splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
1967
+ const fitting = [];
1968
+ let remaining = [];
1969
+ const measureContainer = this.document.createElement("div");
797
1970
  measureContainer.style.position = "absolute";
798
1971
  measureContainer.style.visibility = "hidden";
799
1972
  measureContainer.style.width = `${contentWidth}pt`;
800
1973
  measureContainer.style.left = "-9999px";
801
1974
  measureContainer.className = this.cssPrefix + "footnotes";
802
- const hr = document.createElement("hr");
803
- measureContainer.appendChild(hr);
804
- for (const el of continuation.remainingElements) {
805
- measureContainer.appendChild(el.cloneNode(true));
806
- }
807
- this.stagingElement.appendChild(measureContainer);
808
- const rect = measureContainer.getBoundingClientRect();
809
- const heightPt = pxToPt(rect.height);
810
- this.stagingElement.removeChild(measureContainer);
811
- return heightPt;
812
- }
813
- /**
814
- * Splits a footnote element into parts that fit within the available height.
815
- * Returns the elements that fit and the elements that need to continue.
816
- */
817
- splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
818
- const footnoteContent = footnoteElement.querySelector(".footnote-content");
819
- if (!footnoteContent) {
820
- return {
821
- fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
822
- overflow: []
823
- };
824
- }
825
- const children = Array.from(footnoteContent.children);
826
- if (children.length <= 1) {
827
- return {
828
- fits: children.map((el) => el.cloneNode(true)),
829
- overflow: []
830
- };
831
- }
832
- const fits = [];
833
- const overflow = [];
834
- let currentHeight = 0;
835
- const hrMeasure = document.createElement("div");
836
- hrMeasure.style.position = "absolute";
837
- hrMeasure.style.visibility = "hidden";
838
- hrMeasure.style.width = `${contentWidth}pt`;
839
- hrMeasure.style.left = "-9999px";
840
- hrMeasure.className = this.cssPrefix + "footnotes";
841
- const hr = document.createElement("hr");
842
- hrMeasure.appendChild(hr);
843
- this.stagingElement.appendChild(hrMeasure);
844
- const hrHeight = pxToPt(hrMeasure.getBoundingClientRect().height);
845
- this.stagingElement.removeChild(hrMeasure);
846
- currentHeight = hrHeight;
847
- const footnoteNumber = footnoteElement.querySelector(".footnote-number");
848
- for (let i = 0; i < children.length; i++) {
849
- const child = children[i];
850
- const measureContainer = document.createElement("div");
851
- measureContainer.style.position = "absolute";
852
- measureContainer.style.visibility = "hidden";
853
- measureContainer.style.width = `${contentWidth}pt`;
854
- measureContainer.style.left = "-9999px";
855
- measureContainer.className = this.cssPrefix + "footnotes";
856
- measureContainer.appendChild(child.cloneNode(true));
857
- this.stagingElement.appendChild(measureContainer);
858
- const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
859
- this.stagingElement.removeChild(measureContainer);
860
- if (currentHeight + childHeight <= availableHeightPt) {
861
- fits.push(child.cloneNode(true));
862
- currentHeight += childHeight;
863
- } else {
864
- for (let j = i; j < children.length; j++) {
865
- overflow.push(children[j].cloneNode(true));
1975
+ this.appendFootnoteSeparator(measureContainer, true);
1976
+ const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
1977
+ const content = wrapper.querySelector(":scope > .footnote-content");
1978
+ if (!content) {
1979
+ throw new Error("Footnote continuation is missing its content shell");
1980
+ }
1981
+ measureContainer.appendChild(wrapper);
1982
+ this.noteMeasurementHost().appendChild(measureContainer);
1983
+ try {
1984
+ for (let index = 0; index < continuation.remainingElements.length; index++) {
1985
+ this.checkpoint();
1986
+ const element = continuation.remainingElements[index];
1987
+ const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
1988
+ if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
1989
+ const sentinel = this.document.createElement("p");
1990
+ sentinel.className = "footnote-continuation-position-sentinel";
1991
+ sentinel.setAttribute("aria-hidden", "true");
1992
+ sentinel.style.setProperty("display", "none", "important");
1993
+ content.appendChild(sentinel);
1994
+ }
1995
+ const candidate = element.cloneNode(true);
1996
+ candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
1997
+ if (fitting.length === 0 && candidate.tagName === "P") {
1998
+ candidate.style.setProperty("display", "block", "important");
1999
+ }
2000
+ content.appendChild(candidate);
2001
+ const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
2002
+ if (candidateHeight <= availableHeightPt) {
2003
+ fitting.push(element);
2004
+ continue;
2005
+ }
2006
+ const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
2007
+ candidate.remove();
2008
+ let split = null;
2009
+ if (canSplitCandidate) {
2010
+ split = this.splitParagraphAtLargestFit(candidate, (head) => {
2011
+ content.appendChild(head);
2012
+ try {
2013
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2014
+ } finally {
2015
+ head.remove();
2016
+ }
2017
+ }, {
2018
+ // Both fallbacks below cut where ordinary line breaking would not,
2019
+ // so they are only ever right for an element that owns the whole
2020
+ // band: if this page already carries earlier content, a paragraph
2021
+ // that will not start here belongs intact in the next note band.
2022
+ emergencyGraphemeBreaks: fitting.length === 0,
2023
+ // A continuation page owns the full note band. If even one legal
2024
+ // grapheme is taller than it, clip only that unit and keep draining.
2025
+ forceFirstOnNoFit: fitting.length === 0
2026
+ });
2027
+ }
2028
+ if (split?.kind === "split") {
2029
+ if (sourcePosition) {
2030
+ split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
2031
+ split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
2032
+ }
2033
+ fitting.push(split.head);
2034
+ remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
2035
+ } else if (fitting.length === 0) {
2036
+ fitting.push(element);
2037
+ remaining = continuation.remainingElements.slice(index + 1);
2038
+ } else {
2039
+ remaining = continuation.remainingElements.slice(index);
866
2040
  }
867
2041
  break;
868
2042
  }
2043
+ } finally {
2044
+ measureContainer.remove();
869
2045
  }
870
- return { fits, overflow };
2046
+ return {
2047
+ current: {
2048
+ footnoteId: continuation.footnoteId,
2049
+ sourceAnchorId: continuation.sourceAnchorId,
2050
+ remainingElements: fitting
2051
+ },
2052
+ overflow: remaining.length > 0 ? {
2053
+ footnoteId: continuation.footnoteId,
2054
+ sourceAnchorId: continuation.sourceAnchorId,
2055
+ remainingElements: remaining
2056
+ } : null
2057
+ };
871
2058
  }
872
2059
  /**
873
- * Measures a single footnote's height.
2060
+ * Splits a footnote element into parts that fit within the available height.
2061
+ * Returns the elements that fit and the elements that need to continue.
874
2062
  */
875
- measureSingleFootnoteHeight(footnoteId, contentWidth) {
876
- const footnote = this.footnoteRegistry.get(footnoteId);
877
- if (!footnote) return 0;
878
- const measureContainer = document.createElement("div");
2063
+ splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
2064
+ const layoutContext = this.document.createElement("div");
2065
+ layoutContext.style.position = "absolute";
2066
+ layoutContext.style.visibility = "hidden";
2067
+ layoutContext.style.width = `${contentWidth}pt`;
2068
+ layoutContext.style.left = "-9999px";
2069
+ layoutContext.className = this.cssPrefix + "footnotes";
2070
+ this.appendFootnoteSeparator(layoutContext, false);
2071
+ const attachedFootnote = footnoteElement.cloneNode(true);
2072
+ layoutContext.appendChild(attachedFootnote);
2073
+ this.noteMeasurementHost().appendChild(layoutContext);
2074
+ const measureContainer = this.document.createElement("div");
879
2075
  measureContainer.style.position = "absolute";
880
2076
  measureContainer.style.visibility = "hidden";
881
2077
  measureContainer.style.width = `${contentWidth}pt`;
882
2078
  measureContainer.style.left = "-9999px";
883
2079
  measureContainer.className = this.cssPrefix + "footnotes";
884
- measureContainer.appendChild(footnote.cloneNode(true));
885
- this.stagingElement.appendChild(measureContainer);
886
- const rect = measureContainer.getBoundingClientRect();
887
- const heightPt = pxToPt(rect.height);
888
- this.stagingElement.removeChild(measureContainer);
889
- return heightPt;
2080
+ const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
2081
+ this.appendFootnoteSeparator(measureContainer, hasContinuation);
2082
+ if (hasContinuation) {
2083
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(
2084
+ existingPayload.continuation
2085
+ ));
2086
+ }
2087
+ const partialById = new Map(
2088
+ existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
2089
+ );
2090
+ for (const id of existingPayload?.footnoteIds ?? []) {
2091
+ this.checkpoint();
2092
+ const source = this.footnoteRegistry.get(id);
2093
+ if (!source) continue;
2094
+ const partial = partialById.get(id);
2095
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
2096
+ }
2097
+ const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
2098
+ const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
2099
+ if (!measuredContent) {
2100
+ layoutContext.remove();
2101
+ throw new Error("Footnote is missing its content shell");
2102
+ }
2103
+ measureContainer.appendChild(measuredPartial);
2104
+ this.noteMeasurementHost().appendChild(measureContainer);
2105
+ try {
2106
+ const footnoteContent = attachedFootnote.querySelector(".footnote-content");
2107
+ if (!footnoteContent) {
2108
+ return {
2109
+ fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
2110
+ overflow: []
2111
+ };
2112
+ }
2113
+ const children = Array.from(footnoteContent.children);
2114
+ const fits = [];
2115
+ let overflow = [];
2116
+ for (let i = 0; i < children.length; i++) {
2117
+ this.checkpoint();
2118
+ const child = children[i];
2119
+ const measuredCandidate = child.cloneNode(true);
2120
+ measuredContent.appendChild(measuredCandidate);
2121
+ const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2122
+ if (candidateFits) {
2123
+ fits.push(child.cloneNode(true));
2124
+ continue;
2125
+ }
2126
+ measuredCandidate.remove();
2127
+ const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
2128
+ measuredContent.appendChild(head);
2129
+ try {
2130
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2131
+ } finally {
2132
+ head.remove();
2133
+ }
2134
+ }, {
2135
+ // Both fallbacks cut where ordinary line breaking would not, so they
2136
+ // are only ever right for an element that owns the whole band: with
2137
+ // earlier siblings already packed here, a paragraph that will not
2138
+ // start belongs intact in the next note band.
2139
+ emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
2140
+ forceFirstOnNoFit: fits.length === 0 && forceProgress
2141
+ }) : null;
2142
+ if (split?.kind === "split") {
2143
+ fits.push(split.head);
2144
+ split.tail.setAttribute(
2145
+ FOOTNOTE_SOURCE_POSITION_ATTR,
2146
+ i === 0 ? "first" : "later"
2147
+ );
2148
+ overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
2149
+ } else if (split?.kind === "no-fit") {
2150
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
2151
+ } else if (fits.length === 0 && forceProgress) {
2152
+ fits.push(child.cloneNode(true));
2153
+ overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
2154
+ } else {
2155
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
2156
+ }
2157
+ break;
2158
+ }
2159
+ return { fits, overflow };
2160
+ } finally {
2161
+ measureContainer.remove();
2162
+ layoutContext.remove();
2163
+ }
890
2164
  }
891
2165
  /**
892
2166
  * Adds footnotes to a page container, including continuation content.
@@ -899,12 +2173,11 @@ var DocxodusPagination = (() => {
899
2173
  if (this.footnoteRegistry.size === 0 && !hasContinuation) {
900
2174
  return;
901
2175
  }
902
- const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
903
2176
  const maxFootnoteHeight = Math.min(
904
2177
  footnoteHeight,
905
2178
  bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
906
2179
  );
907
- const footnotesDiv = document.createElement("div");
2180
+ const footnotesDiv = this.document.createElement("div");
908
2181
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
909
2182
  footnotesDiv.style.position = "absolute";
910
2183
  footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
@@ -913,35 +2186,18 @@ var DocxodusPagination = (() => {
913
2186
  footnotesDiv.style.boxSizing = "border-box";
914
2187
  footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
915
2188
  footnotesDiv.style.overflow = "hidden";
916
- const hr = document.createElement("hr");
917
- footnotesDiv.appendChild(hr);
2189
+ this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
918
2190
  if (hasContinuation) {
919
- const contWrapper = document.createElement("div");
920
- contWrapper.className = "footnote-continuation";
921
- for (const el of continuation.remainingElements) {
922
- contWrapper.appendChild(el.cloneNode(true));
923
- }
924
- footnotesDiv.appendChild(contWrapper);
2191
+ footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
925
2192
  }
926
2193
  for (const id of footnoteIds) {
927
2194
  const partial = partialFootnotes?.find((p) => p.footnoteId === id);
928
2195
  if (partial) {
929
2196
  const footnote = this.footnoteRegistry.get(id);
930
2197
  if (footnote) {
931
- const partialDiv = document.createElement("div");
932
- partialDiv.className = "footnote-item";
933
- partialDiv.dataset.footnoteId = id;
934
- const numberSpan = footnote.querySelector(".footnote-number");
935
- if (numberSpan) {
936
- partialDiv.appendChild(numberSpan.cloneNode(true));
937
- }
938
- const contentSpan = document.createElement("span");
939
- contentSpan.className = "footnote-content";
940
- for (const el of partial.fittingElements) {
941
- contentSpan.appendChild(el.cloneNode(true));
942
- }
943
- partialDiv.appendChild(contentSpan);
944
- footnotesDiv.appendChild(partialDiv);
2198
+ footnotesDiv.appendChild(
2199
+ this.createPartialFootnoteItem(footnote, partial.fittingElements)
2200
+ );
945
2201
  }
946
2202
  } else {
947
2203
  const footnote = this.footnoteRegistry.get(id);
@@ -952,30 +2208,26 @@ var DocxodusPagination = (() => {
952
2208
  }
953
2209
  pageBox.appendChild(footnotesDiv);
954
2210
  }
955
- /**
956
- * Selects the appropriate header for a page based on section, page position, and page number.
957
- */
958
- selectHeader(sectionIndex, pageInSection, globalPageNumber) {
2211
+ /** Select the section's first/odd/even header from its one-based page position. */
2212
+ selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
959
2213
  const sectionHf = this.hfRegistry.get(sectionIndex);
960
2214
  if (!sectionHf) return void 0;
961
2215
  if (pageInSection === 1 && sectionHf.headerFirst) {
962
2216
  return sectionHf.headerFirst;
963
2217
  }
964
- if (globalPageNumber % 2 === 0 && sectionHf.headerEven) {
2218
+ if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
965
2219
  return sectionHf.headerEven;
966
2220
  }
967
2221
  return sectionHf.headerDefault;
968
2222
  }
969
- /**
970
- * Selects the appropriate footer for a page based on section, page position, and page number.
971
- */
972
- selectFooter(sectionIndex, pageInSection, globalPageNumber) {
2223
+ /** Select the section's first/odd/even footer from its one-based page position. */
2224
+ selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
973
2225
  const sectionHf = this.hfRegistry.get(sectionIndex);
974
2226
  if (!sectionHf) return void 0;
975
2227
  if (pageInSection === 1 && sectionHf.footerFirst) {
976
2228
  return sectionHf.footerFirst;
977
2229
  }
978
- if (globalPageNumber % 2 === 0 && sectionHf.footerEven) {
2230
+ if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
979
2231
  return sectionHf.footerEven;
980
2232
  }
981
2233
  return sectionHf.footerDefault;
@@ -990,7 +2242,7 @@ var DocxodusPagination = (() => {
990
2242
  * Deterministic: it depends only on the section's page setup and the registry's pre-measured
991
2243
  * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
992
2244
  */
993
- getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
2245
+ getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
994
2246
  const sectionHf = this.hfRegistry.get(sectionIndex);
995
2247
  return resolvePageBands(
996
2248
  dims,
@@ -999,14 +2251,14 @@ var DocxodusPagination = (() => {
999
2251
  sectionHf?.headerEvenHeight,
1000
2252
  sectionHf?.headerDefaultHeight,
1001
2253
  pageInSection,
1002
- globalPageNumber
2254
+ displayedPageNumber
1003
2255
  ),
1004
2256
  this.selectStoryHeight(
1005
2257
  sectionHf?.footerFirstHeight,
1006
2258
  sectionHf?.footerEvenHeight,
1007
2259
  sectionHf?.footerDefaultHeight,
1008
2260
  pageInSection,
1009
- globalPageNumber
2261
+ displayedPageNumber
1010
2262
  )
1011
2263
  );
1012
2264
  }
@@ -1014,9 +2266,9 @@ var DocxodusPagination = (() => {
1014
2266
  * The measured height of the running story this page position selects, mirroring
1015
2267
  * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1016
2268
  */
1017
- selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
2269
+ selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
1018
2270
  if (pageInSection === 1 && first != null) return first;
1019
- if (globalPageNumber % 2 === 0 && even != null) return even;
2271
+ if (displayedPageNumber % 2 === 0 && even != null) return even;
1020
2272
  return fallback ?? 0;
1021
2273
  }
1022
2274
  /**
@@ -1028,7 +2280,7 @@ var DocxodusPagination = (() => {
1028
2280
  * silently starts overlapping body text.
1029
2281
  */
1030
2282
  measureHeaderFooterHeight(source, contentWidth) {
1031
- const measureContainer = document.createElement("div");
2283
+ const measureContainer = this.document.createElement("div");
1032
2284
  measureContainer.style.position = "absolute";
1033
2285
  measureContainer.style.visibility = "hidden";
1034
2286
  measureContainer.style.width = `${contentWidth}pt`;
@@ -1047,79 +2299,275 @@ var DocxodusPagination = (() => {
1047
2299
  * Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
1048
2300
  * Supports footnote continuation - long footnotes can split across pages.
1049
2301
  */
1050
- flowToPages(blocks, dims, startPageNumber, sectionIndex) {
2302
+ flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
1051
2303
  const pages = [];
1052
2304
  let currentContent = [];
1053
2305
  let pageNumber = startPageNumber;
1054
- let pageInSection = 1;
2306
+ let pageSectionIndex = sectionIndex;
2307
+ const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
2308
+ const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
2309
+ const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
2310
+ const markSectionPlaced = (owner) => {
2311
+ if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
2312
+ };
2313
+ const previousBox = this.containerElement.querySelector(
2314
+ `.${this.cssPrefix}box:last-of-type`
2315
+ );
2316
+ let precedingDisplayedPageNumber = parseInt(
2317
+ previousBox?.dataset.displayedPageNumber ?? "0",
2318
+ 10
2319
+ );
2320
+ const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
2321
+ const numbering = this.pageNumbering.get(owner) ?? {};
2322
+ if (numbering.start !== void 0) {
2323
+ return numbering.start + ownerPageInSection - 1;
2324
+ }
2325
+ const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
2326
+ const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
2327
+ return currentPhysicalNumber + physicalPage - pageNumber;
2328
+ };
1055
2329
  let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1056
- dims,
1057
- sectionIndex,
1058
- pageInSection,
1059
- pageNumber
2330
+ dimensionsFor(),
2331
+ pageSectionIndex,
2332
+ pageInSection(),
2333
+ displayedPageNumber()
1060
2334
  );
1061
2335
  let remainingHeight = effectiveContentHeight;
1062
2336
  let prevMarginBottomPt = 0;
1063
2337
  let currentFootnoteIds = [];
2338
+ let currentPageHasFootnoteReference = false;
1064
2339
  let currentFootnoteHeight = 0;
1065
2340
  let currentContinuation = this.pendingFootnoteContinuation;
1066
2341
  let nextPageContinuation = null;
2342
+ let currentContinuationPartitioned = false;
2343
+ let currentPageAdmitted = false;
1067
2344
  let deferredFootnoteIds = [];
1068
2345
  let currentPartialFootnotes = [];
1069
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1070
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1071
- }
1072
- const finishPage = () => {
2346
+ const adoptEmptyPageOwner = (owner) => {
2347
+ pageSectionIndex = owner;
2348
+ const bands = this.getPageBands(
2349
+ dimensionsFor(owner),
2350
+ owner,
2351
+ pageInSection(owner),
2352
+ displayedPageNumber(owner)
2353
+ );
2354
+ effectiveContentHeight = bands.bodyHeight;
2355
+ remainingHeight = effectiveContentHeight;
2356
+ };
2357
+ const admitCurrentPage = () => {
2358
+ if (currentPageAdmitted) return;
2359
+ this.admitPageAllocation();
2360
+ currentPageAdmitted = true;
2361
+ };
2362
+ const prepareCurrentContinuation = () => {
2363
+ if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
2364
+ admitCurrentPage();
2365
+ const ownedDimensions = dimensionsFor();
2366
+ const pageBands = this.getPageBands(
2367
+ ownedDimensions,
2368
+ pageSectionIndex,
2369
+ pageInSection(),
2370
+ displayedPageNumber()
2371
+ );
2372
+ const partition = this.splitContinuationForPage(
2373
+ currentContinuation,
2374
+ pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
2375
+ ownedDimensions.contentWidth
2376
+ );
2377
+ currentContinuation = partition.current;
2378
+ if (partition.overflow) nextPageContinuation = partition.overflow;
2379
+ currentContinuationPartitioned = true;
2380
+ currentFootnoteHeight = this.measureFootnotesHeight(
2381
+ currentFootnoteIds,
2382
+ ownedDimensions.contentWidth,
2383
+ currentContinuation,
2384
+ currentPartialFootnotes
2385
+ );
2386
+ };
2387
+ const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
1073
2388
  const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
1074
- if (currentContent.length === 0 && !hasCurrentContinuation) return;
2389
+ if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
2390
+ adoptEmptyPageOwner(nextPageSectionIndex);
2391
+ return;
2392
+ }
2393
+ prepareCurrentContinuation();
2394
+ admitCurrentPage();
2395
+ let pageContinuation = currentContinuation;
2396
+ const ownedPageInSection = pageInSection();
2397
+ const ownedDimensions = dimensionsFor();
2398
+ const ownedDisplayedPageNumber = displayedPageNumber(
2399
+ pageSectionIndex,
2400
+ ownedPageInSection
2401
+ );
2402
+ const pageBands = this.getPageBands(
2403
+ ownedDimensions,
2404
+ pageSectionIndex,
2405
+ ownedPageInSection,
2406
+ ownedDisplayedPageNumber
2407
+ );
2408
+ const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2409
+ pageContinuation = currentContinuation;
2410
+ currentFootnoteHeight = this.measureFootnotesHeight(
2411
+ currentFootnoteIds,
2412
+ ownedDimensions.contentWidth,
2413
+ pageContinuation,
2414
+ currentPartialFootnotes
2415
+ );
2416
+ if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
2417
+ const fittingIds = [];
2418
+ for (let index = 0; index < currentFootnoteIds.length; index++) {
2419
+ const footnoteId = currentFootnoteIds[index];
2420
+ const candidateIds = [...fittingIds, footnoteId];
2421
+ const candidateHeight = this.measureFootnotesHeight(
2422
+ candidateIds,
2423
+ ownedDimensions.contentWidth,
2424
+ pageContinuation
2425
+ );
2426
+ const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
2427
+ if (guardedCandidateHeight <= maxFootnoteHeight) {
2428
+ fittingIds.push(footnoteId);
2429
+ currentFootnoteHeight = guardedCandidateHeight;
2430
+ continue;
2431
+ }
2432
+ const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
2433
+ if (fittingIds.length === 0 && !hasPageContinuation) {
2434
+ const source = this.footnoteRegistry.get(footnoteId);
2435
+ const split = source ? this.splitFootnoteToFit(
2436
+ source,
2437
+ maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
2438
+ ownedDimensions.contentWidth,
2439
+ true
2440
+ ) : null;
2441
+ fittingIds.push(footnoteId);
2442
+ if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
2443
+ currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
2444
+ nextPageContinuation = {
2445
+ footnoteId,
2446
+ sourceAnchorId: source.dataset.sourceAnchorId,
2447
+ remainingElements: split.overflow
2448
+ };
2449
+ currentFootnoteHeight = maxFootnoteHeight;
2450
+ } else {
2451
+ currentFootnoteHeight = guardedCandidateHeight;
2452
+ }
2453
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
2454
+ } else {
2455
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
2456
+ }
2457
+ break;
2458
+ }
2459
+ currentFootnoteIds = fittingIds;
2460
+ }
1075
2461
  const page = this.createPage(
1076
- dims,
2462
+ ownedDimensions,
1077
2463
  pageNumber,
1078
- sectionIndex,
2464
+ pageSectionIndex,
2465
+ ownedDisplayedPageNumber,
1079
2466
  currentContent,
1080
- pageInSection,
2467
+ ownedPageInSection,
1081
2468
  currentFootnoteIds,
1082
2469
  currentFootnoteHeight,
1083
- currentContinuation,
1084
- currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
2470
+ pageContinuation,
2471
+ currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
2472
+ false,
2473
+ currentPageAdmitted
1085
2474
  );
1086
2475
  pages.push(page);
2476
+ precedingDisplayedPageNumber = ownedDisplayedPageNumber;
1087
2477
  pageNumber++;
1088
- pageInSection++;
1089
2478
  currentContent = [];
1090
- const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2479
+ pageSectionIndex = nextPageSectionIndex;
2480
+ if (!sectionStartPages.has(pageSectionIndex)) {
2481
+ sectionStartPages.set(pageSectionIndex, pageNumber);
2482
+ }
2483
+ const newBands = this.getPageBands(
2484
+ dimensionsFor(),
2485
+ pageSectionIndex,
2486
+ pageInSection(),
2487
+ displayedPageNumber()
2488
+ );
1091
2489
  effectiveContentHeight = newBands.bodyHeight;
1092
2490
  remainingHeight = effectiveContentHeight;
1093
2491
  prevMarginBottomPt = 0;
1094
2492
  currentFootnoteIds = [];
2493
+ currentPageHasFootnoteReference = false;
1095
2494
  currentPartialFootnotes = [];
1096
2495
  currentContinuation = nextPageContinuation;
1097
2496
  nextPageContinuation = null;
2497
+ currentContinuationPartitioned = false;
2498
+ currentPageAdmitted = false;
1098
2499
  if (deferredFootnoteIds.length > 0) {
1099
2500
  currentFootnoteIds = [...deferredFootnoteIds];
1100
2501
  deferredFootnoteIds = [];
1101
2502
  }
1102
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1103
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1104
- } else {
1105
- currentFootnoteHeight = 0;
1106
- }
1107
- if (currentFootnoteIds.length > 0) {
1108
- currentFootnoteHeight += this.measureFootnotesHeight(
1109
- currentFootnoteIds,
1110
- dims.contentWidth,
1111
- null
1112
- );
1113
- }
2503
+ const nextDimensions = dimensionsFor();
2504
+ currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
1114
2505
  };
2506
+ const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
1115
2507
  for (let i = 0; i < blocks.length; i++) {
2508
+ this.checkpoint();
1116
2509
  const block = blocks[i];
2510
+ const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
2511
+ if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
2512
+ finishPage(block.sectionIndex);
2513
+ }
2514
+ const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
2515
+ if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
2516
+ adoptEmptyPageOwner(block.sectionIndex);
2517
+ }
2518
+ prepareCurrentContinuation();
2519
+ if (currentFootnoteHeight > 0) {
2520
+ const ownerDimensions = dimensionsFor();
2521
+ const ownerBands = this.getPageBands(
2522
+ ownerDimensions,
2523
+ pageSectionIndex,
2524
+ pageInSection(),
2525
+ displayedPageNumber()
2526
+ );
2527
+ const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2528
+ if (currentFootnoteHeight > ownerMaxFootnoteArea) {
2529
+ const fittingIds = [];
2530
+ let fittingHeight = 0;
2531
+ if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
2532
+ for (const footnoteId of currentFootnoteIds) {
2533
+ this.checkpoint();
2534
+ const candidateHeight = this.measureFootnotesHeight(
2535
+ [...fittingIds, footnoteId],
2536
+ ownerDimensions.contentWidth
2537
+ );
2538
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
2539
+ fittingIds.push(footnoteId);
2540
+ fittingHeight = candidateHeight;
2541
+ }
2542
+ }
2543
+ if (fittingIds.length > 0) {
2544
+ deferredFootnoteIds = [
2545
+ ...currentFootnoteIds.slice(fittingIds.length),
2546
+ ...deferredFootnoteIds
2547
+ ];
2548
+ currentFootnoteIds = fittingIds;
2549
+ currentFootnoteHeight = fittingHeight;
2550
+ } else {
2551
+ finishPage(block.sectionIndex);
2552
+ i--;
2553
+ continue;
2554
+ }
2555
+ }
2556
+ }
2557
+ const blockDimensions = dimensionsFor(block.sectionIndex);
2558
+ const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
2559
+ const freshBlockPageBodyHeight = this.getPageBands(
2560
+ blockDimensions,
2561
+ block.sectionIndex,
2562
+ nextBlockPageInSection,
2563
+ displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
2564
+ ).bodyHeight;
1117
2565
  if (block.isPageBreak) {
1118
- finishPage();
2566
+ finishPage(block.sectionIndex);
1119
2567
  continue;
1120
2568
  }
1121
2569
  if (block.pageBreakBefore && currentContent.length > 0) {
1122
- finishPage();
2570
+ finishPage(block.sectionIndex);
1123
2571
  }
1124
2572
  const previousBlock = blocks[i - 1];
1125
2573
  const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
@@ -1135,7 +2583,7 @@ var DocxodusPagination = (() => {
1135
2583
  if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1136
2584
  const totalChainFootnoteHeight = this.measureFootnotesHeight(
1137
2585
  [...currentFootnoteIds, ...newChainFootnoteIds],
1138
- dims.contentWidth,
2586
+ blockDimensions.contentWidth,
1139
2587
  currentContinuation
1140
2588
  );
1141
2589
  additionalChainFootnoteHeight = Math.max(
@@ -1146,58 +2594,69 @@ var DocxodusPagination = (() => {
1146
2594
  const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
1147
2595
  keepChain,
1148
2596
  prevMarginBottomPt,
1149
- currentContent.length === 0
2597
+ currentContent.length === 0,
2598
+ pageInSection(block.sectionIndex)
1150
2599
  ) + additionalChainFootnoteHeight;
1151
2600
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1152
2601
  if (currentChainHeight > currentAvailableHeight) {
1153
2602
  const nextPageBands = this.getPageBands(
1154
- dims,
1155
- sectionIndex,
1156
- pageInSection + 1,
1157
- pageNumber + 1
2603
+ dimensionsFor(block.sectionIndex),
2604
+ block.sectionIndex,
2605
+ pageInSection(block.sectionIndex, pageNumber + 1),
2606
+ displayedPageNumber(
2607
+ block.sectionIndex,
2608
+ pageInSection(block.sectionIndex, pageNumber + 1),
2609
+ pageNumber + 1
2610
+ )
1158
2611
  );
1159
2612
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
1160
2613
  keepChain,
1161
2614
  0,
1162
- true
2615
+ true,
2616
+ pageInSection(block.sectionIndex, pageNumber + 1)
1163
2617
  );
1164
2618
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
1165
2619
  newChainFootnoteIds,
1166
- dims.contentWidth,
2620
+ blockDimensions.contentWidth,
1167
2621
  nextPageContinuation
1168
2622
  );
1169
2623
  if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1170
- finishPage();
2624
+ finishPage(block.sectionIndex);
1171
2625
  }
1172
2626
  }
1173
2627
  }
1174
2628
  }
1175
- const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
1176
2629
  const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
2630
+ let combinedFootnoteHeight = currentFootnoteHeight;
1177
2631
  let additionalFootnoteHeight = 0;
1178
2632
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1179
2633
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
1180
- const totalFootnoteHeight = this.measureFootnotesHeight(
2634
+ combinedFootnoteHeight = this.measureFootnotesHeight(
1181
2635
  combinedFootnoteIds,
1182
- dims.contentWidth,
1183
- currentContinuation
2636
+ blockDimensions.contentWidth,
2637
+ currentContinuation,
2638
+ currentPartialFootnotes
2639
+ );
2640
+ additionalFootnoteHeight = Math.max(
2641
+ 0,
2642
+ combinedFootnoteHeight - currentFootnoteHeight
1184
2643
  );
1185
- additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
1186
2644
  }
1187
2645
  const isFirstOnPage = currentContent.length === 0;
1188
- let effectiveMarginTop = block.marginTopPt;
1189
- if (!isFirstOnPage) {
1190
- effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
1191
- }
2646
+ const effectiveMarginTop = this.effectiveBlockMarginTop(
2647
+ block,
2648
+ prevMarginBottomPt,
2649
+ isFirstOnPage,
2650
+ pageInSection(block.sectionIndex)
2651
+ );
1192
2652
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
1193
2653
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
1194
2654
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
1195
2655
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
1196
- const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
1197
2656
  if (blockSpace > effectiveRemainingHeight) {
1198
2657
  const paragraphFragments = this.tryFragmentParagraph(
1199
2658
  block,
1200
- dims,
2659
+ blockDimensions,
1201
2660
  effectiveRemainingHeight,
1202
2661
  effectiveMarginTop
1203
2662
  );
@@ -1207,40 +2666,74 @@ var DocxodusPagination = (() => {
1207
2666
  continue;
1208
2667
  }
1209
2668
  }
1210
- if (blockSpace <= effectiveRemainingHeight) {
1211
- currentContent.push(block.element.cloneNode(true));
2669
+ if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
2670
+ markSectionPlaced(block.sectionIndex);
2671
+ currentContent.push(this.cloneBlockForPage(
2672
+ block,
2673
+ isFirstOnPage,
2674
+ pageInSection(block.sectionIndex)
2675
+ ));
1212
2676
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1213
2677
  prevMarginBottomPt = block.marginBottomPt;
1214
2678
  if (newFootnoteIds.length > 0) {
1215
2679
  currentFootnoteIds.push(...newFootnoteIds);
1216
- currentFootnoteHeight += additionalFootnoteHeight;
2680
+ currentFootnoteHeight = combinedFootnoteHeight;
1217
2681
  }
1218
- } else if (block.heightPt + block.marginTopPt <= effectiveContentHeight) {
2682
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2683
+ } else if (block.heightPt + this.effectiveBlockMarginTop(
2684
+ block,
2685
+ 0,
2686
+ true,
2687
+ pageInSection(block.sectionIndex, pageNumber + 1)
2688
+ ) <= freshBlockPageBodyHeight) {
1219
2689
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
1220
- const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
1221
- const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
1222
2690
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
1223
- currentContent.push(block.element.cloneNode(true));
2691
+ markSectionPlaced(block.sectionIndex);
2692
+ currentContent.push(this.cloneBlockForPage(
2693
+ block,
2694
+ isFirstOnPage,
2695
+ pageInSection(block.sectionIndex)
2696
+ ));
1224
2697
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1225
2698
  prevMarginBottomPt = block.marginBottomPt;
1226
2699
  const availableForFootnotes = Math.min(
1227
2700
  maxFootnoteArea,
1228
2701
  effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
1229
2702
  );
1230
- for (const footnoteId of newFootnoteIds) {
2703
+ const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2704
+ const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
2705
+ for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
2706
+ this.checkpoint();
2707
+ const footnoteId = newFootnoteIds[noteIndex];
1231
2708
  const footnote = this.footnoteRegistry.get(footnoteId);
1232
2709
  if (!footnote) continue;
1233
- const footnoteHeight = this.measureSingleFootnoteHeight(footnoteId, dims.contentWidth);
2710
+ if (nextPageContinuation) {
2711
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2712
+ break;
2713
+ }
2714
+ const candidateIds = [...currentFootnoteIds, footnoteId];
2715
+ const candidateHeight = this.measureFootnotesHeight(
2716
+ candidateIds,
2717
+ blockDimensions.contentWidth,
2718
+ currentContinuation,
2719
+ currentPartialFootnotes
2720
+ );
1234
2721
  const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
1235
- if (footnoteHeight <= spaceLeftForFootnotes) {
2722
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
1236
2723
  currentFootnoteIds.push(footnoteId);
1237
- currentFootnoteHeight += footnoteHeight;
2724
+ currentFootnoteHeight = candidateHeight;
1238
2725
  } else {
1239
2726
  if (spaceLeftForFootnotes > 20) {
1240
2727
  const { fits, overflow } = this.splitFootnoteToFit(
1241
2728
  footnote,
1242
- spaceLeftForFootnotes,
1243
- dims.contentWidth
2729
+ availableForFootnotes,
2730
+ blockDimensions.contentWidth,
2731
+ deferralCannotHelp,
2732
+ {
2733
+ footnoteIds: currentFootnoteIds,
2734
+ continuation: currentContinuation,
2735
+ partialFootnotes: currentPartialFootnotes
2736
+ }
1244
2737
  );
1245
2738
  if (fits.length > 0) {
1246
2739
  currentFootnoteIds.push(footnoteId);
@@ -1251,66 +2744,70 @@ var DocxodusPagination = (() => {
1251
2744
  if (overflow.length > 0) {
1252
2745
  nextPageContinuation = {
1253
2746
  footnoteId,
2747
+ sourceAnchorId: footnote.dataset.sourceAnchorId,
1254
2748
  remainingElements: overflow
1255
2749
  };
1256
2750
  }
1257
- currentFootnoteHeight = availableForFootnotes;
2751
+ currentFootnoteHeight = this.measureFootnotesHeight(
2752
+ currentFootnoteIds,
2753
+ blockDimensions.contentWidth,
2754
+ currentContinuation,
2755
+ currentPartialFootnotes
2756
+ );
2757
+ if (overflow.length > 0) {
2758
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
2759
+ break;
2760
+ }
1258
2761
  } else {
1259
- deferredFootnoteIds.push(footnoteId);
2762
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2763
+ break;
1260
2764
  }
1261
2765
  } else {
1262
- deferredFootnoteIds.push(footnoteId);
2766
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2767
+ break;
1263
2768
  }
1264
2769
  }
1265
2770
  }
1266
- } else if (canExpandFootnotes && newFootnoteIds.length > 0) {
1267
- const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
1268
- const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
1269
- if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
1270
- currentContent.push(block.element.cloneNode(true));
1271
- remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
1272
- prevMarginBottomPt = block.marginBottomPt;
1273
- currentFootnoteIds.push(...newFootnoteIds);
1274
- currentFootnoteHeight = expandedFootnoteSpace;
1275
- } else {
1276
- finishPage();
1277
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1278
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1279
- currentContent.push(block.element.cloneNode(true));
1280
- remainingHeight = effectiveContentHeight - newPageSpace;
1281
- prevMarginBottomPt = block.marginBottomPt;
1282
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1283
- currentFootnoteHeight = newPageFootnoteHeight;
1284
- }
2771
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
1285
2772
  } else {
1286
- finishPage();
1287
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1288
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1289
- currentContent.push(block.element.cloneNode(true));
1290
- remainingHeight = effectiveContentHeight - newPageSpace;
1291
- prevMarginBottomPt = block.marginBottomPt;
1292
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1293
- currentFootnoteHeight = newPageFootnoteHeight;
2773
+ finishPage(block.sectionIndex, !pageHasPayload());
2774
+ i--;
2775
+ continue;
1294
2776
  }
1295
2777
  } else {
1296
- const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
2778
+ const tableFragments = this.trySplitSimpleOversizedTable(
2779
+ block,
2780
+ dimensionsFor(block.sectionIndex),
2781
+ block.sectionIndex
2782
+ );
1297
2783
  if (tableFragments) {
1298
2784
  if (currentContent.length > 0 || currentContinuation) {
1299
- finishPage();
2785
+ finishPage(block.sectionIndex);
1300
2786
  }
1301
2787
  blocks.splice(i, 1, ...tableFragments);
1302
2788
  i--;
1303
2789
  continue;
1304
2790
  }
1305
- if (currentContent.length > 0) {
1306
- finishPage();
2791
+ if (pageHasPayload()) {
2792
+ finishPage(block.sectionIndex);
2793
+ i--;
2794
+ continue;
1307
2795
  }
1308
- currentContent.push(block.element.cloneNode(true));
1309
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1310
- finishPage();
2796
+ markSectionPlaced(block.sectionIndex);
2797
+ currentContent.push(this.cloneBlockForPage(
2798
+ block,
2799
+ true,
2800
+ pageInSection(block.sectionIndex)
2801
+ ));
2802
+ deferredFootnoteIds.push(...newFootnoteIds);
2803
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2804
+ finishPage(block.sectionIndex);
1311
2805
  }
1312
2806
  }
1313
2807
  finishPage();
2808
+ while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
2809
+ finishPage();
2810
+ }
1314
2811
  this.pendingFootnoteContinuation = nextPageContinuation;
1315
2812
  return pages;
1316
2813
  }
@@ -1335,6 +2832,19 @@ var DocxodusPagination = (() => {
1335
2832
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1336
2833
  }
1337
2834
  }
2835
+ /** A repeated margin note is presentation, not a second bookmark/link target. */
2836
+ makeClonedMarginCommentInert(root) {
2837
+ this.makeClonedStoryInert(root);
2838
+ const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
2839
+ for (const element of nodes) {
2840
+ element.removeAttribute("id");
2841
+ if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
2842
+ element.removeAttribute("href");
2843
+ element.setAttribute("aria-disabled", "true");
2844
+ element.tabIndex = -1;
2845
+ }
2846
+ }
2847
+ }
1338
2848
  /**
1339
2849
  * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1340
2850
  *
@@ -1360,7 +2870,7 @@ var DocxodusPagination = (() => {
1360
2870
  const context = {
1361
2871
  staticLeft: toPageX(staticRect.left),
1362
2872
  staticTop: toPageY(staticRect.top),
1363
- lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2873
+ lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
1364
2874
  paragraphLeft: toPageX(paragraphRect.left),
1365
2875
  paragraphTop: toPageY(paragraphRect.top),
1366
2876
  paragraphWidth: paragraphRect.width / pixelsPerPoint,
@@ -1490,36 +3000,48 @@ var DocxodusPagination = (() => {
1490
3000
  else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1491
3001
  return reference.start + (reference.size - objectSize) * fraction;
1492
3002
  }
3003
+ /** Charge a physical page before any page-owned partitioning or DOM allocation. */
3004
+ admitPageAllocation() {
3005
+ const prospectivePageCount = this.createdPageCount + 1;
3006
+ this.pageCountCheckpoint?.(prospectivePageCount);
3007
+ this.createdPageCount = prospectivePageCount;
3008
+ }
1493
3009
  /**
1494
3010
  * Creates a page container element.
1495
3011
  */
1496
- createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
1497
- const pageBox = document.createElement("div");
3012
+ createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
3013
+ if (!pageAlreadyAdmitted) this.admitPageAllocation();
3014
+ const pageBox = this.document.createElement("div");
1498
3015
  pageBox.className = `${this.cssPrefix}box`;
1499
3016
  pageBox.style.width = `${dims.pageWidth}pt`;
1500
3017
  pageBox.style.height = `${dims.pageHeight}pt`;
1501
3018
  pageBox.style.overflow = "hidden";
1502
3019
  pageBox.style.position = "relative";
1503
3020
  if (this.scale !== 1) {
1504
- pageBox.style.zoom = String(this.scale);
1505
- pageBox.style.transform = `scale(${this.scale})`;
1506
- pageBox.style.transformOrigin = "top left";
1507
- const heightReductionPt = dims.pageHeight * (1 - this.scale);
1508
- const widthReductionPt = dims.pageWidth * (1 - this.scale);
1509
- const heightReductionPx = ptToPx(heightReductionPt);
1510
- const widthReductionPx = ptToPx(widthReductionPt);
1511
- pageBox.style.marginRight = `-${widthReductionPx}px`;
1512
- pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
3021
+ if (this.view.CSS?.supports("zoom", "1")) {
3022
+ pageBox.style.zoom = String(this.scale);
3023
+ } else {
3024
+ pageBox.style.transform = `scale(${this.scale})`;
3025
+ pageBox.style.transformOrigin = "top left";
3026
+ const heightReductionPt = dims.pageHeight * (1 - this.scale);
3027
+ const widthReductionPt = dims.pageWidth * (1 - this.scale);
3028
+ const heightReductionPx = ptToPx(heightReductionPt);
3029
+ const widthReductionPx = ptToPx(widthReductionPt);
3030
+ pageBox.style.marginRight = `-${widthReductionPx}px`;
3031
+ pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
3032
+ }
1513
3033
  }
1514
3034
  pageBox.style.willChange = "transform";
1515
3035
  pageBox.style.contain = "layout paint";
1516
3036
  pageBox.dataset.pageNumber = String(pageNumber);
1517
3037
  pageBox.dataset.sectionIndex = String(sectionIndex);
3038
+ pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
3039
+ if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
1518
3040
  pageBox.dataset.pageInSection = String(pageInSection);
1519
- const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1520
- const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
3041
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
3042
+ const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
1521
3043
  if (headerSource) {
1522
- const headerDiv = document.createElement("div");
3044
+ const headerDiv = this.document.createElement("div");
1523
3045
  headerDiv.className = `${this.cssPrefix}header`;
1524
3046
  headerDiv.style.position = "absolute";
1525
3047
  headerDiv.style.top = `${bands.headerTop}pt`;
@@ -1541,7 +3063,7 @@ var DocxodusPagination = (() => {
1541
3063
  }
1542
3064
  const contentAreaTop = bands.bodyTop;
1543
3065
  const contentAreaHeight = bands.bodyHeight;
1544
- const contentArea = document.createElement("div");
3066
+ const contentArea = this.document.createElement("div");
1545
3067
  contentArea.className = `${this.cssPrefix}content`;
1546
3068
  contentArea.style.position = "absolute";
1547
3069
  contentArea.style.top = `${contentAreaTop}pt`;
@@ -1554,12 +3076,46 @@ var DocxodusPagination = (() => {
1554
3076
  }
1555
3077
  pageBox.appendChild(contentArea);
1556
3078
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1557
- if (footnoteIds.length > 0 || hasContinuation) {
3079
+ if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
1558
3080
  this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1559
3081
  }
1560
- const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
3082
+ const pageCommentIds = [];
3083
+ for (const marker of Array.from(
3084
+ pageBox.querySelectorAll("[data-comment-id]")
3085
+ )) {
3086
+ if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
3087
+ const id = marker.dataset.commentId;
3088
+ if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
3089
+ pageCommentIds.push(id);
3090
+ }
3091
+ }
3092
+ if (pageCommentIds.length > 0) {
3093
+ const marginColumn = this.document.createElement("aside");
3094
+ marginColumn.className = `${this.cssPrefix}comment-margin`;
3095
+ marginColumn.style.position = "absolute";
3096
+ marginColumn.style.top = `${contentAreaTop}pt`;
3097
+ marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
3098
+ marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
3099
+ marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
3100
+ marginColumn.style.overflow = "hidden";
3101
+ marginColumn.style.boxSizing = "border-box";
3102
+ const placedThreadRoots = /* @__PURE__ */ new Set();
3103
+ for (const id of pageCommentIds) {
3104
+ const source = this.commentMarginRegistry.get(id);
3105
+ if (source) {
3106
+ const rootId = source.dataset.commentId ?? id;
3107
+ if (placedThreadRoots.has(rootId)) continue;
3108
+ placedThreadRoots.add(rootId);
3109
+ const clone = source.cloneNode(true);
3110
+ this.makeClonedMarginCommentInert(clone);
3111
+ marginColumn.appendChild(clone);
3112
+ }
3113
+ }
3114
+ pageBox.appendChild(marginColumn);
3115
+ }
3116
+ const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
1561
3117
  if (footerSource) {
1562
- const footerDiv = document.createElement("div");
3118
+ const footerDiv = this.document.createElement("div");
1563
3119
  footerDiv.className = `${this.cssPrefix}footer`;
1564
3120
  footerDiv.style.position = "absolute";
1565
3121
  footerDiv.style.top = "auto";
@@ -1579,8 +3135,8 @@ var DocxodusPagination = (() => {
1579
3135
  }
1580
3136
  pageBox.appendChild(footerDiv);
1581
3137
  }
1582
- if (this.showPageNumbers) {
1583
- const pageNum = document.createElement("div");
3138
+ if (this.showPageNumbers && !isSectionFiller) {
3139
+ const pageNum = this.document.createElement("div");
1584
3140
  pageNum.className = `${this.cssPrefix}number`;
1585
3141
  pageNum.textContent = String(pageNumber);
1586
3142
  pageBox.appendChild(pageNum);
@@ -1603,7 +3159,8 @@ var DocxodusPagination = (() => {
1603
3159
  }
1604
3160
  };
1605
3161
  function paginateHtml(html, container, options = {}) {
1606
- const containerEl = typeof container === "string" ? document.getElementById(container) : container;
3162
+ const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
3163
+ const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
1607
3164
  if (!containerEl) {
1608
3165
  throw new Error("Container element not found");
1609
3166
  }