docxodus 9.7.0 → 9.9.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 (101) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +0 -1
  3. package/dist/editor.bundle.js +454 -99
  4. package/dist/editor.d.ts +4 -4
  5. package/dist/editor.d.ts.map +1 -1
  6. package/dist/editor.js +55 -8
  7. package/dist/editor.js.map +1 -1
  8. package/dist/embed.bundle.js +454 -99
  9. package/dist/embed.iife.js +454 -99
  10. package/dist/page-geometry.d.ts +50 -3
  11. package/dist/page-geometry.d.ts.map +1 -1
  12. package/dist/page-geometry.js +63 -6
  13. package/dist/page-geometry.js.map +1 -1
  14. package/dist/pagination.bundle.js +416 -91
  15. package/dist/pagination.d.ts +61 -8
  16. package/dist/pagination.d.ts.map +1 -1
  17. package/dist/pagination.js +353 -105
  18. package/dist/pagination.js.map +1 -1
  19. package/dist/ribbon-chrome.d.ts +1 -1
  20. package/dist/ribbon-chrome.d.ts.map +1 -1
  21. package/dist/ribbon-chrome.js +1 -0
  22. package/dist/ribbon-chrome.js.map +1 -1
  23. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  24. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  25. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  26. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  27. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  28. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  29. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  30. package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
  31. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
  32. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
  33. package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
  34. package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
  35. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  36. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  37. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
  38. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
  39. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
  40. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
  41. package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
  42. package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
  43. package/dist/wasm/_framework/System.Console.wasm +0 -0
  44. package/dist/wasm/_framework/System.Console.wasm.br +0 -0
  45. package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
  46. package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
  47. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  48. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  49. package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
  50. package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
  51. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  52. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  53. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  54. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  55. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  56. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  57. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  58. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  59. package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
  60. package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
  61. package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
  62. package/dist/wasm/_framework/System.ObjectModel.wasm.br +0 -0
  63. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  64. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  65. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  66. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  67. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  68. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  69. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  70. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  71. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  72. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  73. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  74. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  75. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  76. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  77. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
  78. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
  79. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  80. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  81. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  82. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  83. package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
  84. package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
  85. package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
  86. package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
  87. package/dist/wasm/_framework/System.wasm +0 -0
  88. package/dist/wasm/_framework/System.wasm.br +0 -0
  89. package/dist/wasm/_framework/dotnet.boot.js +35 -35
  90. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  91. package/dist/wasm/_framework/dotnet.js +1 -1
  92. package/dist/wasm/_framework/dotnet.js.br +0 -0
  93. package/dist/wasm/_framework/dotnet.native.js +3 -3
  94. package/dist/wasm/_framework/dotnet.native.js.br +0 -0
  95. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  96. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  97. package/dist/wasm/_framework/dotnet.runtime.js +1 -1
  98. package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
  99. package/package.json +14 -5
  100. package/dist/wasm/index.html +0 -238
  101. package/dist/wasm/main.js +0 -26
@@ -165,7 +165,7 @@ var DocxodusEditor = (() => {
165
165
  var DEFAULT_PAGE_WIDTH = 612;
166
166
  var DEFAULT_PAGE_HEIGHT = 792;
167
167
  var DEFAULT_MARGIN = 72;
168
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
168
+ var DEFAULT_HEADER_FOOTER_DISTANCE = 36;
169
169
  function pxToPt(px) {
170
170
  return px * 0.75;
171
171
  }
@@ -181,8 +181,8 @@ var DocxodusEditor = (() => {
181
181
  const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
182
182
  const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
183
183
  const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
184
- const headerHeight = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
185
- const footerHeight = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
184
+ const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
185
+ const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
186
186
  return {
187
187
  pageWidth,
188
188
  pageHeight,
@@ -192,8 +192,37 @@ var DocxodusEditor = (() => {
192
192
  marginRight,
193
193
  marginBottom,
194
194
  marginLeft,
195
- headerHeight,
196
- footerHeight
195
+ headerDistance,
196
+ footerDistance,
197
+ headerHeight: headerDistance,
198
+ footerHeight: footerDistance
199
+ };
200
+ }
201
+ var MIN_BODY_BAND_PT = 12;
202
+ function resolvePageBands(dims, headerContentHeight, footerContentHeight) {
203
+ const headerTop = dims.headerDistance;
204
+ const footerBottom = dims.pageHeight - dims.footerDistance;
205
+ const headerReach = headerContentHeight > 0 ? headerTop + headerContentHeight : 0;
206
+ const footerReach = footerContentHeight > 0 ? footerBottom - footerContentHeight : dims.pageHeight;
207
+ let bodyTop = Math.max(dims.marginTop, headerReach);
208
+ let bodyBottom = Math.min(dims.pageHeight - dims.marginBottom, footerReach);
209
+ const shortfall = MIN_BODY_BAND_PT - (bodyBottom - bodyTop);
210
+ if (shortfall > 0) {
211
+ const headerGrowth = bodyTop - dims.marginTop;
212
+ const footerGrowth = dims.pageHeight - dims.marginBottom - bodyBottom;
213
+ const growth = headerGrowth + footerGrowth;
214
+ if (growth > 0) {
215
+ bodyTop -= shortfall * headerGrowth / growth;
216
+ bodyBottom += shortfall * footerGrowth / growth;
217
+ }
218
+ }
219
+ return {
220
+ headerTop,
221
+ headerHeight: Math.max(0, Math.min(headerContentHeight, bodyTop - headerTop)),
222
+ bodyTop,
223
+ bodyHeight: Math.max(0, bodyBottom - bodyTop),
224
+ footerTop: footerBottom - Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
225
+ footerHeight: Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom))
197
226
  };
198
227
  }
199
228
  function sectionWrappers(root) {
@@ -230,6 +259,9 @@ var DocxodusEditor = (() => {
230
259
  }
231
260
 
232
261
  // src/pagination.ts
262
+ function samePageBox(a, b) {
263
+ 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;
264
+ }
233
265
  var MAX_FOOTNOTE_AREA_RATIO = 0.6;
234
266
  var MIN_BODY_CONTENT_HEIGHT = 72;
235
267
  var PaginationEngine = class {
@@ -275,16 +307,43 @@ var DocxodusEditor = (() => {
275
307
  );
276
308
  this.pageNumbering = this.parsePageNumbering(sections);
277
309
  const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
310
+ const runs = [];
278
311
  for (const section of sectionsToProcess) {
279
- const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
280
312
  const dims = parseSectionDimensions(section);
313
+ const previous = runs[runs.length - 1];
314
+ if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
315
+ previous.sections.push(section);
316
+ } else {
317
+ runs.push({
318
+ sections: [section],
319
+ sectionIndex: parseInt(section.dataset.sectionIndex || "0", 10),
320
+ dims
321
+ });
322
+ }
323
+ }
324
+ for (const run of runs) {
281
325
  this.stagingElement.style.visibility = "hidden";
282
326
  this.stagingElement.style.position = "absolute";
283
327
  this.stagingElement.style.left = "-9999px";
284
328
  this.stagingElement.style.display = "block";
285
- section.style.width = `${dims.contentWidth}pt`;
286
- const blocks = this.measureBlocks(section, dims);
287
- const sectionPages = this.flowToPages(blocks, dims, pageNumber, sectionIndex);
329
+ const blocks = [];
330
+ for (const section of run.sections) {
331
+ section.style.width = `${run.dims.contentWidth}pt`;
332
+ const columnCount = parseInt(section.dataset.cols || "1", 10);
333
+ if (columnCount > 1) {
334
+ const gap = parseFloat(section.dataset.colGap || "");
335
+ blocks.push(...this.buildColumnBlocks(
336
+ section,
337
+ run.dims,
338
+ run.sectionIndex,
339
+ columnCount,
340
+ Number.isFinite(gap) ? gap : 36
341
+ ));
342
+ } else {
343
+ blocks.push(...this.measureBlocks(section, run.dims));
344
+ }
345
+ }
346
+ const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
288
347
  pages.push(...sectionPages);
289
348
  pageNumber += sectionPages.length;
290
349
  }
@@ -373,11 +432,70 @@ var DocxodusEditor = (() => {
373
432
  keepWithNext: child.dataset.keepWithNext === "true",
374
433
  keepLines: child.dataset.keepLines === "true",
375
434
  pageBreakBefore: child.dataset.pageBreakBefore === "true",
376
- isPageBreak
435
+ isPageBreak,
436
+ isWordParagraph: this.isWordParagraphElement(child)
377
437
  });
378
438
  }
379
439
  return blocks;
380
440
  }
441
+ /**
442
+ * Flows a multi-column (`w:cols`) section's children into CSS-multicol container
443
+ * blocks. Word lays such a section out as N columns inside the same body extent;
444
+ * a balanced `column-count` container reproduces that geometry, and the paginator
445
+ * then places each container as one ordinary measured block. A container grows
446
+ * greedily until its balanced height would exceed the smallest page body available
447
+ * to the section, so a long columned section still splits across pages at block
448
+ * boundaries. Each child lands in exactly one container, so anchors never
449
+ * duplicate. An explicit page break child passes through as its own block, which
450
+ * ends the current container and lets the normal flow logic turn the page.
451
+ */
452
+ buildColumnBlocks(section, dims, sectionIndex, columnCount, columnGapPt) {
453
+ const children = Array.from(section.children);
454
+ const blocks = [];
455
+ const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
456
+ const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
457
+ const makeContainer = (slice) => {
458
+ const container = document.createElement("div");
459
+ container.style.columnCount = String(columnCount);
460
+ container.style.columnGap = `${columnGapPt}pt`;
461
+ for (const child of slice) {
462
+ container.appendChild(child.cloneNode(true));
463
+ }
464
+ return container;
465
+ };
466
+ let start2 = 0;
467
+ while (start2 < children.length) {
468
+ if (isBreak(children[start2])) {
469
+ blocks.push(this.measureElement(children[start2], dims));
470
+ start2++;
471
+ continue;
472
+ }
473
+ let end = start2 + 1;
474
+ let container = makeContainer(children.slice(start2, end));
475
+ let measured = this.measureElement(container, dims);
476
+ while (end < children.length && !isBreak(children[end])) {
477
+ const candidate = makeContainer(children.slice(start2, end + 1));
478
+ const candidateMeasured = this.measureElement(candidate, dims);
479
+ if (candidateMeasured.heightPt > maxFragmentHeight) break;
480
+ container = candidate;
481
+ measured = candidateMeasured;
482
+ end++;
483
+ }
484
+ blocks.push({
485
+ element: container,
486
+ heightPt: measured.heightPt,
487
+ marginTopPt: measured.marginTopPt,
488
+ marginBottomPt: measured.marginBottomPt,
489
+ keepWithNext: false,
490
+ keepLines: false,
491
+ pageBreakBefore: false,
492
+ isPageBreak: false,
493
+ isWordParagraph: false
494
+ });
495
+ start2 = end;
496
+ }
497
+ return blocks;
498
+ }
381
499
  /**
382
500
  * Measures one element in the same hidden staging context used for the source blocks.
383
501
  * This is intentionally DOM-based: table row heights cannot be inferred from individual
@@ -402,7 +520,8 @@ var DocxodusEditor = (() => {
402
520
  keepWithNext: element.dataset.keepWithNext === "true",
403
521
  keepLines: element.dataset.keepLines === "true",
404
522
  pageBreakBefore: element.dataset.pageBreakBefore === "true",
405
- isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
523
+ isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`),
524
+ isWordParagraph: this.isWordParagraphElement(element)
406
525
  };
407
526
  this.stagingElement.removeChild(measurementHost);
408
527
  return measured;
@@ -434,10 +553,15 @@ var DocxodusEditor = (() => {
434
553
  * collapsed-margin rules as normal block placement. The trailing margin is
435
554
  * intentionally excluded, matching the individual block fit check.
436
555
  */
437
- measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
556
+ measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage, pageInSection) {
438
557
  const firstBlock = chain[0];
439
558
  if (!firstBlock) return 0;
440
- const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
559
+ const firstMarginTop = this.effectiveBlockMarginTop(
560
+ firstBlock,
561
+ previousMarginBottomPt,
562
+ isFirstOnPage,
563
+ pageInSection
564
+ );
441
565
  let bodyHeight = firstMarginTop + firstBlock.heightPt;
442
566
  for (let index = 1; index < chain.length; index++) {
443
567
  const previousBlock = chain[index - 1];
@@ -446,6 +570,35 @@ var DocxodusEditor = (() => {
446
570
  }
447
571
  return bodyHeight;
448
572
  }
573
+ /** Word paragraphs render as `p`, or as `h1`–`h6` when their style has an outline level. */
574
+ isWordParagraphElement(element) {
575
+ return element.tagName === "P" || /^H[1-6]$/.test(element.tagName);
576
+ }
577
+ shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection) {
578
+ return isFirstOnPage && pageInSection > 1 && block.isWordParagraph === true;
579
+ }
580
+ /**
581
+ * Resolves the part of a block's top margin that consumes the current page.
582
+ *
583
+ * In Word's native DOCX layout, paragraph space-before is suppressed when a paragraph is the
584
+ * first body block on a later page of the SAME section. The first page of a document/section is
585
+ * the exception and keeps its spacing. Tables and other block margins are not paragraph spacing,
586
+ * so they continue to use the ordinary CSS collapsing rule.
587
+ */
588
+ effectiveBlockMarginTop(block, previousMarginBottomPt, isFirstOnPage, pageInSection) {
589
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
590
+ return 0;
591
+ }
592
+ return isFirstOnPage ? block.marginTopPt : Math.max(block.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
593
+ }
594
+ /** Clone a source block with the same page-top spacing decision used by the height budget. */
595
+ cloneBlockForPage(block, isFirstOnPage, pageInSection) {
596
+ const clone = block.element.cloneNode(true);
597
+ if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
598
+ clone.style.setProperty("margin-top", "0", "important");
599
+ }
600
+ return clone;
601
+ }
449
602
  /**
450
603
  * Finds footnote references introduced by a sequence of blocks, preserving
451
604
  * document order and excluding references already assigned to the page.
@@ -470,9 +623,9 @@ var DocxodusEditor = (() => {
470
623
  */
471
624
  smallestEffectiveContentHeight(dims, sectionIndex) {
472
625
  return Math.min(
473
- this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
474
- this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
475
- this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
626
+ this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
627
+ this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
628
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
476
629
  );
477
630
  }
478
631
  /**
@@ -996,7 +1149,7 @@ var DocxodusEditor = (() => {
996
1149
  /**
997
1150
  * Adds footnotes to a page container, including continuation content.
998
1151
  */
999
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
1152
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
1000
1153
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1001
1154
  if (footnoteIds.length === 0 && !hasContinuation) {
1002
1155
  return;
@@ -1007,12 +1160,12 @@ var DocxodusEditor = (() => {
1007
1160
  const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
1008
1161
  const maxFootnoteHeight = Math.min(
1009
1162
  footnoteHeight,
1010
- dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO
1163
+ bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
1011
1164
  );
1012
1165
  const footnotesDiv = document.createElement("div");
1013
1166
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
1014
1167
  footnotesDiv.style.position = "absolute";
1015
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`;
1168
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
1016
1169
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
1017
1170
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
1018
1171
  footnotesDiv.style.boxSizing = "border-box";
@@ -1086,50 +1239,51 @@ var DocxodusEditor = (() => {
1086
1239
  return sectionHf.footerDefault;
1087
1240
  }
1088
1241
  /**
1089
- * Computes effective header, footer, and content heights for a specific page position.
1090
- * Uses pre-measured header/footer heights from the registry.
1091
- * This method is deterministic - same inputs always produce same outputs.
1092
- * This enables lazy loading compatibility since available height can be computed
1093
- * for any page position without knowing the page's content.
1242
+ * The header, body, and footer bands for one page position.
1243
+ *
1244
+ * The single owner of "where does anything sit vertically on this page" — placement in
1245
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
1246
+ * read it, so those three cannot disagree about where the body ends.
1247
+ *
1248
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
1249
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
1094
1250
  */
1095
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
1251
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
1096
1252
  const sectionHf = this.hfRegistry.get(sectionIndex);
1097
- let headerHeight = dims.marginTop;
1098
- if (sectionHf) {
1099
- let measuredHeaderHeight;
1100
- if (pageInSection === 1 && sectionHf.headerFirstHeight != null) {
1101
- measuredHeaderHeight = sectionHf.headerFirstHeight;
1102
- } else if (globalPageNumber % 2 === 0 && sectionHf.headerEvenHeight != null) {
1103
- measuredHeaderHeight = sectionHf.headerEvenHeight;
1104
- } else if (sectionHf.headerDefaultHeight != null) {
1105
- measuredHeaderHeight = sectionHf.headerDefaultHeight;
1106
- }
1107
- if (measuredHeaderHeight != null) {
1108
- headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
1109
- }
1110
- }
1111
- let footerHeight = dims.marginBottom;
1112
- if (sectionHf) {
1113
- let measuredFooterHeight;
1114
- if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
1115
- measuredFooterHeight = sectionHf.footerFirstHeight;
1116
- } else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
1117
- measuredFooterHeight = sectionHf.footerEvenHeight;
1118
- } else if (sectionHf.footerDefaultHeight != null) {
1119
- measuredFooterHeight = sectionHf.footerDefaultHeight;
1120
- }
1121
- if (measuredFooterHeight != null) {
1122
- footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
1123
- }
1124
- }
1125
- const headerExpansion = headerHeight - dims.marginTop;
1126
- const footerExpansion = footerHeight - dims.marginBottom;
1127
- const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
1128
- return { headerHeight, footerHeight, contentHeight };
1253
+ return resolvePageBands(
1254
+ dims,
1255
+ this.selectStoryHeight(
1256
+ sectionHf?.headerFirstHeight,
1257
+ sectionHf?.headerEvenHeight,
1258
+ sectionHf?.headerDefaultHeight,
1259
+ pageInSection,
1260
+ globalPageNumber
1261
+ ),
1262
+ this.selectStoryHeight(
1263
+ sectionHf?.footerFirstHeight,
1264
+ sectionHf?.footerEvenHeight,
1265
+ sectionHf?.footerDefaultHeight,
1266
+ pageInSection,
1267
+ globalPageNumber
1268
+ )
1269
+ );
1270
+ }
1271
+ /**
1272
+ * The measured height of the running story this page position selects, mirroring
1273
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1274
+ */
1275
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
1276
+ if (pageInSection === 1 && first != null) return first;
1277
+ if (globalPageNumber % 2 === 0 && even != null) return even;
1278
+ return fallback ?? 0;
1129
1279
  }
1130
1280
  /**
1131
1281
  * Measures the content height of a header or footer element.
1132
- * This is needed because headers/footers can contain more content than fits in the margin area.
1282
+ *
1283
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
1284
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
1285
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
1286
+ * silently starts overlapping body text.
1133
1287
  */
1134
1288
  measureHeaderFooterHeight(source, contentWidth) {
1135
1289
  const measureContainer = document.createElement("div");
@@ -1137,7 +1291,6 @@ var DocxodusEditor = (() => {
1137
1291
  measureContainer.style.visibility = "hidden";
1138
1292
  measureContainer.style.width = `${contentWidth}pt`;
1139
1293
  measureContainer.style.left = "-9999px";
1140
- measureContainer.style.paddingBottom = "4pt";
1141
1294
  for (const child of Array.from(source.childNodes)) {
1142
1295
  measureContainer.appendChild(child.cloneNode(true));
1143
1296
  }
@@ -1157,7 +1310,7 @@ var DocxodusEditor = (() => {
1157
1310
  let currentContent = [];
1158
1311
  let pageNumber = startPageNumber;
1159
1312
  let pageInSection = 1;
1160
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(
1313
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1161
1314
  dims,
1162
1315
  sectionIndex,
1163
1316
  pageInSection,
@@ -1192,8 +1345,8 @@ var DocxodusEditor = (() => {
1192
1345
  pageNumber++;
1193
1346
  pageInSection++;
1194
1347
  currentContent = [];
1195
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1196
- effectiveContentHeight = newHeights.contentHeight;
1348
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1349
+ effectiveContentHeight = newBands.bodyHeight;
1197
1350
  remainingHeight = effectiveContentHeight;
1198
1351
  prevMarginBottomPt = 0;
1199
1352
  currentFootnoteIds = [];
@@ -1251,11 +1404,12 @@ var DocxodusEditor = (() => {
1251
1404
  const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
1252
1405
  keepChain,
1253
1406
  prevMarginBottomPt,
1254
- currentContent.length === 0
1407
+ currentContent.length === 0,
1408
+ pageInSection
1255
1409
  ) + additionalChainFootnoteHeight;
1256
1410
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1257
1411
  if (currentChainHeight > currentAvailableHeight) {
1258
- const nextPageHeights = this.getEffectiveHeights(
1412
+ const nextPageBands = this.getPageBands(
1259
1413
  dims,
1260
1414
  sectionIndex,
1261
1415
  pageInSection + 1,
@@ -1264,14 +1418,15 @@ var DocxodusEditor = (() => {
1264
1418
  const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
1265
1419
  keepChain,
1266
1420
  0,
1267
- true
1421
+ true,
1422
+ pageInSection + 1
1268
1423
  );
1269
1424
  const freshChainFootnoteHeight = this.measureFootnotesHeight(
1270
1425
  newChainFootnoteIds,
1271
1426
  dims.contentWidth,
1272
1427
  nextPageContinuation
1273
1428
  );
1274
- if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
1429
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1275
1430
  finishPage();
1276
1431
  }
1277
1432
  }
@@ -1290,10 +1445,12 @@ var DocxodusEditor = (() => {
1290
1445
  additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
1291
1446
  }
1292
1447
  const isFirstOnPage = currentContent.length === 0;
1293
- let effectiveMarginTop = block.marginTopPt;
1294
- if (!isFirstOnPage) {
1295
- effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
1296
- }
1448
+ const effectiveMarginTop = this.effectiveBlockMarginTop(
1449
+ block,
1450
+ prevMarginBottomPt,
1451
+ isFirstOnPage,
1452
+ pageInSection
1453
+ );
1297
1454
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
1298
1455
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
1299
1456
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
@@ -1313,19 +1470,19 @@ var DocxodusEditor = (() => {
1313
1470
  }
1314
1471
  }
1315
1472
  if (blockSpace <= effectiveRemainingHeight) {
1316
- currentContent.push(block.element.cloneNode(true));
1473
+ currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
1317
1474
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1318
1475
  prevMarginBottomPt = block.marginBottomPt;
1319
1476
  if (newFootnoteIds.length > 0) {
1320
1477
  currentFootnoteIds.push(...newFootnoteIds);
1321
1478
  currentFootnoteHeight += additionalFootnoteHeight;
1322
1479
  }
1323
- } else if (block.heightPt + block.marginTopPt <= effectiveContentHeight) {
1480
+ } else if (block.heightPt + this.effectiveBlockMarginTop(block, 0, true, pageInSection + 1) <= effectiveContentHeight) {
1324
1481
  const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
1325
1482
  const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
1326
1483
  const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
1327
1484
  if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
1328
- currentContent.push(block.element.cloneNode(true));
1485
+ currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
1329
1486
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
1330
1487
  prevMarginBottomPt = block.marginBottomPt;
1331
1488
  const availableForFootnotes = Math.min(
@@ -1372,7 +1529,7 @@ var DocxodusEditor = (() => {
1372
1529
  const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
1373
1530
  const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
1374
1531
  if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
1375
- currentContent.push(block.element.cloneNode(true));
1532
+ currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
1376
1533
  remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
1377
1534
  prevMarginBottomPt = block.marginBottomPt;
1378
1535
  currentFootnoteIds.push(...newFootnoteIds);
@@ -1380,8 +1537,14 @@ var DocxodusEditor = (() => {
1380
1537
  } else {
1381
1538
  finishPage();
1382
1539
  const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1383
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1384
- currentContent.push(block.element.cloneNode(true));
1540
+ const newPageMarginTop = this.effectiveBlockMarginTop(
1541
+ block,
1542
+ 0,
1543
+ true,
1544
+ pageInSection
1545
+ );
1546
+ const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
1547
+ currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1385
1548
  remainingHeight = effectiveContentHeight - newPageSpace;
1386
1549
  prevMarginBottomPt = block.marginBottomPt;
1387
1550
  currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
@@ -1390,8 +1553,14 @@ var DocxodusEditor = (() => {
1390
1553
  } else {
1391
1554
  finishPage();
1392
1555
  const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1393
- const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
1394
- currentContent.push(block.element.cloneNode(true));
1556
+ const newPageMarginTop = this.effectiveBlockMarginTop(
1557
+ block,
1558
+ 0,
1559
+ true,
1560
+ pageInSection
1561
+ );
1562
+ const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
1563
+ currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1395
1564
  remainingHeight = effectiveContentHeight - newPageSpace;
1396
1565
  prevMarginBottomPt = block.marginBottomPt;
1397
1566
  currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
@@ -1410,7 +1579,7 @@ var DocxodusEditor = (() => {
1410
1579
  if (currentContent.length > 0) {
1411
1580
  finishPage();
1412
1581
  }
1413
- currentContent.push(block.element.cloneNode(true));
1582
+ currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
1414
1583
  currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
1415
1584
  finishPage();
1416
1585
  }
@@ -1440,6 +1609,161 @@ var DocxodusEditor = (() => {
1440
1609
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1441
1610
  }
1442
1611
  }
1612
+ /**
1613
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1614
+ *
1615
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
1616
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
1617
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
1618
+ * from the clipped text column into the page box and position it in one shared point space.
1619
+ */
1620
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
1621
+ const anchors = Array.from(
1622
+ contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]')
1623
+ );
1624
+ if (anchors.length === 0) return;
1625
+ const pageRect = pageBox.getBoundingClientRect();
1626
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
1627
+ if (!(pixelsPerPoint > 0)) return;
1628
+ for (const anchor of anchors) {
1629
+ const staticRect = anchor.getBoundingClientRect();
1630
+ const paragraph = anchor.closest("p, h1, h2, h3, h4, h5, h6");
1631
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
1632
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
1633
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
1634
+ const context = {
1635
+ staticLeft: toPageX(staticRect.left),
1636
+ staticTop: toPageY(staticRect.top),
1637
+ lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
1638
+ paragraphLeft: toPageX(paragraphRect.left),
1639
+ paragraphTop: toPageY(paragraphRect.top),
1640
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
1641
+ paragraphHeight: paragraphRect.height / pixelsPerPoint
1642
+ };
1643
+ const widthReference = this.horizontalAnchorReference(
1644
+ anchor.getAttribute("data-docx-anchor-width-relative") ?? "margin",
1645
+ dims,
1646
+ pageNumber,
1647
+ context
1648
+ );
1649
+ const widthPercent = this.anchorNumber(anchor, "width-percent");
1650
+ if (widthPercent !== void 0) {
1651
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
1652
+ }
1653
+ const heightReference = this.verticalAnchorReference(
1654
+ anchor.getAttribute("data-docx-anchor-height-relative") ?? "margin",
1655
+ dims,
1656
+ pageNumber,
1657
+ context
1658
+ );
1659
+ const heightPercent = this.anchorNumber(anchor, "height-percent");
1660
+ if (heightPercent !== void 0 && anchor.dataset.docxAnchorAutofit !== "true") {
1661
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
1662
+ }
1663
+ const sizedRect = anchor.getBoundingClientRect();
1664
+ const width = sizedRect.width / pixelsPerPoint;
1665
+ const height = sizedRect.height / pixelsPerPoint;
1666
+ const horizontal2 = this.horizontalAnchorReference(
1667
+ anchor.getAttribute("data-docx-anchor-h-relative") ?? "column",
1668
+ dims,
1669
+ pageNumber,
1670
+ context
1671
+ );
1672
+ const vertical2 = this.verticalAnchorReference(
1673
+ anchor.getAttribute("data-docx-anchor-v-relative") ?? "paragraph",
1674
+ dims,
1675
+ pageNumber,
1676
+ context
1677
+ );
1678
+ const left2 = this.resolveAnchorAxis(
1679
+ horizontal2,
1680
+ width,
1681
+ anchor.getAttribute("data-docx-anchor-h-align"),
1682
+ this.anchorNumber(anchor, "h-offset"),
1683
+ pageNumber
1684
+ );
1685
+ const top2 = this.resolveAnchorAxis(
1686
+ vertical2,
1687
+ height,
1688
+ anchor.getAttribute("data-docx-anchor-v-align"),
1689
+ this.anchorNumber(anchor, "v-offset"),
1690
+ pageNumber
1691
+ );
1692
+ pageBox.appendChild(anchor);
1693
+ anchor.style.position = "absolute";
1694
+ anchor.style.left = `${left2}pt`;
1695
+ anchor.style.top = `${top2}pt`;
1696
+ anchor.style.margin = "0";
1697
+ }
1698
+ }
1699
+ anchorNumber(anchor, suffix) {
1700
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
1701
+ if (raw === null) return void 0;
1702
+ const value = Number(raw);
1703
+ return Number.isFinite(value) ? value : void 0;
1704
+ }
1705
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
1706
+ const leftMargin = { start: 0, size: dims.marginLeft };
1707
+ const rightMargin = {
1708
+ start: dims.marginLeft + dims.contentWidth,
1709
+ size: dims.marginRight
1710
+ };
1711
+ switch (relativeFrom) {
1712
+ case "page":
1713
+ return { start: 0, size: dims.pageWidth };
1714
+ case "character":
1715
+ return { start: context.staticLeft, size: 0 };
1716
+ case "leftMargin":
1717
+ return leftMargin;
1718
+ case "rightMargin":
1719
+ return rightMargin;
1720
+ case "insideMargin":
1721
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
1722
+ case "outsideMargin":
1723
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
1724
+ case "paragraph":
1725
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
1726
+ case "column":
1727
+ case "margin":
1728
+ default:
1729
+ return { start: dims.marginLeft, size: dims.contentWidth };
1730
+ }
1731
+ }
1732
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
1733
+ const topMargin = { start: 0, size: dims.marginTop };
1734
+ const bottomMargin = {
1735
+ start: dims.marginTop + dims.contentHeight,
1736
+ size: dims.marginBottom
1737
+ };
1738
+ switch (relativeFrom) {
1739
+ case "page":
1740
+ return { start: 0, size: dims.pageHeight };
1741
+ case "paragraph":
1742
+ return { start: context.paragraphTop, size: context.paragraphHeight };
1743
+ case "line":
1744
+ return { start: context.staticTop, size: context.lineHeight };
1745
+ case "topMargin":
1746
+ return topMargin;
1747
+ case "bottomMargin":
1748
+ return bottomMargin;
1749
+ case "insideMargin":
1750
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
1751
+ case "outsideMargin":
1752
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
1753
+ case "margin":
1754
+ default:
1755
+ return { start: dims.marginTop, size: dims.contentHeight };
1756
+ }
1757
+ }
1758
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
1759
+ if (offset !== void 0) return reference.start + offset;
1760
+ let fraction = 0;
1761
+ if (alignment === "center") fraction = 0.5;
1762
+ else if (alignment === "right" || alignment === "bottom") fraction = 1;
1763
+ else if (alignment === "inside") fraction = pageNumber % 2 === 1 ? 0 : 1;
1764
+ else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1765
+ return reference.start + (reference.size - objectSize) * fraction;
1766
+ }
1443
1767
  /**
1444
1768
  * Creates a page container element.
1445
1769
  */
@@ -1466,22 +1790,22 @@ var DocxodusEditor = (() => {
1466
1790
  pageBox.dataset.pageNumber = String(pageNumber);
1467
1791
  pageBox.dataset.sectionIndex = String(sectionIndex);
1468
1792
  pageBox.dataset.pageInSection = String(pageInSection);
1469
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1793
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1470
1794
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
1471
1795
  if (headerSource) {
1472
1796
  const headerDiv = document.createElement("div");
1473
1797
  headerDiv.className = `${this.cssPrefix}header`;
1474
1798
  headerDiv.style.position = "absolute";
1475
- headerDiv.style.top = "0";
1799
+ headerDiv.style.top = `${bands.headerTop}pt`;
1800
+ headerDiv.style.bottom = "auto";
1476
1801
  headerDiv.style.left = `${dims.marginLeft}pt`;
1477
1802
  headerDiv.style.width = `${dims.contentWidth}pt`;
1478
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`;
1803
+ headerDiv.style.height = `${bands.headerHeight}pt`;
1479
1804
  headerDiv.style.overflow = "hidden";
1480
1805
  headerDiv.style.boxSizing = "border-box";
1481
1806
  headerDiv.style.display = "flex";
1482
1807
  headerDiv.style.flexDirection = "column";
1483
- headerDiv.style.justifyContent = "flex-end";
1484
- headerDiv.style.paddingBottom = "4pt";
1808
+ headerDiv.style.justifyContent = "flex-start";
1485
1809
  for (const child of Array.from(headerSource.childNodes)) {
1486
1810
  const clonedheaderDiv = child.cloneNode(true);
1487
1811
  if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
@@ -1489,8 +1813,8 @@ var DocxodusEditor = (() => {
1489
1813
  }
1490
1814
  pageBox.appendChild(headerDiv);
1491
1815
  }
1492
- const contentAreaTop = effectiveHeights.headerHeight;
1493
- const contentAreaHeight = effectiveHeights.contentHeight;
1816
+ const contentAreaTop = bands.bodyTop;
1817
+ const contentAreaHeight = bands.bodyHeight;
1494
1818
  const contentArea = document.createElement("div");
1495
1819
  contentArea.className = `${this.cssPrefix}content`;
1496
1820
  contentArea.style.position = "absolute";
@@ -1505,23 +1829,23 @@ var DocxodusEditor = (() => {
1505
1829
  pageBox.appendChild(contentArea);
1506
1830
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1507
1831
  if (footnoteIds.length > 0 || hasContinuation) {
1508
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
1832
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1509
1833
  }
1510
1834
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
1511
1835
  if (footerSource) {
1512
1836
  const footerDiv = document.createElement("div");
1513
1837
  footerDiv.className = `${this.cssPrefix}footer`;
1514
1838
  footerDiv.style.position = "absolute";
1515
- footerDiv.style.bottom = "0";
1839
+ footerDiv.style.top = "auto";
1840
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
1516
1841
  footerDiv.style.left = `${dims.marginLeft}pt`;
1517
1842
  footerDiv.style.width = `${dims.contentWidth}pt`;
1518
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`;
1843
+ footerDiv.style.height = `${bands.footerHeight}pt`;
1519
1844
  footerDiv.style.overflow = "hidden";
1520
1845
  footerDiv.style.boxSizing = "border-box";
1521
1846
  footerDiv.style.display = "flex";
1522
1847
  footerDiv.style.flexDirection = "column";
1523
- footerDiv.style.justifyContent = "flex-start";
1524
- footerDiv.style.paddingTop = "4pt";
1848
+ footerDiv.style.justifyContent = "flex-end";
1525
1849
  for (const child of Array.from(footerSource.childNodes)) {
1526
1850
  const clonedfooterDiv = child.cloneNode(true);
1527
1851
  if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
@@ -1536,6 +1860,7 @@ var DocxodusEditor = (() => {
1536
1860
  pageBox.appendChild(pageNum);
1537
1861
  }
1538
1862
  this.containerElement.appendChild(pageBox);
1863
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
1539
1864
  const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
1540
1865
  if (notesEl) {
1541
1866
  const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
@@ -4910,6 +5235,19 @@ var DocxodusEditor = (() => {
4910
5235
  function isListBlock(block) {
4911
5236
  return !!block.querySelector(":scope > [data-list-marker]");
4912
5237
  }
5238
+ function ensureListMarkerSeparator(block) {
5239
+ const marker = block.querySelector(":scope > [data-list-marker]");
5240
+ if (!marker) return;
5241
+ const next = marker.nextElementSibling;
5242
+ if (next?.hasAttribute("data-editor-list-separator")) return;
5243
+ const separator = block.ownerDocument.createElement("span");
5244
+ separator.setAttribute("data-editor-list-separator", "");
5245
+ separator.setAttribute("data-list-marker", "true");
5246
+ separator.setAttribute("aria-hidden", "true");
5247
+ separator.style.display = "none";
5248
+ separator.textContent = " ";
5249
+ marker.after(separator);
5250
+ }
4913
5251
  var GENERATED_CHROME_SELECTOR = '[data-list-marker], a.footnote-ref, a.endnote-ref, a[class$="-backref"]';
4914
5252
  function isGeneratedChrome(node) {
4915
5253
  return node?.nodeType === 1 && !!node.matches?.(GENERATED_CHROME_SELECTOR);
@@ -6200,6 +6538,7 @@ var DocxodusEditor = (() => {
6200
6538
  const unid = el.getAttribute("data-anchor");
6201
6539
  if (!unid || !this.anchorIdOf(el)) return;
6202
6540
  el.setAttribute("contenteditable", "true");
6541
+ ensureListMarkerSeparator(el);
6203
6542
  el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
6204
6543
  el.dataset.committedText = blockContentText(el);
6205
6544
  el.addEventListener("focus", () => {
@@ -7062,8 +7401,8 @@ var DocxodusEditor = (() => {
7062
7401
  * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
7063
7402
  *
7064
7403
  * Body blocks only: Word disallows a note reference inside a header/footer story or inside
7065
- * another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new
7066
- * note renumbers the citations after it and can add a whole part.
7404
+ * another note, and the session rejects those with `AnchorWrongKind`. Reconciliation creates
7405
+ * the first notes section when needed and renumbers every affected citation in place.
7067
7406
  */
7068
7407
  insertFootnote(markdown = "New footnote.") {
7069
7408
  this.insertNote("footnote", markdown);
@@ -7455,7 +7794,7 @@ var DocxodusEditor = (() => {
7455
7794
  * the current node — never a section div (multi-child) or the edit root. */
7456
7795
  unitWrapperOf(el) {
7457
7796
  let n = el;
7458
- while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
7797
+ while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-section-index") && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
7459
7798
  n = n.parentElement;
7460
7799
  }
7461
7800
  return n;
@@ -7491,6 +7830,7 @@ var DocxodusEditor = (() => {
7491
7830
  const pureRemoved = diff.removed.filter(
7492
7831
  (i) => !diff.substituted.some((s) => s.oldIndex === i) && !movedOld.has(i)
7493
7832
  );
7833
+ const appliedFresh = /* @__PURE__ */ new Set();
7494
7834
  const nodeAt = (j) => {
7495
7835
  const oi = diff.keep.get(j);
7496
7836
  if (oi !== void 0) return oldNodes[oi];
@@ -7498,7 +7838,7 @@ var DocxodusEditor = (() => {
7498
7838
  if (movedOi !== void 0) return oldNodes[movedOi];
7499
7839
  if (subOldByNew.has(j)) return fresh.get(j);
7500
7840
  const f = fresh.get(j);
7501
- return f && f.isConnected ? f : null;
7841
+ return f && appliedFresh.has(j) ? f : null;
7502
7842
  };
7503
7843
  for (const j of pureAdded) {
7504
7844
  const el = fresh.get(j);
@@ -7513,6 +7853,7 @@ var DocxodusEditor = (() => {
7513
7853
  if (prevW) prevW.after(el);
7514
7854
  else if (nextW) nextW.before(el);
7515
7855
  else return "insert with no anchored neighbor";
7856
+ appliedFresh.add(j);
7516
7857
  this.wireUnit(el, units[j]);
7517
7858
  }
7518
7859
  for (const i of pureRemoved) {
@@ -7551,13 +7892,13 @@ var DocxodusEditor = (() => {
7551
7892
  if (anchorEl) this.wireBlock(anchorEl);
7552
7893
  root.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
7553
7894
  }
7554
- /** Old-sequence diff state for one notes section. `null` requests remount (DOM not
7555
- * stampable/consistent). */
7895
+ /** Old-sequence diff state for one notes section. `null` requests remount when an
7896
+ * existing list is not stampable; an absent list is the valid first-note case. */
7556
7897
  notesDiff(sectionClass, units) {
7557
7898
  const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
7558
7899
  const lis = ol ? Array.from(ol.children).filter((c) => c.tagName === "LI") : [];
7559
7900
  if (units.length === 0 && lis.length === 0) return { lis, diff: diffUnits([], []) };
7560
- if (!ol) return null;
7901
+ if (!ol) return { lis, diff: diffUnits([], units) };
7561
7902
  const tokens = [];
7562
7903
  for (const li of lis) {
7563
7904
  const unid = li.getAttribute("data-note-anchor");
@@ -7574,7 +7915,20 @@ var DocxodusEditor = (() => {
7574
7915
  this.editRoot.querySelector(`section.${sectionClass}`)?.remove();
7575
7916
  return;
7576
7917
  }
7577
- const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
7918
+ let section = this.editRoot.querySelector(`:scope > section.${sectionClass}`);
7919
+ let ol = section?.querySelector(`:scope > ol`) ?? null;
7920
+ if (!ol) {
7921
+ if (!section) {
7922
+ section = document.createElement("section");
7923
+ section.className = sectionClass;
7924
+ const following = sectionClass === "footnotes" ? this.editRoot.querySelector(`:scope > section.endnotes`) : null;
7925
+ if (following) following.before(section);
7926
+ else this.editRoot.append(section);
7927
+ }
7928
+ if (!section.querySelector(":scope > hr")) section.prepend(document.createElement("hr"));
7929
+ ol = document.createElement("ol");
7930
+ section.append(ol);
7931
+ }
7578
7932
  const prefix = sectionClass === "footnotes" ? "fn" : "en";
7579
7933
  const nodes = [];
7580
7934
  for (let j = 0; j < units.length; j++) {
@@ -7755,6 +8109,7 @@ var DocxodusEditor = (() => {
7755
8109
  color: var(--dxr-ink);
7756
8110
  background: var(--dxr-desk);
7757
8111
  -webkit-text-size-adjust: 100%;
8112
+ text-size-adjust: 100%;
7758
8113
  }
7759
8114
  .dxr *, .dxr *::before, .dxr *::after { box-sizing: border-box; }
7760
8115