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.
Files changed (80) hide show
  1. package/dist/docxodus.worker.js +2 -2
  2. package/dist/docxodus.worker.js.map +1 -1
  3. package/dist/editor-headerfooter.d.ts +130 -0
  4. package/dist/editor-headerfooter.d.ts.map +1 -0
  5. package/dist/editor-headerfooter.js +414 -0
  6. package/dist/editor-headerfooter.js.map +1 -0
  7. package/dist/editor.bundle.js +1075 -81
  8. package/dist/editor.d.ts +82 -4
  9. package/dist/editor.d.ts.map +1 -1
  10. package/dist/editor.js +304 -63
  11. package/dist/editor.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/pagination.bundle.js +480 -19
  16. package/dist/pagination.d.ts +81 -0
  17. package/dist/pagination.d.ts.map +1 -1
  18. package/dist/pagination.js +528 -26
  19. package/dist/pagination.js.map +1 -1
  20. package/dist/react.d.ts +3 -1
  21. package/dist/react.d.ts.map +1 -1
  22. package/dist/react.js +6 -4
  23. package/dist/react.js.map +1 -1
  24. package/dist/session.bundle.js +15 -0
  25. package/dist/session.d.ts +13 -0
  26. package/dist/session.d.ts.map +1 -1
  27. package/dist/session.js +15 -0
  28. package/dist/session.js.map +1 -1
  29. package/dist/types.d.ts +47 -10
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js +9 -11
  32. package/dist/types.js.map +1 -1
  33. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  34. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  35. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  36. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  37. package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
  38. package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
  39. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  40. package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
  41. package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
  42. package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
  43. package/dist/wasm/_framework/System.Console.wasm +0 -0
  44. package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
  45. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  46. package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
  47. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  48. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  49. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  50. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  51. package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
  52. package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
  53. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  54. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  55. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  56. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  57. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  58. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  59. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  60. package/dist/wasm/_framework/System.Text.Encoding.Extensions.wasm +0 -0
  61. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
  62. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  63. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  64. package/dist/wasm/_framework/System.Threading.Thread.wasm +0 -0
  65. package/dist/wasm/_framework/System.Threading.wasm +0 -0
  66. package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
  67. package/dist/wasm/_framework/System.Xml.ReaderWriter.wasm +0 -0
  68. package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
  69. package/dist/wasm/_framework/System.Xml.XPath.XDocument.wasm +0 -0
  70. package/dist/wasm/_framework/System.Xml.XPath.wasm +0 -0
  71. package/dist/wasm/_framework/System.wasm +0 -0
  72. package/dist/wasm/_framework/dotnet.boot.js +41 -41
  73. package/dist/wasm/_framework/dotnet.js +1 -1
  74. package/dist/wasm/_framework/dotnet.js.map +1 -1
  75. package/dist/wasm/_framework/dotnet.native.js +3 -3
  76. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  77. package/dist/wasm/_framework/dotnet.runtime.js +1 -1
  78. package/dist/wasm/_framework/dotnet.runtime.js.map +1 -1
  79. package/dist/wasm/index.html +2 -1
  80. package/package.json +1 -1
@@ -82,6 +82,7 @@ var DocxodusPagination = (() => {
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 = /* @__PURE__ */ new Map();
86
87
  this.footnoteRegistry = /* @__PURE__ */ new Map();
87
88
  }
@@ -148,6 +149,393 @@ var DocxodusPagination = (() => {
148
149
  }
149
150
  return blocks;
150
151
  }
152
+ /**
153
+ * Measures one element in the same hidden staging context used for the source blocks.
154
+ * This is intentionally DOM-based: table row heights cannot be inferred from individual
155
+ * rows because wrapping and collapsed borders change the height of a fragment.
156
+ */
157
+ measureElement(element, dims) {
158
+ const measurementHost = document.createElement("div");
159
+ measurementHost.style.position = "absolute";
160
+ measurementHost.style.visibility = "hidden";
161
+ measurementHost.style.left = "-9999px";
162
+ measurementHost.style.width = `${dims.contentWidth}pt`;
163
+ const measuredElement = element.cloneNode(true);
164
+ measurementHost.appendChild(measuredElement);
165
+ this.stagingElement.appendChild(measurementHost);
166
+ const rect = measuredElement.getBoundingClientRect();
167
+ const style = window.getComputedStyle(measuredElement);
168
+ const measured = {
169
+ element,
170
+ heightPt: pxToPt(rect.height),
171
+ marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
172
+ marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
173
+ keepWithNext: element.dataset.keepWithNext === "true",
174
+ keepLines: element.dataset.keepLines === "true",
175
+ pageBreakBefore: element.dataset.pageBreakBefore === "true",
176
+ isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
177
+ };
178
+ this.stagingElement.removeChild(measurementHost);
179
+ return measured;
180
+ }
181
+ /**
182
+ * Returns the contiguous keep-with-next chain beginning at a block.
183
+ *
184
+ * A hard page break or a page-break-before directive is stronger than a
185
+ * keep-with-next directive, so it terminates the chain. The caller only
186
+ * keeps a chain together when the whole chain can fit on a fresh page.
187
+ */
188
+ getKeepWithNextChain(blocks, startIndex) {
189
+ const firstBlock = blocks[startIndex];
190
+ if (!firstBlock) return [];
191
+ const chain = [firstBlock];
192
+ let lastIndex = startIndex;
193
+ while (blocks[lastIndex].keepWithNext) {
194
+ const nextBlock = blocks[lastIndex + 1];
195
+ if (!nextBlock || nextBlock.isPageBreak || nextBlock.pageBreakBefore) {
196
+ break;
197
+ }
198
+ chain.push(nextBlock);
199
+ lastIndex++;
200
+ }
201
+ return chain;
202
+ }
203
+ /**
204
+ * Measures the visible body height of a keep-with-next chain using the same
205
+ * collapsed-margin rules as normal block placement. The trailing margin is
206
+ * intentionally excluded, matching the individual block fit check.
207
+ */
208
+ measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
209
+ const firstBlock = chain[0];
210
+ if (!firstBlock) return 0;
211
+ const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
212
+ let bodyHeight = firstMarginTop + firstBlock.heightPt;
213
+ for (let index = 1; index < chain.length; index++) {
214
+ const previousBlock = chain[index - 1];
215
+ const block = chain[index];
216
+ bodyHeight += Math.max(previousBlock.marginBottomPt, block.marginTopPt) + block.heightPt;
217
+ }
218
+ return bodyHeight;
219
+ }
220
+ /**
221
+ * Finds footnote references introduced by a sequence of blocks, preserving
222
+ * document order and excluding references already assigned to the page.
223
+ */
224
+ collectNewFootnoteIds(blocks, existingFootnoteIds) {
225
+ const knownIds = new Set(existingFootnoteIds);
226
+ const newIds = [];
227
+ for (const block of blocks) {
228
+ for (const id of this.extractFootnoteRefs(block.element)) {
229
+ if (!knownIds.has(id)) {
230
+ knownIds.add(id);
231
+ newIds.push(id);
232
+ }
233
+ }
234
+ }
235
+ return newIds;
236
+ }
237
+ /**
238
+ * The shortest body available to a section's first, default, or even page.
239
+ * A row fragment must fit every variant, otherwise a later header/footer could
240
+ * send it through the oversized-block fallback again.
241
+ */
242
+ smallestEffectiveContentHeight(dims, sectionIndex) {
243
+ return Math.min(
244
+ this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
245
+ this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
246
+ this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
247
+ );
248
+ }
249
+ /**
250
+ * Builds a clone of a simple table wrapper containing a contiguous run of rows.
251
+ * Complex table features are deliberately rejected by the caller: a split across
252
+ * merged cells, nested tables, or footnotes cannot be made correct by cloning rows.
253
+ */
254
+ createSimpleTableFragment(wrapper, table, body, rows, retainAnchor) {
255
+ const wrapperClone = wrapper.cloneNode(false);
256
+ const tableClone = table.cloneNode(false);
257
+ for (const child of Array.from(table.children)) {
258
+ if (child !== body) {
259
+ tableClone.appendChild(child.cloneNode(true));
260
+ }
261
+ }
262
+ const bodyClone = body.cloneNode(false);
263
+ for (const row of rows) {
264
+ bodyClone.appendChild(row.cloneNode(true));
265
+ }
266
+ tableClone.appendChild(bodyClone);
267
+ if (!retainAnchor) {
268
+ wrapperClone.removeAttribute("data-anchor");
269
+ tableClone.removeAttribute("data-anchor");
270
+ }
271
+ wrapperClone.appendChild(tableClone);
272
+ return wrapperClone;
273
+ }
274
+ /**
275
+ * Splits an oversized, ordinary table at row boundaries. This only participates
276
+ * in the existing oversized-block fallback; unsupported tables keep the previous
277
+ * overflow behavior rather than risking broken table semantics.
278
+ */
279
+ trySplitSimpleOversizedTable(block, dims, sectionIndex) {
280
+ const wrapper = block.element;
281
+ if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
282
+ return null;
283
+ }
284
+ const table = wrapper.firstElementChild;
285
+ if (!(table instanceof HTMLTableElement)) {
286
+ return null;
287
+ }
288
+ const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
289
+ if (!body || table.tHead || table.tFoot || body.rows.length < 2 || Array.from(table.children).some((child) => child !== body && child.tagName !== "COLGROUP") || table.querySelector("table, [rowspan], [colspan], [data-footnote-id]") || wrapper.querySelector("[data-footnote-id]")) {
290
+ return null;
291
+ }
292
+ const rows = Array.from(body.rows);
293
+ const minimumContentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
294
+ const maximumFragmentHeight = minimumContentHeight - block.marginTopPt - block.marginBottomPt;
295
+ if (maximumFragmentHeight <= 0) {
296
+ return null;
297
+ }
298
+ const groups = [];
299
+ let start = 0;
300
+ while (start < rows.length) {
301
+ let end = start;
302
+ while (end < rows.length) {
303
+ const candidate = this.createSimpleTableFragment(
304
+ wrapper,
305
+ table,
306
+ body,
307
+ rows.slice(start, end + 1),
308
+ start === 0
309
+ );
310
+ const measured = this.measureElement(candidate, dims);
311
+ if (measured.heightPt > maximumFragmentHeight) {
312
+ break;
313
+ }
314
+ end++;
315
+ }
316
+ if (end === start) {
317
+ return null;
318
+ }
319
+ groups.push(rows.slice(start, end));
320
+ start = end;
321
+ }
322
+ if (groups.length < 2) {
323
+ return null;
324
+ }
325
+ const fragments = [];
326
+ for (let index = 0; index < groups.length; index++) {
327
+ const isFirst = index === 0;
328
+ const isLast = index === groups.length - 1;
329
+ const fragment = this.createSimpleTableFragment(
330
+ wrapper,
331
+ table,
332
+ body,
333
+ groups[index],
334
+ isFirst
335
+ );
336
+ if (!isFirst) {
337
+ fragment.style.setProperty("margin-top", "0", "important");
338
+ }
339
+ if (!isLast) {
340
+ fragment.style.setProperty("margin-bottom", "0", "important");
341
+ }
342
+ const measured = this.measureElement(fragment, dims);
343
+ if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
344
+ return null;
345
+ }
346
+ fragments.push({
347
+ ...measured,
348
+ keepWithNext: false,
349
+ keepLines: false,
350
+ pageBreakBefore: false,
351
+ isPageBreak: false
352
+ });
353
+ }
354
+ return fragments;
355
+ }
356
+ /**
357
+ * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
358
+ * after whitespace or at a run boundary so the paginator never deliberately
359
+ * cuts through an ordinary word merely to fill a little more of a page.
360
+ */
361
+ paragraphFragmentEndpoints(paragraph) {
362
+ const endpoints = [];
363
+ const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
364
+ let textNode;
365
+ while (textNode = walker.nextNode()) {
366
+ const text = textNode.data;
367
+ if (text.length === 0) continue;
368
+ const whitespace = /\s+/g;
369
+ let match;
370
+ while ((match = whitespace.exec(text)) !== null) {
371
+ endpoints.push({ node: textNode, offset: match.index + match[0].length });
372
+ }
373
+ if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
374
+ endpoints.push({ node: textNode, offset: text.length });
375
+ }
376
+ }
377
+ return endpoints;
378
+ }
379
+ /**
380
+ * Whether a range contains visible text after ignoring bidi/zero-width marks.
381
+ * Paragraph fragmentation deliberately excludes non-textual descendants, so
382
+ * this is enough to reject empty head or tail fragments.
383
+ */
384
+ hasVisibleFragmentText(fragment) {
385
+ return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
386
+ }
387
+ /**
388
+ * Phase one only handles text-like paragraph descendants. Objects, explicit
389
+ * line breaks, list markers, notes, and out-of-flow/inline-block content all
390
+ * require their own line-layout rules and retain the established whole-block
391
+ * fallback instead of risking broken content or duplicate anchors.
392
+ */
393
+ canFragmentParagraph(block) {
394
+ if (!this.fragmentParagraphs || block.element.tagName !== "P") {
395
+ return false;
396
+ }
397
+ const paragraph = block.element;
398
+ if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
399
+ return false;
400
+ }
401
+ const unsupportedDescendants = [
402
+ "br",
403
+ "img",
404
+ "picture",
405
+ "svg",
406
+ "math",
407
+ "canvas",
408
+ "video",
409
+ "audio",
410
+ "iframe",
411
+ "object",
412
+ "embed",
413
+ "input",
414
+ "button",
415
+ "select",
416
+ "textarea",
417
+ "table",
418
+ "ol",
419
+ "ul",
420
+ "li",
421
+ "dl",
422
+ "div",
423
+ "p",
424
+ "section",
425
+ "article",
426
+ "aside",
427
+ "figure",
428
+ "fieldset",
429
+ "details",
430
+ "[data-footnote-id]",
431
+ "[data-list-marker]",
432
+ "[data-anchor]",
433
+ "[id]",
434
+ "[contenteditable]"
435
+ ].join(", ");
436
+ if (paragraph.querySelector(unsupportedDescendants)) {
437
+ return false;
438
+ }
439
+ const paragraphStyle = window.getComputedStyle(paragraph);
440
+ if (paragraphStyle.display !== "block" || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
441
+ return false;
442
+ }
443
+ for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
444
+ const style = window.getComputedStyle(descendant);
445
+ if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
446
+ return false;
447
+ }
448
+ }
449
+ return true;
450
+ }
451
+ /**
452
+ * Builds one range-cloned paragraph fragment. Only the leading fragment keeps
453
+ * the source paragraph's addressability; continuations must not duplicate an
454
+ * id/data-anchor in the rendered document.
455
+ */
456
+ createParagraphFragment(paragraph, range, retainSourceIdentity, isFinalFragment) {
457
+ const fragment = paragraph.cloneNode(false);
458
+ const contents = range.cloneContents();
459
+ fragment.appendChild(contents);
460
+ if (!retainSourceIdentity) {
461
+ fragment.removeAttribute("id");
462
+ fragment.removeAttribute("data-anchor");
463
+ fragment.style.setProperty("margin-top", "0", "important");
464
+ fragment.style.setProperty("text-indent", "0", "important");
465
+ }
466
+ if (!isFinalFragment) {
467
+ fragment.style.setProperty("margin-bottom", "0", "important");
468
+ }
469
+ return fragment;
470
+ }
471
+ /**
472
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
473
+ * currently available body space. The caller then processes the tail normally,
474
+ * allowing it to fragment again on later pages when necessary.
475
+ */
476
+ tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
477
+ if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
478
+ return null;
479
+ }
480
+ const paragraph = block.element;
481
+ const endpoints = this.paragraphFragmentEndpoints(paragraph);
482
+ if (endpoints.length < 2) {
483
+ return null;
484
+ }
485
+ let low = 0;
486
+ let high = endpoints.length - 2;
487
+ let best = null;
488
+ while (low <= high) {
489
+ const middle = Math.floor((low + high) / 2);
490
+ const endpoint = endpoints[middle];
491
+ const headRange = document.createRange();
492
+ headRange.setStart(paragraph, 0);
493
+ headRange.setEnd(endpoint.node, endpoint.offset);
494
+ const headContents = headRange.cloneContents();
495
+ if (!this.hasVisibleFragmentText(headContents)) {
496
+ low = middle + 1;
497
+ continue;
498
+ }
499
+ const head = this.createParagraphFragment(paragraph, headRange, true, false);
500
+ const measured = this.measureElement(head, dims);
501
+ if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
502
+ best = { endpoint, element: head, measured };
503
+ low = middle + 1;
504
+ } else {
505
+ high = middle - 1;
506
+ }
507
+ }
508
+ if (!best) {
509
+ return null;
510
+ }
511
+ const tailRange = document.createRange();
512
+ tailRange.setStart(best.endpoint.node, best.endpoint.offset);
513
+ tailRange.setEnd(paragraph, paragraph.childNodes.length);
514
+ const tailContents = tailRange.cloneContents();
515
+ if (!this.hasVisibleFragmentText(tailContents)) {
516
+ return null;
517
+ }
518
+ const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
519
+ const tailMeasured = this.measureElement(tail, dims);
520
+ return [
521
+ {
522
+ ...best.measured,
523
+ element: best.element,
524
+ keepWithNext: false,
525
+ keepLines: false,
526
+ pageBreakBefore: false,
527
+ isPageBreak: false
528
+ },
529
+ {
530
+ ...tailMeasured,
531
+ element: tail,
532
+ keepWithNext: false,
533
+ keepLines: false,
534
+ pageBreakBefore: false,
535
+ isPageBreak: false
536
+ }
537
+ ];
538
+ }
151
539
  /**
152
540
  * Parses the header/footer registry from the staging element.
153
541
  * Also measures heights during parsing for lazy-loading compatibility.
@@ -546,7 +934,8 @@ var DocxodusPagination = (() => {
546
934
  currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
547
935
  }
548
936
  const finishPage = () => {
549
- if (currentContent.length === 0 && !currentContinuation) return;
937
+ const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
938
+ if (currentContent.length === 0 && !hasCurrentContinuation) return;
550
939
  const page = this.createPage(
551
940
  dims,
552
941
  pageNumber,
@@ -578,7 +967,6 @@ var DocxodusPagination = (() => {
578
967
  };
579
968
  for (let i = 0; i < blocks.length; i++) {
580
969
  const block = blocks[i];
581
- const nextBlock = blocks[i + 1];
582
970
  if (block.isPageBreak) {
583
971
  finishPage();
584
972
  continue;
@@ -586,8 +974,59 @@ var DocxodusPagination = (() => {
586
974
  if (block.pageBreakBefore && currentContent.length > 0) {
587
975
  finishPage();
588
976
  }
589
- const blockFootnoteIds = this.extractFootnoteRefs(block.element);
590
- const newFootnoteIds = blockFootnoteIds.filter((id) => !currentFootnoteIds.includes(id));
977
+ const previousBlock = blocks[i - 1];
978
+ const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
979
+ const pageHasOccupiedSpace = currentContent.length > 0 || currentFootnoteHeight > 0;
980
+ if (pageHasOccupiedSpace && block.keepWithNext && startsKeepChain) {
981
+ const keepChain = this.getKeepWithNextChain(blocks, i);
982
+ if (keepChain.length > 1) {
983
+ const newChainFootnoteIds = this.collectNewFootnoteIds(
984
+ keepChain,
985
+ currentFootnoteIds
986
+ );
987
+ let additionalChainFootnoteHeight = 0;
988
+ if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
989
+ const totalChainFootnoteHeight = this.measureFootnotesHeight(
990
+ [...currentFootnoteIds, ...newChainFootnoteIds],
991
+ dims.contentWidth,
992
+ currentContinuation
993
+ );
994
+ additionalChainFootnoteHeight = Math.max(
995
+ 0,
996
+ totalChainFootnoteHeight - currentFootnoteHeight
997
+ );
998
+ }
999
+ const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
1000
+ keepChain,
1001
+ prevMarginBottomPt,
1002
+ currentContent.length === 0
1003
+ ) + additionalChainFootnoteHeight;
1004
+ const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
1005
+ if (currentChainHeight > currentAvailableHeight) {
1006
+ const nextPageHeights = this.getEffectiveHeights(
1007
+ dims,
1008
+ sectionIndex,
1009
+ pageInSection + 1,
1010
+ pageNumber + 1
1011
+ );
1012
+ const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
1013
+ keepChain,
1014
+ 0,
1015
+ true
1016
+ );
1017
+ const freshChainFootnoteHeight = this.measureFootnotesHeight(
1018
+ newChainFootnoteIds,
1019
+ dims.contentWidth,
1020
+ nextPageContinuation
1021
+ );
1022
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
1023
+ finishPage();
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
1029
+ const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
591
1030
  let additionalFootnoteHeight = 0;
592
1031
  if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
593
1032
  const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
@@ -604,15 +1043,23 @@ var DocxodusPagination = (() => {
604
1043
  effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
605
1044
  }
606
1045
  const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
607
- let neededHeight = blockSpace;
608
- if (block.keepWithNext && nextBlock && !nextBlock.isPageBreak) {
609
- const collapsedMargin = Math.max(block.marginBottomPt, nextBlock.marginTopPt);
610
- neededHeight = effectiveMarginTop + block.heightPt + collapsedMargin + nextBlock.heightPt + additionalFootnoteHeight;
611
- }
612
1046
  const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
613
1047
  const bodyContentUsed = effectiveContentHeight - remainingHeight;
614
1048
  const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
615
1049
  const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
1050
+ if (blockSpace > effectiveRemainingHeight) {
1051
+ const paragraphFragments = this.tryFragmentParagraph(
1052
+ block,
1053
+ dims,
1054
+ effectiveRemainingHeight,
1055
+ effectiveMarginTop
1056
+ );
1057
+ if (paragraphFragments) {
1058
+ blocks.splice(i, 1, ...paragraphFragments);
1059
+ i--;
1060
+ continue;
1061
+ }
1062
+ }
616
1063
  if (blockSpace <= effectiveRemainingHeight) {
617
1064
  currentContent.push(block.element.cloneNode(true));
618
1065
  remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
@@ -654,10 +1101,12 @@ var DocxodusPagination = (() => {
654
1101
  footnoteId,
655
1102
  fittingElements: fits
656
1103
  });
657
- nextPageContinuation = {
658
- footnoteId,
659
- remainingElements: overflow
660
- };
1104
+ if (overflow.length > 0) {
1105
+ nextPageContinuation = {
1106
+ footnoteId,
1107
+ remainingElements: overflow
1108
+ };
1109
+ }
661
1110
  currentFootnoteHeight = availableForFootnotes;
662
1111
  } else {
663
1112
  nextPageContinuation = {
@@ -690,30 +1139,39 @@ var DocxodusPagination = (() => {
690
1139
  currentFootnoteHeight = expandedFootnoteSpace;
691
1140
  } else {
692
1141
  finishPage();
693
- const newPageFootnoteHeight = blockFootnoteIds.length > 0 ? this.measureFootnotesHeight(blockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1142
+ const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
694
1143
  const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
695
1144
  currentContent.push(block.element.cloneNode(true));
696
1145
  remainingHeight = effectiveContentHeight - newPageSpace;
697
1146
  prevMarginBottomPt = block.marginBottomPt;
698
- currentFootnoteIds = [...blockFootnoteIds];
1147
+ currentFootnoteIds = [...allBlockFootnoteIds];
699
1148
  currentFootnoteHeight = newPageFootnoteHeight;
700
1149
  }
701
1150
  } else {
702
1151
  finishPage();
703
- const newPageFootnoteHeight = blockFootnoteIds.length > 0 ? this.measureFootnotesHeight(blockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
1152
+ const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
704
1153
  const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
705
1154
  currentContent.push(block.element.cloneNode(true));
706
1155
  remainingHeight = effectiveContentHeight - newPageSpace;
707
1156
  prevMarginBottomPt = block.marginBottomPt;
708
- currentFootnoteIds = [...blockFootnoteIds];
1157
+ currentFootnoteIds = [...allBlockFootnoteIds];
709
1158
  currentFootnoteHeight = newPageFootnoteHeight;
710
1159
  }
711
1160
  } else {
1161
+ const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
1162
+ if (tableFragments) {
1163
+ if (currentContent.length > 0 || currentContinuation) {
1164
+ finishPage();
1165
+ }
1166
+ blocks.splice(i, 1, ...tableFragments);
1167
+ i--;
1168
+ continue;
1169
+ }
712
1170
  if (currentContent.length > 0) {
713
1171
  finishPage();
714
1172
  }
715
1173
  currentContent.push(block.element.cloneNode(true));
716
- currentFootnoteIds = [...blockFootnoteIds];
1174
+ currentFootnoteIds = [...allBlockFootnoteIds];
717
1175
  finishPage();
718
1176
  }
719
1177
  }
@@ -837,7 +1295,10 @@ var DocxodusPagination = (() => {
837
1295
  if (!pageContainer) {
838
1296
  throw new Error("Pagination container element not found");
839
1297
  }
840
- const engine = new PaginationEngine(staging, pageContainer, options);
1298
+ const engine = new PaginationEngine(staging, pageContainer, {
1299
+ ...options,
1300
+ fragmentParagraphs: options.fragmentParagraphs ?? true
1301
+ });
841
1302
  return engine.paginate();
842
1303
  }
843
1304
  return __toCommonJS(pagination_exports);
@@ -117,6 +117,12 @@ export interface PaginationOptions {
117
117
  showPageNumbers?: boolean;
118
118
  /** Gap between pages in pixels. Default: 20 */
119
119
  pageGap?: number;
120
+ /**
121
+ * Whether ordinary paragraphs may be fragmented across page boundaries.
122
+ * Defaults to false for direct PaginationEngine callers; read-only viewer
123
+ * entry points opt in explicitly.
124
+ */
125
+ fragmentParagraphs?: boolean;
120
126
  }
121
127
  /**
122
128
  * Pagination engine that converts HTML with pagination metadata
@@ -133,6 +139,7 @@ export declare class PaginationEngine {
133
139
  private cssPrefix;
134
140
  private showPageNumbers;
135
141
  private pageGap;
142
+ private fragmentParagraphs;
136
143
  private hfRegistry;
137
144
  private footnoteRegistry;
138
145
  private pendingFootnoteContinuation;
@@ -154,6 +161,80 @@ export declare class PaginationEngine {
154
161
  * Measures all content blocks in a section.
155
162
  */
156
163
  private measureBlocks;
164
+ /**
165
+ * Measures one element in the same hidden staging context used for the source blocks.
166
+ * This is intentionally DOM-based: table row heights cannot be inferred from individual
167
+ * rows because wrapping and collapsed borders change the height of a fragment.
168
+ */
169
+ private measureElement;
170
+ /**
171
+ * Returns the contiguous keep-with-next chain beginning at a block.
172
+ *
173
+ * A hard page break or a page-break-before directive is stronger than a
174
+ * keep-with-next directive, so it terminates the chain. The caller only
175
+ * keeps a chain together when the whole chain can fit on a fresh page.
176
+ */
177
+ private getKeepWithNextChain;
178
+ /**
179
+ * Measures the visible body height of a keep-with-next chain using the same
180
+ * collapsed-margin rules as normal block placement. The trailing margin is
181
+ * intentionally excluded, matching the individual block fit check.
182
+ */
183
+ private measureKeepWithNextChainBodyHeight;
184
+ /**
185
+ * Finds footnote references introduced by a sequence of blocks, preserving
186
+ * document order and excluding references already assigned to the page.
187
+ */
188
+ private collectNewFootnoteIds;
189
+ /**
190
+ * The shortest body available to a section's first, default, or even page.
191
+ * A row fragment must fit every variant, otherwise a later header/footer could
192
+ * send it through the oversized-block fallback again.
193
+ */
194
+ private smallestEffectiveContentHeight;
195
+ /**
196
+ * Builds a clone of a simple table wrapper containing a contiguous run of rows.
197
+ * Complex table features are deliberately rejected by the caller: a split across
198
+ * merged cells, nested tables, or footnotes cannot be made correct by cloning rows.
199
+ */
200
+ private createSimpleTableFragment;
201
+ /**
202
+ * Splits an oversized, ordinary table at row boundaries. This only participates
203
+ * in the existing oversized-block fallback; unsupported tables keep the previous
204
+ * overflow behavior rather than risking broken table semantics.
205
+ */
206
+ private trySplitSimpleOversizedTable;
207
+ /**
208
+ * A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
209
+ * after whitespace or at a run boundary so the paginator never deliberately
210
+ * cuts through an ordinary word merely to fill a little more of a page.
211
+ */
212
+ private paragraphFragmentEndpoints;
213
+ /**
214
+ * Whether a range contains visible text after ignoring bidi/zero-width marks.
215
+ * Paragraph fragmentation deliberately excludes non-textual descendants, so
216
+ * this is enough to reject empty head or tail fragments.
217
+ */
218
+ private hasVisibleFragmentText;
219
+ /**
220
+ * Phase one only handles text-like paragraph descendants. Objects, explicit
221
+ * line breaks, list markers, notes, and out-of-flow/inline-block content all
222
+ * require their own line-layout rules and retain the established whole-block
223
+ * fallback instead of risking broken content or duplicate anchors.
224
+ */
225
+ private canFragmentParagraph;
226
+ /**
227
+ * Builds one range-cloned paragraph fragment. Only the leading fragment keeps
228
+ * the source paragraph's addressability; continuations must not duplicate an
229
+ * id/data-anchor in the rendered document.
230
+ */
231
+ private createParagraphFragment;
232
+ /**
233
+ * Splits a simple paragraph at the largest DOM Range endpoint that fits the
234
+ * currently available body space. The caller then processes the tail normally,
235
+ * allowing it to fragment again on later pages when necessary.
236
+ */
237
+ private tryFragmentParagraph;
157
238
  /**
158
239
  * Parses the header/footer registry from the staging element.
159
240
  * Also measures heights during parsing for lazy-loading compatibility.