@stll/folio-core 0.7.0 → 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 (92) 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 +76 -9
  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/numberingParser.js +1 -0
  24. package/dist/docx/paragraphParser.js +9 -0
  25. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  26. package/dist/docx/parser.js +2 -0
  27. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  28. package/dist/docx/selectiveXmlPatch.js +1 -1
  29. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  30. package/dist/docx/serializer/runSerializer.js +7 -2
  31. package/dist/docx/serializer/tableSerializer.js +4 -0
  32. package/dist/docx/tableParser.js +8 -0
  33. package/dist/docx/textBoxParser.js +6 -1
  34. package/dist/docx/vmlImageParser.js +145 -1
  35. package/dist/index.d.ts +5 -2
  36. package/dist/index.js +4 -1
  37. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  38. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  39. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  40. package/dist/layout-bridge/convert/toFlowBlocks.js +42 -3
  41. package/dist/layout-bridge/engine/hitTest.js +2 -1
  42. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  43. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  44. package/dist/layout-engine/headerFooterRefs.js +40 -0
  45. package/dist/layout-engine/index.d.ts +3 -2
  46. package/dist/layout-engine/index.js +36 -82
  47. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  48. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  49. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  50. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  51. package/dist/layout-engine/measure/measureParagraph.js +13 -4
  52. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  53. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  54. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  55. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  56. package/dist/layout-engine/paginator.d.ts +2 -1
  57. package/dist/layout-engine/paginator.js +7 -6
  58. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  59. package/dist/layout-engine/paragraphFrame.js +17 -0
  60. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  61. package/dist/layout-engine/paragraphSequence.js +25 -0
  62. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  63. package/dist/layout-engine/paragraphSpacing.js +30 -0
  64. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  65. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  66. package/dist/layout-engine/types.d.ts +17 -8
  67. package/dist/layout-engine/types.js +15 -2
  68. package/dist/layout-painter/renderPage.js +2 -2
  69. package/dist/layout-painter/renderParagraph.js +6 -5
  70. package/dist/layout-painter/renderTable.js +42 -12
  71. package/dist/layout-painter/renderTextBox.js +17 -7
  72. package/dist/managers/DocumentLoaderManager.js +1 -1
  73. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  74. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  75. package/dist/paged-layout/sectionGeometry.js +1 -1
  76. package/dist/prosemirror/attrs/index.js +11 -0
  77. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  78. package/dist/prosemirror/conversion/toProseDoc.js +27 -23
  79. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  80. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  81. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  82. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  83. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  84. package/dist/prosemirror/schema/nodes.d.ts +4 -2
  85. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  86. package/dist/server.d.ts +4 -2
  87. package/dist/server.js +3 -1
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/utils/createDocument.js +26 -18
  90. package/dist/utils/hexId.d.ts +8 -1
  91. package/dist/utils/hexId.js +11 -3
  92. package/package.json +1 -1
@@ -1,11 +1,15 @@
1
1
  import { emuToPixels } from "../utils/units.js";
2
2
  import { measuredLineAdvance } from "./lineFlow.js";
3
- import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
3
+ import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
4
+ import { getParagraphSpacingAfter, getParagraphSpacingBefore } from "./paragraphSpacing.js";
4
5
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
6
+ import { continuesNumberedSequence } from "./paragraphSequence.js";
5
7
  import { createPaginator } from "./paginator.js";
6
8
  import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
9
+ import { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
7
10
  import { buildTableRowBreakInfo, snapRowBreak } from "./tableRowBreak.js";
8
11
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
12
+ import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
9
13
  import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
10
14
  import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
11
15
  import { panic } from "better-result";
@@ -42,63 +46,6 @@ function collectSectionConfigs(blocks, initialConfig, finalConfig) {
42
46
  breakIndices
43
47
  };
44
48
  }
45
- /**
46
- * Whether a paragraph block has no visible content (no runs, or a single
47
- * empty text run).
48
- */
49
- function isEmptyParagraph(block) {
50
- if (block.runs.length === 0) return true;
51
- if (block.runs.length !== 1) return false;
52
- const r = block.runs[0];
53
- return r?.kind === "text" && r.text === "";
54
- }
55
- function isPaginationEmptyParagraph(block) {
56
- return block.runs.every((run) => {
57
- if (run.kind === "text") return run.text.trim().length === 0;
58
- return run.kind === "tab" || run.kind === "lineBreak";
59
- });
60
- }
61
- function pageHasVisibleBodyContent(page, blocksById) {
62
- for (const fragment of page.fragments) {
63
- if (fragment.kind !== "paragraph") return true;
64
- const block = blocksById.get(String(fragment.blockId));
65
- if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true;
66
- }
67
- return false;
68
- }
69
- function continuesNumberedSequence(previous, current) {
70
- if (previous?.kind !== "paragraph" || current.kind !== "paragraph") return false;
71
- const previousNumPr = previous.attrs?.numPr;
72
- const currentNumPr = current.attrs?.numPr;
73
- if (previousNumPr?.numId === void 0 || currentNumPr?.numId === void 0) return false;
74
- return previousNumPr.numId === currentNumPr.numId && previousNumPr.ilvl === currentNumPr.ilvl;
75
- }
76
- function pageStartsWithPreviousParagraphContinuation(page, previousBlock) {
77
- if (previousBlock?.kind !== "paragraph") return false;
78
- return page.fragments.some((fragment) => fragment.kind === "paragraph" && String(fragment.blockId) === String(previousBlock.id) && fragment.continuesFromPrev === true);
79
- }
80
- /**
81
- * Get spacing before a paragraph block. Empty paragraphs whose
82
- * `before` came only from the implicit default paragraph style collapse to
83
- * zero. Reference layout keeps inherited spacing when the empty paragraph itself is
84
- * authored through direct `w:pPr` formatting or an explicit `w:pStyle`.
85
- */
86
- function getSpacingBefore(block) {
87
- const value = block.attrs?.spacing?.before ?? 0;
88
- if (value === 0) return 0;
89
- if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.spacingExplicit?.before) return 0;
90
- return value;
91
- }
92
- /**
93
- * Get spacing after a paragraph block. Same implicit-default-style collapse
94
- * rule as `getSpacingBefore`.
95
- */
96
- function getSpacingAfter(block) {
97
- const value = block.attrs?.spacing?.after ?? 0;
98
- if (value === 0) return 0;
99
- if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.hasDirectParagraphFormatting && !block.attrs?.spacingExplicit?.after) return 0;
100
- return value;
101
- }
102
49
  function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex, incomingSpacing, columnCount, availableHeight }) {
103
50
  if (columnCount <= 1 || startIndex >= endIndex || availableHeight <= 0) return;
104
51
  const lineUnits = [];
@@ -110,7 +57,7 @@ function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex
110
57
  const measure = measures[index];
111
58
  if (block?.kind !== "paragraph" || measure?.kind !== "paragraph") return;
112
59
  if (block.attrs?.keepNext === true || block.attrs?.keepLines === true || block.runs.some((run) => run.kind === "text" && run.footnoteRefId !== void 0)) return;
113
- const leadingSpacing = Math.max(getSpacingBefore(block), trailingSpacing);
60
+ const leadingSpacing = Math.max(getParagraphSpacingBefore(block), trailingSpacing);
114
61
  if (measure.lines.length === 0) {
115
62
  if (leadingSpacing > 0) {
116
63
  lineUnits.push(leadingSpacing);
@@ -127,7 +74,7 @@ function balancedParagraphSectionHeight({ blocks, measures, startIndex, endIndex
127
74
  tallestUnit = Math.max(tallestUnit, unitHeight);
128
75
  }
129
76
  if (tallestUnit > availableHeight || totalHeight > availableHeight * columnCount) return;
130
- trailingSpacing = getSpacingAfter(block);
77
+ trailingSpacing = getParagraphSpacingAfter(block);
131
78
  }
132
79
  if (totalHeight <= 0) return;
133
80
  const columnsNeeded = (targetHeight) => {
@@ -243,6 +190,7 @@ function layoutDocument(blocks, measures, options) {
243
190
  margins: initialConfig.margins,
244
191
  ...options.mirrorMargins === true ? { mirrorMargins: true } : {},
245
192
  ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
193
+ ...options.sectionEvenPageMargins !== void 0 ? { sectionEvenPageMargins: options.sectionEvenPageMargins } : {},
246
194
  columns: initialConfig.columns ?? DEFAULT_COLUMNS,
247
195
  ...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
248
196
  ...options.sectionHeaderFooterRefs !== void 0 ? { sectionHeaderFooterRefs: options.sectionHeaderFooterRefs } : {}
@@ -257,23 +205,28 @@ function layoutDocument(blocks, measures, options) {
257
205
  let activeSectionMarginTop = initialConfig.margins.top;
258
206
  let activeSectionPageHeight = initialConfig.pageSize.h;
259
207
  let activeSectionMarginBottom = initialConfig.margins.bottom;
260
- let naturalPageAdvanceSinceRenderedBreak = "none";
208
+ let renderedBreakState = INITIAL_RENDERED_BREAK_STATE;
261
209
  for (let i = 0; i < blocks.length; i++) {
262
210
  const block = blocks[i];
263
211
  const measure = measures[i];
264
- const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true;
265
- if (hasPageBreakBefore(block)) paginator.forcePageBreak();
266
- else if (hasRenderedPageBreak) {
267
- const state = paginator.getCurrentState();
268
- if (!(pageStartsWithPreviousParagraphContinuation(state.page, blocks[i - 1]) || naturalPageAdvanceSinceRenderedBreak === "reflowBoundary" || block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak !== "none" || naturalPageAdvanceSinceRenderedBreak !== "none" && continuesNumberedSequence(blocks[i - 1], block)) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
269
- }
270
- if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = "none";
212
+ const renderedBreakNeedsSnap = measure.kind === "paragraph" && !paginator.fits(measure.totalHeight);
213
+ const breakDecision = reconcileBreakBeforeBlock({
214
+ state: renderedBreakState,
215
+ block,
216
+ previousBlock: blocks[i - 1],
217
+ page: paginator.getCurrentState().page,
218
+ blocksById,
219
+ hasExplicitPageBreak: hasPageBreakBefore(block),
220
+ renderedBreakNeedsSnap
221
+ });
222
+ if (breakDecision.forcePageBreak) paginator.forcePageBreak();
223
+ renderedBreakState = breakDecision.state;
271
224
  const chain = keepNextChains.get(i);
272
225
  if (chain && !midChainIndices.has(i)) {
273
226
  const chainHeight = calculateChainHeight(chain, blocks, measures);
274
227
  const pageBeforeChainLayout = paginator.getCurrentState().page.number;
275
228
  paginator.ensureFits(chainHeight);
276
- if (paginator.getCurrentState().page.number > pageBeforeChainLayout) naturalPageAdvanceSinceRenderedBreak = "reflowBoundary";
229
+ if (paginator.getCurrentState().page.number > pageBeforeChainLayout) renderedBreakState = recordReflowBoundary(renderedBreakState, true);
277
230
  }
278
231
  const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
279
232
  switch (block.kind) {
@@ -329,9 +282,13 @@ function layoutDocument(blocks, measures, options) {
329
282
  }
330
283
  default: break;
331
284
  }
332
- const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak";
333
- if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = "none";
334
- else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = paginator.pages[pageBeforeBlockLayout - 1]?.fragments.some(({ blockId }) => blockId === block.id) ? "reflowBoundary" : "ordinary";
285
+ renderedBreakState = reconcileAfterBlock({
286
+ state: renderedBreakState,
287
+ block,
288
+ pageNumberBefore: pageBeforeBlockLayout,
289
+ pageNumberAfter: paginator.getCurrentState().page.number,
290
+ previousPage: paginator.pages[pageBeforeBlockLayout - 1]
291
+ });
335
292
  }
336
293
  if (paginator.pages.length === 0) paginator.getCurrentState();
337
294
  return {
@@ -387,8 +344,8 @@ function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
387
344
  function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeightById) {
388
345
  const lines = measure.lines;
389
346
  if (lines.length === 0) {
390
- const spaceBefore = getSpacingBefore(block);
391
- const spaceAfter = getSpacingAfter(block);
347
+ const spaceBefore = getParagraphSpacingBefore(block);
348
+ const spaceAfter = getParagraphSpacingAfter(block);
392
349
  const state = paginator.getCurrentState();
393
350
  const fragment = {
394
351
  kind: "paragraph",
@@ -406,8 +363,8 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
406
363
  paginator.addFragment(fragment, 0, spaceBefore, spaceAfter);
407
364
  return;
408
365
  }
409
- const spaceBefore = getSpacingBefore(block);
410
- const spaceAfter = getSpacingAfter(block);
366
+ const spaceBefore = getParagraphSpacingBefore(block);
367
+ const spaceAfter = getParagraphSpacingAfter(block);
411
368
  let currentLineIndex = 0;
412
369
  while (currentLineIndex < lines.length) {
413
370
  const state = paginator.getCurrentState();
@@ -534,7 +491,6 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
534
491
  return x;
535
492
  };
536
493
  const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
537
- const getCurrentAvailableHeight = (state = paginator.getCurrentState()) => state.contentBottom - state.cursorY;
538
494
  const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
539
495
  const previous = state.page.fragments.at(-1);
540
496
  return previous?.kind === "table" && previous.blockId === block.id && previous.toRow === rowIndex && previous.y + previous.height === state.cursorY && previous.x === computeTableX(state.columnIndex);
@@ -543,11 +499,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
543
499
  const canSplitRow = (rowIndex, state = paginator.getCurrentState()) => {
544
500
  const row = rows[rowIndex];
545
501
  if (!row) return false;
546
- const repeatedHeaderOverhead = shouldRepeatHeaderRows(rowIndex, 0, state) ? headerRowsHeight : 0;
547
502
  if ((breakInfo.breakOffsets[rowIndex]?.length ?? 0) <= 1) return false;
548
- const requiredHeight = row.height + repeatedHeaderOverhead;
549
- if (requiredHeight > getCurrentRowCapacity(state)) return true;
550
- return hasAdjacentPriorTableRows(rowIndex, state) && requiredHeight > getCurrentAvailableHeight(state) - state.trailingSpacing;
503
+ const freshHeaderOverhead = headerRowCount > 0 && rowIndex >= headerRowCount ? headerRowsHeight : 0;
504
+ return row.height + freshHeaderOverhead > getCurrentRowCapacity(state);
551
505
  };
552
506
  while (currentRowIndex < rows.length) {
553
507
  const oversizedRow = rows[currentRowIndex];
@@ -929,4 +883,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
929
883
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
930
884
  }
931
885
  //#endregion
932
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned };
886
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, scheduleSectionBreak, tableColumnsArePinned };
@@ -36,5 +36,7 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
36
36
  * grid is not erased by custom tabs, just augmented).
37
37
  */
38
38
  declare function getListMarkerInlineWidth(block: ParagraphBlock): number;
39
+ /** Paint-only offset that places the marker around its authored list anchor. */
40
+ declare function getListMarkerVisualOffset(block: ParagraphBlock): number;
39
41
  //#endregion
40
- export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, resolveListMarkerFont };
42
+ export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont };
@@ -51,15 +51,16 @@ function getListMarkerInlineWidth(block) {
51
51
  fontSize
52
52
  };
53
53
  const naturalWidth = measureTextWidth(attrs.listMarker, style);
54
+ const markerEndOffset = getMarkerEndOffset(naturalWidth, attrs.listMarkerAlignment);
54
55
  const suffix = attrs.listMarkerSuffix ?? "tab";
55
- if (suffix === "nothing") return naturalWidth;
56
- if (suffix === "space") return naturalWidth + measureTextWidth(" ", style);
56
+ if (suffix === "nothing") return markerEndOffset;
57
+ if (suffix === "space") return markerEndOffset + measureTextWidth(" ", style);
57
58
  const indent = attrs.indent;
58
59
  const indentLeft = indent?.left ?? 0;
59
60
  const firstLine = indent?.firstLine ?? 0;
60
61
  const hanging = indent?.hanging ?? 0;
61
62
  const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
62
- const minBodyStart = markerStartPx + naturalWidth;
63
+ const minBodyStart = markerStartPx + markerEndOffset;
63
64
  const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
64
65
  if (hanging > 0) customTabs.push(indentLeft);
65
66
  const searchStart = hanging > 0 ? Math.max(minBodyStart, indentLeft) : minBodyStart;
@@ -72,5 +73,23 @@ function getListMarkerInlineWidth(block) {
72
73
  if (bodyStart === void 0) return naturalWidth + ptToPx(fontSize) * .5;
73
74
  return bodyStart - markerStartPx;
74
75
  }
76
+ /** Paint-only offset that places the marker around its authored list anchor. */
77
+ function getListMarkerVisualOffset(block) {
78
+ const attrs = block.attrs;
79
+ if (!attrs?.listMarker || attrs.listMarkerHidden) return 0;
80
+ const { fontFamily, fontSize } = resolveListMarkerFont(block);
81
+ const naturalWidth = measureTextWidth(attrs.listMarker, {
82
+ fontFamily,
83
+ fontSize
84
+ });
85
+ if (attrs.listMarkerAlignment === "right") return -naturalWidth;
86
+ if (attrs.listMarkerAlignment === "center") return -naturalWidth / 2;
87
+ return 0;
88
+ }
89
+ const getMarkerEndOffset = (naturalWidth, alignment) => {
90
+ if (alignment === "right") return 0;
91
+ if (alignment === "center") return naturalWidth / 2;
92
+ return naturalWidth;
93
+ };
75
94
  //#endregion
76
- export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, resolveListMarkerFont };
95
+ export { DEFAULT_TAB_STOP_TWIPS, getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont };
@@ -4,7 +4,10 @@ import { FloatingImageZone } from "./floatingZones.js";
4
4
  declare function measureTableCellBlockVisualHeight(block: FlowBlock, blockMeasure: Measure): number;
5
5
  declare function measureTableBlock(tableBlock: TableBlock, contentWidth: number, fieldValues?: ReadonlyMap<number, string>): TableMeasure;
6
6
  type BandPageGeometry = {
7
+ pageWidth?: number | number[];
7
8
  pageHeight: number | number[];
9
+ marginLeft?: number | number[];
10
+ marginRight?: number | number[];
8
11
  marginBottom: number | number[];
9
12
  };
10
13
  /**
@@ -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) {
@@ -339,7 +341,7 @@ function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonB
339
341
  const hanging = block.attrs.indent?.hanging ?? 0;
340
342
  if (isFirstLine) return hanging <= DEFAULT_LIST_HANGING_INDENT_PX ? JUSTIFY_SHRINK_TOLERANCE_RATIO : JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO;
341
343
  const left = block.attrs.indent?.left ?? 0;
342
- if (hanging > 0 && left > hanging) return JUSTIFY_SHRINK_TOLERANCE_RATIO;
344
+ if (hanging > 0 && left > hanging) return JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO;
343
345
  return fixedSpaceAdjustedShrinkTolerance({
344
346
  tolerance: JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO,
345
347
  regularSpaceCount,
@@ -552,7 +554,14 @@ function measureParagraph(block, maxWidth, options) {
552
554
  ...firstLineFloatingMargins.segments?.length ? { segmentZones: firstLineFloatingMargins.segments } : {}
553
555
  };
554
556
  const calculateLineTypography = (line) => {
555
- 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) };
556
565
  const inlineObjectHeight = Math.max(line.maxImageHeightPx, line.maxMathHeightPx);
557
566
  if (inlineObjectHeight > finalTypography.lineHeight) {
558
567
  const objectHeight = inlineObjectHeight;
@@ -682,7 +691,7 @@ function measureParagraph(block, maxWidth, options) {
682
691
  }
683
692
  if (isImageRun(run)) {
684
693
  const wrapType = run.wrapType;
685
- if (run.displayMode === "float" || wrapType === "square" || wrapType === "tight" || wrapType === "through" || wrapType === "behind" || wrapType === "inFront") {
694
+ if (isFloatingImageRun(run)) {
686
695
  currentLine.toRun = runIndex;
687
696
  currentLine.toChar = 1;
688
697
  continue;