@stll/folio-core 0.48.0 → 0.50.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 (170) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/headless.d.ts +37 -1
  3. package/dist/ai-edits/headless.js +47 -16
  4. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  5. package/dist/ai-edits/minimal-replacement.js +282 -0
  6. package/dist/controller/layoutPipeline.js +11 -1
  7. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  8. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  9. package/dist/display-list/build/unsupported.d.ts +2 -2
  10. package/dist/display-list/build/unsupported.js +0 -0
  11. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  12. package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
  13. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  14. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  15. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  16. package/dist/display-list/selectDisplayPages.js +44 -0
  17. package/dist/docx/blockContentParser.js +2 -2
  18. package/dist/docx/bulletMarkers.d.ts +18 -2
  19. package/dist/docx/bulletMarkers.js +27 -2
  20. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  21. package/dist/docx/drawingGroupChildren.js +176 -0
  22. package/dist/docx/drawingUtils.js +3 -0
  23. package/dist/docx/groupDrawingParser.d.ts +7 -1
  24. package/dist/docx/groupDrawingParser.js +123 -32
  25. package/dist/docx/packageParts.d.ts +45 -1
  26. package/dist/docx/packageParts.js +93 -2
  27. package/dist/docx/paraIdAttribute.d.ts +3 -2
  28. package/dist/docx/paragraphParser.js +91 -7
  29. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  30. package/dist/docx/runParser.js +10 -0
  31. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  32. package/dist/docx/selectiveSaveFlags.js +1 -1
  33. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  34. package/dist/docx/selectiveXmlPatch.js +222 -115
  35. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  36. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  37. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  38. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  39. package/dist/docx/serializer/runSerializer.js +4 -3
  40. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  41. package/dist/docx/shapeAlternateContent.js +52 -0
  42. package/dist/docx/textBoxParser.d.ts +10 -2
  43. package/dist/docx/textBoxParser.js +9 -3
  44. package/dist/export-pdf.d.ts +35 -2
  45. package/dist/export-pdf.js +51 -20
  46. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  47. package/dist/fonts/fontsourceFaces.js +230 -0
  48. package/dist/internal/headlessRevisionResolution.js +50 -13
  49. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  50. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  51. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  52. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  53. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  54. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  55. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  56. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  57. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  58. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  59. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  60. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  61. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  62. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  63. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  64. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  65. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  66. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  67. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  68. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  69. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  70. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  71. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  72. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  73. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  74. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  75. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  76. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  77. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  78. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  79. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  80. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  81. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  82. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  83. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2060
  86. package/dist/layout-bridge/engine/hitTest.js +1 -1
  87. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  88. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  89. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  90. package/dist/layout-engine/imageLayout.d.ts +9 -0
  91. package/dist/layout-engine/imageLayout.js +48 -0
  92. package/dist/layout-engine/index.d.ts +4 -19
  93. package/dist/layout-engine/index.js +14 -910
  94. package/dist/layout-engine/keep-together.d.ts +5 -2
  95. package/dist/layout-engine/keep-together.js +5 -1
  96. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  97. package/dist/layout-engine/layoutFlowShared.js +8 -0
  98. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  99. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  100. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  101. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  102. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  103. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  104. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  105. package/dist/layout-engine/measure/justification.d.ts +32 -0
  106. package/dist/layout-engine/measure/justification.js +83 -0
  107. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  108. package/dist/layout-engine/measure/lineFitting.js +145 -0
  109. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  110. package/dist/layout-engine/measure/lineTypography.js +124 -0
  111. package/dist/layout-engine/measure/measureBlocks.js +7 -4
  112. package/dist/layout-engine/measure/measureContainer.js +46 -3
  113. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  114. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  115. package/dist/layout-engine/measure/measureParagraph.js +41 -780
  116. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  117. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  118. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  119. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  120. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  121. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  122. package/dist/layout-engine/paginator.d.ts +2 -0
  123. package/dist/layout-engine/paginator.js +5 -1
  124. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  125. package/dist/layout-engine/paragraphLayout.js +204 -0
  126. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  127. package/dist/layout-engine/paragraphSpacing.js +2 -1
  128. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  129. package/dist/layout-engine/sectionLayout.js +216 -0
  130. package/dist/layout-engine/tableLayout.d.ts +17 -0
  131. package/dist/layout-engine/tableLayout.js +369 -0
  132. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  133. package/dist/layout-engine/tableRowBreak.js +22 -1
  134. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  135. package/dist/layout-engine/textBoxLayout.js +137 -0
  136. package/dist/layout-engine/types.d.ts +21 -2
  137. package/dist/layout-painter/renderPage.js +1 -0
  138. package/dist/layout-painter/renderParagraph.js +41 -4
  139. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  140. package/dist/layout-painter/renderTextBox.js +26 -2
  141. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  142. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  143. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  144. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  145. package/dist/prosemirror/attrs/index.js +90 -0
  146. package/dist/prosemirror/commands/comments.js +51 -41
  147. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  148. package/dist/prosemirror/contentControlRevisions.js +84 -0
  149. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  150. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  151. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  152. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  153. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  154. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  155. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  156. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  157. package/dist/prosemirror/listMarker.js +2 -2
  158. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  159. package/dist/prosemirror/paragraphDirection.js +21 -2
  160. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  161. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  162. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  163. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  164. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  165. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  166. package/dist/server.d.ts +3 -2
  167. package/dist/types/content.d.ts +2 -2
  168. package/package.json +2 -2
  169. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  170. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -1,20 +1,21 @@
1
- import { emuToPixels } from "../utils/units.js";
2
1
  import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
3
2
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
3
+ import { layoutImage } from "./imageLayout.js";
4
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
5
- import { measuredLineAdvance, measuredLineRangeHeight } from "./lineFlow.js";
6
- import { resolveFloatingTablePageX } from "./measure/floatingTablePosition.js";
7
- import { resolveTableInlineOffset } from "./measure/tableInlinePlacement.js";
5
+ import "./layoutFlowShared.js";
6
+ import { measuredLineAdvance } from "./lineFlow.js";
8
7
  import { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation } from "./noteAreaFlow.js";
9
- import { SECTION_START_PLACEMENT, createPaginator } from "./paginator.js";
10
- import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
11
- import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
8
+ import { createPaginator } from "./paginator.js";
9
+ import { layoutParagraph } from "./paragraphLayout.js";
10
+ import { paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
12
11
  import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
13
12
  import { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
14
- import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, physicalColumnRegionIsShared, scheduleSectionBreak } from "./section-breaks.js";
13
+ import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, scheduleSectionBreak } from "./section-breaks.js";
14
+ import { CONTINUE_PAGE_NUMBERING, DEFAULT_COLUMNS, balancedParagraphSectionHeight, collectSectionConfigs, handleSectionBreak } from "./sectionLayout.js";
15
15
  import { applySectionVerticalAlignment } from "./sectionVerticalAlignment.js";
16
- import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
17
- import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
16
+ import { getHeaderRowsHeight, layoutFloatingTable, layoutTable } from "./tableLayout.js";
17
+ import { tableKeepNextOpeningHeight } from "./tableRowBreak.js";
18
+ import { layoutTextBox } from "./textBoxLayout.js";
18
19
  import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, resolveTableCellPadding, resolveTableWidthPx, stripFlowBlockPmAnchors, tableColumnsArePinned } from "./types.js";
19
20
  import { panic } from "better-result";
20
21
  //#region src/layout-engine/index.ts
@@ -23,92 +24,6 @@ import { panic } from "better-result";
23
24
  *
24
25
  * Converts blocks + measures into positioned fragments on pages.
25
26
  */
26
- const RENDERED_BREAK_REFLOW_TOLERANCE_LINES = 3;
27
- const DEFAULT_COLUMNS = {
28
- count: 1,
29
- gap: 0
30
- };
31
- const CONTINUE_PAGE_NUMBERING = { type: "continue" };
32
- function collectSectionConfigs(blocks, initialConfig, finalConfig) {
33
- const configs = [];
34
- const breakIndices = [];
35
- let previousConfig = initialConfig;
36
- for (let index = 0; index < blocks.length; index += 1) {
37
- const block = blocks[index];
38
- if (block?.kind !== "sectionBreak") continue;
39
- const sectionBreak = block;
40
- const config = {
41
- pageSize: sectionBreak.pageSize ?? previousConfig.pageSize,
42
- margins: sectionBreak.margins ?? previousConfig.margins,
43
- pageNumbering: sectionBreak.pageNumbering ?? CONTINUE_PAGE_NUMBERING
44
- };
45
- if (sectionBreak.columns !== void 0) config.columns = sectionBreak.columns;
46
- configs.push(config);
47
- breakIndices.push(index);
48
- previousConfig = configs.at(-1) ?? previousConfig;
49
- }
50
- configs.push(finalConfig);
51
- return {
52
- configs,
53
- breakIndices
54
- };
55
- }
56
- function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
57
- if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
58
- const lineUnits = [];
59
- let totalHeight = 0;
60
- let tallestUnit = 0;
61
- let trailingSpacing = incomingSpacing;
62
- for (let index = startIndex; index < endIndex; index++) {
63
- const block = blocks[index];
64
- const measure = measures[index];
65
- if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
66
- if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
67
- const leadingSpacing = collapseParagraphSpacing({
68
- before: getParagraphSpacingBefore(block),
69
- after: trailingSpacing
70
- });
71
- if (measure.lines.length === 0) {
72
- if (leadingSpacing > 0) {
73
- lineUnits.push(leadingSpacing);
74
- totalHeight += leadingSpacing;
75
- tallestUnit = Math.max(tallestUnit, leadingSpacing);
76
- }
77
- }
78
- for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
79
- const line = measure.lines[lineIndex];
80
- if (!line) continue;
81
- const unitHeight = measuredLineAdvance(line) + (lineIndex === 0 ? leadingSpacing : 0);
82
- lineUnits.push(unitHeight);
83
- totalHeight += unitHeight;
84
- tallestUnit = Math.max(tallestUnit, unitHeight);
85
- }
86
- if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
87
- trailingSpacing = getParagraphSpacingAfter(block);
88
- }
89
- if (totalHeight <= 0) return;
90
- const columnsNeeded = (targetHeight) => {
91
- let usedHeight = 0;
92
- let usedColumns = 1;
93
- for (const unitHeight of lineUnits) if (usedHeight > 0 && usedHeight + unitHeight > targetHeight) {
94
- usedColumns += 1;
95
- usedHeight = unitHeight;
96
- } else usedHeight += unitHeight;
97
- return usedColumns;
98
- };
99
- let lower = Math.max(tallestUnit, totalHeight / columnCount);
100
- let upper = Math.min(totalHeight, availableHeight);
101
- if (columnsNeeded(upper) > columnCount) return;
102
- for (let iteration = 0; iteration < 32; iteration++) {
103
- const middle = (lower + upper) / 2;
104
- if (columnsNeeded(middle) <= columnCount) upper = middle;
105
- else lower = middle;
106
- }
107
- return Math.ceil(upper * 1e3) / 1e3;
108
- }
109
- function hasWidowControl(block) {
110
- return block.attrs?.widowControl !== false;
111
- }
112
27
  /**
113
28
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
114
29
  *
@@ -194,6 +109,7 @@ function layoutDocumentPass(blocks, measures, options) {
194
109
  ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
195
110
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
196
111
  ...options.sectionEvenPageMargins !== void 0 ? { sectionEvenPageMargins: options.sectionEvenPageMargins } : {},
112
+ ...options.sectionFirstPageMargins !== void 0 ? { sectionFirstPageMargins: options.sectionFirstPageMargins } : {},
197
113
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
198
114
  pageNumbering: initialConfig.pageNumbering,
199
115
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
@@ -216,7 +132,7 @@ function layoutDocumentPass(blocks, measures, options) {
216
132
  if (noteAreaContinuation !== void 0) paginator.setPageTopContinuation(continuesNoteArea(options.noteAreas, block) ? noteAreaContinuation : void 0);
217
133
  const firstLine = measure.kind === "paragraph" ? measure.lines.at(0) : void 0;
218
134
  const firstLineAdvance = firstLine ? measuredLineAdvance(firstLine) : 0;
219
- const renderedBreakNeedsSnap = measure.kind === "paragraph" && (!paginator.fits(measure.totalHeight) || firstLineAdvance > 0 && paginator.getAvailableHeight() <= firstLineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES);
135
+ const renderedBreakNeedsSnap = measure.kind === "paragraph" && (!paginator.fits(measure.totalHeight) || firstLineAdvance > 0 && paginator.getAvailableHeight() <= firstLineAdvance * 3);
220
136
  const hasExplicitPageBreak = block.kind === "pageBreak" || hasPageBreakBefore(block);
221
137
  const breakDecision = reconcileBreakBeforeBlock({
222
138
  state: renderedBreakState,
@@ -234,7 +150,7 @@ function layoutDocumentPass(blocks, measures, options) {
234
150
  renderedBreakState = breakDecision.state;
235
151
  const chain = keepNextChains.get(i);
236
152
  if (chain && !midChainIndices.has(i)) {
237
- const chainHeight = calculateChainHeight(chain, blocks, measures, paginator.getCurrentState().trailingSpacing);
153
+ const chainHeight = calculateChainHeight(chain, blocks, measures, paginator.getCurrentState().trailingSpacing, tableKeepNextOpeningHeight);
238
154
  const pageBeforeChainLayout = paginator.getCurrentState().page.number;
239
155
  paginator.ensureFits(chainHeight);
240
156
  if (paginator.getCurrentState().page.number > pageBeforeChainLayout) renderedBreakState = recordReflowBoundary(renderedBreakState, true);
@@ -318,817 +234,5 @@ function layoutDocumentPass(blocks, measures, options) {
318
234
  ...options.pageGap !== void 0 ? { pageGap: options.pageGap } : {}
319
235
  };
320
236
  }
321
- /**
322
- * Footnote refs whose run sits in line `[fromRun..toRun]`, with their
323
- * pre-measured content heights. Empty when the engine isn't tracking
324
- * dynamic fn demand. Used by `layoutParagraph` to (a) reserve fn
325
- * height per line and (b) record the IDs on the host page so the
326
- * painter renders the fn on the page where the ref-bearing line
327
- * actually landed — even when the paragraph splits across pages
328
- * (fragment pmStart/pmEnd is paragraph-wide and cannot disambiguate
329
- * between split halves).
330
- */
331
- function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
332
- if (!fnHeights) return {
333
- ids: [],
334
- height: 0
335
- };
336
- const ids = [];
337
- let height = 0;
338
- for (let r = fromRun; r <= toRun; r++) {
339
- const run = block.runs[r];
340
- if (!run || run.kind !== "text") continue;
341
- const id = run.footnoteRefId;
342
- if (id === void 0) continue;
343
- const h = fnHeights.get(id);
344
- if (h !== void 0) {
345
- ids.push(id);
346
- height += h;
347
- }
348
- }
349
- return {
350
- ids,
351
- height
352
- };
353
- }
354
- function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
355
- const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
356
- return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
357
- }
358
- function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
359
- const lines = measure.lines;
360
- if (lines.length === 0) {
361
- const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
362
- const spaceAfter = getParagraphSpacingAfter(block);
363
- const state = paginator.getCurrentState();
364
- const fragment = {
365
- kind: "paragraph",
366
- ...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
367
- blockId: block.id,
368
- x: paginator.getColumnX(state.columnIndex),
369
- y: state.cursorY + spaceBefore,
370
- width: contentWidth,
371
- height: 0,
372
- fromLine: 0,
373
- toLine: 0,
374
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
375
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
376
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
377
- };
378
- paginator.addFragment(fragment, 0, spaceBefore, spaceAfter);
379
- return;
380
- }
381
- const spaceBefore = suppressSpaceBefore ? 0 : getParagraphSpacingBefore(block);
382
- const spaceAfter = getParagraphSpacingAfter(block);
383
- let keepLinesDemand;
384
- if (lines.length > 1 && hasKeepLines(block)) {
385
- let footnoteHeight = 0;
386
- for (const line of lines) footnoteHeight += getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById).height;
387
- keepLinesDemand = {
388
- linesHeight: measuredLineRangeHeight(lines, 0, lines.length),
389
- footnoteHeight
390
- };
391
- }
392
- let keepLinesMoveAttempted = false;
393
- let currentLineIndex = 0;
394
- while (currentLineIndex < lines.length) {
395
- const state = paginator.getCurrentState();
396
- const availableHeight = paginator.getAvailableHeight();
397
- const currentLine = lines.at(currentLineIndex);
398
- const markerAlreadySatisfied = state.columnIndex === 0 && state.cursorY === state.topMargin && state.page.fragments.length === 0;
399
- if (currentLine?.renderedPageBreakBefore === true && !markerAlreadySatisfied && availableHeight <= measuredLineAdvance(currentLine) * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
400
- paginator.forcePageBreak();
401
- continue;
402
- }
403
- if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
404
- const firstLine = lines[0];
405
- const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
406
- const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
407
- const collapsedLead = collapseParagraphSpacing({
408
- before: spaceBefore,
409
- after: state.trailingSpacing
410
- });
411
- const columnCapacity = state.contentBottom - state.topMargin;
412
- if (collapsedLead + firstLineHeight > availableHeight && spaceBefore + firstLineHeight <= columnCapacity) {
413
- paginator.ensureFits(collapsedLead + firstLineHeight);
414
- continue;
415
- }
416
- }
417
- if (keepLinesDemand !== void 0 && !keepLinesMoveAttempted && currentLineIndex === 0 && state.cursorY !== state.topMargin) {
418
- const wholeHeight = keepLinesDemand.linesHeight + projectedFootnoteReserveGrowth(state, keepLinesDemand.footnoteHeight);
419
- const collapsedLead = collapseParagraphSpacing({
420
- before: spaceBefore,
421
- after: state.trailingSpacing
422
- });
423
- const columnCapacity = state.contentBottom - state.topMargin;
424
- if (collapsedLead + wholeHeight > availableHeight && spaceBefore + wholeHeight <= columnCapacity) {
425
- keepLinesMoveAttempted = true;
426
- paginator.ensureFits(collapsedLead + wholeHeight);
427
- continue;
428
- }
429
- }
430
- let linesHeight = 0;
431
- let linesFnHeight = 0;
432
- const linesFnIds = [];
433
- let fittingLines = 0;
434
- let forcePageBreakAfterFragment = false;
435
- const firstFragmentSpaceBefore = currentLineIndex === 0 ? collapseParagraphSpacing({
436
- before: spaceBefore,
437
- after: state.trailingSpacing
438
- }) : 0;
439
- for (let j = currentLineIndex; j < lines.length; j++) {
440
- const line = lines[j];
441
- const lineAdvance = measuredLineAdvance(line);
442
- if (j > currentLineIndex && line.renderedPageBreakBefore === true && availableHeight - (linesHeight + firstFragmentSpaceBefore + linesFnHeight) <= lineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
443
- forcePageBreakAfterFragment = true;
444
- break;
445
- }
446
- const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
447
- const totalWithLine = linesHeight + lineAdvance;
448
- const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
449
- if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
450
- linesHeight = totalWithLine;
451
- linesFnHeight += lineRefs.height;
452
- for (const id of lineRefs.ids) linesFnIds.push(id);
453
- fittingLines++;
454
- } else break;
455
- }
456
- let forceBreakAfterFragment = false;
457
- if (hasWidowControl(block)) {
458
- const remainingAfter = lines.length - (currentLineIndex + fittingLines);
459
- if (fittingLines > 1 && remainingAfter === 1) {
460
- if (currentLineIndex === 0 && fittingLines === 2 && state.cursorY !== state.topMargin) {
461
- paginator.forceColumnBreak();
462
- continue;
463
- }
464
- fittingLines -= 1;
465
- forceBreakAfterFragment = true;
466
- linesHeight = 0;
467
- linesFnHeight = 0;
468
- linesFnIds.length = 0;
469
- for (let j = currentLineIndex; j < currentLineIndex + fittingLines; j++) {
470
- const line = lines[j];
471
- linesHeight += measuredLineAdvance(line);
472
- const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
473
- linesFnHeight += lineRefs.height;
474
- for (const id of lineRefs.ids) linesFnIds.push(id);
475
- }
476
- }
477
- }
478
- const isFirstFragment = currentLineIndex === 0;
479
- const isLastFragment = currentLineIndex + fittingLines >= lines.length;
480
- const effectiveSpaceBefore = isFirstFragment ? spaceBefore : 0;
481
- const effectiveSpaceAfter = isLastFragment ? spaceAfter : 0;
482
- const pmRange = getParagraphFragmentPmRange(block, measure, currentLineIndex, currentLineIndex + fittingLines);
483
- const fragment = {
484
- kind: "paragraph",
485
- ...block.attrs?.suppressEmptyParagraphHeight === true ? { paginationRole: "empty-carrier" } : {},
486
- blockId: block.id,
487
- x: paginator.getColumnX(state.columnIndex),
488
- y: 0,
489
- width: contentWidth,
490
- height: linesHeight,
491
- fromLine: currentLineIndex,
492
- toLine: currentLineIndex + fittingLines,
493
- ...pmRange.pmStart !== void 0 ? { pmStart: pmRange.pmStart } : {},
494
- ...pmRange.pmEnd !== void 0 ? { pmEnd: pmRange.pmEnd } : {},
495
- ...!isFirstFragment ? { continuesFromPrev: true } : {},
496
- ...!isLastFragment ? { continuesOnNext: true } : {},
497
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
498
- };
499
- if (linesFnHeight > 0) {
500
- const stateBefore = paginator.getCurrentState();
501
- paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
502
- const stateAfter = paginator.getCurrentState();
503
- if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
504
- }
505
- fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
506
- if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
507
- currentLineIndex += fittingLines;
508
- if (currentLineIndex < lines.length) if (forcePageBreakAfterFragment) paginator.forcePageBreak();
509
- else if (forceBreakAfterFragment) paginator.forceColumnBreak();
510
- else paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
511
- }
512
- }
513
- /**
514
- * Count consecutive header rows at the start of a table.
515
- * Header rows are marked with isHeader: true in the block data.
516
- */
517
- function countHeaderRows(block) {
518
- let count = 0;
519
- for (const row of block.rows) if (row.isHeader) count++;
520
- else break;
521
- return count;
522
- }
523
- /**
524
- * Calculate total height of header rows from their measures.
525
- */
526
- function getHeaderRowsHeight(measure, headerRowCount) {
527
- let height = 0;
528
- for (let i = 0; i < headerRowCount && i < measure.rows.length; i++) height += measure.rows[i].height;
529
- return height;
530
- }
531
- const tableRowStartsWithRenderedPageBreak = (block, rowIndex) => {
532
- const row = block.rows[rowIndex];
533
- const visibleCells = row?.cells.filter((cell) => cell.blocks.some((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock)));
534
- if (!visibleCells || visibleCells.length === 0) return false;
535
- const startsWithRenderedPageBreak = (cell) => {
536
- const firstVisibleBlock = cell.blocks.find((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock));
537
- return firstVisibleBlock?.kind === "paragraph" && firstVisibleBlock.attrs?.renderedPageBreakBefore === true;
538
- };
539
- if (row?.heightRule === "exact") return startsWithRenderedPageBreak(visibleCells[0]);
540
- return visibleCells.every(startsWithRenderedPageBreak);
541
- };
542
- const getVerticallyMergedRows = (block) => {
543
- const mergedRows = /* @__PURE__ */ new Set();
544
- for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
545
- const row = block.rows[rowIndex];
546
- if (!row) continue;
547
- for (const cell of row.cells) {
548
- const rowSpan = cell.rowSpan ?? 1;
549
- if (rowSpan <= 1) continue;
550
- for (let mergedRowIndex = rowIndex; mergedRowIndex < Math.min(block.rows.length, rowIndex + rowSpan); mergedRowIndex += 1) mergedRows.add(mergedRowIndex);
551
- }
552
- }
553
- return mergedRows;
554
- };
555
- const flowBlockHasTrackedChanges = (block) => {
556
- if (block.kind === "paragraph") return block.runs.some((run) => {
557
- if (run.kind === "lineBreak") return false;
558
- return run.isInsertion || run.isDeletion;
559
- });
560
- if (block.kind === "table") return block.rows.some((row) => row.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)));
561
- if (block.kind === "textBox") return block.content.some(flowBlockHasTrackedChanges);
562
- return false;
563
- };
564
- const tableRowHasTrackedChanges = (block, rowIndex) => block.rows[rowIndex]?.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)) ?? false;
565
- /**
566
- * Layout a table block onto pages.
567
- */
568
- function layoutTable(block, measure, paginator, footnoteHeightById) {
569
- const rows = measure.rows;
570
- if (rows.length === 0) return;
571
- const rowFootnoteIds = block.rows.map((row) => collectTableRowFootnoteIds(row, footnoteHeightById));
572
- const headerRowCount = countHeaderRows(block);
573
- const headerRowsHeight = getHeaderRowsHeight(measure, headerRowCount);
574
- let currentRowIndex = 0;
575
- const breakInfo = buildTableRowBreakInfo(block, measure);
576
- const verticallyMergedRows = getVerticallyMergedRows(block);
577
- const computeTableX = ({ columnIndex, rowIndex }) => {
578
- return paginator.getColumnX(columnIndex) + resolveTableInlineOffset({
579
- table: block,
580
- rowJustification: block.rows[rowIndex]?.justification,
581
- frameWidth: paginator.columnWidth,
582
- tableWidth: measure.totalWidth
583
- });
584
- };
585
- const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
586
- const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
587
- const previous = state.page.fragments.at(-1);
588
- return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY;
589
- };
590
- const shouldRepeatHeaderRows = (rowIndex, consumed, state = paginator.getCurrentState()) => headerRowCount > 0 && rowIndex >= headerRowCount && !(consumed === 0 && hasAdjacentPriorTableRows(rowIndex, state));
591
- const rowOpensFlowRegion = (rowIndex, state) => {
592
- if (state.cursorY === state.topMargin) return true;
593
- const previous = state.page.fragments.at(-1);
594
- return rowIndex > 0 && rowIndex <= headerRowCount && previous?.kind === "table" && previous.blockId === block.id && previous.fromRow === 0 && previous.toRow === rowIndex && previous.y === state.topMargin && previous.y + previous.height === state.cursorY;
595
- };
596
- const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
597
- const row = rows[rowIndex];
598
- const sourceRow = block.rows[rowIndex];
599
- if (!row || !sourceRow || sourceRow.isHeader || verticallyMergedRows.has(rowIndex)) return false;
600
- if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
601
- const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
602
- const oversized = row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
603
- if (sourceRow.cantSplit) return oversized && sourceRow.heightRule !== "exact" && rowOpensFlowRegion(rowIndex, state);
604
- if (!oversized && (state.footnoteHeight > 0 || (rowFootnoteIds[rowIndex]?.length ?? 0) > 0)) return false;
605
- const currentHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
606
- const currentAvailableHeight = paginator.getAvailableHeight() - currentHeaderOverhead - state.trailingSpacing;
607
- return oversized || row.height > currentAvailableHeight;
608
- };
609
- while (currentRowIndex < rows.length) {
610
- const rowState = paginator.getCurrentState();
611
- const rowHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, rowState) ? headerRowsHeight : 0;
612
- const rowAvailableHeight = paginator.getAvailableHeight() - rowHeaderOverhead - rowState.trailingSpacing;
613
- const rowStartsFreshPage = rowState.cursorY === rowState.topMargin && rowState.page.fragments.length === 0;
614
- if (block.rows[currentRowIndex]?.breakBefore === "page" && !rowStartsFreshPage) {
615
- paginator.forcePageBreak();
616
- continue;
617
- }
618
- if (!rowStartsFreshPage && tableRowStartsWithRenderedPageBreak(block, currentRowIndex) && !tableRowHasTrackedChanges(block, currentRowIndex) && rows[currentRowIndex].height > rowAvailableHeight) {
619
- paginator.forcePageBreak();
620
- continue;
621
- }
622
- const splittableRow = rows[currentRowIndex];
623
- if (canSplitRow(currentRowIndex)) {
624
- let consumed = 0;
625
- const renderedBreakHints = tableRowHasTrackedChanges(block, currentRowIndex) ? [] : breakInfo.renderedBreakHints[currentRowIndex] ?? [];
626
- let renderedBreakHintIndex = 0;
627
- const discardPassedRenderedBreakHints = () => {
628
- while ((renderedBreakHints[renderedBreakHintIndex]?.offset ?? Infinity) <= consumed) renderedBreakHintIndex += 1;
629
- };
630
- const advanceAfterNaturalFlowBreak = (previousPageNumber) => {
631
- discardPassedRenderedBreakHints();
632
- if (paginator.getCurrentState().page.number > previousPageNumber && renderedBreakHintIndex < renderedBreakHints.length) renderedBreakHintIndex += 1;
633
- };
634
- while (consumed < splittableRow.height) {
635
- const sliceState = paginator.getCurrentState();
636
- const repeatHeaderRows = shouldRepeatHeaderRows(currentRowIndex, consumed, sliceState);
637
- const headerOverhead = repeatHeaderRows ? headerRowsHeight : 0;
638
- const sliceAvail = paginator.getAvailableHeight() - headerOverhead - (consumed === 0 ? sliceState.trailingSpacing : 0);
639
- let slice = snapRowBreak(breakInfo, currentRowIndex, consumed, sliceAvail);
640
- let forceRenderedPageBreak = false;
641
- if (slice <= 0) {
642
- if (!(sliceState.cursorY === sliceState.topMargin && sliceState.page.fragments.length === 0)) {
643
- const previousPageNumber = sliceState.page.number;
644
- paginator.forceColumnBreak();
645
- advanceAfterNaturalFlowBreak(previousPageNumber);
646
- continue;
647
- }
648
- const from = consumed;
649
- slice = (breakInfo.breakOffsets[currentRowIndex]?.find((o) => o > from) ?? splittableRow.height) - consumed;
650
- }
651
- discardPassedRenderedBreakHints();
652
- const renderedBreakHint = renderedBreakHints[renderedBreakHintIndex];
653
- if (renderedBreakHint && renderedBreakHint.offset <= consumed + slice && sliceAvail - (renderedBreakHint.offset - consumed) <= renderedBreakHint.lineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
654
- slice = renderedBreakHint.offset - consumed;
655
- renderedBreakHintIndex += 1;
656
- forceRenderedPageBreak = true;
657
- }
658
- const sliceBottom = consumed + slice;
659
- const continuationSkip = sliceBottom < splittableRow.height ? getRowContinuationSkip(breakInfo, currentRowIndex, sliceBottom) : 0;
660
- const nextConsumed = Math.min(splittableRow.height, sliceBottom + continuationSkip);
661
- const moreAfter = !(nextConsumed >= splittableRow.height) || currentRowIndex + 1 < rows.length;
662
- const fragmentHeight = headerOverhead + slice;
663
- const sliceFragment = {
664
- kind: "table",
665
- blockId: block.id,
666
- x: computeTableX({
667
- columnIndex: sliceState.columnIndex,
668
- rowIndex: currentRowIndex
669
- }),
670
- y: 0,
671
- width: measure.totalWidth,
672
- height: fragmentHeight,
673
- fromRow: currentRowIndex,
674
- toRow: currentRowIndex + 1,
675
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
676
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
677
- ...consumed > 0 || currentRowIndex > 0 ? { continuesFromPrev: true } : {},
678
- ...moreAfter ? { continuesOnNext: true } : {},
679
- ...repeatHeaderRows ? { headerRowCount } : {},
680
- ...consumed > 0 ? { topClip: consumed } : {},
681
- ...sliceBottom >= splittableRow.height ? {} : { bottomClip: sliceBottom },
682
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
683
- };
684
- const sliceResult = paginator.addFragment(sliceFragment, fragmentHeight, 0, 0);
685
- sliceFragment.y = sliceResult.y;
686
- sliceFragment.x = computeTableX({
687
- columnIndex: sliceResult.state.columnIndex,
688
- rowIndex: currentRowIndex
689
- });
690
- consumed = nextConsumed;
691
- if (consumed < splittableRow.height) if (forceRenderedPageBreak) paginator.forcePageBreak();
692
- else {
693
- const previousPageNumber = paginator.getCurrentState().page.number;
694
- paginator.forceColumnBreak();
695
- advanceAfterNaturalFlowBreak(previousPageNumber);
696
- }
697
- }
698
- currentRowIndex += 1;
699
- continue;
700
- }
701
- const state = paginator.getCurrentState();
702
- const rawAvailableHeight = paginator.getAvailableHeight();
703
- const isFirstFragment = currentRowIndex === 0;
704
- const bandSkip = isFirstFragment ? measure.bandSkipBefore ?? 0 : 0;
705
- const availableHeight = rawAvailableHeight - (isFirstFragment ? Math.max(bandSkip, state.trailingSpacing) : 0);
706
- const repeatHeaderRowsForNormalFragment = shouldRepeatHeaderRows(currentRowIndex, 0, state);
707
- const normalHeaderOverhead = repeatHeaderRowsForNormalFragment ? headerRowsHeight : 0;
708
- let rowsHeight = 0;
709
- let fittingRows = 0;
710
- const pageFootnoteIds = new Set(state.page.footnoteIds ?? []);
711
- const fragmentFootnoteIds = [];
712
- let fragmentFootnoteHeight = 0;
713
- let retryOnNextFlowRegion = false;
714
- const fragmentX = computeTableX({
715
- columnIndex: state.columnIndex,
716
- rowIndex: currentRowIndex
717
- });
718
- for (let j = currentRowIndex; j < rows.length; j++) {
719
- if (j > currentRowIndex && block.rows[j]?.breakBefore === "page") break;
720
- if (j > currentRowIndex && computeTableX({
721
- columnIndex: state.columnIndex,
722
- rowIndex: j
723
- }) !== fragmentX) break;
724
- const rowHeight = rows[j].height;
725
- const currentRowFootnoteIds = rowFootnoteIds[j] ?? [];
726
- const fragmentFootnoteCountBeforeRow = fragmentFootnoteIds.length;
727
- let rowFootnoteHeight = 0;
728
- for (const id of currentRowFootnoteIds) {
729
- if (pageFootnoteIds.has(id) || fragmentFootnoteIds.includes(id)) continue;
730
- fragmentFootnoteIds.push(id);
731
- rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
732
- }
733
- const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
734
- const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
735
- if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
736
- rowsHeight += rowHeight;
737
- fragmentFootnoteHeight += rowFootnoteHeight;
738
- fittingRows++;
739
- } else if (fittingRows === 0) {
740
- if (state.cursorY === state.topMargin && state.page.fragments.length === 0) {
741
- rowsHeight += rowHeight;
742
- fragmentFootnoteHeight += rowFootnoteHeight;
743
- fittingRows++;
744
- break;
745
- }
746
- fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
747
- paginator.forceColumnBreak();
748
- retryOnNextFlowRegion = true;
749
- break;
750
- } else {
751
- fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
752
- break;
753
- }
754
- }
755
- if (retryOnNextFlowRegion) continue;
756
- const fragmentHeight = rowsHeight + normalHeaderOverhead;
757
- const isLastFragment = currentRowIndex + fittingRows >= rows.length;
758
- const desiredX = fragmentX;
759
- const fragment = {
760
- kind: "table",
761
- blockId: block.id,
762
- x: desiredX,
763
- y: 0,
764
- width: measure.totalWidth,
765
- height: fragmentHeight,
766
- fromRow: currentRowIndex,
767
- toRow: currentRowIndex + fittingRows,
768
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
769
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
770
- ...!isFirstFragment ? { continuesFromPrev: true } : {},
771
- ...!isLastFragment ? { continuesOnNext: true } : {},
772
- ...repeatHeaderRowsForNormalFragment ? { headerRowCount } : {},
773
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
774
- };
775
- if (fragmentFootnoteHeight > 0) paginator.addFootnoteHeight(fragmentFootnoteHeight, fragmentFootnoteIds);
776
- fragment.y = paginator.addFragment(fragment, fragmentHeight, bandSkip, 0).y;
777
- fragment.x = desiredX;
778
- currentRowIndex += fittingRows;
779
- if (currentRowIndex < rows.length) {
780
- if (canSplitRow(currentRowIndex)) {
781
- const nextState = paginator.getCurrentState();
782
- const nextHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0;
783
- const nextSliceAvail = paginator.getAvailableHeight() - nextHeaderOverhead - nextState.trailingSpacing;
784
- if (snapRowBreak(breakInfo, currentRowIndex, 0, nextSliceAvail) > 0) continue;
785
- }
786
- const nextState = paginator.getCurrentState();
787
- const nextRowHeight = rows[currentRowIndex].height + (shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0);
788
- paginator.ensureFits(nextRowHeight);
789
- }
790
- }
791
- }
792
- function collectTableRowFootnoteIds(row, footnoteHeightById) {
793
- if (!row || !footnoteHeightById || row.hidden) return [];
794
- const ids = [];
795
- const walk = (blocks) => {
796
- for (const block of blocks) {
797
- if (block.kind === "paragraph") {
798
- 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);
799
- continue;
800
- }
801
- if (block.kind === "table") {
802
- for (const nestedRow of block.rows) {
803
- if (nestedRow.hidden) continue;
804
- for (const cell of nestedRow.cells) walk(cell.blocks);
805
- }
806
- continue;
807
- }
808
- if (block.kind === "textBox") walk(block.content);
809
- }
810
- };
811
- for (const cell of row.cells) walk(cell.blocks);
812
- return ids;
813
- }
814
- /**
815
- * Layout a floating table (anchored) without advancing the cursor.
816
- */
817
- function layoutFloatingTable(block, measure, paginator, contentWidth) {
818
- const state = paginator.getCurrentState();
819
- const floating = block.floating;
820
- const page = state.page;
821
- const margins = page.margins;
822
- const tableWidth = measure.totalWidth;
823
- const tableHeight = measure.totalHeight;
824
- const contentHeight = page.size.h - margins.top - margins.bottom;
825
- let baseY = margins.top;
826
- if (floating?.vertAnchor === "page") baseY = 0;
827
- else if (floating?.vertAnchor === "text") baseY = state.cursorY;
828
- let x = paginator.getColumnX(state.columnIndex);
829
- if (floating) x = resolveFloatingTablePageX({
830
- anchor: floating,
831
- justification: block.justification,
832
- tableWidth,
833
- marginWidth: contentWidth,
834
- pageWidth: page.size.w,
835
- marginLeft: margins.left,
836
- textFrameWidth: paginator.columnWidth,
837
- textFrameLeft: paginator.getColumnX(state.columnIndex)
838
- });
839
- let y = state.cursorY;
840
- let usedExplicitY = false;
841
- if (floating?.tblpY !== void 0) {
842
- y = baseY + floating.tblpY;
843
- usedExplicitY = true;
844
- } else if (floating?.tblpYSpec) {
845
- usedExplicitY = true;
846
- const spec = floating.tblpYSpec;
847
- if (spec === "top") y = baseY;
848
- else if (spec === "bottom") y = baseY + contentHeight - tableHeight;
849
- else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
850
- }
851
- if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
852
- const fragment = {
853
- kind: "table",
854
- blockId: block.id,
855
- x,
856
- y,
857
- width: tableWidth,
858
- height: tableHeight,
859
- fromRow: 0,
860
- toRow: block.rows.length,
861
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
862
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
863
- isFloating: true,
864
- ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
865
- };
866
- paginator.addUnflowedFragment(fragment);
867
- }
868
- /**
869
- * Layout an image block onto pages.
870
- */
871
- function layoutImage(block, measure, paginator) {
872
- if (block.anchor?.isAnchored) {
873
- layoutAnchoredImage(block, measure, paginator);
874
- return;
875
- }
876
- const bandSkip = measure.bandSkipBefore ?? 0;
877
- const state = paginator.ensureFits(bandSkip + measure.height);
878
- const fragment = {
879
- kind: "image",
880
- blockId: block.id,
881
- x: paginator.getColumnX(state.columnIndex),
882
- y: 0,
883
- width: measure.width,
884
- height: measure.height,
885
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
886
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
887
- };
888
- fragment.y = paginator.addFragment(fragment, measure.height, bandSkip, 0).y;
889
- }
890
- /**
891
- * Layout an anchored (floating) image.
892
- */
893
- function layoutAnchoredImage(block, measure, paginator) {
894
- const state = paginator.getCurrentState();
895
- const anchor = block.anchor;
896
- if (!anchor) return;
897
- const x = anchor.offsetH ?? paginator.getColumnX(state.columnIndex);
898
- const y = anchor.offsetV ?? state.cursorY;
899
- const fragment = {
900
- kind: "image",
901
- blockId: block.id,
902
- x,
903
- y,
904
- width: measure.width,
905
- height: measure.height,
906
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
907
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
908
- isAnchored: true,
909
- zIndex: anchor.behindDoc ? -1 : 1
910
- };
911
- paginator.addUnflowedFragment(fragment);
912
- }
913
- const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
914
- const readTextBoxAnchorBlockId = (block) => Reflect.get(block, TEXT_BOX_ANCHOR_BLOCK_ID);
915
- /**
916
- * Layout a text box block onto pages.
917
- */
918
- function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPageHeight, sectionMarginBottom }) {
919
- if (isPagePinnedBandTextBox(block)) {
920
- const state = paginator.getCurrentState();
921
- const bandTop = bandTopContentY(block.position?.vertical, {
922
- pageHeight: sectionPageHeight,
923
- marginTop: sectionMarginTop,
924
- marginBottom: sectionMarginBottom,
925
- boxHeight: measure.height
926
- });
927
- const x = bandFragmentX(block.position?.horizontal, {
928
- pageWidth: state.page.size.w,
929
- marginLeft: state.page.margins.left,
930
- marginRight: state.page.margins.right,
931
- activeColumnLeft: paginator.getColumnX(state.columnIndex),
932
- activeColumnWidth: paginator.columnWidth,
933
- boxWidth: measure.width
934
- });
935
- const fragment = {
936
- kind: "textBox",
937
- blockId: block.id,
938
- x,
939
- y: block.position?.vertical?.relativeTo === "page" ? sectionMarginTop + bandTop : state.topMargin + bandTop,
940
- width: measure.width,
941
- height: measure.height,
942
- isPositioned: true,
943
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
944
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
945
- };
946
- paginator.addUnflowedFragment(fragment);
947
- return;
948
- }
949
- if (block.position !== void 0) {
950
- const state = paginator.getCurrentState();
951
- const x = bandFragmentX(block.position.horizontal, {
952
- pageWidth: state.page.size.w,
953
- marginLeft: state.page.margins.left,
954
- marginRight: state.page.margins.right,
955
- activeColumnLeft: paginator.getColumnX(state.columnIndex),
956
- activeColumnWidth: paginator.columnWidth,
957
- boxWidth: measure.width
958
- });
959
- const vertical = block.position.vertical;
960
- const anchorBlockId = readTextBoxAnchorBlockId(block);
961
- const anchorParagraph = vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? state.page.fragments.find((fragment) => fragment.kind === "paragraph" && fragment.blockId === anchorBlockId) : void 0;
962
- const bandGeometry = {
963
- pageHeight: sectionPageHeight,
964
- marginTop: sectionMarginTop,
965
- marginBottom: sectionMarginBottom,
966
- boxHeight: measure.height
967
- };
968
- let y;
969
- if (vertical?.relativeTo === "page") y = sectionMarginTop + bandTopContentY(vertical, bandGeometry);
970
- else if (isPageFrameRelativeAnchor(vertical?.relativeTo)) y = state.topMargin + bandTopContentY(vertical, {
971
- pageHeight: sectionPageHeight,
972
- marginTop: sectionMarginTop,
973
- marginBottom: sectionMarginBottom,
974
- boxHeight: measure.height
975
- });
976
- else y = (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
977
- const fragment = {
978
- kind: "textBox",
979
- blockId: block.id,
980
- x,
981
- y,
982
- width: measure.width,
983
- height: measure.height,
984
- isPositioned: true,
985
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
986
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
987
- };
988
- paginator.addUnflowedFragment(fragment);
989
- return;
990
- }
991
- const effect = block.effectExtent;
992
- const occupiedHeight = measure.height + (effect?.top ?? 0) + (effect?.bottom ?? 0);
993
- const occupiedWidth = measure.width + (effect?.left ?? 0) + (effect?.right ?? 0);
994
- const host = block.hostParagraph;
995
- const fragment = {
996
- kind: "textBox",
997
- blockId: block.id,
998
- x: 0,
999
- y: 0,
1000
- width: measure.width,
1001
- height: measure.height,
1002
- ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
1003
- ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
1004
- };
1005
- const result = paginator.addFragment(fragment, occupiedHeight, host?.spacing?.before ?? 0, host?.spacing?.after ?? 0);
1006
- fragment.x = result.x + hostParagraphLineOffset(host, paginator.columnWidth, occupiedWidth) + (effect?.left ?? 0);
1007
- fragment.y = result.y + (effect?.top ?? 0);
1008
- }
1009
- /**
1010
- * Where a line holding only an object of `width` starts within a column:
1011
- * after the host paragraph's start and first-line indents, then aligned in
1012
- * what remains before its end indent.
1013
- */
1014
- function hostParagraphLineOffset(host, columnWidth, width) {
1015
- if (!host) return 0;
1016
- const indent = host.indent;
1017
- const start = (indent?.left ?? 0) + (indent?.firstLine ?? 0) - (indent?.hanging ?? 0);
1018
- const slack = Math.max(0, columnWidth - start - (indent?.right ?? 0) - width);
1019
- switch (host.alignment) {
1020
- case "center": return start + slack / 2;
1021
- case "right": return start + slack;
1022
- case "left":
1023
- case "justify":
1024
- case void 0: return start;
1025
- default:
1026
- host.alignment;
1027
- return start;
1028
- }
1029
- }
1030
- /**
1031
- * A topAndBottom text box whose vertical anchor pins it to the page frame
1032
- * (page/margin/margin-strip) — it floats to a fixed page position rather than
1033
- * flowing in document order. Must agree with the measure pass's band extraction
1034
- * (extractFloatingZones), which uses the same predicate. eigenpal #694.
1035
- */
1036
- function isPagePinnedBandTextBox(block) {
1037
- if (!floatingTextBoxReservesBand(block)) return false;
1038
- return isPageFrameRelativeAnchor(block.position?.vertical?.relativeTo);
1039
- }
1040
- /**
1041
- * Start the next section in the current page region, below the outgoing
1042
- * content.
1043
- *
1044
- * ECMA-376 §17.6.22: a `continuous` break normally keeps the current page
1045
- * geometry and defers the new size/margins to the next natural page break. But
1046
- * a break that changes page size or orientation cannot share a physical sheet
1047
- * with the preceding section, so Word and LibreOffice promote it to a page
1048
- * break (eigenpal/docx-editor#841).
1049
- *
1050
- * Compare against the last laid-out page without materializing one: a break
1051
- * before any content has no sheet to share, so it defers (the first content
1052
- * then opens a page with the new geometry) rather than stranding a blank
1053
- * leading page.
1054
- */
1055
- function startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex) {
1056
- const currentPage = paginator.states.at(-1)?.page;
1057
- const nextSize = nextSectionConfig.pageSize;
1058
- const pageSizeChanges = currentPage != null && (Math.round(nextSize.w) !== Math.round(currentPage.size.w) || Math.round(nextSize.h) !== Math.round(currentPage.size.h));
1059
- if (nextSectionIndex !== void 0) paginator.startSection({
1060
- sectionIndex: nextSectionIndex,
1061
- pageNumbering: nextSectionConfig.pageNumbering,
1062
- placement: pageSizeChanges ? SECTION_START_PLACEMENT.NEXT_PAGE : SECTION_START_PLACEMENT.CONTINUOUS
1063
- });
1064
- if (pageSizeChanges) {
1065
- paginator.updatePageLayout(nextSize, nextSectionConfig.margins);
1066
- if (!paginator.retargetCurrentBlankPage()) paginator.forcePageBreak({ coalesceBlankPage: true });
1067
- return;
1068
- }
1069
- paginator.updatePageLayout(nextSize, nextSectionConfig.margins, false);
1070
- paginator.retargetCurrentBlankPage();
1071
- }
1072
- /**
1073
- * Handle a section break block.
1074
- * @param block - The section break block (current section's properties)
1075
- * @param paginator - The paginator instance
1076
- * @param nextSectionConfig - Page layout for the NEXT section
1077
- * @param nextSectionType - Break type of the NEXT section (how it starts relative to current)
1078
- */
1079
- function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionType, nextSectionIndex) {
1080
- switch (nextSectionType) {
1081
- case "nextPage":
1082
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1083
- if (nextSectionIndex !== void 0) paginator.startSection({
1084
- sectionIndex: nextSectionIndex,
1085
- pageNumbering: nextSectionConfig.pageNumbering
1086
- });
1087
- paginator.forcePageBreak({ coalesceBlankPage: true });
1088
- break;
1089
- case "evenPage":
1090
- if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 !== 0) paginator.forcePageBreak();
1091
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1092
- if (nextSectionIndex !== void 0) paginator.startSection({
1093
- sectionIndex: nextSectionIndex,
1094
- pageNumbering: nextSectionConfig.pageNumbering
1095
- });
1096
- if (!paginator.retargetCurrentBlankPage()) panic("Even-page section target must be blank");
1097
- break;
1098
- case "oddPage":
1099
- if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 === 0) paginator.forcePageBreak();
1100
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
1101
- if (nextSectionIndex !== void 0) paginator.startSection({
1102
- sectionIndex: nextSectionIndex,
1103
- pageNumbering: nextSectionConfig.pageNumbering
1104
- });
1105
- if (!paginator.retargetCurrentBlankPage()) panic("Odd-page section target must be blank");
1106
- break;
1107
- case "nextColumn": {
1108
- const currentPage = paginator.states.at(-1)?.page;
1109
- if (currentPage === void 0 || !physicalColumnRegionIsShared({
1110
- pageSize: currentPage.size,
1111
- margins: currentPage.margins,
1112
- columns: paginator.columns
1113
- }, nextSectionConfig)) {
1114
- startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
1115
- break;
1116
- }
1117
- if (nextSectionIndex !== void 0) paginator.startSection({
1118
- sectionIndex: nextSectionIndex,
1119
- pageNumbering: nextSectionConfig.pageNumbering,
1120
- placement: SECTION_START_PLACEMENT.CONTINUOUS
1121
- });
1122
- paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins, false);
1123
- paginator.forceColumnBreak();
1124
- return;
1125
- }
1126
- case "continuous":
1127
- startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
1128
- break;
1129
- default:
1130
- }
1131
- paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
1132
- }
1133
237
  //#endregion
1134
238
  export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, NOTE_SEPARATOR_RULE_THICKNESS, NOTE_SEPARATOR_WIDTH_FRACTION, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isListNumPr, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, resolveTableWidthPx, scheduleSectionBreak, stripFlowBlockPmAnchors, tableColumnsArePinned };