docxodus 9.7.0 → 9.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +0 -1
- package/dist/editor.bundle.js +454 -99
- 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 +454 -99
- package/dist/embed.iife.js +454 -99
- 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 +416 -91
- package/dist/pagination.d.ts +61 -8
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +353 -105
- package/dist/pagination.js.map +1 -1
- package/dist/ribbon-chrome.d.ts +1 -1
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +1 -0
- package/dist/ribbon-chrome.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.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm.br +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.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.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.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/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/System.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +35 -35
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
- package/package.json +14 -5
- package/dist/wasm/index.html +0 -238
- package/dist/wasm/main.js +0 -26
package/dist/pagination.js
CHANGED
|
@@ -5,7 +5,20 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Whether two sections describe the same physical page box. A continuous section
|
|
11
|
+
* break is only honorable when it does — a changed page size or margin set forces
|
|
12
|
+
* a real page break, which is Word's behavior too.
|
|
13
|
+
*/
|
|
14
|
+
function samePageBox(a, b) {
|
|
15
|
+
return (a.pageWidth === b.pageWidth &&
|
|
16
|
+
a.pageHeight === b.pageHeight &&
|
|
17
|
+
a.marginTop === b.marginTop &&
|
|
18
|
+
a.marginRight === b.marginRight &&
|
|
19
|
+
a.marginBottom === b.marginBottom &&
|
|
20
|
+
a.marginLeft === b.marginLeft);
|
|
21
|
+
}
|
|
9
22
|
// Default letter size in points (612 x 792 = 8.5" x 11")
|
|
10
23
|
// Maximum percentage of content height that footnotes can occupy
|
|
11
24
|
// This allows footnotes to expand upward into body content space when needed
|
|
@@ -63,20 +76,44 @@ export class PaginationEngine {
|
|
|
63
76
|
this.pageNumbering = this.parsePageNumbering(sections);
|
|
64
77
|
// If no sections found, treat the entire staging content as one section
|
|
65
78
|
const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
|
|
79
|
+
const runs = [];
|
|
66
80
|
for (const section of sectionsToProcess) {
|
|
67
|
-
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
68
81
|
const dims = parseSectionDimensions(section);
|
|
82
|
+
const previous = runs[runs.length - 1];
|
|
83
|
+
if (previous &&
|
|
84
|
+
section.dataset.sectionType === "continuous" &&
|
|
85
|
+
samePageBox(previous.dims, dims)) {
|
|
86
|
+
previous.sections.push(section);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
runs.push({
|
|
90
|
+
sections: [section],
|
|
91
|
+
sectionIndex: parseInt(section.dataset.sectionIndex || "0", 10),
|
|
92
|
+
dims,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const run of runs) {
|
|
69
97
|
// Make staging visible for measurement
|
|
70
98
|
this.stagingElement.style.visibility = "hidden";
|
|
71
99
|
this.stagingElement.style.position = "absolute";
|
|
72
100
|
this.stagingElement.style.left = "-9999px";
|
|
73
101
|
this.stagingElement.style.display = "block";
|
|
74
|
-
|
|
75
|
-
section.
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
const blocks = [];
|
|
103
|
+
for (const section of run.sections) {
|
|
104
|
+
// Set width for accurate line wrapping
|
|
105
|
+
section.style.width = `${run.dims.contentWidth}pt`;
|
|
106
|
+
const columnCount = parseInt(section.dataset.cols || "1", 10);
|
|
107
|
+
if (columnCount > 1) {
|
|
108
|
+
const gap = parseFloat(section.dataset.colGap || "");
|
|
109
|
+
blocks.push(...this.buildColumnBlocks(section, run.dims, run.sectionIndex, columnCount, Number.isFinite(gap) ? gap : 36));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
blocks.push(...this.measureBlocks(section, run.dims));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
78
115
|
// Flow blocks into pages
|
|
79
|
-
const sectionPages = this.flowToPages(blocks, dims, pageNumber, sectionIndex);
|
|
116
|
+
const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
|
|
80
117
|
pages.push(...sectionPages);
|
|
81
118
|
pageNumber += sectionPages.length;
|
|
82
119
|
}
|
|
@@ -176,7 +213,70 @@ export class PaginationEngine {
|
|
|
176
213
|
keepLines: child.dataset.keepLines === "true",
|
|
177
214
|
pageBreakBefore: child.dataset.pageBreakBefore === "true",
|
|
178
215
|
isPageBreak,
|
|
216
|
+
isWordParagraph: this.isWordParagraphElement(child),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return blocks;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Flows a multi-column (`w:cols`) section's children into CSS-multicol container
|
|
223
|
+
* blocks. Word lays such a section out as N columns inside the same body extent;
|
|
224
|
+
* a balanced `column-count` container reproduces that geometry, and the paginator
|
|
225
|
+
* then places each container as one ordinary measured block. A container grows
|
|
226
|
+
* greedily until its balanced height would exceed the smallest page body available
|
|
227
|
+
* to the section, so a long columned section still splits across pages at block
|
|
228
|
+
* boundaries. Each child lands in exactly one container, so anchors never
|
|
229
|
+
* duplicate. An explicit page break child passes through as its own block, which
|
|
230
|
+
* ends the current container and lets the normal flow logic turn the page.
|
|
231
|
+
*/
|
|
232
|
+
buildColumnBlocks(section, dims, sectionIndex, columnCount, columnGapPt) {
|
|
233
|
+
const children = Array.from(section.children);
|
|
234
|
+
const blocks = [];
|
|
235
|
+
const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
236
|
+
const isBreak = (child) => child.dataset.pageBreak === "true" ||
|
|
237
|
+
child.classList.contains(`${this.cssPrefix}break`);
|
|
238
|
+
const makeContainer = (slice) => {
|
|
239
|
+
const container = document.createElement("div");
|
|
240
|
+
container.style.columnCount = String(columnCount);
|
|
241
|
+
container.style.columnGap = `${columnGapPt}pt`;
|
|
242
|
+
for (const child of slice) {
|
|
243
|
+
container.appendChild(child.cloneNode(true));
|
|
244
|
+
}
|
|
245
|
+
return container;
|
|
246
|
+
};
|
|
247
|
+
let start = 0;
|
|
248
|
+
while (start < children.length) {
|
|
249
|
+
if (isBreak(children[start])) {
|
|
250
|
+
blocks.push(this.measureElement(children[start], dims));
|
|
251
|
+
start++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// Grow the container one child at a time. Even a single oversized child is
|
|
255
|
+
// emitted alone, preserving the established oversized-block fallback.
|
|
256
|
+
let end = start + 1;
|
|
257
|
+
let container = makeContainer(children.slice(start, end));
|
|
258
|
+
let measured = this.measureElement(container, dims);
|
|
259
|
+
while (end < children.length && !isBreak(children[end])) {
|
|
260
|
+
const candidate = makeContainer(children.slice(start, end + 1));
|
|
261
|
+
const candidateMeasured = this.measureElement(candidate, dims);
|
|
262
|
+
if (candidateMeasured.heightPt > maxFragmentHeight)
|
|
263
|
+
break;
|
|
264
|
+
container = candidate;
|
|
265
|
+
measured = candidateMeasured;
|
|
266
|
+
end++;
|
|
267
|
+
}
|
|
268
|
+
blocks.push({
|
|
269
|
+
element: container,
|
|
270
|
+
heightPt: measured.heightPt,
|
|
271
|
+
marginTopPt: measured.marginTopPt,
|
|
272
|
+
marginBottomPt: measured.marginBottomPt,
|
|
273
|
+
keepWithNext: false,
|
|
274
|
+
keepLines: false,
|
|
275
|
+
pageBreakBefore: false,
|
|
276
|
+
isPageBreak: false,
|
|
277
|
+
isWordParagraph: false,
|
|
179
278
|
});
|
|
279
|
+
start = end;
|
|
180
280
|
}
|
|
181
281
|
return blocks;
|
|
182
282
|
}
|
|
@@ -206,6 +306,7 @@ export class PaginationEngine {
|
|
|
206
306
|
pageBreakBefore: element.dataset.pageBreakBefore === "true",
|
|
207
307
|
isPageBreak: element.dataset.pageBreak === "true" ||
|
|
208
308
|
element.classList.contains(`${this.cssPrefix}break`),
|
|
309
|
+
isWordParagraph: this.isWordParagraphElement(element),
|
|
209
310
|
};
|
|
210
311
|
this.stagingElement.removeChild(measurementHost);
|
|
211
312
|
return measured;
|
|
@@ -238,13 +339,11 @@ export class PaginationEngine {
|
|
|
238
339
|
* collapsed-margin rules as normal block placement. The trailing margin is
|
|
239
340
|
* intentionally excluded, matching the individual block fit check.
|
|
240
341
|
*/
|
|
241
|
-
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
|
|
342
|
+
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage, pageInSection) {
|
|
242
343
|
const firstBlock = chain[0];
|
|
243
344
|
if (!firstBlock)
|
|
244
345
|
return 0;
|
|
245
|
-
const firstMarginTop = isFirstOnPage
|
|
246
|
-
? firstBlock.marginTopPt
|
|
247
|
-
: Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
346
|
+
const firstMarginTop = this.effectiveBlockMarginTop(firstBlock, previousMarginBottomPt, isFirstOnPage, pageInSection);
|
|
248
347
|
let bodyHeight = firstMarginTop + firstBlock.heightPt;
|
|
249
348
|
for (let index = 1; index < chain.length; index++) {
|
|
250
349
|
const previousBlock = chain[index - 1];
|
|
@@ -253,6 +352,37 @@ export class PaginationEngine {
|
|
|
253
352
|
}
|
|
254
353
|
return bodyHeight;
|
|
255
354
|
}
|
|
355
|
+
/** Word paragraphs render as `p`, or as `h1`–`h6` when their style has an outline level. */
|
|
356
|
+
isWordParagraphElement(element) {
|
|
357
|
+
return element.tagName === "P" || /^H[1-6]$/.test(element.tagName);
|
|
358
|
+
}
|
|
359
|
+
shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection) {
|
|
360
|
+
return isFirstOnPage && pageInSection > 1 && block.isWordParagraph === true;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Resolves the part of a block's top margin that consumes the current page.
|
|
364
|
+
*
|
|
365
|
+
* In Word's native DOCX layout, paragraph space-before is suppressed when a paragraph is the
|
|
366
|
+
* first body block on a later page of the SAME section. The first page of a document/section is
|
|
367
|
+
* the exception and keeps its spacing. Tables and other block margins are not paragraph spacing,
|
|
368
|
+
* so they continue to use the ordinary CSS collapsing rule.
|
|
369
|
+
*/
|
|
370
|
+
effectiveBlockMarginTop(block, previousMarginBottomPt, isFirstOnPage, pageInSection) {
|
|
371
|
+
if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
return isFirstOnPage
|
|
375
|
+
? block.marginTopPt
|
|
376
|
+
: Math.max(block.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
377
|
+
}
|
|
378
|
+
/** Clone a source block with the same page-top spacing decision used by the height budget. */
|
|
379
|
+
cloneBlockForPage(block, isFirstOnPage, pageInSection) {
|
|
380
|
+
const clone = block.element.cloneNode(true);
|
|
381
|
+
if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
|
|
382
|
+
clone.style.setProperty("margin-top", "0", "important");
|
|
383
|
+
}
|
|
384
|
+
return clone;
|
|
385
|
+
}
|
|
256
386
|
/**
|
|
257
387
|
* Finds footnote references introduced by a sequence of blocks, preserving
|
|
258
388
|
* document order and excluding references already assigned to the page.
|
|
@@ -276,7 +406,7 @@ export class PaginationEngine {
|
|
|
276
406
|
* send it through the oversized-block fallback again.
|
|
277
407
|
*/
|
|
278
408
|
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
279
|
-
return Math.min(this.
|
|
409
|
+
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
410
|
}
|
|
281
411
|
/**
|
|
282
412
|
* Builds a clone of a simple table wrapper containing a contiguous run of rows.
|
|
@@ -724,7 +854,7 @@ export class PaginationEngine {
|
|
|
724
854
|
return 0;
|
|
725
855
|
}
|
|
726
856
|
// Measure in the SAME styling context the notes render in: `.page-footnotes` carries
|
|
727
|
-
// font-size
|
|
857
|
+
// its own font-size and line-height, so measuring without the class sizes the note
|
|
728
858
|
// block against body type and the reserve can never match what is drawn.
|
|
729
859
|
// Create a temporary measurement container
|
|
730
860
|
const measureContainer = document.createElement("div");
|
|
@@ -884,7 +1014,7 @@ export class PaginationEngine {
|
|
|
884
1014
|
/**
|
|
885
1015
|
* Adds footnotes to a page container, including continuation content.
|
|
886
1016
|
*/
|
|
887
|
-
addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
|
|
1017
|
+
addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
|
|
888
1018
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
889
1019
|
if (footnoteIds.length === 0 && !hasContinuation) {
|
|
890
1020
|
return;
|
|
@@ -895,11 +1025,13 @@ export class PaginationEngine {
|
|
|
895
1025
|
// Create a set of partial footnote IDs for quick lookup
|
|
896
1026
|
const partialFootnoteIds = new Set(partialFootnotes?.map(p => p.footnoteId) || []);
|
|
897
1027
|
// Calculate max height for footnotes area (content height minus margin for body content)
|
|
898
|
-
const maxFootnoteHeight = Math.min(footnoteHeight,
|
|
1028
|
+
const maxFootnoteHeight = Math.min(footnoteHeight, bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO);
|
|
899
1029
|
const footnotesDiv = document.createElement("div");
|
|
900
1030
|
footnotesDiv.className = `${this.cssPrefix}footnotes`;
|
|
901
1031
|
footnotesDiv.style.position = "absolute";
|
|
902
|
-
|
|
1032
|
+
// Notes sit at the FOOT OF THE BODY BAND, not at the bottom margin: a footer taller than
|
|
1033
|
+
// its margin raises that edge, and anchoring to the raw margin would draw notes over it.
|
|
1034
|
+
footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
|
|
903
1035
|
footnotesDiv.style.left = `${dims.marginLeft}pt`;
|
|
904
1036
|
footnotesDiv.style.width = `${dims.contentWidth}pt`;
|
|
905
1037
|
footnotesDiv.style.boxSizing = "border-box";
|
|
@@ -991,63 +1123,37 @@ export class PaginationEngine {
|
|
|
991
1123
|
return sectionHf.footerDefault;
|
|
992
1124
|
}
|
|
993
1125
|
/**
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1126
|
+
* The header, body, and footer bands for one page position.
|
|
1127
|
+
*
|
|
1128
|
+
* The single owner of "where does anything sit vertically on this page" — placement in
|
|
1129
|
+
* {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
|
|
1130
|
+
* read it, so those three cannot disagree about where the body ends.
|
|
1131
|
+
*
|
|
1132
|
+
* Deterministic: it depends only on the section's page setup and the registry's pre-measured
|
|
1133
|
+
* story heights, never on the page's content, which is what keeps it lazy-loading compatible.
|
|
999
1134
|
*/
|
|
1000
|
-
|
|
1135
|
+
getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
|
|
1001
1136
|
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 };
|
|
1137
|
+
return resolvePageBands(dims, this.selectStoryHeight(sectionHf?.headerFirstHeight, sectionHf?.headerEvenHeight, sectionHf?.headerDefaultHeight, pageInSection, globalPageNumber), this.selectStoryHeight(sectionHf?.footerFirstHeight, sectionHf?.footerEvenHeight, sectionHf?.footerDefaultHeight, pageInSection, globalPageNumber));
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The measured height of the running story this page position selects, mirroring
|
|
1141
|
+
* {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
|
|
1142
|
+
*/
|
|
1143
|
+
selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
|
|
1144
|
+
if (pageInSection === 1 && first != null)
|
|
1145
|
+
return first;
|
|
1146
|
+
if (globalPageNumber % 2 === 0 && even != null)
|
|
1147
|
+
return even;
|
|
1148
|
+
return fallback ?? 0;
|
|
1047
1149
|
}
|
|
1048
1150
|
/**
|
|
1049
1151
|
* Measures the content height of a header or footer element.
|
|
1050
|
-
*
|
|
1152
|
+
*
|
|
1153
|
+
* This is what tells {@link resolvePageBands} whether the story stays inside its margin or
|
|
1154
|
+
* pushes the body, so it must measure the story ALONE — any padding added here would have to
|
|
1155
|
+
* be added to the rendered band too, and the two drifting apart is exactly how a header
|
|
1156
|
+
* silently starts overlapping body text.
|
|
1051
1157
|
*/
|
|
1052
1158
|
measureHeaderFooterHeight(source, contentWidth) {
|
|
1053
1159
|
// Create a temporary measurement container
|
|
@@ -1056,8 +1162,6 @@ export class PaginationEngine {
|
|
|
1056
1162
|
measureContainer.style.visibility = "hidden";
|
|
1057
1163
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1058
1164
|
measureContainer.style.left = "-9999px";
|
|
1059
|
-
// Add padding to match the actual rendering
|
|
1060
|
-
measureContainer.style.paddingBottom = "4pt";
|
|
1061
1165
|
// Clone and add the header/footer content
|
|
1062
1166
|
for (const child of Array.from(source.childNodes)) {
|
|
1063
1167
|
measureContainer.appendChild(child.cloneNode(true));
|
|
@@ -1083,7 +1187,7 @@ export class PaginationEngine {
|
|
|
1083
1187
|
// Track page number within this section for first-page header/footer selection
|
|
1084
1188
|
let pageInSection = 1;
|
|
1085
1189
|
// Get effective content height for first page (accounts for header/footer sizes)
|
|
1086
|
-
let {
|
|
1190
|
+
let { bodyHeight: effectiveContentHeight } = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1087
1191
|
let remainingHeight = effectiveContentHeight;
|
|
1088
1192
|
// Track the previous block's bottom margin for margin collapsing
|
|
1089
1193
|
let prevMarginBottomPt = 0;
|
|
@@ -1123,8 +1227,8 @@ export class PaginationEngine {
|
|
|
1123
1227
|
pageInSection++;
|
|
1124
1228
|
currentContent = [];
|
|
1125
1229
|
// Get effective content height for new page position
|
|
1126
|
-
const
|
|
1127
|
-
effectiveContentHeight =
|
|
1230
|
+
const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1231
|
+
effectiveContentHeight = newBands.bodyHeight;
|
|
1128
1232
|
remainingHeight = effectiveContentHeight;
|
|
1129
1233
|
prevMarginBottomPt = 0; // Reset margin tracking for new page
|
|
1130
1234
|
currentFootnoteIds = []; // Reset footnotes for new page
|
|
@@ -1184,18 +1288,18 @@ export class PaginationEngine {
|
|
|
1184
1288
|
const totalChainFootnoteHeight = this.measureFootnotesHeight([...currentFootnoteIds, ...newChainFootnoteIds], dims.contentWidth, currentContinuation);
|
|
1185
1289
|
additionalChainFootnoteHeight = Math.max(0, totalChainFootnoteHeight - currentFootnoteHeight);
|
|
1186
1290
|
}
|
|
1187
|
-
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(keepChain, prevMarginBottomPt, currentContent.length === 0) +
|
|
1291
|
+
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(keepChain, prevMarginBottomPt, currentContent.length === 0, pageInSection) +
|
|
1188
1292
|
additionalChainFootnoteHeight;
|
|
1189
1293
|
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1190
1294
|
if (currentChainHeight > currentAvailableHeight) {
|
|
1191
|
-
const
|
|
1192
|
-
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(keepChain, 0, true);
|
|
1295
|
+
const nextPageBands = this.getPageBands(dims, sectionIndex, pageInSection + 1, pageNumber + 1);
|
|
1296
|
+
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(keepChain, 0, true, pageInSection + 1);
|
|
1193
1297
|
// finishPage transfers this continuation to the new page's
|
|
1194
1298
|
// currentContinuation state, so include it in the destination
|
|
1195
1299
|
// page's footnote reservation before deciding to move the chain.
|
|
1196
1300
|
const freshChainFootnoteHeight = this.measureFootnotesHeight(newChainFootnoteIds, dims.contentWidth, nextPageContinuation);
|
|
1197
1301
|
if (freshChainBodyHeight + freshChainFootnoteHeight <=
|
|
1198
|
-
|
|
1302
|
+
nextPageBands.bodyHeight) {
|
|
1199
1303
|
finishPage();
|
|
1200
1304
|
}
|
|
1201
1305
|
}
|
|
@@ -1217,11 +1321,7 @@ export class PaginationEngine {
|
|
|
1217
1321
|
// Calculate the effective height this block will consume
|
|
1218
1322
|
// Account for margin collapsing: the gap between blocks is max(prevBottom, currTop), not sum
|
|
1219
1323
|
const isFirstOnPage = currentContent.length === 0;
|
|
1220
|
-
|
|
1221
|
-
if (!isFirstOnPage) {
|
|
1222
|
-
// Margin collapsing: use the larger of the two adjacent margins
|
|
1223
|
-
effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
|
|
1224
|
-
}
|
|
1324
|
+
const effectiveMarginTop = this.effectiveBlockMarginTop(block, prevMarginBottomPt, isFirstOnPage, pageInSection);
|
|
1225
1325
|
// Visible height = top margin gap + content + footnote space
|
|
1226
1326
|
// Note: bottom margin is NOT included in the fit check because the last block's
|
|
1227
1327
|
// bottom margin extends beyond the content area and is clipped by overflow:hidden.
|
|
@@ -1248,7 +1348,7 @@ export class PaginationEngine {
|
|
|
1248
1348
|
// Check if block fits on current page (including its footnotes)
|
|
1249
1349
|
if (blockSpace <= effectiveRemainingHeight) {
|
|
1250
1350
|
// Block fits with current footnote allocation
|
|
1251
|
-
currentContent.push(
|
|
1351
|
+
currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
|
|
1252
1352
|
remainingHeight -= (effectiveMarginTop + block.heightPt + block.marginBottomPt);
|
|
1253
1353
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1254
1354
|
// Add new footnotes to current page
|
|
@@ -1257,7 +1357,8 @@ export class PaginationEngine {
|
|
|
1257
1357
|
currentFootnoteHeight += additionalFootnoteHeight;
|
|
1258
1358
|
}
|
|
1259
1359
|
}
|
|
1260
|
-
else if (block.heightPt + block
|
|
1360
|
+
else if (block.heightPt + this.effectiveBlockMarginTop(block, 0, true, pageInSection + 1) <=
|
|
1361
|
+
effectiveContentHeight) {
|
|
1261
1362
|
// Block doesn't fit with current allocation - try expanding footnote area
|
|
1262
1363
|
const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
|
|
1263
1364
|
// Check if block fits if we expand footnote area
|
|
@@ -1266,7 +1367,7 @@ export class PaginationEngine {
|
|
|
1266
1367
|
const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
|
|
1267
1368
|
if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
|
|
1268
1369
|
// Block itself fits, but footnotes don't - expand footnote area
|
|
1269
|
-
currentContent.push(
|
|
1370
|
+
currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
|
|
1270
1371
|
remainingHeight -= (effectiveMarginTop + block.heightPt + block.marginBottomPt);
|
|
1271
1372
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1272
1373
|
// Calculate EXPANDED space available for footnotes
|
|
@@ -1325,7 +1426,7 @@ export class PaginationEngine {
|
|
|
1325
1426
|
const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
|
|
1326
1427
|
if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
|
|
1327
1428
|
// Block fits after expanding footnote area.
|
|
1328
|
-
currentContent.push(
|
|
1429
|
+
currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
|
|
1329
1430
|
// `remainingHeight` tracks BODY consumption only — every other branch maintains it
|
|
1330
1431
|
// that way, and the footnote reserve is applied separately via `effectiveRemainingHeight`
|
|
1331
1432
|
// at the top of each iteration. Assigning `bodySpaceAfterExpansion - …` here folded the
|
|
@@ -1343,8 +1444,9 @@ export class PaginationEngine {
|
|
|
1343
1444
|
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0
|
|
1344
1445
|
? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation)
|
|
1345
1446
|
: (currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0);
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1447
|
+
const newPageMarginTop = this.effectiveBlockMarginTop(block, 0, true, pageInSection);
|
|
1448
|
+
const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
|
|
1449
|
+
currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
|
|
1348
1450
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1349
1451
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1350
1452
|
// Merge, never replace: finishPage() may have just seeded this page with notes deferred
|
|
@@ -1361,9 +1463,9 @@ export class PaginationEngine {
|
|
|
1361
1463
|
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0
|
|
1362
1464
|
? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation)
|
|
1363
1465
|
: (currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0);
|
|
1364
|
-
|
|
1365
|
-
const newPageSpace =
|
|
1366
|
-
currentContent.push(
|
|
1466
|
+
const newPageMarginTop = this.effectiveBlockMarginTop(block, 0, true, pageInSection);
|
|
1467
|
+
const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
|
|
1468
|
+
currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
|
|
1367
1469
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1368
1470
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1369
1471
|
// Merge, never replace: finishPage() may have just seeded this page with notes deferred
|
|
@@ -1390,7 +1492,7 @@ export class PaginationEngine {
|
|
|
1390
1492
|
if (currentContent.length > 0) {
|
|
1391
1493
|
finishPage();
|
|
1392
1494
|
}
|
|
1393
|
-
currentContent.push(
|
|
1495
|
+
currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
|
|
1394
1496
|
// Merge, never replace: finishPage() may have just seeded this page with notes deferred
|
|
1395
1497
|
// from the previous one, and overwriting here dropped them from the document.
|
|
1396
1498
|
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
@@ -1425,6 +1527,144 @@ export class PaginationEngine {
|
|
|
1425
1527
|
el.setAttribute("contenteditable", "false");
|
|
1426
1528
|
}
|
|
1427
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
|
|
1532
|
+
*
|
|
1533
|
+
* The converter deliberately carries the OOXML bases and offsets as data instead of flattening
|
|
1534
|
+
* them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
|
|
1535
|
+
* bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
|
|
1536
|
+
* from the clipped text column into the page box and position it in one shared point space.
|
|
1537
|
+
*/
|
|
1538
|
+
positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
|
|
1539
|
+
const anchors = Array.from(contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]'));
|
|
1540
|
+
if (anchors.length === 0)
|
|
1541
|
+
return;
|
|
1542
|
+
const pageRect = pageBox.getBoundingClientRect();
|
|
1543
|
+
const pixelsPerPoint = pageRect.width / dims.pageWidth;
|
|
1544
|
+
if (!(pixelsPerPoint > 0))
|
|
1545
|
+
return;
|
|
1546
|
+
for (const anchor of anchors) {
|
|
1547
|
+
const staticRect = anchor.getBoundingClientRect();
|
|
1548
|
+
const paragraph = anchor.closest('p, h1, h2, h3, h4, h5, h6');
|
|
1549
|
+
const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
|
|
1550
|
+
const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
|
|
1551
|
+
const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
|
|
1552
|
+
const context = {
|
|
1553
|
+
staticLeft: toPageX(staticRect.left),
|
|
1554
|
+
staticTop: toPageY(staticRect.top),
|
|
1555
|
+
lineHeight: paragraph
|
|
1556
|
+
? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint
|
|
1557
|
+
: staticRect.height / pixelsPerPoint,
|
|
1558
|
+
paragraphLeft: toPageX(paragraphRect.left),
|
|
1559
|
+
paragraphTop: toPageY(paragraphRect.top),
|
|
1560
|
+
paragraphWidth: paragraphRect.width / pixelsPerPoint,
|
|
1561
|
+
paragraphHeight: paragraphRect.height / pixelsPerPoint,
|
|
1562
|
+
};
|
|
1563
|
+
const widthReference = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-width-relative') ?? 'margin', dims, pageNumber, context);
|
|
1564
|
+
const widthPercent = this.anchorNumber(anchor, 'width-percent');
|
|
1565
|
+
if (widthPercent !== undefined) {
|
|
1566
|
+
anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
|
|
1567
|
+
}
|
|
1568
|
+
const heightReference = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-height-relative') ?? 'margin', dims, pageNumber, context);
|
|
1569
|
+
const heightPercent = this.anchorNumber(anchor, 'height-percent');
|
|
1570
|
+
if (heightPercent !== undefined && anchor.dataset.docxAnchorAutofit !== 'true') {
|
|
1571
|
+
anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
|
|
1572
|
+
}
|
|
1573
|
+
// Read the final border-box size after relative sizing and before reparenting. The stored
|
|
1574
|
+
// wp:extent remains the fallback; wps:bodyPr insets stay inside it through border-box sizing.
|
|
1575
|
+
const sizedRect = anchor.getBoundingClientRect();
|
|
1576
|
+
const width = sizedRect.width / pixelsPerPoint;
|
|
1577
|
+
const height = sizedRect.height / pixelsPerPoint;
|
|
1578
|
+
const horizontal = this.horizontalAnchorReference(anchor.getAttribute('data-docx-anchor-h-relative') ?? 'column', dims, pageNumber, context);
|
|
1579
|
+
const vertical = this.verticalAnchorReference(anchor.getAttribute('data-docx-anchor-v-relative') ?? 'paragraph', dims, pageNumber, context);
|
|
1580
|
+
const left = this.resolveAnchorAxis(horizontal, width, anchor.getAttribute('data-docx-anchor-h-align'), this.anchorNumber(anchor, 'h-offset'), pageNumber);
|
|
1581
|
+
const top = this.resolveAnchorAxis(vertical, height, anchor.getAttribute('data-docx-anchor-v-align'), this.anchorNumber(anchor, 'v-offset'), pageNumber);
|
|
1582
|
+
pageBox.appendChild(anchor);
|
|
1583
|
+
anchor.style.position = 'absolute';
|
|
1584
|
+
anchor.style.left = `${left}pt`;
|
|
1585
|
+
anchor.style.top = `${top}pt`;
|
|
1586
|
+
anchor.style.margin = '0';
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
anchorNumber(anchor, suffix) {
|
|
1590
|
+
const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
|
|
1591
|
+
if (raw === null)
|
|
1592
|
+
return undefined;
|
|
1593
|
+
const value = Number(raw);
|
|
1594
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1595
|
+
}
|
|
1596
|
+
horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
|
|
1597
|
+
const leftMargin = { start: 0, size: dims.marginLeft };
|
|
1598
|
+
const rightMargin = {
|
|
1599
|
+
start: dims.marginLeft + dims.contentWidth,
|
|
1600
|
+
size: dims.marginRight,
|
|
1601
|
+
};
|
|
1602
|
+
switch (relativeFrom) {
|
|
1603
|
+
case 'page':
|
|
1604
|
+
return { start: 0, size: dims.pageWidth };
|
|
1605
|
+
case 'character':
|
|
1606
|
+
return { start: context.staticLeft, size: 0 };
|
|
1607
|
+
case 'leftMargin':
|
|
1608
|
+
return leftMargin;
|
|
1609
|
+
case 'rightMargin':
|
|
1610
|
+
return rightMargin;
|
|
1611
|
+
case 'insideMargin':
|
|
1612
|
+
return pageNumber % 2 === 1 ? leftMargin : rightMargin;
|
|
1613
|
+
case 'outsideMargin':
|
|
1614
|
+
return pageNumber % 2 === 1 ? rightMargin : leftMargin;
|
|
1615
|
+
case 'paragraph':
|
|
1616
|
+
// Not part of ST_RelFromH, but accepting it gives malformed/legacy producers the
|
|
1617
|
+
// intuitive base without conflating it with the whole text column.
|
|
1618
|
+
return { start: context.paragraphLeft, size: context.paragraphWidth };
|
|
1619
|
+
case 'column':
|
|
1620
|
+
case 'margin':
|
|
1621
|
+
default:
|
|
1622
|
+
// The paginator currently lays out one text column per section, so the column and page
|
|
1623
|
+
// margin boxes coincide. Keeping the names distinct here leaves multi-column geometry
|
|
1624
|
+
// localized to this resolver when that layout support is added.
|
|
1625
|
+
return { start: dims.marginLeft, size: dims.contentWidth };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
|
|
1629
|
+
const topMargin = { start: 0, size: dims.marginTop };
|
|
1630
|
+
const bottomMargin = {
|
|
1631
|
+
start: dims.marginTop + dims.contentHeight,
|
|
1632
|
+
size: dims.marginBottom,
|
|
1633
|
+
};
|
|
1634
|
+
switch (relativeFrom) {
|
|
1635
|
+
case 'page':
|
|
1636
|
+
return { start: 0, size: dims.pageHeight };
|
|
1637
|
+
case 'paragraph':
|
|
1638
|
+
return { start: context.paragraphTop, size: context.paragraphHeight };
|
|
1639
|
+
case 'line':
|
|
1640
|
+
return { start: context.staticTop, size: context.lineHeight };
|
|
1641
|
+
case 'topMargin':
|
|
1642
|
+
return topMargin;
|
|
1643
|
+
case 'bottomMargin':
|
|
1644
|
+
return bottomMargin;
|
|
1645
|
+
case 'insideMargin':
|
|
1646
|
+
return pageNumber % 2 === 1 ? topMargin : bottomMargin;
|
|
1647
|
+
case 'outsideMargin':
|
|
1648
|
+
return pageNumber % 2 === 1 ? bottomMargin : topMargin;
|
|
1649
|
+
case 'margin':
|
|
1650
|
+
default:
|
|
1651
|
+
return { start: dims.marginTop, size: dims.contentHeight };
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
|
|
1655
|
+
if (offset !== undefined)
|
|
1656
|
+
return reference.start + offset;
|
|
1657
|
+
let fraction = 0;
|
|
1658
|
+
if (alignment === 'center')
|
|
1659
|
+
fraction = 0.5;
|
|
1660
|
+
else if (alignment === 'right' || alignment === 'bottom')
|
|
1661
|
+
fraction = 1;
|
|
1662
|
+
else if (alignment === 'inside')
|
|
1663
|
+
fraction = pageNumber % 2 === 1 ? 0 : 1;
|
|
1664
|
+
else if (alignment === 'outside')
|
|
1665
|
+
fraction = pageNumber % 2 === 1 ? 1 : 0;
|
|
1666
|
+
return reference.start + (reference.size - objectSize) * fraction;
|
|
1667
|
+
}
|
|
1428
1668
|
/**
|
|
1429
1669
|
* Creates a page container element.
|
|
1430
1670
|
*/
|
|
@@ -1464,24 +1704,27 @@ export class PaginationEngine {
|
|
|
1464
1704
|
// Needed by substitutePageNumberFields: a section that restarts numbering counts from its own
|
|
1465
1705
|
// first page, not from the document's.
|
|
1466
1706
|
pageBox.dataset.pageInSection = String(pageInSection);
|
|
1467
|
-
//
|
|
1468
|
-
const
|
|
1707
|
+
// Where the three bands sit on this page (no re-measurement needed)
|
|
1708
|
+
const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
|
|
1469
1709
|
// Add header if available for this section/page
|
|
1470
1710
|
const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
|
|
1471
1711
|
if (headerSource) {
|
|
1472
1712
|
const headerDiv = document.createElement("div");
|
|
1473
1713
|
headerDiv.className = `${this.cssPrefix}header`;
|
|
1474
1714
|
headerDiv.style.position = "absolute";
|
|
1475
|
-
|
|
1715
|
+
// `w:header` is the distance to the TOP of the story, and the story grows downward from
|
|
1716
|
+
// there; `flex-start` therefore pins the edge the OOXML actually declares, and content
|
|
1717
|
+
// taller than the measurement clips at the bottom rather than sliding up the page.
|
|
1718
|
+
headerDiv.style.top = `${bands.headerTop}pt`;
|
|
1719
|
+
headerDiv.style.bottom = "auto";
|
|
1476
1720
|
headerDiv.style.left = `${dims.marginLeft}pt`;
|
|
1477
1721
|
headerDiv.style.width = `${dims.contentWidth}pt`;
|
|
1478
|
-
headerDiv.style.height = `${
|
|
1722
|
+
headerDiv.style.height = `${bands.headerHeight}pt`;
|
|
1479
1723
|
headerDiv.style.overflow = "hidden";
|
|
1480
1724
|
headerDiv.style.boxSizing = "border-box";
|
|
1481
1725
|
headerDiv.style.display = "flex";
|
|
1482
1726
|
headerDiv.style.flexDirection = "column";
|
|
1483
|
-
headerDiv.style.justifyContent = "flex-
|
|
1484
|
-
headerDiv.style.paddingBottom = "4pt"; // Small gap before content area
|
|
1727
|
+
headerDiv.style.justifyContent = "flex-start";
|
|
1485
1728
|
// Clone the header content (skip the wrapper div's data attributes)
|
|
1486
1729
|
for (const child of Array.from(headerSource.childNodes)) {
|
|
1487
1730
|
const clonedheaderDiv = child.cloneNode(true);
|
|
@@ -1492,8 +1735,8 @@ export class PaginationEngine {
|
|
|
1492
1735
|
pageBox.appendChild(headerDiv);
|
|
1493
1736
|
}
|
|
1494
1737
|
// Create content area using pre-computed effective heights
|
|
1495
|
-
const contentAreaTop =
|
|
1496
|
-
const contentAreaHeight =
|
|
1738
|
+
const contentAreaTop = bands.bodyTop;
|
|
1739
|
+
const contentAreaHeight = bands.bodyHeight;
|
|
1497
1740
|
const contentArea = document.createElement("div");
|
|
1498
1741
|
contentArea.className = `${this.cssPrefix}content`;
|
|
1499
1742
|
contentArea.style.position = "absolute";
|
|
@@ -1510,7 +1753,7 @@ export class PaginationEngine {
|
|
|
1510
1753
|
// Add footnotes if any references appear on this page (or continuation from previous)
|
|
1511
1754
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
1512
1755
|
if (footnoteIds.length > 0 || hasContinuation) {
|
|
1513
|
-
this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
|
|
1756
|
+
this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
|
|
1514
1757
|
}
|
|
1515
1758
|
// Add footer if available for this section/page
|
|
1516
1759
|
const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
|
|
@@ -1518,16 +1761,18 @@ export class PaginationEngine {
|
|
|
1518
1761
|
const footerDiv = document.createElement("div");
|
|
1519
1762
|
footerDiv.className = `${this.cssPrefix}footer`;
|
|
1520
1763
|
footerDiv.style.position = "absolute";
|
|
1521
|
-
|
|
1764
|
+
// `w:footer` is the distance to the BOTTOM of the story, and the story grows upward from
|
|
1765
|
+
// there — the mirror of the header, so `flex-end` and a bottom anchor.
|
|
1766
|
+
footerDiv.style.top = "auto";
|
|
1767
|
+
footerDiv.style.bottom = `${dims.footerDistance}pt`;
|
|
1522
1768
|
footerDiv.style.left = `${dims.marginLeft}pt`;
|
|
1523
1769
|
footerDiv.style.width = `${dims.contentWidth}pt`;
|
|
1524
|
-
footerDiv.style.height = `${
|
|
1770
|
+
footerDiv.style.height = `${bands.footerHeight}pt`;
|
|
1525
1771
|
footerDiv.style.overflow = "hidden";
|
|
1526
1772
|
footerDiv.style.boxSizing = "border-box";
|
|
1527
1773
|
footerDiv.style.display = "flex";
|
|
1528
1774
|
footerDiv.style.flexDirection = "column";
|
|
1529
|
-
footerDiv.style.justifyContent = "flex-
|
|
1530
|
-
footerDiv.style.paddingTop = "4pt"; // Small gap after content area
|
|
1775
|
+
footerDiv.style.justifyContent = "flex-end";
|
|
1531
1776
|
// Clone the footer content (skip the wrapper div's data attributes)
|
|
1532
1777
|
for (const child of Array.from(footerSource.childNodes)) {
|
|
1533
1778
|
const clonedfooterDiv = child.cloneNode(true);
|
|
@@ -1546,6 +1791,9 @@ export class PaginationEngine {
|
|
|
1546
1791
|
}
|
|
1547
1792
|
// Add to container
|
|
1548
1793
|
this.containerElement.appendChild(pageBox);
|
|
1794
|
+
// Floating objects need the final page and anchor-paragraph boxes. Resolve them only after
|
|
1795
|
+
// append, when getBoundingClientRect() is meaningful, and lift them out of content clipping.
|
|
1796
|
+
this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
|
|
1549
1797
|
// Body and notes must occupy DISJOINT bands. The note block is absolutely positioned against
|
|
1550
1798
|
// the page bottom and grows upward, while the content area spans the whole text height — so
|
|
1551
1799
|
// nothing but the layout loop's arithmetic keeps them apart, and any disagreement between the
|