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.
@@ -1450,7 +1450,7 @@ var Docxodus = (() => {
1450
1450
  var DEFAULT_PAGE_WIDTH = 612;
1451
1451
  var DEFAULT_PAGE_HEIGHT = 792;
1452
1452
  var DEFAULT_MARGIN = 72;
1453
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
1453
+ var DEFAULT_HEADER_FOOTER_DISTANCE = 36;
1454
1454
  function pxToPt(px) {
1455
1455
  return px * 0.75;
1456
1456
  }
@@ -1466,8 +1466,8 @@ var Docxodus = (() => {
1466
1466
  const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
1467
1467
  const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
1468
1468
  const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
1469
- const headerHeight = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
1470
- const footerHeight = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
1469
+ const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1470
+ const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1471
1471
  return {
1472
1472
  pageWidth,
1473
1473
  pageHeight,
@@ -1477,8 +1477,37 @@ var Docxodus = (() => {
1477
1477
  marginRight,
1478
1478
  marginBottom,
1479
1479
  marginLeft,
1480
- headerHeight,
1481
- footerHeight
1480
+ headerDistance,
1481
+ footerDistance,
1482
+ headerHeight: headerDistance,
1483
+ footerHeight: footerDistance
1484
+ };
1485
+ }
1486
+ var MIN_BODY_BAND_PT = 12;
1487
+ function resolvePageBands(dims, headerContentHeight, footerContentHeight) {
1488
+ const headerTop = dims.headerDistance;
1489
+ const footerBottom = dims.pageHeight - dims.footerDistance;
1490
+ const headerReach = headerContentHeight > 0 ? headerTop + headerContentHeight : 0;
1491
+ const footerReach = footerContentHeight > 0 ? footerBottom - footerContentHeight : dims.pageHeight;
1492
+ let bodyTop = Math.max(dims.marginTop, headerReach);
1493
+ let bodyBottom = Math.min(dims.pageHeight - dims.marginBottom, footerReach);
1494
+ const shortfall = MIN_BODY_BAND_PT - (bodyBottom - bodyTop);
1495
+ if (shortfall > 0) {
1496
+ const headerGrowth = bodyTop - dims.marginTop;
1497
+ const footerGrowth = dims.pageHeight - dims.marginBottom - bodyBottom;
1498
+ const growth = headerGrowth + footerGrowth;
1499
+ if (growth > 0) {
1500
+ bodyTop -= shortfall * headerGrowth / growth;
1501
+ bodyBottom += shortfall * footerGrowth / growth;
1502
+ }
1503
+ }
1504
+ return {
1505
+ headerTop,
1506
+ headerHeight: Math.max(0, Math.min(headerContentHeight, bodyTop - headerTop)),
1507
+ bodyTop,
1508
+ bodyHeight: Math.max(0, bodyBottom - bodyTop),
1509
+ footerTop: footerBottom - Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
1510
+ footerHeight: Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom))
1482
1511
  };
1483
1512
  }
1484
1513
  function sectionWrappers(root) {
@@ -1755,9 +1784,9 @@ var Docxodus = (() => {
1755
1784
  */
1756
1785
  smallestEffectiveContentHeight(dims, sectionIndex) {
1757
1786
  return Math.min(
1758
- this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
1759
- this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
1760
- this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
1787
+ this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
1788
+ this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
1789
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
1761
1790
  );
1762
1791
  }
1763
1792
  /**
@@ -2281,7 +2310,7 @@ var Docxodus = (() => {
2281
2310
  /**
2282
2311
  * Adds footnotes to a page container, including continuation content.
2283
2312
  */
2284
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
2313
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
2285
2314
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2286
2315
  if (footnoteIds.length === 0 && !hasContinuation) {
2287
2316
  return;
@@ -2292,12 +2321,12 @@ var Docxodus = (() => {
2292
2321
  const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
2293
2322
  const maxFootnoteHeight = Math.min(
2294
2323
  footnoteHeight,
2295
- dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO
2324
+ bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
2296
2325
  );
2297
2326
  const footnotesDiv = document.createElement("div");
2298
2327
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
2299
2328
  footnotesDiv.style.position = "absolute";
2300
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`;
2329
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
2301
2330
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
2302
2331
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
2303
2332
  footnotesDiv.style.boxSizing = "border-box";
@@ -2371,50 +2400,51 @@ var Docxodus = (() => {
2371
2400
  return sectionHf.footerDefault;
2372
2401
  }
2373
2402
  /**
2374
- * Computes effective header, footer, and content heights for a specific page position.
2375
- * Uses pre-measured header/footer heights from the registry.
2376
- * This method is deterministic - same inputs always produce same outputs.
2377
- * This enables lazy loading compatibility since available height can be computed
2378
- * for any page position without knowing the page's content.
2403
+ * The header, body, and footer bands for one page position.
2404
+ *
2405
+ * The single owner of "where does anything sit vertically on this page" — placement in
2406
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
2407
+ * read it, so those three cannot disagree about where the body ends.
2408
+ *
2409
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
2410
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
2379
2411
  */
2380
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
2412
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
2381
2413
  const sectionHf = this.hfRegistry.get(sectionIndex);
2382
- let headerHeight = dims.marginTop;
2383
- if (sectionHf) {
2384
- let measuredHeaderHeight;
2385
- if (pageInSection === 1 && sectionHf.headerFirstHeight != null) {
2386
- measuredHeaderHeight = sectionHf.headerFirstHeight;
2387
- } else if (globalPageNumber % 2 === 0 && sectionHf.headerEvenHeight != null) {
2388
- measuredHeaderHeight = sectionHf.headerEvenHeight;
2389
- } else if (sectionHf.headerDefaultHeight != null) {
2390
- measuredHeaderHeight = sectionHf.headerDefaultHeight;
2391
- }
2392
- if (measuredHeaderHeight != null) {
2393
- headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
2394
- }
2395
- }
2396
- let footerHeight = dims.marginBottom;
2397
- if (sectionHf) {
2398
- let measuredFooterHeight;
2399
- if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
2400
- measuredFooterHeight = sectionHf.footerFirstHeight;
2401
- } else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
2402
- measuredFooterHeight = sectionHf.footerEvenHeight;
2403
- } else if (sectionHf.footerDefaultHeight != null) {
2404
- measuredFooterHeight = sectionHf.footerDefaultHeight;
2405
- }
2406
- if (measuredFooterHeight != null) {
2407
- footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
2408
- }
2409
- }
2410
- const headerExpansion = headerHeight - dims.marginTop;
2411
- const footerExpansion = footerHeight - dims.marginBottom;
2412
- const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
2413
- return { headerHeight, footerHeight, contentHeight };
2414
+ return resolvePageBands(
2415
+ dims,
2416
+ this.selectStoryHeight(
2417
+ sectionHf?.headerFirstHeight,
2418
+ sectionHf?.headerEvenHeight,
2419
+ sectionHf?.headerDefaultHeight,
2420
+ pageInSection,
2421
+ globalPageNumber
2422
+ ),
2423
+ this.selectStoryHeight(
2424
+ sectionHf?.footerFirstHeight,
2425
+ sectionHf?.footerEvenHeight,
2426
+ sectionHf?.footerDefaultHeight,
2427
+ pageInSection,
2428
+ globalPageNumber
2429
+ )
2430
+ );
2431
+ }
2432
+ /**
2433
+ * The measured height of the running story this page position selects, mirroring
2434
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
2435
+ */
2436
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
2437
+ if (pageInSection === 1 && first != null) return first;
2438
+ if (globalPageNumber % 2 === 0 && even != null) return even;
2439
+ return fallback ?? 0;
2414
2440
  }
2415
2441
  /**
2416
2442
  * Measures the content height of a header or footer element.
2417
- * This is needed because headers/footers can contain more content than fits in the margin area.
2443
+ *
2444
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
2445
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
2446
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
2447
+ * silently starts overlapping body text.
2418
2448
  */
2419
2449
  measureHeaderFooterHeight(source, contentWidth) {
2420
2450
  const measureContainer = document.createElement("div");
@@ -2422,7 +2452,6 @@ var Docxodus = (() => {
2422
2452
  measureContainer.style.visibility = "hidden";
2423
2453
  measureContainer.style.width = `${contentWidth}pt`;
2424
2454
  measureContainer.style.left = "-9999px";
2425
- measureContainer.style.paddingBottom = "4pt";
2426
2455
  for (const child of Array.from(source.childNodes)) {
2427
2456
  measureContainer.appendChild(child.cloneNode(true));
2428
2457
  }
@@ -2442,7 +2471,7 @@ var Docxodus = (() => {
2442
2471
  let currentContent = [];
2443
2472
  let pageNumber = startPageNumber;
2444
2473
  let pageInSection = 1;
2445
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(
2474
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(
2446
2475
  dims,
2447
2476
  sectionIndex,
2448
2477
  pageInSection,
@@ -2477,8 +2506,8 @@ var Docxodus = (() => {
2477
2506
  pageNumber++;
2478
2507
  pageInSection++;
2479
2508
  currentContent = [];
2480
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
2481
- effectiveContentHeight = newHeights.contentHeight;
2509
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2510
+ effectiveContentHeight = newBands.bodyHeight;
2482
2511
  remainingHeight = effectiveContentHeight;
2483
2512
  prevMarginBottomPt = 0;
2484
2513
  currentFootnoteIds = [];
@@ -2540,7 +2569,7 @@ var Docxodus = (() => {
2540
2569
  ) + additionalChainFootnoteHeight;
2541
2570
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
2542
2571
  if (currentChainHeight > currentAvailableHeight) {
2543
- const nextPageHeights = this.getEffectiveHeights(
2572
+ const nextPageBands = this.getPageBands(
2544
2573
  dims,
2545
2574
  sectionIndex,
2546
2575
  pageInSection + 1,
@@ -2556,7 +2585,7 @@ var Docxodus = (() => {
2556
2585
  dims.contentWidth,
2557
2586
  nextPageContinuation
2558
2587
  );
2559
- if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
2588
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
2560
2589
  finishPage();
2561
2590
  }
2562
2591
  }
@@ -2725,6 +2754,161 @@ var Docxodus = (() => {
2725
2754
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
2726
2755
  }
2727
2756
  }
2757
+ /**
2758
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
2759
+ *
2760
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
2761
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
2762
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
2763
+ * from the clipped text column into the page box and position it in one shared point space.
2764
+ */
2765
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
2766
+ const anchors = Array.from(
2767
+ contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]')
2768
+ );
2769
+ if (anchors.length === 0) return;
2770
+ const pageRect = pageBox.getBoundingClientRect();
2771
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
2772
+ if (!(pixelsPerPoint > 0)) return;
2773
+ for (const anchor of anchors) {
2774
+ const staticRect = anchor.getBoundingClientRect();
2775
+ const paragraph = anchor.closest("p, h1, h2, h3, h4, h5, h6");
2776
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
2777
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
2778
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
2779
+ const context = {
2780
+ staticLeft: toPageX(staticRect.left),
2781
+ staticTop: toPageY(staticRect.top),
2782
+ lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2783
+ paragraphLeft: toPageX(paragraphRect.left),
2784
+ paragraphTop: toPageY(paragraphRect.top),
2785
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
2786
+ paragraphHeight: paragraphRect.height / pixelsPerPoint
2787
+ };
2788
+ const widthReference = this.horizontalAnchorReference(
2789
+ anchor.getAttribute("data-docx-anchor-width-relative") ?? "margin",
2790
+ dims,
2791
+ pageNumber,
2792
+ context
2793
+ );
2794
+ const widthPercent = this.anchorNumber(anchor, "width-percent");
2795
+ if (widthPercent !== void 0) {
2796
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
2797
+ }
2798
+ const heightReference = this.verticalAnchorReference(
2799
+ anchor.getAttribute("data-docx-anchor-height-relative") ?? "margin",
2800
+ dims,
2801
+ pageNumber,
2802
+ context
2803
+ );
2804
+ const heightPercent = this.anchorNumber(anchor, "height-percent");
2805
+ if (heightPercent !== void 0 && anchor.dataset.docxAnchorAutofit !== "true") {
2806
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
2807
+ }
2808
+ const sizedRect = anchor.getBoundingClientRect();
2809
+ const width = sizedRect.width / pixelsPerPoint;
2810
+ const height = sizedRect.height / pixelsPerPoint;
2811
+ const horizontal2 = this.horizontalAnchorReference(
2812
+ anchor.getAttribute("data-docx-anchor-h-relative") ?? "column",
2813
+ dims,
2814
+ pageNumber,
2815
+ context
2816
+ );
2817
+ const vertical2 = this.verticalAnchorReference(
2818
+ anchor.getAttribute("data-docx-anchor-v-relative") ?? "paragraph",
2819
+ dims,
2820
+ pageNumber,
2821
+ context
2822
+ );
2823
+ const left2 = this.resolveAnchorAxis(
2824
+ horizontal2,
2825
+ width,
2826
+ anchor.getAttribute("data-docx-anchor-h-align"),
2827
+ this.anchorNumber(anchor, "h-offset"),
2828
+ pageNumber
2829
+ );
2830
+ const top2 = this.resolveAnchorAxis(
2831
+ vertical2,
2832
+ height,
2833
+ anchor.getAttribute("data-docx-anchor-v-align"),
2834
+ this.anchorNumber(anchor, "v-offset"),
2835
+ pageNumber
2836
+ );
2837
+ pageBox.appendChild(anchor);
2838
+ anchor.style.position = "absolute";
2839
+ anchor.style.left = `${left2}pt`;
2840
+ anchor.style.top = `${top2}pt`;
2841
+ anchor.style.margin = "0";
2842
+ }
2843
+ }
2844
+ anchorNumber(anchor, suffix) {
2845
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
2846
+ if (raw === null) return void 0;
2847
+ const value = Number(raw);
2848
+ return Number.isFinite(value) ? value : void 0;
2849
+ }
2850
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
2851
+ const leftMargin = { start: 0, size: dims.marginLeft };
2852
+ const rightMargin = {
2853
+ start: dims.marginLeft + dims.contentWidth,
2854
+ size: dims.marginRight
2855
+ };
2856
+ switch (relativeFrom) {
2857
+ case "page":
2858
+ return { start: 0, size: dims.pageWidth };
2859
+ case "character":
2860
+ return { start: context.staticLeft, size: 0 };
2861
+ case "leftMargin":
2862
+ return leftMargin;
2863
+ case "rightMargin":
2864
+ return rightMargin;
2865
+ case "insideMargin":
2866
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
2867
+ case "outsideMargin":
2868
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
2869
+ case "paragraph":
2870
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
2871
+ case "column":
2872
+ case "margin":
2873
+ default:
2874
+ return { start: dims.marginLeft, size: dims.contentWidth };
2875
+ }
2876
+ }
2877
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
2878
+ const topMargin = { start: 0, size: dims.marginTop };
2879
+ const bottomMargin = {
2880
+ start: dims.marginTop + dims.contentHeight,
2881
+ size: dims.marginBottom
2882
+ };
2883
+ switch (relativeFrom) {
2884
+ case "page":
2885
+ return { start: 0, size: dims.pageHeight };
2886
+ case "paragraph":
2887
+ return { start: context.paragraphTop, size: context.paragraphHeight };
2888
+ case "line":
2889
+ return { start: context.staticTop, size: context.lineHeight };
2890
+ case "topMargin":
2891
+ return topMargin;
2892
+ case "bottomMargin":
2893
+ return bottomMargin;
2894
+ case "insideMargin":
2895
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
2896
+ case "outsideMargin":
2897
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
2898
+ case "margin":
2899
+ default:
2900
+ return { start: dims.marginTop, size: dims.contentHeight };
2901
+ }
2902
+ }
2903
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
2904
+ if (offset !== void 0) return reference.start + offset;
2905
+ let fraction = 0;
2906
+ if (alignment === "center") fraction = 0.5;
2907
+ else if (alignment === "right" || alignment === "bottom") fraction = 1;
2908
+ else if (alignment === "inside") fraction = pageNumber % 2 === 1 ? 0 : 1;
2909
+ else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
2910
+ return reference.start + (reference.size - objectSize) * fraction;
2911
+ }
2728
2912
  /**
2729
2913
  * Creates a page container element.
2730
2914
  */
@@ -2751,22 +2935,22 @@ var Docxodus = (() => {
2751
2935
  pageBox.dataset.pageNumber = String(pageNumber);
2752
2936
  pageBox.dataset.sectionIndex = String(sectionIndex);
2753
2937
  pageBox.dataset.pageInSection = String(pageInSection);
2754
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
2938
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2755
2939
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
2756
2940
  if (headerSource) {
2757
2941
  const headerDiv = document.createElement("div");
2758
2942
  headerDiv.className = `${this.cssPrefix}header`;
2759
2943
  headerDiv.style.position = "absolute";
2760
- headerDiv.style.top = "0";
2944
+ headerDiv.style.top = `${bands.headerTop}pt`;
2945
+ headerDiv.style.bottom = "auto";
2761
2946
  headerDiv.style.left = `${dims.marginLeft}pt`;
2762
2947
  headerDiv.style.width = `${dims.contentWidth}pt`;
2763
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`;
2948
+ headerDiv.style.height = `${bands.headerHeight}pt`;
2764
2949
  headerDiv.style.overflow = "hidden";
2765
2950
  headerDiv.style.boxSizing = "border-box";
2766
2951
  headerDiv.style.display = "flex";
2767
2952
  headerDiv.style.flexDirection = "column";
2768
- headerDiv.style.justifyContent = "flex-end";
2769
- headerDiv.style.paddingBottom = "4pt";
2953
+ headerDiv.style.justifyContent = "flex-start";
2770
2954
  for (const child of Array.from(headerSource.childNodes)) {
2771
2955
  const clonedheaderDiv = child.cloneNode(true);
2772
2956
  if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
@@ -2774,8 +2958,8 @@ var Docxodus = (() => {
2774
2958
  }
2775
2959
  pageBox.appendChild(headerDiv);
2776
2960
  }
2777
- const contentAreaTop = effectiveHeights.headerHeight;
2778
- const contentAreaHeight = effectiveHeights.contentHeight;
2961
+ const contentAreaTop = bands.bodyTop;
2962
+ const contentAreaHeight = bands.bodyHeight;
2779
2963
  const contentArea = document.createElement("div");
2780
2964
  contentArea.className = `${this.cssPrefix}content`;
2781
2965
  contentArea.style.position = "absolute";
@@ -2790,23 +2974,23 @@ var Docxodus = (() => {
2790
2974
  pageBox.appendChild(contentArea);
2791
2975
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2792
2976
  if (footnoteIds.length > 0 || hasContinuation) {
2793
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
2977
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
2794
2978
  }
2795
2979
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
2796
2980
  if (footerSource) {
2797
2981
  const footerDiv = document.createElement("div");
2798
2982
  footerDiv.className = `${this.cssPrefix}footer`;
2799
2983
  footerDiv.style.position = "absolute";
2800
- footerDiv.style.bottom = "0";
2984
+ footerDiv.style.top = "auto";
2985
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
2801
2986
  footerDiv.style.left = `${dims.marginLeft}pt`;
2802
2987
  footerDiv.style.width = `${dims.contentWidth}pt`;
2803
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`;
2988
+ footerDiv.style.height = `${bands.footerHeight}pt`;
2804
2989
  footerDiv.style.overflow = "hidden";
2805
2990
  footerDiv.style.boxSizing = "border-box";
2806
2991
  footerDiv.style.display = "flex";
2807
2992
  footerDiv.style.flexDirection = "column";
2808
- footerDiv.style.justifyContent = "flex-start";
2809
- footerDiv.style.paddingTop = "4pt";
2993
+ footerDiv.style.justifyContent = "flex-end";
2810
2994
  for (const child of Array.from(footerSource.childNodes)) {
2811
2995
  const clonedfooterDiv = child.cloneNode(true);
2812
2996
  if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
@@ -2821,6 +3005,7 @@ var Docxodus = (() => {
2821
3005
  pageBox.appendChild(pageNum);
2822
3006
  }
2823
3007
  this.containerElement.appendChild(pageBox);
3008
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
2824
3009
  const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
2825
3010
  if (notesEl) {
2826
3011
  const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
@@ -6195,6 +6380,19 @@ var Docxodus = (() => {
6195
6380
  function isListBlock(block) {
6196
6381
  return !!block.querySelector(":scope > [data-list-marker]");
6197
6382
  }
6383
+ function ensureListMarkerSeparator(block) {
6384
+ const marker = block.querySelector(":scope > [data-list-marker]");
6385
+ if (!marker) return;
6386
+ const next = marker.nextElementSibling;
6387
+ if (next?.hasAttribute("data-editor-list-separator")) return;
6388
+ const separator = block.ownerDocument.createElement("span");
6389
+ separator.setAttribute("data-editor-list-separator", "");
6390
+ separator.setAttribute("data-list-marker", "true");
6391
+ separator.setAttribute("aria-hidden", "true");
6392
+ separator.style.display = "none";
6393
+ separator.textContent = " ";
6394
+ marker.after(separator);
6395
+ }
6198
6396
  var GENERATED_CHROME_SELECTOR = '[data-list-marker], a.footnote-ref, a.endnote-ref, a[class$="-backref"]';
6199
6397
  function isGeneratedChrome(node) {
6200
6398
  return node?.nodeType === 1 && !!node.matches?.(GENERATED_CHROME_SELECTOR);
@@ -7485,6 +7683,7 @@ var Docxodus = (() => {
7485
7683
  const unid = el.getAttribute("data-anchor");
7486
7684
  if (!unid || !this.anchorIdOf(el)) return;
7487
7685
  el.setAttribute("contenteditable", "true");
7686
+ ensureListMarkerSeparator(el);
7488
7687
  el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
7489
7688
  el.dataset.committedText = blockContentText(el);
7490
7689
  el.addEventListener("focus", () => {
@@ -8347,8 +8546,8 @@ var Docxodus = (() => {
8347
8546
  * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
8348
8547
  *
8349
8548
  * Body blocks only: Word disallows a note reference inside a header/footer story or inside
8350
- * another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new
8351
- * note renumbers the citations after it and can add a whole part.
8549
+ * another note, and the session rejects those with `AnchorWrongKind`. Reconciliation creates
8550
+ * the first notes section when needed and renumbers every affected citation in place.
8352
8551
  */
8353
8552
  insertFootnote(markdown = "New footnote.") {
8354
8553
  this.insertNote("footnote", markdown);
@@ -8740,7 +8939,7 @@ var Docxodus = (() => {
8740
8939
  * the current node — never a section div (multi-child) or the edit root. */
8741
8940
  unitWrapperOf(el) {
8742
8941
  let n = el;
8743
- while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
8942
+ 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) {
8744
8943
  n = n.parentElement;
8745
8944
  }
8746
8945
  return n;
@@ -8776,6 +8975,7 @@ var Docxodus = (() => {
8776
8975
  const pureRemoved = diff.removed.filter(
8777
8976
  (i) => !diff.substituted.some((s) => s.oldIndex === i) && !movedOld.has(i)
8778
8977
  );
8978
+ const appliedFresh = /* @__PURE__ */ new Set();
8779
8979
  const nodeAt = (j) => {
8780
8980
  const oi = diff.keep.get(j);
8781
8981
  if (oi !== void 0) return oldNodes[oi];
@@ -8783,7 +8983,7 @@ var Docxodus = (() => {
8783
8983
  if (movedOi !== void 0) return oldNodes[movedOi];
8784
8984
  if (subOldByNew.has(j)) return fresh.get(j);
8785
8985
  const f = fresh.get(j);
8786
- return f && f.isConnected ? f : null;
8986
+ return f && appliedFresh.has(j) ? f : null;
8787
8987
  };
8788
8988
  for (const j of pureAdded) {
8789
8989
  const el = fresh.get(j);
@@ -8798,6 +8998,7 @@ var Docxodus = (() => {
8798
8998
  if (prevW) prevW.after(el);
8799
8999
  else if (nextW) nextW.before(el);
8800
9000
  else return "insert with no anchored neighbor";
9001
+ appliedFresh.add(j);
8801
9002
  this.wireUnit(el, units[j]);
8802
9003
  }
8803
9004
  for (const i of pureRemoved) {
@@ -8836,13 +9037,13 @@ var Docxodus = (() => {
8836
9037
  if (anchorEl) this.wireBlock(anchorEl);
8837
9038
  root.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
8838
9039
  }
8839
- /** Old-sequence diff state for one notes section. `null` requests remount (DOM not
8840
- * stampable/consistent). */
9040
+ /** Old-sequence diff state for one notes section. `null` requests remount when an
9041
+ * existing list is not stampable; an absent list is the valid first-note case. */
8841
9042
  notesDiff(sectionClass, units) {
8842
9043
  const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
8843
9044
  const lis = ol ? Array.from(ol.children).filter((c) => c.tagName === "LI") : [];
8844
9045
  if (units.length === 0 && lis.length === 0) return { lis, diff: diffUnits([], []) };
8845
- if (!ol) return null;
9046
+ if (!ol) return { lis, diff: diffUnits([], units) };
8846
9047
  const tokens = [];
8847
9048
  for (const li of lis) {
8848
9049
  const unid = li.getAttribute("data-note-anchor");
@@ -8859,7 +9060,20 @@ var Docxodus = (() => {
8859
9060
  this.editRoot.querySelector(`section.${sectionClass}`)?.remove();
8860
9061
  return;
8861
9062
  }
8862
- const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
9063
+ let section = this.editRoot.querySelector(`:scope > section.${sectionClass}`);
9064
+ let ol = section?.querySelector(`:scope > ol`) ?? null;
9065
+ if (!ol) {
9066
+ if (!section) {
9067
+ section = document.createElement("section");
9068
+ section.className = sectionClass;
9069
+ const following = sectionClass === "footnotes" ? this.editRoot.querySelector(`:scope > section.endnotes`) : null;
9070
+ if (following) following.before(section);
9071
+ else this.editRoot.append(section);
9072
+ }
9073
+ if (!section.querySelector(":scope > hr")) section.prepend(document.createElement("hr"));
9074
+ ol = document.createElement("ol");
9075
+ section.append(ol);
9076
+ }
8863
9077
  const prefix = sectionClass === "footnotes" ? "fn" : "en";
8864
9078
  const nodes = [];
8865
9079
  for (let j = 0; j < units.length; j++) {
@@ -32,16 +32,22 @@ export interface PageDimensions {
32
32
  marginBottom: number;
33
33
  /** Left margin in points */
34
34
  marginLeft: number;
35
- /** Header distance from top of page in points */
35
+ /** `w:pgMar/@w:header` — page top edge to the TOP of the header story, in points. */
36
+ headerDistance: number;
37
+ /** `w:pgMar/@w:footer` — page bottom edge to the BOTTOM of the footer story, in points. */
38
+ footerDistance: number;
39
+ /** @deprecated Misnamed alias of {@link headerDistance} — it never held a height. */
36
40
  headerHeight: number;
37
- /** Footer distance from bottom of page in points */
41
+ /** @deprecated Misnamed alias of {@link footerDistance} — it never held a height. */
38
42
  footerHeight: number;
39
43
  }
40
44
  /** US Letter portrait — Word's default, and this module's fallback for an unstamped section. */
41
45
  export declare const DEFAULT_PAGE_WIDTH = 612;
42
46
  export declare const DEFAULT_PAGE_HEIGHT = 792;
43
47
  export declare const DEFAULT_MARGIN = 72;
44
- /** Default header/footer distance (0.5 inch). */
48
+ /** Default header/footer distance from the paper edge (0.5 inch). */
49
+ export declare const DEFAULT_HEADER_FOOTER_DISTANCE = 36;
50
+ /** @deprecated Misnamed alias of {@link DEFAULT_HEADER_FOOTER_DISTANCE}. */
45
51
  export declare const DEFAULT_HEADER_FOOTER_HEIGHT = 36;
46
52
  /** Converts pixels to points (assuming 96 DPI screen). */
47
53
  export declare function pxToPt(px: number): number;
@@ -49,6 +55,47 @@ export declare function pxToPt(px: number): number;
49
55
  export declare function ptToPx(pt: number): number;
50
56
  /** Parses page dimensions from a section wrapper's data attributes. */
51
57
  export declare function parseSectionDimensions(section: HTMLElement): PageDimensions;
58
+ /**
59
+ * Where a page's three bands sit, in points measured from the page's TOP edge.
60
+ *
61
+ * Resolved by {@link resolvePageBands} from the section's page setup plus the measured
62
+ * height of the running content actually selected for that page.
63
+ */
64
+ export interface PageBands {
65
+ /** Top of the header band; it grows downward from here. */
66
+ headerTop: number;
67
+ /** Height of the header band (the measured header story, clipped to the body's top). */
68
+ headerHeight: number;
69
+ /** Top of the body text band. */
70
+ bodyTop: number;
71
+ /** Height of the body text band. */
72
+ bodyHeight: number;
73
+ /** Top of the footer band; it grows upward, so its BOTTOM is the fixed edge. */
74
+ footerTop: number;
75
+ /** Height of the footer band (the measured footer story, clipped to the body's bottom). */
76
+ footerHeight: number;
77
+ }
78
+ /**
79
+ * A body band this short cannot hold a single line, and a page that can hold nothing sends
80
+ * every block down the oversized-block path. Running content may push the body — Word does
81
+ * that — but it may not delete it, so a pathological header/footer gives this much back.
82
+ */
83
+ export declare const MIN_BODY_BAND_PT = 12;
84
+ /**
85
+ * Word's page band model.
86
+ *
87
+ * `w:header`/`w:footer` are DISTANCES from the paper edge to the running content, and
88
+ * `w:top`/`w:bottom` are distances from the paper edge to the body text — four independent
89
+ * numbers, not two nested boxes. The header is laid out from `headerDistance` downward and
90
+ * the footer from `footerDistance` upward; the body then starts at the top margin unless the
91
+ * header has already run past it, and ends at the bottom margin unless the footer has
92
+ * already climbed above it.
93
+ *
94
+ * Anchoring the bands to the margins instead — header bottom-aligned to `marginTop`, footer
95
+ * top-aligned to `pageHeight - marginBottom` — collapses both toward the body by exactly
96
+ * `margin - distance`, which is what this function replaced.
97
+ */
98
+ export declare function resolvePageBands(dims: PageDimensions, headerContentHeight: number, footerContentHeight: number): PageBands;
52
99
  /** Every section wrapper under `root`, or `root` itself when the render stamped none. */
53
100
  export declare function sectionWrappers(root: HTMLElement): HTMLElement[];
54
101
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"page-geometry.d.ts","sourceRoot":"","sources":["../src/page-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,iDAAiD;AACjD,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C,0DAA0D;AAC1D,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAwB3E;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE,CAGhE;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAKhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,IAAI,CAqBN"}
1
+ {"version":3,"file":"page-geometry.d.ts","sourceRoot":"","sources":["../src/page-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;IACvB,2FAA2F;IAC3F,cAAc,EAAE,MAAM,CAAC;IACvB,qFAAqF;IACrF,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,qEAAqE;AACrE,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,4EAA4E;AAC5E,eAAO,MAAM,4BAA4B,KAAiC,CAAC;AAE3E,0DAA0D;AAC1D,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CA8B3E;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,SAAS,CAkCX;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE,CAGhE;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAKhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,IAAI,CAqBN"}