@stll/folio-core 0.49.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 (91) hide show
  1. package/dist/ai-edits/headless.d.ts +37 -1
  2. package/dist/ai-edits/headless.js +47 -16
  3. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  4. package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
  5. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  6. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  7. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  8. package/dist/display-list/selectDisplayPages.js +44 -0
  9. package/dist/export-pdf.d.ts +35 -2
  10. package/dist/export-pdf.js +51 -20
  11. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  12. package/dist/fonts/fontsourceFaces.js +230 -0
  13. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  14. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  15. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  16. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  17. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  18. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  19. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  20. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  21. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  22. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  23. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  24. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  25. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  26. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  27. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  28. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  29. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  30. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  31. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  32. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  33. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  34. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  35. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  36. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  37. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  38. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  39. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  40. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  41. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  42. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  43. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  44. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  45. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  46. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  47. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  48. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  49. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
  50. package/dist/layout-bridge/engine/hitTest.js +1 -1
  51. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  52. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  53. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  54. package/dist/layout-engine/imageLayout.d.ts +9 -0
  55. package/dist/layout-engine/imageLayout.js +48 -0
  56. package/dist/layout-engine/index.d.ts +3 -18
  57. package/dist/layout-engine/index.js +12 -937
  58. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  59. package/dist/layout-engine/layoutFlowShared.js +8 -0
  60. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  61. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  62. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  63. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  64. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  65. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  66. package/dist/layout-engine/measure/justification.d.ts +32 -0
  67. package/dist/layout-engine/measure/justification.js +83 -0
  68. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  69. package/dist/layout-engine/measure/lineFitting.js +145 -0
  70. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  71. package/dist/layout-engine/measure/lineTypography.js +124 -0
  72. package/dist/layout-engine/measure/measureBlocks.js +2 -1
  73. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  74. package/dist/layout-engine/measure/measureParagraph.js +32 -734
  75. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  76. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  77. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  78. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  79. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  80. package/dist/layout-engine/paragraphLayout.js +204 -0
  81. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  82. package/dist/layout-engine/sectionLayout.js +216 -0
  83. package/dist/layout-engine/tableLayout.d.ts +17 -0
  84. package/dist/layout-engine/tableLayout.js +369 -0
  85. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  86. package/dist/layout-engine/textBoxLayout.js +137 -0
  87. package/dist/layout-painter/renderPage.js +1 -0
  88. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  89. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  90. package/dist/server.d.ts +3 -2
  91. package/package.json +2 -2
@@ -0,0 +1,216 @@
1
+ import { measuredLineAdvance } from "./lineFlow.js";
2
+ import { SECTION_START_PLACEMENT } from "./paginator.js";
3
+ import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore } from "./paragraphSpacing.js";
4
+ import { physicalColumnRegionIsShared } from "./section-breaks.js";
5
+ import { panic } from "better-result";
6
+ //#region src/layout-engine/sectionLayout.ts
7
+ /**
8
+ * Section handling during pagination: per-section layout configuration,
9
+ * section-break scheduling (in place, next page, odd/even filler pages), and
10
+ * balanced column height for continuous sections.
11
+ */
12
+ const DEFAULT_COLUMNS = {
13
+ count: 1,
14
+ gap: 0
15
+ };
16
+ const CONTINUE_PAGE_NUMBERING = { type: "continue" };
17
+ function collectSectionConfigs(blocks, initialConfig, finalConfig) {
18
+ const configs = [];
19
+ const breakIndices = [];
20
+ let previousConfig = initialConfig;
21
+ for (let index = 0; index < blocks.length; index += 1) {
22
+ const block = blocks[index];
23
+ if (block?.kind !== "sectionBreak") continue;
24
+ const sectionBreak = block;
25
+ const config = {
26
+ pageSize: sectionBreak.pageSize ?? previousConfig.pageSize,
27
+ margins: sectionBreak.margins ?? previousConfig.margins,
28
+ pageNumbering: sectionBreak.pageNumbering ?? CONTINUE_PAGE_NUMBERING
29
+ };
30
+ if (sectionBreak.columns !== void 0) config.columns = sectionBreak.columns;
31
+ configs.push(config);
32
+ breakIndices.push(index);
33
+ previousConfig = configs.at(-1) ?? previousConfig;
34
+ }
35
+ configs.push(finalConfig);
36
+ return {
37
+ configs,
38
+ breakIndices
39
+ };
40
+ }
41
+ function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
42
+ if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
43
+ const lineUnits = [];
44
+ let totalHeight = 0;
45
+ let tallestUnit = 0;
46
+ let trailingSpacing = incomingSpacing;
47
+ for (let index = startIndex; index < endIndex; index++) {
48
+ const block = blocks[index];
49
+ const measure = measures[index];
50
+ if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
51
+ if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
52
+ const leadingSpacing = collapseParagraphSpacing({
53
+ before: getParagraphSpacingBefore(block),
54
+ after: trailingSpacing
55
+ });
56
+ if (measure.lines.length === 0) {
57
+ if (leadingSpacing > 0) {
58
+ lineUnits.push(leadingSpacing);
59
+ totalHeight += leadingSpacing;
60
+ tallestUnit = Math.max(tallestUnit, leadingSpacing);
61
+ }
62
+ }
63
+ for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
64
+ const line = measure.lines[lineIndex];
65
+ if (!line) continue;
66
+ const unitHeight = measuredLineAdvance(line) + (lineIndex === 0 ? leadingSpacing : 0);
67
+ lineUnits.push(unitHeight);
68
+ totalHeight += unitHeight;
69
+ tallestUnit = Math.max(tallestUnit, unitHeight);
70
+ }
71
+ if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
72
+ trailingSpacing = getParagraphSpacingAfter(block);
73
+ }
74
+ if (totalHeight <= 0) return;
75
+ const columnsNeeded = (targetHeight) => {
76
+ let usedHeight = 0;
77
+ let usedColumns = 1;
78
+ for (const unitHeight of lineUnits) if (usedHeight > 0 && usedHeight + unitHeight > targetHeight) {
79
+ usedColumns += 1;
80
+ usedHeight = unitHeight;
81
+ } else usedHeight += unitHeight;
82
+ return usedColumns;
83
+ };
84
+ let lower = Math.max(tallestUnit, totalHeight / columnCount);
85
+ let upper = Math.min(totalHeight, availableHeight);
86
+ if (columnsNeeded(upper) > columnCount) return;
87
+ for (let iteration = 0; iteration < 32; iteration++) {
88
+ const middle = (lower + upper) / 2;
89
+ if (columnsNeeded(middle) <= columnCount) upper = middle;
90
+ else lower = middle;
91
+ }
92
+ return Math.ceil(upper * 1e3) / 1e3;
93
+ }
94
+ /**
95
+ * Start the next section in the current page region, below the outgoing
96
+ * content.
97
+ *
98
+ * ECMA-376 §17.6.22: a `continuous` break normally keeps the current page
99
+ * geometry and defers the new size/margins to the next natural page break. But
100
+ * a break that changes page size or orientation cannot share a physical sheet
101
+ * with the preceding section, so Word and LibreOffice promote it to a page
102
+ * break (eigenpal/docx-editor#841).
103
+ *
104
+ * Compare against the last laid-out page without materializing one: a break
105
+ * before any content has no sheet to share, so it defers (the first content
106
+ * then opens a page with the new geometry) rather than stranding a blank
107
+ * leading page.
108
+ */
109
+ function startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex) {
110
+ const currentPage = paginator.states.at(-1)?.page;
111
+ const nextSize = nextSectionConfig.pageSize;
112
+ const pageSizeChanges = currentPage != null && (Math.round(nextSize.w) !== Math.round(currentPage.size.w) || Math.round(nextSize.h) !== Math.round(currentPage.size.h));
113
+ if (nextSectionIndex !== void 0) paginator.startSection({
114
+ sectionIndex: nextSectionIndex,
115
+ pageNumbering: nextSectionConfig.pageNumbering,
116
+ placement: pageSizeChanges ? SECTION_START_PLACEMENT.NEXT_PAGE : SECTION_START_PLACEMENT.CONTINUOUS
117
+ });
118
+ if (pageSizeChanges) {
119
+ paginator.updatePageLayout(nextSize, nextSectionConfig.margins);
120
+ if (!paginator.retargetCurrentBlankPage()) paginator.forcePageBreak({ coalesceBlankPage: true });
121
+ return;
122
+ }
123
+ paginator.updatePageLayout(nextSize, nextSectionConfig.margins, false);
124
+ paginator.retargetCurrentBlankPage();
125
+ }
126
+ /**
127
+ * Whether a section opening on `target` would give two consecutive sheets page
128
+ * numbers that are both odd or both even under `w:evenAndOddHeaders` (§17.10.1). Odd and
129
+ * even page numbers alternate between right- and left-hand sheets, so a
130
+ * `w:pgNumType w:start` restart that repeats whether the previous sheet's number is odd or even
131
+ * needs a blank sheet between them.
132
+ */
133
+ function sectionStartRepeatsOddEven(paginator, target) {
134
+ if (target.page.headerFooterRefs?.evenAndOddHeaders !== true) return false;
135
+ const previous = paginator.states.at(-2)?.page;
136
+ if (previous === void 0 || previous.number !== target.page.number - 1) return false;
137
+ return previous.logicalNumber % 2 === target.page.logicalNumber % 2;
138
+ }
139
+ /**
140
+ * Keep the blank sheet the section break opened as a filler with no page
141
+ * furniture, and open the section again on the following sheet so its first
142
+ * page keeps the restarted page number.
143
+ */
144
+ function insertOddEvenFillerPage(paginator, filler, sectionIndex, sectionConfig) {
145
+ filler.page.headerFooterRefs = {};
146
+ paginator.startSection({
147
+ sectionIndex,
148
+ pageNumbering: sectionConfig.pageNumbering
149
+ });
150
+ paginator.forcePageBreak();
151
+ }
152
+ /**
153
+ * Handle a section break block.
154
+ * @param block - The section break block (current section's properties)
155
+ * @param paginator - The paginator instance
156
+ * @param nextSectionConfig - Page layout for the NEXT section
157
+ * @param nextSectionType - Break type of the NEXT section (how it starts relative to current)
158
+ */
159
+ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionType, nextSectionIndex) {
160
+ switch (nextSectionType) {
161
+ case "nextPage": {
162
+ paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
163
+ if (nextSectionIndex !== void 0) paginator.startSection({
164
+ sectionIndex: nextSectionIndex,
165
+ pageNumbering: nextSectionConfig.pageNumbering
166
+ });
167
+ const target = paginator.forcePageBreak({ coalesceBlankPage: true });
168
+ if (nextSectionIndex !== void 0 && sectionStartRepeatsOddEven(paginator, target)) insertOddEvenFillerPage(paginator, target, nextSectionIndex, nextSectionConfig);
169
+ break;
170
+ }
171
+ case "evenPage":
172
+ if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 !== 0) paginator.forcePageBreak();
173
+ paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
174
+ if (nextSectionIndex !== void 0) paginator.startSection({
175
+ sectionIndex: nextSectionIndex,
176
+ pageNumbering: nextSectionConfig.pageNumbering
177
+ });
178
+ if (!paginator.retargetCurrentBlankPage()) panic("Even-page section target must be blank");
179
+ break;
180
+ case "oddPage":
181
+ if (paginator.forcePageBreak({ coalesceBlankPage: true }).page.number % 2 === 0) paginator.forcePageBreak();
182
+ paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins);
183
+ if (nextSectionIndex !== void 0) paginator.startSection({
184
+ sectionIndex: nextSectionIndex,
185
+ pageNumbering: nextSectionConfig.pageNumbering
186
+ });
187
+ if (!paginator.retargetCurrentBlankPage()) panic("Odd-page section target must be blank");
188
+ break;
189
+ case "nextColumn": {
190
+ const currentPage = paginator.states.at(-1)?.page;
191
+ if (currentPage === void 0 || !physicalColumnRegionIsShared({
192
+ pageSize: currentPage.size,
193
+ margins: currentPage.margins,
194
+ columns: paginator.columns
195
+ }, nextSectionConfig)) {
196
+ startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
197
+ break;
198
+ }
199
+ if (nextSectionIndex !== void 0) paginator.startSection({
200
+ sectionIndex: nextSectionIndex,
201
+ pageNumbering: nextSectionConfig.pageNumbering,
202
+ placement: SECTION_START_PLACEMENT.CONTINUOUS
203
+ });
204
+ paginator.updatePageLayout(nextSectionConfig.pageSize, nextSectionConfig.margins, false);
205
+ paginator.forceColumnBreak();
206
+ return;
207
+ }
208
+ case "continuous":
209
+ startSectionInPlace(paginator, nextSectionConfig, nextSectionIndex);
210
+ break;
211
+ default:
212
+ }
213
+ paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
214
+ }
215
+ //#endregion
216
+ export { CONTINUE_PAGE_NUMBERING, DEFAULT_COLUMNS, balancedParagraphSectionHeight, collectSectionConfigs, handleSectionBreak };
@@ -0,0 +1,17 @@
1
+ import { TableBlock, TableMeasure } from "./types.js";
2
+ import { createPaginator } from "./paginator.js";
3
+ //#region src/layout-engine/tableLayout.d.ts
4
+ /**
5
+ * Calculate total height of header rows from their measures.
6
+ */
7
+ declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
8
+ /**
9
+ * Layout a table block onto pages.
10
+ */
11
+ declare function layoutTable(block: TableBlock, measure: TableMeasure, paginator: ReturnType<typeof createPaginator>, footnoteHeightById?: Map<number, number>): void;
12
+ /**
13
+ * Layout a floating table (anchored) without advancing the cursor.
14
+ */
15
+ declare function layoutFloatingTable(block: TableBlock, measure: TableMeasure, paginator: ReturnType<typeof createPaginator>, contentWidth: number): void;
16
+ //#endregion
17
+ export { getHeaderRowsHeight, layoutFloatingTable, layoutTable };
@@ -0,0 +1,369 @@
1
+ import { projectedFootnoteReserveGrowth } from "./layoutFlowShared.js";
2
+ import { resolveFloatingTablePageX } from "./measure/floatingTablePosition.js";
3
+ import { resolveTableInlineOffset } from "./measure/tableInlinePlacement.js";
4
+ import { isEmptyParagraph } from "./paragraphSpacing.js";
5
+ import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
6
+ import "./types.js";
7
+ //#region src/layout-engine/tableLayout.ts
8
+ /**
9
+ * Places measured tables onto pages: row splitting across pages, repeated
10
+ * header rows, rendered-break and tracked-change row handling, row footnote
11
+ * collection, and floating table placement.
12
+ */
13
+ /**
14
+ * Count consecutive header rows at the start of a table.
15
+ * Header rows are marked with isHeader: true in the block data.
16
+ */
17
+ function countHeaderRows(block) {
18
+ let count = 0;
19
+ for (const row of block.rows) if (row.isHeader) count++;
20
+ else break;
21
+ return count;
22
+ }
23
+ /**
24
+ * Calculate total height of header rows from their measures.
25
+ */
26
+ function getHeaderRowsHeight(measure, headerRowCount) {
27
+ let height = 0;
28
+ for (let i = 0; i < headerRowCount && i < measure.rows.length; i++) height += measure.rows[i].height;
29
+ return height;
30
+ }
31
+ const tableRowStartsWithRenderedPageBreak = (block, rowIndex) => {
32
+ const row = block.rows[rowIndex];
33
+ const visibleCells = row?.cells.filter((cell) => cell.blocks.some((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock)));
34
+ if (!visibleCells || visibleCells.length === 0) return false;
35
+ const startsWithRenderedPageBreak = (cell) => {
36
+ const firstVisibleBlock = cell.blocks.find((cellBlock) => cellBlock.kind !== "paragraph" || !isEmptyParagraph(cellBlock));
37
+ return firstVisibleBlock?.kind === "paragraph" && firstVisibleBlock.attrs?.renderedPageBreakBefore === true;
38
+ };
39
+ if (row?.heightRule === "exact") return startsWithRenderedPageBreak(visibleCells[0]);
40
+ return visibleCells.every(startsWithRenderedPageBreak);
41
+ };
42
+ const getVerticallyMergedRows = (block) => {
43
+ const mergedRows = /* @__PURE__ */ new Set();
44
+ for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
45
+ const row = block.rows[rowIndex];
46
+ if (!row) continue;
47
+ for (const cell of row.cells) {
48
+ const rowSpan = cell.rowSpan ?? 1;
49
+ if (rowSpan <= 1) continue;
50
+ for (let mergedRowIndex = rowIndex; mergedRowIndex < Math.min(block.rows.length, rowIndex + rowSpan); mergedRowIndex += 1) mergedRows.add(mergedRowIndex);
51
+ }
52
+ }
53
+ return mergedRows;
54
+ };
55
+ const flowBlockHasTrackedChanges = (block) => {
56
+ if (block.kind === "paragraph") return block.runs.some((run) => {
57
+ if (run.kind === "lineBreak") return false;
58
+ return run.isInsertion || run.isDeletion;
59
+ });
60
+ if (block.kind === "table") return block.rows.some((row) => row.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)));
61
+ if (block.kind === "textBox") return block.content.some(flowBlockHasTrackedChanges);
62
+ return false;
63
+ };
64
+ const tableRowHasTrackedChanges = (block, rowIndex) => block.rows[rowIndex]?.cells.some((cell) => cell.blocks.some(flowBlockHasTrackedChanges)) ?? false;
65
+ /**
66
+ * Layout a table block onto pages.
67
+ */
68
+ function layoutTable(block, measure, paginator, footnoteHeightById) {
69
+ const rows = measure.rows;
70
+ if (rows.length === 0) return;
71
+ const rowFootnoteIds = block.rows.map((row) => collectTableRowFootnoteIds(row, footnoteHeightById));
72
+ const headerRowCount = countHeaderRows(block);
73
+ const headerRowsHeight = getHeaderRowsHeight(measure, headerRowCount);
74
+ let currentRowIndex = 0;
75
+ const breakInfo = buildTableRowBreakInfo(block, measure);
76
+ const verticallyMergedRows = getVerticallyMergedRows(block);
77
+ const computeTableX = ({ columnIndex, rowIndex }) => {
78
+ return paginator.getColumnX(columnIndex) + resolveTableInlineOffset({
79
+ table: block,
80
+ rowJustification: block.rows[rowIndex]?.justification,
81
+ frameWidth: paginator.columnWidth,
82
+ tableWidth: measure.totalWidth
83
+ });
84
+ };
85
+ const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
86
+ const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
87
+ const previous = state.page.fragments.at(-1);
88
+ return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY;
89
+ };
90
+ const shouldRepeatHeaderRows = (rowIndex, consumed, state = paginator.getCurrentState()) => headerRowCount > 0 && rowIndex >= headerRowCount && !(consumed === 0 && hasAdjacentPriorTableRows(rowIndex, state));
91
+ const rowOpensFlowRegion = (rowIndex, state) => {
92
+ if (state.cursorY === state.topMargin) return true;
93
+ const previous = state.page.fragments.at(-1);
94
+ 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;
95
+ };
96
+ const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
97
+ const row = rows[rowIndex];
98
+ const sourceRow = block.rows[rowIndex];
99
+ if (!row || !sourceRow || sourceRow.isHeader || verticallyMergedRows.has(rowIndex)) return false;
100
+ if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
101
+ const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
102
+ const oversized = row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
103
+ if (sourceRow.cantSplit) return oversized && sourceRow.heightRule !== "exact" && rowOpensFlowRegion(rowIndex, state);
104
+ if (!oversized && (state.footnoteHeight > 0 || (rowFootnoteIds[rowIndex]?.length ?? 0) > 0)) return false;
105
+ const currentHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
106
+ const currentAvailableHeight = paginator.getAvailableHeight() - currentHeaderOverhead - state.trailingSpacing;
107
+ return oversized || row.height > currentAvailableHeight;
108
+ };
109
+ while (currentRowIndex < rows.length) {
110
+ const rowState = paginator.getCurrentState();
111
+ const rowHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, rowState) ? headerRowsHeight : 0;
112
+ const rowAvailableHeight = paginator.getAvailableHeight() - rowHeaderOverhead - rowState.trailingSpacing;
113
+ const rowStartsFreshPage = rowState.cursorY === rowState.topMargin && rowState.page.fragments.length === 0;
114
+ if (block.rows[currentRowIndex]?.breakBefore === "page" && !rowStartsFreshPage) {
115
+ paginator.forcePageBreak();
116
+ continue;
117
+ }
118
+ if (!rowStartsFreshPage && tableRowStartsWithRenderedPageBreak(block, currentRowIndex) && !tableRowHasTrackedChanges(block, currentRowIndex) && rows[currentRowIndex].height > rowAvailableHeight) {
119
+ paginator.forcePageBreak();
120
+ continue;
121
+ }
122
+ const splittableRow = rows[currentRowIndex];
123
+ if (canSplitRow(currentRowIndex)) {
124
+ let consumed = 0;
125
+ const renderedBreakHints = tableRowHasTrackedChanges(block, currentRowIndex) ? [] : breakInfo.renderedBreakHints[currentRowIndex] ?? [];
126
+ let renderedBreakHintIndex = 0;
127
+ const discardPassedRenderedBreakHints = () => {
128
+ while ((renderedBreakHints[renderedBreakHintIndex]?.offset ?? Infinity) <= consumed) renderedBreakHintIndex += 1;
129
+ };
130
+ const advanceAfterNaturalFlowBreak = (previousPageNumber) => {
131
+ discardPassedRenderedBreakHints();
132
+ if (paginator.getCurrentState().page.number > previousPageNumber && renderedBreakHintIndex < renderedBreakHints.length) renderedBreakHintIndex += 1;
133
+ };
134
+ while (consumed < splittableRow.height) {
135
+ const sliceState = paginator.getCurrentState();
136
+ const repeatHeaderRows = shouldRepeatHeaderRows(currentRowIndex, consumed, sliceState);
137
+ const headerOverhead = repeatHeaderRows ? headerRowsHeight : 0;
138
+ const sliceAvail = paginator.getAvailableHeight() - headerOverhead - (consumed === 0 ? sliceState.trailingSpacing : 0);
139
+ let slice = snapRowBreak(breakInfo, currentRowIndex, consumed, sliceAvail);
140
+ let forceRenderedPageBreak = false;
141
+ if (slice <= 0) {
142
+ if (!(sliceState.cursorY === sliceState.topMargin && sliceState.page.fragments.length === 0)) {
143
+ const previousPageNumber = sliceState.page.number;
144
+ paginator.forceColumnBreak();
145
+ advanceAfterNaturalFlowBreak(previousPageNumber);
146
+ continue;
147
+ }
148
+ const from = consumed;
149
+ slice = (breakInfo.breakOffsets[currentRowIndex]?.find((o) => o > from) ?? splittableRow.height) - consumed;
150
+ }
151
+ discardPassedRenderedBreakHints();
152
+ const renderedBreakHint = renderedBreakHints[renderedBreakHintIndex];
153
+ if (renderedBreakHint && renderedBreakHint.offset <= consumed + slice && sliceAvail - (renderedBreakHint.offset - consumed) <= renderedBreakHint.lineAdvance * 3) {
154
+ slice = renderedBreakHint.offset - consumed;
155
+ renderedBreakHintIndex += 1;
156
+ forceRenderedPageBreak = true;
157
+ }
158
+ const sliceBottom = consumed + slice;
159
+ const continuationSkip = sliceBottom < splittableRow.height ? getRowContinuationSkip(breakInfo, currentRowIndex, sliceBottom) : 0;
160
+ const nextConsumed = Math.min(splittableRow.height, sliceBottom + continuationSkip);
161
+ const moreAfter = !(nextConsumed >= splittableRow.height) || currentRowIndex + 1 < rows.length;
162
+ const fragmentHeight = headerOverhead + slice;
163
+ const sliceFragment = {
164
+ kind: "table",
165
+ blockId: block.id,
166
+ x: computeTableX({
167
+ columnIndex: sliceState.columnIndex,
168
+ rowIndex: currentRowIndex
169
+ }),
170
+ y: 0,
171
+ width: measure.totalWidth,
172
+ height: fragmentHeight,
173
+ fromRow: currentRowIndex,
174
+ toRow: currentRowIndex + 1,
175
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
176
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
177
+ ...consumed > 0 || currentRowIndex > 0 ? { continuesFromPrev: true } : {},
178
+ ...moreAfter ? { continuesOnNext: true } : {},
179
+ ...repeatHeaderRows ? { headerRowCount } : {},
180
+ ...consumed > 0 ? { topClip: consumed } : {},
181
+ ...sliceBottom >= splittableRow.height ? {} : { bottomClip: sliceBottom },
182
+ ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
183
+ };
184
+ const sliceResult = paginator.addFragment(sliceFragment, fragmentHeight, 0, 0);
185
+ sliceFragment.y = sliceResult.y;
186
+ sliceFragment.x = computeTableX({
187
+ columnIndex: sliceResult.state.columnIndex,
188
+ rowIndex: currentRowIndex
189
+ });
190
+ consumed = nextConsumed;
191
+ if (consumed < splittableRow.height) if (forceRenderedPageBreak) paginator.forcePageBreak();
192
+ else {
193
+ const previousPageNumber = paginator.getCurrentState().page.number;
194
+ paginator.forceColumnBreak();
195
+ advanceAfterNaturalFlowBreak(previousPageNumber);
196
+ }
197
+ }
198
+ currentRowIndex += 1;
199
+ continue;
200
+ }
201
+ const state = paginator.getCurrentState();
202
+ const rawAvailableHeight = paginator.getAvailableHeight();
203
+ const isFirstFragment = currentRowIndex === 0;
204
+ const bandSkip = isFirstFragment ? measure.bandSkipBefore ?? 0 : 0;
205
+ const availableHeight = rawAvailableHeight - (isFirstFragment ? Math.max(bandSkip, state.trailingSpacing) : 0);
206
+ const repeatHeaderRowsForNormalFragment = shouldRepeatHeaderRows(currentRowIndex, 0, state);
207
+ const normalHeaderOverhead = repeatHeaderRowsForNormalFragment ? headerRowsHeight : 0;
208
+ let rowsHeight = 0;
209
+ let fittingRows = 0;
210
+ const pageFootnoteIds = new Set(state.page.footnoteIds ?? []);
211
+ const fragmentFootnoteIds = [];
212
+ let fragmentFootnoteHeight = 0;
213
+ let retryOnNextFlowRegion = false;
214
+ const fragmentX = computeTableX({
215
+ columnIndex: state.columnIndex,
216
+ rowIndex: currentRowIndex
217
+ });
218
+ for (let j = currentRowIndex; j < rows.length; j++) {
219
+ if (j > currentRowIndex && block.rows[j]?.breakBefore === "page") break;
220
+ if (j > currentRowIndex && computeTableX({
221
+ columnIndex: state.columnIndex,
222
+ rowIndex: j
223
+ }) !== fragmentX) break;
224
+ const rowHeight = rows[j].height;
225
+ const currentRowFootnoteIds = rowFootnoteIds[j] ?? [];
226
+ const fragmentFootnoteCountBeforeRow = fragmentFootnoteIds.length;
227
+ let rowFootnoteHeight = 0;
228
+ for (const id of currentRowFootnoteIds) {
229
+ if (pageFootnoteIds.has(id) || fragmentFootnoteIds.includes(id)) continue;
230
+ fragmentFootnoteIds.push(id);
231
+ rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
232
+ }
233
+ const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
234
+ const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
235
+ if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
236
+ rowsHeight += rowHeight;
237
+ fragmentFootnoteHeight += rowFootnoteHeight;
238
+ fittingRows++;
239
+ } else if (fittingRows === 0) {
240
+ if (state.cursorY === state.topMargin && state.page.fragments.length === 0) {
241
+ rowsHeight += rowHeight;
242
+ fragmentFootnoteHeight += rowFootnoteHeight;
243
+ fittingRows++;
244
+ break;
245
+ }
246
+ fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
247
+ paginator.forceColumnBreak();
248
+ retryOnNextFlowRegion = true;
249
+ break;
250
+ } else {
251
+ fragmentFootnoteIds.splice(fragmentFootnoteCountBeforeRow);
252
+ break;
253
+ }
254
+ }
255
+ if (retryOnNextFlowRegion) continue;
256
+ const fragmentHeight = rowsHeight + normalHeaderOverhead;
257
+ const isLastFragment = currentRowIndex + fittingRows >= rows.length;
258
+ const desiredX = fragmentX;
259
+ const fragment = {
260
+ kind: "table",
261
+ blockId: block.id,
262
+ x: desiredX,
263
+ y: 0,
264
+ width: measure.totalWidth,
265
+ height: fragmentHeight,
266
+ fromRow: currentRowIndex,
267
+ toRow: currentRowIndex + fittingRows,
268
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
269
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
270
+ ...!isFirstFragment ? { continuesFromPrev: true } : {},
271
+ ...!isLastFragment ? { continuesOnNext: true } : {},
272
+ ...repeatHeaderRowsForNormalFragment ? { headerRowCount } : {},
273
+ ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
274
+ };
275
+ if (fragmentFootnoteHeight > 0) paginator.addFootnoteHeight(fragmentFootnoteHeight, fragmentFootnoteIds);
276
+ fragment.y = paginator.addFragment(fragment, fragmentHeight, bandSkip, 0).y;
277
+ fragment.x = desiredX;
278
+ currentRowIndex += fittingRows;
279
+ if (currentRowIndex < rows.length) {
280
+ if (canSplitRow(currentRowIndex)) {
281
+ const nextState = paginator.getCurrentState();
282
+ const nextHeaderOverhead = shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0;
283
+ const nextSliceAvail = paginator.getAvailableHeight() - nextHeaderOverhead - nextState.trailingSpacing;
284
+ if (snapRowBreak(breakInfo, currentRowIndex, 0, nextSliceAvail) > 0) continue;
285
+ }
286
+ const nextState = paginator.getCurrentState();
287
+ const nextRowHeight = rows[currentRowIndex].height + (shouldRepeatHeaderRows(currentRowIndex, 0, nextState) ? headerRowsHeight : 0);
288
+ paginator.ensureFits(nextRowHeight);
289
+ }
290
+ }
291
+ }
292
+ function collectTableRowFootnoteIds(row, footnoteHeightById) {
293
+ if (!row || !footnoteHeightById || row.hidden) return [];
294
+ const ids = [];
295
+ const walk = (blocks) => {
296
+ for (const block of blocks) {
297
+ if (block.kind === "paragraph") {
298
+ 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);
299
+ continue;
300
+ }
301
+ if (block.kind === "table") {
302
+ for (const nestedRow of block.rows) {
303
+ if (nestedRow.hidden) continue;
304
+ for (const cell of nestedRow.cells) walk(cell.blocks);
305
+ }
306
+ continue;
307
+ }
308
+ if (block.kind === "textBox") walk(block.content);
309
+ }
310
+ };
311
+ for (const cell of row.cells) walk(cell.blocks);
312
+ return ids;
313
+ }
314
+ /**
315
+ * Layout a floating table (anchored) without advancing the cursor.
316
+ */
317
+ function layoutFloatingTable(block, measure, paginator, contentWidth) {
318
+ const state = paginator.getCurrentState();
319
+ const floating = block.floating;
320
+ const page = state.page;
321
+ const margins = page.margins;
322
+ const tableWidth = measure.totalWidth;
323
+ const tableHeight = measure.totalHeight;
324
+ const contentHeight = page.size.h - margins.top - margins.bottom;
325
+ let baseY = margins.top;
326
+ if (floating?.vertAnchor === "page") baseY = 0;
327
+ else if (floating?.vertAnchor === "text") baseY = state.cursorY;
328
+ let x = paginator.getColumnX(state.columnIndex);
329
+ if (floating) x = resolveFloatingTablePageX({
330
+ anchor: floating,
331
+ justification: block.justification,
332
+ tableWidth,
333
+ marginWidth: contentWidth,
334
+ pageWidth: page.size.w,
335
+ marginLeft: margins.left,
336
+ textFrameWidth: paginator.columnWidth,
337
+ textFrameLeft: paginator.getColumnX(state.columnIndex)
338
+ });
339
+ let y = state.cursorY;
340
+ let usedExplicitY = false;
341
+ if (floating?.tblpY !== void 0) {
342
+ y = baseY + floating.tblpY;
343
+ usedExplicitY = true;
344
+ } else if (floating?.tblpYSpec) {
345
+ usedExplicitY = true;
346
+ const spec = floating.tblpYSpec;
347
+ if (spec === "top") y = baseY;
348
+ else if (spec === "bottom") y = baseY + contentHeight - tableHeight;
349
+ else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
350
+ }
351
+ if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
352
+ const fragment = {
353
+ kind: "table",
354
+ blockId: block.id,
355
+ x,
356
+ y,
357
+ width: tableWidth,
358
+ height: tableHeight,
359
+ fromRow: 0,
360
+ toRow: block.rows.length,
361
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
362
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {},
363
+ isFloating: true,
364
+ ...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
365
+ };
366
+ paginator.addUnflowedFragment(fragment);
367
+ }
368
+ //#endregion
369
+ export { getHeaderRowsHeight, layoutFloatingTable, layoutTable };
@@ -0,0 +1,15 @@
1
+ import { TextBoxBlock, TextBoxMeasure } from "./types.js";
2
+ import { createPaginator } from "./paginator.js";
3
+ //#region src/layout-engine/textBoxLayout.d.ts
4
+ type LayoutTextBoxOptions = {
5
+ paginator: ReturnType<typeof createPaginator>;
6
+ sectionMarginTop: number;
7
+ sectionPageHeight: number;
8
+ sectionMarginBottom: number;
9
+ };
10
+ /**
11
+ * Layout a text box block onto pages.
12
+ */
13
+ declare function layoutTextBox(block: TextBoxBlock, measure: TextBoxMeasure, { paginator, sectionMarginTop, sectionPageHeight, sectionMarginBottom }: LayoutTextBoxOptions): void;
14
+ //#endregion
15
+ export { layoutTextBox };