docxodus 7.1.0 → 8.0.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/dist/docxodus.worker.js +2 -2
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor-headerfooter.d.ts +130 -0
- package/dist/editor-headerfooter.d.ts.map +1 -0
- package/dist/editor-headerfooter.js +414 -0
- package/dist/editor-headerfooter.js.map +1 -0
- package/dist/editor.bundle.js +1075 -81
- package/dist/editor.d.ts +82 -4
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +304 -63
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/pagination.bundle.js +480 -19
- package/dist/pagination.d.ts +81 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +528 -26
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/react.js.map +1 -1
- package/dist/session.bundle.js +15 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +15 -0
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +47 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -11
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encoding.Extensions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.Thread.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.ReaderWriter.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.wasm +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +41 -41
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.map +1 -1
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.map +1 -1
- package/dist/wasm/index.html +2 -1
- package/package.json +1 -1
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;AAEH;;GAEG;AACH,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;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,wBAAwB;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,uBAAuB;IACvB,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,wBAAwB;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,uBAAuB;IACvB,UAAU,CAAC,EAAE,WAAW,CAAC;IAGzB,kDAAkD;IAClD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,OAAO,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,YAAY,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,SAAS,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,eAAe,EAAE,OAAO,CAAC;IACzB,0CAA0C;IAC1C,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAC;IAC3B,iCAAiC;IACjC,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
|
package/dist/pagination.js
CHANGED
|
@@ -82,6 +82,7 @@ export class PaginationEngine {
|
|
|
82
82
|
this.cssPrefix = options.cssPrefix ?? "page-";
|
|
83
83
|
this.showPageNumbers = options.showPageNumbers ?? true;
|
|
84
84
|
this.pageGap = options.pageGap ?? 20;
|
|
85
|
+
this.fragmentParagraphs = options.fragmentParagraphs ?? false;
|
|
85
86
|
this.hfRegistry = new Map();
|
|
86
87
|
this.footnoteRegistry = new Map();
|
|
87
88
|
}
|
|
@@ -161,6 +162,443 @@ export class PaginationEngine {
|
|
|
161
162
|
}
|
|
162
163
|
return blocks;
|
|
163
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Measures one element in the same hidden staging context used for the source blocks.
|
|
167
|
+
* This is intentionally DOM-based: table row heights cannot be inferred from individual
|
|
168
|
+
* rows because wrapping and collapsed borders change the height of a fragment.
|
|
169
|
+
*/
|
|
170
|
+
measureElement(element, dims) {
|
|
171
|
+
const measurementHost = document.createElement("div");
|
|
172
|
+
measurementHost.style.position = "absolute";
|
|
173
|
+
measurementHost.style.visibility = "hidden";
|
|
174
|
+
measurementHost.style.left = "-9999px";
|
|
175
|
+
measurementHost.style.width = `${dims.contentWidth}pt`;
|
|
176
|
+
const measuredElement = element.cloneNode(true);
|
|
177
|
+
measurementHost.appendChild(measuredElement);
|
|
178
|
+
this.stagingElement.appendChild(measurementHost);
|
|
179
|
+
const rect = measuredElement.getBoundingClientRect();
|
|
180
|
+
const style = window.getComputedStyle(measuredElement);
|
|
181
|
+
const measured = {
|
|
182
|
+
element,
|
|
183
|
+
heightPt: pxToPt(rect.height),
|
|
184
|
+
marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
|
|
185
|
+
marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
|
|
186
|
+
keepWithNext: element.dataset.keepWithNext === "true",
|
|
187
|
+
keepLines: element.dataset.keepLines === "true",
|
|
188
|
+
pageBreakBefore: element.dataset.pageBreakBefore === "true",
|
|
189
|
+
isPageBreak: element.dataset.pageBreak === "true" ||
|
|
190
|
+
element.classList.contains(`${this.cssPrefix}break`),
|
|
191
|
+
};
|
|
192
|
+
this.stagingElement.removeChild(measurementHost);
|
|
193
|
+
return measured;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Returns the contiguous keep-with-next chain beginning at a block.
|
|
197
|
+
*
|
|
198
|
+
* A hard page break or a page-break-before directive is stronger than a
|
|
199
|
+
* keep-with-next directive, so it terminates the chain. The caller only
|
|
200
|
+
* keeps a chain together when the whole chain can fit on a fresh page.
|
|
201
|
+
*/
|
|
202
|
+
getKeepWithNextChain(blocks, startIndex) {
|
|
203
|
+
const firstBlock = blocks[startIndex];
|
|
204
|
+
if (!firstBlock)
|
|
205
|
+
return [];
|
|
206
|
+
const chain = [firstBlock];
|
|
207
|
+
let lastIndex = startIndex;
|
|
208
|
+
while (blocks[lastIndex].keepWithNext) {
|
|
209
|
+
const nextBlock = blocks[lastIndex + 1];
|
|
210
|
+
if (!nextBlock || nextBlock.isPageBreak || nextBlock.pageBreakBefore) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
chain.push(nextBlock);
|
|
214
|
+
lastIndex++;
|
|
215
|
+
}
|
|
216
|
+
return chain;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Measures the visible body height of a keep-with-next chain using the same
|
|
220
|
+
* collapsed-margin rules as normal block placement. The trailing margin is
|
|
221
|
+
* intentionally excluded, matching the individual block fit check.
|
|
222
|
+
*/
|
|
223
|
+
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
|
|
224
|
+
const firstBlock = chain[0];
|
|
225
|
+
if (!firstBlock)
|
|
226
|
+
return 0;
|
|
227
|
+
const firstMarginTop = isFirstOnPage
|
|
228
|
+
? firstBlock.marginTopPt
|
|
229
|
+
: Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
230
|
+
let bodyHeight = firstMarginTop + firstBlock.heightPt;
|
|
231
|
+
for (let index = 1; index < chain.length; index++) {
|
|
232
|
+
const previousBlock = chain[index - 1];
|
|
233
|
+
const block = chain[index];
|
|
234
|
+
bodyHeight += Math.max(previousBlock.marginBottomPt, block.marginTopPt) + block.heightPt;
|
|
235
|
+
}
|
|
236
|
+
return bodyHeight;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Finds footnote references introduced by a sequence of blocks, preserving
|
|
240
|
+
* document order and excluding references already assigned to the page.
|
|
241
|
+
*/
|
|
242
|
+
collectNewFootnoteIds(blocks, existingFootnoteIds) {
|
|
243
|
+
const knownIds = new Set(existingFootnoteIds);
|
|
244
|
+
const newIds = [];
|
|
245
|
+
for (const block of blocks) {
|
|
246
|
+
for (const id of this.extractFootnoteRefs(block.element)) {
|
|
247
|
+
if (!knownIds.has(id)) {
|
|
248
|
+
knownIds.add(id);
|
|
249
|
+
newIds.push(id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return newIds;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The shortest body available to a section's first, default, or even page.
|
|
257
|
+
* A row fragment must fit every variant, otherwise a later header/footer could
|
|
258
|
+
* send it through the oversized-block fallback again.
|
|
259
|
+
*/
|
|
260
|
+
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
261
|
+
return Math.min(this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight, this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight, this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Builds a clone of a simple table wrapper containing a contiguous run of rows.
|
|
265
|
+
* Complex table features are deliberately rejected by the caller: a split across
|
|
266
|
+
* merged cells, nested tables, or footnotes cannot be made correct by cloning rows.
|
|
267
|
+
*/
|
|
268
|
+
createSimpleTableFragment(wrapper, table, body, rows, retainAnchor) {
|
|
269
|
+
const wrapperClone = wrapper.cloneNode(false);
|
|
270
|
+
const tableClone = table.cloneNode(false);
|
|
271
|
+
// The eligibility gate permits only colgroups alongside the body. Keep each
|
|
272
|
+
// colgroup so fixed and proportional column widths remain stable per fragment.
|
|
273
|
+
for (const child of Array.from(table.children)) {
|
|
274
|
+
if (child !== body) {
|
|
275
|
+
tableClone.appendChild(child.cloneNode(true));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const bodyClone = body.cloneNode(false);
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
bodyClone.appendChild(row.cloneNode(true));
|
|
281
|
+
}
|
|
282
|
+
tableClone.appendChild(bodyClone);
|
|
283
|
+
if (!retainAnchor) {
|
|
284
|
+
wrapperClone.removeAttribute("data-anchor");
|
|
285
|
+
tableClone.removeAttribute("data-anchor");
|
|
286
|
+
}
|
|
287
|
+
wrapperClone.appendChild(tableClone);
|
|
288
|
+
return wrapperClone;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Splits an oversized, ordinary table at row boundaries. This only participates
|
|
292
|
+
* in the existing oversized-block fallback; unsupported tables keep the previous
|
|
293
|
+
* overflow behavior rather than risking broken table semantics.
|
|
294
|
+
*/
|
|
295
|
+
trySplitSimpleOversizedTable(block, dims, sectionIndex) {
|
|
296
|
+
const wrapper = block.element;
|
|
297
|
+
if (wrapper.tagName !== "DIV" ||
|
|
298
|
+
wrapper.children.length !== 1 ||
|
|
299
|
+
block.keepWithNext ||
|
|
300
|
+
block.keepLines ||
|
|
301
|
+
block.pageBreakBefore ||
|
|
302
|
+
block.isPageBreak) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const table = wrapper.firstElementChild;
|
|
306
|
+
if (!(table instanceof HTMLTableElement)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
310
|
+
if (!body ||
|
|
311
|
+
table.tHead ||
|
|
312
|
+
table.tFoot ||
|
|
313
|
+
body.rows.length < 2 ||
|
|
314
|
+
Array.from(table.children).some(child => child !== body && child.tagName !== "COLGROUP") ||
|
|
315
|
+
table.querySelector("table, [rowspan], [colspan], [data-footnote-id]") ||
|
|
316
|
+
wrapper.querySelector("[data-footnote-id]")) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const rows = Array.from(body.rows);
|
|
320
|
+
const minimumContentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
321
|
+
// Use the source's full vertical margins while forming groups. Continuation
|
|
322
|
+
// fragments later clear their joining margins, so this conservative bound
|
|
323
|
+
// cannot create a fragment that overflows a header/footer variant.
|
|
324
|
+
const maximumFragmentHeight = minimumContentHeight - block.marginTopPt - block.marginBottomPt;
|
|
325
|
+
if (maximumFragmentHeight <= 0) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const groups = [];
|
|
329
|
+
let start = 0;
|
|
330
|
+
while (start < rows.length) {
|
|
331
|
+
let end = start;
|
|
332
|
+
while (end < rows.length) {
|
|
333
|
+
const candidate = this.createSimpleTableFragment(wrapper, table, body, rows.slice(start, end + 1), start === 0);
|
|
334
|
+
const measured = this.measureElement(candidate, dims);
|
|
335
|
+
if (measured.heightPt > maximumFragmentHeight) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
end++;
|
|
339
|
+
}
|
|
340
|
+
// Even a one-row fragment cannot fit. Preserve the established overflow
|
|
341
|
+
// fallback rather than looping or clipping a partially split row.
|
|
342
|
+
if (end === start) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
groups.push(rows.slice(start, end));
|
|
346
|
+
start = end;
|
|
347
|
+
}
|
|
348
|
+
if (groups.length < 2) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const fragments = [];
|
|
352
|
+
for (let index = 0; index < groups.length; index++) {
|
|
353
|
+
const isFirst = index === 0;
|
|
354
|
+
const isLast = index === groups.length - 1;
|
|
355
|
+
const fragment = this.createSimpleTableFragment(wrapper, table, body, groups[index], isFirst);
|
|
356
|
+
// Keep the source's outer spacing only at the table's real boundaries.
|
|
357
|
+
// Continuation margins would otherwise add blank space at the top/bottom
|
|
358
|
+
// of every paginated fragment.
|
|
359
|
+
if (!isFirst) {
|
|
360
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
361
|
+
}
|
|
362
|
+
if (!isLast) {
|
|
363
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
364
|
+
}
|
|
365
|
+
const measured = this.measureElement(fragment, dims);
|
|
366
|
+
if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt >
|
|
367
|
+
minimumContentHeight) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
fragments.push({
|
|
371
|
+
...measured,
|
|
372
|
+
keepWithNext: false,
|
|
373
|
+
keepLines: false,
|
|
374
|
+
pageBreakBefore: false,
|
|
375
|
+
isPageBreak: false,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return fragments;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
|
|
382
|
+
* after whitespace or at a run boundary so the paginator never deliberately
|
|
383
|
+
* cuts through an ordinary word merely to fill a little more of a page.
|
|
384
|
+
*/
|
|
385
|
+
paragraphFragmentEndpoints(paragraph) {
|
|
386
|
+
const endpoints = [];
|
|
387
|
+
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
|
|
388
|
+
let textNode;
|
|
389
|
+
while ((textNode = walker.nextNode())) {
|
|
390
|
+
const text = textNode.data;
|
|
391
|
+
if (text.length === 0)
|
|
392
|
+
continue;
|
|
393
|
+
// A whitespace boundary preserves normal word wrapping. Always retain the
|
|
394
|
+
// end of a run too: adjacent runs can change formatting without containing
|
|
395
|
+
// a whitespace character between them.
|
|
396
|
+
const whitespace = /\s+/g;
|
|
397
|
+
let match;
|
|
398
|
+
while ((match = whitespace.exec(text)) !== null) {
|
|
399
|
+
endpoints.push({ node: textNode, offset: match.index + match[0].length });
|
|
400
|
+
}
|
|
401
|
+
if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode ||
|
|
402
|
+
endpoints[endpoints.length - 1].offset !== text.length) {
|
|
403
|
+
endpoints.push({ node: textNode, offset: text.length });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return endpoints;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Whether a range contains visible text after ignoring bidi/zero-width marks.
|
|
410
|
+
* Paragraph fragmentation deliberately excludes non-textual descendants, so
|
|
411
|
+
* this is enough to reject empty head or tail fragments.
|
|
412
|
+
*/
|
|
413
|
+
hasVisibleFragmentText(fragment) {
|
|
414
|
+
return (fragment.textContent || "")
|
|
415
|
+
.replace(/[\u200B-\u200F\uFEFF]/g, "")
|
|
416
|
+
.trim()
|
|
417
|
+
.length > 0;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Phase one only handles text-like paragraph descendants. Objects, explicit
|
|
421
|
+
* line breaks, list markers, notes, and out-of-flow/inline-block content all
|
|
422
|
+
* require their own line-layout rules and retain the established whole-block
|
|
423
|
+
* fallback instead of risking broken content or duplicate anchors.
|
|
424
|
+
*/
|
|
425
|
+
canFragmentParagraph(block) {
|
|
426
|
+
if (!this.fragmentParagraphs || block.element.tagName !== "P") {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
const paragraph = block.element;
|
|
430
|
+
if (block.keepWithNext ||
|
|
431
|
+
block.keepLines ||
|
|
432
|
+
block.pageBreakBefore ||
|
|
433
|
+
block.isPageBreak ||
|
|
434
|
+
paragraph.dataset.widowControl === "true" ||
|
|
435
|
+
paragraph.hasAttribute("contenteditable")) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
// The outer paragraph may carry its source identity. Descendant identities
|
|
439
|
+
// are not safe to duplicate in a continuation fragment, so reject them.
|
|
440
|
+
const unsupportedDescendants = [
|
|
441
|
+
"br",
|
|
442
|
+
"img",
|
|
443
|
+
"picture",
|
|
444
|
+
"svg",
|
|
445
|
+
"math",
|
|
446
|
+
"canvas",
|
|
447
|
+
"video",
|
|
448
|
+
"audio",
|
|
449
|
+
"iframe",
|
|
450
|
+
"object",
|
|
451
|
+
"embed",
|
|
452
|
+
"input",
|
|
453
|
+
"button",
|
|
454
|
+
"select",
|
|
455
|
+
"textarea",
|
|
456
|
+
"table",
|
|
457
|
+
"ol",
|
|
458
|
+
"ul",
|
|
459
|
+
"li",
|
|
460
|
+
"dl",
|
|
461
|
+
"div",
|
|
462
|
+
"p",
|
|
463
|
+
"section",
|
|
464
|
+
"article",
|
|
465
|
+
"aside",
|
|
466
|
+
"figure",
|
|
467
|
+
"fieldset",
|
|
468
|
+
"details",
|
|
469
|
+
"[data-footnote-id]",
|
|
470
|
+
"[data-list-marker]",
|
|
471
|
+
"[data-anchor]",
|
|
472
|
+
"[id]",
|
|
473
|
+
"[contenteditable]",
|
|
474
|
+
].join(", ");
|
|
475
|
+
if (paragraph.querySelector(unsupportedDescendants)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const paragraphStyle = window.getComputedStyle(paragraph);
|
|
479
|
+
if (paragraphStyle.display !== "block" ||
|
|
480
|
+
paragraphStyle.position !== "static" ||
|
|
481
|
+
paragraphStyle.float !== "none" ||
|
|
482
|
+
paragraphStyle.whiteSpace !== "normal" ||
|
|
483
|
+
paragraphStyle.breakBefore !== "auto" ||
|
|
484
|
+
paragraphStyle.breakAfter !== "auto" ||
|
|
485
|
+
paragraphStyle.breakInside === "avoid" ||
|
|
486
|
+
paragraphStyle.pageBreakBefore !== "auto" ||
|
|
487
|
+
paragraphStyle.pageBreakAfter !== "auto" ||
|
|
488
|
+
paragraphStyle.pageBreakInside === "avoid") {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
// A range clone preserves nested inline formatting exactly. Anything that
|
|
492
|
+
// establishes its own box/layout context is intentionally deferred until a
|
|
493
|
+
// future fragmenter can model it accurately.
|
|
494
|
+
for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
|
|
495
|
+
const style = window.getComputedStyle(descendant);
|
|
496
|
+
if (style.display !== "inline" ||
|
|
497
|
+
style.position !== "static" ||
|
|
498
|
+
style.float !== "none" ||
|
|
499
|
+
style.whiteSpace !== "normal") {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Builds one range-cloned paragraph fragment. Only the leading fragment keeps
|
|
507
|
+
* the source paragraph's addressability; continuations must not duplicate an
|
|
508
|
+
* id/data-anchor in the rendered document.
|
|
509
|
+
*/
|
|
510
|
+
createParagraphFragment(paragraph, range, retainSourceIdentity, isFinalFragment) {
|
|
511
|
+
const fragment = paragraph.cloneNode(false);
|
|
512
|
+
const contents = range.cloneContents();
|
|
513
|
+
fragment.appendChild(contents);
|
|
514
|
+
if (!retainSourceIdentity) {
|
|
515
|
+
fragment.removeAttribute("id");
|
|
516
|
+
fragment.removeAttribute("data-anchor");
|
|
517
|
+
// A continuation starts with a normal line rather than repeating a first-
|
|
518
|
+
// line/hanging indent. Keep the side margin so paragraph alignment remains.
|
|
519
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
520
|
+
fragment.style.setProperty("text-indent", "0", "important");
|
|
521
|
+
}
|
|
522
|
+
if (!isFinalFragment) {
|
|
523
|
+
// The original bottom spacing belongs after the complete paragraph, not
|
|
524
|
+
// between synthetic page fragments.
|
|
525
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
526
|
+
}
|
|
527
|
+
return fragment;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Splits a simple paragraph at the largest DOM Range endpoint that fits the
|
|
531
|
+
* currently available body space. The caller then processes the tail normally,
|
|
532
|
+
* allowing it to fragment again on later pages when necessary.
|
|
533
|
+
*/
|
|
534
|
+
tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
|
|
535
|
+
if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const paragraph = block.element;
|
|
539
|
+
const endpoints = this.paragraphFragmentEndpoints(paragraph);
|
|
540
|
+
// The final endpoint is the full paragraph and cannot leave a tail. A
|
|
541
|
+
// single text run with no earlier whitespace remains an overflow fallback.
|
|
542
|
+
if (endpoints.length < 2) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
let low = 0;
|
|
546
|
+
let high = endpoints.length - 2;
|
|
547
|
+
let best = null;
|
|
548
|
+
// Fragment height is monotonic for the deliberately narrow eligible subset,
|
|
549
|
+
// so binary search avoids measuring every word in long body paragraphs.
|
|
550
|
+
while (low <= high) {
|
|
551
|
+
const middle = Math.floor((low + high) / 2);
|
|
552
|
+
const endpoint = endpoints[middle];
|
|
553
|
+
const headRange = document.createRange();
|
|
554
|
+
headRange.setStart(paragraph, 0);
|
|
555
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
556
|
+
const headContents = headRange.cloneContents();
|
|
557
|
+
if (!this.hasVisibleFragmentText(headContents)) {
|
|
558
|
+
low = middle + 1;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const head = this.createParagraphFragment(paragraph, headRange, true, false);
|
|
562
|
+
const measured = this.measureElement(head, dims);
|
|
563
|
+
if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
|
|
564
|
+
best = { endpoint, element: head, measured };
|
|
565
|
+
low = middle + 1;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
high = middle - 1;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!best) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const tailRange = document.createRange();
|
|
575
|
+
tailRange.setStart(best.endpoint.node, best.endpoint.offset);
|
|
576
|
+
tailRange.setEnd(paragraph, paragraph.childNodes.length);
|
|
577
|
+
const tailContents = tailRange.cloneContents();
|
|
578
|
+
if (!this.hasVisibleFragmentText(tailContents)) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
|
|
582
|
+
const tailMeasured = this.measureElement(tail, dims);
|
|
583
|
+
return [
|
|
584
|
+
{
|
|
585
|
+
...best.measured,
|
|
586
|
+
element: best.element,
|
|
587
|
+
keepWithNext: false,
|
|
588
|
+
keepLines: false,
|
|
589
|
+
pageBreakBefore: false,
|
|
590
|
+
isPageBreak: false,
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
...tailMeasured,
|
|
594
|
+
element: tail,
|
|
595
|
+
keepWithNext: false,
|
|
596
|
+
keepLines: false,
|
|
597
|
+
pageBreakBefore: false,
|
|
598
|
+
isPageBreak: false,
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
}
|
|
164
602
|
/**
|
|
165
603
|
* Parses the header/footer registry from the staging element.
|
|
166
604
|
* Also measures heights during parsing for lazy-loading compatibility.
|
|
@@ -625,7 +1063,8 @@ export class PaginationEngine {
|
|
|
625
1063
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
626
1064
|
}
|
|
627
1065
|
const finishPage = () => {
|
|
628
|
-
|
|
1066
|
+
const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1067
|
+
if (currentContent.length === 0 && !hasCurrentContinuation)
|
|
629
1068
|
return;
|
|
630
1069
|
const page = this.createPage(dims, pageNumber, sectionIndex, currentContent, pageInSection, currentFootnoteIds, currentFootnoteHeight, currentContinuation, currentPartialFootnotes.length > 0 ? currentPartialFootnotes : undefined);
|
|
631
1070
|
pages.push(page);
|
|
@@ -652,7 +1091,6 @@ export class PaginationEngine {
|
|
|
652
1091
|
};
|
|
653
1092
|
for (let i = 0; i < blocks.length; i++) {
|
|
654
1093
|
const block = blocks[i];
|
|
655
|
-
const nextBlock = blocks[i + 1];
|
|
656
1094
|
// Handle explicit page breaks
|
|
657
1095
|
if (block.isPageBreak) {
|
|
658
1096
|
finishPage();
|
|
@@ -662,10 +1100,50 @@ export class PaginationEngine {
|
|
|
662
1100
|
if (block.pageBreakBefore && currentContent.length > 0) {
|
|
663
1101
|
finishPage();
|
|
664
1102
|
}
|
|
1103
|
+
// A series of keep-with-next blocks is one indivisible placement unit
|
|
1104
|
+
// when it can fit a new page. The former one-block lookahead was never
|
|
1105
|
+
// applied to the placement decision, and it could not preserve a chain of
|
|
1106
|
+
// headings/paragraphs. Do not force an oversized chain to a fresh page:
|
|
1107
|
+
// its members retain the established greedy/overflow behavior instead.
|
|
1108
|
+
const previousBlock = blocks[i - 1];
|
|
1109
|
+
const startsKeepChain = !previousBlock ||
|
|
1110
|
+
!previousBlock.keepWithNext ||
|
|
1111
|
+
previousBlock.isPageBreak ||
|
|
1112
|
+
block.pageBreakBefore;
|
|
1113
|
+
// A footnote continuation also occupies this page even when its body has
|
|
1114
|
+
// no blocks yet. In that case a feasible chain may need to move past the
|
|
1115
|
+
// continuation as a unit.
|
|
1116
|
+
const pageHasOccupiedSpace = currentContent.length > 0 || currentFootnoteHeight > 0;
|
|
1117
|
+
if (pageHasOccupiedSpace && block.keepWithNext && startsKeepChain) {
|
|
1118
|
+
const keepChain = this.getKeepWithNextChain(blocks, i);
|
|
1119
|
+
if (keepChain.length > 1) {
|
|
1120
|
+
const newChainFootnoteIds = this.collectNewFootnoteIds(keepChain, currentFootnoteIds);
|
|
1121
|
+
let additionalChainFootnoteHeight = 0;
|
|
1122
|
+
if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1123
|
+
const totalChainFootnoteHeight = this.measureFootnotesHeight([...currentFootnoteIds, ...newChainFootnoteIds], dims.contentWidth, currentContinuation);
|
|
1124
|
+
additionalChainFootnoteHeight = Math.max(0, totalChainFootnoteHeight - currentFootnoteHeight);
|
|
1125
|
+
}
|
|
1126
|
+
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(keepChain, prevMarginBottomPt, currentContent.length === 0) +
|
|
1127
|
+
additionalChainFootnoteHeight;
|
|
1128
|
+
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1129
|
+
if (currentChainHeight > currentAvailableHeight) {
|
|
1130
|
+
const nextPageHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection + 1, pageNumber + 1);
|
|
1131
|
+
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(keepChain, 0, true);
|
|
1132
|
+
// finishPage transfers this continuation to the new page's
|
|
1133
|
+
// currentContinuation state, so include it in the destination
|
|
1134
|
+
// page's footnote reservation before deciding to move the chain.
|
|
1135
|
+
const freshChainFootnoteHeight = this.measureFootnotesHeight(newChainFootnoteIds, dims.contentWidth, nextPageContinuation);
|
|
1136
|
+
if (freshChainBodyHeight + freshChainFootnoteHeight <=
|
|
1137
|
+
nextPageHeights.contentHeight) {
|
|
1138
|
+
finishPage();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
665
1143
|
// Extract footnote references from this block
|
|
666
|
-
const
|
|
1144
|
+
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
667
1145
|
// Only count new footnotes (not already on this page)
|
|
668
|
-
const newFootnoteIds =
|
|
1146
|
+
const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
|
|
669
1147
|
// Calculate additional footnote height if this block is added
|
|
670
1148
|
let additionalFootnoteHeight = 0;
|
|
671
1149
|
if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
@@ -688,20 +1166,24 @@ export class PaginationEngine {
|
|
|
688
1166
|
// bottom margin extends beyond the content area and is clipped by overflow:hidden.
|
|
689
1167
|
// It is still tracked in remainingHeight for correct margin collapsing with the next block.
|
|
690
1168
|
const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
|
|
691
|
-
// Calculate needed height (including keepWithNext)
|
|
692
|
-
let neededHeight = blockSpace;
|
|
693
|
-
if (block.keepWithNext && nextBlock && !nextBlock.isPageBreak) {
|
|
694
|
-
// For keepWithNext, include the next block with collapsed margins
|
|
695
|
-
const collapsedMargin = Math.max(block.marginBottomPt, nextBlock.marginTopPt);
|
|
696
|
-
neededHeight = effectiveMarginTop + block.heightPt + collapsedMargin +
|
|
697
|
-
nextBlock.heightPt + additionalFootnoteHeight;
|
|
698
|
-
}
|
|
699
1169
|
// Effective remaining height (content area minus footnotes already on page)
|
|
700
1170
|
const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
|
|
701
1171
|
// Calculate maximum footnote area for this page (can expand into body content space)
|
|
702
1172
|
const bodyContentUsed = effectiveContentHeight - remainingHeight;
|
|
703
1173
|
const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
704
1174
|
const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
|
|
1175
|
+
// A paragraph that cannot fit as a whole may still have a simple text-only
|
|
1176
|
+
// prefix that fits this page. Fragment before the ordinary next-page or
|
|
1177
|
+
// oversized fallback so the cloned head participates in the same margin
|
|
1178
|
+
// and footnote accounting as every other block.
|
|
1179
|
+
if (blockSpace > effectiveRemainingHeight) {
|
|
1180
|
+
const paragraphFragments = this.tryFragmentParagraph(block, dims, effectiveRemainingHeight, effectiveMarginTop);
|
|
1181
|
+
if (paragraphFragments) {
|
|
1182
|
+
blocks.splice(i, 1, ...paragraphFragments);
|
|
1183
|
+
i--;
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
705
1187
|
// Check if block fits on current page (including its footnotes)
|
|
706
1188
|
if (blockSpace <= effectiveRemainingHeight) {
|
|
707
1189
|
// Block fits with current footnote allocation
|
|
@@ -752,10 +1234,12 @@ export class PaginationEngine {
|
|
|
752
1234
|
footnoteId,
|
|
753
1235
|
fittingElements: fits
|
|
754
1236
|
});
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1237
|
+
if (overflow.length > 0) {
|
|
1238
|
+
nextPageContinuation = {
|
|
1239
|
+
footnoteId,
|
|
1240
|
+
remainingElements: overflow
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
759
1243
|
currentFootnoteHeight = availableForFootnotes;
|
|
760
1244
|
}
|
|
761
1245
|
else {
|
|
@@ -802,14 +1286,14 @@ export class PaginationEngine {
|
|
|
802
1286
|
else {
|
|
803
1287
|
// Still doesn't fit - start new page
|
|
804
1288
|
finishPage();
|
|
805
|
-
const newPageFootnoteHeight =
|
|
806
|
-
? this.measureFootnotesHeight(
|
|
1289
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0
|
|
1290
|
+
? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation)
|
|
807
1291
|
: (currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0);
|
|
808
1292
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
809
1293
|
currentContent.push(block.element.cloneNode(true));
|
|
810
1294
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
811
1295
|
prevMarginBottomPt = block.marginBottomPt;
|
|
812
|
-
currentFootnoteIds = [...
|
|
1296
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
813
1297
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
814
1298
|
}
|
|
815
1299
|
}
|
|
@@ -818,26 +1302,38 @@ export class PaginationEngine {
|
|
|
818
1302
|
finishPage();
|
|
819
1303
|
// On new page, recalculate footnote height for just this block's footnotes
|
|
820
1304
|
// (plus any continuation from previous page)
|
|
821
|
-
const newPageFootnoteHeight =
|
|
822
|
-
? this.measureFootnotesHeight(
|
|
1305
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0
|
|
1306
|
+
? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation)
|
|
823
1307
|
: (currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0);
|
|
824
1308
|
// Include full top margin
|
|
825
1309
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
826
1310
|
currentContent.push(block.element.cloneNode(true));
|
|
827
1311
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
828
1312
|
prevMarginBottomPt = block.marginBottomPt;
|
|
829
|
-
currentFootnoteIds = [...
|
|
1313
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
830
1314
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
831
1315
|
}
|
|
832
1316
|
}
|
|
833
1317
|
else {
|
|
834
|
-
// Block is taller than a page
|
|
835
|
-
//
|
|
1318
|
+
// Block is taller than a page. Ordinary tables can be split at complete
|
|
1319
|
+
// row boundaries; every other block retains the established overflow path.
|
|
1320
|
+
const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
|
|
1321
|
+
if (tableFragments) {
|
|
1322
|
+
if (currentContent.length > 0 || currentContinuation) {
|
|
1323
|
+
finishPage();
|
|
1324
|
+
}
|
|
1325
|
+
blocks.splice(i, 1, ...tableFragments);
|
|
1326
|
+
i--;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
// Unsupported oversized blocks are intentionally left intact. Splitting
|
|
1330
|
+
// arbitrary HTML, merged tables, or footnote-bearing tables would be less
|
|
1331
|
+
// correct than the prior clipped fallback.
|
|
836
1332
|
if (currentContent.length > 0) {
|
|
837
1333
|
finishPage();
|
|
838
1334
|
}
|
|
839
1335
|
currentContent.push(block.element.cloneNode(true));
|
|
840
|
-
currentFootnoteIds = [...
|
|
1336
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
841
1337
|
finishPage();
|
|
842
1338
|
}
|
|
843
1339
|
}
|
|
@@ -1014,7 +1510,13 @@ export function paginateHtml(html, container, options = {}) {
|
|
|
1014
1510
|
if (!pageContainer) {
|
|
1015
1511
|
throw new Error("Pagination container element not found");
|
|
1016
1512
|
}
|
|
1017
|
-
|
|
1513
|
+
// This convenience function is the read-only HTML viewer path. Keep the
|
|
1514
|
+
// lower-level engine conservative by default, while enabling paragraph
|
|
1515
|
+
// fragmentation here unless a caller deliberately opts out.
|
|
1516
|
+
const engine = new PaginationEngine(staging, pageContainer, {
|
|
1517
|
+
...options,
|
|
1518
|
+
fragmentParagraphs: options.fragmentParagraphs ?? true,
|
|
1519
|
+
});
|
|
1018
1520
|
return engine.paginate();
|
|
1019
1521
|
}
|
|
1020
1522
|
//# sourceMappingURL=pagination.js.map
|