docxodus 9.9.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +178 -0
  2. package/dist/canonical.d.ts +7 -0
  3. package/dist/canonical.d.ts.map +1 -0
  4. package/dist/canonical.js +56 -0
  5. package/dist/canonical.js.map +1 -0
  6. package/dist/docxodus.worker.d.ts +2 -1
  7. package/dist/docxodus.worker.d.ts.map +1 -1
  8. package/dist/docxodus.worker.js +244 -3
  9. package/dist/docxodus.worker.js.map +1 -1
  10. package/dist/editor.bundle.js +1914 -419
  11. package/dist/editor.d.ts +22 -2
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +94 -9
  14. package/dist/editor.js.map +1 -1
  15. package/dist/embed.bundle.js +2803 -469
  16. package/dist/embed.iife.js +2801 -467
  17. package/dist/export-assets.json +283 -0
  18. package/dist/export-browser.bundle.js +7990 -0
  19. package/dist/export-browser.d.ts +266 -0
  20. package/dist/export-browser.d.ts.map +1 -0
  21. package/dist/export-browser.js +2915 -0
  22. package/dist/export-browser.js.map +1 -0
  23. package/dist/export-resource-limits-v1.json +57 -0
  24. package/dist/font-contract.d.ts +150 -0
  25. package/dist/font-contract.d.ts.map +1 -0
  26. package/dist/font-contract.js +59 -0
  27. package/dist/font-contract.js.map +1 -0
  28. package/dist/font-runtime.d.ts +33 -0
  29. package/dist/font-runtime.d.ts.map +1 -0
  30. package/dist/font-runtime.js +1100 -0
  31. package/dist/font-runtime.js.map +1 -0
  32. package/dist/index.d.ts +62 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +130 -16
  35. package/dist/index.js.map +1 -1
  36. package/dist/page-geometry.d.ts +8 -1
  37. package/dist/page-geometry.d.ts.map +1 -1
  38. package/dist/page-geometry.js +37 -11
  39. package/dist/page-geometry.js.map +1 -1
  40. package/dist/pagination.bundle.js +1827 -410
  41. package/dist/pagination.d.ts +249 -15
  42. package/dist/pagination.d.ts.map +1 -1
  43. package/dist/pagination.js +1942 -453
  44. package/dist/pagination.js.map +1 -1
  45. package/dist/react.d.ts +11 -4
  46. package/dist/react.d.ts.map +1 -1
  47. package/dist/react.js +38 -5
  48. package/dist/react.js.map +1 -1
  49. package/dist/render-report-v2.schema.json +1581 -0
  50. package/dist/ribbon-chrome.d.ts +1 -1
  51. package/dist/ribbon-chrome.d.ts.map +1 -1
  52. package/dist/ribbon-chrome.js +45 -0
  53. package/dist/ribbon-chrome.js.map +1 -1
  54. package/dist/ribbon.js +70 -0
  55. package/dist/ribbon.js.map +1 -1
  56. package/dist/session.bundle.js +695 -29
  57. package/dist/session.d.ts +134 -19
  58. package/dist/session.d.ts.map +1 -1
  59. package/dist/session.js +562 -25
  60. package/dist/session.js.map +1 -1
  61. package/dist/types.d.ts +1207 -16
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js.map +1 -1
  64. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  65. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  66. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  67. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  68. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  69. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  70. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  71. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  72. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  73. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  74. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  75. package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
  76. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  77. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  78. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  79. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  80. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  81. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  82. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  83. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  84. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  85. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  86. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  87. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  88. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  89. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  90. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  91. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  92. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  93. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  94. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  95. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  96. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  97. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  98. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  99. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  100. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  101. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  102. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  103. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  104. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  105. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  106. package/dist/worker-proxy.bundle.js +196 -29
  107. package/dist/worker-proxy.d.ts +36 -3
  108. package/dist/worker-proxy.d.ts.map +1 -1
  109. package/dist/worker-proxy.js +186 -33
  110. package/dist/worker-proxy.js.map +1 -1
  111. package/package.json +22 -6
@@ -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,
@@ -128,8 +155,93 @@ var DocxodusPagination = (() => {
128
155
  function samePageBox(a, b) {
129
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;
130
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
+ }
131
242
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
132
- var MIN_BODY_CONTENT_HEIGHT = 72;
243
+ var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
244
+ var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
133
245
  var PaginationEngine = class {
134
246
  /**
135
247
  * Creates a new pagination engine.
@@ -139,24 +251,46 @@ var DocxodusPagination = (() => {
139
251
  * @param options - Pagination options
140
252
  */
141
253
  constructor(staging, container, options = {}) {
254
+ this.createdPageCount = 0;
255
+ this.footnoteSeparator = null;
256
+ this.footnoteContinuationSeparator = null;
257
+ this.footnoteLayoutLikeWord8 = false;
142
258
  this.pendingFootnoteContinuation = null;
143
259
  /** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
144
260
  this.pageNumbering = /* @__PURE__ */ new Map();
145
- this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
146
- 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;
147
267
  if (!this.stagingElement) {
148
268
  throw new Error("Staging element not found");
149
269
  }
150
270
  if (!this.containerElement) {
151
271
  throw new Error("Container element not found");
152
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;
153
282
  this.scale = options.scale ?? 1;
154
283
  this.cssPrefix = options.cssPrefix ?? "page-";
155
284
  this.showPageNumbers = options.showPageNumbers ?? true;
156
285
  this.pageGap = options.pageGap ?? 20;
157
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;
158
291
  this.hfRegistry = /* @__PURE__ */ new Map();
159
292
  this.footnoteRegistry = /* @__PURE__ */ new Map();
293
+ this.commentMarginRegistry = /* @__PURE__ */ new Map();
160
294
  }
161
295
  /**
162
296
  * Runs the pagination process.
@@ -164,58 +298,469 @@ var DocxodusPagination = (() => {
164
298
  * @returns PaginationResult with page information
165
299
  */
166
300
  paginate() {
167
- const pages = [];
168
- let pageNumber = 1;
169
- this.hfRegistry = this.parseHeaderFooterRegistry();
170
- this.footnoteRegistry = this.parseFootnoteRegistry();
171
- const sections = this.stagingElement.querySelectorAll(
172
- "[data-section-index]"
173
- );
174
- this.pageNumbering = this.parsePageNumbering(sections);
175
- const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
176
- const runs = [];
177
- for (const section of sectionsToProcess) {
178
- const dims = parseSectionDimensions(section);
179
- const previous = runs[runs.length - 1];
180
- if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
181
- previous.sections.push(section);
182
- } else {
183
- runs.push({
184
- sections: [section],
185
- sectionIndex: parseInt(section.dataset.sectionIndex || "0", 10),
186
- dims
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
187
578
  });
188
579
  }
189
580
  }
190
- for (const run of runs) {
191
- this.stagingElement.style.visibility = "hidden";
192
- this.stagingElement.style.position = "absolute";
193
- this.stagingElement.style.left = "-9999px";
194
- this.stagingElement.style.display = "block";
195
- const blocks = [];
196
- for (const section of run.sections) {
197
- section.style.width = `${run.dims.contentWidth}pt`;
198
- const columnCount = parseInt(section.dataset.cols || "1", 10);
199
- if (columnCount > 1) {
200
- const gap = parseFloat(section.dataset.colGap || "");
201
- blocks.push(...this.buildColumnBlocks(
202
- section,
203
- run.dims,
204
- run.sectionIndex,
205
- columnCount,
206
- Number.isFinite(gap) ? gap : 36
207
- ));
208
- } else {
209
- blocks.push(...this.measureBlocks(section, run.dims));
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
+ }
210
730
  }
211
731
  }
212
- const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
213
- pages.push(...sectionPages);
214
- pageNumber += sectionPages.length;
215
732
  }
216
- this.stagingElement.style.display = "none";
217
- this.substitutePageNumberFields(pages.length);
218
- return { totalPages: pages.length, pages };
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
+ }
219
764
  }
220
765
  /** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
221
766
  parsePageNumbering(sections) {
@@ -259,9 +804,8 @@ var DocxodusPagination = (() => {
259
804
  if (markers.length === 0) continue;
260
805
  const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
261
806
  const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
262
- const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
263
807
  const numbering = this.pageNumbering.get(sectionIndex) ?? {};
264
- const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
808
+ const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
265
809
  for (const marker of Array.from(markers)) {
266
810
  const kind = marker.dataset.field;
267
811
  if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
@@ -273,17 +817,26 @@ var DocxodusPagination = (() => {
273
817
  /**
274
818
  * Measures all content blocks in a section.
275
819
  */
276
- measureBlocks(section, dims) {
820
+ measureBlocks(section, dims, sectionIndex) {
277
821
  const blocks = [];
278
822
  const children = Array.from(section.children);
279
823
  for (const child of children) {
824
+ this.checkpoint();
280
825
  if (child.dataset.sectionIndex !== void 0) {
281
- const nestedBlocks = this.measureBlocks(child, dims);
826
+ const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
827
+ const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
282
828
  blocks.push(...nestedBlocks);
283
829
  continue;
284
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
+ }
285
838
  const rect = child.getBoundingClientRect();
286
- const style = window.getComputedStyle(child);
839
+ const style = this.view.getComputedStyle(child);
287
840
  const marginTopPx = parseFloat(style.marginTop) || 0;
288
841
  const marginBottomPx = parseFloat(style.marginBottom) || 0;
289
842
  const heightPt = pxToPt(rect.height);
@@ -292,6 +845,7 @@ var DocxodusPagination = (() => {
292
845
  const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
293
846
  blocks.push({
294
847
  element: child,
848
+ sectionIndex,
295
849
  heightPt,
296
850
  marginTopPt,
297
851
  marginBottomPt,
@@ -304,6 +858,95 @@ var DocxodusPagination = (() => {
304
858
  }
305
859
  return blocks;
306
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
+ }
307
950
  /**
308
951
  * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
309
952
  * blocks. Word lays such a section out as N columns inside the same body extent;
@@ -321,7 +964,7 @@ var DocxodusPagination = (() => {
321
964
  const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
322
965
  const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
323
966
  const makeContainer = (slice) => {
324
- const container = document.createElement("div");
967
+ const container = this.document.createElement("div");
325
968
  container.style.columnCount = String(columnCount);
326
969
  container.style.columnGap = `${columnGapPt}pt`;
327
970
  for (const child of slice) {
@@ -331,17 +974,19 @@ var DocxodusPagination = (() => {
331
974
  };
332
975
  let start = 0;
333
976
  while (start < children.length) {
977
+ this.checkpoint();
334
978
  if (isBreak(children[start])) {
335
- blocks.push(this.measureElement(children[start], dims));
979
+ blocks.push(this.measureElement(children[start], dims, sectionIndex));
336
980
  start++;
337
981
  continue;
338
982
  }
339
983
  let end = start + 1;
340
984
  let container = makeContainer(children.slice(start, end));
341
- let measured = this.measureElement(container, dims);
985
+ let measured = this.measureElement(container, dims, sectionIndex);
342
986
  while (end < children.length && !isBreak(children[end])) {
987
+ this.checkpoint();
343
988
  const candidate = makeContainer(children.slice(start, end + 1));
344
- const candidateMeasured = this.measureElement(candidate, dims);
989
+ const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
345
990
  if (candidateMeasured.heightPt > maxFragmentHeight) break;
346
991
  container = candidate;
347
992
  measured = candidateMeasured;
@@ -349,6 +994,7 @@ var DocxodusPagination = (() => {
349
994
  }
350
995
  blocks.push({
351
996
  element: container,
997
+ sectionIndex,
352
998
  heightPt: measured.heightPt,
353
999
  marginTopPt: measured.marginTopPt,
354
1000
  marginBottomPt: measured.marginBottomPt,
@@ -367,8 +1013,8 @@ var DocxodusPagination = (() => {
367
1013
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
368
1014
  * rows because wrapping and collapsed borders change the height of a fragment.
369
1015
  */
370
- measureElement(element, dims) {
371
- const measurementHost = document.createElement("div");
1016
+ measureElement(element, dims, sectionIndex) {
1017
+ const measurementHost = this.document.createElement("div");
372
1018
  measurementHost.style.position = "absolute";
373
1019
  measurementHost.style.visibility = "hidden";
374
1020
  measurementHost.style.left = "-9999px";
@@ -377,9 +1023,10 @@ var DocxodusPagination = (() => {
377
1023
  measurementHost.appendChild(measuredElement);
378
1024
  this.stagingElement.appendChild(measurementHost);
379
1025
  const rect = measuredElement.getBoundingClientRect();
380
- const style = window.getComputedStyle(measuredElement);
1026
+ const style = this.view.getComputedStyle(measuredElement);
381
1027
  const measured = {
382
1028
  element,
1029
+ sectionIndex,
383
1030
  heightPt: pxToPt(rect.height),
384
1031
  marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
385
1032
  marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
@@ -473,6 +1120,7 @@ var DocxodusPagination = (() => {
473
1120
  const knownIds = new Set(existingFootnoteIds);
474
1121
  const newIds = [];
475
1122
  for (const block of blocks) {
1123
+ this.checkpoint();
476
1124
  for (const id of this.extractFootnoteRefs(block.element)) {
477
1125
  if (!knownIds.has(id)) {
478
1126
  knownIds.add(id);
@@ -490,8 +1138,8 @@ var DocxodusPagination = (() => {
490
1138
  smallestEffectiveContentHeight(dims, sectionIndex) {
491
1139
  return Math.min(
492
1140
  this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
493
- this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
494
- this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
1141
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
1142
+ this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
495
1143
  );
496
1144
  }
497
1145
  /**
@@ -529,10 +1177,11 @@ var DocxodusPagination = (() => {
529
1177
  if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
530
1178
  return null;
531
1179
  }
532
- const table = wrapper.firstElementChild;
533
- if (!(table instanceof HTMLTableElement)) {
1180
+ const tableElement = wrapper.firstElementChild;
1181
+ if (!tableElement || tableElement.localName !== "table") {
534
1182
  return null;
535
1183
  }
1184
+ const table = tableElement;
536
1185
  const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
537
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]")) {
538
1187
  return null;
@@ -546,8 +1195,10 @@ var DocxodusPagination = (() => {
546
1195
  const groups = [];
547
1196
  let start = 0;
548
1197
  while (start < rows.length) {
1198
+ this.checkpoint();
549
1199
  let end = start;
550
1200
  while (end < rows.length) {
1201
+ this.checkpoint();
551
1202
  const candidate = this.createSimpleTableFragment(
552
1203
  wrapper,
553
1204
  table,
@@ -555,7 +1206,7 @@ var DocxodusPagination = (() => {
555
1206
  rows.slice(start, end + 1),
556
1207
  start === 0
557
1208
  );
558
- const measured = this.measureElement(candidate, dims);
1209
+ const measured = this.measureElement(candidate, dims, block.sectionIndex);
559
1210
  if (measured.heightPt > maximumFragmentHeight) {
560
1211
  break;
561
1212
  }
@@ -587,7 +1238,7 @@ var DocxodusPagination = (() => {
587
1238
  if (!isLast) {
588
1239
  fragment.style.setProperty("margin-bottom", "0", "important");
589
1240
  }
590
- const measured = this.measureElement(fragment, dims);
1241
+ const measured = this.measureElement(fragment, dims, block.sectionIndex);
591
1242
  if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
592
1243
  return null;
593
1244
  }
@@ -602,35 +1253,198 @@ var DocxodusPagination = (() => {
602
1253
  return fragments;
603
1254
  }
604
1255
  /**
605
- * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
606
- * after whitespace or at a run boundary so the paginator never deliberately
607
- * 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.
608
1262
  */
609
- paragraphFragmentEndpoints(paragraph) {
610
- const endpoints = [];
611
- 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;
612
1270
  let textNode;
613
1271
  while (textNode = walker.nextNode()) {
614
- const text = textNode.data;
615
- if (text.length === 0) continue;
616
- 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;
617
1317
  let match;
618
- while ((match = whitespace.exec(text)) !== null) {
619
- 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 });
620
1323
  }
621
- if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
622
- 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);
623
1362
  }
624
1363
  }
625
1364
  return endpoints;
626
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
+ }
627
1438
  /**
628
1439
  * Whether a range contains visible text after ignoring bidi/zero-width marks.
629
1440
  * Paragraph fragmentation deliberately excludes non-textual descendants, so
630
1441
  * this is enough to reject empty head or tail fragments.
631
1442
  */
632
1443
  hasVisibleFragmentText(fragment) {
633
- 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;
634
1448
  }
635
1449
  /**
636
1450
  * Phase one only handles text-like paragraph descendants. Objects, explicit
@@ -639,13 +1453,25 @@ var DocxodusPagination = (() => {
639
1453
  * fallback instead of risking broken content or duplicate anchors.
640
1454
  */
641
1455
  canFragmentParagraph(block) {
642
- if (!this.fragmentParagraphs || block.element.tagName !== "P") {
1456
+ if (!this.fragmentParagraphs) {
643
1457
  return false;
644
1458
  }
645
1459
  const paragraph = block.element;
646
1460
  if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
647
1461
  return false;
648
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
+ }
649
1475
  const unsupportedDescendants = [
650
1476
  "br",
651
1477
  "img",
@@ -676,21 +1502,27 @@ var DocxodusPagination = (() => {
676
1502
  "fieldset",
677
1503
  "details",
678
1504
  "[data-footnote-id]",
679
- "[data-list-marker]",
680
- "[data-anchor]",
681
- "[id]",
682
1505
  "[contenteditable]"
683
1506
  ].join(", ");
684
1507
  if (paragraph.querySelector(unsupportedDescendants)) {
685
1508
  return false;
686
1509
  }
687
- const paragraphStyle = window.getComputedStyle(paragraph);
688
- 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") {
689
1521
  return false;
690
1522
  }
691
1523
  for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
692
- const style = window.getComputedStyle(descendant);
693
- 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") {
694
1526
  return false;
695
1527
  }
696
1528
  }
@@ -717,58 +1549,122 @@ var DocxodusPagination = (() => {
717
1549
  return fragment;
718
1550
  }
719
1551
  /**
720
- * Splits a simple paragraph at the largest DOM Range endpoint that fits the
721
- * currently available body space. The caller then processes the tail normally,
722
- * 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`.
723
1556
  */
724
- tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
725
- if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
726
- 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");
727
1565
  }
728
- const paragraph = block.element;
729
- const endpoints = this.paragraphFragmentEndpoints(paragraph);
730
- if (endpoints.length < 2) {
731
- 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");
732
1571
  }
733
- let low = 0;
734
- let high = endpoints.length - 2;
735
- let best = null;
736
- while (low <= high) {
737
- const middle = Math.floor((low + high) / 2);
738
- const endpoint = endpoints[middle];
739
- const headRange = document.createRange();
740
- headRange.setStart(paragraph, 0);
741
- headRange.setEnd(endpoint.node, endpoint.offset);
742
- const headContents = headRange.cloneContents();
743
- if (!this.hasVisibleFragmentText(headContents)) {
744
- low = middle + 1;
745
- 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
+ }
746
1590
  }
747
- const head = this.createParagraphFragment(paragraph, headRange, true, false);
748
- const measured = this.measureElement(head, dims);
749
- if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
750
- best = { endpoint, element: head, measured };
751
- low = middle + 1;
752
- } else {
753
- 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
+ }
754
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
+ };
755
1631
  }
756
1632
  if (!best) {
757
- return null;
1633
+ return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
758
1634
  }
759
- const tailRange = document.createRange();
1635
+ const tailRange = this.document.createRange();
760
1636
  tailRange.setStart(best.endpoint.node, best.endpoint.offset);
761
1637
  tailRange.setEnd(paragraph, paragraph.childNodes.length);
762
1638
  const tailContents = tailRange.cloneContents();
763
- 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) {
764
1655
  return null;
765
1656
  }
766
- const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
767
- 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);
768
1664
  return [
769
1665
  {
770
- ...best.measured,
771
- element: best.element,
1666
+ ...headMeasured,
1667
+ element: split.head,
772
1668
  keepWithNext: false,
773
1669
  keepLines: false,
774
1670
  pageBreakBefore: false,
@@ -776,7 +1672,7 @@ var DocxodusPagination = (() => {
776
1672
  },
777
1673
  {
778
1674
  ...tailMeasured,
779
- element: tail,
1675
+ element: split.tail,
780
1676
  keepWithNext: false,
781
1677
  keepLines: false,
782
1678
  pageBreakBefore: false,
@@ -847,7 +1743,7 @@ var DocxodusPagination = (() => {
847
1743
  const registry = /* @__PURE__ */ new Map();
848
1744
  const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
849
1745
  if (!registryEl) return registry;
850
- 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"));
851
1747
  for (const entry of entries) {
852
1748
  const footnoteId = entry.dataset.footnoteId;
853
1749
  if (footnoteId) {
@@ -856,6 +1752,68 @@ var DocxodusPagination = (() => {
856
1752
  }
857
1753
  return registry;
858
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
+ }
859
1817
  /**
860
1818
  * Extracts footnote reference IDs from an element.
861
1819
  */
@@ -870,6 +1828,92 @@ var DocxodusPagination = (() => {
870
1828
  }
871
1829
  return ids;
872
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
+ }
873
1917
  /**
874
1918
  * Measures the height of footnotes for given IDs (in points).
875
1919
  * Creates a temporary container to measure the footnotes.
@@ -877,140 +1921,246 @@ var DocxodusPagination = (() => {
877
1921
  * @param contentWidth - Width for measurement
878
1922
  * @param continuation - Optional continuation content to include first
879
1923
  */
880
- measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
1924
+ measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
881
1925
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
882
- if (footnoteIds.length === 0 && !hasContinuation || this.footnoteRegistry.size === 0) {
1926
+ if (footnoteIds.length === 0 && !hasContinuation) {
1927
+ return 0;
1928
+ }
1929
+ if (this.footnoteRegistry.size === 0 && !hasContinuation) {
883
1930
  return 0;
884
1931
  }
885
- const measureContainer = document.createElement("div");
1932
+ const measureContainer = this.document.createElement("div");
886
1933
  measureContainer.style.position = "absolute";
887
1934
  measureContainer.style.visibility = "hidden";
888
1935
  measureContainer.style.width = `${contentWidth}pt`;
889
1936
  measureContainer.style.left = "-9999px";
890
1937
  measureContainer.className = this.cssPrefix + "footnotes";
891
- const hr = document.createElement("hr");
892
- measureContainer.appendChild(hr);
1938
+ this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
893
1939
  if (hasContinuation) {
894
- const contWrapper = document.createElement("div");
895
- contWrapper.className = "footnote-continuation";
896
- for (const el of continuation.remainingElements) {
897
- contWrapper.appendChild(el.cloneNode(true));
898
- }
899
- measureContainer.appendChild(contWrapper);
1940
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
900
1941
  }
1942
+ const partialById = new Map(
1943
+ partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
1944
+ );
901
1945
  for (const id of footnoteIds) {
1946
+ this.checkpoint();
902
1947
  const footnote = this.footnoteRegistry.get(id);
903
1948
  if (footnote) {
904
- measureContainer.appendChild(footnote.cloneNode(true));
1949
+ const partial = partialById.get(id);
1950
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
905
1951
  }
906
1952
  }
907
- this.stagingElement.appendChild(measureContainer);
908
- const rect = measureContainer.getBoundingClientRect();
909
- const heightPt = pxToPt(rect.height);
910
- this.stagingElement.removeChild(measureContainer);
911
- return heightPt;
1953
+ this.noteMeasurementHost().appendChild(measureContainer);
1954
+ try {
1955
+ return pxToPt(measureContainer.getBoundingClientRect().height);
1956
+ } finally {
1957
+ measureContainer.remove();
1958
+ }
912
1959
  }
913
1960
  /**
914
- * 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.
915
1965
  */
916
- measureContinuationHeight(continuation, contentWidth) {
917
- if (!continuation || continuation.remainingElements.length === 0) {
918
- return 0;
919
- }
920
- const measureContainer = document.createElement("div");
1966
+ splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
1967
+ const fitting = [];
1968
+ let remaining = [];
1969
+ const measureContainer = this.document.createElement("div");
921
1970
  measureContainer.style.position = "absolute";
922
1971
  measureContainer.style.visibility = "hidden";
923
1972
  measureContainer.style.width = `${contentWidth}pt`;
924
1973
  measureContainer.style.left = "-9999px";
925
1974
  measureContainer.className = this.cssPrefix + "footnotes";
926
- const hr = document.createElement("hr");
927
- measureContainer.appendChild(hr);
928
- for (const el of continuation.remainingElements) {
929
- measureContainer.appendChild(el.cloneNode(true));
930
- }
931
- this.stagingElement.appendChild(measureContainer);
932
- const rect = measureContainer.getBoundingClientRect();
933
- const heightPt = pxToPt(rect.height);
934
- this.stagingElement.removeChild(measureContainer);
935
- return heightPt;
936
- }
937
- /**
938
- * Splits a footnote element into parts that fit within the available height.
939
- * Returns the elements that fit and the elements that need to continue.
940
- */
941
- splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
942
- const footnoteContent = footnoteElement.querySelector(".footnote-content");
943
- if (!footnoteContent) {
944
- return {
945
- fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
946
- overflow: []
947
- };
948
- }
949
- const children = Array.from(footnoteContent.children);
950
- if (children.length <= 1) {
951
- return {
952
- fits: children.map((el) => el.cloneNode(true)),
953
- overflow: []
954
- };
955
- }
956
- const fits = [];
957
- const overflow = [];
958
- let currentHeight = 0;
959
- const hrMeasure = document.createElement("div");
960
- hrMeasure.style.position = "absolute";
961
- hrMeasure.style.visibility = "hidden";
962
- hrMeasure.style.width = `${contentWidth}pt`;
963
- hrMeasure.style.left = "-9999px";
964
- hrMeasure.className = this.cssPrefix + "footnotes";
965
- const hr = document.createElement("hr");
966
- hrMeasure.appendChild(hr);
967
- this.stagingElement.appendChild(hrMeasure);
968
- const hrHeight = pxToPt(hrMeasure.getBoundingClientRect().height);
969
- this.stagingElement.removeChild(hrMeasure);
970
- currentHeight = hrHeight;
971
- const footnoteNumber = footnoteElement.querySelector(".footnote-number");
972
- for (let i = 0; i < children.length; i++) {
973
- const child = children[i];
974
- const measureContainer = document.createElement("div");
975
- measureContainer.style.position = "absolute";
976
- measureContainer.style.visibility = "hidden";
977
- measureContainer.style.width = `${contentWidth}pt`;
978
- measureContainer.style.left = "-9999px";
979
- measureContainer.className = this.cssPrefix + "footnotes";
980
- measureContainer.appendChild(child.cloneNode(true));
981
- this.stagingElement.appendChild(measureContainer);
982
- const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
983
- this.stagingElement.removeChild(measureContainer);
984
- if (currentHeight + childHeight <= availableHeightPt) {
985
- fits.push(child.cloneNode(true));
986
- currentHeight += childHeight;
987
- } else {
988
- for (let j = i; j < children.length; j++) {
989
- 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);
990
2040
  }
991
2041
  break;
992
2042
  }
2043
+ } finally {
2044
+ measureContainer.remove();
993
2045
  }
994
- 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
+ };
995
2058
  }
996
2059
  /**
997
- * 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.
998
2062
  */
999
- measureSingleFootnoteHeight(footnoteId, contentWidth) {
1000
- const footnote = this.footnoteRegistry.get(footnoteId);
1001
- if (!footnote) return 0;
1002
- 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");
1003
2075
  measureContainer.style.position = "absolute";
1004
2076
  measureContainer.style.visibility = "hidden";
1005
2077
  measureContainer.style.width = `${contentWidth}pt`;
1006
2078
  measureContainer.style.left = "-9999px";
1007
2079
  measureContainer.className = this.cssPrefix + "footnotes";
1008
- measureContainer.appendChild(footnote.cloneNode(true));
1009
- this.stagingElement.appendChild(measureContainer);
1010
- const rect = measureContainer.getBoundingClientRect();
1011
- const heightPt = pxToPt(rect.height);
1012
- this.stagingElement.removeChild(measureContainer);
1013
- 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
+ }
1014
2164
  }
1015
2165
  /**
1016
2166
  * Adds footnotes to a page container, including continuation content.
@@ -1023,12 +2173,11 @@ var DocxodusPagination = (() => {
1023
2173
  if (this.footnoteRegistry.size === 0 && !hasContinuation) {
1024
2174
  return;
1025
2175
  }
1026
- const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
1027
2176
  const maxFootnoteHeight = Math.min(
1028
2177
  footnoteHeight,
1029
2178
  bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
1030
2179
  );
1031
- const footnotesDiv = document.createElement("div");
2180
+ const footnotesDiv = this.document.createElement("div");
1032
2181
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
1033
2182
  footnotesDiv.style.position = "absolute";
1034
2183
  footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
@@ -1037,35 +2186,18 @@ var DocxodusPagination = (() => {
1037
2186
  footnotesDiv.style.boxSizing = "border-box";
1038
2187
  footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
1039
2188
  footnotesDiv.style.overflow = "hidden";
1040
- const hr = document.createElement("hr");
1041
- footnotesDiv.appendChild(hr);
2189
+ this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
1042
2190
  if (hasContinuation) {
1043
- const contWrapper = document.createElement("div");
1044
- contWrapper.className = "footnote-continuation";
1045
- for (const el of continuation.remainingElements) {
1046
- contWrapper.appendChild(el.cloneNode(true));
1047
- }
1048
- footnotesDiv.appendChild(contWrapper);
2191
+ footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
1049
2192
  }
1050
2193
  for (const id of footnoteIds) {
1051
2194
  const partial = partialFootnotes?.find((p) => p.footnoteId === id);
1052
2195
  if (partial) {
1053
2196
  const footnote = this.footnoteRegistry.get(id);
1054
2197
  if (footnote) {
1055
- const partialDiv = document.createElement("div");
1056
- partialDiv.className = "footnote-item";
1057
- partialDiv.dataset.footnoteId = id;
1058
- const numberSpan = footnote.querySelector(".footnote-number");
1059
- if (numberSpan) {
1060
- partialDiv.appendChild(numberSpan.cloneNode(true));
1061
- }
1062
- const contentSpan = document.createElement("span");
1063
- contentSpan.className = "footnote-content";
1064
- for (const el of partial.fittingElements) {
1065
- contentSpan.appendChild(el.cloneNode(true));
1066
- }
1067
- partialDiv.appendChild(contentSpan);
1068
- footnotesDiv.appendChild(partialDiv);
2198
+ footnotesDiv.appendChild(
2199
+ this.createPartialFootnoteItem(footnote, partial.fittingElements)
2200
+ );
1069
2201
  }
1070
2202
  } else {
1071
2203
  const footnote = this.footnoteRegistry.get(id);
@@ -1076,30 +2208,26 @@ var DocxodusPagination = (() => {
1076
2208
  }
1077
2209
  pageBox.appendChild(footnotesDiv);
1078
2210
  }
1079
- /**
1080
- * Selects the appropriate header for a page based on section, page position, and page number.
1081
- */
1082
- selectHeader(sectionIndex, pageInSection, globalPageNumber) {
2211
+ /** Select the section's first/odd/even header from its one-based page position. */
2212
+ selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
1083
2213
  const sectionHf = this.hfRegistry.get(sectionIndex);
1084
2214
  if (!sectionHf) return void 0;
1085
2215
  if (pageInSection === 1 && sectionHf.headerFirst) {
1086
2216
  return sectionHf.headerFirst;
1087
2217
  }
1088
- if (globalPageNumber % 2 === 0 && sectionHf.headerEven) {
2218
+ if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
1089
2219
  return sectionHf.headerEven;
1090
2220
  }
1091
2221
  return sectionHf.headerDefault;
1092
2222
  }
1093
- /**
1094
- * Selects the appropriate footer for a page based on section, page position, and page number.
1095
- */
1096
- selectFooter(sectionIndex, pageInSection, globalPageNumber) {
2223
+ /** Select the section's first/odd/even footer from its one-based page position. */
2224
+ selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
1097
2225
  const sectionHf = this.hfRegistry.get(sectionIndex);
1098
2226
  if (!sectionHf) return void 0;
1099
2227
  if (pageInSection === 1 && sectionHf.footerFirst) {
1100
2228
  return sectionHf.footerFirst;
1101
2229
  }
1102
- if (globalPageNumber % 2 === 0 && sectionHf.footerEven) {
2230
+ if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
1103
2231
  return sectionHf.footerEven;
1104
2232
  }
1105
2233
  return sectionHf.footerDefault;
@@ -1114,7 +2242,7 @@ var DocxodusPagination = (() => {
1114
2242
  * Deterministic: it depends only on the section's page setup and the registry's pre-measured
1115
2243
  * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
1116
2244
  */
1117
- getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
2245
+ getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
1118
2246
  const sectionHf = this.hfRegistry.get(sectionIndex);
1119
2247
  return resolvePageBands(
1120
2248
  dims,
@@ -1123,14 +2251,14 @@ var DocxodusPagination = (() => {
1123
2251
  sectionHf?.headerEvenHeight,
1124
2252
  sectionHf?.headerDefaultHeight,
1125
2253
  pageInSection,
1126
- globalPageNumber
2254
+ displayedPageNumber
1127
2255
  ),
1128
2256
  this.selectStoryHeight(
1129
2257
  sectionHf?.footerFirstHeight,
1130
2258
  sectionHf?.footerEvenHeight,
1131
2259
  sectionHf?.footerDefaultHeight,
1132
2260
  pageInSection,
1133
- globalPageNumber
2261
+ displayedPageNumber
1134
2262
  )
1135
2263
  );
1136
2264
  }
@@ -1138,9 +2266,9 @@ var DocxodusPagination = (() => {
1138
2266
  * The measured height of the running story this page position selects, mirroring
1139
2267
  * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1140
2268
  */
1141
- selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
2269
+ selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
1142
2270
  if (pageInSection === 1 && first != null) return first;
1143
- if (globalPageNumber % 2 === 0 && even != null) return even;
2271
+ if (displayedPageNumber % 2 === 0 && even != null) return even;
1144
2272
  return fallback ?? 0;
1145
2273
  }
1146
2274
  /**
@@ -1152,7 +2280,7 @@ var DocxodusPagination = (() => {
1152
2280
  * silently starts overlapping body text.
1153
2281
  */
1154
2282
  measureHeaderFooterHeight(source, contentWidth) {
1155
- const measureContainer = document.createElement("div");
2283
+ const measureContainer = this.document.createElement("div");
1156
2284
  measureContainer.style.position = "absolute";
1157
2285
  measureContainer.style.visibility = "hidden";
1158
2286
  measureContainer.style.width = `${contentWidth}pt`;
@@ -1171,79 +2299,275 @@ var DocxodusPagination = (() => {
1171
2299
  * Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
1172
2300
  * Supports footnote continuation - long footnotes can split across pages.
1173
2301
  */
1174
- flowToPages(blocks, dims, startPageNumber, sectionIndex) {
2302
+ flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
1175
2303
  const pages = [];
1176
2304
  let currentContent = [];
1177
2305
  let pageNumber = startPageNumber;
1178
- 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
+ };
1179
2329
  let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1180
- dims,
1181
- sectionIndex,
1182
- pageInSection,
1183
- pageNumber
2330
+ dimensionsFor(),
2331
+ pageSectionIndex,
2332
+ pageInSection(),
2333
+ displayedPageNumber()
1184
2334
  );
1185
2335
  let remainingHeight = effectiveContentHeight;
1186
2336
  let prevMarginBottomPt = 0;
1187
2337
  let currentFootnoteIds = [];
2338
+ let currentPageHasFootnoteReference = false;
1188
2339
  let currentFootnoteHeight = 0;
1189
2340
  let currentContinuation = this.pendingFootnoteContinuation;
1190
2341
  let nextPageContinuation = null;
2342
+ let currentContinuationPartitioned = false;
2343
+ let currentPageAdmitted = false;
1191
2344
  let deferredFootnoteIds = [];
1192
2345
  let currentPartialFootnotes = [];
1193
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1194
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1195
- }
1196
- 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) => {
1197
2388
  const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
1198
- 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
+ }
1199
2461
  const page = this.createPage(
1200
- dims,
2462
+ ownedDimensions,
1201
2463
  pageNumber,
1202
- sectionIndex,
2464
+ pageSectionIndex,
2465
+ ownedDisplayedPageNumber,
1203
2466
  currentContent,
1204
- pageInSection,
2467
+ ownedPageInSection,
1205
2468
  currentFootnoteIds,
1206
2469
  currentFootnoteHeight,
1207
- currentContinuation,
1208
- currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
2470
+ pageContinuation,
2471
+ currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
2472
+ false,
2473
+ currentPageAdmitted
1209
2474
  );
1210
2475
  pages.push(page);
2476
+ precedingDisplayedPageNumber = ownedDisplayedPageNumber;
1211
2477
  pageNumber++;
1212
- pageInSection++;
1213
2478
  currentContent = [];
1214
- 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
+ );
1215
2489
  effectiveContentHeight = newBands.bodyHeight;
1216
2490
  remainingHeight = effectiveContentHeight;
1217
2491
  prevMarginBottomPt = 0;
1218
2492
  currentFootnoteIds = [];
2493
+ currentPageHasFootnoteReference = false;
1219
2494
  currentPartialFootnotes = [];
1220
2495
  currentContinuation = nextPageContinuation;
1221
2496
  nextPageContinuation = null;
2497
+ currentContinuationPartitioned = false;
2498
+ currentPageAdmitted = false;
1222
2499
  if (deferredFootnoteIds.length > 0) {
1223
2500
  currentFootnoteIds = [...deferredFootnoteIds];
1224
2501
  deferredFootnoteIds = [];
1225
2502
  }
1226
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1227
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1228
- } else {
1229
- currentFootnoteHeight = 0;
1230
- }
1231
- if (currentFootnoteIds.length > 0) {
1232
- currentFootnoteHeight += this.measureFootnotesHeight(
1233
- currentFootnoteIds,
1234
- dims.contentWidth,
1235
- null
1236
- );
1237
- }
2503
+ const nextDimensions = dimensionsFor();
2504
+ currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
1238
2505
  };
2506
+ const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
1239
2507
  for (let i = 0; i < blocks.length; i++) {
2508
+ this.checkpoint();
1240
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;
1241
2565
  if (block.isPageBreak) {
1242
- finishPage();
2566
+ finishPage(block.sectionIndex);
1243
2567
  continue;
1244
2568
  }
1245
2569
  if (block.pageBreakBefore && currentContent.length > 0) {
1246
- finishPage();
2570
+ finishPage(block.sectionIndex);
1247
2571
  }
1248
2572
  const previousBlock = blocks[i - 1];
1249
2573
  const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
@@ -1259,7 +2583,7 @@ var DocxodusPagination = (() => {
1259
2583
  if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1260
2584
  const totalChainFootnoteHeight = this.measureFootnotesHeight(
1261
2585
  [...currentFootnoteIds, ...newChainFootnoteIds],
1262
- dims.contentWidth,
2586
+ blockDimensions.contentWidth,
1263
2587
  currentContinuation
1264
2588
  );
1265
2589
  additionalChainFootnoteHeight = Math.max(
@@ -1271,61 +2595,68 @@ var DocxodusPagination = (() => {
1271
2595
  keepChain,
1272
2596
  prevMarginBottomPt,
1273
2597
  currentContent.length === 0,
1274
- pageInSection
2598
+ pageInSection(block.sectionIndex)
1275
2599
  ) + additionalChainFootnoteHeight;
1276
2600
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1277
2601
  if (currentChainHeight > currentAvailableHeight) {
1278
2602
  const nextPageBands = this.getPageBands(
1279
- dims,
1280
- sectionIndex,
1281
- pageInSection + 1,
1282
- 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
+ )
1283
2611
  );
1284
2612
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
1285
2613
  keepChain,
1286
2614
  0,
1287
2615
  true,
1288
- pageInSection + 1
2616
+ pageInSection(block.sectionIndex, pageNumber + 1)
1289
2617
  );
1290
2618
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
1291
2619
  newChainFootnoteIds,
1292
- dims.contentWidth,
2620
+ blockDimensions.contentWidth,
1293
2621
  nextPageContinuation
1294
2622
  );
1295
2623
  if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1296
- finishPage();
2624
+ finishPage(block.sectionIndex);
1297
2625
  }
1298
2626
  }
1299
2627
  }
1300
2628
  }
1301
- const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
1302
2629
  const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
2630
+ let combinedFootnoteHeight = currentFootnoteHeight;
1303
2631
  let additionalFootnoteHeight = 0;
1304
2632
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1305
2633
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
1306
- const totalFootnoteHeight = this.measureFootnotesHeight(
2634
+ combinedFootnoteHeight = this.measureFootnotesHeight(
1307
2635
  combinedFootnoteIds,
1308
- dims.contentWidth,
1309
- currentContinuation
2636
+ blockDimensions.contentWidth,
2637
+ currentContinuation,
2638
+ currentPartialFootnotes
2639
+ );
2640
+ additionalFootnoteHeight = Math.max(
2641
+ 0,
2642
+ combinedFootnoteHeight - currentFootnoteHeight
1310
2643
  );
1311
- additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
1312
2644
  }
1313
2645
  const isFirstOnPage = currentContent.length === 0;
1314
2646
  const effectiveMarginTop = this.effectiveBlockMarginTop(
1315
2647
  block,
1316
2648
  prevMarginBottomPt,
1317
2649
  isFirstOnPage,
1318
- pageInSection
2650
+ pageInSection(block.sectionIndex)
1319
2651
  );
1320
2652
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
1321
2653
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
1322
2654
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
1323
2655
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
1324
- const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
1325
2656
  if (blockSpace > effectiveRemainingHeight) {
1326
2657
  const paragraphFragments = this.tryFragmentParagraph(
1327
2658
  block,
1328
- dims,
2659
+ blockDimensions,
1329
2660
  effectiveRemainingHeight,
1330
2661
  effectiveMarginTop
1331
2662
  );
@@ -1335,40 +2666,74 @@ var DocxodusPagination = (() => {
1335
2666
  continue;
1336
2667
  }
1337
2668
  }
1338
- if (blockSpace <= effectiveRemainingHeight) {
1339
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
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
+ ));
1340
2676
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1341
2677
  prevMarginBottomPt = block.marginBottomPt;
1342
2678
  if (newFootnoteIds.length > 0) {
1343
2679
  currentFootnoteIds.push(...newFootnoteIds);
1344
- currentFootnoteHeight += additionalFootnoteHeight;
2680
+ currentFootnoteHeight = combinedFootnoteHeight;
1345
2681
  }
1346
- } else if (block.heightPt + this.effectiveBlockMarginTop(block, 0, true, pageInSection + 1) <= 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) {
1347
2689
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
1348
- const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
1349
- const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
1350
2690
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
1351
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
2691
+ markSectionPlaced(block.sectionIndex);
2692
+ currentContent.push(this.cloneBlockForPage(
2693
+ block,
2694
+ isFirstOnPage,
2695
+ pageInSection(block.sectionIndex)
2696
+ ));
1352
2697
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1353
2698
  prevMarginBottomPt = block.marginBottomPt;
1354
2699
  const availableForFootnotes = Math.min(
1355
2700
  maxFootnoteArea,
1356
2701
  effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
1357
2702
  );
1358
- 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];
1359
2708
  const footnote = this.footnoteRegistry.get(footnoteId);
1360
2709
  if (!footnote) continue;
1361
- 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
+ );
1362
2721
  const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
1363
- if (footnoteHeight <= spaceLeftForFootnotes) {
2722
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
1364
2723
  currentFootnoteIds.push(footnoteId);
1365
- currentFootnoteHeight += footnoteHeight;
2724
+ currentFootnoteHeight = candidateHeight;
1366
2725
  } else {
1367
2726
  if (spaceLeftForFootnotes > 20) {
1368
2727
  const { fits, overflow } = this.splitFootnoteToFit(
1369
2728
  footnote,
1370
- spaceLeftForFootnotes,
1371
- dims.contentWidth
2729
+ availableForFootnotes,
2730
+ blockDimensions.contentWidth,
2731
+ deferralCannotHelp,
2732
+ {
2733
+ footnoteIds: currentFootnoteIds,
2734
+ continuation: currentContinuation,
2735
+ partialFootnotes: currentPartialFootnotes
2736
+ }
1372
2737
  );
1373
2738
  if (fits.length > 0) {
1374
2739
  currentFootnoteIds.push(footnoteId);
@@ -1379,78 +2744,70 @@ var DocxodusPagination = (() => {
1379
2744
  if (overflow.length > 0) {
1380
2745
  nextPageContinuation = {
1381
2746
  footnoteId,
2747
+ sourceAnchorId: footnote.dataset.sourceAnchorId,
1382
2748
  remainingElements: overflow
1383
2749
  };
1384
2750
  }
1385
- 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
+ }
1386
2761
  } else {
1387
- deferredFootnoteIds.push(footnoteId);
2762
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2763
+ break;
1388
2764
  }
1389
2765
  } else {
1390
- deferredFootnoteIds.push(footnoteId);
2766
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2767
+ break;
1391
2768
  }
1392
2769
  }
1393
2770
  }
1394
- } else if (canExpandFootnotes && newFootnoteIds.length > 0) {
1395
- const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
1396
- const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
1397
- if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
1398
- currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
1399
- remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
1400
- prevMarginBottomPt = block.marginBottomPt;
1401
- currentFootnoteIds.push(...newFootnoteIds);
1402
- currentFootnoteHeight = expandedFootnoteSpace;
1403
- } else {
1404
- finishPage();
1405
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1406
- const newPageMarginTop = this.effectiveBlockMarginTop(
1407
- block,
1408
- 0,
1409
- true,
1410
- pageInSection
1411
- );
1412
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
1413
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1414
- remainingHeight = effectiveContentHeight - newPageSpace;
1415
- prevMarginBottomPt = block.marginBottomPt;
1416
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1417
- currentFootnoteHeight = newPageFootnoteHeight;
1418
- }
2771
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
1419
2772
  } else {
1420
- finishPage();
1421
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1422
- const newPageMarginTop = this.effectiveBlockMarginTop(
1423
- block,
1424
- 0,
1425
- true,
1426
- pageInSection
1427
- );
1428
- const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
1429
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1430
- remainingHeight = effectiveContentHeight - newPageSpace;
1431
- prevMarginBottomPt = block.marginBottomPt;
1432
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1433
- currentFootnoteHeight = newPageFootnoteHeight;
2773
+ finishPage(block.sectionIndex, !pageHasPayload());
2774
+ i--;
2775
+ continue;
1434
2776
  }
1435
2777
  } else {
1436
- const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
2778
+ const tableFragments = this.trySplitSimpleOversizedTable(
2779
+ block,
2780
+ dimensionsFor(block.sectionIndex),
2781
+ block.sectionIndex
2782
+ );
1437
2783
  if (tableFragments) {
1438
2784
  if (currentContent.length > 0 || currentContinuation) {
1439
- finishPage();
2785
+ finishPage(block.sectionIndex);
1440
2786
  }
1441
2787
  blocks.splice(i, 1, ...tableFragments);
1442
2788
  i--;
1443
2789
  continue;
1444
2790
  }
1445
- if (currentContent.length > 0) {
1446
- finishPage();
2791
+ if (pageHasPayload()) {
2792
+ finishPage(block.sectionIndex);
2793
+ i--;
2794
+ continue;
1447
2795
  }
1448
- currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1449
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1450
- 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);
1451
2805
  }
1452
2806
  }
1453
2807
  finishPage();
2808
+ while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
2809
+ finishPage();
2810
+ }
1454
2811
  this.pendingFootnoteContinuation = nextPageContinuation;
1455
2812
  return pages;
1456
2813
  }
@@ -1475,6 +2832,19 @@ var DocxodusPagination = (() => {
1475
2832
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1476
2833
  }
1477
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
+ }
1478
2848
  /**
1479
2849
  * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1480
2850
  *
@@ -1500,7 +2870,7 @@ var DocxodusPagination = (() => {
1500
2870
  const context = {
1501
2871
  staticLeft: toPageX(staticRect.left),
1502
2872
  staticTop: toPageY(staticRect.top),
1503
- 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,
1504
2874
  paragraphLeft: toPageX(paragraphRect.left),
1505
2875
  paragraphTop: toPageY(paragraphRect.top),
1506
2876
  paragraphWidth: paragraphRect.width / pixelsPerPoint,
@@ -1630,36 +3000,48 @@ var DocxodusPagination = (() => {
1630
3000
  else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1631
3001
  return reference.start + (reference.size - objectSize) * fraction;
1632
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
+ }
1633
3009
  /**
1634
3010
  * Creates a page container element.
1635
3011
  */
1636
- createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
1637
- 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");
1638
3015
  pageBox.className = `${this.cssPrefix}box`;
1639
3016
  pageBox.style.width = `${dims.pageWidth}pt`;
1640
3017
  pageBox.style.height = `${dims.pageHeight}pt`;
1641
3018
  pageBox.style.overflow = "hidden";
1642
3019
  pageBox.style.position = "relative";
1643
3020
  if (this.scale !== 1) {
1644
- pageBox.style.zoom = String(this.scale);
1645
- pageBox.style.transform = `scale(${this.scale})`;
1646
- pageBox.style.transformOrigin = "top left";
1647
- const heightReductionPt = dims.pageHeight * (1 - this.scale);
1648
- const widthReductionPt = dims.pageWidth * (1 - this.scale);
1649
- const heightReductionPx = ptToPx(heightReductionPt);
1650
- const widthReductionPx = ptToPx(widthReductionPt);
1651
- pageBox.style.marginRight = `-${widthReductionPx}px`;
1652
- 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
+ }
1653
3033
  }
1654
3034
  pageBox.style.willChange = "transform";
1655
3035
  pageBox.style.contain = "layout paint";
1656
3036
  pageBox.dataset.pageNumber = String(pageNumber);
1657
3037
  pageBox.dataset.sectionIndex = String(sectionIndex);
3038
+ pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
3039
+ if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
1658
3040
  pageBox.dataset.pageInSection = String(pageInSection);
1659
- const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1660
- 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);
1661
3043
  if (headerSource) {
1662
- const headerDiv = document.createElement("div");
3044
+ const headerDiv = this.document.createElement("div");
1663
3045
  headerDiv.className = `${this.cssPrefix}header`;
1664
3046
  headerDiv.style.position = "absolute";
1665
3047
  headerDiv.style.top = `${bands.headerTop}pt`;
@@ -1681,7 +3063,7 @@ var DocxodusPagination = (() => {
1681
3063
  }
1682
3064
  const contentAreaTop = bands.bodyTop;
1683
3065
  const contentAreaHeight = bands.bodyHeight;
1684
- const contentArea = document.createElement("div");
3066
+ const contentArea = this.document.createElement("div");
1685
3067
  contentArea.className = `${this.cssPrefix}content`;
1686
3068
  contentArea.style.position = "absolute";
1687
3069
  contentArea.style.top = `${contentAreaTop}pt`;
@@ -1694,12 +3076,46 @@ var DocxodusPagination = (() => {
1694
3076
  }
1695
3077
  pageBox.appendChild(contentArea);
1696
3078
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1697
- if (footnoteIds.length > 0 || hasContinuation) {
3079
+ if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
1698
3080
  this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1699
3081
  }
1700
- 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);
1701
3117
  if (footerSource) {
1702
- const footerDiv = document.createElement("div");
3118
+ const footerDiv = this.document.createElement("div");
1703
3119
  footerDiv.className = `${this.cssPrefix}footer`;
1704
3120
  footerDiv.style.position = "absolute";
1705
3121
  footerDiv.style.top = "auto";
@@ -1719,8 +3135,8 @@ var DocxodusPagination = (() => {
1719
3135
  }
1720
3136
  pageBox.appendChild(footerDiv);
1721
3137
  }
1722
- if (this.showPageNumbers) {
1723
- const pageNum = document.createElement("div");
3138
+ if (this.showPageNumbers && !isSectionFiller) {
3139
+ const pageNum = this.document.createElement("div");
1724
3140
  pageNum.className = `${this.cssPrefix}number`;
1725
3141
  pageNum.textContent = String(pageNumber);
1726
3142
  pageBox.appendChild(pageNum);
@@ -1743,7 +3159,8 @@ var DocxodusPagination = (() => {
1743
3159
  }
1744
3160
  };
1745
3161
  function paginateHtml(html, container, options = {}) {
1746
- 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;
1747
3164
  if (!containerEl) {
1748
3165
  throw new Error("Container element not found");
1749
3166
  }