docxodus 9.7.0 → 9.8.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Microsoft Corporation
4
+ Copyright (c) 2025-2026 John Scrudato IV
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -515,7 +515,6 @@ public/
515
515
  dotnet.js
516
516
  dotnet.native.wasm
517
517
  ... (other framework files)
518
- main.js
519
518
  ```
520
519
 
521
520
  ## Package and Runtime Size
@@ -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) {
@@ -470,9 +499,9 @@ var DocxodusEditor = (() => {
470
499
  */
471
500
  smallestEffectiveContentHeight(dims, sectionIndex) {
472
501
  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
502
+ this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
503
+ this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
504
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
476
505
  );
477
506
  }
478
507
  /**
@@ -996,7 +1025,7 @@ var DocxodusEditor = (() => {
996
1025
  /**
997
1026
  * Adds footnotes to a page container, including continuation content.
998
1027
  */
999
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
1028
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
1000
1029
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1001
1030
  if (footnoteIds.length === 0 && !hasContinuation) {
1002
1031
  return;
@@ -1007,12 +1036,12 @@ var DocxodusEditor = (() => {
1007
1036
  const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
1008
1037
  const maxFootnoteHeight = Math.min(
1009
1038
  footnoteHeight,
1010
- dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO
1039
+ bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
1011
1040
  );
1012
1041
  const footnotesDiv = document.createElement("div");
1013
1042
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
1014
1043
  footnotesDiv.style.position = "absolute";
1015
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`;
1044
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
1016
1045
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
1017
1046
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
1018
1047
  footnotesDiv.style.boxSizing = "border-box";
@@ -1086,50 +1115,51 @@ var DocxodusEditor = (() => {
1086
1115
  return sectionHf.footerDefault;
1087
1116
  }
1088
1117
  /**
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.
1118
+ * The header, body, and footer bands for one page position.
1119
+ *
1120
+ * The single owner of "where does anything sit vertically on this page" — placement in
1121
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
1122
+ * read it, so those three cannot disagree about where the body ends.
1123
+ *
1124
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
1125
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
1094
1126
  */
1095
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
1127
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
1096
1128
  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 };
1129
+ return resolvePageBands(
1130
+ dims,
1131
+ this.selectStoryHeight(
1132
+ sectionHf?.headerFirstHeight,
1133
+ sectionHf?.headerEvenHeight,
1134
+ sectionHf?.headerDefaultHeight,
1135
+ pageInSection,
1136
+ globalPageNumber
1137
+ ),
1138
+ this.selectStoryHeight(
1139
+ sectionHf?.footerFirstHeight,
1140
+ sectionHf?.footerEvenHeight,
1141
+ sectionHf?.footerDefaultHeight,
1142
+ pageInSection,
1143
+ globalPageNumber
1144
+ )
1145
+ );
1146
+ }
1147
+ /**
1148
+ * The measured height of the running story this page position selects, mirroring
1149
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
1150
+ */
1151
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
1152
+ if (pageInSection === 1 && first != null) return first;
1153
+ if (globalPageNumber % 2 === 0 && even != null) return even;
1154
+ return fallback ?? 0;
1129
1155
  }
1130
1156
  /**
1131
1157
  * 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.
1158
+ *
1159
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
1160
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
1161
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
1162
+ * silently starts overlapping body text.
1133
1163
  */
1134
1164
  measureHeaderFooterHeight(source, contentWidth) {
1135
1165
  const measureContainer = document.createElement("div");
@@ -1137,7 +1167,6 @@ var DocxodusEditor = (() => {
1137
1167
  measureContainer.style.visibility = "hidden";
1138
1168
  measureContainer.style.width = `${contentWidth}pt`;
1139
1169
  measureContainer.style.left = "-9999px";
1140
- measureContainer.style.paddingBottom = "4pt";
1141
1170
  for (const child of Array.from(source.childNodes)) {
1142
1171
  measureContainer.appendChild(child.cloneNode(true));
1143
1172
  }
@@ -1157,7 +1186,7 @@ var DocxodusEditor = (() => {
1157
1186
  let currentContent = [];
1158
1187
  let pageNumber = startPageNumber;
1159
1188
  let pageInSection = 1;
1160
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(
1189
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(
1161
1190
  dims,
1162
1191
  sectionIndex,
1163
1192
  pageInSection,
@@ -1192,8 +1221,8 @@ var DocxodusEditor = (() => {
1192
1221
  pageNumber++;
1193
1222
  pageInSection++;
1194
1223
  currentContent = [];
1195
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1196
- effectiveContentHeight = newHeights.contentHeight;
1224
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1225
+ effectiveContentHeight = newBands.bodyHeight;
1197
1226
  remainingHeight = effectiveContentHeight;
1198
1227
  prevMarginBottomPt = 0;
1199
1228
  currentFootnoteIds = [];
@@ -1255,7 +1284,7 @@ var DocxodusEditor = (() => {
1255
1284
  ) + additionalChainFootnoteHeight;
1256
1285
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1257
1286
  if (currentChainHeight > currentAvailableHeight) {
1258
- const nextPageHeights = this.getEffectiveHeights(
1287
+ const nextPageBands = this.getPageBands(
1259
1288
  dims,
1260
1289
  sectionIndex,
1261
1290
  pageInSection + 1,
@@ -1271,7 +1300,7 @@ var DocxodusEditor = (() => {
1271
1300
  dims.contentWidth,
1272
1301
  nextPageContinuation
1273
1302
  );
1274
- if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
1303
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
1275
1304
  finishPage();
1276
1305
  }
1277
1306
  }
@@ -1440,6 +1469,161 @@ var DocxodusEditor = (() => {
1440
1469
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
1441
1470
  }
1442
1471
  }
1472
+ /**
1473
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
1474
+ *
1475
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
1476
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
1477
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
1478
+ * from the clipped text column into the page box and position it in one shared point space.
1479
+ */
1480
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
1481
+ const anchors = Array.from(
1482
+ contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]')
1483
+ );
1484
+ if (anchors.length === 0) return;
1485
+ const pageRect = pageBox.getBoundingClientRect();
1486
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
1487
+ if (!(pixelsPerPoint > 0)) return;
1488
+ for (const anchor of anchors) {
1489
+ const staticRect = anchor.getBoundingClientRect();
1490
+ const paragraph = anchor.closest("p, h1, h2, h3, h4, h5, h6");
1491
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
1492
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
1493
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
1494
+ const context = {
1495
+ staticLeft: toPageX(staticRect.left),
1496
+ staticTop: toPageY(staticRect.top),
1497
+ lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
1498
+ paragraphLeft: toPageX(paragraphRect.left),
1499
+ paragraphTop: toPageY(paragraphRect.top),
1500
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
1501
+ paragraphHeight: paragraphRect.height / pixelsPerPoint
1502
+ };
1503
+ const widthReference = this.horizontalAnchorReference(
1504
+ anchor.getAttribute("data-docx-anchor-width-relative") ?? "margin",
1505
+ dims,
1506
+ pageNumber,
1507
+ context
1508
+ );
1509
+ const widthPercent = this.anchorNumber(anchor, "width-percent");
1510
+ if (widthPercent !== void 0) {
1511
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
1512
+ }
1513
+ const heightReference = this.verticalAnchorReference(
1514
+ anchor.getAttribute("data-docx-anchor-height-relative") ?? "margin",
1515
+ dims,
1516
+ pageNumber,
1517
+ context
1518
+ );
1519
+ const heightPercent = this.anchorNumber(anchor, "height-percent");
1520
+ if (heightPercent !== void 0 && anchor.dataset.docxAnchorAutofit !== "true") {
1521
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
1522
+ }
1523
+ const sizedRect = anchor.getBoundingClientRect();
1524
+ const width = sizedRect.width / pixelsPerPoint;
1525
+ const height = sizedRect.height / pixelsPerPoint;
1526
+ const horizontal2 = this.horizontalAnchorReference(
1527
+ anchor.getAttribute("data-docx-anchor-h-relative") ?? "column",
1528
+ dims,
1529
+ pageNumber,
1530
+ context
1531
+ );
1532
+ const vertical2 = this.verticalAnchorReference(
1533
+ anchor.getAttribute("data-docx-anchor-v-relative") ?? "paragraph",
1534
+ dims,
1535
+ pageNumber,
1536
+ context
1537
+ );
1538
+ const left2 = this.resolveAnchorAxis(
1539
+ horizontal2,
1540
+ width,
1541
+ anchor.getAttribute("data-docx-anchor-h-align"),
1542
+ this.anchorNumber(anchor, "h-offset"),
1543
+ pageNumber
1544
+ );
1545
+ const top2 = this.resolveAnchorAxis(
1546
+ vertical2,
1547
+ height,
1548
+ anchor.getAttribute("data-docx-anchor-v-align"),
1549
+ this.anchorNumber(anchor, "v-offset"),
1550
+ pageNumber
1551
+ );
1552
+ pageBox.appendChild(anchor);
1553
+ anchor.style.position = "absolute";
1554
+ anchor.style.left = `${left2}pt`;
1555
+ anchor.style.top = `${top2}pt`;
1556
+ anchor.style.margin = "0";
1557
+ }
1558
+ }
1559
+ anchorNumber(anchor, suffix) {
1560
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
1561
+ if (raw === null) return void 0;
1562
+ const value = Number(raw);
1563
+ return Number.isFinite(value) ? value : void 0;
1564
+ }
1565
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
1566
+ const leftMargin = { start: 0, size: dims.marginLeft };
1567
+ const rightMargin = {
1568
+ start: dims.marginLeft + dims.contentWidth,
1569
+ size: dims.marginRight
1570
+ };
1571
+ switch (relativeFrom) {
1572
+ case "page":
1573
+ return { start: 0, size: dims.pageWidth };
1574
+ case "character":
1575
+ return { start: context.staticLeft, size: 0 };
1576
+ case "leftMargin":
1577
+ return leftMargin;
1578
+ case "rightMargin":
1579
+ return rightMargin;
1580
+ case "insideMargin":
1581
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
1582
+ case "outsideMargin":
1583
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
1584
+ case "paragraph":
1585
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
1586
+ case "column":
1587
+ case "margin":
1588
+ default:
1589
+ return { start: dims.marginLeft, size: dims.contentWidth };
1590
+ }
1591
+ }
1592
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
1593
+ const topMargin = { start: 0, size: dims.marginTop };
1594
+ const bottomMargin = {
1595
+ start: dims.marginTop + dims.contentHeight,
1596
+ size: dims.marginBottom
1597
+ };
1598
+ switch (relativeFrom) {
1599
+ case "page":
1600
+ return { start: 0, size: dims.pageHeight };
1601
+ case "paragraph":
1602
+ return { start: context.paragraphTop, size: context.paragraphHeight };
1603
+ case "line":
1604
+ return { start: context.staticTop, size: context.lineHeight };
1605
+ case "topMargin":
1606
+ return topMargin;
1607
+ case "bottomMargin":
1608
+ return bottomMargin;
1609
+ case "insideMargin":
1610
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
1611
+ case "outsideMargin":
1612
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
1613
+ case "margin":
1614
+ default:
1615
+ return { start: dims.marginTop, size: dims.contentHeight };
1616
+ }
1617
+ }
1618
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
1619
+ if (offset !== void 0) return reference.start + offset;
1620
+ let fraction = 0;
1621
+ if (alignment === "center") fraction = 0.5;
1622
+ else if (alignment === "right" || alignment === "bottom") fraction = 1;
1623
+ else if (alignment === "inside") fraction = pageNumber % 2 === 1 ? 0 : 1;
1624
+ else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
1625
+ return reference.start + (reference.size - objectSize) * fraction;
1626
+ }
1443
1627
  /**
1444
1628
  * Creates a page container element.
1445
1629
  */
@@ -1466,22 +1650,22 @@ var DocxodusEditor = (() => {
1466
1650
  pageBox.dataset.pageNumber = String(pageNumber);
1467
1651
  pageBox.dataset.sectionIndex = String(sectionIndex);
1468
1652
  pageBox.dataset.pageInSection = String(pageInSection);
1469
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
1653
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
1470
1654
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
1471
1655
  if (headerSource) {
1472
1656
  const headerDiv = document.createElement("div");
1473
1657
  headerDiv.className = `${this.cssPrefix}header`;
1474
1658
  headerDiv.style.position = "absolute";
1475
- headerDiv.style.top = "0";
1659
+ headerDiv.style.top = `${bands.headerTop}pt`;
1660
+ headerDiv.style.bottom = "auto";
1476
1661
  headerDiv.style.left = `${dims.marginLeft}pt`;
1477
1662
  headerDiv.style.width = `${dims.contentWidth}pt`;
1478
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`;
1663
+ headerDiv.style.height = `${bands.headerHeight}pt`;
1479
1664
  headerDiv.style.overflow = "hidden";
1480
1665
  headerDiv.style.boxSizing = "border-box";
1481
1666
  headerDiv.style.display = "flex";
1482
1667
  headerDiv.style.flexDirection = "column";
1483
- headerDiv.style.justifyContent = "flex-end";
1484
- headerDiv.style.paddingBottom = "4pt";
1668
+ headerDiv.style.justifyContent = "flex-start";
1485
1669
  for (const child of Array.from(headerSource.childNodes)) {
1486
1670
  const clonedheaderDiv = child.cloneNode(true);
1487
1671
  if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
@@ -1489,8 +1673,8 @@ var DocxodusEditor = (() => {
1489
1673
  }
1490
1674
  pageBox.appendChild(headerDiv);
1491
1675
  }
1492
- const contentAreaTop = effectiveHeights.headerHeight;
1493
- const contentAreaHeight = effectiveHeights.contentHeight;
1676
+ const contentAreaTop = bands.bodyTop;
1677
+ const contentAreaHeight = bands.bodyHeight;
1494
1678
  const contentArea = document.createElement("div");
1495
1679
  contentArea.className = `${this.cssPrefix}content`;
1496
1680
  contentArea.style.position = "absolute";
@@ -1505,23 +1689,23 @@ var DocxodusEditor = (() => {
1505
1689
  pageBox.appendChild(contentArea);
1506
1690
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
1507
1691
  if (footnoteIds.length > 0 || hasContinuation) {
1508
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
1692
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
1509
1693
  }
1510
1694
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
1511
1695
  if (footerSource) {
1512
1696
  const footerDiv = document.createElement("div");
1513
1697
  footerDiv.className = `${this.cssPrefix}footer`;
1514
1698
  footerDiv.style.position = "absolute";
1515
- footerDiv.style.bottom = "0";
1699
+ footerDiv.style.top = "auto";
1700
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
1516
1701
  footerDiv.style.left = `${dims.marginLeft}pt`;
1517
1702
  footerDiv.style.width = `${dims.contentWidth}pt`;
1518
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`;
1703
+ footerDiv.style.height = `${bands.footerHeight}pt`;
1519
1704
  footerDiv.style.overflow = "hidden";
1520
1705
  footerDiv.style.boxSizing = "border-box";
1521
1706
  footerDiv.style.display = "flex";
1522
1707
  footerDiv.style.flexDirection = "column";
1523
- footerDiv.style.justifyContent = "flex-start";
1524
- footerDiv.style.paddingTop = "4pt";
1708
+ footerDiv.style.justifyContent = "flex-end";
1525
1709
  for (const child of Array.from(footerSource.childNodes)) {
1526
1710
  const clonedfooterDiv = child.cloneNode(true);
1527
1711
  if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
@@ -1536,6 +1720,7 @@ var DocxodusEditor = (() => {
1536
1720
  pageBox.appendChild(pageNum);
1537
1721
  }
1538
1722
  this.containerElement.appendChild(pageBox);
1723
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
1539
1724
  const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
1540
1725
  if (notesEl) {
1541
1726
  const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
@@ -4910,6 +5095,19 @@ var DocxodusEditor = (() => {
4910
5095
  function isListBlock(block) {
4911
5096
  return !!block.querySelector(":scope > [data-list-marker]");
4912
5097
  }
5098
+ function ensureListMarkerSeparator(block) {
5099
+ const marker = block.querySelector(":scope > [data-list-marker]");
5100
+ if (!marker) return;
5101
+ const next = marker.nextElementSibling;
5102
+ if (next?.hasAttribute("data-editor-list-separator")) return;
5103
+ const separator = block.ownerDocument.createElement("span");
5104
+ separator.setAttribute("data-editor-list-separator", "");
5105
+ separator.setAttribute("data-list-marker", "true");
5106
+ separator.setAttribute("aria-hidden", "true");
5107
+ separator.style.display = "none";
5108
+ separator.textContent = " ";
5109
+ marker.after(separator);
5110
+ }
4913
5111
  var GENERATED_CHROME_SELECTOR = '[data-list-marker], a.footnote-ref, a.endnote-ref, a[class$="-backref"]';
4914
5112
  function isGeneratedChrome(node) {
4915
5113
  return node?.nodeType === 1 && !!node.matches?.(GENERATED_CHROME_SELECTOR);
@@ -6200,6 +6398,7 @@ var DocxodusEditor = (() => {
6200
6398
  const unid = el.getAttribute("data-anchor");
6201
6399
  if (!unid || !this.anchorIdOf(el)) return;
6202
6400
  el.setAttribute("contenteditable", "true");
6401
+ ensureListMarkerSeparator(el);
6203
6402
  el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
6204
6403
  el.dataset.committedText = blockContentText(el);
6205
6404
  el.addEventListener("focus", () => {
@@ -7062,8 +7261,8 @@ var DocxodusEditor = (() => {
7062
7261
  * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
7063
7262
  *
7064
7263
  * 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.
7264
+ * another note, and the session rejects those with `AnchorWrongKind`. Reconciliation creates
7265
+ * the first notes section when needed and renumbers every affected citation in place.
7067
7266
  */
7068
7267
  insertFootnote(markdown = "New footnote.") {
7069
7268
  this.insertNote("footnote", markdown);
@@ -7455,7 +7654,7 @@ var DocxodusEditor = (() => {
7455
7654
  * the current node — never a section div (multi-child) or the edit root. */
7456
7655
  unitWrapperOf(el) {
7457
7656
  let n = el;
7458
- while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
7657
+ 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
7658
  n = n.parentElement;
7460
7659
  }
7461
7660
  return n;
@@ -7491,6 +7690,7 @@ var DocxodusEditor = (() => {
7491
7690
  const pureRemoved = diff.removed.filter(
7492
7691
  (i) => !diff.substituted.some((s) => s.oldIndex === i) && !movedOld.has(i)
7493
7692
  );
7693
+ const appliedFresh = /* @__PURE__ */ new Set();
7494
7694
  const nodeAt = (j) => {
7495
7695
  const oi = diff.keep.get(j);
7496
7696
  if (oi !== void 0) return oldNodes[oi];
@@ -7498,7 +7698,7 @@ var DocxodusEditor = (() => {
7498
7698
  if (movedOi !== void 0) return oldNodes[movedOi];
7499
7699
  if (subOldByNew.has(j)) return fresh.get(j);
7500
7700
  const f = fresh.get(j);
7501
- return f && f.isConnected ? f : null;
7701
+ return f && appliedFresh.has(j) ? f : null;
7502
7702
  };
7503
7703
  for (const j of pureAdded) {
7504
7704
  const el = fresh.get(j);
@@ -7513,6 +7713,7 @@ var DocxodusEditor = (() => {
7513
7713
  if (prevW) prevW.after(el);
7514
7714
  else if (nextW) nextW.before(el);
7515
7715
  else return "insert with no anchored neighbor";
7716
+ appliedFresh.add(j);
7516
7717
  this.wireUnit(el, units[j]);
7517
7718
  }
7518
7719
  for (const i of pureRemoved) {
@@ -7551,13 +7752,13 @@ var DocxodusEditor = (() => {
7551
7752
  if (anchorEl) this.wireBlock(anchorEl);
7552
7753
  root.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
7553
7754
  }
7554
- /** Old-sequence diff state for one notes section. `null` requests remount (DOM not
7555
- * stampable/consistent). */
7755
+ /** Old-sequence diff state for one notes section. `null` requests remount when an
7756
+ * existing list is not stampable; an absent list is the valid first-note case. */
7556
7757
  notesDiff(sectionClass, units) {
7557
7758
  const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
7558
7759
  const lis = ol ? Array.from(ol.children).filter((c) => c.tagName === "LI") : [];
7559
7760
  if (units.length === 0 && lis.length === 0) return { lis, diff: diffUnits([], []) };
7560
- if (!ol) return null;
7761
+ if (!ol) return { lis, diff: diffUnits([], units) };
7561
7762
  const tokens = [];
7562
7763
  for (const li of lis) {
7563
7764
  const unid = li.getAttribute("data-note-anchor");
@@ -7574,7 +7775,20 @@ var DocxodusEditor = (() => {
7574
7775
  this.editRoot.querySelector(`section.${sectionClass}`)?.remove();
7575
7776
  return;
7576
7777
  }
7577
- const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
7778
+ let section = this.editRoot.querySelector(`:scope > section.${sectionClass}`);
7779
+ let ol = section?.querySelector(`:scope > ol`) ?? null;
7780
+ if (!ol) {
7781
+ if (!section) {
7782
+ section = document.createElement("section");
7783
+ section.className = sectionClass;
7784
+ const following = sectionClass === "footnotes" ? this.editRoot.querySelector(`:scope > section.endnotes`) : null;
7785
+ if (following) following.before(section);
7786
+ else this.editRoot.append(section);
7787
+ }
7788
+ if (!section.querySelector(":scope > hr")) section.prepend(document.createElement("hr"));
7789
+ ol = document.createElement("ol");
7790
+ section.append(ol);
7791
+ }
7578
7792
  const prefix = sectionClass === "footnotes" ? "fn" : "en";
7579
7793
  const nodes = [];
7580
7794
  for (let j = 0; j < units.length; j++) {
package/dist/editor.d.ts CHANGED
@@ -616,8 +616,8 @@ export declare class DocxEditor {
616
616
  * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
617
617
  *
618
618
  * Body blocks only: Word disallows a note reference inside a header/footer story or inside
619
- * another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new
620
- * note renumbers the citations after it and can add a whole part.
619
+ * another note, and the session rejects those with `AnchorWrongKind`. Reconciliation creates
620
+ * the first notes section when needed and renumbers every affected citation in place.
621
621
  */
622
622
  insertFootnote(markdown?: string): void;
623
623
  /** Cite a new endnote from the caret — see {@link insertFootnote}; writes the endnotes part. */
@@ -725,8 +725,8 @@ export declare class DocxEditor {
725
725
  * content signature on its `[data-anchor]` element — the element the next
726
726
  * reconcile's DOM walk reads tokens from. */
727
727
  private wireUnit;
728
- /** Old-sequence diff state for one notes section. `null` requests remount (DOM not
729
- * stampable/consistent). */
728
+ /** Old-sequence diff state for one notes section. `null` requests remount when an
729
+ * existing list is not stampable; an absent list is the valid first-note case. */
730
730
  private notesDiff;
731
731
  /** Apply a notes-section diff: rebuild the `<ol>`'s li list, preserving kept nodes. */
732
732
  private applyNotesDiff;