@stll/folio-core 0.4.0 → 0.6.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 (98) 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 +37 -4
  6. package/dist/ai-edits/headless.js +134 -16
  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 -137
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +139 -0
  19. package/dist/document-operations.js +578 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.js +2 -1
  22. package/dist/docx/encryption/compoundFile.js +4 -2
  23. package/dist/docx/footnoteParser.d.ts +1 -17
  24. package/dist/docx/footnoteParser.js +1 -27
  25. package/dist/docx/groupDrawingParser.d.ts +8 -0
  26. package/dist/docx/groupDrawingParser.js +131 -0
  27. package/dist/docx/numberingParser.d.ts +3 -1
  28. package/dist/docx/numberingParser.js +8 -2
  29. package/dist/docx/paragraphParser.js +9 -3
  30. package/dist/docx/runParser.js +8 -1
  31. package/dist/docx/selectiveXmlPatch.js +4 -10
  32. package/dist/docx/serializer/numberingSerializer.js +3 -1
  33. package/dist/docx/settingsParser.d.ts +6 -3
  34. package/dist/docx/settingsParser.js +2 -0
  35. package/dist/docx/xmlParser.js +11 -4
  36. package/dist/i18n/messages/catalogs.gen.d.ts +1054 -34
  37. package/dist/i18n/messages/catalogs.gen.js +1122 -68
  38. package/dist/i18n/messages/messages.gen.d.ts +62 -2
  39. package/dist/index.d.ts +3 -2
  40. package/dist/index.js +6 -5
  41. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  42. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  43. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  45. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  46. package/dist/layout-engine/index.d.ts +6 -5
  47. package/dist/layout-engine/index.js +143 -30
  48. package/dist/layout-engine/keep-together.d.ts +13 -8
  49. package/dist/layout-engine/keep-together.js +40 -25
  50. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  51. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  52. package/dist/layout-engine/measure/measureBlocks.js +50 -18
  53. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  54. package/dist/layout-engine/paginator.d.ts +2 -1
  55. package/dist/layout-engine/paginator.js +9 -3
  56. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  57. package/dist/layout-engine/textBoxGroup.js +8 -0
  58. package/dist/layout-engine/types.d.ts +30 -9
  59. package/dist/layout-engine/types.js +20 -2
  60. package/dist/layout-painter/renderPage.d.ts +15 -1
  61. package/dist/layout-painter/renderPage.js +27 -11
  62. package/dist/layout-painter/renderParagraph.js +18 -8
  63. package/dist/layout-painter/renderTable.js +10 -7
  64. package/dist/managers/DocumentLoaderManager.js +12 -5
  65. package/dist/model.js +1 -1
  66. package/dist/paged-layout/pageText.d.ts +14 -0
  67. package/dist/paged-layout/pageText.js +21 -0
  68. package/dist/prosemirror/attrs/index.js +2 -0
  69. package/dist/prosemirror/commands/comments.js +75 -5
  70. package/dist/prosemirror/commands/hyperlink.js +4 -2
  71. package/dist/prosemirror/commands/image.js +2 -1
  72. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  73. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  74. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  75. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  76. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  77. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  78. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  79. package/dist/prosemirror/findReplaceSelection.js +2 -1
  80. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  81. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  82. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  83. package/dist/redline.d.ts +20 -0
  84. package/dist/redline.js +127 -0
  85. package/dist/server.d.ts +7 -3
  86. package/dist/server.js +6 -2
  87. package/dist/symbols.d.ts +35 -0
  88. package/dist/symbols.js +512 -0
  89. package/dist/utils/clipboard.js +3 -3
  90. package/dist/utils/fontLoader.js +3 -0
  91. package/dist/utils/fontResolver.js +48 -1
  92. package/dist/utils/zoom.d.ts +21 -0
  93. package/dist/utils/zoom.js +21 -0
  94. package/dist/version-comparison.d.ts +92 -0
  95. package/dist/version-comparison.js +436 -0
  96. package/package.json +3 -3
  97. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  98. package/dist/paged-layout/headerFooterMargins.js +0 -122
@@ -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,6 +746,7 @@ 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;
735
752
  if (pmAttrs.widowControl === false) attrs.widowControl = false;
@@ -809,6 +826,17 @@ function convertParagraph(node, startPos, options) {
809
826
  const pmAttrs = expectParagraphAttrs(node);
810
827
  const runs = paragraphToRuns(node, startPos, options);
811
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
+ }
812
840
  const bookmarkNames = pmAttrs.bookmarks?.map((b) => b.name);
813
841
  return {
814
842
  kind: "paragraph",
@@ -893,6 +921,11 @@ function convertTableCell(node, startPos, options, tableCellMargins) {
893
921
  } else if (child.type.name === "table") blocks.push(convertTable(child, offset, options));
894
922
  offset += child.nodeSize;
895
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
+ };
896
929
  const attrs = expectTableCellAttrs(node);
897
930
  const margins = attrs.margins;
898
931
  const resolvePaddingSide = (side, cellTwips, tableTwips) => {
@@ -993,6 +1026,7 @@ function convertTable(node, startPos, options) {
993
1026
  if (columnWidths) tableBlock.columnWidths = columnWidths;
994
1027
  if (width !== void 0) tableBlock.width = width;
995
1028
  if (widthType !== void 0) tableBlock.widthType = widthType;
1029
+ if (originalFormatting?.layout !== void 0) tableBlock.layout = originalFormatting.layout;
996
1030
  if (justification) tableBlock.justification = justification;
997
1031
  if (indentPx !== void 0) tableBlock.indent = indentPx;
998
1032
  if (floatingPx) tableBlock.floating = floatingPx;
@@ -1006,7 +1040,7 @@ function convertImage(node, startPos, pageContentHeight) {
1006
1040
  const attrs = expectImageAttrs(node);
1007
1041
  const wrapType = attrs.wrapType;
1008
1042
  const shouldAnchor = wrapType === "behind" || wrapType === "inFront";
1009
- const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, pageContentHeight);
1043
+ const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, pageContentHeight);
1010
1044
  const imgBlock = {
1011
1045
  kind: "image",
1012
1046
  id: nextBlockId(),
@@ -1075,6 +1109,7 @@ function convertTextBoxNode(node, startPos, opts) {
1075
1109
  if (attrs.distLeft !== void 0) textBox.distLeft = attrs.distLeft;
1076
1110
  if (attrs.distRight !== void 0) textBox.distRight = attrs.distRight;
1077
1111
  if (attrs.position !== void 0) textBox.position = attrs.position;
1112
+ if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId);
1078
1113
  return textBox;
1079
1114
  }
1080
1115
  /**
@@ -1235,14 +1270,16 @@ function toFlowBlocks(doc, options = {}) {
1235
1270
  trackedPush(convertTextBoxNode(node, pos, opts));
1236
1271
  break;
1237
1272
  case "horizontalRule":
1238
- case "pageBreak":
1239
- trackedPush({
1273
+ case "pageBreak": {
1274
+ const pb = {
1240
1275
  kind: "pageBreak",
1241
1276
  id: nextBlockId(),
1242
1277
  pmStart: pos,
1243
1278
  pmEnd: pos + node.nodeSize
1244
- });
1279
+ };
1280
+ trackedPush(pb);
1245
1281
  break;
1282
+ }
1246
1283
  default: break;
1247
1284
  }
1248
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,25 +52,42 @@ 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
  }
75
93
  function hasWidowControl(block) {
@@ -78,9 +96,10 @@ function hasWidowControl(block) {
78
96
  /**
79
97
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
80
98
  *
81
- * When two consecutive paragraph blocks both have `contextualSpacing: true`
82
- * and share the same `styleId`, the spaceAfter of the first paragraph and
83
- * 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.
84
103
  *
85
104
  * This mutates the block attrs in-place before layout runs.
86
105
  */
@@ -91,16 +110,15 @@ function applyContextualSpacing(blocks) {
91
110
  if (curr.kind !== "paragraph" || next.kind !== "paragraph") continue;
92
111
  const currAttrs = curr.attrs;
93
112
  const nextAttrs = next.attrs;
94
- if (currAttrs?.contextualSpacing && nextAttrs?.contextualSpacing && currAttrs.styleId && currAttrs.styleId === nextAttrs.styleId) {
95
- if (currAttrs.spacing) currAttrs.spacing = {
96
- ...currAttrs.spacing,
97
- after: 0
98
- };
99
- if (nextAttrs.spacing) nextAttrs.spacing = {
100
- ...nextAttrs.spacing,
101
- before: 0
102
- };
103
- }
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
+ };
104
122
  }
105
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);
106
124
  else if (block.kind === "textBox") applyContextualSpacing(block.content);
@@ -145,6 +163,7 @@ function layoutDocument(blocks, measures, options) {
145
163
  const paginator = createPaginator({
146
164
  pageSize: initialConfig.pageSize,
147
165
  margins: initialConfig.margins,
166
+ ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
148
167
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
149
168
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
150
169
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
@@ -153,28 +172,36 @@ function layoutDocument(blocks, measures, options) {
153
172
  applyContextualSpacing(blocks);
154
173
  const keepNextChains = computeKeepNextChains(blocks);
155
174
  const midChainIndices = getMidChainIndices(keepNextChains);
175
+ const blocksById = /* @__PURE__ */ new Map();
176
+ for (const block of blocks) blocksById.set(String(block.id), block);
156
177
  let sectionIdx = 0;
157
178
  let activeSectionMarginTop = initialConfig.margins.top;
158
179
  let activeSectionPageHeight = initialConfig.pageSize.h;
159
180
  let activeSectionMarginBottom = initialConfig.margins.bottom;
181
+ let naturalPageAdvanceSinceRenderedBreak = false;
160
182
  for (let i = 0; i < blocks.length; i++) {
161
183
  const block = blocks[i];
162
184
  const measure = measures[i];
185
+ const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true;
163
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;
164
192
  const chain = keepNextChains.get(i);
165
193
  if (chain && !midChainIndices.has(i)) {
166
194
  const chainHeight = calculateChainHeight(chain, blocks, measures);
167
- const state = paginator.getCurrentState();
168
- const availableHeight = paginator.getAvailableHeight();
169
- if (chainHeight <= state.contentBottom - state.topMargin && chainHeight > availableHeight && state.page.fragments.length > 0) paginator.forcePageBreak();
195
+ paginator.ensureFits(chainHeight);
170
196
  }
197
+ const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
171
198
  switch (block.kind) {
172
199
  case "paragraph":
173
200
  layoutParagraph(block, measure, paginator, paginator.getContentWidth(), options.footnoteHeightById);
174
201
  break;
175
202
  case "table":
176
203
  if (block.floating) layoutFloatingTable(block, measure, paginator, paginator.getContentWidth());
177
- else layoutTable(block, measure, paginator);
204
+ else layoutTable(block, measure, paginator, options.footnoteHeightById);
178
205
  break;
179
206
  case "image":
180
207
  layoutImage(block, measure, paginator);
@@ -204,6 +231,9 @@ function layoutDocument(blocks, measures, options) {
204
231
  }
205
232
  default: break;
206
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;
207
237
  }
208
238
  if (paginator.pages.length === 0) paginator.getCurrentState();
209
239
  return {
@@ -386,9 +416,10 @@ function getHeaderRowsHeight(measure, headerRowCount) {
386
416
  /**
387
417
  * Layout a table block onto pages.
388
418
  */
389
- function layoutTable(block, measure, paginator) {
419
+ function layoutTable(block, measure, paginator, footnoteHeightById) {
390
420
  const rows = measure.rows;
391
421
  if (rows.length === 0) return;
422
+ const rowFootnoteIds = block.rows.map((row) => collectTableRowFootnoteIds(row, footnoteHeightById));
392
423
  const headerRowCount = countHeaderRows(block);
393
424
  const headerRowsHeight = getHeaderRowsHeight(measure, headerRowCount);
394
425
  let currentRowIndex = 0;
@@ -397,7 +428,10 @@ function layoutTable(block, measure, paginator) {
397
428
  let x = paginator.getColumnX(columnIndex);
398
429
  if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
399
430
  else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
400
- else if (block.indent) x += block.indent;
431
+ else if (block.indent !== void 0) {
432
+ const leadingCellMargin = block.rows[0]?.cells[0]?.padding?.left ?? 0;
433
+ x += block.indent - leadingCellMargin;
434
+ }
401
435
  return x;
402
436
  };
403
437
  const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
@@ -474,13 +508,42 @@ function layoutTable(block, measure, paginator) {
474
508
  const normalHeaderOverhead = repeatHeaderRowsForNormalFragment ? headerRowsHeight : 0;
475
509
  let rowsHeight = 0;
476
510
  let fittingRows = 0;
511
+ const pageFootnoteIds = new Set(state.page.footnoteIds ?? []);
512
+ const fragmentFootnoteIds = [];
513
+ let fragmentFootnoteHeight = 0;
514
+ let retryOnNextFlowRegion = false;
477
515
  for (let j = currentRowIndex; j < rows.length; j++) {
478
516
  const rowHeight = rows[j].height;
479
- if (rowsHeight + rowHeight + normalHeaderOverhead <= availableHeight || fittingRows === 0) {
517
+ const currentRowFootnoteIds = rowFootnoteIds[j] ?? [];
518
+ const fragmentFootnoteCountBeforeRow = fragmentFootnoteIds.length;
519
+ let rowFootnoteHeight = 0;
520
+ for (const id of currentRowFootnoteIds) {
521
+ if (pageFootnoteIds.has(id) || fragmentFootnoteIds.includes(id)) continue;
522
+ fragmentFootnoteIds.push(id);
523
+ rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
524
+ }
525
+ const separatorHeight = pageFootnoteIds.size === 0 && fragmentFootnoteHeight + rowFootnoteHeight > 0 ? 12 : 0;
526
+ if (rowsHeight + rowHeight + normalHeaderOverhead + fragmentFootnoteHeight + rowFootnoteHeight + separatorHeight <= availableHeight) {
480
527
  rowsHeight += rowHeight;
528
+ fragmentFootnoteHeight += rowFootnoteHeight;
481
529
  fittingRows++;
482
- } else break;
530
+ } else if (fittingRows === 0) {
531
+ if (state.cursorY === state.topMargin && state.page.fragments.length === 0) {
532
+ rowsHeight += rowHeight;
533
+ fragmentFootnoteHeight += rowFootnoteHeight;
534
+ fittingRows++;
535
+ break;
536
+ }
537
+ fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
538
+ paginator.forceColumnBreak();
539
+ retryOnNextFlowRegion = true;
540
+ break;
541
+ } else {
542
+ fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
543
+ break;
544
+ }
483
545
  }
546
+ if (retryOnNextFlowRegion) continue;
484
547
  const fragmentHeight = rowsHeight + normalHeaderOverhead;
485
548
  const isLastFragment = currentRowIndex + fittingRows >= rows.length;
486
549
  const desiredX = computeTableX(state.columnIndex);
@@ -500,6 +563,7 @@ function layoutTable(block, measure, paginator) {
500
563
  ...repeatHeaderRowsForNormalFragment ? { headerRowCount } : {},
501
564
  ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
502
565
  };
566
+ if (fragmentFootnoteHeight > 0) paginator.addFootnoteHeight(fragmentFootnoteHeight, fragmentFootnoteIds);
503
567
  fragment.y = paginator.addFragment(fragment, fragmentHeight, bandSkip, 0).y;
504
568
  fragment.x = desiredX;
505
569
  currentRowIndex += fittingRows;
@@ -516,6 +580,25 @@ function layoutTable(block, measure, paginator) {
516
580
  }
517
581
  }
518
582
  }
583
+ function collectTableRowFootnoteIds(row, footnoteHeightById) {
584
+ if (!row || !footnoteHeightById) return [];
585
+ const ids = [];
586
+ const walk = (blocks) => {
587
+ for (const block of blocks) {
588
+ if (block.kind === "paragraph") {
589
+ for (const run of block.runs) if (run.kind === "text" && run.footnoteRefId !== void 0 && footnoteHeightById.has(run.footnoteRefId) && !ids.includes(run.footnoteRefId)) ids.push(run.footnoteRefId);
590
+ continue;
591
+ }
592
+ if (block.kind === "table") {
593
+ for (const nestedRow of block.rows) for (const cell of nestedRow.cells) walk(cell.blocks);
594
+ continue;
595
+ }
596
+ if (block.kind === "textBox") walk(block.content);
597
+ }
598
+ };
599
+ for (const cell of row.cells) walk(cell.blocks);
600
+ return ids;
601
+ }
519
602
  /**
520
603
  * Layout a floating table (anchored) without advancing the cursor.
521
604
  */
@@ -531,6 +614,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
531
614
  let baseY = margins.top;
532
615
  if (floating?.horzAnchor === "page") baseX = 0;
533
616
  if (floating?.vertAnchor === "page") baseY = 0;
617
+ else if (floating?.vertAnchor === "text") baseY = state.cursorY;
534
618
  let x = paginator.getColumnX(state.columnIndex);
535
619
  if (floating?.tblpX !== void 0) x = baseX + floating.tblpX;
536
620
  else if (floating?.tblpXSpec) {
@@ -649,6 +733,35 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
649
733
  state.page.fragments.push(fragment);
650
734
  return;
651
735
  }
736
+ if (block.position !== void 0) {
737
+ const state = paginator.getCurrentState();
738
+ const horizontal = block.position.horizontal;
739
+ const x = horizontal ? bandFragmentX(horizontal, {
740
+ pageWidth: state.page.size.w,
741
+ marginLeft: state.page.margins.left,
742
+ marginRight: state.page.margins.right,
743
+ boxWidth: measure.width
744
+ }) : paginator.getColumnX(state.columnIndex);
745
+ const vertical = block.position.vertical;
746
+ const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, {
747
+ pageHeight: sectionPageHeight,
748
+ marginTop: sectionMarginTop,
749
+ marginBottom: sectionMarginBottom,
750
+ boxHeight: measure.height
751
+ }) : state.cursorY + emuToPixels(vertical?.posOffset ?? 0);
752
+ const fragment = {
753
+ kind: "textBox",
754
+ blockId: block.id,
755
+ x,
756
+ y,
757
+ width: measure.width,
758
+ height: measure.height,
759
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
760
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
761
+ };
762
+ state.page.fragments.push(fragment);
763
+ return;
764
+ }
652
765
  const state = paginator.ensureFits(measure.height);
653
766
  const fragment = {
654
767
  kind: "textBox",
@@ -715,4 +828,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
715
828
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
716
829
  }
717
830
  //#endregion
718
- 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 };
831
+ 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
  /**