@stll/folio-core 0.3.1 → 0.5.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 (120) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +35 -5
  6. package/dist/ai-edits/headless.js +92 -22
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -115
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.d.ts +6 -0
  22. package/dist/docx/encryption/agileDecryption.js +209 -0
  23. package/dist/docx/encryption/compoundFile.d.ts +20 -0
  24. package/dist/docx/encryption/compoundFile.js +251 -0
  25. package/dist/docx/encryption/containerFormat.d.ts +15 -0
  26. package/dist/docx/encryption/containerFormat.js +40 -0
  27. package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
  28. package/dist/docx/encryption/cryptoBytes.js +55 -0
  29. package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
  30. package/dist/docx/encryption/encryptionInfo.js +128 -0
  31. package/dist/docx/encryption/errors.d.ts +18 -0
  32. package/dist/docx/encryption/errors.js +13 -0
  33. package/dist/docx/encryption/index.d.ts +4 -0
  34. package/dist/docx/encryption/index.js +4 -0
  35. package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
  36. package/dist/docx/encryption/openEncryptedDocx.js +65 -0
  37. package/dist/docx/footnoteParser.d.ts +1 -17
  38. package/dist/docx/footnoteParser.js +1 -27
  39. package/dist/docx/groupDrawingParser.d.ts +8 -0
  40. package/dist/docx/groupDrawingParser.js +131 -0
  41. package/dist/docx/index.d.ts +3 -1
  42. package/dist/docx/index.js +3 -1
  43. package/dist/docx/numberingParser.d.ts +3 -1
  44. package/dist/docx/numberingParser.js +8 -2
  45. package/dist/docx/paragraphParser.js +9 -3
  46. package/dist/docx/parser.d.ts +4 -5
  47. package/dist/docx/parser.js +8 -2
  48. package/dist/docx/runParser.js +8 -1
  49. package/dist/docx/selectiveXmlPatch.js +4 -10
  50. package/dist/docx/serializer/numberingSerializer.js +3 -1
  51. package/dist/docx/settingsParser.d.ts +6 -3
  52. package/dist/docx/settingsParser.js +2 -0
  53. package/dist/docx/unzip.d.ts +7 -5
  54. package/dist/docx/unzip.js +8 -2
  55. package/dist/docx/xmlParser.js +11 -4
  56. package/dist/index.d.ts +3 -2
  57. package/dist/index.js +6 -5
  58. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  59. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  60. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
  61. package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
  63. package/dist/layout-engine/index.d.ts +6 -5
  64. package/dist/layout-engine/index.js +116 -27
  65. package/dist/layout-engine/keep-together.d.ts +13 -8
  66. package/dist/layout-engine/keep-together.js +40 -25
  67. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  68. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  69. package/dist/layout-engine/measure/measureBlocks.js +43 -19
  70. package/dist/layout-engine/measure/measureParagraph.js +91 -14
  71. package/dist/layout-engine/paginator.d.ts +2 -1
  72. package/dist/layout-engine/paginator.js +9 -3
  73. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  74. package/dist/layout-engine/textBoxGroup.js +8 -0
  75. package/dist/layout-engine/types.d.ts +33 -9
  76. package/dist/layout-engine/types.js +20 -2
  77. package/dist/layout-painter/renderPage.d.ts +15 -1
  78. package/dist/layout-painter/renderPage.js +29 -12
  79. package/dist/layout-painter/renderParagraph.js +44 -9
  80. package/dist/layout-painter/renderTable.js +27 -7
  81. package/dist/managers/DocumentLoaderManager.d.ts +3 -1
  82. package/dist/managers/DocumentLoaderManager.js +13 -5
  83. package/dist/managers/TableSelectionManager.js +1 -1
  84. package/dist/model.js +1 -1
  85. package/dist/paged-layout/pageText.d.ts +14 -0
  86. package/dist/paged-layout/pageText.js +21 -0
  87. package/dist/prosemirror/attrs/index.js +3 -0
  88. package/dist/prosemirror/commands/comments.js +75 -5
  89. package/dist/prosemirror/commands/hyperlink.js +4 -2
  90. package/dist/prosemirror/commands/image.js +2 -1
  91. package/dist/prosemirror/commands/index.js +1 -1
  92. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  93. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  94. package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
  95. package/dist/prosemirror/conversion/toProseDoc.js +43 -15
  96. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
  97. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
  99. package/dist/prosemirror/findReplaceSelection.js +2 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/insertOperations.js +1 -1
  102. package/dist/prosemirror/schema/nodes.d.ts +14 -7
  103. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  104. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  105. package/dist/redline.d.ts +20 -0
  106. package/dist/redline.js +127 -0
  107. package/dist/server.d.ts +7 -3
  108. package/dist/server.js +6 -2
  109. package/dist/symbols.d.ts +35 -0
  110. package/dist/symbols.js +512 -0
  111. package/dist/utils/clipboard.js +3 -3
  112. package/dist/utils/fontLoader.js +3 -0
  113. package/dist/utils/fontResolver.js +65 -1
  114. package/dist/utils/zoom.d.ts +21 -0
  115. package/dist/utils/zoom.js +21 -0
  116. package/dist/version-comparison.d.ts +92 -0
  117. package/dist/version-comparison.js +436 -0
  118. package/package.json +7 -3
  119. package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
  120. package/dist/paged-layout/headerFooterMargins.js +0 -120
@@ -5,6 +5,7 @@ import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
5
5
  import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
6
6
  import { resolveShadingFill } from "../../utils/formatToStyle.js";
7
7
  import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
8
+ import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
8
9
  import { directionIsRtl } from "../../prosemirror/paragraphDirection.js";
9
10
  import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
10
11
  import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
@@ -100,7 +101,7 @@ function toLetter(value, upper) {
100
101
  return String.fromCodePoint(baseCodePoint + zeroBased % 26).repeat(Math.floor(zeroBased / 26) + 1);
101
102
  }
102
103
  function formatCounter(value, format) {
103
- if (value <= 0) return "";
104
+ if (!Number.isFinite(value)) return "";
104
105
  switch (format) {
105
106
  case "upperRoman": return toRoman(value, true);
106
107
  case "lowerRoman": return toRoman(value, false);
@@ -119,7 +120,9 @@ function resolveListTemplate(template, counters, levelFormats, forceDecimal = fa
119
120
  const { digit, punct = "" } = args.at(-1);
120
121
  const index = Number.parseInt(digit, 10) - 1;
121
122
  if (index < 0) return "";
122
- const formatted = formatCounter(counters[index] ?? 0, forceDecimal ? "decimal" : levelFormats?.[index]);
123
+ const counter = counters[index];
124
+ if (counter === void 0 || Number.isNaN(counter)) return "";
125
+ const formatted = formatCounter(counter, forceDecimal ? "decimal" : levelFormats?.[index]);
123
126
  return formatted ? `${formatted}${punct}` : "";
124
127
  });
125
128
  }
@@ -143,22 +146,28 @@ function computeListMarker(pmAttrs, listCounters, abstractCounters, seenNumIds)
143
146
  }
144
147
  if (pmAttrs.listIsBullet) return convertBulletToUnicode(pmAttrs.listMarker || "");
145
148
  const level = pmAttrs.numPr?.ilvl ?? 0;
146
- const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => 0);
149
+ const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => NaN);
147
150
  const abstractNumId = pmAttrs.listAbstractNumId;
148
- if (abstractNumId !== void 0 && level > 0) {
149
- const latestAbstractCounters = abstractCounters.get(abstractNumId);
150
- const missingParentCounters = counters.slice(0, level).every((value) => value === 0);
151
- if (latestAbstractCounters && missingParentCounters) for (let i = 0; i < level; i += 1) counters[i] = latestAbstractCounters[i] ?? 0;
151
+ if (level > 0) {
152
+ const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId);
153
+ if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
154
+ const latestCounter = latestAbstractCounters?.[i];
155
+ counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
156
+ }
152
157
  }
153
158
  const seenKey = `${numId}:${level}`;
154
159
  if (!seenNumIds.has(seenKey)) {
155
160
  seenNumIds.add(seenKey);
156
161
  if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1;
162
+ else if (Number.isNaN(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
157
163
  }
158
164
  counters[level] = (counters[level] ?? 0) + 1;
159
- for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0;
165
+ for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
160
166
  const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0;
161
- if (childAdvances > 0 && level + 1 < counters.length) counters[level + 1] = (counters[level + 1] ?? 0) + childAdvances;
167
+ if (childAdvances > 0 && level + 1 < counters.length) {
168
+ const childCounter = counters[level + 1];
169
+ counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
170
+ }
162
171
  listCounters.set(numId, counters);
163
172
  if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]);
164
173
  const levelFormats = pmAttrs.listLevelNumFmts ?? (pmAttrs.listNumFmt ? [pmAttrs.listNumFmt] : void 0);
@@ -234,7 +243,7 @@ function extractRunFormatting(marks, theme) {
234
243
  }
235
244
  case "characterSpacing": {
236
245
  const attrs = expectCharacterSpacingMarkAttrs(mark);
237
- if (attrs.spacing !== void 0 && attrs.spacing !== 0) formatting.letterSpacing = twipsToPixels(attrs.spacing);
246
+ if (attrs.spacing !== void 0) formatting.letterSpacing = twipsToPixels(attrs.spacing);
238
247
  if (attrs.position !== void 0 && attrs.position !== 0) formatting.positionPx = halfPointsToPixels(attrs.position);
239
248
  if (attrs.scale !== void 0 && attrs.scale !== 100) formatting.horizontalScale = attrs.scale;
240
249
  if (attrs.kerning !== void 0 && attrs.kerning > 0) formatting.kerningMinPt = halfPointsToPoints(attrs.kerning);
@@ -336,10 +345,12 @@ function markDefaultBlackTextColorSource(formatting, paraDefaults) {
336
345
  };
337
346
  }
338
347
  function mergeRunFormatting(paraDefaults, formatting) {
339
- return {
348
+ const merged = {
340
349
  ...paraDefaults,
341
350
  ...markDefaultBlackTextColorSource(formatting, paraDefaults)
342
351
  };
352
+ if (merged.letterSpacing === 0) delete merged.letterSpacing;
353
+ return merged;
343
354
  }
344
355
  function applyRunFormattingOverrides(formatting, attrs) {
345
356
  if (attrs.bold === false) formatting.bold = false;
@@ -485,9 +496,10 @@ function paragraphToRuns(node, startPos, _options) {
485
496
  return;
486
497
  }
487
498
  if (child.type.name === "tab") {
499
+ const formatting = extractRunFormatting(child.marks, theme);
488
500
  const run = {
489
501
  kind: "tab",
490
- ...mergeRunFormatting(paraDefaults, extractRunFormatting(child.marks, theme)),
502
+ ...mergeRunFormatting(paraDefaults, formatting),
491
503
  pmStart: childPos,
492
504
  pmEnd: childPos + child.nodeSize
493
505
  };
@@ -496,7 +508,8 @@ function paragraphToRuns(node, startPos, _options) {
496
508
  }
497
509
  if (child.type.name === "image") {
498
510
  const attrs = expectImageAttrs(child);
499
- const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, _options.pageContentHeight);
511
+ if (!attrs.src) return;
512
+ const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, _options.pageContentHeight);
500
513
  const trackedFmt = extractRunFormatting(child.marks, theme);
501
514
  const run = buildImageRun(attrs, constrained, childPos, childPos + child.nodeSize, trackedFmt);
502
515
  runs.push(run);
@@ -613,6 +626,8 @@ function toPreviousListAttrs(previousFormatting) {
613
626
  if (isNumberFormat(listNumFmt)) attrs.listNumFmt = listNumFmt;
614
627
  const listLevelNumFmts = readNumberFormats(previousFormatting["listLevelNumFmts"]);
615
628
  if (listLevelNumFmts) attrs.listLevelNumFmts = listLevelNumFmts;
629
+ const listLevelStarts = previousFormatting["listLevelStarts"];
630
+ if (Array.isArray(listLevelStarts) && listLevelStarts.every((value) => typeof value === "number")) attrs.listLevelStarts = listLevelStarts;
616
631
  const listAbstractNumId = previousFormatting["listAbstractNumId"];
617
632
  if (typeof listAbstractNumId === "number") attrs.listAbstractNumId = listAbstractNumId;
618
633
  const listStartOverride = previousFormatting["listStartOverride"];
@@ -669,9 +684,10 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
669
684
  if (autoAfter) attrs.spacing.after = 14;
670
685
  else if (typeof spaceAfter === "number") attrs.spacing.after = twipsToPixels(spaceAfter);
671
686
  const pmSpacingExplicit = pmAttrs.spacingExplicit;
687
+ const spacingFromDocDefaults = pmAttrs.spacingFromDocDefaults;
672
688
  const explicit = {};
673
- if (autoBefore || pmSpacingExplicit?.before) explicit.before = true;
674
- if (autoAfter || pmSpacingExplicit?.after) explicit.after = true;
689
+ if (autoBefore || pmSpacingExplicit?.before || spacingFromDocDefaults?.before) explicit.before = true;
690
+ if (autoAfter || pmSpacingExplicit?.after || spacingFromDocDefaults?.after) explicit.after = true;
675
691
  if (explicit.before !== void 0 || explicit.after !== void 0) attrs.spacingExplicit = explicit;
676
692
  if (typeof lineSpacing === "number") if (pmAttrs.lineSpacingRule === "exact" || pmAttrs.lineSpacingRule === "atLeast") {
677
693
  attrs.spacing.line = twipsToPixels(lineSpacing);
@@ -730,8 +746,10 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
730
746
  return tabStop;
731
747
  });
732
748
  if (pmAttrs.pageBreakBefore) attrs.pageBreakBefore = true;
749
+ if (pmAttrs.renderedPageBreakBefore) attrs.renderedPageBreakBefore = true;
733
750
  if (pmAttrs.keepNext) attrs.keepNext = true;
734
751
  if (pmAttrs.keepLines) attrs.keepLines = true;
752
+ if (pmAttrs.widowControl === false) attrs.widowControl = false;
735
753
  if (pmAttrs.contextualSpacing) attrs.contextualSpacing = true;
736
754
  if (pmAttrs.runInWithNext) attrs.runInWithNext = true;
737
755
  if (directionIsRtl(pmAttrs.direction)) attrs.bidi = true;
@@ -808,6 +826,17 @@ function convertParagraph(node, startPos, options) {
808
826
  const pmAttrs = expectParagraphAttrs(node);
809
827
  const runs = paragraphToRuns(node, startPos, options);
810
828
  const attrs = convertParagraphAttrs(pmAttrs, options.theme, options.listCounters, options.listAbstractCounters, options.listSeenNumIds, options.defaultTabStopTwips, options.originalListCounters, options.originalListAbstractCounters, options.originalListSeenNumIds);
829
+ const defaultTextFormatting = pmAttrs.defaultTextFormatting;
830
+ if (runs.length === 0) {
831
+ const paragraphMarkFormatting = pmAttrs._originalFormatting?.runProperties;
832
+ if (paragraphMarkFormatting?.fontSize !== void 0) attrs.defaultFontSize = paragraphMarkFormatting.fontSize / 2;
833
+ const paragraphMarkFontFamily = paragraphMarkFormatting?.fontFamily?.ascii ?? paragraphMarkFormatting?.fontFamily?.hAnsi;
834
+ if (paragraphMarkFontFamily) attrs.defaultFontFamily = paragraphMarkFontFamily;
835
+ }
836
+ if (runs.length === 0 && defaultTextFormatting?.hidden === true) {
837
+ attrs.suppressEmptyParagraphHeight = true;
838
+ if (attrs.listMarker !== void 0) attrs.listMarkerHidden = true;
839
+ }
811
840
  const bookmarkNames = pmAttrs.bookmarks?.map((b) => b.name);
812
841
  return {
813
842
  kind: "paragraph",
@@ -892,6 +921,11 @@ function convertTableCell(node, startPos, options, tableCellMargins) {
892
921
  } else if (child.type.name === "table") blocks.push(convertTable(child, offset, options));
893
922
  offset += child.nodeSize;
894
923
  });
924
+ const trailingBlock = blocks.at(-1);
925
+ if (blocks.length > 1 && trailingBlock?.kind === "paragraph" && trailingBlock.runs.every((run) => run.kind === "text" && run.text.length === 0)) trailingBlock.attrs = {
926
+ ...trailingBlock.attrs,
927
+ suppressEmptyParagraphHeight: true
928
+ };
895
929
  const attrs = expectTableCellAttrs(node);
896
930
  const margins = attrs.margins;
897
931
  const resolvePaddingSide = (side, cellTwips, tableTwips) => {
@@ -941,6 +975,7 @@ function convertTableRow(node, startPos, options, tableCellMargins) {
941
975
  if (attrs.height) row.height = twipsToPixels(attrs.height);
942
976
  if (attrs.heightRule) row.heightRule = attrs.heightRule;
943
977
  if (attrs.isHeader) row.isHeader = attrs.isHeader;
978
+ if (attrs.hidden) row.hidden = attrs.hidden;
944
979
  return row;
945
980
  }
946
981
  /**
@@ -991,6 +1026,7 @@ function convertTable(node, startPos, options) {
991
1026
  if (columnWidths) tableBlock.columnWidths = columnWidths;
992
1027
  if (width !== void 0) tableBlock.width = width;
993
1028
  if (widthType !== void 0) tableBlock.widthType = widthType;
1029
+ if (originalFormatting?.layout !== void 0) tableBlock.layout = originalFormatting.layout;
994
1030
  if (justification) tableBlock.justification = justification;
995
1031
  if (indentPx !== void 0) tableBlock.indent = indentPx;
996
1032
  if (floatingPx) tableBlock.floating = floatingPx;
@@ -1004,7 +1040,7 @@ function convertImage(node, startPos, pageContentHeight) {
1004
1040
  const attrs = expectImageAttrs(node);
1005
1041
  const wrapType = attrs.wrapType;
1006
1042
  const shouldAnchor = wrapType === "behind" || wrapType === "inFront";
1007
- const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, pageContentHeight);
1043
+ const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, pageContentHeight);
1008
1044
  const imgBlock = {
1009
1045
  kind: "image",
1010
1046
  id: nextBlockId(),
@@ -1073,6 +1109,7 @@ function convertTextBoxNode(node, startPos, opts) {
1073
1109
  if (attrs.distLeft !== void 0) textBox.distLeft = attrs.distLeft;
1074
1110
  if (attrs.distRight !== void 0) textBox.distRight = attrs.distRight;
1075
1111
  if (attrs.position !== void 0) textBox.position = attrs.position;
1112
+ if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId);
1076
1113
  return textBox;
1077
1114
  }
1078
1115
  /**
@@ -1233,14 +1270,16 @@ function toFlowBlocks(doc, options = {}) {
1233
1270
  trackedPush(convertTextBoxNode(node, pos, opts));
1234
1271
  break;
1235
1272
  case "horizontalRule":
1236
- case "pageBreak":
1237
- trackedPush({
1273
+ case "pageBreak": {
1274
+ const pb = {
1238
1275
  kind: "pageBreak",
1239
1276
  id: nextBlockId(),
1240
1277
  pmStart: pos,
1241
1278
  pmEnd: pos + node.nodeSize
1242
- });
1279
+ };
1280
+ trackedPush(pb);
1243
1281
  break;
1282
+ }
1244
1283
  default: break;
1245
1284
  }
1246
1285
  };
@@ -1,4 +1,4 @@
1
- import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js";
1
+ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { BreakDecision, SectionState, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
@@ -20,9 +20,10 @@ declare function collectSectionConfigs(blocks: FlowBlock[], initialConfig: Secti
20
20
  /**
21
21
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
22
22
  *
23
- * When two consecutive paragraph blocks both have `contextualSpacing: true`
24
- * and share the same `styleId`, the spaceAfter of the first paragraph and
25
- * the spaceBefore of the second paragraph are suppressed (set to 0).
23
+ * Contextual spacing applies independently to each paragraph: suppress the
24
+ * current paragraph's spaceAfter when it opts in, and the next paragraph's
25
+ * spaceBefore when it opts in, provided both paragraphs share a style. Two
26
+ * absent style ids both refer to the document's default paragraph style.
26
27
  *
27
28
  * This mutates the block attrs in-place before layout runs.
28
29
  */
@@ -42,4 +43,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
42
43
  */
43
44
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
44
45
  //#endregion
45
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
46
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned };
@@ -1,5 +1,6 @@
1
+ import { emuToPixels } from "../utils/units.js";
1
2
  import { measuredLineAdvance } from "./lineFlow.js";
2
- import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js";
3
+ import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
3
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
5
  import { createPaginator } from "./paginator.js";
5
6
  import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
@@ -51,33 +52,54 @@ function isEmptyParagraph(block) {
51
52
  if (block.runs.length === 0) return true;
52
53
  if (block.runs.length !== 1) return false;
53
54
  const r = block.runs[0];
54
- return r?.kind === "text" && (r.text ?? "") === "";
55
+ return r?.kind === "text" && r.text === "";
56
+ }
57
+ function isPaginationEmptyParagraph(block) {
58
+ return block.runs.every((run) => {
59
+ if (run.kind === "text") return run.text.trim().length === 0;
60
+ return run.kind === "tab" || run.kind === "lineBreak";
61
+ });
62
+ }
63
+ function pageHasVisibleBodyContent(page, blocksById) {
64
+ for (const fragment of page.fragments) {
65
+ if (fragment.kind !== "paragraph") return true;
66
+ const block = blocksById.get(String(fragment.blockId));
67
+ if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true;
68
+ }
69
+ return false;
55
70
  }
56
71
  /**
57
72
  * Get spacing before a paragraph block. Empty paragraphs whose
58
- * `before` was inherited from a paragraph style (not set inline) collapse
59
- * to zero Word fidelity for incidental empty separators.
73
+ * `before` came only from the implicit default paragraph style collapse to
74
+ * zero. An explicit `w:pStyle` selection is authored paragraph formatting,
75
+ * so Word keeps the selected style's spacing even when the paragraph is empty.
60
76
  */
61
77
  function getSpacingBefore(block) {
62
78
  const value = block.attrs?.spacing?.before ?? 0;
63
- if (isEmptyParagraph(block) && !block.attrs?.spacingExplicit?.before) return 0;
79
+ if (value === 0) return 0;
80
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.before) return 0;
64
81
  return value;
65
82
  }
66
83
  /**
67
- * Get spacing after a paragraph block. Same empty-paragraph collapse rule
68
- * as `getSpacingBefore`.
84
+ * Get spacing after a paragraph block. Same implicit-default-style collapse
85
+ * rule as `getSpacingBefore`.
69
86
  */
70
87
  function getSpacingAfter(block) {
71
88
  const value = block.attrs?.spacing?.after ?? 0;
72
- if (isEmptyParagraph(block) && !block.attrs?.spacingExplicit?.after) return 0;
89
+ if (value === 0) return 0;
90
+ if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.after) return 0;
73
91
  return value;
74
92
  }
93
+ function hasWidowControl(block) {
94
+ return block.attrs?.widowControl !== false;
95
+ }
75
96
  /**
76
97
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
77
98
  *
78
- * When two consecutive paragraph blocks both have `contextualSpacing: true`
79
- * and share the same `styleId`, the spaceAfter of the first paragraph and
80
- * the spaceBefore of the second paragraph are suppressed (set to 0).
99
+ * Contextual spacing applies independently to each paragraph: suppress the
100
+ * current paragraph's spaceAfter when it opts in, and the next paragraph's
101
+ * spaceBefore when it opts in, provided both paragraphs share a style. Two
102
+ * absent style ids both refer to the document's default paragraph style.
81
103
  *
82
104
  * This mutates the block attrs in-place before layout runs.
83
105
  */
@@ -88,16 +110,15 @@ function applyContextualSpacing(blocks) {
88
110
  if (curr.kind !== "paragraph" || next.kind !== "paragraph") continue;
89
111
  const currAttrs = curr.attrs;
90
112
  const nextAttrs = next.attrs;
91
- if (currAttrs?.contextualSpacing && nextAttrs?.contextualSpacing && currAttrs.styleId && currAttrs.styleId === nextAttrs.styleId) {
92
- if (currAttrs.spacing) currAttrs.spacing = {
93
- ...currAttrs.spacing,
94
- after: 0
95
- };
96
- if (nextAttrs.spacing) nextAttrs.spacing = {
97
- ...nextAttrs.spacing,
98
- before: 0
99
- };
100
- }
113
+ if (currAttrs?.styleId !== nextAttrs?.styleId) continue;
114
+ if (currAttrs?.contextualSpacing && currAttrs.spacing) currAttrs.spacing = {
115
+ ...currAttrs.spacing,
116
+ after: 0
117
+ };
118
+ if (nextAttrs?.contextualSpacing && nextAttrs.spacing) nextAttrs.spacing = {
119
+ ...nextAttrs.spacing,
120
+ before: 0
121
+ };
101
122
  }
102
123
  for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyContextualSpacing(cell.blocks);
103
124
  else if (block.kind === "textBox") applyContextualSpacing(block.content);
@@ -142,6 +163,7 @@ function layoutDocument(blocks, measures, options) {
142
163
  const paginator = createPaginator({
143
164
  pageSize: initialConfig.pageSize,
144
165
  margins: initialConfig.margins,
166
+ ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
145
167
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
146
168
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
147
169
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
@@ -150,21 +172,29 @@ function layoutDocument(blocks, measures, options) {
150
172
  applyContextualSpacing(blocks);
151
173
  const keepNextChains = computeKeepNextChains(blocks);
152
174
  const midChainIndices = getMidChainIndices(keepNextChains);
175
+ const blocksById = /* @__PURE__ */ new Map();
176
+ for (const block of blocks) blocksById.set(String(block.id), block);
153
177
  let sectionIdx = 0;
154
178
  let activeSectionMarginTop = initialConfig.margins.top;
155
179
  let activeSectionPageHeight = initialConfig.pageSize.h;
156
180
  let activeSectionMarginBottom = initialConfig.margins.bottom;
181
+ let naturalPageAdvanceSinceRenderedBreak = false;
157
182
  for (let i = 0; i < blocks.length; i++) {
158
183
  const block = blocks[i];
159
184
  const measure = measures[i];
185
+ const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true;
160
186
  if (hasPageBreakBefore(block)) paginator.forcePageBreak();
187
+ else if (hasRenderedPageBreak) {
188
+ const state = paginator.getCurrentState();
189
+ if (!(block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
190
+ }
191
+ if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = false;
161
192
  const chain = keepNextChains.get(i);
162
193
  if (chain && !midChainIndices.has(i)) {
163
194
  const chainHeight = calculateChainHeight(chain, blocks, measures);
164
- const state = paginator.getCurrentState();
165
- const availableHeight = paginator.getAvailableHeight();
166
- if (chainHeight <= state.contentBottom - state.topMargin && chainHeight > availableHeight && state.page.fragments.length > 0) paginator.forcePageBreak();
195
+ paginator.ensureFits(chainHeight);
167
196
  }
197
+ const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
168
198
  switch (block.kind) {
169
199
  case "paragraph":
170
200
  layoutParagraph(block, measure, paginator, paginator.getContentWidth(), options.footnoteHeightById);
@@ -201,6 +231,9 @@ function layoutDocument(blocks, measures, options) {
201
231
  }
202
232
  default: break;
203
233
  }
234
+ const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak";
235
+ if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = false;
236
+ else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = true;
204
237
  }
205
238
  if (paginator.pages.length === 0) paginator.getCurrentState();
206
239
  return {
@@ -309,6 +342,28 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
309
342
  fittingLines++;
310
343
  } else break;
311
344
  }
345
+ let forceBreakAfterFragment = false;
346
+ if (hasWidowControl(block)) {
347
+ const remainingAfter = lines.length - (currentLineIndex + fittingLines);
348
+ if (fittingLines > 1 && remainingAfter === 1) {
349
+ if (currentLineIndex === 0 && fittingLines === 2 && state.cursorY !== state.topMargin) {
350
+ paginator.forceColumnBreak();
351
+ continue;
352
+ }
353
+ fittingLines -= 1;
354
+ forceBreakAfterFragment = true;
355
+ linesHeight = 0;
356
+ linesFnHeight = 0;
357
+ linesFnIds.length = 0;
358
+ for (let j = currentLineIndex; j < currentLineIndex + fittingLines; j++) {
359
+ const line = lines[j];
360
+ linesHeight += measuredLineAdvance(line);
361
+ const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
362
+ linesFnHeight += lineRefs.height;
363
+ for (const id of lineRefs.ids) linesFnIds.push(id);
364
+ }
365
+ }
366
+ }
312
367
  const isFirstFragment = currentLineIndex === 0;
313
368
  const isLastFragment = currentLineIndex + fittingLines >= lines.length;
314
369
  const effectiveSpaceBefore = isFirstFragment ? spaceBefore : 0;
@@ -336,7 +391,8 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
336
391
  fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
337
392
  if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
338
393
  currentLineIndex += fittingLines;
339
- if (currentLineIndex < lines.length) paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
394
+ if (currentLineIndex < lines.length) if (forceBreakAfterFragment) paginator.forceColumnBreak();
395
+ else paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
340
396
  }
341
397
  }
342
398
  /**
@@ -371,7 +427,10 @@ function layoutTable(block, measure, paginator) {
371
427
  let x = paginator.getColumnX(columnIndex);
372
428
  if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
373
429
  else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
374
- else if (block.indent) x += block.indent;
430
+ else if (block.indent !== void 0) {
431
+ const leadingCellMargin = block.rows[0]?.cells[0]?.padding?.left ?? 0;
432
+ x += block.indent - leadingCellMargin;
433
+ }
375
434
  return x;
376
435
  };
377
436
  const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
@@ -505,6 +564,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
505
564
  let baseY = margins.top;
506
565
  if (floating?.horzAnchor === "page") baseX = 0;
507
566
  if (floating?.vertAnchor === "page") baseY = 0;
567
+ else if (floating?.vertAnchor === "text") baseY = state.cursorY;
508
568
  let x = paginator.getColumnX(state.columnIndex);
509
569
  if (floating?.tblpX !== void 0) x = baseX + floating.tblpX;
510
570
  else if (floating?.tblpXSpec) {
@@ -623,6 +683,35 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
623
683
  state.page.fragments.push(fragment);
624
684
  return;
625
685
  }
686
+ if (block.position !== void 0) {
687
+ const state = paginator.getCurrentState();
688
+ const horizontal = block.position.horizontal;
689
+ const x = horizontal ? bandFragmentX(horizontal, {
690
+ pageWidth: state.page.size.w,
691
+ marginLeft: state.page.margins.left,
692
+ marginRight: state.page.margins.right,
693
+ boxWidth: measure.width
694
+ }) : paginator.getColumnX(state.columnIndex);
695
+ const vertical = block.position.vertical;
696
+ const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, {
697
+ pageHeight: sectionPageHeight,
698
+ marginTop: sectionMarginTop,
699
+ marginBottom: sectionMarginBottom,
700
+ boxHeight: measure.height
701
+ }) : state.cursorY + emuToPixels(vertical?.posOffset ?? 0);
702
+ const fragment = {
703
+ kind: "textBox",
704
+ blockId: block.id,
705
+ x,
706
+ y,
707
+ width: measure.width,
708
+ height: measure.height,
709
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
710
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
711
+ };
712
+ state.page.fragments.push(fragment);
713
+ return;
714
+ }
626
715
  const state = paginator.ensureFits(measure.height);
627
716
  const fragment = {
628
717
  kind: "textBox",
@@ -689,4 +778,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
689
778
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
690
779
  }
691
780
  //#endregion
692
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
781
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned };
@@ -2,28 +2,33 @@ import { FlowBlock, Measure } from "./types.js";
2
2
 
3
3
  //#region src/layout-engine/keep-together.d.ts
4
4
  /**
5
- * A chain of consecutive keepNext paragraphs.
5
+ * A chain of keepNext-linked paragraphs, including empty separators Word
6
+ * carries through to the next paragraph with visible content.
6
7
  */
7
8
  type KeepNextChain = {
8
- /** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last paragraph in the chain. */
9
- endIndex: number; /** All paragraph indices in the chain. */
9
+ /** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last keepNext or pass-through empty member. */
10
+ endIndex: number; /** All keepNext and pass-through empty paragraph indices in the chain. */
10
11
  memberIndices: number[]; /** Index of the anchor paragraph (first non-keepNext after chain), or -1 if none. */
11
12
  anchorIndex: number;
12
13
  };
13
14
  /**
14
15
  * Pre-scan blocks to find all keepNext chains.
15
16
  *
16
- * A keepNext chain is a sequence of consecutive paragraphs with keepNext=true,
17
- * followed by an anchor paragraph (the first non-keepNext paragraph).
18
- * The entire chain must stay on the same page as the anchor's first line.
17
+ * A keepNext chain starts with a paragraph whose keepNext=true and continues
18
+ * through further keepNext paragraphs and structural empty separators. The
19
+ * first visible non-keepNext paragraph is its anchor.
19
20
  *
20
21
  * Returns a map from chain start index to chain info.
21
22
  */
22
23
  declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNextChain>;
23
24
  /**
24
- * Calculate the total height needed to keep a chain together.
25
+ * Calculate the height needed to keep consecutive paragraph boundaries from
26
+ * breaking across pages.
25
27
  *
26
- * Includes all chain members plus the first line of the anchor paragraph.
28
+ * Single-line and `keepLines` members are indivisible, so the reservation must
29
+ * continue through them to the anchor's first line. A multi-line member without
30
+ * `keepLines` may itself split; only its first line is needed to satisfy the
31
+ * preceding member's `keepNext`, and the chain can stop there.
27
32
  */
28
33
  declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[]): number;
29
34
  /**
@@ -1,10 +1,16 @@
1
1
  //#region src/layout-engine/keep-together.ts
2
+ function isVisuallyEmptyParagraph(block) {
3
+ if (block.runs.length === 0) return true;
4
+ if (block.runs.length !== 1) return false;
5
+ const run = block.runs.at(0);
6
+ return run?.kind === "text" && run.text === "";
7
+ }
2
8
  /**
3
9
  * Pre-scan blocks to find all keepNext chains.
4
10
  *
5
- * A keepNext chain is a sequence of consecutive paragraphs with keepNext=true,
6
- * followed by an anchor paragraph (the first non-keepNext paragraph).
7
- * The entire chain must stay on the same page as the anchor's first line.
11
+ * A keepNext chain starts with a paragraph whose keepNext=true and continues
12
+ * through further keepNext paragraphs and structural empty separators. The
13
+ * first visible non-keepNext paragraph is its anchor.
8
14
  *
9
15
  * Returns a map from chain start index to chain info.
10
16
  */
@@ -22,7 +28,8 @@ function computeKeepNextChains(blocks) {
22
28
  const nextBlock = blocks[j];
23
29
  if (nextBlock.kind === "sectionBreak" || nextBlock.kind === "pageBreak" || nextBlock.kind === "columnBreak") break;
24
30
  if (nextBlock.kind !== "paragraph") break;
25
- if (nextBlock.attrs?.keepNext) {
31
+ const nextPara = nextBlock;
32
+ if (nextPara.attrs?.keepNext || isVisuallyEmptyParagraph(nextPara)) {
26
33
  memberIndices.push(j);
27
34
  endIndex = j;
28
35
  processed.add(j);
@@ -44,30 +51,38 @@ function computeKeepNextChains(blocks) {
44
51
  return chains;
45
52
  }
46
53
  /**
47
- * Calculate the total height needed to keep a chain together.
54
+ * Calculate the height needed to keep consecutive paragraph boundaries from
55
+ * breaking across pages.
48
56
  *
49
- * Includes all chain members plus the first line of the anchor paragraph.
57
+ * Single-line and `keepLines` members are indivisible, so the reservation must
58
+ * continue through them to the anchor's first line. A multi-line member without
59
+ * `keepLines` may itself split; only its first line is needed to satisfy the
60
+ * preceding member's `keepNext`, and the chain can stop there.
50
61
  */
51
62
  function calculateChainHeight(chain, blocks, measures) {
52
- let totalHeight = 0;
53
- for (const memberIndex of chain.memberIndices) {
54
- const block = blocks[memberIndex];
55
- const measure = measures[memberIndex];
56
- if (!block || !measure || block.kind !== "paragraph" || measure.kind !== "paragraph") continue;
57
- const para = block;
58
- const paraMeasure = measure;
59
- const spacingBefore = para.attrs?.spacing?.before ?? 0;
60
- totalHeight += spacingBefore;
61
- totalHeight += paraMeasure.totalHeight;
62
- const spacingAfter = para.attrs?.spacing?.after ?? 0;
63
- totalHeight += spacingAfter;
64
- }
65
- if (chain.anchorIndex !== -1) {
66
- const anchorMeasure = measures[chain.anchorIndex];
67
- if (anchorMeasure?.kind === "paragraph") {
68
- const anchorPara = anchorMeasure;
69
- if (anchorPara.lines.length > 0) totalHeight += anchorPara.lines[0].lineHeight;
70
- }
63
+ const firstMemberIndex = chain.memberIndices.at(0);
64
+ if (firstMemberIndex === void 0) return 0;
65
+ const firstBlock = blocks[firstMemberIndex];
66
+ const firstMeasure = measures[firstMemberIndex];
67
+ if (firstBlock?.kind !== "paragraph" || firstMeasure?.kind !== "paragraph") return 0;
68
+ let totalHeight = (firstBlock.attrs?.spacing?.before ?? 0) + firstMeasure.totalHeight;
69
+ let trailingSpacing = firstBlock.attrs?.spacing?.after ?? 0;
70
+ const successorIndices = [...chain.memberIndices.slice(1)];
71
+ if (chain.anchorIndex !== -1) successorIndices.push(chain.anchorIndex);
72
+ for (let index = 0; index < successorIndices.length; index++) {
73
+ const successorIndex = successorIndices[index];
74
+ if (successorIndex === void 0) continue;
75
+ const successorBlock = blocks[successorIndex];
76
+ const successorMeasure = measures[successorIndex];
77
+ if (successorBlock?.kind !== "paragraph" || successorMeasure?.kind !== "paragraph") return totalHeight;
78
+ totalHeight += Math.max(trailingSpacing, successorBlock.attrs?.spacing?.before ?? 0);
79
+ const firstLine = successorMeasure.lines.at(0);
80
+ if (!firstLine) return totalHeight;
81
+ const isAnchor = index === successorIndices.length - 1 && chain.anchorIndex !== -1;
82
+ const isSplittable = successorMeasure.lines.length > 1 && !successorBlock.attrs?.keepLines;
83
+ if (isAnchor || isSplittable) return totalHeight + firstLine.lineHeight;
84
+ totalHeight += successorMeasure.totalHeight;
85
+ trailingSpacing = successorBlock.attrs?.spacing?.after ?? 0;
71
86
  }
72
87
  return totalHeight;
73
88
  }