docxodus 9.8.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +178 -0
  2. package/dist/canonical.d.ts +7 -0
  3. package/dist/canonical.d.ts.map +1 -0
  4. package/dist/canonical.js +56 -0
  5. package/dist/canonical.js.map +1 -0
  6. package/dist/docxodus.worker.d.ts +2 -1
  7. package/dist/docxodus.worker.d.ts.map +1 -1
  8. package/dist/docxodus.worker.js +244 -3
  9. package/dist/docxodus.worker.js.map +1 -1
  10. package/dist/editor.bundle.js +2024 -388
  11. package/dist/editor.d.ts +22 -2
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +94 -9
  14. package/dist/editor.js.map +1 -1
  15. package/dist/embed.bundle.js +2912 -437
  16. package/dist/embed.iife.js +2911 -436
  17. package/dist/export-assets.json +283 -0
  18. package/dist/export-browser.bundle.js +7990 -0
  19. package/dist/export-browser.d.ts +266 -0
  20. package/dist/export-browser.d.ts.map +1 -0
  21. package/dist/export-browser.js +2915 -0
  22. package/dist/export-browser.js.map +1 -0
  23. package/dist/export-resource-limits-v1.json +57 -0
  24. package/dist/font-contract.d.ts +150 -0
  25. package/dist/font-contract.d.ts.map +1 -0
  26. package/dist/font-contract.js +59 -0
  27. package/dist/font-contract.js.map +1 -0
  28. package/dist/font-runtime.d.ts +33 -0
  29. package/dist/font-runtime.d.ts.map +1 -0
  30. package/dist/font-runtime.js +1100 -0
  31. package/dist/font-runtime.js.map +1 -0
  32. package/dist/index.d.ts +62 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +130 -16
  35. package/dist/index.js.map +1 -1
  36. package/dist/page-geometry.d.ts +8 -1
  37. package/dist/page-geometry.d.ts.map +1 -1
  38. package/dist/page-geometry.js +37 -11
  39. package/dist/page-geometry.js.map +1 -1
  40. package/dist/pagination.bundle.js +1937 -380
  41. package/dist/pagination.d.ts +277 -15
  42. package/dist/pagination.d.ts.map +1 -1
  43. package/dist/pagination.js +2055 -438
  44. package/dist/pagination.js.map +1 -1
  45. package/dist/react.d.ts +11 -4
  46. package/dist/react.d.ts.map +1 -1
  47. package/dist/react.js +38 -5
  48. package/dist/react.js.map +1 -1
  49. package/dist/render-report-v2.schema.json +1581 -0
  50. package/dist/ribbon-chrome.d.ts +1 -1
  51. package/dist/ribbon-chrome.d.ts.map +1 -1
  52. package/dist/ribbon-chrome.js +46 -0
  53. package/dist/ribbon-chrome.js.map +1 -1
  54. package/dist/ribbon.js +70 -0
  55. package/dist/ribbon.js.map +1 -1
  56. package/dist/session.bundle.js +695 -29
  57. package/dist/session.d.ts +134 -19
  58. package/dist/session.d.ts.map +1 -1
  59. package/dist/session.js +562 -25
  60. package/dist/session.js.map +1 -1
  61. package/dist/types.d.ts +1207 -16
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js.map +1 -1
  64. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  65. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  66. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  67. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  68. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  69. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  70. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  71. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  72. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  73. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  74. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  75. package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
  76. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
  77. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
  78. package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
  79. package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
  80. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  81. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  82. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
  83. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
  84. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
  85. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
  86. package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
  87. package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
  88. package/dist/wasm/_framework/System.Console.wasm +0 -0
  89. package/dist/wasm/_framework/System.Console.wasm.br +0 -0
  90. package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
  91. package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
  92. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  93. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  94. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  95. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  96. package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
  97. package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
  98. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  99. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  100. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  101. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  102. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  103. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  104. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  105. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  106. package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
  107. package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
  108. package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
  109. package/dist/wasm/_framework/System.ObjectModel.wasm.br +0 -0
  110. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  111. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  112. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  113. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  114. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  115. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  116. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  117. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  118. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  119. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  120. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  121. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  122. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  123. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  124. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
  125. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
  126. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  127. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  128. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  129. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  130. package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
  131. package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
  132. package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
  133. package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
  134. package/dist/wasm/_framework/System.wasm +0 -0
  135. package/dist/wasm/_framework/System.wasm.br +0 -0
  136. package/dist/wasm/_framework/dotnet.boot.js +38 -38
  137. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  138. package/dist/wasm/_framework/dotnet.js +1 -1
  139. package/dist/wasm/_framework/dotnet.js.br +0 -0
  140. package/dist/wasm/_framework/dotnet.native.js +3 -3
  141. package/dist/wasm/_framework/dotnet.native.js.br +0 -0
  142. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  143. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  144. package/dist/wasm/_framework/dotnet.runtime.js +1 -1
  145. package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
  146. package/dist/worker-proxy.bundle.js +196 -29
  147. package/dist/worker-proxy.d.ts +36 -3
  148. package/dist/worker-proxy.d.ts.map +1 -1
  149. package/dist/worker-proxy.js +186 -33
  150. package/dist/worker-proxy.js.map +1 -1
  151. package/package.json +24 -7
@@ -172,17 +172,41 @@ var DocxodusEditor = (() => {
172
172
  function ptToPx(pt) {
173
173
  return pt / 0.75;
174
174
  }
175
+ function finiteDataNumber(value, fallback) {
176
+ if (value === void 0 || value.trim() === "") return fallback;
177
+ const parsed = Number(value);
178
+ return Number.isFinite(parsed) ? parsed : fallback;
179
+ }
180
+ function positiveDataNumber(value, fallback) {
181
+ const parsed = finiteDataNumber(value, fallback);
182
+ return parsed > 0 ? parsed : fallback;
183
+ }
184
+ function nonNegativeDataNumber(value, fallback) {
185
+ const parsed = finiteDataNumber(value, fallback);
186
+ return parsed >= 0 ? parsed : fallback;
187
+ }
188
+ function positiveFinite(value, fallback) {
189
+ return Number.isFinite(value) && value > 0 ? value : fallback;
190
+ }
175
191
  function parseSectionDimensions(section) {
176
- const pageWidth = parseFloat(section.dataset.pageWidth || "") || DEFAULT_PAGE_WIDTH;
177
- const pageHeight = parseFloat(section.dataset.pageHeight || "") || DEFAULT_PAGE_HEIGHT;
178
- const contentWidth = parseFloat(section.dataset.contentWidth || "") || pageWidth - 2 * DEFAULT_MARGIN;
179
- const contentHeight = parseFloat(section.dataset.contentHeight || "") || pageHeight - 2 * DEFAULT_MARGIN;
180
- const marginTop = parseFloat(section.dataset.marginTop || "") || DEFAULT_MARGIN;
181
- const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
182
- const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
183
- const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
184
- const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
185
- const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
192
+ const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
193
+ const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
194
+ const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
195
+ const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
196
+ const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
197
+ const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
198
+ const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
199
+ const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
200
+ const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
201
+ const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
202
+ const headerDistance = nonNegativeDataNumber(
203
+ section.dataset.headerHeight,
204
+ DEFAULT_HEADER_FOOTER_DISTANCE
205
+ );
206
+ const footerDistance = nonNegativeDataNumber(
207
+ section.dataset.footerHeight,
208
+ DEFAULT_HEADER_FOOTER_DISTANCE
209
+ );
186
210
  return {
187
211
  pageWidth,
188
212
  pageHeight,
@@ -259,8 +283,12 @@ var DocxodusEditor = (() => {
259
283
  }
260
284
 
261
285
  // src/pagination.ts
286
+ function samePageBox(a, b) {
287
+ 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;
288
+ }
262
289
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
263
- var MIN_BODY_CONTENT_HEIGHT = 72;
290
+ var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
291
+ var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
264
292
  var PaginationEngine = class {
265
293
  /**
266
294
  * Creates a new pagination engine.
@@ -270,24 +298,46 @@ var DocxodusEditor = (() => {
270
298
  * @param options - Pagination options
271
299
  */
272
300
  constructor(staging, container, options = {}) {
301
+ this.createdPageCount = 0;
302
+ this.footnoteSeparator = null;
303
+ this.footnoteContinuationSeparator = null;
304
+ this.footnoteLayoutLikeWord8 = false;
273
305
  this.pendingFootnoteContinuation = null;
274
306
  /** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
275
307
  this.pageNumbering = /* @__PURE__ */ new Map();
276
- this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
277
- this.containerElement = typeof container === "string" ? document.getElementById(container) : container;
308
+ this.lastPages = [];
309
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
310
+ this.state = "ready";
311
+ const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
312
+ this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
313
+ this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
278
314
  if (!this.stagingElement) {
279
315
  throw new Error("Staging element not found");
280
316
  }
281
317
  if (!this.containerElement) {
282
318
  throw new Error("Container element not found");
283
319
  }
320
+ if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
321
+ throw new Error("Staging and container elements must belong to the same document");
322
+ }
323
+ const view = ownerDocument.defaultView;
324
+ if (!view) {
325
+ throw new Error("Pagination requires an attached document with a defaultView");
326
+ }
327
+ this.document = ownerDocument;
328
+ this.view = view;
284
329
  this.scale = options.scale ?? 1;
285
330
  this.cssPrefix = options.cssPrefix ?? "page-";
286
331
  this.showPageNumbers = options.showPageNumbers ?? true;
287
332
  this.pageGap = options.pageGap ?? 20;
288
333
  this.fragmentParagraphs = options.fragmentParagraphs ?? false;
334
+ this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
335
+ this.cancellationCheckpoint = options.checkCancellation;
336
+ this.pageCountCheckpoint = options.checkPageCount;
337
+ this.layoutToken = options.layoutToken;
289
338
  this.hfRegistry = /* @__PURE__ */ new Map();
290
339
  this.footnoteRegistry = /* @__PURE__ */ new Map();
340
+ this.commentMarginRegistry = /* @__PURE__ */ new Map();
291
341
  }
292
342
  /**
293
343
  * Runs the pagination process.
@@ -295,31 +345,469 @@ var DocxodusEditor = (() => {
295
345
  * @returns PaginationResult with page information
296
346
  */
297
347
  paginate() {
298
- const pages = [];
299
- let pageNumber = 1;
300
- this.hfRegistry = this.parseHeaderFooterRegistry();
301
- this.footnoteRegistry = this.parseFootnoteRegistry();
302
- const sections = this.stagingElement.querySelectorAll(
303
- "[data-section-index]"
304
- );
305
- this.pageNumbering = this.parsePageNumbering(sections);
306
- const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
307
- for (const section of sectionsToProcess) {
308
- const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
309
- const dims = parseSectionDimensions(section);
310
- this.stagingElement.style.visibility = "hidden";
311
- this.stagingElement.style.position = "absolute";
312
- this.stagingElement.style.left = "-9999px";
313
- this.stagingElement.style.display = "block";
314
- section.style.width = `${dims.contentWidth}pt`;
315
- const blocks = this.measureBlocks(section, dims);
316
- const sectionPages = this.flowToPages(blocks, dims, pageNumber, sectionIndex);
317
- pages.push(...sectionPages);
318
- pageNumber += sectionPages.length;
348
+ this.checkpoint();
349
+ if (this.state !== "ready") {
350
+ throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
351
+ }
352
+ if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
353
+ throw new Error("Pagination requires staging and container elements attached to their document");
354
+ }
355
+ if (this.document.documentElement.getBoundingClientRect().width <= 0) {
356
+ throw new Error("Pagination requires a browsing context with non-zero layout");
357
+ }
358
+ this.state = "running";
359
+ try {
360
+ const pages = [];
361
+ let pageNumber = 1;
362
+ this.hfRegistry = this.parseHeaderFooterRegistry();
363
+ this.footnoteRegistry = this.parseFootnoteRegistry();
364
+ ({
365
+ normal: this.footnoteSeparator,
366
+ continuation: this.footnoteContinuationSeparator
367
+ } = this.parseFootnoteSeparators());
368
+ this.commentMarginRegistry = this.parseCommentMarginRegistry();
369
+ this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
370
+ const sections = this.stagingElement.querySelectorAll(
371
+ "[data-section-index]"
372
+ );
373
+ this.pageNumbering = this.parsePageNumbering(sections);
374
+ const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
375
+ this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
376
+ const referencedFootnoteIds = /* @__PURE__ */ new Set();
377
+ const referencedCommentIds = /* @__PURE__ */ new Set();
378
+ for (const section of sectionsToProcess) {
379
+ this.checkpoint();
380
+ this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
381
+ for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
382
+ this.checkpoint();
383
+ if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
384
+ const id = reference.dataset.footnoteId;
385
+ if (id) referencedFootnoteIds.add(id);
386
+ }
387
+ for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
388
+ this.checkpoint();
389
+ if (reference.closest(
390
+ "#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
391
+ )) continue;
392
+ const id = reference.dataset.commentId;
393
+ if (id) referencedCommentIds.add(id);
394
+ }
395
+ }
396
+ for (const id of referencedFootnoteIds) {
397
+ this.checkpoint();
398
+ const source = this.footnoteRegistry.get(id);
399
+ if (!source) continue;
400
+ this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
401
+ for (const reference of Array.from(
402
+ source.querySelectorAll("[data-comment-id]")
403
+ )) {
404
+ this.checkpoint();
405
+ const commentId = reference.dataset.commentId;
406
+ if (commentId) referencedCommentIds.add(commentId);
407
+ }
408
+ }
409
+ for (const id of referencedCommentIds) {
410
+ this.checkpoint();
411
+ const source = this.commentMarginRegistry.get(id);
412
+ if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
413
+ }
414
+ const runs = [];
415
+ for (const section of sectionsToProcess) {
416
+ this.checkpoint();
417
+ const dims = parseSectionDimensions(section);
418
+ const previous = runs[runs.length - 1];
419
+ if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
420
+ previous.sections.push(section);
421
+ previous.sectionDimensions.set(
422
+ parseInt(section.dataset.sectionIndex || "0", 10),
423
+ dims
424
+ );
425
+ } else {
426
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
427
+ runs.push({
428
+ sections: [section],
429
+ sectionIndex,
430
+ sectionType: section.dataset.sectionType ?? "nextPage",
431
+ dims,
432
+ sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
433
+ });
434
+ }
435
+ }
436
+ for (const run of runs) {
437
+ this.checkpoint();
438
+ const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
439
+ if (needsParityFiller) {
440
+ const precedingPage = pages[pages.length - 1];
441
+ const precedingPageInSection = parseInt(
442
+ precedingPage.element.dataset.pageInSection ?? "1",
443
+ 10
444
+ );
445
+ const precedingDisplayedPageNumber = parseInt(
446
+ precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
447
+ 10
448
+ );
449
+ const filler = this.createPage(
450
+ precedingPage.dimensions,
451
+ pageNumber,
452
+ precedingPage.sectionIndex,
453
+ precedingDisplayedPageNumber + 1,
454
+ [],
455
+ precedingPageInSection + 1,
456
+ [],
457
+ 0,
458
+ null,
459
+ void 0,
460
+ true
461
+ );
462
+ pages.push(filler);
463
+ pageNumber++;
464
+ }
465
+ this.stagingElement.style.visibility = "hidden";
466
+ this.stagingElement.style.position = "absolute";
467
+ this.stagingElement.style.left = "-9999px";
468
+ this.stagingElement.style.display = "block";
469
+ const blocks = [];
470
+ for (const section of run.sections) {
471
+ this.checkpoint();
472
+ const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
473
+ const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
474
+ section.style.width = `${sectionDims.contentWidth}pt`;
475
+ const columnCount = parseInt(section.dataset.cols || "1", 10);
476
+ if (columnCount > 1) {
477
+ const gap = parseFloat(section.dataset.colGap || "");
478
+ blocks.push(...this.buildColumnBlocks(
479
+ section,
480
+ sectionDims,
481
+ sectionIndex,
482
+ columnCount,
483
+ Number.isFinite(gap) ? gap : 36
484
+ ));
485
+ } else {
486
+ blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
487
+ }
488
+ }
489
+ const sectionPages = this.flowToPages(
490
+ blocks,
491
+ run.dims,
492
+ pageNumber,
493
+ run.sectionIndex,
494
+ run.sectionDimensions
495
+ );
496
+ this.checkpoint();
497
+ pages.push(...sectionPages);
498
+ pageNumber += sectionPages.length;
499
+ }
500
+ this.stagingElement.style.display = "none";
501
+ this.substitutePageNumberFields(pages.length);
502
+ for (const page of pages) {
503
+ this.checkpoint();
504
+ if (page.element.dataset.sectionFiller === "true") continue;
505
+ const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
506
+ const displayedPageNumber = parseInt(
507
+ page.element.dataset.displayedPageNumber || String(page.pageNumber),
508
+ 10
509
+ );
510
+ const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
511
+ const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
512
+ if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
513
+ if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
514
+ }
515
+ this.qualifyPageFragments(pages);
516
+ this.transferVisibleFragmentTargets();
517
+ if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
518
+ this.lastPages = pages;
519
+ const result = {
520
+ totalPages: pages.length,
521
+ pages,
522
+ pageMap: this.layoutToken ? this.materializePageMap(
523
+ this.layoutToken.documentVersion,
524
+ this.layoutToken.rendererFingerprint
525
+ ) : void 0
526
+ };
527
+ this.state = "complete";
528
+ return result;
529
+ } catch (error) {
530
+ this.state = "failed";
531
+ throw error;
532
+ }
533
+ }
534
+ checkpoint() {
535
+ this.cancellationCheckpoint?.();
536
+ }
537
+ /**
538
+ * Normalize visible fragment identities after a caller applies final standalone styles. This
539
+ * deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
540
+ * measurement cannot mutate a tree after it was declared stable.
541
+ */
542
+ normalizePageMapFragmentIdentities() {
543
+ if (this.lastPages.length === 0) {
544
+ throw new Error("paginate() must complete before fragment identities can be normalized");
545
+ }
546
+ this.normalizeVisiblePageFragments(this.lastPages);
547
+ }
548
+ /**
549
+ * Materialize the last completed browser layout as portable page-relative point geometry.
550
+ * The caller supplies both invalidation tokens; this engine never guesses a document version
551
+ * or renderer fingerprint.
552
+ */
553
+ materializePageMap(documentVersion, rendererFingerprint) {
554
+ if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
555
+ throw new Error("documentVersion must be a non-negative safe integer");
556
+ }
557
+ if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
558
+ if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
559
+ const pages = this.lastPages.map((page) => ({
560
+ pageNumber: page.pageNumber,
561
+ pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
562
+ width: page.dimensions.pageWidth,
563
+ height: page.dimensions.pageHeight,
564
+ sectionIndex: page.sectionIndex,
565
+ pageName: `docxodus-section-${page.sectionIndex}`
566
+ }));
567
+ const fragments = [];
568
+ const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
569
+ if (requiredAnchorIds.size === 0) {
570
+ throw new Error("cannot publish an available PageMap without canonical source inventory");
571
+ }
572
+ const measuredAnchorIds = /* @__PURE__ */ new Set();
573
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
574
+ for (const page of this.lastPages) {
575
+ this.checkpoint();
576
+ const pageRect = page.element.getBoundingClientRect();
577
+ if (pageRect.width <= 0 || pageRect.height <= 0) {
578
+ throw new Error(`page ${page.pageNumber} has no measurable geometry`);
579
+ }
580
+ const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
581
+ const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
582
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
583
+ for (const element of Array.from(nodes)) {
584
+ this.checkpoint();
585
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
586
+ const anchorId = element.dataset.sourceAnchorId;
587
+ if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
588
+ throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
589
+ }
590
+ const rect = element.getBoundingClientRect();
591
+ const style = this.view.getComputedStyle(element);
592
+ const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
593
+ if (deliberatelyHidden) continue;
594
+ requiredAnchorIds.add(anchorId);
595
+ const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
596
+ const left2 = visibleRect.left;
597
+ const top2 = visibleRect.top;
598
+ const right2 = visibleRect.right;
599
+ const bottom2 = visibleRect.bottom;
600
+ if (rect.width <= 0 || rect.height <= 0 || right2 <= left2 || bottom2 <= top2) {
601
+ continue;
602
+ }
603
+ measuredAnchorIds.add(anchorId);
604
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
605
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
606
+ const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
607
+ if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
608
+ throw new Error(
609
+ `page ${page.pageNumber} fragment identity changed after final-tree normalization`
610
+ );
611
+ }
612
+ fragments.push({
613
+ fragmentId,
614
+ anchorId,
615
+ fragmentIndex,
616
+ pageNumber: page.pageNumber,
617
+ geometry: {
618
+ x: (left2 - pageRect.left) * pointPerRenderedX,
619
+ y: (top2 - pageRect.top) * pointPerRenderedY,
620
+ width: (right2 - left2) * pointPerRenderedX,
621
+ height: (bottom2 - top2) * pointPerRenderedY
622
+ },
623
+ story: this.storyForCanonicalAnchor(anchorId),
624
+ inTableCell: element.matches("td,th") || element.closest("td,th") !== null
625
+ });
626
+ }
627
+ }
628
+ const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
629
+ if (missingAnchor) {
630
+ throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
631
+ }
632
+ return {
633
+ schemaVersion: 1,
634
+ mode: "paginated",
635
+ availability: "available",
636
+ documentVersion,
637
+ rendererFingerprint,
638
+ pages,
639
+ fragments
640
+ };
641
+ }
642
+ normalizeVisiblePageFragments(pages) {
643
+ const emittedFragmentCounts = /* @__PURE__ */ new Map();
644
+ for (const page of pages) {
645
+ this.checkpoint();
646
+ const pageRect = page.element.getBoundingClientRect();
647
+ for (const element of Array.from(
648
+ page.element.querySelectorAll("[data-source-anchor-id]")
649
+ )) {
650
+ this.checkpoint();
651
+ if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
652
+ const anchorId = element.dataset.sourceAnchorId;
653
+ if (!anchorId) continue;
654
+ const style = this.view.getComputedStyle(element);
655
+ if (style.display === "none" || style.visibility === "hidden") continue;
656
+ const rect = element.getBoundingClientRect();
657
+ const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
658
+ if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
659
+ const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
660
+ emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
661
+ element.dataset.pageNumber = String(page.pageNumber);
662
+ element.dataset.fragmentIndex = String(fragmentIndex);
663
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
664
+ }
665
+ }
666
+ }
667
+ /**
668
+ * Intersect an element with every ancestor that establishes an overflow clip before the page
669
+ * root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
670
+ * therefore must not satisfy PageMap completeness or inflate portable geometry.
671
+ */
672
+ intersectWithClippingAncestors(element, page, pageRect, rect) {
673
+ let left2 = Math.max(rect.left, pageRect.left);
674
+ let top2 = Math.max(rect.top, pageRect.top);
675
+ let right2 = Math.min(rect.right, pageRect.right);
676
+ let bottom2 = Math.min(rect.bottom, pageRect.bottom);
677
+ const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
678
+ for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
679
+ const style = this.view.getComputedStyle(ancestor);
680
+ const clipsX = clips(style.overflowX);
681
+ const clipsY = clips(style.overflowY);
682
+ if (!clipsX && !clipsY) continue;
683
+ const ancestorRect = ancestor.getBoundingClientRect();
684
+ if (clipsX) {
685
+ left2 = Math.max(left2, ancestorRect.left);
686
+ right2 = Math.min(right2, ancestorRect.right);
687
+ }
688
+ if (clipsY) {
689
+ top2 = Math.max(top2, ancestorRect.top);
690
+ bottom2 = Math.min(bottom2, ancestorRect.bottom);
691
+ }
692
+ }
693
+ return { left: left2, top: top2, right: right2, bottom: bottom2 };
694
+ }
695
+ storyForCanonicalAnchor(anchorId) {
696
+ const first = anchorId.indexOf(":");
697
+ const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
698
+ const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
699
+ if (scope.startsWith("hdr")) return "header";
700
+ if (scope.startsWith("ftr")) return "footer";
701
+ if (scope === "fn") return "footnote";
702
+ if (scope === "en") return "endnote";
703
+ if (scope === "cmt") return "comment";
704
+ return "body";
705
+ }
706
+ /**
707
+ * Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
708
+ * Registry wrappers are excluded when scanning staging because selectable registry contents are
709
+ * inventoried separately. Producers may explicitly mark content that has no visual substrate
710
+ * with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
711
+ */
712
+ collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
713
+ const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
714
+ candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
715
+ for (const element of candidates) {
716
+ if (excludeRegistries && element.closest(
717
+ "#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
718
+ )) {
719
+ continue;
720
+ }
721
+ if (this.isZeroHeightExplicitBreakCarrier(element)) {
722
+ element.dataset.pageMapExclude = "true";
723
+ }
724
+ if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
725
+ const anchorId = element.dataset.sourceAnchorId;
726
+ if (anchorId) destination.add(anchorId);
727
+ }
728
+ }
729
+ isZeroHeightExplicitBreakCarrier(element) {
730
+ const next = element.nextElementSibling;
731
+ const bounds = element.getBoundingClientRect();
732
+ 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);
733
+ }
734
+ isDeliberatelyUnrenderedSource(element, sourceRoot) {
735
+ for (let current = element; current; current = current.parentElement) {
736
+ const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
737
+ 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") {
738
+ return true;
739
+ }
740
+ if (current === sourceRoot) break;
741
+ }
742
+ return false;
743
+ }
744
+ /**
745
+ * Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
746
+ * canonical source identity plus page/fragment qualification instead.
747
+ */
748
+ qualifyPageFragments(pages) {
749
+ const fragmentCounts = /* @__PURE__ */ new Map();
750
+ const activeCanonicalIds = /* @__PURE__ */ new Set();
751
+ const activeBareAnchorIds = /* @__PURE__ */ new Set();
752
+ const makeInactive = (element) => {
753
+ element.removeAttribute("data-anchor");
754
+ element.removeAttribute("data-committed-text");
755
+ if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
756
+ };
757
+ for (const page of pages) {
758
+ const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
759
+ for (const element of Array.from(nodes)) {
760
+ const anchorId = element.dataset.sourceAnchorId;
761
+ if (!anchorId) continue;
762
+ const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
763
+ fragmentCounts.set(anchorId, fragmentIndex + 1);
764
+ element.dataset.pageNumber = String(page.pageNumber);
765
+ element.dataset.fragmentIndex = String(fragmentIndex);
766
+ element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
767
+ const story = this.storyForCanonicalAnchor(anchorId);
768
+ const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
769
+ if (element.hasAttribute("data-anchor")) {
770
+ const bareAnchorId = element.dataset.anchor;
771
+ if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
772
+ activeCanonicalIds.add(anchorId);
773
+ activeBareAnchorIds.add(bareAnchorId);
774
+ } else {
775
+ makeInactive(element);
776
+ }
777
+ }
778
+ }
779
+ }
780
+ const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
781
+ for (const element of Array.from(
782
+ this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
783
+ )) {
784
+ const anchorId = element.dataset.sourceAnchorId;
785
+ if (!anchorId) continue;
786
+ const bareAnchorId = element.dataset.anchor;
787
+ if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
788
+ makeInactive(element);
789
+ } else {
790
+ activeStagingCanonicalIds.add(anchorId);
791
+ activeBareAnchorIds.add(bareAnchorId);
792
+ }
793
+ }
794
+ }
795
+ /**
796
+ * Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
797
+ * fragment target copied into a visible page would therefore resolve to its earlier hidden
798
+ * source. Transfer target ownership to the page presentation after flow is complete; registry
799
+ * and wrapper IDs that have no visible counterpart remain available to pagination internals.
800
+ */
801
+ transferVisibleFragmentTargets() {
802
+ const visibleIds = new Set(Array.from(
803
+ this.containerElement.querySelectorAll("[id]")
804
+ ).map((element) => element.id).filter(Boolean));
805
+ if (visibleIds.size === 0) return;
806
+ for (const source of Array.from(
807
+ this.stagingElement.querySelectorAll("[id]")
808
+ )) {
809
+ if (visibleIds.has(source.id)) source.removeAttribute("id");
319
810
  }
320
- this.stagingElement.style.display = "none";
321
- this.substitutePageNumberFields(pages.length);
322
- return { totalPages: pages.length, pages };
323
811
  }
324
812
  /** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
325
813
  parsePageNumbering(sections) {
@@ -363,9 +851,8 @@ var DocxodusEditor = (() => {
363
851
  if (markers.length === 0) continue;
364
852
  const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
365
853
  const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
366
- const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
367
854
  const numbering = this.pageNumbering.get(sectionIndex) ?? {};
368
- const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
855
+ const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
369
856
  for (const marker of Array.from(markers)) {
370
857
  const kind = marker.dataset.field;
371
858
  if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
@@ -377,17 +864,26 @@ var DocxodusEditor = (() => {
377
864
  /**
378
865
  * Measures all content blocks in a section.
379
866
  */
380
- measureBlocks(section, dims) {
867
+ measureBlocks(section, dims, sectionIndex) {
381
868
  const blocks = [];
382
869
  const children = Array.from(section.children);
383
870
  for (const child of children) {
871
+ this.checkpoint();
384
872
  if (child.dataset.sectionIndex !== void 0) {
385
- const nestedBlocks = this.measureBlocks(child, dims);
873
+ const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
874
+ const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
386
875
  blocks.push(...nestedBlocks);
387
876
  continue;
388
877
  }
878
+ if (child.matches("section.endnotes")) {
879
+ const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
880
+ if (endnoteBlocks) {
881
+ blocks.push(...endnoteBlocks);
882
+ continue;
883
+ }
884
+ }
389
885
  const rect = child.getBoundingClientRect();
390
- const style = window.getComputedStyle(child);
886
+ const style = this.view.getComputedStyle(child);
391
887
  const marginTopPx = parseFloat(style.marginTop) || 0;
392
888
  const marginBottomPx = parseFloat(style.marginBottom) || 0;
393
889
  const heightPt = pxToPt(rect.height);
@@ -396,14 +892,166 @@ var DocxodusEditor = (() => {
396
892
  const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
397
893
  blocks.push({
398
894
  element: child,
895
+ sectionIndex,
399
896
  heightPt,
400
897
  marginTopPt,
401
898
  marginBottomPt,
402
899
  keepWithNext: child.dataset.keepWithNext === "true",
403
900
  keepLines: child.dataset.keepLines === "true",
404
901
  pageBreakBefore: child.dataset.pageBreakBefore === "true",
405
- isPageBreak
902
+ isPageBreak,
903
+ isWordParagraph: this.isWordParagraphElement(child)
904
+ });
905
+ }
906
+ return blocks;
907
+ }
908
+ /**
909
+ * Flatten the converter's `section.endnotes > ol > li > p` presentation into
910
+ * ordinary paragraph blocks. This preserves paragraph formatting and canonical
911
+ * p:en/en:en identities while allowing the existing paragraph fragmenter to
912
+ * split a long endnote across page boundaries.
913
+ */
914
+ measureSafeEndnoteBlocks(section, dims, sectionIndex) {
915
+ const sectionChildren = Array.from(section.children);
916
+ const list = sectionChildren.find((child) => child.tagName === "OL");
917
+ if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
918
+ return null;
919
+ }
920
+ const items = Array.from(list.children);
921
+ if (items.length === 0 || items.some(
922
+ (item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
923
+ )) {
924
+ return null;
925
+ }
926
+ const blocks = [];
927
+ const sectionStyle = this.view.getComputedStyle(section);
928
+ for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
929
+ this.checkpoint();
930
+ const clonedRule = rule.cloneNode(true);
931
+ if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
932
+ blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
933
+ }
934
+ const listStyle = this.view.getComputedStyle(list);
935
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
936
+ this.checkpoint();
937
+ const item = items[itemIndex];
938
+ const ownerAnchorId = item.dataset.sourceAnchorId;
939
+ const paragraphs = Array.from(item.children);
940
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
941
+ this.checkpoint();
942
+ const paragraph = paragraphs[paragraphIndex];
943
+ if (!this.hasRangeFragmentSafeLayout(paragraph)) {
944
+ return null;
945
+ }
946
+ const clone = paragraph.cloneNode(true);
947
+ clone.dataset.paginationSafeEndnote = "true";
948
+ clone.style.fontSize ||= sectionStyle.fontSize;
949
+ clone.style.lineHeight ||= sectionStyle.lineHeight;
950
+ clone.style.paddingLeft ||= listStyle.paddingLeft;
951
+ if (ownerAnchorId && !Array.from(
952
+ clone.querySelectorAll("[data-source-anchor-id]")
953
+ ).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
954
+ const owner = this.document.createElement("span");
955
+ owner.dataset.sourceAnchorId = ownerAnchorId;
956
+ while (clone.firstChild) owner.appendChild(clone.firstChild);
957
+ clone.appendChild(owner);
958
+ }
959
+ if (paragraphIndex === 0) {
960
+ const itemId = item.id;
961
+ if (itemId) {
962
+ if (clone.id && clone.id !== itemId) return null;
963
+ clone.id = itemId;
964
+ }
965
+ const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
966
+ const marker = this.formatOrderedListMarker(
967
+ Number.isFinite(value) ? value : itemIndex + 1,
968
+ listStyle.listStyleType
969
+ );
970
+ clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
971
+ }
972
+ blocks.push(this.measureElement(clone, dims, sectionIndex));
973
+ }
974
+ }
975
+ return blocks;
976
+ }
977
+ /** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
978
+ formatOrderedListMarker(value, listStyleType) {
979
+ const format = (() => {
980
+ switch (listStyleType) {
981
+ case "lower-roman":
982
+ return "lowerRoman";
983
+ case "upper-roman":
984
+ return "upperRoman";
985
+ case "lower-alpha":
986
+ case "lower-latin":
987
+ return "lowerLetter";
988
+ case "upper-alpha":
989
+ case "upper-latin":
990
+ return "upperLetter";
991
+ default:
992
+ return "decimal";
993
+ }
994
+ })();
995
+ return formatPageNumber(value, format);
996
+ }
997
+ /**
998
+ * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
999
+ * blocks. Word lays such a section out as N columns inside the same body extent;
1000
+ * a balanced `column-count` container reproduces that geometry, and the paginator
1001
+ * then places each container as one ordinary measured block. A container grows
1002
+ * greedily until its balanced height would exceed the smallest page body available
1003
+ * to the section, so a long columned section still splits across pages at block
1004
+ * boundaries. Each child lands in exactly one container, so anchors never
1005
+ * duplicate. An explicit page break child passes through as its own block, which
1006
+ * ends the current container and lets the normal flow logic turn the page.
1007
+ */
1008
+ buildColumnBlocks(section, dims, sectionIndex, columnCount, columnGapPt) {
1009
+ const children = Array.from(section.children);
1010
+ const blocks = [];
1011
+ const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
1012
+ const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
1013
+ const makeContainer = (slice) => {
1014
+ const container = this.document.createElement("div");
1015
+ container.style.columnCount = String(columnCount);
1016
+ container.style.columnGap = `${columnGapPt}pt`;
1017
+ for (const child of slice) {
1018
+ container.appendChild(child.cloneNode(true));
1019
+ }
1020
+ return container;
1021
+ };
1022
+ let start2 = 0;
1023
+ while (start2 < children.length) {
1024
+ this.checkpoint();
1025
+ if (isBreak(children[start2])) {
1026
+ blocks.push(this.measureElement(children[start2], dims, sectionIndex));
1027
+ start2++;
1028
+ continue;
1029
+ }
1030
+ let end = start2 + 1;
1031
+ let container = makeContainer(children.slice(start2, end));
1032
+ let measured = this.measureElement(container, dims, sectionIndex);
1033
+ while (end < children.length && !isBreak(children[end])) {
1034
+ this.checkpoint();
1035
+ const candidate = makeContainer(children.slice(start2, end + 1));
1036
+ const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
1037
+ if (candidateMeasured.heightPt > maxFragmentHeight) break;
1038
+ container = candidate;
1039
+ measured = candidateMeasured;
1040
+ end++;
1041
+ }
1042
+ blocks.push({
1043
+ element: container,
1044
+ sectionIndex,
1045
+ heightPt: measured.heightPt,
1046
+ marginTopPt: measured.marginTopPt,
1047
+ marginBottomPt: measured.marginBottomPt,
1048
+ keepWithNext: false,
1049
+ keepLines: false,
1050
+ pageBreakBefore: false,
1051
+ isPageBreak: false,
1052
+ isWordParagraph: false
406
1053
  });
1054
+ start2 = end;
407
1055
  }
408
1056
  return blocks;
409
1057
  }
@@ -412,8 +1060,8 @@ var DocxodusEditor = (() => {
412
1060
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
413
1061
  * rows because wrapping and collapsed borders change the height of a fragment.
414
1062
  */
415
- measureElement(element, dims) {
416
- const measurementHost = document.createElement("div");
1063
+ measureElement(element, dims, sectionIndex) {
1064
+ const measurementHost = this.document.createElement("div");
417
1065
  measurementHost.style.position = "absolute";
418
1066
  measurementHost.style.visibility = "hidden";
419
1067
  measurementHost.style.left = "-9999px";
@@ -422,16 +1070,18 @@ var DocxodusEditor = (() => {
422
1070
  measurementHost.appendChild(measuredElement);
423
1071
  this.stagingElement.appendChild(measurementHost);
424
1072
  const rect = measuredElement.getBoundingClientRect();
425
- const style = window.getComputedStyle(measuredElement);
1073
+ const style = this.view.getComputedStyle(measuredElement);
426
1074
  const measured = {
427
1075
  element,
1076
+ sectionIndex,
428
1077
  heightPt: pxToPt(rect.height),
429
1078
  marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
430
1079
  marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
431
1080
  keepWithNext: element.dataset.keepWithNext === "true",
432
1081
  keepLines: element.dataset.keepLines === "true",
433
1082
  pageBreakBefore: element.dataset.pageBreakBefore === "true",
434
- isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
1083
+ isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`),
1084
+ isWordParagraph: this.isWordParagraphElement(element)
435
1085
  };
436
1086
  this.stagingElement.removeChild(measurementHost);
437
1087
  return measured;
@@ -463,10 +1113,15 @@ var DocxodusEditor = (() => {
463
1113
  * collapsed-margin rules as normal block placement. The trailing margin is
464
1114
  * intentionally excluded, matching the individual block fit check.
465
1115
  */
466
- measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
1116
+ measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage, pageInSection) {
467
1117
  const firstBlock = chain[0];
468
1118
  if (!firstBlock) return 0;
469
- const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
1119
+ const firstMarginTop = this.effectiveBlockMarginTop(
1120
+ firstBlock,
1121
+ previousMarginBottomPt,
1122
+ isFirstOnPage,
1123
+ pageInSection
1124
+ );
470
1125
  let bodyHeight = firstMarginTop + firstBlock.heightPt;
471
1126
  for (let index = 1; index < chain.length; index++) {
472
1127
  const previousBlock = chain[index - 1];
@@ -475,6 +1130,35 @@ var DocxodusEditor = (() => {
475
1130
  }
476
1131
  return bodyHeight;
477
1132
  }
1133
+ /** Word paragraphs render as `p`, or as `h1`–`h6` when their style has an outline level. */
1134
+ isWordParagraphElement(element) {
1135
+ return element.tagName === "P" || /^H[1-6]$/.test(element.tagName);
1136
+ }
1137
+ shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection) {
1138
+ return isFirstOnPage && pageInSection > 1 && block.isWordParagraph === true;
1139
+ }
1140
+ /**
1141
+ * Resolves the part of a block's top margin that consumes the current page.
1142
+ *
1143
+ * In Word's native DOCX layout, paragraph space-before is suppressed when a paragraph is the
1144
+ * first body block on a later page of the SAME section. The first page of a document/section is
1145
+ * the exception and keeps its spacing. Tables and other block margins are not paragraph spacing,
1146
+ * so they continue to use the ordinary CSS collapsing rule.
1147
+ */
1148
+ effectiveBlockMarginTop(block, previousMarginBottomPt, isFirstOnPage, pageInSection) {
1149
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
1150
+ return 0;
1151
+ }
1152
+ return isFirstOnPage ? block.marginTopPt : Math.max(block.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
1153
+ }
1154
+ /** Clone a source block with the same page-top spacing decision used by the height budget. */
1155
+ cloneBlockForPage(block, isFirstOnPage, pageInSection) {
1156
+ const clone = block.element.cloneNode(true);
1157
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
1158
+ clone.style.setProperty("margin-top", "0", "important");
1159
+ }
1160
+ return clone;
1161
+ }
478
1162
  /**
479
1163
  * Finds footnote references introduced by a sequence of blocks, preserving
480
1164
  * document order and excluding references already assigned to the page.
@@ -483,6 +1167,7 @@ var DocxodusEditor = (() => {
483
1167
  const knownIds = new Set(existingFootnoteIds);
484
1168
  const newIds = [];
485
1169
  for (const block of blocks) {
1170
+ this.checkpoint();
486
1171
  for (const id of this.extractFootnoteRefs(block.element)) {
487
1172
  if (!knownIds.has(id)) {
488
1173
  knownIds.add(id);
@@ -500,8 +1185,8 @@ var DocxodusEditor = (() => {
500
1185
  smallestEffectiveContentHeight(dims, sectionIndex) {
501
1186
  return Math.min(
502
1187
  this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
503
- this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
504
- this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
1188
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
1189
+ this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
505
1190
  );
506
1191
  }
507
1192
  /**
@@ -539,10 +1224,11 @@ var DocxodusEditor = (() => {
539
1224
  if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
540
1225
  return null;
541
1226
  }
542
- const table = wrapper.firstElementChild;
543
- if (!(table instanceof HTMLTableElement)) {
1227
+ const tableElement = wrapper.firstElementChild;
1228
+ if (!tableElement || tableElement.localName !== "table") {
544
1229
  return null;
545
1230
  }
1231
+ const table = tableElement;
546
1232
  const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
547
1233
  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]")) {
548
1234
  return null;
@@ -556,8 +1242,10 @@ var DocxodusEditor = (() => {
556
1242
  const groups = [];
557
1243
  let start2 = 0;
558
1244
  while (start2 < rows.length) {
1245
+ this.checkpoint();
559
1246
  let end = start2;
560
1247
  while (end < rows.length) {
1248
+ this.checkpoint();
561
1249
  const candidate = this.createSimpleTableFragment(
562
1250
  wrapper,
563
1251
  table,
@@ -565,7 +1253,7 @@ var DocxodusEditor = (() => {
565
1253
  rows.slice(start2, end + 1),
566
1254
  start2 === 0
567
1255
  );
568
- const measured = this.measureElement(candidate, dims);
1256
+ const measured = this.measureElement(candidate, dims, block.sectionIndex);
569
1257
  if (measured.heightPt > maximumFragmentHeight) {
570
1258
  break;
571
1259
  }
@@ -597,7 +1285,7 @@ var DocxodusEditor = (() => {
597
1285
  if (!isLast) {
598
1286
  fragment.style.setProperty("margin-bottom", "0", "important");
599
1287
  }
600
- const measured = this.measureElement(fragment, dims);
1288
+ const measured = this.measureElement(fragment, dims, block.sectionIndex);
601
1289
  if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
602
1290
  return null;
603
1291
  }
@@ -612,35 +1300,198 @@ var DocxodusEditor = (() => {
612
1300
  return fragments;
613
1301
  }
614
1302
  /**
615
- * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
616
- * after whitespace or at a run boundary so the paginator never deliberately
617
- * cuts through an ordinary word merely to fill a little more of a page.
1303
+ * DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
1304
+ * offsets are checked against the browser's Unicode grapheme segmenter, so a
1305
+ * formatting-run boundary can never bisect a surrogate pair, combining
1306
+ * sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
1307
+ * PAGE/NUMPAGES field results are atomic because splitting their marker would
1308
+ * make later substitution duplicate or replace only half of the field.
618
1309
  */
619
- paragraphFragmentEndpoints(paragraph) {
620
- const endpoints = [];
621
- const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
1310
+ paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
1311
+ const textNodes = [];
1312
+ const textStarts = /* @__PURE__ */ new Map();
1313
+ const lastTextByField = /* @__PURE__ */ new Map();
1314
+ const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
1315
+ const flattenedChunks = [];
1316
+ let flattenedLength = 0;
622
1317
  let textNode;
623
1318
  while (textNode = walker.nextNode()) {
624
- const text = textNode.data;
625
- if (text.length === 0) continue;
626
- const whitespace = /\s+/g;
1319
+ this.checkpoint();
1320
+ textStarts.set(textNode, flattenedLength);
1321
+ textNodes.push(textNode);
1322
+ flattenedChunks.push(textNode.data);
1323
+ flattenedLength += textNode.data.length;
1324
+ let field = textNode.parentElement?.closest("[data-field]") ?? null;
1325
+ while (field?.parentElement?.closest("[data-field]")) {
1326
+ field = field.parentElement.closest("[data-field]");
1327
+ }
1328
+ if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
1329
+ }
1330
+ const flattenedText = flattenedChunks.join("");
1331
+ if (!this.hasVisibleText(flattenedText)) return [];
1332
+ const invisible = /[\s\u200B-\u200F\uFEFF]/;
1333
+ let firstVisibleOffset = -1;
1334
+ let lastVisibleOffset = -1;
1335
+ for (let index = 0; index < flattenedText.length; index++) {
1336
+ if (index % 4096 === 0) this.checkpoint();
1337
+ if (invisible.test(flattenedText[index])) continue;
1338
+ if (firstVisibleOffset < 0) firstVisibleOffset = index;
1339
+ lastVisibleOffset = index;
1340
+ }
1341
+ const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
1342
+ const candidates = [];
1343
+ const addCandidate = (candidate) => {
1344
+ const { textOffset } = candidate;
1345
+ if (textOffset <= 0 || textOffset >= flattenedText.length) return;
1346
+ if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
1347
+ if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
1348
+ if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
1349
+ if (graphemeBoundaries) {
1350
+ if (!graphemeBoundaries.has(textOffset)) return;
1351
+ } else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
1352
+ return;
1353
+ }
1354
+ candidates.push(candidate);
1355
+ };
1356
+ for (const node of textNodes) {
1357
+ this.checkpoint();
1358
+ const start2 = textStarts.get(node);
1359
+ const atomic = node.parentElement?.closest(
1360
+ "[data-field], [data-list-marker], a[data-comment-id]"
1361
+ );
1362
+ if (atomic && paragraph.contains(atomic)) continue;
1363
+ const whitespace = /[\u0009-\u000D\u0020]+/g;
627
1364
  let match;
628
- while ((match = whitespace.exec(text)) !== null) {
629
- endpoints.push({ node: textNode, offset: match.index + match[0].length });
1365
+ let matchesSinceCheckpoint = 0;
1366
+ while ((match = whitespace.exec(node.data)) !== null) {
1367
+ if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
1368
+ const offset = match.index + match[0].length;
1369
+ addCandidate({ node, offset, textOffset: start2 + offset, priority: 0 });
630
1370
  }
631
- if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
632
- endpoints.push({ node: textNode, offset: text.length });
1371
+ addCandidate({
1372
+ node,
1373
+ offset: node.data.length,
1374
+ textOffset: start2 + node.data.length,
1375
+ priority: 0
1376
+ });
1377
+ if (emergencyGraphemeBreaks) {
1378
+ for (let offset = 1; offset < node.data.length; offset++) {
1379
+ if (offset % 256 === 0) this.checkpoint();
1380
+ addCandidate({
1381
+ node,
1382
+ offset,
1383
+ textOffset: start2 + offset,
1384
+ priority: -1
1385
+ });
1386
+ }
1387
+ }
1388
+ }
1389
+ for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
1390
+ this.checkpoint();
1391
+ if (field.parentElement?.closest("[data-field]")) continue;
1392
+ const last = lastTextByField.get(field);
1393
+ const parent = field.parentNode;
1394
+ if (!last || !parent) continue;
1395
+ const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
1396
+ if (childIndex < 0) continue;
1397
+ addCandidate({
1398
+ node: parent,
1399
+ offset: childIndex + 1,
1400
+ textOffset: textStarts.get(last) + last.data.length,
1401
+ priority: 1
1402
+ });
1403
+ }
1404
+ candidates.sort((left2, right2) => left2.textOffset - right2.textOffset || right2.priority - left2.priority);
1405
+ const endpoints = [];
1406
+ for (const candidate of candidates) {
1407
+ if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
1408
+ endpoints.push(candidate);
633
1409
  }
634
1410
  }
635
1411
  return endpoints;
636
1412
  }
1413
+ /** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
1414
+ graphemeBoundaryOffsets(text) {
1415
+ const SegmenterConstructor = this.view.Intl.Segmenter;
1416
+ if (!SegmenterConstructor) return null;
1417
+ const boundaries = /* @__PURE__ */ new Set([0, text.length]);
1418
+ let segmentCount = 0;
1419
+ for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
1420
+ if (++segmentCount % 256 === 0) this.checkpoint();
1421
+ boundaries.add(part.index);
1422
+ boundaries.add(part.index + part.segment.length);
1423
+ }
1424
+ return boundaries;
1425
+ }
1426
+ /**
1427
+ * Conservative UAX #14/CSS wrapping opportunities used for synthetic page
1428
+ * boundaries. In particular, a formatting-run boundary is not itself a word
1429
+ * boundary, and Japanese opening/closing punctuation stays with its pair.
1430
+ * Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
1431
+ * the browser to wrap anywhere.
1432
+ */
1433
+ isLegalParagraphLineBoundary(paragraph, text, offset) {
1434
+ const lastCodeUnit = text.charCodeAt(offset - 1);
1435
+ const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
1436
+ const beforeCodePoint = text.codePointAt(beforeOffset);
1437
+ const afterCodePoint = text.codePointAt(offset);
1438
+ const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
1439
+ const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
1440
+ if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
1441
+ if (/[-\u00AD\u2010]$/u.test(before)) return true;
1442
+ const style = this.view.getComputedStyle(paragraph);
1443
+ if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
1444
+ const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
1445
+ if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
1446
+ const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
1447
+ 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";
1448
+ return !opening.includes(before) && !closing.includes(after);
1449
+ }
1450
+ /** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
1451
+ isNonBreakingTextBoundary(text, offset) {
1452
+ const codePointBefore = (index) => {
1453
+ if (index <= 0) return void 0;
1454
+ const last = text.charCodeAt(index - 1);
1455
+ const start2 = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
1456
+ return text.codePointAt(Math.max(0, start2));
1457
+ };
1458
+ const nonBreaking = /* @__PURE__ */ new Set([
1459
+ 160,
1460
+ 8206,
1461
+ 8207,
1462
+ 8209,
1463
+ 8234,
1464
+ 8235,
1465
+ 8236,
1466
+ 8237,
1467
+ 8238,
1468
+ 8239,
1469
+ 8288,
1470
+ 8294,
1471
+ 8295,
1472
+ 8296,
1473
+ 8297,
1474
+ 65279
1475
+ ]);
1476
+ return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
1477
+ }
1478
+ /**
1479
+ * Without Intl.Segmenter, admit only an all-ASCII boundary. This still
1480
+ * fragments ordinary prose while refusing to guess about Unicode clusters.
1481
+ */
1482
+ isConservativeFallbackTextBoundary(text, offset) {
1483
+ return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
1484
+ }
637
1485
  /**
638
1486
  * Whether a range contains visible text after ignoring bidi/zero-width marks.
639
1487
  * Paragraph fragmentation deliberately excludes non-textual descendants, so
640
1488
  * this is enough to reject empty head or tail fragments.
641
1489
  */
642
1490
  hasVisibleFragmentText(fragment) {
643
- return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
1491
+ return this.hasVisibleText(fragment.textContent || "");
1492
+ }
1493
+ hasVisibleText(text) {
1494
+ return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
644
1495
  }
645
1496
  /**
646
1497
  * Phase one only handles text-like paragraph descendants. Objects, explicit
@@ -649,13 +1500,25 @@ var DocxodusEditor = (() => {
649
1500
  * fallback instead of risking broken content or duplicate anchors.
650
1501
  */
651
1502
  canFragmentParagraph(block) {
652
- if (!this.fragmentParagraphs || block.element.tagName !== "P") {
1503
+ if (!this.fragmentParagraphs) {
653
1504
  return false;
654
1505
  }
655
1506
  const paragraph = block.element;
656
1507
  if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
657
1508
  return false;
658
1509
  }
1510
+ return this.canRangeFragmentParagraph(paragraph);
1511
+ }
1512
+ /**
1513
+ * Shared structural gate for body and note paragraph fragmentation. Footnote
1514
+ * first paragraphs are inline beside their marker, so that one known layout
1515
+ * context may opt into an inline root while retaining every descendant and
1516
+ * break-safety restriction used for body text.
1517
+ */
1518
+ canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
1519
+ if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
1520
+ return false;
1521
+ }
659
1522
  const unsupportedDescendants = [
660
1523
  "br",
661
1524
  "img",
@@ -686,21 +1549,27 @@ var DocxodusEditor = (() => {
686
1549
  "fieldset",
687
1550
  "details",
688
1551
  "[data-footnote-id]",
689
- "[data-list-marker]",
690
- "[data-anchor]",
691
- "[id]",
692
1552
  "[contenteditable]"
693
1553
  ].join(", ");
694
1554
  if (paragraph.querySelector(unsupportedDescendants)) {
695
1555
  return false;
696
1556
  }
697
- const paragraphStyle = window.getComputedStyle(paragraph);
698
- 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") {
1557
+ const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
1558
+ return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
1559
+ }
1560
+ /**
1561
+ * A range clone preserves nested inline formatting exactly. Anything that establishes its own
1562
+ * box/layout context is deferred until a future fragmenter can model it accurately. Callers
1563
+ * must invoke this while the paragraph is attached to the styled document.
1564
+ */
1565
+ hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
1566
+ const paragraphStyle = this.view.getComputedStyle(paragraph);
1567
+ 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") {
699
1568
  return false;
700
1569
  }
701
1570
  for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
702
- const style = window.getComputedStyle(descendant);
703
- if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
1571
+ const style = this.view.getComputedStyle(descendant);
1572
+ if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
704
1573
  return false;
705
1574
  }
706
1575
  }
@@ -727,58 +1596,122 @@ var DocxodusEditor = (() => {
727
1596
  return fragment;
728
1597
  }
729
1598
  /**
730
- * Splits a simple paragraph at the largest DOM Range endpoint that fits the
731
- * currently available body space. The caller then processes the tail normally,
732
- * allowing it to fragment again on later pages when necessary.
1599
+ * Range clones repeat an inline ancestor when the split lands inside it. Keep
1600
+ * semantic wrappers (links, comments, formatting) on both sides, but retain a
1601
+ * duplicated HTML/editor identity only on the leading fragment. Targets that
1602
+ * occur wholly after the split are absent from `head` and remain on `tail`.
733
1603
  */
734
- tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
735
- if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
736
- return null;
1604
+ reconcileParagraphFragmentIdentities(head, tail) {
1605
+ const elements = (root, selector2) => [
1606
+ ...root.matches(selector2) ? [root] : [],
1607
+ ...Array.from(root.querySelectorAll(selector2))
1608
+ ];
1609
+ const headIds = new Set(elements(head, "[id]").map((element) => element.id));
1610
+ for (const element of elements(tail, "[id]")) {
1611
+ if (headIds.has(element.id)) element.removeAttribute("id");
737
1612
  }
738
- const paragraph = block.element;
739
- const endpoints = this.paragraphFragmentEndpoints(paragraph);
740
- if (endpoints.length < 2) {
741
- return null;
1613
+ const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
1614
+ for (const element of elements(tail, "[data-anchor]")) {
1615
+ if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
1616
+ element.removeAttribute("data-anchor");
1617
+ element.removeAttribute("data-committed-text");
742
1618
  }
743
- let low = 0;
744
- let high = endpoints.length - 2;
745
- let best = null;
746
- while (low <= high) {
747
- const middle = Math.floor((low + high) / 2);
748
- const endpoint = endpoints[middle];
749
- const headRange = document.createRange();
750
- headRange.setStart(paragraph, 0);
751
- headRange.setEnd(endpoint.node, endpoint.offset);
752
- const headContents = headRange.cloneContents();
753
- if (!this.hasVisibleFragmentText(headContents)) {
754
- low = middle + 1;
755
- continue;
1619
+ }
1620
+ /**
1621
+ * Range-clone the largest safe prefix accepted by `fits`. The measurement
1622
+ * policy stays with the caller, allowing body blocks and note bands to share
1623
+ * one DOM fragmenter while measuring in their respective layout contexts.
1624
+ */
1625
+ splitParagraphAtLargestFit(paragraph, fits, options = {}) {
1626
+ const largestFit = (endpoints2) => {
1627
+ const lastEndpoint = endpoints2[endpoints2.length - 1];
1628
+ if (lastEndpoint) {
1629
+ const lastRange = this.document.createRange();
1630
+ lastRange.setStart(paragraph, 0);
1631
+ lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
1632
+ const lastContents = lastRange.cloneContents();
1633
+ if (this.hasVisibleFragmentText(lastContents)) {
1634
+ const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
1635
+ if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
1636
+ }
756
1637
  }
757
- const head = this.createParagraphFragment(paragraph, headRange, true, false);
758
- const measured = this.measureElement(head, dims);
759
- if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
760
- best = { endpoint, element: head, measured };
761
- low = middle + 1;
762
- } else {
763
- high = middle - 1;
1638
+ let low = 0;
1639
+ let high = endpoints2.length - 2;
1640
+ let best2 = null;
1641
+ while (low <= high) {
1642
+ this.checkpoint();
1643
+ const middle = Math.floor((low + high) / 2);
1644
+ const endpoint = endpoints2[middle];
1645
+ const headRange = this.document.createRange();
1646
+ headRange.setStart(paragraph, 0);
1647
+ headRange.setEnd(endpoint.node, endpoint.offset);
1648
+ const headContents = headRange.cloneContents();
1649
+ if (!this.hasVisibleFragmentText(headContents)) {
1650
+ low = middle + 1;
1651
+ continue;
1652
+ }
1653
+ const head = this.createParagraphFragment(paragraph, headRange, true, false);
1654
+ if (fits(head)) {
1655
+ best2 = { endpoint, head };
1656
+ low = middle + 1;
1657
+ } else {
1658
+ high = middle - 1;
1659
+ }
764
1660
  }
1661
+ return best2;
1662
+ };
1663
+ let endpoints = this.paragraphFragmentEndpoints(paragraph);
1664
+ let best = largestFit(endpoints);
1665
+ if (!best && options.emergencyGraphemeBreaks) {
1666
+ endpoints = this.paragraphFragmentEndpoints(paragraph, true);
1667
+ best = largestFit(endpoints);
1668
+ }
1669
+ if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
1670
+ const endpoint = endpoints[0];
1671
+ const headRange = this.document.createRange();
1672
+ headRange.setStart(paragraph, 0);
1673
+ headRange.setEnd(endpoint.node, endpoint.offset);
1674
+ best = {
1675
+ endpoint,
1676
+ head: this.createParagraphFragment(paragraph, headRange, true, false)
1677
+ };
765
1678
  }
766
1679
  if (!best) {
767
- return null;
1680
+ return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
768
1681
  }
769
- const tailRange = document.createRange();
1682
+ const tailRange = this.document.createRange();
770
1683
  tailRange.setStart(best.endpoint.node, best.endpoint.offset);
771
1684
  tailRange.setEnd(paragraph, paragraph.childNodes.length);
772
1685
  const tailContents = tailRange.cloneContents();
773
- if (!this.hasVisibleFragmentText(tailContents)) {
1686
+ if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
1687
+ const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
1688
+ this.reconcileParagraphFragmentIdentities(best.head, tail);
1689
+ return {
1690
+ kind: "split",
1691
+ head: best.head,
1692
+ tail
1693
+ };
1694
+ }
1695
+ /**
1696
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
1697
+ * currently available body space. The caller then processes the tail normally,
1698
+ * allowing it to fragment again on later pages when necessary.
1699
+ */
1700
+ tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
1701
+ if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
774
1702
  return null;
775
1703
  }
776
- const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
777
- const tailMeasured = this.measureElement(tail, dims);
1704
+ const split = this.splitParagraphAtLargestFit(block.element, (head) => {
1705
+ const measured = this.measureElement(head, dims, block.sectionIndex);
1706
+ return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
1707
+ });
1708
+ if (split.kind !== "split") return null;
1709
+ const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
1710
+ const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
778
1711
  return [
779
1712
  {
780
- ...best.measured,
781
- element: best.element,
1713
+ ...headMeasured,
1714
+ element: split.head,
782
1715
  keepWithNext: false,
783
1716
  keepLines: false,
784
1717
  pageBreakBefore: false,
@@ -786,7 +1719,7 @@ var DocxodusEditor = (() => {
786
1719
  },
787
1720
  {
788
1721
  ...tailMeasured,
789
- element: tail,
1722
+ element: split.tail,
790
1723
  keepWithNext: false,
791
1724
  keepLines: false,
792
1725
  pageBreakBefore: false,
@@ -857,7 +1790,7 @@ var DocxodusEditor = (() => {
857
1790
  const registry = /* @__PURE__ */ new Map();
858
1791
  const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
859
1792
  if (!registryEl) return registry;
860
- const entries = Array.from(registryEl.querySelectorAll("[data-footnote-id]"));
1793
+ const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
861
1794
  for (const entry of entries) {
862
1795
  const footnoteId = entry.dataset.footnoteId;
863
1796
  if (footnoteId) {
@@ -866,6 +1799,68 @@ var DocxodusEditor = (() => {
866
1799
  }
867
1800
  return registry;
868
1801
  }
1802
+ /** Read Word's optional normal and continuation separator stories. */
1803
+ parseFootnoteSeparators() {
1804
+ const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
1805
+ const clone = (kind) => {
1806
+ const source = Array.from(registry?.children ?? []).find(
1807
+ (entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
1808
+ );
1809
+ return source?.cloneNode(true);
1810
+ };
1811
+ return {
1812
+ normal: clone("normal") ?? null,
1813
+ continuation: clone("continuation") ?? null
1814
+ };
1815
+ }
1816
+ /** Append the exact separator story selected for this initial/continued note band. */
1817
+ appendFootnoteSeparator(container, continuation) {
1818
+ const kind = continuation ? "continuation" : "normal";
1819
+ const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
1820
+ if (!source) {
1821
+ const fallback = this.document.createElement("hr");
1822
+ fallback.dataset.footnoteSeparator = kind;
1823
+ container.appendChild(fallback);
1824
+ return;
1825
+ }
1826
+ const clone = source.cloneNode(true);
1827
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
1828
+ element.removeAttribute("id");
1829
+ element.removeAttribute("data-anchor");
1830
+ element.removeAttribute("data-committed-text");
1831
+ element.removeAttribute("data-source-anchor-id");
1832
+ element.removeAttribute("data-page-fragment-id");
1833
+ element.removeAttribute("data-fragment-index");
1834
+ element.removeAttribute("data-footnote-id");
1835
+ element.removeAttribute("data-comment-id");
1836
+ element.removeAttribute("name");
1837
+ if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
1838
+ element.removeAttribute("href");
1839
+ }
1840
+ element.setAttribute("contenteditable", "false");
1841
+ }
1842
+ container.appendChild(clone);
1843
+ }
1844
+ /** Parses the hidden source notes used to render paginated margin comments. */
1845
+ parseCommentMarginRegistry() {
1846
+ const registry = /* @__PURE__ */ new Map();
1847
+ const registryEl = this.stagingElement.querySelector(
1848
+ "#pagination-comment-margin-registry"
1849
+ );
1850
+ if (!registryEl) return registry;
1851
+ for (const entry of Array.from(
1852
+ registryEl.querySelectorAll("[data-comment-id]")
1853
+ )) {
1854
+ const commentId = entry.dataset.commentId;
1855
+ if (!commentId) continue;
1856
+ let root = entry;
1857
+ for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
1858
+ root = parent;
1859
+ }
1860
+ registry.set(commentId, root.cloneNode(true));
1861
+ }
1862
+ return registry;
1863
+ }
869
1864
  /**
870
1865
  * Extracts footnote reference IDs from an element.
871
1866
  */
@@ -880,6 +1875,92 @@ var DocxodusEditor = (() => {
880
1875
  }
881
1876
  return ids;
882
1877
  }
1878
+ /** Clone a note child for the continuation queue with its original selector position. */
1879
+ cloneFootnoteElementForContinuation(element, sourceIndex) {
1880
+ const clone = element.cloneNode(true);
1881
+ clone.setAttribute(
1882
+ FOOTNOTE_SOURCE_POSITION_ATTR,
1883
+ sourceIndex === 0 ? "first" : "later"
1884
+ );
1885
+ return clone;
1886
+ }
1887
+ /** Remove identities that belong only to the initial registry presentation shell. */
1888
+ makeContinuationShellInert(element) {
1889
+ element.removeAttribute("id");
1890
+ element.removeAttribute("data-anchor");
1891
+ element.removeAttribute("data-committed-text");
1892
+ }
1893
+ /**
1894
+ * Build the exact continuation shape shared by measurement and paint.
1895
+ *
1896
+ * Keep the source `.footnote-item > .footnote-content` ancestry: document
1897
+ * CSS, inherited direction/language, and custom classes frequently target
1898
+ * those shells. Only the number and initial HTML/editor identities are
1899
+ * omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
1900
+ * whose first carried element was a later source paragraph.
1901
+ */
1902
+ createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
1903
+ const source = this.footnoteRegistry.get(continuation.footnoteId);
1904
+ const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
1905
+ this.makeContinuationShellInert(wrapper);
1906
+ wrapper.classList.add("footnote-continuation");
1907
+ wrapper.dataset.footnoteId = continuation.footnoteId;
1908
+ if (continuation.sourceAnchorId) {
1909
+ wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
1910
+ }
1911
+ const sourceContent = source?.querySelector(".footnote-content");
1912
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
1913
+ this.makeContinuationShellInert(content);
1914
+ if (!sourceContent) content.className = "footnote-content";
1915
+ const first = elements[0];
1916
+ if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
1917
+ const sentinel = this.document.createElement("p");
1918
+ sentinel.className = "footnote-continuation-position-sentinel";
1919
+ sentinel.setAttribute("aria-hidden", "true");
1920
+ sentinel.style.setProperty("display", "none", "important");
1921
+ content.appendChild(sentinel);
1922
+ }
1923
+ for (let index = 0; index < elements.length; index++) {
1924
+ this.checkpoint();
1925
+ const element = elements[index];
1926
+ const clone = element.cloneNode(true);
1927
+ clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
1928
+ if (index === 0 && clone.tagName === "P") {
1929
+ clone.style.setProperty("display", "block", "important");
1930
+ }
1931
+ content.appendChild(clone);
1932
+ }
1933
+ wrapper.appendChild(content);
1934
+ return wrapper;
1935
+ }
1936
+ /** Build the exact partial-note item shape shared by measurement and paint. */
1937
+ createPartialFootnoteItem(footnote, fittingElements) {
1938
+ const item = footnote.cloneNode(false);
1939
+ const number = footnote.querySelector(".footnote-number");
1940
+ if (number) item.appendChild(number.cloneNode(true));
1941
+ const sourceContent = footnote.querySelector(".footnote-content");
1942
+ const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
1943
+ if (!sourceContent) content.className = "footnote-content";
1944
+ for (const element of fittingElements) {
1945
+ this.checkpoint();
1946
+ content.appendChild(element.cloneNode(true));
1947
+ }
1948
+ item.appendChild(content);
1949
+ return item;
1950
+ }
1951
+ /**
1952
+ * Where a note measurement tree is attached.
1953
+ *
1954
+ * A note band is reserved from a measurement and then painted; if the two
1955
+ * happen under different inherited typography the painted notes overflow the
1956
+ * reserve, which is the invisible clipping this engine exists to avoid. The
1957
+ * registry lives in the staging tree but a band paints inside the output
1958
+ * container, so measure there. The host is never a page box, so a page's
1959
+ * `zoom` cannot scale a reserve that is accounted for in unscaled points.
1960
+ */
1961
+ noteMeasurementHost() {
1962
+ return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
1963
+ }
883
1964
  /**
884
1965
  * Measures the height of footnotes for given IDs (in points).
885
1966
  * Creates a temporary container to measure the footnotes.
@@ -887,140 +1968,246 @@ var DocxodusEditor = (() => {
887
1968
  * @param contentWidth - Width for measurement
888
1969
  * @param continuation - Optional continuation content to include first
889
1970
  */
890
- measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
1971
+ measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
891
1972
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
892
- if (footnoteIds.length === 0 && !hasContinuation || this.footnoteRegistry.size === 0) {
1973
+ if (footnoteIds.length === 0 && !hasContinuation) {
1974
+ return 0;
1975
+ }
1976
+ if (this.footnoteRegistry.size === 0 && !hasContinuation) {
893
1977
  return 0;
894
1978
  }
895
- const measureContainer = document.createElement("div");
1979
+ const measureContainer = this.document.createElement("div");
896
1980
  measureContainer.style.position = "absolute";
897
1981
  measureContainer.style.visibility = "hidden";
898
1982
  measureContainer.style.width = `${contentWidth}pt`;
899
1983
  measureContainer.style.left = "-9999px";
900
1984
  measureContainer.className = this.cssPrefix + "footnotes";
901
- const hr = document.createElement("hr");
902
- measureContainer.appendChild(hr);
1985
+ this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
903
1986
  if (hasContinuation) {
904
- const contWrapper = document.createElement("div");
905
- contWrapper.className = "footnote-continuation";
906
- for (const el of continuation.remainingElements) {
907
- contWrapper.appendChild(el.cloneNode(true));
908
- }
909
- measureContainer.appendChild(contWrapper);
1987
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
910
1988
  }
1989
+ const partialById = new Map(
1990
+ partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
1991
+ );
911
1992
  for (const id of footnoteIds) {
1993
+ this.checkpoint();
912
1994
  const footnote = this.footnoteRegistry.get(id);
913
1995
  if (footnote) {
914
- measureContainer.appendChild(footnote.cloneNode(true));
1996
+ const partial = partialById.get(id);
1997
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
915
1998
  }
916
1999
  }
917
- this.stagingElement.appendChild(measureContainer);
918
- const rect = measureContainer.getBoundingClientRect();
919
- const heightPt = pxToPt(rect.height);
920
- this.stagingElement.removeChild(measureContainer);
921
- return heightPt;
2000
+ this.noteMeasurementHost().appendChild(measureContainer);
2001
+ try {
2002
+ return pxToPt(measureContainer.getBoundingClientRect().height);
2003
+ } finally {
2004
+ measureContainer.remove();
2005
+ }
922
2006
  }
923
2007
  /**
924
- * Measures the height of just the continuation content (in points).
2008
+ * Partition a continuation for one page's note band, preferring complete children
2009
+ * and range-fragmenting an eligible paragraph when necessary. Always advances by
2010
+ * at least one element so an indivisible oversized paragraph follows the established
2011
+ * clipped fallback without trapping pagination in a loop.
925
2012
  */
926
- measureContinuationHeight(continuation, contentWidth) {
927
- if (!continuation || continuation.remainingElements.length === 0) {
928
- return 0;
929
- }
930
- const measureContainer = document.createElement("div");
2013
+ splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
2014
+ const fitting = [];
2015
+ let remaining = [];
2016
+ const measureContainer = this.document.createElement("div");
931
2017
  measureContainer.style.position = "absolute";
932
2018
  measureContainer.style.visibility = "hidden";
933
2019
  measureContainer.style.width = `${contentWidth}pt`;
934
2020
  measureContainer.style.left = "-9999px";
935
2021
  measureContainer.className = this.cssPrefix + "footnotes";
936
- const hr = document.createElement("hr");
937
- measureContainer.appendChild(hr);
938
- for (const el of continuation.remainingElements) {
939
- measureContainer.appendChild(el.cloneNode(true));
940
- }
941
- this.stagingElement.appendChild(measureContainer);
942
- const rect = measureContainer.getBoundingClientRect();
943
- const heightPt = pxToPt(rect.height);
944
- this.stagingElement.removeChild(measureContainer);
945
- return heightPt;
946
- }
947
- /**
948
- * Splits a footnote element into parts that fit within the available height.
949
- * Returns the elements that fit and the elements that need to continue.
950
- */
951
- splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
952
- const footnoteContent = footnoteElement.querySelector(".footnote-content");
953
- if (!footnoteContent) {
954
- return {
955
- fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
956
- overflow: []
957
- };
958
- }
959
- const children = Array.from(footnoteContent.children);
960
- if (children.length <= 1) {
961
- return {
962
- fits: children.map((el) => el.cloneNode(true)),
963
- overflow: []
964
- };
965
- }
966
- const fits = [];
967
- const overflow = [];
968
- let currentHeight = 0;
969
- const hrMeasure = document.createElement("div");
970
- hrMeasure.style.position = "absolute";
971
- hrMeasure.style.visibility = "hidden";
972
- hrMeasure.style.width = `${contentWidth}pt`;
973
- hrMeasure.style.left = "-9999px";
974
- hrMeasure.className = this.cssPrefix + "footnotes";
975
- const hr = document.createElement("hr");
976
- hrMeasure.appendChild(hr);
977
- this.stagingElement.appendChild(hrMeasure);
978
- const hrHeight = pxToPt(hrMeasure.getBoundingClientRect().height);
979
- this.stagingElement.removeChild(hrMeasure);
980
- currentHeight = hrHeight;
981
- const footnoteNumber = footnoteElement.querySelector(".footnote-number");
982
- for (let i = 0; i < children.length; i++) {
983
- const child = children[i];
984
- const measureContainer = document.createElement("div");
985
- measureContainer.style.position = "absolute";
986
- measureContainer.style.visibility = "hidden";
987
- measureContainer.style.width = `${contentWidth}pt`;
988
- measureContainer.style.left = "-9999px";
989
- measureContainer.className = this.cssPrefix + "footnotes";
990
- measureContainer.appendChild(child.cloneNode(true));
991
- this.stagingElement.appendChild(measureContainer);
992
- const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
993
- this.stagingElement.removeChild(measureContainer);
994
- if (currentHeight + childHeight <= availableHeightPt) {
995
- fits.push(child.cloneNode(true));
996
- currentHeight += childHeight;
997
- } else {
998
- for (let j = i; j < children.length; j++) {
999
- overflow.push(children[j].cloneNode(true));
2022
+ this.appendFootnoteSeparator(measureContainer, true);
2023
+ const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
2024
+ const content = wrapper.querySelector(":scope > .footnote-content");
2025
+ if (!content) {
2026
+ throw new Error("Footnote continuation is missing its content shell");
2027
+ }
2028
+ measureContainer.appendChild(wrapper);
2029
+ this.noteMeasurementHost().appendChild(measureContainer);
2030
+ try {
2031
+ for (let index = 0; index < continuation.remainingElements.length; index++) {
2032
+ this.checkpoint();
2033
+ const element = continuation.remainingElements[index];
2034
+ const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
2035
+ if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
2036
+ const sentinel = this.document.createElement("p");
2037
+ sentinel.className = "footnote-continuation-position-sentinel";
2038
+ sentinel.setAttribute("aria-hidden", "true");
2039
+ sentinel.style.setProperty("display", "none", "important");
2040
+ content.appendChild(sentinel);
2041
+ }
2042
+ const candidate = element.cloneNode(true);
2043
+ candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
2044
+ if (fitting.length === 0 && candidate.tagName === "P") {
2045
+ candidate.style.setProperty("display", "block", "important");
2046
+ }
2047
+ content.appendChild(candidate);
2048
+ const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
2049
+ if (candidateHeight <= availableHeightPt) {
2050
+ fitting.push(element);
2051
+ continue;
2052
+ }
2053
+ const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
2054
+ candidate.remove();
2055
+ let split = null;
2056
+ if (canSplitCandidate) {
2057
+ split = this.splitParagraphAtLargestFit(candidate, (head) => {
2058
+ content.appendChild(head);
2059
+ try {
2060
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2061
+ } finally {
2062
+ head.remove();
2063
+ }
2064
+ }, {
2065
+ // Both fallbacks below cut where ordinary line breaking would not,
2066
+ // so they are only ever right for an element that owns the whole
2067
+ // band: if this page already carries earlier content, a paragraph
2068
+ // that will not start here belongs intact in the next note band.
2069
+ emergencyGraphemeBreaks: fitting.length === 0,
2070
+ // A continuation page owns the full note band. If even one legal
2071
+ // grapheme is taller than it, clip only that unit and keep draining.
2072
+ forceFirstOnNoFit: fitting.length === 0
2073
+ });
2074
+ }
2075
+ if (split?.kind === "split") {
2076
+ if (sourcePosition) {
2077
+ split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
2078
+ split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
2079
+ }
2080
+ fitting.push(split.head);
2081
+ remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
2082
+ } else if (fitting.length === 0) {
2083
+ fitting.push(element);
2084
+ remaining = continuation.remainingElements.slice(index + 1);
2085
+ } else {
2086
+ remaining = continuation.remainingElements.slice(index);
1000
2087
  }
1001
2088
  break;
1002
2089
  }
2090
+ } finally {
2091
+ measureContainer.remove();
1003
2092
  }
1004
- return { fits, overflow };
2093
+ return {
2094
+ current: {
2095
+ footnoteId: continuation.footnoteId,
2096
+ sourceAnchorId: continuation.sourceAnchorId,
2097
+ remainingElements: fitting
2098
+ },
2099
+ overflow: remaining.length > 0 ? {
2100
+ footnoteId: continuation.footnoteId,
2101
+ sourceAnchorId: continuation.sourceAnchorId,
2102
+ remainingElements: remaining
2103
+ } : null
2104
+ };
1005
2105
  }
1006
2106
  /**
1007
- * Measures a single footnote's height.
2107
+ * Splits a footnote element into parts that fit within the available height.
2108
+ * Returns the elements that fit and the elements that need to continue.
1008
2109
  */
1009
- measureSingleFootnoteHeight(footnoteId, contentWidth) {
1010
- const footnote = this.footnoteRegistry.get(footnoteId);
1011
- if (!footnote) return 0;
1012
- const measureContainer = document.createElement("div");
2110
+ splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
2111
+ const layoutContext = this.document.createElement("div");
2112
+ layoutContext.style.position = "absolute";
2113
+ layoutContext.style.visibility = "hidden";
2114
+ layoutContext.style.width = `${contentWidth}pt`;
2115
+ layoutContext.style.left = "-9999px";
2116
+ layoutContext.className = this.cssPrefix + "footnotes";
2117
+ this.appendFootnoteSeparator(layoutContext, false);
2118
+ const attachedFootnote = footnoteElement.cloneNode(true);
2119
+ layoutContext.appendChild(attachedFootnote);
2120
+ this.noteMeasurementHost().appendChild(layoutContext);
2121
+ const measureContainer = this.document.createElement("div");
1013
2122
  measureContainer.style.position = "absolute";
1014
2123
  measureContainer.style.visibility = "hidden";
1015
2124
  measureContainer.style.width = `${contentWidth}pt`;
1016
2125
  measureContainer.style.left = "-9999px";
1017
2126
  measureContainer.className = this.cssPrefix + "footnotes";
1018
- measureContainer.appendChild(footnote.cloneNode(true));
1019
- this.stagingElement.appendChild(measureContainer);
1020
- const rect = measureContainer.getBoundingClientRect();
1021
- const heightPt = pxToPt(rect.height);
1022
- this.stagingElement.removeChild(measureContainer);
1023
- return heightPt;
2127
+ const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
2128
+ this.appendFootnoteSeparator(measureContainer, hasContinuation);
2129
+ if (hasContinuation) {
2130
+ measureContainer.appendChild(this.createFootnoteContinuationWrapper(
2131
+ existingPayload.continuation
2132
+ ));
2133
+ }
2134
+ const partialById = new Map(
2135
+ existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
2136
+ );
2137
+ for (const id of existingPayload?.footnoteIds ?? []) {
2138
+ this.checkpoint();
2139
+ const source = this.footnoteRegistry.get(id);
2140
+ if (!source) continue;
2141
+ const partial = partialById.get(id);
2142
+ measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
2143
+ }
2144
+ const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
2145
+ const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
2146
+ if (!measuredContent) {
2147
+ layoutContext.remove();
2148
+ throw new Error("Footnote is missing its content shell");
2149
+ }
2150
+ measureContainer.appendChild(measuredPartial);
2151
+ this.noteMeasurementHost().appendChild(measureContainer);
2152
+ try {
2153
+ const footnoteContent = attachedFootnote.querySelector(".footnote-content");
2154
+ if (!footnoteContent) {
2155
+ return {
2156
+ fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
2157
+ overflow: []
2158
+ };
2159
+ }
2160
+ const children = Array.from(footnoteContent.children);
2161
+ const fits = [];
2162
+ let overflow = [];
2163
+ for (let i = 0; i < children.length; i++) {
2164
+ this.checkpoint();
2165
+ const child = children[i];
2166
+ const measuredCandidate = child.cloneNode(true);
2167
+ measuredContent.appendChild(measuredCandidate);
2168
+ const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2169
+ if (candidateFits) {
2170
+ fits.push(child.cloneNode(true));
2171
+ continue;
2172
+ }
2173
+ measuredCandidate.remove();
2174
+ const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
2175
+ measuredContent.appendChild(head);
2176
+ try {
2177
+ return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
2178
+ } finally {
2179
+ head.remove();
2180
+ }
2181
+ }, {
2182
+ // Both fallbacks cut where ordinary line breaking would not, so they
2183
+ // are only ever right for an element that owns the whole band: with
2184
+ // earlier siblings already packed here, a paragraph that will not
2185
+ // start belongs intact in the next note band.
2186
+ emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
2187
+ forceFirstOnNoFit: fits.length === 0 && forceProgress
2188
+ }) : null;
2189
+ if (split?.kind === "split") {
2190
+ fits.push(split.head);
2191
+ split.tail.setAttribute(
2192
+ FOOTNOTE_SOURCE_POSITION_ATTR,
2193
+ i === 0 ? "first" : "later"
2194
+ );
2195
+ overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
2196
+ } else if (split?.kind === "no-fit") {
2197
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
2198
+ } else if (fits.length === 0 && forceProgress) {
2199
+ fits.push(child.cloneNode(true));
2200
+ overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
2201
+ } else {
2202
+ overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
2203
+ }
2204
+ break;
2205
+ }
2206
+ return { fits, overflow };
2207
+ } finally {
2208
+ measureContainer.remove();
2209
+ layoutContext.remove();
2210
+ }
1024
2211
  }
1025
2212
  /**
1026
2213
  * Adds footnotes to a page container, including continuation content.
@@ -1033,12 +2220,11 @@ var DocxodusEditor = (() => {
1033
2220
  if (this.footnoteRegistry.size === 0 && !hasContinuation) {
1034
2221
  return;
1035
2222
  }
1036
- const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
1037
2223
  const maxFootnoteHeight = Math.min(
1038
2224
  footnoteHeight,
1039
2225
  bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
1040
2226
  );
1041
- const footnotesDiv = document.createElement("div");
2227
+ const footnotesDiv = this.document.createElement("div");
1042
2228
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
1043
2229
  footnotesDiv.style.position = "absolute";
1044
2230
  footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
@@ -1047,35 +2233,18 @@ var DocxodusEditor = (() => {
1047
2233
  footnotesDiv.style.boxSizing = "border-box";
1048
2234
  footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
1049
2235
  footnotesDiv.style.overflow = "hidden";
1050
- const hr = document.createElement("hr");
1051
- footnotesDiv.appendChild(hr);
2236
+ this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
1052
2237
  if (hasContinuation) {
1053
- const contWrapper = document.createElement("div");
1054
- contWrapper.className = "footnote-continuation";
1055
- for (const el of continuation.remainingElements) {
1056
- contWrapper.appendChild(el.cloneNode(true));
1057
- }
1058
- footnotesDiv.appendChild(contWrapper);
2238
+ footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
1059
2239
  }
1060
2240
  for (const id of footnoteIds) {
1061
2241
  const partial = partialFootnotes?.find((p) => p.footnoteId === id);
1062
2242
  if (partial) {
1063
2243
  const footnote = this.footnoteRegistry.get(id);
1064
2244
  if (footnote) {
1065
- const partialDiv = document.createElement("div");
1066
- partialDiv.className = "footnote-item";
1067
- partialDiv.dataset.footnoteId = id;
1068
- const numberSpan = footnote.querySelector(".footnote-number");
1069
- if (numberSpan) {
1070
- partialDiv.appendChild(numberSpan.cloneNode(true));
1071
- }
1072
- const contentSpan = document.createElement("span");
1073
- contentSpan.className = "footnote-content";
1074
- for (const el of partial.fittingElements) {
1075
- contentSpan.appendChild(el.cloneNode(true));
1076
- }
1077
- partialDiv.appendChild(contentSpan);
1078
- footnotesDiv.appendChild(partialDiv);
2245
+ footnotesDiv.appendChild(
2246
+ this.createPartialFootnoteItem(footnote, partial.fittingElements)
2247
+ );
1079
2248
  }
1080
2249
  } else {
1081
2250
  const footnote = this.footnoteRegistry.get(id);
@@ -1086,30 +2255,26 @@ var DocxodusEditor = (() => {
1086
2255
  }
1087
2256
  pageBox.appendChild(footnotesDiv);
1088
2257
  }
1089
- /**
1090
- * Selects the appropriate header for a page based on section, page position, and page number.
1091
- */
1092
- selectHeader(sectionIndex, pageInSection, globalPageNumber) {
2258
+ /** Select the section's first/odd/even header from its one-based page position. */
2259
+ selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
1093
2260
  const sectionHf = this.hfRegistry.get(sectionIndex);
1094
2261
  if (!sectionHf) return void 0;
1095
2262
  if (pageInSection === 1 && sectionHf.headerFirst) {
1096
2263
  return sectionHf.headerFirst;
1097
2264
  }
1098
- if (globalPageNumber % 2 === 0 && sectionHf.headerEven) {
2265
+ if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
1099
2266
  return sectionHf.headerEven;
1100
2267
  }
1101
2268
  return sectionHf.headerDefault;
1102
2269
  }
1103
- /**
1104
- * Selects the appropriate footer for a page based on section, page position, and page number.
1105
- */
1106
- selectFooter(sectionIndex, pageInSection, globalPageNumber) {
2270
+ /** Select the section's first/odd/even footer from its one-based page position. */
2271
+ selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
1107
2272
  const sectionHf = this.hfRegistry.get(sectionIndex);
1108
2273
  if (!sectionHf) return void 0;
1109
2274
  if (pageInSection === 1 && sectionHf.footerFirst) {
1110
2275
  return sectionHf.footerFirst;
1111
2276
  }
1112
- if (globalPageNumber % 2 === 0 && sectionHf.footerEven) {
2277
+ if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
1113
2278
  return sectionHf.footerEven;
1114
2279
  }
1115
2280
  return sectionHf.footerDefault;
@@ -1124,7 +2289,7 @@ var DocxodusEditor = (() => {
1124
2289
  * Deterministic: it depends only on the section's page setup and the registry's pre-measured
1125
2290
  * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
1126
2291
  */
1127
- getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
2292
+ getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
1128
2293
  const sectionHf = this.hfRegistry.get(sectionIndex);
1129
2294
  return resolvePageBands(
1130
2295
  dims,
@@ -1133,14 +2298,14 @@ var DocxodusEditor = (() => {
1133
2298
  sectionHf?.headerEvenHeight,
1134
2299
  sectionHf?.headerDefaultHeight,
1135
2300
  pageInSection,
1136
- globalPageNumber
2301
+ displayedPageNumber
1137
2302
  ),
1138
2303
  this.selectStoryHeight(
1139
2304
  sectionHf?.footerFirstHeight,
1140
2305
  sectionHf?.footerEvenHeight,
1141
2306
  sectionHf?.footerDefaultHeight,
1142
2307
  pageInSection,
1143
- globalPageNumber
2308
+ displayedPageNumber
1144
2309
  )
1145
2310
  );
1146
2311
  }
@@ -1148,9 +2313,9 @@ var DocxodusEditor = (() => {
1148
2313
  * The measured height of the running story this page position selects, mirroring
1149
2314
  * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1150
2315
  */
1151
- selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
2316
+ selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
1152
2317
  if (pageInSection === 1 && first != null) return first;
1153
- if (globalPageNumber % 2 === 0 && even != null) return even;
2318
+ if (displayedPageNumber % 2 === 0 && even != null) return even;
1154
2319
  return fallback ?? 0;
1155
2320
  }
1156
2321
  /**
@@ -1162,7 +2327,7 @@ var DocxodusEditor = (() => {
1162
2327
  * silently starts overlapping body text.
1163
2328
  */
1164
2329
  measureHeaderFooterHeight(source, contentWidth) {
1165
- const measureContainer = document.createElement("div");
2330
+ const measureContainer = this.document.createElement("div");
1166
2331
  measureContainer.style.position = "absolute";
1167
2332
  measureContainer.style.visibility = "hidden";
1168
2333
  measureContainer.style.width = `${contentWidth}pt`;
@@ -1181,79 +2346,275 @@ var DocxodusEditor = (() => {
1181
2346
  * Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
1182
2347
  * Supports footnote continuation - long footnotes can split across pages.
1183
2348
  */
1184
- flowToPages(blocks, dims, startPageNumber, sectionIndex) {
2349
+ flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
1185
2350
  const pages = [];
1186
2351
  let currentContent = [];
1187
2352
  let pageNumber = startPageNumber;
1188
- let pageInSection = 1;
2353
+ let pageSectionIndex = sectionIndex;
2354
+ const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
2355
+ const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
2356
+ const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
2357
+ const markSectionPlaced = (owner) => {
2358
+ if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
2359
+ };
2360
+ const previousBox = this.containerElement.querySelector(
2361
+ `.${this.cssPrefix}box:last-of-type`
2362
+ );
2363
+ let precedingDisplayedPageNumber = parseInt(
2364
+ previousBox?.dataset.displayedPageNumber ?? "0",
2365
+ 10
2366
+ );
2367
+ const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
2368
+ const numbering = this.pageNumbering.get(owner) ?? {};
2369
+ if (numbering.start !== void 0) {
2370
+ return numbering.start + ownerPageInSection - 1;
2371
+ }
2372
+ const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
2373
+ const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
2374
+ return currentPhysicalNumber + physicalPage - pageNumber;
2375
+ };
1189
2376
  let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1190
- dims,
1191
- sectionIndex,
1192
- pageInSection,
1193
- pageNumber
2377
+ dimensionsFor(),
2378
+ pageSectionIndex,
2379
+ pageInSection(),
2380
+ displayedPageNumber()
1194
2381
  );
1195
2382
  let remainingHeight = effectiveContentHeight;
1196
2383
  let prevMarginBottomPt = 0;
1197
2384
  let currentFootnoteIds = [];
2385
+ let currentPageHasFootnoteReference = false;
1198
2386
  let currentFootnoteHeight = 0;
1199
2387
  let currentContinuation = this.pendingFootnoteContinuation;
1200
2388
  let nextPageContinuation = null;
2389
+ let currentContinuationPartitioned = false;
2390
+ let currentPageAdmitted = false;
1201
2391
  let deferredFootnoteIds = [];
1202
2392
  let currentPartialFootnotes = [];
1203
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1204
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1205
- }
1206
- const finishPage = () => {
2393
+ const adoptEmptyPageOwner = (owner) => {
2394
+ pageSectionIndex = owner;
2395
+ const bands = this.getPageBands(
2396
+ dimensionsFor(owner),
2397
+ owner,
2398
+ pageInSection(owner),
2399
+ displayedPageNumber(owner)
2400
+ );
2401
+ effectiveContentHeight = bands.bodyHeight;
2402
+ remainingHeight = effectiveContentHeight;
2403
+ };
2404
+ const admitCurrentPage = () => {
2405
+ if (currentPageAdmitted) return;
2406
+ this.admitPageAllocation();
2407
+ currentPageAdmitted = true;
2408
+ };
2409
+ const prepareCurrentContinuation = () => {
2410
+ if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
2411
+ admitCurrentPage();
2412
+ const ownedDimensions = dimensionsFor();
2413
+ const pageBands = this.getPageBands(
2414
+ ownedDimensions,
2415
+ pageSectionIndex,
2416
+ pageInSection(),
2417
+ displayedPageNumber()
2418
+ );
2419
+ const partition = this.splitContinuationForPage(
2420
+ currentContinuation,
2421
+ pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
2422
+ ownedDimensions.contentWidth
2423
+ );
2424
+ currentContinuation = partition.current;
2425
+ if (partition.overflow) nextPageContinuation = partition.overflow;
2426
+ currentContinuationPartitioned = true;
2427
+ currentFootnoteHeight = this.measureFootnotesHeight(
2428
+ currentFootnoteIds,
2429
+ ownedDimensions.contentWidth,
2430
+ currentContinuation,
2431
+ currentPartialFootnotes
2432
+ );
2433
+ };
2434
+ const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
1207
2435
  const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
1208
- if (currentContent.length === 0 && !hasCurrentContinuation) return;
2436
+ if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
2437
+ adoptEmptyPageOwner(nextPageSectionIndex);
2438
+ return;
2439
+ }
2440
+ prepareCurrentContinuation();
2441
+ admitCurrentPage();
2442
+ let pageContinuation = currentContinuation;
2443
+ const ownedPageInSection = pageInSection();
2444
+ const ownedDimensions = dimensionsFor();
2445
+ const ownedDisplayedPageNumber = displayedPageNumber(
2446
+ pageSectionIndex,
2447
+ ownedPageInSection
2448
+ );
2449
+ const pageBands = this.getPageBands(
2450
+ ownedDimensions,
2451
+ pageSectionIndex,
2452
+ ownedPageInSection,
2453
+ ownedDisplayedPageNumber
2454
+ );
2455
+ const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2456
+ pageContinuation = currentContinuation;
2457
+ currentFootnoteHeight = this.measureFootnotesHeight(
2458
+ currentFootnoteIds,
2459
+ ownedDimensions.contentWidth,
2460
+ pageContinuation,
2461
+ currentPartialFootnotes
2462
+ );
2463
+ if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
2464
+ const fittingIds = [];
2465
+ for (let index = 0; index < currentFootnoteIds.length; index++) {
2466
+ const footnoteId = currentFootnoteIds[index];
2467
+ const candidateIds = [...fittingIds, footnoteId];
2468
+ const candidateHeight = this.measureFootnotesHeight(
2469
+ candidateIds,
2470
+ ownedDimensions.contentWidth,
2471
+ pageContinuation
2472
+ );
2473
+ const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
2474
+ if (guardedCandidateHeight <= maxFootnoteHeight) {
2475
+ fittingIds.push(footnoteId);
2476
+ currentFootnoteHeight = guardedCandidateHeight;
2477
+ continue;
2478
+ }
2479
+ const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
2480
+ if (fittingIds.length === 0 && !hasPageContinuation) {
2481
+ const source = this.footnoteRegistry.get(footnoteId);
2482
+ const split = source ? this.splitFootnoteToFit(
2483
+ source,
2484
+ maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
2485
+ ownedDimensions.contentWidth,
2486
+ true
2487
+ ) : null;
2488
+ fittingIds.push(footnoteId);
2489
+ if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
2490
+ currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
2491
+ nextPageContinuation = {
2492
+ footnoteId,
2493
+ sourceAnchorId: source.dataset.sourceAnchorId,
2494
+ remainingElements: split.overflow
2495
+ };
2496
+ currentFootnoteHeight = maxFootnoteHeight;
2497
+ } else {
2498
+ currentFootnoteHeight = guardedCandidateHeight;
2499
+ }
2500
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
2501
+ } else {
2502
+ deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
2503
+ }
2504
+ break;
2505
+ }
2506
+ currentFootnoteIds = fittingIds;
2507
+ }
1209
2508
  const page = this.createPage(
1210
- dims,
2509
+ ownedDimensions,
1211
2510
  pageNumber,
1212
- sectionIndex,
2511
+ pageSectionIndex,
2512
+ ownedDisplayedPageNumber,
1213
2513
  currentContent,
1214
- pageInSection,
2514
+ ownedPageInSection,
1215
2515
  currentFootnoteIds,
1216
2516
  currentFootnoteHeight,
1217
- currentContinuation,
1218
- currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
2517
+ pageContinuation,
2518
+ currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
2519
+ false,
2520
+ currentPageAdmitted
1219
2521
  );
1220
2522
  pages.push(page);
2523
+ precedingDisplayedPageNumber = ownedDisplayedPageNumber;
1221
2524
  pageNumber++;
1222
- pageInSection++;
1223
2525
  currentContent = [];
1224
- const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2526
+ pageSectionIndex = nextPageSectionIndex;
2527
+ if (!sectionStartPages.has(pageSectionIndex)) {
2528
+ sectionStartPages.set(pageSectionIndex, pageNumber);
2529
+ }
2530
+ const newBands = this.getPageBands(
2531
+ dimensionsFor(),
2532
+ pageSectionIndex,
2533
+ pageInSection(),
2534
+ displayedPageNumber()
2535
+ );
1225
2536
  effectiveContentHeight = newBands.bodyHeight;
1226
2537
  remainingHeight = effectiveContentHeight;
1227
2538
  prevMarginBottomPt = 0;
1228
2539
  currentFootnoteIds = [];
2540
+ currentPageHasFootnoteReference = false;
1229
2541
  currentPartialFootnotes = [];
1230
2542
  currentContinuation = nextPageContinuation;
1231
2543
  nextPageContinuation = null;
2544
+ currentContinuationPartitioned = false;
2545
+ currentPageAdmitted = false;
1232
2546
  if (deferredFootnoteIds.length > 0) {
1233
2547
  currentFootnoteIds = [...deferredFootnoteIds];
1234
2548
  deferredFootnoteIds = [];
1235
2549
  }
1236
- if (currentContinuation && currentContinuation.remainingElements.length > 0) {
1237
- currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
1238
- } else {
1239
- currentFootnoteHeight = 0;
1240
- }
1241
- if (currentFootnoteIds.length > 0) {
1242
- currentFootnoteHeight += this.measureFootnotesHeight(
1243
- currentFootnoteIds,
1244
- dims.contentWidth,
1245
- null
1246
- );
1247
- }
2550
+ const nextDimensions = dimensionsFor();
2551
+ currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
1248
2552
  };
2553
+ const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
1249
2554
  for (let i = 0; i < blocks.length; i++) {
2555
+ this.checkpoint();
1250
2556
  const block = blocks[i];
2557
+ const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
2558
+ if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
2559
+ finishPage(block.sectionIndex);
2560
+ }
2561
+ const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
2562
+ if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
2563
+ adoptEmptyPageOwner(block.sectionIndex);
2564
+ }
2565
+ prepareCurrentContinuation();
2566
+ if (currentFootnoteHeight > 0) {
2567
+ const ownerDimensions = dimensionsFor();
2568
+ const ownerBands = this.getPageBands(
2569
+ ownerDimensions,
2570
+ pageSectionIndex,
2571
+ pageInSection(),
2572
+ displayedPageNumber()
2573
+ );
2574
+ const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2575
+ if (currentFootnoteHeight > ownerMaxFootnoteArea) {
2576
+ const fittingIds = [];
2577
+ let fittingHeight = 0;
2578
+ if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
2579
+ for (const footnoteId of currentFootnoteIds) {
2580
+ this.checkpoint();
2581
+ const candidateHeight = this.measureFootnotesHeight(
2582
+ [...fittingIds, footnoteId],
2583
+ ownerDimensions.contentWidth
2584
+ );
2585
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
2586
+ fittingIds.push(footnoteId);
2587
+ fittingHeight = candidateHeight;
2588
+ }
2589
+ }
2590
+ if (fittingIds.length > 0) {
2591
+ deferredFootnoteIds = [
2592
+ ...currentFootnoteIds.slice(fittingIds.length),
2593
+ ...deferredFootnoteIds
2594
+ ];
2595
+ currentFootnoteIds = fittingIds;
2596
+ currentFootnoteHeight = fittingHeight;
2597
+ } else {
2598
+ finishPage(block.sectionIndex);
2599
+ i--;
2600
+ continue;
2601
+ }
2602
+ }
2603
+ }
2604
+ const blockDimensions = dimensionsFor(block.sectionIndex);
2605
+ const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
2606
+ const freshBlockPageBodyHeight = this.getPageBands(
2607
+ blockDimensions,
2608
+ block.sectionIndex,
2609
+ nextBlockPageInSection,
2610
+ displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
2611
+ ).bodyHeight;
1251
2612
  if (block.isPageBreak) {
1252
- finishPage();
2613
+ finishPage(block.sectionIndex);
1253
2614
  continue;
1254
2615
  }
1255
2616
  if (block.pageBreakBefore && currentContent.length > 0) {
1256
- finishPage();
2617
+ finishPage(block.sectionIndex);
1257
2618
  }
1258
2619
  const previousBlock = blocks[i - 1];
1259
2620
  const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
@@ -1269,7 +2630,7 @@ var DocxodusEditor = (() => {
1269
2630
  if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1270
2631
  const totalChainFootnoteHeight = this.measureFootnotesHeight(
1271
2632
  [...currentFootnoteIds, ...newChainFootnoteIds],
1272
- dims.contentWidth,
2633
+ blockDimensions.contentWidth,
1273
2634
  currentContinuation
1274
2635
  );
1275
2636
  additionalChainFootnoteHeight = Math.max(
@@ -1280,58 +2641,69 @@ var DocxodusEditor = (() => {
1280
2641
  const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
1281
2642
  keepChain,
1282
2643
  prevMarginBottomPt,
1283
- currentContent.length === 0
2644
+ currentContent.length === 0,
2645
+ pageInSection(block.sectionIndex)
1284
2646
  ) + additionalChainFootnoteHeight;
1285
2647
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1286
2648
  if (currentChainHeight > currentAvailableHeight) {
1287
2649
  const nextPageBands = this.getPageBands(
1288
- dims,
1289
- sectionIndex,
1290
- pageInSection + 1,
1291
- pageNumber + 1
2650
+ dimensionsFor(block.sectionIndex),
2651
+ block.sectionIndex,
2652
+ pageInSection(block.sectionIndex, pageNumber + 1),
2653
+ displayedPageNumber(
2654
+ block.sectionIndex,
2655
+ pageInSection(block.sectionIndex, pageNumber + 1),
2656
+ pageNumber + 1
2657
+ )
1292
2658
  );
1293
2659
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
1294
2660
  keepChain,
1295
2661
  0,
1296
- true
2662
+ true,
2663
+ pageInSection(block.sectionIndex, pageNumber + 1)
1297
2664
  );
1298
2665
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
1299
2666
  newChainFootnoteIds,
1300
- dims.contentWidth,
2667
+ blockDimensions.contentWidth,
1301
2668
  nextPageContinuation
1302
2669
  );
1303
2670
  if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1304
- finishPage();
2671
+ finishPage(block.sectionIndex);
1305
2672
  }
1306
2673
  }
1307
2674
  }
1308
2675
  }
1309
- const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
1310
2676
  const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
2677
+ let combinedFootnoteHeight = currentFootnoteHeight;
1311
2678
  let additionalFootnoteHeight = 0;
1312
2679
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
1313
2680
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
1314
- const totalFootnoteHeight = this.measureFootnotesHeight(
2681
+ combinedFootnoteHeight = this.measureFootnotesHeight(
1315
2682
  combinedFootnoteIds,
1316
- dims.contentWidth,
1317
- currentContinuation
2683
+ blockDimensions.contentWidth,
2684
+ currentContinuation,
2685
+ currentPartialFootnotes
2686
+ );
2687
+ additionalFootnoteHeight = Math.max(
2688
+ 0,
2689
+ combinedFootnoteHeight - currentFootnoteHeight
1318
2690
  );
1319
- additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
1320
2691
  }
1321
2692
  const isFirstOnPage = currentContent.length === 0;
1322
- let effectiveMarginTop = block.marginTopPt;
1323
- if (!isFirstOnPage) {
1324
- effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
1325
- }
2693
+ const effectiveMarginTop = this.effectiveBlockMarginTop(
2694
+ block,
2695
+ prevMarginBottomPt,
2696
+ isFirstOnPage,
2697
+ pageInSection(block.sectionIndex)
2698
+ );
1326
2699
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
1327
2700
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
1328
2701
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
1329
2702
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
1330
- const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
1331
2703
  if (blockSpace > effectiveRemainingHeight) {
1332
2704
  const paragraphFragments = this.tryFragmentParagraph(
1333
2705
  block,
1334
- dims,
2706
+ blockDimensions,
1335
2707
  effectiveRemainingHeight,
1336
2708
  effectiveMarginTop
1337
2709
  );
@@ -1341,40 +2713,74 @@ var DocxodusEditor = (() => {
1341
2713
  continue;
1342
2714
  }
1343
2715
  }
1344
- if (blockSpace <= effectiveRemainingHeight) {
1345
- currentContent.push(block.element.cloneNode(true));
2716
+ if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
2717
+ markSectionPlaced(block.sectionIndex);
2718
+ currentContent.push(this.cloneBlockForPage(
2719
+ block,
2720
+ isFirstOnPage,
2721
+ pageInSection(block.sectionIndex)
2722
+ ));
1346
2723
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1347
2724
  prevMarginBottomPt = block.marginBottomPt;
1348
2725
  if (newFootnoteIds.length > 0) {
1349
2726
  currentFootnoteIds.push(...newFootnoteIds);
1350
- currentFootnoteHeight += additionalFootnoteHeight;
2727
+ currentFootnoteHeight = combinedFootnoteHeight;
1351
2728
  }
1352
- } else if (block.heightPt + block.marginTopPt <= effectiveContentHeight) {
2729
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2730
+ } else if (block.heightPt + this.effectiveBlockMarginTop(
2731
+ block,
2732
+ 0,
2733
+ true,
2734
+ pageInSection(block.sectionIndex, pageNumber + 1)
2735
+ ) <= freshBlockPageBodyHeight) {
1353
2736
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
1354
- const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
1355
- const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
1356
2737
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
1357
- currentContent.push(block.element.cloneNode(true));
2738
+ markSectionPlaced(block.sectionIndex);
2739
+ currentContent.push(this.cloneBlockForPage(
2740
+ block,
2741
+ isFirstOnPage,
2742
+ pageInSection(block.sectionIndex)
2743
+ ));
1358
2744
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1359
2745
  prevMarginBottomPt = block.marginBottomPt;
1360
2746
  const availableForFootnotes = Math.min(
1361
2747
  maxFootnoteArea,
1362
2748
  effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
1363
2749
  );
1364
- for (const footnoteId of newFootnoteIds) {
2750
+ const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
2751
+ const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
2752
+ for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
2753
+ this.checkpoint();
2754
+ const footnoteId = newFootnoteIds[noteIndex];
1365
2755
  const footnote = this.footnoteRegistry.get(footnoteId);
1366
2756
  if (!footnote) continue;
1367
- const footnoteHeight = this.measureSingleFootnoteHeight(footnoteId, dims.contentWidth);
2757
+ if (nextPageContinuation) {
2758
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2759
+ break;
2760
+ }
2761
+ const candidateIds = [...currentFootnoteIds, footnoteId];
2762
+ const candidateHeight = this.measureFootnotesHeight(
2763
+ candidateIds,
2764
+ blockDimensions.contentWidth,
2765
+ currentContinuation,
2766
+ currentPartialFootnotes
2767
+ );
1368
2768
  const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
1369
- if (footnoteHeight <= spaceLeftForFootnotes) {
2769
+ if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
1370
2770
  currentFootnoteIds.push(footnoteId);
1371
- currentFootnoteHeight += footnoteHeight;
2771
+ currentFootnoteHeight = candidateHeight;
1372
2772
  } else {
1373
2773
  if (spaceLeftForFootnotes > 20) {
1374
2774
  const { fits, overflow } = this.splitFootnoteToFit(
1375
2775
  footnote,
1376
- spaceLeftForFootnotes,
1377
- dims.contentWidth
2776
+ availableForFootnotes,
2777
+ blockDimensions.contentWidth,
2778
+ deferralCannotHelp,
2779
+ {
2780
+ footnoteIds: currentFootnoteIds,
2781
+ continuation: currentContinuation,
2782
+ partialFootnotes: currentPartialFootnotes
2783
+ }
1378
2784
  );
1379
2785
  if (fits.length > 0) {
1380
2786
  currentFootnoteIds.push(footnoteId);
@@ -1385,66 +2791,70 @@ var DocxodusEditor = (() => {
1385
2791
  if (overflow.length > 0) {
1386
2792
  nextPageContinuation = {
1387
2793
  footnoteId,
2794
+ sourceAnchorId: footnote.dataset.sourceAnchorId,
1388
2795
  remainingElements: overflow
1389
2796
  };
1390
2797
  }
1391
- currentFootnoteHeight = availableForFootnotes;
2798
+ currentFootnoteHeight = this.measureFootnotesHeight(
2799
+ currentFootnoteIds,
2800
+ blockDimensions.contentWidth,
2801
+ currentContinuation,
2802
+ currentPartialFootnotes
2803
+ );
2804
+ if (overflow.length > 0) {
2805
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
2806
+ break;
2807
+ }
1392
2808
  } else {
1393
- deferredFootnoteIds.push(footnoteId);
2809
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2810
+ break;
1394
2811
  }
1395
2812
  } else {
1396
- deferredFootnoteIds.push(footnoteId);
2813
+ deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
2814
+ break;
1397
2815
  }
1398
2816
  }
1399
2817
  }
1400
- } else if (canExpandFootnotes && newFootnoteIds.length > 0) {
1401
- const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
1402
- const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
1403
- if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
1404
- currentContent.push(block.element.cloneNode(true));
1405
- remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
1406
- prevMarginBottomPt = block.marginBottomPt;
1407
- currentFootnoteIds.push(...newFootnoteIds);
1408
- currentFootnoteHeight = expandedFootnoteSpace;
1409
- } else {
1410
- finishPage();
1411
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1412
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1413
- currentContent.push(block.element.cloneNode(true));
1414
- remainingHeight = effectiveContentHeight - newPageSpace;
1415
- prevMarginBottomPt = block.marginBottomPt;
1416
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1417
- currentFootnoteHeight = newPageFootnoteHeight;
1418
- }
2818
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
1419
2819
  } else {
1420
- finishPage();
1421
- const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1422
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1423
- currentContent.push(block.element.cloneNode(true));
1424
- remainingHeight = effectiveContentHeight - newPageSpace;
1425
- prevMarginBottomPt = block.marginBottomPt;
1426
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1427
- currentFootnoteHeight = newPageFootnoteHeight;
2820
+ finishPage(block.sectionIndex, !pageHasPayload());
2821
+ i--;
2822
+ continue;
1428
2823
  }
1429
2824
  } else {
1430
- const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
2825
+ const tableFragments = this.trySplitSimpleOversizedTable(
2826
+ block,
2827
+ dimensionsFor(block.sectionIndex),
2828
+ block.sectionIndex
2829
+ );
1431
2830
  if (tableFragments) {
1432
2831
  if (currentContent.length > 0 || currentContinuation) {
1433
- finishPage();
2832
+ finishPage(block.sectionIndex);
1434
2833
  }
1435
2834
  blocks.splice(i, 1, ...tableFragments);
1436
2835
  i--;
1437
2836
  continue;
1438
2837
  }
1439
- if (currentContent.length > 0) {
1440
- finishPage();
2838
+ if (pageHasPayload()) {
2839
+ finishPage(block.sectionIndex);
2840
+ i--;
2841
+ continue;
1441
2842
  }
1442
- currentContent.push(block.element.cloneNode(true));
1443
- currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1444
- finishPage();
2843
+ markSectionPlaced(block.sectionIndex);
2844
+ currentContent.push(this.cloneBlockForPage(
2845
+ block,
2846
+ true,
2847
+ pageInSection(block.sectionIndex)
2848
+ ));
2849
+ deferredFootnoteIds.push(...newFootnoteIds);
2850
+ currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
2851
+ finishPage(block.sectionIndex);
1445
2852
  }
1446
2853
  }
1447
2854
  finishPage();
2855
+ while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
2856
+ finishPage();
2857
+ }
1448
2858
  this.pendingFootnoteContinuation = nextPageContinuation;
1449
2859
  return pages;
1450
2860
  }
@@ -1469,6 +2879,19 @@ var DocxodusEditor = (() => {
1469
2879
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1470
2880
  }
1471
2881
  }
2882
+ /** A repeated margin note is presentation, not a second bookmark/link target. */
2883
+ makeClonedMarginCommentInert(root) {
2884
+ this.makeClonedStoryInert(root);
2885
+ const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
2886
+ for (const element of nodes) {
2887
+ element.removeAttribute("id");
2888
+ if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
2889
+ element.removeAttribute("href");
2890
+ element.setAttribute("aria-disabled", "true");
2891
+ element.tabIndex = -1;
2892
+ }
2893
+ }
2894
+ }
1472
2895
  /**
1473
2896
  * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1474
2897
  *
@@ -1494,7 +2917,7 @@ var DocxodusEditor = (() => {
1494
2917
  const context = {
1495
2918
  staticLeft: toPageX(staticRect.left),
1496
2919
  staticTop: toPageY(staticRect.top),
1497
- lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2920
+ lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
1498
2921
  paragraphLeft: toPageX(paragraphRect.left),
1499
2922
  paragraphTop: toPageY(paragraphRect.top),
1500
2923
  paragraphWidth: paragraphRect.width / pixelsPerPoint,
@@ -1624,36 +3047,48 @@ var DocxodusEditor = (() => {
1624
3047
  else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1625
3048
  return reference.start + (reference.size - objectSize) * fraction;
1626
3049
  }
3050
+ /** Charge a physical page before any page-owned partitioning or DOM allocation. */
3051
+ admitPageAllocation() {
3052
+ const prospectivePageCount = this.createdPageCount + 1;
3053
+ this.pageCountCheckpoint?.(prospectivePageCount);
3054
+ this.createdPageCount = prospectivePageCount;
3055
+ }
1627
3056
  /**
1628
3057
  * Creates a page container element.
1629
3058
  */
1630
- createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
1631
- const pageBox = document.createElement("div");
3059
+ createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
3060
+ if (!pageAlreadyAdmitted) this.admitPageAllocation();
3061
+ const pageBox = this.document.createElement("div");
1632
3062
  pageBox.className = `${this.cssPrefix}box`;
1633
3063
  pageBox.style.width = `${dims.pageWidth}pt`;
1634
3064
  pageBox.style.height = `${dims.pageHeight}pt`;
1635
3065
  pageBox.style.overflow = "hidden";
1636
3066
  pageBox.style.position = "relative";
1637
3067
  if (this.scale !== 1) {
1638
- pageBox.style.zoom = String(this.scale);
1639
- pageBox.style.transform = `scale(${this.scale})`;
1640
- pageBox.style.transformOrigin = "top left";
1641
- const heightReductionPt = dims.pageHeight * (1 - this.scale);
1642
- const widthReductionPt = dims.pageWidth * (1 - this.scale);
1643
- const heightReductionPx = ptToPx(heightReductionPt);
1644
- const widthReductionPx = ptToPx(widthReductionPt);
1645
- pageBox.style.marginRight = `-${widthReductionPx}px`;
1646
- pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
3068
+ if (this.view.CSS?.supports("zoom", "1")) {
3069
+ pageBox.style.zoom = String(this.scale);
3070
+ } else {
3071
+ pageBox.style.transform = `scale(${this.scale})`;
3072
+ pageBox.style.transformOrigin = "top left";
3073
+ const heightReductionPt = dims.pageHeight * (1 - this.scale);
3074
+ const widthReductionPt = dims.pageWidth * (1 - this.scale);
3075
+ const heightReductionPx = ptToPx(heightReductionPt);
3076
+ const widthReductionPx = ptToPx(widthReductionPt);
3077
+ pageBox.style.marginRight = `-${widthReductionPx}px`;
3078
+ pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
3079
+ }
1647
3080
  }
1648
3081
  pageBox.style.willChange = "transform";
1649
3082
  pageBox.style.contain = "layout paint";
1650
3083
  pageBox.dataset.pageNumber = String(pageNumber);
1651
3084
  pageBox.dataset.sectionIndex = String(sectionIndex);
3085
+ pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
3086
+ if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
1652
3087
  pageBox.dataset.pageInSection = String(pageInSection);
1653
- const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1654
- const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
3088
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
3089
+ const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
1655
3090
  if (headerSource) {
1656
- const headerDiv = document.createElement("div");
3091
+ const headerDiv = this.document.createElement("div");
1657
3092
  headerDiv.className = `${this.cssPrefix}header`;
1658
3093
  headerDiv.style.position = "absolute";
1659
3094
  headerDiv.style.top = `${bands.headerTop}pt`;
@@ -1675,7 +3110,7 @@ var DocxodusEditor = (() => {
1675
3110
  }
1676
3111
  const contentAreaTop = bands.bodyTop;
1677
3112
  const contentAreaHeight = bands.bodyHeight;
1678
- const contentArea = document.createElement("div");
3113
+ const contentArea = this.document.createElement("div");
1679
3114
  contentArea.className = `${this.cssPrefix}content`;
1680
3115
  contentArea.style.position = "absolute";
1681
3116
  contentArea.style.top = `${contentAreaTop}pt`;
@@ -1688,12 +3123,46 @@ var DocxodusEditor = (() => {
1688
3123
  }
1689
3124
  pageBox.appendChild(contentArea);
1690
3125
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1691
- if (footnoteIds.length > 0 || hasContinuation) {
3126
+ if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
1692
3127
  this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1693
3128
  }
1694
- const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
3129
+ const pageCommentIds = [];
3130
+ for (const marker of Array.from(
3131
+ pageBox.querySelectorAll("[data-comment-id]")
3132
+ )) {
3133
+ if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
3134
+ const id = marker.dataset.commentId;
3135
+ if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
3136
+ pageCommentIds.push(id);
3137
+ }
3138
+ }
3139
+ if (pageCommentIds.length > 0) {
3140
+ const marginColumn = this.document.createElement("aside");
3141
+ marginColumn.className = `${this.cssPrefix}comment-margin`;
3142
+ marginColumn.style.position = "absolute";
3143
+ marginColumn.style.top = `${contentAreaTop}pt`;
3144
+ marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
3145
+ marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
3146
+ marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
3147
+ marginColumn.style.overflow = "hidden";
3148
+ marginColumn.style.boxSizing = "border-box";
3149
+ const placedThreadRoots = /* @__PURE__ */ new Set();
3150
+ for (const id of pageCommentIds) {
3151
+ const source = this.commentMarginRegistry.get(id);
3152
+ if (source) {
3153
+ const rootId = source.dataset.commentId ?? id;
3154
+ if (placedThreadRoots.has(rootId)) continue;
3155
+ placedThreadRoots.add(rootId);
3156
+ const clone = source.cloneNode(true);
3157
+ this.makeClonedMarginCommentInert(clone);
3158
+ marginColumn.appendChild(clone);
3159
+ }
3160
+ }
3161
+ pageBox.appendChild(marginColumn);
3162
+ }
3163
+ const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
1695
3164
  if (footerSource) {
1696
- const footerDiv = document.createElement("div");
3165
+ const footerDiv = this.document.createElement("div");
1697
3166
  footerDiv.className = `${this.cssPrefix}footer`;
1698
3167
  footerDiv.style.position = "absolute";
1699
3168
  footerDiv.style.top = "auto";
@@ -1713,8 +3182,8 @@ var DocxodusEditor = (() => {
1713
3182
  }
1714
3183
  pageBox.appendChild(footerDiv);
1715
3184
  }
1716
- if (this.showPageNumbers) {
1717
- const pageNum = document.createElement("div");
3185
+ if (this.showPageNumbers && !isSectionFiller) {
3186
+ const pageNum = this.document.createElement("div");
1718
3187
  pageNum.className = `${this.cssPrefix}number`;
1719
3188
  pageNum.textContent = String(pageNumber);
1720
3189
  pageBox.appendChild(pageNum);
@@ -1737,7 +3206,8 @@ var DocxodusEditor = (() => {
1737
3206
  }
1738
3207
  };
1739
3208
  function paginateHtml(html, container, options = {}) {
1740
- const containerEl = typeof container === "string" ? document.getElementById(container) : container;
3209
+ const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
3210
+ const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
1741
3211
  if (!containerEl) {
1742
3212
  throw new Error("Container element not found");
1743
3213
  }
@@ -5039,6 +6509,13 @@ var DocxodusEditor = (() => {
5039
6509
  function escapeInlineMarkdown(text) {
5040
6510
  return text.replace(/([\\`*_[\]])/g, "\\$1");
5041
6511
  }
6512
+ function escapeLeadingBlockMarkers(md) {
6513
+ return md.split("\n").map((line) => {
6514
+ if (/^[#>|]/.test(line) || /^[-+] /.test(line)) return "\\" + line;
6515
+ if (/^\d+\. /.test(line)) return line.replace(".", "\\.");
6516
+ return line;
6517
+ }).join("\n");
6518
+ }
5042
6519
  function collectInlineSegments(node, out) {
5043
6520
  node.childNodes.forEach((child) => {
5044
6521
  if (isGeneratedChrome(child)) return;
@@ -5090,7 +6567,7 @@ var DocxodusEditor = (() => {
5090
6567
  merged.push({ ...s });
5091
6568
  }
5092
6569
  }
5093
- return merged.map(segToMarkdown).join("").trim();
6570
+ return escapeLeadingBlockMarkers(merged.map(segToMarkdown).join("").trim());
5094
6571
  }
5095
6572
  function isListBlock(block) {
5096
6573
  return !!block.querySelector(":scope > [data-list-marker]");
@@ -7125,17 +8602,19 @@ var DocxodusEditor = (() => {
7125
8602
  this.refreshAfter(block, idx, false);
7126
8603
  }
7127
8604
  // ─── Table row / column editing (active block must be inside a table cell) ──────────
7128
- /** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */
8605
+ /** Run a table-structure op on the active cell's canonical tc anchor and re-render. */
7129
8606
  tableEdit(run) {
7130
8607
  const block = this.activeBlock;
7131
- if (this.closed || !block || !block.closest("table")) return;
8608
+ const cell = block?.closest("td, th");
8609
+ if (this.closed || !block || !cell || !block.closest("table")) return;
7132
8610
  const unid = block.getAttribute("data-anchor");
7133
8611
  if (!unid) return;
7134
- let fullId = this.anchorIdOf(block);
7135
- if (!fullId) return;
8612
+ const paragraphId = this.anchorIdOf(block);
8613
+ const cellId = this.anchorIdOf(cell);
8614
+ if (!paragraphId || !cellId) return;
7136
8615
  const idx = this.blockIndex(block);
7137
- fullId = this.syncBlock(block, fullId);
7138
- const res = this.parseEdit(run(fullId));
8616
+ this.syncBlock(block, paragraphId);
8617
+ const res = this.parseEdit(run(cellId));
7139
8618
  if (!res.success) return;
7140
8619
  this.refreshAfter(block, idx, false);
7141
8620
  }
@@ -7288,6 +8767,59 @@ var DocxodusEditor = (() => {
7288
8767
  if (!res.success) return;
7289
8768
  this.reconcile(idx, false);
7290
8769
  }
8770
+ /**
8771
+ * Add a native Word comment on the current selection in the active block — or on the whole
8772
+ * block when the selection is collapsed (a null span comments the paragraph). The definition,
8773
+ * threading parts, and body-side range markers are written by the session's `AddComment`
8774
+ * (issue #300); the re-render shows the marker chrome the converter already draws. The
8775
+ * annotation type legal review runs on, finally authorable from the shipped surface
8776
+ * (issue #580).
8777
+ */
8778
+ addComment(markdown = "New comment.", author = "Reviewer") {
8779
+ const block = this.activeBlock;
8780
+ if (this.closed || !block) return;
8781
+ const bridge = this.exports.DocxSessionBridge;
8782
+ if (!bridge.AddComment) return;
8783
+ let fullId = this.anchorIdOf(block);
8784
+ if (!fullId) return;
8785
+ const idx = this.blockIndex(block);
8786
+ const span = selectionSpanIn(block);
8787
+ fullId = this.syncBlock(block, fullId);
8788
+ const res = this.parseEdit(
8789
+ bridge.AddComment(
8790
+ this.handle,
8791
+ fullId,
8792
+ span ? JSON.stringify(span) : "",
8793
+ author,
8794
+ "",
8795
+ "",
8796
+ markdown
8797
+ )
8798
+ );
8799
+ if (!res.success) return;
8800
+ this.reconcile(idx, false);
8801
+ }
8802
+ /** The document's native comment threads (session truth), for review UIs — flat entries
8803
+ * with `parentAnchorId` linking replies and `resolved` carrying thread state. */
8804
+ listComments() {
8805
+ if (this.closed) return [];
8806
+ const bridge = this.exports.DocxSessionBridge;
8807
+ if (!bridge.ListComments) return [];
8808
+ try {
8809
+ return JSON.parse(bridge.ListComments(this.handle));
8810
+ } catch {
8811
+ return [];
8812
+ }
8813
+ }
8814
+ /** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}).
8815
+ * Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is
8816
+ * needed — a review UI re-reads {@link listComments} for the new state. */
8817
+ setCommentResolved(commentAnchorId, resolved) {
8818
+ if (this.closed) return;
8819
+ const bridge = this.exports.DocxSessionBridge;
8820
+ if (!bridge.SetCommentResolved) return;
8821
+ this.parseEdit(bridge.SetCommentResolved(this.handle, commentAnchorId, resolved));
8822
+ }
7291
8823
  applyParagraphFormat(op) {
7292
8824
  const block = this.activeBlock;
7293
8825
  if (this.closed || !block) return;
@@ -7815,8 +9347,8 @@ var DocxodusEditor = (() => {
7815
9347
  const backref = document.createElement("a");
7816
9348
  backref.setAttribute("class", `${prefix}-backref`);
7817
9349
  backref.setAttribute("contenteditable", "false");
7818
- backref.textContent = "\u21A9";
7819
- last.append(" ", backref);
9350
+ backref.textContent = " \u21A9";
9351
+ last.append(backref);
7820
9352
  }
7821
9353
  li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
7822
9354
  return li;
@@ -7969,6 +9501,7 @@ var DocxodusEditor = (() => {
7969
9501
  color: var(--dxr-ink);
7970
9502
  background: var(--dxr-desk);
7971
9503
  -webkit-text-size-adjust: 100%;
9504
+ text-size-adjust: 100%;
7972
9505
  }
7973
9506
  .dxr *, .dxr *::before, .dxr *::after { box-sizing: border-box; }
7974
9507
 
@@ -8372,6 +9905,25 @@ var DocxodusEditor = (() => {
8372
9905
  box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
8373
9906
  }
8374
9907
 
9908
+ /* \u2500\u2500 Review: comment threads \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
9909
+ .dxr-threads {
9910
+ display: flex; flex-direction: column; gap: 4px;
9911
+ max-height: 92px; overflow-y: auto; min-width: 260px; padding-right: 2px;
9912
+ }
9913
+ .dxr-thread {
9914
+ display: flex; align-items: center; gap: 6px;
9915
+ font-size: 11px; line-height: 1.3; padding: 3px 6px;
9916
+ border: 1px solid var(--dxr-rule); border-radius: 4px; background: var(--dxr-chrome-sunk);
9917
+ }
9918
+ .dxr-thread[data-resolved] { opacity: 0.55; }
9919
+ .dxr-thread[data-reply] { margin-left: 14px; }
9920
+ .dxr-thread .dxr-tauthor { font-weight: 600; white-space: nowrap; }
9921
+ .dxr-thread .dxr-ttext {
9922
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
9923
+ }
9924
+ .dxr-thread button { flex: none; }
9925
+ .dxr-group.dxr-grow { flex: 1; min-width: 0; }
9926
+
8375
9927
  /* \u2500\u2500 Table size picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
8376
9928
  .dxr-pop {
8377
9929
  display: none;
@@ -8690,6 +10242,7 @@ var DocxodusEditor = (() => {
8690
10242
  <button type="button" class="dxr-tab" role="tab" data-tab="home" aria-selected="true">Home</button>
8691
10243
  <button type="button" class="dxr-tab" role="tab" data-tab="insert" aria-selected="false">Insert</button>
8692
10244
  <button type="button" class="dxr-tab" role="tab" data-tab="layout" aria-selected="false">Layout</button>
10245
+ <button type="button" class="dxr-tab" role="tab" data-tab="review" aria-selected="false">Review</button>
8693
10246
  <button type="button" class="dxr-tab" role="tab" data-tab="table" data-contextual aria-selected="false" hidden>Table</button>
8694
10247
  </div>
8695
10248
 
@@ -8855,6 +10408,31 @@ var DocxodusEditor = (() => {
8855
10408
  </div>
8856
10409
  </div>
8857
10410
 
10411
+ <!-- REVIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
10412
+ Comment authoring over the session's native comment family (issue #580):
10413
+ the button comments the selection (the whole block when collapsed), and
10414
+ the thread list is live session truth with resolve/reopen per thread. -->
10415
+ <div class="dxr-panel" data-panel="review">
10416
+ <div class="dxr-group">
10417
+ <span class="dxr-glabel">Comments</span>
10418
+ <div class="dxr-row">
10419
+ <input data-dxr="commenttext" type="text" placeholder="Comment text&#8230;"
10420
+ title="The comment body; markdown is honored" />
10421
+ <button type="button" data-dxr="comment"
10422
+ title="Comment on the selection, or on the whole block when nothing is selected">&#128172; Comment</button>
10423
+ </div>
10424
+ <div class="dxr-row">
10425
+ <span class="dxr-note">Comments the selected text; the whole block when the caret is collapsed.</span>
10426
+ </div>
10427
+ </div>
10428
+ <div class="dxr-group dxr-grow">
10429
+ <span class="dxr-glabel">Threads</span>
10430
+ <div class="dxr-threads" data-dxr="threads" aria-live="polite">
10431
+ <span class="dxr-note">No comments yet.</span>
10432
+ </div>
10433
+ </div>
10434
+ </div>
10435
+
8858
10436
  <!-- TABLE (contextual) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8859
10437
  Row/column editing lives here rather than in a floating toolbar: a docked
8860
10438
  contextual tab cannot overlap the cell you are editing. -->
@@ -9362,6 +10940,18 @@ var DocxodusEditor = (() => {
9362
10940
  if (this.live && style.value) this.run("style", () => this.live.setParagraphStyle(style.value));
9363
10941
  style.value = "";
9364
10942
  });
10943
+ const commentButton = this.control("comment");
10944
+ if (commentButton) {
10945
+ this.keepSelection(commentButton);
10946
+ commentButton.addEventListener("click", () => {
10947
+ if (!this.live) return;
10948
+ const input = this.control("commenttext");
10949
+ const text = input?.value.trim() || "New comment.";
10950
+ this.run("comment", () => this.live.addComment(text, this.options.revisionAuthor ?? "Reviewer"));
10951
+ if (input) input.value = "";
10952
+ this.renderThreads();
10953
+ });
10954
+ }
9365
10955
  this.wireFileActions();
9366
10956
  this.wireLayout();
9367
10957
  this.wirePicker();
@@ -9484,6 +11074,52 @@ var DocxodusEditor = (() => {
9484
11074
  else panel.removeAttribute("data-active");
9485
11075
  }
9486
11076
  if (name === "layout") this.syncPageNumbering();
11077
+ if (name === "review") this.renderThreads();
11078
+ }
11079
+ /** Re-render the Review tab's thread list from session truth ({@link DocxEditor.listComments}):
11080
+ * one row per comment, replies indented under their thread root, resolve/reopen per root. */
11081
+ renderThreads() {
11082
+ const host = this.control("threads");
11083
+ if (!host || !this.live) return;
11084
+ const doc = this.element.ownerDocument ?? document;
11085
+ host.textContent = "";
11086
+ const comments = this.live.listComments();
11087
+ if (comments.length === 0) {
11088
+ const empty = doc.createElement("span");
11089
+ empty.className = "dxr-note";
11090
+ empty.textContent = "No comments yet.";
11091
+ host.appendChild(empty);
11092
+ return;
11093
+ }
11094
+ for (const entry of comments) {
11095
+ const row = doc.createElement("div");
11096
+ row.className = "dxr-thread";
11097
+ row.setAttribute("data-thread", entry.anchorId);
11098
+ if (entry.resolved) row.setAttribute("data-resolved", "");
11099
+ if (entry.parentAnchorId) row.setAttribute("data-reply", "");
11100
+ const author = doc.createElement("span");
11101
+ author.className = "dxr-tauthor";
11102
+ author.textContent = entry.author || "\u2014";
11103
+ row.appendChild(author);
11104
+ const text = doc.createElement("span");
11105
+ text.className = "dxr-ttext";
11106
+ text.textContent = entry.text;
11107
+ text.title = entry.text;
11108
+ row.appendChild(text);
11109
+ if (!entry.parentAnchorId) {
11110
+ const toggle = doc.createElement("button");
11111
+ toggle.type = "button";
11112
+ toggle.textContent = entry.resolved ? "Reopen" : "Resolve";
11113
+ toggle.title = entry.resolved ? "Reopen this comment thread" : "Mark this comment thread resolved";
11114
+ toggle.addEventListener("click", () => {
11115
+ if (!this.live) return;
11116
+ this.run(entry.resolved ? "reopen comment" : "resolve comment", () => this.live.setCommentResolved(entry.anchorId, !entry.resolved));
11117
+ this.renderThreads();
11118
+ });
11119
+ row.appendChild(toggle);
11120
+ }
11121
+ host.appendChild(row);
11122
+ }
9487
11123
  }
9488
11124
  // ── selection-driven state ──────────────────────────────────────────────────
9489
11125
  selectionElement() {