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 +22 -0
- package/README.md +0 -1
- package/dist/editor.bundle.js +290 -76
- package/dist/editor.d.ts +4 -4
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +55 -8
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +290 -76
- package/dist/embed.iife.js +290 -76
- package/dist/page-geometry.d.ts +50 -3
- package/dist/page-geometry.d.ts.map +1 -1
- package/dist/page-geometry.js +63 -6
- package/dist/page-geometry.js.map +1 -1
- package/dist/pagination.bundle.js +253 -68
- package/dist/pagination.d.ts +33 -8
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +198 -78
- package/dist/pagination.js.map +1 -1
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +6 -6
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/package.json +13 -5
- package/dist/wasm/index.html +0 -238
- package/dist/wasm/main.js +0 -26
package/dist/pagination.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
|
package/dist/pagination.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and flows it across fixed-size page containers based on document dimensions.
|
|
6
6
|
*/
|
|
7
7
|
import { formatPageNumber } from "./page-number-format.js";
|
|
8
|
-
import { DEFAULT_MARGIN, DEFAULT_PAGE_WIDTH, parseSectionDimensions, ptToPx, pxToPt, } from "./page-geometry.js";
|
|
8
|
+
import { DEFAULT_MARGIN, DEFAULT_PAGE_WIDTH, parseSectionDimensions, ptToPx, pxToPt, resolvePageBands, } from "./page-geometry.js";
|
|
9
9
|
// Default letter size in points (612 x 792 = 8.5" x 11")
|
|
10
10
|
// Maximum percentage of content height that footnotes can occupy
|
|
11
11
|
// This allows footnotes to expand upward into body content space when needed
|
|
@@ -276,7 +276,7 @@ export class PaginationEngine {
|
|
|
276
276
|
* send it through the oversized-block fallback again.
|
|
277
277
|
*/
|
|
278
278
|
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
279
|
-
return Math.min(this.
|
|
279
|
+
return Math.min(this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight, this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight, this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight);
|
|
280
280
|
}
|
|
281
281
|
/**
|
|
282
282
|
* Builds a clone of a simple table wrapper containing a contiguous run of rows.
|
|
@@ -724,7 +724,7 @@ export class PaginationEngine {
|
|
|
724
724
|
return 0;
|
|
725
725
|
}
|
|
726
726
|
// Measure in the SAME styling context the notes render in: `.page-footnotes` carries
|
|
727
|
-
// font-size
|
|
727
|
+
// its own font-size and line-height, so measuring without the class sizes the note
|
|
728
728
|
// block against body type and the reserve can never match what is drawn.
|
|
729
729
|
// Create a temporary measurement container
|
|
730
730
|
const measureContainer = document.createElement("div");
|
|
@@ -884,7 +884,7 @@ export class PaginationEngine {
|
|
|
884
884
|
/**
|
|
885
885
|
* Adds footnotes to a page container, including continuation content.
|
|
886
886
|
*/
|
|
887
|
-
addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
|
|
887
|
+
addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
|
|
888
888
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
889
889
|
if (footnoteIds.length === 0 && !hasContinuation) {
|
|
890
890
|
return;
|
|
@@ -895,11 +895,13 @@ export class PaginationEngine {
|
|
|
895
895
|
// Create a set of partial footnote IDs for quick lookup
|
|
896
896
|
const partialFootnoteIds = new Set(partialFootnotes?.map(p => p.footnoteId) || []);
|
|
897
897
|
// Calculate max height for footnotes area (content height minus margin for body content)
|
|
898
|
-
const maxFootnoteHeight = Math.min(footnoteHeight,
|
|
898
|
+
const maxFootnoteHeight = Math.min(footnoteHeight, bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO);
|
|
899
899
|
const footnotesDiv = document.createElement("div");
|
|
900
900
|
footnotesDiv.className = `${this.cssPrefix}footnotes`;
|
|
901
901
|
footnotesDiv.style.position = "absolute";
|
|
902
|
-
|
|
902
|
+
// Notes sit at the FOOT OF THE BODY BAND, not at the bottom margin: a footer taller than
|
|
903
|
+
// its margin raises that edge, and anchoring to the raw margin would draw notes over it.
|
|
904
|
+
footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
|
|
903
905
|
footnotesDiv.style.left = `${dims.marginLeft}pt`;
|
|
904
906
|
footnotesDiv.style.width = `${dims.contentWidth}pt`;
|
|
905
907
|
footnotesDiv.style.boxSizing = "border-box";
|
|
@@ -991,63 +993,37 @@ export class PaginationEngine {
|
|
|
991
993
|
return sectionHf.footerDefault;
|
|
992
994
|
}
|
|
993
995
|
/**
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
996
|
+
* The header, body, and footer bands for one page position.
|
|
997
|
+
*
|
|
998
|
+
* The single owner of "where does anything sit vertically on this page" — placement in
|
|
999
|
+
* {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
|
|
1000
|
+
* read it, so those three cannot disagree about where the body ends.
|
|
1001
|
+
*
|
|
1002
|
+
* Deterministic: it depends only on the section's page setup and the registry's pre-measured
|
|
1003
|
+
* story heights, never on the page's content, which is what keeps it lazy-loading compatible.
|
|
999
1004
|
*/
|
|
1000
|
-
|
|
1005
|
+
getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
|
|
1001
1006
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
measuredHeaderHeight = sectionHf.headerDefaultHeight;
|
|
1015
|
-
}
|
|
1016
|
-
if (measuredHeaderHeight != null) {
|
|
1017
|
-
// Use the larger of margin height or measured content height
|
|
1018
|
-
headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
// Determine effective footer height
|
|
1022
|
-
let footerHeight = dims.marginBottom;
|
|
1023
|
-
if (sectionHf) {
|
|
1024
|
-
let measuredFooterHeight;
|
|
1025
|
-
// Select the appropriate footer height based on page position
|
|
1026
|
-
if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
|
|
1027
|
-
measuredFooterHeight = sectionHf.footerFirstHeight;
|
|
1028
|
-
}
|
|
1029
|
-
else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
|
|
1030
|
-
measuredFooterHeight = sectionHf.footerEvenHeight;
|
|
1031
|
-
}
|
|
1032
|
-
else if (sectionHf.footerDefaultHeight != null) {
|
|
1033
|
-
measuredFooterHeight = sectionHf.footerDefaultHeight;
|
|
1034
|
-
}
|
|
1035
|
-
if (measuredFooterHeight != null) {
|
|
1036
|
-
// Use the larger of margin height or measured content height
|
|
1037
|
-
footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
// Calculate effective content height
|
|
1041
|
-
// contentHeight from dims is: pageHeight - marginTop - marginBottom
|
|
1042
|
-
// We need to adjust for any header/footer expansion beyond margins
|
|
1043
|
-
const headerExpansion = headerHeight - dims.marginTop;
|
|
1044
|
-
const footerExpansion = footerHeight - dims.marginBottom;
|
|
1045
|
-
const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
|
|
1046
|
-
return { headerHeight, footerHeight, contentHeight };
|
|
1007
|
+
return resolvePageBands(dims, this.selectStoryHeight(sectionHf?.headerFirstHeight, sectionHf?.headerEvenHeight, sectionHf?.headerDefaultHeight, pageInSection, globalPageNumber), this.selectStoryHeight(sectionHf?.footerFirstHeight, sectionHf?.footerEvenHeight, sectionHf?.footerDefaultHeight, pageInSection, globalPageNumber));
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* The measured height of the running story this page position selects, mirroring
|
|
1011
|
+
* {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
|
|
1012
|
+
*/
|
|
1013
|
+
selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
|
|
1014
|
+
if (pageInSection === 1 && first != null)
|
|
1015
|
+
return first;
|
|
1016
|
+
if (globalPageNumber % 2 === 0 && even != null)
|
|
1017
|
+
return even;
|
|
1018
|
+
return fallback ?? 0;
|
|
1047
1019
|
}
|
|
1048
1020
|
/**
|
|
1049
1021
|
* Measures the content height of a header or footer element.
|
|
1050
|
-
*
|
|
1022
|
+
*
|
|
1023
|
+
* This is what tells {@link resolvePageBands} whether the story stays inside its margin or
|
|
1024
|
+
* pushes the body, so it must measure the story ALONE — any padding added here would have to
|
|
1025
|
+
* be added to the rendered band too, and the two drifting apart is exactly how a header
|
|
1026
|
+
* silently starts overlapping body text.
|
|
1051
1027
|
*/
|
|
1052
1028
|
measureHeaderFooterHeight(source, contentWidth) {
|
|
1053
1029
|
// Create a temporary measurement container
|
|
@@ -1056,8 +1032,6 @@ export class PaginationEngine {
|
|
|
1056
1032
|
measureContainer.style.visibility = "hidden";
|
|
1057
1033
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1058
1034
|
measureContainer.style.left = "-9999px";
|
|
1059
|
-
// Add padding to match the actual rendering
|
|
1060
|
-
measureContainer.style.paddingBottom = "4pt";
|
|
1061
1035
|
// Clone and add the header/footer content
|
|
1062
1036
|
for (const child of Array.from(source.childNodes)) {
|
|
1063
1037
|
measureContainer.appendChild(child.cloneNode(true));
|
|
@@ -1083,7 +1057,7 @@ export class PaginationEngine {
|
|
|
1083
1057
|
// Track page number within this section for first-page header/footer selection
|
|
1084
1058
|
let pageInSection = 1;
|
|
1085
1059
|
// Get effective content height for first page (accounts for header/footer sizes)
|
|
1086
|
-
let {
|
|
1060
|
+
let { bodyHeight: effectiveContentHeight } = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1087
1061
|
let remainingHeight = effectiveContentHeight;
|
|
1088
1062
|
// Track the previous block's bottom margin for margin collapsing
|
|
1089
1063
|
let prevMarginBottomPt = 0;
|
|
@@ -1123,8 +1097,8 @@ export class PaginationEngine {
|
|
|
1123
1097
|
pageInSection++;
|
|
1124
1098
|
currentContent = [];
|
|
1125
1099
|
// Get effective content height for new page position
|
|
1126
|
-
const
|
|
1127
|
-
effectiveContentHeight =
|
|
1100
|
+
const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1101
|
+
effectiveContentHeight = newBands.bodyHeight;
|
|
1128
1102
|
remainingHeight = effectiveContentHeight;
|
|
1129
1103
|
prevMarginBottomPt = 0; // Reset margin tracking for new page
|
|
1130
1104
|
currentFootnoteIds = []; // Reset footnotes for new page
|
|
@@ -1188,14 +1162,14 @@ export class PaginationEngine {
|
|
|
1188
1162
|
additionalChainFootnoteHeight;
|
|
1189
1163
|
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1190
1164
|
if (currentChainHeight > currentAvailableHeight) {
|
|
1191
|
-
const
|
|
1165
|
+
const nextPageBands = this.getPageBands(dims, sectionIndex, pageInSection + 1, pageNumber + 1);
|
|
1192
1166
|
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(keepChain, 0, true);
|
|
1193
1167
|
// finishPage transfers this continuation to the new page's
|
|
1194
1168
|
// currentContinuation state, so include it in the destination
|
|
1195
1169
|
// page's footnote reservation before deciding to move the chain.
|
|
1196
1170
|
const freshChainFootnoteHeight = this.measureFootnotesHeight(newChainFootnoteIds, dims.contentWidth, nextPageContinuation);
|
|
1197
1171
|
if (freshChainBodyHeight + freshChainFootnoteHeight <=
|
|
1198
|
-
|
|
1172
|
+
nextPageBands.bodyHeight) {
|
|
1199
1173
|
finishPage();
|
|
1200
1174
|
}
|
|
1201
1175
|
}
|
|
@@ -1425,6 +1399,144 @@ export class PaginationEngine {
|
|
|
1425
1399
|
el.setAttribute("contenteditable", "false");
|
|
1426
1400
|
}
|
|
1427
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
|
|
1404
|
+
*
|
|
1405
|
+
* The converter deliberately carries the OOXML bases and offsets as data instead of flattening
|
|
1406
|
+
* them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
|
|
1407
|
+
* bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
|
|
1408
|
+
* from the clipped text column into the page box and position it in one shared point space.
|
|
1409
|
+
*/
|
|
1410
|
+
positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
|
|
1411
|
+
const anchors = Array.from(contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]'));
|
|
1412
|
+
if (anchors.length === 0)
|
|
1413
|
+
return;
|
|
1414
|
+
const pageRect = pageBox.getBoundingClientRect();
|
|
1415
|
+
const pixelsPerPoint = pageRect.width / dims.pageWidth;
|
|
1416
|
+
if (!(pixelsPerPoint > 0))
|
|
1417
|
+
return;
|
|
1418
|
+
for (const anchor of anchors) {
|
|
1419
|
+
const staticRect = anchor.getBoundingClientRect();
|
|
1420
|
+
const paragraph = anchor.closest('p, h1, h2, h3, h4, h5, h6');
|
|
1421
|
+
const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
|
|
1422
|
+
const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
|
|
1423
|
+
const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
|
|
1424
|
+
const context = {
|
|
1425
|
+
staticLeft: toPageX(staticRect.left),
|
|
1426
|
+
staticTop: toPageY(staticRect.top),
|
|
1427
|
+
lineHeight: paragraph
|
|
1428
|
+
? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint
|
|
1429
|
+
: staticRect.height / pixelsPerPoint,
|
|
1430
|
+
paragraphLeft: toPageX(paragraphRect.left),
|
|
1431
|
+
paragraphTop: toPageY(paragraphRect.top),
|
|
1432
|
+
paragraphWidth: paragraphRect.width / pixelsPerPoint,
|
|
1433
|
+
paragraphHeight: paragraphRect.height / pixelsPerPoint,
|
|
1434
|
+
};
|
|
1435
|
+
const widthReference = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-width-relative') ?? 'margin', dims, pageNumber, context);
|
|
1436
|
+
const widthPercent = this.anchorNumber(anchor, 'width-percent');
|
|
1437
|
+
if (widthPercent !== undefined) {
|
|
1438
|
+
anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
|
|
1439
|
+
}
|
|
1440
|
+
const heightReference = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-height-relative') ?? 'margin', dims, pageNumber, context);
|
|
1441
|
+
const heightPercent = this.anchorNumber(anchor, 'height-percent');
|
|
1442
|
+
if (heightPercent !== undefined && anchor.dataset.docxAnchorAutofit !== 'true') {
|
|
1443
|
+
anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
|
|
1444
|
+
}
|
|
1445
|
+
// Read the final border-box size after relative sizing and before reparenting. The stored
|
|
1446
|
+
// wp:extent remains the fallback; wps:bodyPr insets stay inside it through border-box sizing.
|
|
1447
|
+
const sizedRect = anchor.getBoundingClientRect();
|
|
1448
|
+
const width = sizedRect.width / pixelsPerPoint;
|
|
1449
|
+
const height = sizedRect.height / pixelsPerPoint;
|
|
1450
|
+
const horizontal = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-h-relative') ?? 'column', dims, pageNumber, context);
|
|
1451
|
+
const vertical = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-v-relative') ?? 'paragraph', dims, pageNumber, context);
|
|
1452
|
+
const left = this.resolveAnchorAxis(horizontal, width, anchor.getAttribute('data-docx-anchor-h-align'), this.anchorNumber(anchor, 'h-offset'), pageNumber);
|
|
1453
|
+
const top = this.resolveAnchorAxis(vertical, height, anchor.getAttribute('data-docx-anchor-v-align'), this.anchorNumber(anchor, 'v-offset'), pageNumber);
|
|
1454
|
+
pageBox.appendChild(anchor);
|
|
1455
|
+
anchor.style.position = 'absolute';
|
|
1456
|
+
anchor.style.left = `${left}pt`;
|
|
1457
|
+
anchor.style.top = `${top}pt`;
|
|
1458
|
+
anchor.style.margin = '0';
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
anchorNumber(anchor, suffix) {
|
|
1462
|
+
const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
|
|
1463
|
+
if (raw === null)
|
|
1464
|
+
return undefined;
|
|
1465
|
+
const value = Number(raw);
|
|
1466
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1467
|
+
}
|
|
1468
|
+
horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
|
|
1469
|
+
const leftMargin = { start: 0, size: dims.marginLeft };
|
|
1470
|
+
const rightMargin = {
|
|
1471
|
+
start: dims.marginLeft + dims.contentWidth,
|
|
1472
|
+
size: dims.marginRight,
|
|
1473
|
+
};
|
|
1474
|
+
switch (relativeFrom) {
|
|
1475
|
+
case 'page':
|
|
1476
|
+
return { start: 0, size: dims.pageWidth };
|
|
1477
|
+
case 'character':
|
|
1478
|
+
return { start: context.staticLeft, size: 0 };
|
|
1479
|
+
case 'leftMargin':
|
|
1480
|
+
return leftMargin;
|
|
1481
|
+
case 'rightMargin':
|
|
1482
|
+
return rightMargin;
|
|
1483
|
+
case 'insideMargin':
|
|
1484
|
+
return pageNumber % 2 === 1 ? leftMargin : rightMargin;
|
|
1485
|
+
case 'outsideMargin':
|
|
1486
|
+
return pageNumber % 2 === 1 ? rightMargin : leftMargin;
|
|
1487
|
+
case 'paragraph':
|
|
1488
|
+
// Not part of ST_RelFromH, but accepting it gives malformed/legacy producers the
|
|
1489
|
+
// intuitive base without conflating it with the whole text column.
|
|
1490
|
+
return { start: context.paragraphLeft, size: context.paragraphWidth };
|
|
1491
|
+
case 'column':
|
|
1492
|
+
case 'margin':
|
|
1493
|
+
default:
|
|
1494
|
+
// The paginator currently lays out one text column per section, so the column and page
|
|
1495
|
+
// margin boxes coincide. Keeping the names distinct here leaves multi-column geometry
|
|
1496
|
+
// localized to this resolver when that layout support is added.
|
|
1497
|
+
return { start: dims.marginLeft, size: dims.contentWidth };
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
|
|
1501
|
+
const topMargin = { start: 0, size: dims.marginTop };
|
|
1502
|
+
const bottomMargin = {
|
|
1503
|
+
start: dims.marginTop + dims.contentHeight,
|
|
1504
|
+
size: dims.marginBottom,
|
|
1505
|
+
};
|
|
1506
|
+
switch (relativeFrom) {
|
|
1507
|
+
case 'page':
|
|
1508
|
+
return { start: 0, size: dims.pageHeight };
|
|
1509
|
+
case 'paragraph':
|
|
1510
|
+
return { start: context.paragraphTop, size: context.paragraphHeight };
|
|
1511
|
+
case 'line':
|
|
1512
|
+
return { start: context.staticTop, size: context.lineHeight };
|
|
1513
|
+
case 'topMargin':
|
|
1514
|
+
return topMargin;
|
|
1515
|
+
case 'bottomMargin':
|
|
1516
|
+
return bottomMargin;
|
|
1517
|
+
case 'insideMargin':
|
|
1518
|
+
return pageNumber % 2 === 1 ? topMargin : bottomMargin;
|
|
1519
|
+
case 'outsideMargin':
|
|
1520
|
+
return pageNumber % 2 === 1 ? bottomMargin : topMargin;
|
|
1521
|
+
case 'margin':
|
|
1522
|
+
default:
|
|
1523
|
+
return { start: dims.marginTop, size: dims.contentHeight };
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
|
|
1527
|
+
if (offset !== undefined)
|
|
1528
|
+
return reference.start + offset;
|
|
1529
|
+
let fraction = 0;
|
|
1530
|
+
if (alignment === 'center')
|
|
1531
|
+
fraction = 0.5;
|
|
1532
|
+
else if (alignment === 'right' || alignment === 'bottom')
|
|
1533
|
+
fraction = 1;
|
|
1534
|
+
else if (alignment === 'inside')
|
|
1535
|
+
fraction = pageNumber % 2 === 1 ? 0 : 1;
|
|
1536
|
+
else if (alignment === 'outside')
|
|
1537
|
+
fraction = pageNumber % 2 === 1 ? 1 : 0;
|
|
1538
|
+
return reference.start + (reference.size - objectSize) * fraction;
|
|
1539
|
+
}
|
|
1428
1540
|
/**
|
|
1429
1541
|
* Creates a page container element.
|
|
1430
1542
|
*/
|
|
@@ -1464,24 +1576,27 @@ export class PaginationEngine {
|
|
|
1464
1576
|
// Needed by substitutePageNumberFields: a section that restarts numbering counts from its own
|
|
1465
1577
|
// first page, not from the document's.
|
|
1466
1578
|
pageBox.dataset.pageInSection = String(pageInSection);
|
|
1467
|
-
//
|
|
1468
|
-
const
|
|
1579
|
+
// Where the three bands sit on this page (no re-measurement needed)
|
|
1580
|
+
const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1469
1581
|
// Add header if available for this section/page
|
|
1470
1582
|
const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
|
|
1471
1583
|
if (headerSource) {
|
|
1472
1584
|
const headerDiv = document.createElement("div");
|
|
1473
1585
|
headerDiv.className = `${this.cssPrefix}header`;
|
|
1474
1586
|
headerDiv.style.position = "absolute";
|
|
1475
|
-
|
|
1587
|
+
// `w:header` is the distance to the TOP of the story, and the story grows downward from
|
|
1588
|
+
// there; `flex-start` therefore pins the edge the OOXML actually declares, and content
|
|
1589
|
+
// taller than the measurement clips at the bottom rather than sliding up the page.
|
|
1590
|
+
headerDiv.style.top = `${bands.headerTop}pt`;
|
|
1591
|
+
headerDiv.style.bottom = "auto";
|
|
1476
1592
|
headerDiv.style.left = `${dims.marginLeft}pt`;
|
|
1477
1593
|
headerDiv.style.width = `${dims.contentWidth}pt`;
|
|
1478
|
-
headerDiv.style.height = `${
|
|
1594
|
+
headerDiv.style.height = `${bands.headerHeight}pt`;
|
|
1479
1595
|
headerDiv.style.overflow = "hidden";
|
|
1480
1596
|
headerDiv.style.boxSizing = "border-box";
|
|
1481
1597
|
headerDiv.style.display = "flex";
|
|
1482
1598
|
headerDiv.style.flexDirection = "column";
|
|
1483
|
-
headerDiv.style.justifyContent = "flex-
|
|
1484
|
-
headerDiv.style.paddingBottom = "4pt"; // Small gap before content area
|
|
1599
|
+
headerDiv.style.justifyContent = "flex-start";
|
|
1485
1600
|
// Clone the header content (skip the wrapper div's data attributes)
|
|
1486
1601
|
for (const child of Array.from(headerSource.childNodes)) {
|
|
1487
1602
|
const clonedheaderDiv = child.cloneNode(true);
|
|
@@ -1492,8 +1607,8 @@ export class PaginationEngine {
|
|
|
1492
1607
|
pageBox.appendChild(headerDiv);
|
|
1493
1608
|
}
|
|
1494
1609
|
// Create content area using pre-computed effective heights
|
|
1495
|
-
const contentAreaTop =
|
|
1496
|
-
const contentAreaHeight =
|
|
1610
|
+
const contentAreaTop = bands.bodyTop;
|
|
1611
|
+
const contentAreaHeight = bands.bodyHeight;
|
|
1497
1612
|
const contentArea = document.createElement("div");
|
|
1498
1613
|
contentArea.className = `${this.cssPrefix}content`;
|
|
1499
1614
|
contentArea.style.position = "absolute";
|
|
@@ -1510,7 +1625,7 @@ export class PaginationEngine {
|
|
|
1510
1625
|
// Add footnotes if any references appear on this page (or continuation from previous)
|
|
1511
1626
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
1512
1627
|
if (footnoteIds.length > 0 || hasContinuation) {
|
|
1513
|
-
this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
|
|
1628
|
+
this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
|
|
1514
1629
|
}
|
|
1515
1630
|
// Add footer if available for this section/page
|
|
1516
1631
|
const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
|
|
@@ -1518,16 +1633,18 @@ export class PaginationEngine {
|
|
|
1518
1633
|
const footerDiv = document.createElement("div");
|
|
1519
1634
|
footerDiv.className = `${this.cssPrefix}footer`;
|
|
1520
1635
|
footerDiv.style.position = "absolute";
|
|
1521
|
-
|
|
1636
|
+
// `w:footer` is the distance to the BOTTOM of the story, and the story grows upward from
|
|
1637
|
+
// there — the mirror of the header, so `flex-end` and a bottom anchor.
|
|
1638
|
+
footerDiv.style.top = "auto";
|
|
1639
|
+
footerDiv.style.bottom = `${dims.footerDistance}pt`;
|
|
1522
1640
|
footerDiv.style.left = `${dims.marginLeft}pt`;
|
|
1523
1641
|
footerDiv.style.width = `${dims.contentWidth}pt`;
|
|
1524
|
-
footerDiv.style.height = `${
|
|
1642
|
+
footerDiv.style.height = `${bands.footerHeight}pt`;
|
|
1525
1643
|
footerDiv.style.overflow = "hidden";
|
|
1526
1644
|
footerDiv.style.boxSizing = "border-box";
|
|
1527
1645
|
footerDiv.style.display = "flex";
|
|
1528
1646
|
footerDiv.style.flexDirection = "column";
|
|
1529
|
-
footerDiv.style.justifyContent = "flex-
|
|
1530
|
-
footerDiv.style.paddingTop = "4pt"; // Small gap after content area
|
|
1647
|
+
footerDiv.style.justifyContent = "flex-end";
|
|
1531
1648
|
// Clone the footer content (skip the wrapper div's data attributes)
|
|
1532
1649
|
for (const child of Array.from(footerSource.childNodes)) {
|
|
1533
1650
|
const clonedfooterDiv = child.cloneNode(true);
|
|
@@ -1546,6 +1663,9 @@ export class PaginationEngine {
|
|
|
1546
1663
|
}
|
|
1547
1664
|
// Add to container
|
|
1548
1665
|
this.containerElement.appendChild(pageBox);
|
|
1666
|
+
// Floating objects need the final page and anchor-paragraph boxes. Resolve them only after
|
|
1667
|
+
// append, when getBoundingClientRect() is meaningful, and lift them out of content clipping.
|
|
1668
|
+
this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
|
|
1549
1669
|
// Body and notes must occupy DISJOINT bands. The note block is absolutely positioned against
|
|
1550
1670
|
// the page bottom and grows upward, while the content area spans the whole text height — so
|
|
1551
1671
|
// nothing but the layout loop's arithmetic keeps them apart, and any disagreement between the
|