@stll/folio-core 0.6.1 → 0.8.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 (107) hide show
  1. package/dist/ai-edits/blockRange.d.ts +13 -2
  2. package/dist/ai-edits/blockRange.js +23 -2
  3. package/dist/ai-edits/index.d.ts +4 -2
  4. package/dist/ai-edits/index.js +3 -1
  5. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  6. package/dist/ai-edits/scoped-reading.js +60 -0
  7. package/dist/ai-edits/snapshot.js +13 -4
  8. package/dist/ai-edits/types.d.ts +44 -2
  9. package/dist/compat/eigenpal.d.ts +5 -2
  10. package/dist/compat/eigenpal.js +4 -1
  11. package/dist/controller/fontReadiness.d.ts +29 -0
  12. package/dist/controller/fontReadiness.js +139 -0
  13. package/dist/controller/layoutPipeline.js +86 -11
  14. package/dist/docx/blockContentParser.js +51 -8
  15. package/dist/docx/capabilities.d.ts +41 -0
  16. package/dist/docx/capabilities.js +322 -0
  17. package/dist/docx/compatibility.d.ts +34 -2
  18. package/dist/docx/compatibility.js +128 -38
  19. package/dist/docx/conformance.d.ts +6 -0
  20. package/dist/docx/conformance.js +18 -0
  21. package/dist/docx/ensureParaIds.d.ts +29 -0
  22. package/dist/docx/ensureParaIds.js +423 -0
  23. package/dist/docx/groupDrawingParser.d.ts +1 -1
  24. package/dist/docx/groupDrawingParser.js +49 -8
  25. package/dist/docx/numberingParser.js +1 -0
  26. package/dist/docx/paragraphParser.js +9 -0
  27. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  28. package/dist/docx/parser.js +2 -0
  29. package/dist/docx/runParser.js +11 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  31. package/dist/docx/selectiveXmlPatch.js +1 -1
  32. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  33. package/dist/docx/serializer/runSerializer.js +7 -2
  34. package/dist/docx/serializer/tableSerializer.js +4 -0
  35. package/dist/docx/server/boundedArchive.d.ts +24 -0
  36. package/dist/docx/server/boundedArchive.js +106 -0
  37. package/dist/docx/server/extractDocxText.d.ts +23 -0
  38. package/dist/docx/server/extractDocxText.js +154 -0
  39. package/dist/docx/tableParser.js +8 -0
  40. package/dist/docx/textBoxParser.js +6 -1
  41. package/dist/docx/vmlImageParser.js +145 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.js +4 -1
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  45. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  46. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  47. package/dist/layout-bridge/convert/toFlowBlocks.js +106 -13
  48. package/dist/layout-bridge/engine/hitTest.js +2 -1
  49. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  50. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  51. package/dist/layout-engine/headerFooterRefs.js +40 -0
  52. package/dist/layout-engine/index.d.ts +3 -2
  53. package/dist/layout-engine/index.js +125 -72
  54. package/dist/layout-engine/keep-together.d.ts +7 -5
  55. package/dist/layout-engine/keep-together.js +20 -4
  56. package/dist/layout-engine/measure/cache.js +2 -0
  57. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  58. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  59. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  60. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  61. package/dist/layout-engine/measure/measureParagraph.js +42 -15
  62. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  63. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  64. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  65. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  66. package/dist/layout-engine/paginator.d.ts +2 -1
  67. package/dist/layout-engine/paginator.js +17 -8
  68. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  69. package/dist/layout-engine/paragraphFrame.js +17 -0
  70. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  71. package/dist/layout-engine/paragraphSequence.js +25 -0
  72. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  73. package/dist/layout-engine/paragraphSpacing.js +30 -0
  74. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  75. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  76. package/dist/layout-engine/types.d.ts +31 -10
  77. package/dist/layout-engine/types.js +15 -2
  78. package/dist/layout-painter/renderPage.js +2 -2
  79. package/dist/layout-painter/renderParagraph.js +94 -10
  80. package/dist/layout-painter/renderTable.js +42 -12
  81. package/dist/layout-painter/renderTextBox.js +17 -7
  82. package/dist/managers/DocumentLoaderManager.js +1 -1
  83. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  84. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  85. package/dist/paged-layout/sectionGeometry.js +1 -1
  86. package/dist/prosemirror/attrs/index.js +11 -0
  87. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  88. package/dist/prosemirror/conversion/toProseDoc.js +29 -23
  89. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  90. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  91. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  92. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  93. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  94. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  95. package/dist/prosemirror/schema/nodes.d.ts +6 -3
  96. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  97. package/dist/prosemirror/utils/tabCalculator.js +1 -1
  98. package/dist/server.d.ts +6 -2
  99. package/dist/server.js +5 -1
  100. package/dist/types/index.d.ts +2 -1
  101. package/dist/utils/createDocument.js +26 -18
  102. package/dist/utils/formatToStyle.js +3 -3
  103. package/dist/utils/hexId.d.ts +8 -1
  104. package/dist/utils/hexId.js +11 -3
  105. package/dist/utils/units.d.ts +6 -6
  106. package/dist/utils/units.js +8 -8
  107. package/package.json +1 -1
@@ -1,11 +1,15 @@
1
1
  import { emuToPixels } from "../utils/units.js";
2
2
  import { measuredLineAdvance } from "./lineFlow.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
+ import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
4
+ import { getParagraphSpacingAfter, getParagraphSpacingBefore } from "./paragraphSpacing.js";
4
5
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
6
+ import { continuesNumberedSequence } from "./paragraphSequence.js";
5
7
  import { createPaginator } from "./paginator.js";
6
8
  import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
9
+ import { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
7
10
  import { buildTableRowBreakInfo, snapRowBreak } from "./tableRowBreak.js";
8
11
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
12
+ import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
9
13
  import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
10
14
  import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
11
15
  import { panic } from "better-result";
@@ -42,53 +46,55 @@ function collectSectionConfigs(blocks, initialConfig, finalConfig) {
42
46
  breakIndices
43
47
  };
44
48
  }
45
- /**
46
- * Whether a paragraph block has no visible content (no runs, or a single
47
- * empty text run). Word collapses style-inherited spacing on empty
48
- * paragraphs (only direct `<w:pPr><w:spacing>` formatting survives) — see
49
- * eigenpal #402.
50
- */
51
- function isEmptyParagraph(block) {
52
- if (block.runs.length === 0) return true;
53
- if (block.runs.length !== 1) return false;
54
- const r = block.runs[0];
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;
49
+ function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
50
+ if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
51
+ const lineUnits = [];
52
+ let totalHeight = 0;
53
+ let tallestUnit = 0;
54
+ let trailingSpacing = incomingSpacing;
55
+ for (let index = startIndex; index < endIndex; index++) {
56
+ const block = blocks[index];
57
+ const measure = measures[index];
58
+ if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
59
+ if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
60
+ const leadingSpacing = Math.max(getParagraphSpacingBefore(block), trailingSpacing);
61
+ if (measure.lines.length === 0) {
62
+ if (leadingSpacing > 0) {
63
+ lineUnits.push(leadingSpacing);
64
+ totalHeight += leadingSpacing;
65
+ tallestUnit = Math.max(tallestUnit, leadingSpacing);
66
+ }
67
+ }
68
+ for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
69
+ const line = measure.lines[lineIndex];
70
+ if (!line) continue;
71
+ const unitHeight = measuredLineAdvance(line) + (lineIndex === 0 ? leadingSpacing : 0);
72
+ lineUnits.push(unitHeight);
73
+ totalHeight += unitHeight;
74
+ tallestUnit = Math.max(tallestUnit, unitHeight);
75
+ }
76
+ if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
77
+ trailingSpacing = getParagraphSpacingAfter(block);
68
78
  }
69
- return false;
70
- }
71
- /**
72
- * Get spacing before a paragraph block. Empty paragraphs whose
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.
76
- */
77
- function getSpacingBefore(block) {
78
- const value = block.attrs?.spacing?.before ?? 0;
79
- if (value === 0) return 0;
80
- if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.before) return 0;
81
- return value;
82
- }
83
- /**
84
- * Get spacing after a paragraph block. Same implicit-default-style collapse
85
- * rule as `getSpacingBefore`.
86
- */
87
- function getSpacingAfter(block) {
88
- const value = block.attrs?.spacing?.after ?? 0;
89
- if (value === 0) return 0;
90
- if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.after) return 0;
91
- return value;
79
+ if (totalHeight <= 0) return;
80
+ const columnsNeeded = (targetHeight) => {
81
+ let usedHeight = 0;
82
+ let usedColumns = 1;
83
+ for (const unitHeight of lineUnits) if (usedHeight > 0 && usedHeight + unitHeight > targetHeight) {
84
+ usedColumns += 1;
85
+ usedHeight = unitHeight;
86
+ } else usedHeight += unitHeight;
87
+ return usedColumns;
88
+ };
89
+ let lower = Math.max(tallestUnit, totalHeight / columnCount);
90
+ let upper = Math.min(totalHeight, availableHeight);
91
+ if (columnsNeeded(upper) > columnCount) return;
92
+ for (let iteration = 0; iteration < 32; iteration++) {
93
+ const middle = (lower + upper) / 2;
94
+ if (columnsNeeded(middle) <= columnCount) upper = middle;
95
+ else lower = middle;
96
+ }
97
+ return Math.ceil(upper * 1e3) / 1e3;
92
98
  }
93
99
  function hasWidowControl(block) {
94
100
  return block.attrs?.widowControl !== false;
@@ -123,6 +129,24 @@ function applyContextualSpacing(blocks) {
123
129
  for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyContextualSpacing(cell.blocks);
124
130
  else if (block.kind === "textBox") applyContextualSpacing(block.content);
125
131
  }
132
+ /** Suppress automatic inter-item spacing within one numbered sequence. */
133
+ const applyAutomaticListSpacing = (blocks) => {
134
+ for (let index = 1; index < blocks.length; index++) {
135
+ const previous = blocks[index - 1];
136
+ const current = blocks[index];
137
+ if (previous?.kind !== "paragraph" || current?.kind !== "paragraph" || !continuesNumberedSequence(previous, current)) continue;
138
+ if (previous.attrs?.automaticSpacing?.after && previous.attrs.spacing) previous.attrs.spacing = {
139
+ ...previous.attrs.spacing,
140
+ after: 0
141
+ };
142
+ if (current.attrs?.automaticSpacing?.before && current.attrs.spacing) current.attrs.spacing = {
143
+ ...current.attrs.spacing,
144
+ before: 0
145
+ };
146
+ }
147
+ for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyAutomaticListSpacing(cell.blocks);
148
+ else if (block.kind === "textBox") applyAutomaticListSpacing(block.content);
149
+ };
126
150
  /**
127
151
  * Layout a document: convert blocks + measures into pages with positioned fragments.
128
152
  *
@@ -166,11 +190,13 @@ function layoutDocument(blocks, measures, options) {
166
190
  margins: initialConfig.margins,
167
191
  ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
168
192
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
193
+ ...options.sectionEvenPageMargins !== void 0 ? { sectionEvenPageMargins: options.sectionEvenPageMargins } : {},
169
194
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
170
195
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
171
196
  ...options.sectionHeaderFooterRefs !== void 0 ? { sectionHeaderFooterRefs: options.sectionHeaderFooterRefs } : {}
172
197
  });
173
198
  applyContextualSpacing(blocks);
199
+ applyAutomaticListSpacing(blocks);
174
200
  const keepNextChains = computeKeepNextChains(blocks);
175
201
  const midChainIndices = getMidChainIndices(keepNextChains);
176
202
  const blocksById = /* @__PURE__ */ new Map();
@@ -179,21 +205,28 @@ function layoutDocument(blocks, measures, options) {
179
205
  let activeSectionMarginTop = initialConfig.margins.top;
180
206
  let activeSectionPageHeight = initialConfig.pageSize.h;
181
207
  let activeSectionMarginBottom = initialConfig.margins.bottom;
182
- let naturalPageAdvanceSinceRenderedBreak = false;
208
+ let renderedBreakState = INITIAL_RENDERED_BREAK_STATE;
183
209
  for (let i = 0; i < blocks.length; i++) {
184
210
  const block = blocks[i];
185
211
  const measure = measures[i];
186
- const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true;
187
- if (hasPageBreakBefore(block)) paginator.forcePageBreak();
188
- else if (hasRenderedPageBreak) {
189
- const state = paginator.getCurrentState();
190
- if (!(block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
191
- }
192
- if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = false;
212
+ const renderedBreakNeedsSnap = measure.kind === "paragraph" && !paginator.fits(measure.totalHeight);
213
+ const breakDecision = reconcileBreakBeforeBlock({
214
+ state: renderedBreakState,
215
+ block,
216
+ previousBlock: blocks[i - 1],
217
+ page: paginator.getCurrentState().page,
218
+ blocksById,
219
+ hasExplicitPageBreak: hasPageBreakBefore(block),
220
+ renderedBreakNeedsSnap
221
+ });
222
+ if (breakDecision.forcePageBreak) paginator.forcePageBreak();
223
+ renderedBreakState = breakDecision.state;
193
224
  const chain = keepNextChains.get(i);
194
225
  if (chain && !midChainIndices.has(i)) {
195
226
  const chainHeight = calculateChainHeight(chain, blocks, measures);
227
+ const pageBeforeChainLayout = paginator.getCurrentState().page.number;
196
228
  paginator.ensureFits(chainHeight);
229
+ if (paginator.getCurrentState().page.number > pageBeforeChainLayout) renderedBreakState = recordReflowBoundary(renderedBreakState, true);
197
230
  }
198
231
  const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
199
232
  switch (block.kind) {
@@ -224,6 +257,23 @@ function layoutDocument(blocks, measures, options) {
224
257
  case "sectionBreak": {
225
258
  const nextSectionConfig = sectionConfigs[sectionIdx + 1] ?? initialConfig;
226
259
  handleSectionBreak(block, paginator, nextSectionConfig, sectionBreakTypes[sectionIdx + 1] ?? sectionBreakTypes[sectionIdx], sectionIdx + 1);
260
+ const nextColumns = nextSectionConfig.columns;
261
+ const nextBreakIndex = breakIndices[sectionIdx + 1] ?? blocks.length;
262
+ const nextBreak = blocks[nextBreakIndex];
263
+ const sectionEndsContinuously = nextBreakIndex === blocks.length || nextBreak?.kind === "sectionBreak" && nextBreak.type === "continuous";
264
+ if (nextColumns && sectionEndsContinuously) {
265
+ const state = paginator.getCurrentState();
266
+ const balancedHeight = balancedParagraphSectionHeight({
267
+ blocks,
268
+ measures,
269
+ startIndex: i + 1,
270
+ endIndex: nextBreakIndex,
271
+ incomingSpacing: state.trailingSpacing,
272
+ columnCount: nextColumns.count,
273
+ availableHeight: paginator.getAvailableHeight()
274
+ });
275
+ if (balancedHeight !== void 0) state.contentBottom = Math.min(state.contentBottom, state.cursorY + balancedHeight);
276
+ }
227
277
  activeSectionMarginTop = nextSectionConfig.margins.top;
228
278
  activeSectionPageHeight = nextSectionConfig.pageSize.h;
229
279
  activeSectionMarginBottom = nextSectionConfig.margins.bottom;
@@ -232,9 +282,13 @@ function layoutDocument(blocks, measures, options) {
232
282
  }
233
283
  default: break;
234
284
  }
235
- const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak";
236
- if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = false;
237
- else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = true;
285
+ renderedBreakState = reconcileAfterBlock({
286
+ state: renderedBreakState,
287
+ block,
288
+ pageNumberBefore: pageBeforeBlockLayout,
289
+ pageNumberAfter: paginator.getCurrentState().page.number,
290
+ previousPage: paginator.pages[pageBeforeBlockLayout - 1]
291
+ });
238
292
  }
239
293
  if (paginator.pages.length === 0) paginator.getCurrentState();
240
294
  return {
@@ -290,8 +344,8 @@ function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
290
344
  function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeightById) {
291
345
  const lines = measure.lines;
292
346
  if (lines.length === 0) {
293
- const spaceBefore = getSpacingBefore(block);
294
- const spaceAfter = getSpacingAfter(block);
347
+ const spaceBefore = getParagraphSpacingBefore(block);
348
+ const spaceAfter = getParagraphSpacingAfter(block);
295
349
  const state = paginator.getCurrentState();
296
350
  const fragment = {
297
351
  kind: "paragraph",
@@ -309,8 +363,8 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
309
363
  paginator.addFragment(fragment, 0, spaceBefore, spaceAfter);
310
364
  return;
311
365
  }
312
- const spaceBefore = getSpacingBefore(block);
313
- const spaceAfter = getSpacingAfter(block);
366
+ const spaceBefore = getParagraphSpacingBefore(block);
367
+ const spaceAfter = getParagraphSpacingAfter(block);
314
368
  let currentLineIndex = 0;
315
369
  while (currentLineIndex < lines.length) {
316
370
  const state = paginator.getCurrentState();
@@ -429,14 +483,14 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
429
483
  let x = paginator.getColumnX(columnIndex);
430
484
  if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
431
485
  else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
486
+ else if (block.indent !== void 0) x += block.indent;
432
487
  else {
433
488
  const leadingCellMargin = block.rows.at(0)?.cells.at(0)?.padding?.left ?? 0;
434
- x += (block.indent ?? 0) - leadingCellMargin;
489
+ x -= leadingCellMargin;
435
490
  }
436
491
  return x;
437
492
  };
438
493
  const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
439
- const getCurrentAvailableHeight = (state = paginator.getCurrentState()) => state.contentBottom - state.cursorY;
440
494
  const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
441
495
  const previous = state.page.fragments.at(-1);
442
496
  return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY && previous.x === computeTableX(state.columnIndex);
@@ -445,11 +499,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
445
499
  const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
446
500
  const row = rows[rowIndex];
447
501
  if (!row) return false;
448
- const repeatedHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
449
502
  if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
450
- const requiredHeight = row.height + repeatedHeaderOverhead;
451
- if (requiredHeight > getCurrentRowCapacity(state)) return true;
452
- return hasAdjacentPriorTableRows(rowIndex, state) && requiredHeight > getCurrentAvailableHeight(state) - state.trailingSpacing;
503
+ const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
504
+ return row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
453
505
  };
454
506
  while (currentRowIndex < rows.length) {
455
507
  const oversizedRow = rows[currentRowIndex];
@@ -638,9 +690,10 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
638
690
  else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
639
691
  }
640
692
  if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
641
- const pageAnchored = floating?.horzAnchor === "page";
642
- const minX = pageAnchored ? 0 : margins.left;
643
- const maxX = pageAnchored ? page.size.w - tableWidth : margins.left + contentWidth - tableWidth;
693
+ const usesNumericOffset = floating?.tblpX !== void 0;
694
+ const clampToPage = floating?.horzAnchor === "page" || usesNumericOffset;
695
+ const minX = clampToPage ? 0 : margins.left;
696
+ const maxX = clampToPage ? page.size.w - tableWidth : margins.left + contentWidth - tableWidth;
644
697
  if (Number.isFinite(maxX)) x = Math.max(minX, Math.min(x, maxX));
645
698
  const fragment = {
646
699
  kind: "table",
@@ -830,4 +883,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
830
883
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
831
884
  }
832
885
  //#endregion
833
- 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 };
886
+ 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, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, scheduleSectionBreak, tableColumnsArePinned };
@@ -2,8 +2,9 @@ import { FlowBlock, Measure } from "./types.js";
2
2
 
3
3
  //#region src/layout-engine/keep-together.d.ts
4
4
  /**
5
- * A chain of keepNext-linked paragraphs, including empty separators Word
6
- * carries through to the next paragraph with visible content.
5
+ * A chain of paragraphs that Word keeps with following content. This includes
6
+ * explicit keepNext links and a trailing table separator that would otherwise
7
+ * be stranded at the bottom of a page.
7
8
  */
8
9
  type KeepNextChain = {
9
10
  /** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last keepNext or pass-through empty member. */
@@ -14,9 +15,10 @@ type KeepNextChain = {
14
15
  /**
15
16
  * Pre-scan blocks to find all keepNext chains.
16
17
  *
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.
18
+ * A chain starts with a paragraph whose keepNext=true or with an empty
19
+ * separator immediately following a table. It continues through further
20
+ * keepNext paragraphs and structural empty separators. The first visible
21
+ * non-keepNext paragraph is its anchor.
20
22
  *
21
23
  * Returns a map from chain start index to chain info.
22
24
  */
@@ -5,12 +5,23 @@ function isVisuallyEmptyParagraph(block) {
5
5
  const run = block.runs.at(0);
6
6
  return run?.kind === "text" && run.text === "";
7
7
  }
8
+ function startsTrailingTableSeparatorChain(blocks, index) {
9
+ const block = blocks[index];
10
+ if (block?.kind !== "paragraph" || !isVisuallyEmptyParagraph(block) || blocks[index - 1]?.kind !== "table") return false;
11
+ for (let nextIndex = index + 1; nextIndex < blocks.length; nextIndex++) {
12
+ const nextBlock = blocks[nextIndex];
13
+ if (nextBlock?.kind !== "paragraph") return false;
14
+ if (!isVisuallyEmptyParagraph(nextBlock)) return true;
15
+ }
16
+ return false;
17
+ }
8
18
  /**
9
19
  * Pre-scan blocks to find all keepNext chains.
10
20
  *
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.
21
+ * A chain starts with a paragraph whose keepNext=true or with an empty
22
+ * separator immediately following a table. It continues through further
23
+ * keepNext paragraphs and structural empty separators. The first visible
24
+ * non-keepNext paragraph is its anchor.
14
25
  *
15
26
  * Returns a map from chain start index to chain info.
16
27
  */
@@ -21,7 +32,7 @@ function computeKeepNextChains(blocks) {
21
32
  if (processed.has(i)) continue;
22
33
  const block = blocks[i];
23
34
  if (block.kind !== "paragraph") continue;
24
- if (!block.attrs?.keepNext) continue;
35
+ if (!block.attrs?.keepNext && !startsTrailingTableSeparatorChain(blocks, i)) continue;
25
36
  const memberIndices = [i];
26
37
  let endIndex = i;
27
38
  for (let j = i + 1; j < blocks.length; j++) {
@@ -67,6 +78,7 @@ function calculateChainHeight(chain, blocks, measures) {
67
78
  if (firstBlock?.kind !== "paragraph" || firstMeasure?.kind !== "paragraph") return 0;
68
79
  let totalHeight = (firstBlock.attrs?.spacing?.before ?? 0) + firstMeasure.totalHeight;
69
80
  let trailingSpacing = firstBlock.attrs?.spacing?.after ?? 0;
81
+ const startsWithTrailingTableSeparator = blocks[firstMemberIndex - 1]?.kind === "table" && isVisuallyEmptyParagraph(firstBlock) && !firstBlock.attrs?.keepNext;
70
82
  const successorIndices = [...chain.memberIndices.slice(1)];
71
83
  if (chain.anchorIndex !== -1) successorIndices.push(chain.anchorIndex);
72
84
  for (let index = 0; index < successorIndices.length; index++) {
@@ -80,6 +92,10 @@ function calculateChainHeight(chain, blocks, measures) {
80
92
  if (!firstLine) return totalHeight;
81
93
  const isAnchor = index === successorIndices.length - 1 && chain.anchorIndex !== -1;
82
94
  const isSplittable = successorMeasure.lines.length > 1 && !successorBlock.attrs?.keepLines;
95
+ if (isAnchor && startsWithTrailingTableSeparator && successorBlock.attrs?.widowControl !== false && successorMeasure.lines.length > 1) {
96
+ const secondLine = successorMeasure.lines.at(1);
97
+ return totalHeight + firstLine.lineHeight + (secondLine?.lineHeight ?? 0);
98
+ }
83
99
  if (isAnchor || isSplittable) return totalHeight + firstLine.lineHeight;
84
100
  totalHeight += successorMeasure.totalHeight;
85
101
  trailingSpacing = successorBlock.attrs?.spacing?.after ?? 0;
@@ -160,11 +160,13 @@ function hashParagraphBlock(block) {
160
160
  const attrs = block.attrs;
161
161
  if (attrs) {
162
162
  if (attrs.alignment) parts.push(`align:${attrs.alignment}`);
163
+ if (attrs.outlineLevel !== void 0) parts.push(`outline:${attrs.outlineLevel}`);
163
164
  if (attrs.indent) parts.push(`indent:${attrs.indent.left}|${attrs.indent.right}|${attrs.indent.firstLine}|${attrs.indent.hanging}`);
164
165
  if (attrs.spacing) parts.push(`spacing:${attrs.spacing.before}|${attrs.spacing.after}|${attrs.spacing.line}|${attrs.spacing.lineRule}`);
165
166
  if (attrs.defaultFontSize != null) parts.push(`dfs:${attrs.defaultFontSize}`);
166
167
  if (attrs.defaultFontFamily != null) parts.push(`dff:${attrs.defaultFontFamily}`);
167
168
  if (attrs.suppressEmptyParagraphHeight) parts.push("sup");
169
+ if (attrs.reserveEmptyOutlineHeight) parts.push("outline-empty-reserve");
168
170
  const borders = attrs.borders;
169
171
  if (borders) {
170
172
  const signature = (border) => border ? `${border.width ?? ""},${border.style ?? ""},${border.color ?? ""}` : "";
@@ -36,5 +36,7 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
36
36
  * grid is not erased by custom tabs, just augmented).
37
37
  */
38
38
  declare function getListMarkerInlineWidth(block: ParagraphBlock): number;
39
+ /** Paint-only offset that places the marker around its authored list anchor. */
40
+ declare function getListMarkerVisualOffset(block: ParagraphBlock): number;
39
41
  //#endregion
40
- export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, resolveListMarkerFont };
42
+ export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont };
@@ -51,15 +51,16 @@ function getListMarkerInlineWidth(block) {
51
51
  fontSize
52
52
  };
53
53
  const naturalWidth = measureTextWidth(attrs.listMarker, style);
54
+ const markerEndOffset = getMarkerEndOffset(naturalWidth, attrs.listMarkerAlignment);
54
55
  const suffix = attrs.listMarkerSuffix ?? "tab";
55
- if (suffix === "nothing") return naturalWidth;
56
- if (suffix === "space") return naturalWidth + measureTextWidth(" ", style);
56
+ if (suffix === "nothing") return markerEndOffset;
57
+ if (suffix === "space") return markerEndOffset + measureTextWidth(" ", style);
57
58
  const indent = attrs.indent;
58
59
  const indentLeft = indent?.left ?? 0;
59
60
  const firstLine = indent?.firstLine ?? 0;
60
61
  const hanging = indent?.hanging ?? 0;
61
62
  const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
62
- const minBodyStart = markerStartPx + naturalWidth;
63
+ const minBodyStart = markerStartPx + markerEndOffset;
63
64
  const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
64
65
  if (hanging > 0) customTabs.push(indentLeft);
65
66
  const searchStart = hanging > 0 ? Math.max(minBodyStart, indentLeft) : minBodyStart;
@@ -72,5 +73,23 @@ function getListMarkerInlineWidth(block) {
72
73
  if (bodyStart === void 0) return naturalWidth + ptToPx(fontSize) * .5;
73
74
  return bodyStart - markerStartPx;
74
75
  }
76
+ /** Paint-only offset that places the marker around its authored list anchor. */
77
+ function getListMarkerVisualOffset(block) {
78
+ const attrs = block.attrs;
79
+ if (!attrs?.listMarker || attrs.listMarkerHidden) return 0;
80
+ const { fontFamily, fontSize } = resolveListMarkerFont(block);
81
+ const naturalWidth = measureTextWidth(attrs.listMarker, {
82
+ fontFamily,
83
+ fontSize
84
+ });
85
+ if (attrs.listMarkerAlignment === "right") return -naturalWidth;
86
+ if (attrs.listMarkerAlignment === "center") return -naturalWidth / 2;
87
+ return 0;
88
+ }
89
+ const getMarkerEndOffset = (naturalWidth, alignment) => {
90
+ if (alignment === "right") return 0;
91
+ if (alignment === "center") return naturalWidth / 2;
92
+ return naturalWidth;
93
+ };
75
94
  //#endregion
76
- export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, resolveListMarkerFont };
95
+ export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont };
@@ -4,7 +4,10 @@ import { FloatingImageZone } from "./floatingZones.js";
4
4
  declare function measureTableCellBlockVisualHeight(block: FlowBlock, blockMeasure: Measure): number;
5
5
  declare function measureTableBlock(tableBlock: TableBlock, contentWidth: number, fieldValues?: ReadonlyMap<number, string>): TableMeasure;
6
6
  type BandPageGeometry = {
7
+ pageWidth?: number | number[];
7
8
  pageHeight: number | number[];
9
+ marginLeft?: number | number[];
10
+ marginRight?: number | number[];
8
11
  marginBottom: number | number[];
9
12
  };
10
13
  /**