@stll/folio-core 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/ai-edits/blockRange.d.ts +13 -2
  2. package/dist/ai-edits/blockRange.js +23 -2
  3. package/dist/ai-edits/index.d.ts +4 -2
  4. package/dist/ai-edits/index.js +3 -1
  5. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  6. package/dist/ai-edits/scoped-reading.js +60 -0
  7. package/dist/ai-edits/snapshot.js +13 -4
  8. package/dist/ai-edits/types.d.ts +44 -2
  9. package/dist/compat/eigenpal.d.ts +5 -2
  10. package/dist/compat/eigenpal.js +4 -1
  11. package/dist/controller/fontReadiness.d.ts +29 -0
  12. package/dist/controller/fontReadiness.js +139 -0
  13. package/dist/controller/layoutPipeline.js +86 -11
  14. package/dist/docx/blockContentParser.js +51 -8
  15. package/dist/docx/capabilities.d.ts +41 -0
  16. package/dist/docx/capabilities.js +322 -0
  17. package/dist/docx/compatibility.d.ts +34 -2
  18. package/dist/docx/compatibility.js +128 -38
  19. package/dist/docx/conformance.d.ts +6 -0
  20. package/dist/docx/conformance.js +18 -0
  21. package/dist/docx/ensureParaIds.d.ts +29 -0
  22. package/dist/docx/ensureParaIds.js +423 -0
  23. package/dist/docx/groupDrawingParser.d.ts +1 -1
  24. package/dist/docx/groupDrawingParser.js +49 -8
  25. package/dist/docx/numberingParser.js +1 -0
  26. package/dist/docx/paragraphParser.js +9 -0
  27. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  28. package/dist/docx/parser.js +2 -0
  29. package/dist/docx/runParser.js +11 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  31. package/dist/docx/selectiveXmlPatch.js +1 -1
  32. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  33. package/dist/docx/serializer/runSerializer.js +7 -2
  34. package/dist/docx/serializer/tableSerializer.js +4 -0
  35. package/dist/docx/server/boundedArchive.d.ts +24 -0
  36. package/dist/docx/server/boundedArchive.js +106 -0
  37. package/dist/docx/server/extractDocxText.d.ts +23 -0
  38. package/dist/docx/server/extractDocxText.js +154 -0
  39. package/dist/docx/tableParser.js +8 -0
  40. package/dist/docx/textBoxParser.js +6 -1
  41. package/dist/docx/vmlImageParser.js +145 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.js +4 -1
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  45. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  46. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  47. package/dist/layout-bridge/convert/toFlowBlocks.js +106 -13
  48. package/dist/layout-bridge/engine/hitTest.js +2 -1
  49. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  50. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  51. package/dist/layout-engine/headerFooterRefs.js +40 -0
  52. package/dist/layout-engine/index.d.ts +3 -2
  53. package/dist/layout-engine/index.js +125 -72
  54. package/dist/layout-engine/keep-together.d.ts +7 -5
  55. package/dist/layout-engine/keep-together.js +20 -4
  56. package/dist/layout-engine/measure/cache.js +2 -0
  57. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  58. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  59. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  60. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  61. package/dist/layout-engine/measure/measureParagraph.js +42 -15
  62. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  63. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  64. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  65. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  66. package/dist/layout-engine/paginator.d.ts +2 -1
  67. package/dist/layout-engine/paginator.js +17 -8
  68. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  69. package/dist/layout-engine/paragraphFrame.js +17 -0
  70. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  71. package/dist/layout-engine/paragraphSequence.js +25 -0
  72. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  73. package/dist/layout-engine/paragraphSpacing.js +30 -0
  74. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  75. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  76. package/dist/layout-engine/types.d.ts +31 -10
  77. package/dist/layout-engine/types.js +15 -2
  78. package/dist/layout-painter/renderPage.js +2 -2
  79. package/dist/layout-painter/renderParagraph.js +94 -10
  80. package/dist/layout-painter/renderTable.js +42 -12
  81. package/dist/layout-painter/renderTextBox.js +17 -7
  82. package/dist/managers/DocumentLoaderManager.js +1 -1
  83. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  84. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  85. package/dist/paged-layout/sectionGeometry.js +1 -1
  86. package/dist/prosemirror/attrs/index.js +11 -0
  87. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  88. package/dist/prosemirror/conversion/toProseDoc.js +29 -23
  89. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  90. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  91. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  92. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  93. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  94. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  95. package/dist/prosemirror/schema/nodes.d.ts +6 -3
  96. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  97. package/dist/prosemirror/utils/tabCalculator.js +1 -1
  98. package/dist/server.d.ts +6 -2
  99. package/dist/server.js +5 -1
  100. package/dist/types/index.d.ts +2 -1
  101. package/dist/utils/createDocument.js +26 -18
  102. package/dist/utils/formatToStyle.js +3 -3
  103. package/dist/utils/hexId.d.ts +8 -1
  104. package/dist/utils/hexId.js +11 -3
  105. package/dist/utils/units.d.ts +6 -6
  106. package/dist/utils/units.js +8 -8
  107. package/package.json +1 -1
@@ -1,9 +1,12 @@
1
- import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, isFloatingTextBoxBlock, tableColumnsArePinned } from "../types.js";
1
+ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingTextBoxBlock, tableColumnsArePinned } from "../types.js";
2
2
  import { findClearLineY, measureParagraph } from "./measureParagraph.js";
3
3
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
4
+ import { layoutTextBoxParagraphs } from "./textBoxParagraphLayout.js";
4
5
  import { getTextBoxGroupId } from "../textBoxGroup.js";
6
+ import { isParagraphFrameTextBox } from "../paragraphFrame.js";
5
7
  import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
6
- import { bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
8
+ import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
9
+ import { buildTableCellGrid, getFirstAvailableColumn, getTableCellVerticalBorderHeight } from "./tableCellGrid.js";
7
10
  //#region src/layout-engine/measure/measureBlocks.ts
8
11
  /**
9
12
  * Pseudo-infinite measurement width (px) used for `w:noWrap` table cells so
@@ -25,7 +28,7 @@ const NO_WRAP_MEASURE_WIDTH = 1e6;
25
28
  * around a background letterhead or a foreground overlay (Codex
26
29
  * PR #258 review).
27
30
  */
28
- function isFloatingImageRun(run) {
31
+ function isSideWrappingImageRun(run) {
29
32
  const wrapType = run.wrapType;
30
33
  const displayMode = run.displayMode;
31
34
  if (wrapType === "behind" || wrapType === "inFront") return false;
@@ -82,9 +85,6 @@ function measureTableCellBlockVisualHeight(block, blockMeasure) {
82
85
  const spacingAfter = paragraphBlock.attrs?.spacing?.after ?? 0;
83
86
  return spacingBefore + maxImageHeight + spacingAfter;
84
87
  }
85
- function getTableCellVerticalBorderHeight(cell, isFirstRow) {
86
- return (isFirstRow ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
87
- }
88
88
  function measureTableBlock(tableBlock, contentWidth, fieldValues) {
89
89
  const DEFAULT_CELL_PADDING_X = 7;
90
90
  const DEFAULT_CELL_PADDING_Y = 0;
@@ -101,38 +101,17 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
101
101
  columnWidths = columnWidths.map((w) => w * scale);
102
102
  }
103
103
  }
104
- const occupiedColumnsPerRow = /* @__PURE__ */ new Map();
105
- for (let rowIdx = 0; rowIdx < tableBlock.rows.length; rowIdx++) {
106
- const row = tableBlock.rows[rowIdx];
107
- if (!row) continue;
108
- let colIdx = 0;
109
- const occupied = occupiedColumnsPerRow.get(rowIdx) ?? /* @__PURE__ */ new Set();
110
- while (occupied.has(colIdx)) colIdx++;
111
- for (const cell of row.cells) {
112
- const colSpan = cell.colSpan ?? 1;
113
- const rowSpan = cell.rowSpan ?? 1;
114
- if (rowSpan > 1) for (let r = rowIdx + 1; r < rowIdx + rowSpan; r++) {
115
- if (!occupiedColumnsPerRow.has(r)) occupiedColumnsPerRow.set(r, /* @__PURE__ */ new Set());
116
- const occSet = occupiedColumnsPerRow.get(r);
117
- for (let c = 0; c < colSpan; c++) occSet.add(colIdx + c);
118
- }
119
- colIdx += colSpan;
120
- while (occupied.has(colIdx)) colIdx++;
121
- }
122
- }
104
+ const cellGrid = buildTableCellGrid(tableBlock.rows, columnWidths.length);
123
105
  const columnsPinned = tableColumnsArePinned(tableBlock);
124
106
  const rows = tableBlock.rows.map((row, rowIdx) => {
125
- let columnIndex = 0;
126
- const occupied = occupiedColumnsPerRow.get(rowIdx) ?? /* @__PURE__ */ new Set();
127
- while (occupied.has(columnIndex)) columnIndex++;
107
+ let columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, row.gridBefore ?? 0);
128
108
  return {
129
109
  cells: row.cells.map((cell) => {
130
110
  const colSpan = cell.colSpan ?? 1;
131
111
  let cellWidth = 0;
132
112
  for (let c = 0; c < colSpan && columnIndex + c < columnWidths.length; c++) cellWidth += columnWidths[columnIndex + c] ?? 0;
133
113
  if (cellWidth === 0) cellWidth = cell.width ?? 100;
134
- columnIndex += colSpan;
135
- while (occupied.has(columnIndex)) columnIndex++;
114
+ columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, columnIndex + colSpan);
136
115
  const padLeft = cell.padding?.left ?? DEFAULT_CELL_PADDING_X;
137
116
  const padRight = cell.padding?.right ?? DEFAULT_CELL_PADDING_X;
138
117
  const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight);
@@ -163,7 +142,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
163
142
  }
164
143
  const sourceRowCells = tableBlock.rows[rowIdx]?.cells;
165
144
  let maxCellHeightWithBorders = 0;
166
- let maxBorderHeight = 0;
145
+ let maxCellInsets = 0;
167
146
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
168
147
  const cell = row.cells[cellIdx];
169
148
  const sourceCell = sourceRowCells?.[cellIdx];
@@ -178,14 +157,14 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
178
157
  const padBottom = sourceCell?.padding?.bottom ?? DEFAULT_CELL_PADDING_Y;
179
158
  cell.height += padTop + padBottom;
180
159
  if ((sourceCell?.rowSpan ?? 1) > 1) continue;
181
- const borderHeight = getTableCellVerticalBorderHeight(sourceCell, rowIdx === 0);
160
+ const borderHeight = getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx);
182
161
  maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + borderHeight);
183
- maxBorderHeight = Math.max(maxBorderHeight, borderHeight);
162
+ maxCellInsets = Math.max(maxCellInsets, padTop + padBottom + borderHeight);
184
163
  }
185
164
  const explicitHeight = sourceRow?.height;
186
165
  const heightRule = sourceRow?.heightRule;
187
166
  if (explicitHeight && heightRule === "exact") row.height = explicitHeight;
188
- else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxBorderHeight);
167
+ else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxCellInsets);
189
168
  else row.height = maxCellHeightWithBorders;
190
169
  }
191
170
  for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
@@ -200,7 +179,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
200
179
  const spanEnd = Math.min(rows.length, rowIdx + rowSpan);
201
180
  let combinedHeight = 0;
202
181
  for (let spannedRowIdx = rowIdx; spannedRowIdx < spanEnd; spannedRowIdx++) combinedHeight += rows[spannedRowIdx]?.height ?? 0;
203
- const deficit = measuredCell.height + getTableCellVerticalBorderHeight(sourceCell, rowIdx === 0) - combinedHeight;
182
+ const deficit = measuredCell.height + getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx) - combinedHeight;
204
183
  if (deficit <= 0) continue;
205
184
  for (let spannedRowIdx = spanEnd - 1; spannedRowIdx >= rowIdx; spannedRowIdx--) {
206
185
  if (tableBlock.rows[spannedRowIdx]?.heightRule === "exact") continue;
@@ -223,13 +202,34 @@ function perBlockNumberValue(value, blockIndex, fallback) {
223
202
  if (Array.isArray(value)) return value[blockIndex] ?? fallback;
224
203
  return value;
225
204
  }
205
+ function textBoxWrapSide({ box, contentX, boxWidth, contentWidth }) {
206
+ if (box.wrapText === "left") return "left";
207
+ if (box.wrapText === "right") return "right";
208
+ const leftSpace = contentX;
209
+ const rightSpace = contentWidth - contentX - boxWidth;
210
+ if (box.wrapText === "largest") return leftSpace >= rightSpace ? "left" : "right";
211
+ return contentX + boxWidth / 2 < contentWidth / 2 ? "right" : "left";
212
+ }
226
213
  function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry) {
227
214
  const zones = [];
215
+ const defaultContentWidth = Array.isArray(contentWidth) ? contentWidth[0] ?? 0 : contentWidth;
228
216
  const textBoxGroupAnchors = /* @__PURE__ */ new Map();
217
+ const paragraphFrameGroupSizes = /* @__PURE__ */ new Map();
218
+ for (const block of blocks) {
219
+ if (block.kind !== "textBox" || !isParagraphFrameTextBox(block)) continue;
220
+ const groupId = getTextBoxGroupId(block);
221
+ if (groupId !== void 0) paragraphFrameGroupSizes.set(groupId, (paragraphFrameGroupSizes.get(groupId) ?? 0) + 1);
222
+ }
229
223
  const defaultMarginTop = Array.isArray(marginTop) ? marginTop[0] ?? 0 : marginTop;
224
+ const pageWidthInput = pageGeometry?.pageWidth ?? defaultContentWidth;
230
225
  const pageHeightInput = pageGeometry?.pageHeight ?? 0;
226
+ const marginLeftInput = pageGeometry?.marginLeft ?? 0;
227
+ const marginRightInput = pageGeometry?.marginRight ?? 0;
231
228
  const marginBottomInput = pageGeometry?.marginBottom ?? 0;
229
+ const defaultPageWidth = Array.isArray(pageWidthInput) ? pageWidthInput[0] ?? defaultContentWidth : pageWidthInput;
232
230
  const defaultPageHeight = Array.isArray(pageHeightInput) ? pageHeightInput[0] ?? 0 : pageHeightInput;
231
+ const defaultMarginLeft = Array.isArray(marginLeftInput) ? marginLeftInput[0] ?? 0 : marginLeftInput;
232
+ const defaultMarginRight = Array.isArray(marginRightInput) ? marginRightInput[0] ?? 0 : marginRightInput;
233
233
  const defaultMarginBottom = Array.isArray(marginBottomInput) ? marginBottomInput[0] ?? 0 : marginBottomInput;
234
234
  for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
235
235
  const block = blocks[blockIndex];
@@ -238,13 +238,36 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
238
238
  for (const run of paragraphBlock.runs) {
239
239
  if (run.kind !== "image") continue;
240
240
  const imgRun = run;
241
- if (!isFloatingImageRun(imgRun)) continue;
241
+ const positionedBand = imgRun.wrapType === "topAndBottom" && imgRun.position !== void 0;
242
+ if (!isSideWrappingImageRun(imgRun) && !positionedBand) continue;
242
243
  let topY = 0;
243
244
  const position = imgRun.position;
244
245
  const distTop = imgRun.distTop ?? 0;
245
246
  const distBottom = imgRun.distBottom ?? 0;
246
247
  const distLeft = imgRun.distLeft ?? 12;
247
248
  const distRight = imgRun.distRight ?? 12;
249
+ if (positionedBand && position !== void 0) {
250
+ const vertical = position.vertical;
251
+ const blockMarginTop = perBlockNumberValue(marginTop, blockIndex, defaultMarginTop);
252
+ const pageFrameRelative = isPageFrameRelativeAnchor(vertical?.relativeTo);
253
+ const rawTop = pageFrameRelative ? bandTopContentY(vertical, {
254
+ pageHeight: perBlockNumberValue(pageHeightInput, blockIndex, defaultPageHeight),
255
+ marginTop: blockMarginTop,
256
+ marginBottom: perBlockNumberValue(marginBottomInput, blockIndex, defaultMarginBottom),
257
+ boxHeight: imgRun.height
258
+ }) : emuToPixels(vertical?.posOffset ?? 0);
259
+ const bottomY = rawTop + imgRun.height + distBottom;
260
+ if (bottomY > 0) zones.push({
261
+ leftMargin: 0,
262
+ rightMargin: 0,
263
+ topY: Math.max(0, rawTop - distTop),
264
+ bottomY,
265
+ anchorBlockIndex: blockIndex,
266
+ ...pageFrameRelative ? { isMarginRelative: true } : {},
267
+ fullWidthBlock: true
268
+ });
269
+ continue;
270
+ }
248
271
  if (position?.vertical) {
249
272
  const v = position.vertical;
250
273
  if (v.align === "top" && v.relativeTo === "margin") topY = 0;
@@ -259,8 +282,8 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
259
282
  else if (h.align === "right") rightMargin = imgRun.width + distLeft;
260
283
  else if (h.posOffset !== void 0) {
261
284
  const x = emuToPixels(h.posOffset);
262
- if (x < contentWidth / 2) leftMargin = x + imgRun.width + distRight;
263
- else rightMargin = contentWidth - x + distLeft;
285
+ if (x < defaultContentWidth / 2) leftMargin = x + imgRun.width + distRight;
286
+ else rightMargin = defaultContentWidth - x + distLeft;
264
287
  }
265
288
  } else if (imgRun.cssFloat === "left") leftMargin = imgRun.width + distRight;
266
289
  else if (imgRun.cssFloat === "right") rightMargin = imgRun.width + distLeft;
@@ -283,7 +306,7 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
283
306
  const tableBlock = block;
284
307
  const floating = tableBlock.floating;
285
308
  if (!floating) continue;
286
- const tableMeasure = measureTableBlock(tableBlock, contentWidth);
309
+ const tableMeasure = measureTableBlock(tableBlock, defaultContentWidth);
287
310
  const tableWidth = tableMeasure.totalWidth;
288
311
  const tableHeight = tableMeasure.totalHeight;
289
312
  const distLeft = floating.leftFromText ?? 12;
@@ -295,12 +318,12 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
295
318
  let x = 0;
296
319
  if (floating.tblpX !== void 0) x = floating.tblpX;
297
320
  else if (floating.tblpXSpec) if (floating.tblpXSpec === "left" || floating.tblpXSpec === "inside") x = 0;
298
- else if (floating.tblpXSpec === "right" || floating.tblpXSpec === "outside") x = contentWidth - tableWidth;
299
- else x = (contentWidth - tableWidth) / 2;
300
- else if (tableBlock.justification === "center") x = (contentWidth - tableWidth) / 2;
301
- else if (tableBlock.justification === "right") x = contentWidth - tableWidth;
302
- if (x < contentWidth / 2) leftMargin = x + tableWidth + distRight;
303
- else rightMargin = contentWidth - x + distLeft;
321
+ else if (floating.tblpXSpec === "right" || floating.tblpXSpec === "outside") x = defaultContentWidth - tableWidth;
322
+ else x = (defaultContentWidth - tableWidth) / 2;
323
+ else if (tableBlock.justification === "center") x = (defaultContentWidth - tableWidth) / 2;
324
+ else if (tableBlock.justification === "right") x = defaultContentWidth - tableWidth;
325
+ if (x < defaultContentWidth / 2) leftMargin = x + tableWidth + distRight;
326
+ else rightMargin = defaultContentWidth - x + distLeft;
304
327
  const topY = floating.tblpY ?? 0;
305
328
  const bottomY = topY + tableHeight;
306
329
  zones.push({
@@ -311,6 +334,64 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
311
334
  anchorBlockIndex: blockIndex
312
335
  });
313
336
  }
337
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
338
+ const block = blocks[blockIndex];
339
+ if (block.kind !== "textBox") continue;
340
+ const tb = block;
341
+ if (!floatingTextBoxWrapsText(tb) || tb.position === void 0) continue;
342
+ const measure = measureTextBoxBlock(tb);
343
+ const blockMarginTop = perBlockNumberValue(marginTop, blockIndex, defaultMarginTop);
344
+ const blockMarginLeft = perBlockNumberValue(marginLeftInput, blockIndex, defaultMarginLeft);
345
+ const blockMarginRight = perBlockNumberValue(marginRightInput, blockIndex, defaultMarginRight);
346
+ const blockPageWidth = perBlockNumberValue(pageWidthInput, blockIndex, defaultPageWidth);
347
+ const blockContentWidth = perBlockNumberValue(contentWidth, blockIndex, defaultContentWidth);
348
+ const vertical = tb.position.vertical;
349
+ const pageFrameRelative = isPageFrameRelativeAnchor(vertical?.relativeTo);
350
+ const topY = pageFrameRelative ? bandTopContentY(vertical, {
351
+ pageHeight: perBlockNumberValue(pageHeightInput, blockIndex, defaultPageHeight),
352
+ marginTop: blockMarginTop,
353
+ marginBottom: perBlockNumberValue(marginBottomInput, blockIndex, defaultMarginBottom),
354
+ boxHeight: measure.height
355
+ }) : emuToPixels(vertical?.posOffset ?? 0);
356
+ const groupId = getTextBoxGroupId(tb);
357
+ const anchorBlockIndex = groupId ? textBoxGroupAnchors.get(groupId) ?? blockIndex : blockIndex;
358
+ if (groupId && !textBoxGroupAnchors.has(groupId)) textBoxGroupAnchors.set(groupId, blockIndex);
359
+ if (isParagraphFrameTextBox(tb) && groupId !== void 0 && (paragraphFrameGroupSizes.get(groupId) ?? 0) > 1) {
360
+ zones.push({
361
+ leftMargin: 0,
362
+ rightMargin: 0,
363
+ topY: 0,
364
+ bottomY: topY + measure.height + (tb.distBottom ?? 0),
365
+ anchorBlockIndex,
366
+ ...pageFrameRelative ? { isMarginRelative: true } : {},
367
+ fullWidthBlock: true
368
+ });
369
+ continue;
370
+ }
371
+ const horizontal = tb.position.horizontal;
372
+ const contentX = (horizontal ? bandFragmentX(horizontal, {
373
+ pageWidth: blockPageWidth,
374
+ marginLeft: blockMarginLeft,
375
+ marginRight: blockMarginRight,
376
+ boxWidth: measure.width
377
+ }) : blockMarginLeft) - blockMarginLeft;
378
+ const wrapSide = textBoxWrapSide({
379
+ box: tb,
380
+ contentX,
381
+ boxWidth: measure.width,
382
+ contentWidth: blockContentWidth
383
+ });
384
+ const leftMargin = wrapSide === "right" ? contentX + measure.width + (tb.distRight ?? 12) : 0;
385
+ const rightMargin = wrapSide === "left" ? blockContentWidth - contentX + (tb.distLeft ?? 12) : 0;
386
+ zones.push({
387
+ leftMargin: Math.max(0, leftMargin),
388
+ rightMargin: Math.max(0, rightMargin),
389
+ topY: topY - (tb.distTop ?? 0),
390
+ bottomY: topY + measure.height + (tb.distBottom ?? 0),
391
+ anchorBlockIndex,
392
+ ...pageFrameRelative ? { isMarginRelative: true } : {}
393
+ });
394
+ }
314
395
  for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
315
396
  const block = blocks[blockIndex];
316
397
  if (block.kind !== "textBox") continue;
@@ -389,8 +470,8 @@ function measureTextBoxBlock(tb, fieldValues) {
389
470
  const margins = tb.margins ?? DEFAULT_TEXTBOX_MARGINS;
390
471
  const innerWidth = tb.width - margins.left - margins.right;
391
472
  const innerMeasures = tb.content.map((p) => measureParagraph(p, innerWidth, fieldValues ? { fieldValues } : void 0));
392
- const contentHeight = innerMeasures.reduce((sum, m) => sum + m.totalHeight, 0);
393
- const totalHeight = tb.height ?? contentHeight + margins.top + margins.bottom;
473
+ const contentBoxHeight = layoutTextBoxParagraphs(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
474
+ const totalHeight = tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight;
394
475
  return {
395
476
  kind: "textBox",
396
477
  width: tb.width,
@@ -418,7 +499,7 @@ function isNextPageSectionBreak(block) {
418
499
  */
419
500
  function measureBlocks(blocks, contentWidth, marginTop = 0, pageGeometry, fieldValues) {
420
501
  const defaultWidth = Array.isArray(contentWidth) ? contentWidth[0] ?? 0 : contentWidth;
421
- const floatingZonesWithAnchors = extractFloatingZones(blocks, defaultWidth, marginTop, pageGeometry);
502
+ const floatingZonesWithAnchors = extractFloatingZones(blocks, contentWidth, marginTop, pageGeometry);
422
503
  const marginRelative = floatingZonesWithAnchors.filter((z) => z.isMarginRelative && !z.fullWidthBlock);
423
504
  const ownAnchorZones = floatingZonesWithAnchors.filter((z) => !z.isMarginRelative || z.fullWidthBlock);
424
505
  const marginByTopY = /* @__PURE__ */ new Map();
@@ -461,10 +542,11 @@ function measureBlocks(blocks, contentWidth, marginTop = 0, pageGeometry, fieldV
461
542
  try {
462
543
  const blockWidth = Array.isArray(contentWidth) ? contentWidth[blockIndex] ?? defaultWidth : contentWidth;
463
544
  const measure = measureBlock(block, blockWidth, zones, cumulativeY, fieldValues);
464
- const bandZones = zones?.filter((zone) => zone.fullWidthBlock);
465
- if (bandZones?.length && (measure.kind === "image" || measure.kind === "table" && !block.floating)) {
545
+ const clearingZones = measure.kind === "table" ? zones : zones?.filter((zone) => zone.fullWidthBlock);
546
+ if (clearingZones?.length && (measure.kind === "image" || measure.kind === "table" && !block.floating)) {
466
547
  const blockHeight = measure.kind === "image" ? measure.height : measure.totalHeight;
467
- const skip = findClearLineY(cumulativeY, blockHeight, bandZones, blockWidth, 24) - cumulativeY;
548
+ const requiredWidth = measure.kind === "table" ? Math.min(blockWidth, measure.totalWidth) : 24;
549
+ const skip = findClearLineY(cumulativeY, blockHeight, clearingZones, blockWidth, Math.max(24, requiredWidth)) - cumulativeY;
468
550
  if (skip > 0) {
469
551
  measure.bandSkipBefore = skip;
470
552
  cumulativeY += skip;
@@ -22,8 +22,9 @@ const DEFAULT_FONT_FAMILY = "Calibri";
22
22
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
23
23
  const WIDTH_TOLERANCE = .5;
24
24
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
25
+ const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
25
26
  const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
26
- const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025;
27
+ const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .02;
27
28
  const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
28
29
  const DEFAULT_LIST_HANGING_INDENT_PX = 24;
29
30
  const ALL_CAPS_RATIO_THRESHOLD = .8;
@@ -245,6 +246,7 @@ function isEmptyTextRun(run) {
245
246
  * preceding a floating image.
246
247
  */
247
248
  function isBlockLayoutImageRun(run) {
249
+ if (isFloatingImageRun(run)) return false;
248
250
  return run.wrapType === "topAndBottom" || run.displayMode === "block";
249
251
  }
250
252
  function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
@@ -329,20 +331,34 @@ function uppercaseLetterRatio(text) {
329
331
  }
330
332
  return letters === 0 ? 0 : uppercase / letters;
331
333
  }
334
+ function fixedSpaceAdjustedShrinkTolerance({ tolerance, regularSpaceCount, nonBreakingSpaceCount }) {
335
+ const totalSpaces = regularSpaceCount + nonBreakingSpaceCount;
336
+ if (totalSpaces === 0) return tolerance;
337
+ return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, tolerance * (regularSpaceCount / totalSpaces));
338
+ }
332
339
  function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) {
333
340
  if (block.attrs?.listMarker !== void 0) {
334
- if ((block.attrs.indent?.hanging ?? 0) <= DEFAULT_LIST_HANGING_INDENT_PX) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
335
- if (isFirstLine) return JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO;
336
- const totalSpaces = regularSpaceCount + nonBreakingSpaceCount;
337
- if (totalSpaces === 0) return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO;
338
- return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO * (regularSpaceCount / totalSpaces));
341
+ const hanging = block.attrs.indent?.hanging ?? 0;
342
+ if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? JUSTIFY_SHRINK_TOLERANCE_RATIO : JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO;
343
+ const left = block.attrs.indent?.left ?? 0;
344
+ if (hanging > 0 && left > hanging) return JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO;
345
+ return fixedSpaceAdjustedShrinkTolerance({
346
+ tolerance: JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO,
347
+ regularSpaceCount,
348
+ nonBreakingSpaceCount
349
+ });
339
350
  }
340
351
  const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
341
352
  const hasTabRuns = block.runs.some(isTabRun);
353
+ if (isFirstLine && hasTabRuns && (block.attrs?.indent?.hanging ?? 0) > 0) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
342
354
  if (!isFirstLine && hasTabRuns && !hasTabStops) return JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO;
343
- if ((isFirstLine || hasTabStops) && (hasTabStops || hasTabRuns)) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO;
355
+ if (hasTabRuns && (isFirstLine || hasTabStops)) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO;
344
356
  if (uppercaseLetterRatio(block.runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) > ALL_CAPS_RATIO_THRESHOLD) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
345
- return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO;
357
+ return fixedSpaceAdjustedShrinkTolerance({
358
+ tolerance: JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO,
359
+ regularSpaceCount,
360
+ nonBreakingSpaceCount
361
+ });
346
362
  }
347
363
  function trimTrailingSpacesAndTabs(text) {
348
364
  let end = text.length;
@@ -479,15 +495,17 @@ function measureParagraph(block, maxWidth, options) {
479
495
  };
480
496
  }
481
497
  const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
498
+ const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
482
499
  lines.push({
483
500
  fromRun: 0,
484
501
  fromChar: 0,
485
502
  toRun: 0,
486
503
  toChar: 0,
487
504
  width: 0,
488
- ...emptyMetrics
505
+ ...emptyMetrics,
506
+ lineHeight: outlineLineHeight
489
507
  });
490
- let totalHeight = emptyMetrics.lineHeight;
508
+ let totalHeight = outlineLineHeight;
491
509
  if (spacing?.before) totalHeight += spacing.before;
492
510
  if (spacing?.after) totalHeight += spacing.after;
493
511
  return {
@@ -536,7 +554,14 @@ function measureParagraph(block, maxWidth, options) {
536
554
  ...firstLineFloatingMargins.segments?.length ? { segmentZones: firstLineFloatingMargins.segments } : {}
537
555
  };
538
556
  const calculateLineTypography = (line) => {
539
- const finalTypography = { ...calculateTypographyMetrics(line.maxFontSize, spacing, line.maxFontMetrics) };
557
+ const paragraphFontSize = attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE;
558
+ const paragraphFontFamily = attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY;
559
+ const fontSize = line.maxFontMetrics ? line.maxFontSize : paragraphFontSize;
560
+ const metrics = line.maxFontMetrics ?? getFontMetrics({
561
+ fontSize: paragraphFontSize,
562
+ fontFamily: paragraphFontFamily
563
+ });
564
+ const finalTypography = { ...calculateTypographyMetrics(fontSize, spacing, metrics) };
540
565
  const inlineObjectHeight = Math.max(line.maxImageHeightPx, line.maxMathHeightPx);
541
566
  if (inlineObjectHeight > finalTypography.lineHeight) {
542
567
  const objectHeight = inlineObjectHeight;
@@ -651,8 +676,9 @@ function measureParagraph(block, maxWidth, options) {
651
676
  decimalPrefixWidth
652
677
  });
653
678
  let tabWidth = tabResult.width;
679
+ const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= WIDTH_TOLERANCE;
654
680
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
655
- if (!hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
681
+ if (!landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
656
682
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
657
683
  startNewLine(runIndex, 0);
658
684
  updateMaxFont(style);
@@ -665,7 +691,7 @@ function measureParagraph(block, maxWidth, options) {
665
691
  }
666
692
  if (isImageRun(run)) {
667
693
  const wrapType = run.wrapType;
668
- if (run.displayMode === "float" || wrapType === "square" || wrapType === "tight" || wrapType === "through" || wrapType === "behind" || wrapType === "inFront") {
694
+ if (isFloatingImageRun(run)) {
669
695
  currentLine.toRun = runIndex;
670
696
  currentLine.toChar = 1;
671
697
  continue;
@@ -794,12 +820,13 @@ function measureParagraph(block, maxWidth, options) {
794
820
  }
795
821
  const rawGlueWidth = nextBreak === text.length && word.length > 0 && !isBreakChar(word[word.length - 1]) ? trailingGlueWidths[runIndex] ?? 0 : 0;
796
822
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
797
- if (currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance) {
823
+ if (wordWidth > 0 && currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance) {
798
824
  startNewLine(runIndex, charIndex);
799
825
  updateMaxFont(lineHeightStyle);
800
826
  }
801
827
  currentLine.width += fullWordWidth;
802
- currentLine.trailingWhitespaceWidth = fullWordWidth - wordWidth;
828
+ const wordTrailingWhitespaceWidth = fullWordWidth - wordWidth;
829
+ currentLine.trailingWhitespaceWidth = wordWidth === 0 ? currentLine.trailingWhitespaceWidth + wordTrailingWhitespaceWidth : wordTrailingWhitespaceWidth;
803
830
  currentLine.regularSpaceCount += word.split(" ").length - 1;
804
831
  currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
805
832
  currentLine.toRun = runIndex;
@@ -0,0 +1,15 @@
1
+ import { TableCell, TableRow } from "../types.js";
2
+
3
+ //#region src/layout-engine/measure/tableCellGrid.d.ts
4
+ type TableCellGrid = {
5
+ occupiedColumnsByRow: ReadonlyMap<number, ReadonlySet<number>>;
6
+ sourceCellsByRow: ReadonlyMap<number, ReadonlyMap<number, TableCell>>;
7
+ sourceColumnsByCell: ReadonlyMap<TableCell, number>;
8
+ };
9
+ declare const buildTableCellGrid: (rows: readonly TableRow[], columnCount: number) => TableCellGrid;
10
+ declare const getFirstAvailableColumn: (grid: TableCellGrid, rowIndex: number, startingColumn: number) => number;
11
+ declare const getSourceCellAt: (grid: TableCellGrid, rowIndex: number, columnIndex: number) => TableCell | undefined;
12
+ declare const getSourceCellColumn: (grid: TableCellGrid, cell: TableCell) => number | undefined;
13
+ declare const getTableCellVerticalBorderHeight: (grid: TableCellGrid, cell: TableCell | undefined, rowIndex: number) => number;
14
+ //#endregion
15
+ export { TableCellGrid, buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
@@ -0,0 +1,62 @@
1
+ //#region src/layout-engine/measure/tableCellGrid.ts
2
+ const buildTableCellGrid = (rows, columnCount) => {
3
+ const occupiedColumnsByRow = /* @__PURE__ */ new Map();
4
+ const sourceCellsByRow = /* @__PURE__ */ new Map();
5
+ const sourceColumnsByCell = /* @__PURE__ */ new Map();
6
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
7
+ const row = rows[rowIndex];
8
+ if (!row) continue;
9
+ let columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), row.gridBefore ?? 0);
10
+ for (const cell of row.cells) {
11
+ const columnSpan = cell.colSpan ?? 1;
12
+ const rowSpan = cell.rowSpan ?? 1;
13
+ sourceColumnsByCell.set(cell, columnIndex);
14
+ const rowEnd = Math.min(rows.length, rowIndex + rowSpan);
15
+ const columnEnd = Math.min(columnCount, columnIndex + columnSpan);
16
+ for (let gridRowIndex = rowIndex; gridRowIndex < rowEnd; gridRowIndex++) {
17
+ const cellsByColumn = getOrCreateCellRow(sourceCellsByRow, gridRowIndex);
18
+ for (let gridColumnIndex = columnIndex; gridColumnIndex < columnEnd; gridColumnIndex++) cellsByColumn.set(gridColumnIndex, cell);
19
+ }
20
+ if (rowSpan > 1) for (let spannedRowIndex = rowIndex + 1; spannedRowIndex < rowIndex + rowSpan; spannedRowIndex++) {
21
+ const occupied = getOrCreateOccupiedRow(occupiedColumnsByRow, spannedRowIndex);
22
+ for (let columnOffset = 0; columnOffset < columnSpan; columnOffset++) occupied.add(columnIndex + columnOffset);
23
+ }
24
+ columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), columnIndex + columnSpan);
25
+ }
26
+ }
27
+ return {
28
+ occupiedColumnsByRow,
29
+ sourceCellsByRow,
30
+ sourceColumnsByCell
31
+ };
32
+ };
33
+ const getFirstAvailableColumn = (grid, rowIndex, startingColumn) => firstAvailableColumn(grid.occupiedColumnsByRow.get(rowIndex), startingColumn);
34
+ const getSourceCellAt = (grid, rowIndex, columnIndex) => grid.sourceCellsByRow.get(rowIndex)?.get(columnIndex);
35
+ const getSourceCellColumn = (grid, cell) => grid.sourceColumnsByCell.get(cell);
36
+ const getTableCellVerticalBorderHeight = (grid, cell, rowIndex) => {
37
+ const sourceColumn = cell ? getSourceCellColumn(grid, cell) : void 0;
38
+ const aboveBottom = (sourceColumn === void 0 ? void 0 : getSourceCellAt(grid, rowIndex - 1, sourceColumn))?.borders?.bottom;
39
+ const aboveOwnsEdge = aboveBottom !== void 0 && aboveBottom.width !== 0 && aboveBottom.style !== "none" && aboveBottom.style !== "nil";
40
+ return (rowIndex === 0 || !aboveOwnsEdge ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
41
+ };
42
+ const firstAvailableColumn = (occupiedColumns, startingColumn) => {
43
+ let columnIndex = startingColumn;
44
+ while (occupiedColumns?.has(columnIndex)) columnIndex++;
45
+ return columnIndex;
46
+ };
47
+ const getOrCreateCellRow = (sourceCellsByRow, rowIndex) => {
48
+ const existing = sourceCellsByRow.get(rowIndex);
49
+ if (existing !== void 0) return existing;
50
+ const cellsByColumn = /* @__PURE__ */ new Map();
51
+ sourceCellsByRow.set(rowIndex, cellsByColumn);
52
+ return cellsByColumn;
53
+ };
54
+ const getOrCreateOccupiedRow = (occupiedColumnsByRow, rowIndex) => {
55
+ const existing = occupiedColumnsByRow.get(rowIndex);
56
+ if (existing !== void 0) return existing;
57
+ const occupiedColumns = /* @__PURE__ */ new Set();
58
+ occupiedColumnsByRow.set(rowIndex, occupiedColumns);
59
+ return occupiedColumns;
60
+ };
61
+ //#endregion
62
+ export { buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
@@ -0,0 +1,18 @@
1
+ import { ParagraphBlock, ParagraphMeasure } from "../types.js";
2
+
3
+ //#region src/layout-engine/measure/textBoxParagraphLayout.d.ts
4
+ type TextBoxParagraphPlacement = {
5
+ leadingSpacing: number;
6
+ contentHeight: number;
7
+ };
8
+ type TextBoxParagraphLayout = {
9
+ placements: TextBoxParagraphPlacement[];
10
+ totalHeight: number;
11
+ };
12
+ /**
13
+ * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
14
+ * before/after spacing collapses to the larger value, matching body flow.
15
+ */
16
+ declare function layoutTextBoxParagraphs(blocks: readonly ParagraphBlock[], measures: readonly ParagraphMeasure[]): TextBoxParagraphLayout;
17
+ //#endregion
18
+ export { TextBoxParagraphLayout, TextBoxParagraphPlacement, layoutTextBoxParagraphs };
@@ -0,0 +1,32 @@
1
+ import { measuredLineRangeHeight } from "../lineFlow.js";
2
+ import { getParagraphSpacingAfter, getParagraphSpacingBefore } from "../paragraphSpacing.js";
3
+ import { panic } from "better-result";
4
+ //#region src/layout-engine/measure/textBoxParagraphLayout.ts
5
+ /**
6
+ * Lay out a text box's paragraph story as one unpaginated flow. Adjacent
7
+ * before/after spacing collapses to the larger value, matching body flow.
8
+ */
9
+ function layoutTextBoxParagraphs(blocks, measures) {
10
+ if (blocks.length !== measures.length) panic("layoutTextBoxParagraphs: block and measure counts must match");
11
+ const placements = [];
12
+ let totalHeight = 0;
13
+ let trailingSpacing = 0;
14
+ for (let index = 0; index < blocks.length; index += 1) {
15
+ const block = blocks[index];
16
+ const measure = measures[index];
17
+ const leadingSpacing = Math.max(getParagraphSpacingBefore(block), trailingSpacing);
18
+ const contentHeight = measuredLineRangeHeight(measure.lines, 0, measure.lines.length);
19
+ placements.push({
20
+ leadingSpacing,
21
+ contentHeight
22
+ });
23
+ totalHeight += leadingSpacing + contentHeight;
24
+ trailingSpacing = getParagraphSpacingAfter(block);
25
+ }
26
+ return {
27
+ placements,
28
+ totalHeight: totalHeight + trailingSpacing
29
+ };
30
+ }
31
+ //#endregion
32
+ export { layoutTextBoxParagraphs };
@@ -35,7 +35,8 @@ type PaginatorOptions = {
35
35
  * its title page (typically extended top margin to clear an overflowing
36
36
  * first-page header) vs. its body pages. Pages 2+ use `margins`.
37
37
  */
38
- firstPageMargins?: PageMargins; /** Column configuration (optional). */
38
+ firstPageMargins?: PageMargins; /** Per-section body margins used on even section pages. */
39
+ sectionEvenPageMargins?: (PageMargins | undefined)[]; /** Column configuration (optional). */
39
40
  columns?: ColumnLayout; /** Per-page footnote reserved heights (pageNumber → height in pixels). */
40
41
  footnoteReservedHeights?: Map<number, number>; /** Header/footer refs by section index. */
41
42
  sectionHeaderFooterRefs?: PageHeaderFooterRefs[]; /** Callback when a new page is created. */