@stll/folio-core 0.3.1 → 0.5.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 (120) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +35 -5
  6. package/dist/ai-edits/headless.js +92 -22
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -115
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.d.ts +6 -0
  22. package/dist/docx/encryption/agileDecryption.js +209 -0
  23. package/dist/docx/encryption/compoundFile.d.ts +20 -0
  24. package/dist/docx/encryption/compoundFile.js +251 -0
  25. package/dist/docx/encryption/containerFormat.d.ts +15 -0
  26. package/dist/docx/encryption/containerFormat.js +40 -0
  27. package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
  28. package/dist/docx/encryption/cryptoBytes.js +55 -0
  29. package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
  30. package/dist/docx/encryption/encryptionInfo.js +128 -0
  31. package/dist/docx/encryption/errors.d.ts +18 -0
  32. package/dist/docx/encryption/errors.js +13 -0
  33. package/dist/docx/encryption/index.d.ts +4 -0
  34. package/dist/docx/encryption/index.js +4 -0
  35. package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
  36. package/dist/docx/encryption/openEncryptedDocx.js +65 -0
  37. package/dist/docx/footnoteParser.d.ts +1 -17
  38. package/dist/docx/footnoteParser.js +1 -27
  39. package/dist/docx/groupDrawingParser.d.ts +8 -0
  40. package/dist/docx/groupDrawingParser.js +131 -0
  41. package/dist/docx/index.d.ts +3 -1
  42. package/dist/docx/index.js +3 -1
  43. package/dist/docx/numberingParser.d.ts +3 -1
  44. package/dist/docx/numberingParser.js +8 -2
  45. package/dist/docx/paragraphParser.js +9 -3
  46. package/dist/docx/parser.d.ts +4 -5
  47. package/dist/docx/parser.js +8 -2
  48. package/dist/docx/runParser.js +8 -1
  49. package/dist/docx/selectiveXmlPatch.js +4 -10
  50. package/dist/docx/serializer/numberingSerializer.js +3 -1
  51. package/dist/docx/settingsParser.d.ts +6 -3
  52. package/dist/docx/settingsParser.js +2 -0
  53. package/dist/docx/unzip.d.ts +7 -5
  54. package/dist/docx/unzip.js +8 -2
  55. package/dist/docx/xmlParser.js +11 -4
  56. package/dist/index.d.ts +3 -2
  57. package/dist/index.js +6 -5
  58. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  59. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  60. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
  61. package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
  63. package/dist/layout-engine/index.d.ts +6 -5
  64. package/dist/layout-engine/index.js +116 -27
  65. package/dist/layout-engine/keep-together.d.ts +13 -8
  66. package/dist/layout-engine/keep-together.js +40 -25
  67. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  68. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  69. package/dist/layout-engine/measure/measureBlocks.js +43 -19
  70. package/dist/layout-engine/measure/measureParagraph.js +91 -14
  71. package/dist/layout-engine/paginator.d.ts +2 -1
  72. package/dist/layout-engine/paginator.js +9 -3
  73. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  74. package/dist/layout-engine/textBoxGroup.js +8 -0
  75. package/dist/layout-engine/types.d.ts +33 -9
  76. package/dist/layout-engine/types.js +20 -2
  77. package/dist/layout-painter/renderPage.d.ts +15 -1
  78. package/dist/layout-painter/renderPage.js +29 -12
  79. package/dist/layout-painter/renderParagraph.js +44 -9
  80. package/dist/layout-painter/renderTable.js +27 -7
  81. package/dist/managers/DocumentLoaderManager.d.ts +3 -1
  82. package/dist/managers/DocumentLoaderManager.js +13 -5
  83. package/dist/managers/TableSelectionManager.js +1 -1
  84. package/dist/model.js +1 -1
  85. package/dist/paged-layout/pageText.d.ts +14 -0
  86. package/dist/paged-layout/pageText.js +21 -0
  87. package/dist/prosemirror/attrs/index.js +3 -0
  88. package/dist/prosemirror/commands/comments.js +75 -5
  89. package/dist/prosemirror/commands/hyperlink.js +4 -2
  90. package/dist/prosemirror/commands/image.js +2 -1
  91. package/dist/prosemirror/commands/index.js +1 -1
  92. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  93. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  94. package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
  95. package/dist/prosemirror/conversion/toProseDoc.js +43 -15
  96. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
  97. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
  99. package/dist/prosemirror/findReplaceSelection.js +2 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/insertOperations.js +1 -1
  102. package/dist/prosemirror/schema/nodes.d.ts +14 -7
  103. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  104. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  105. package/dist/redline.d.ts +20 -0
  106. package/dist/redline.js +127 -0
  107. package/dist/server.d.ts +7 -3
  108. package/dist/server.js +6 -2
  109. package/dist/symbols.d.ts +35 -0
  110. package/dist/symbols.js +512 -0
  111. package/dist/utils/clipboard.js +3 -3
  112. package/dist/utils/fontLoader.js +3 -0
  113. package/dist/utils/fontResolver.js +65 -1
  114. package/dist/utils/zoom.d.ts +21 -0
  115. package/dist/utils/zoom.js +21 -0
  116. package/dist/version-comparison.d.ts +92 -0
  117. package/dist/version-comparison.js +436 -0
  118. package/package.json +7 -3
  119. package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
  120. package/dist/paged-layout/headerFooterMargins.js +0 -120
@@ -5,7 +5,12 @@ type LayoutRunReason = "font-ready" | "initial" | "layout-input" | "manual" | "t
5
5
  type LayoutPhase = "flow-blocks" | "header-footer" | "initial-fonts" | "layout-document" | "measure-blocks" | "render-pages";
6
6
  type HiddenEditorStateReason = "external-document" | "mount";
7
7
  type HiddenEditorPhase = "editor-state" | "editor-view" | "to-prose-doc" | "update-state";
8
+ type DocumentLoadPhase = "docx-parse";
8
9
  type LayoutInstrumentation = {
10
+ onDocumentLoadPhase?: (event: {
11
+ durationMs: number;
12
+ phase: DocumentLoadPhase;
13
+ }) => void;
9
14
  onHiddenEditorPhase?: (event: {
10
15
  durationMs: number;
11
16
  phase: HiddenEditorPhase;
@@ -21,6 +26,9 @@ type LayoutInstrumentation = {
21
26
  message: string;
22
27
  reason: LayoutRunReason;
23
28
  }) => void;
29
+ onLayoutStart?: (event: {
30
+ reason: LayoutRunReason;
31
+ }) => void;
24
32
  onLayoutPhase?: (event: {
25
33
  durationMs: number;
26
34
  phase: LayoutPhase;
@@ -42,9 +50,11 @@ declare global {
42
50
  declare function recordMeasureBlock(blockIndex: number, block: FlowBlock): void;
43
51
  declare function recordMeasureBlockError(blockIndex: number, block: FlowBlock, error: unknown): void;
44
52
  declare function recordLayoutComplete(reason?: LayoutRunReason): void;
53
+ declare function recordLayoutStart(reason: LayoutRunReason): void;
45
54
  declare function recordLayoutError(reason: LayoutRunReason, error: unknown): void;
46
55
  declare function recordLayoutPhase(reason: LayoutRunReason, phase: LayoutPhase, durationMs: number): void;
47
56
  declare function recordHiddenEditorStateCreate(reason: HiddenEditorStateReason): void;
48
57
  declare function recordHiddenEditorPhase(reason: HiddenEditorStateReason, phase: HiddenEditorPhase, durationMs: number): void;
58
+ declare function recordDocumentLoadPhase(phase: DocumentLoadPhase, durationMs: number): void;
49
59
  //#endregion
50
- export { HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError };
60
+ export { DocumentLoadPhase, HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
@@ -15,6 +15,9 @@ function recordMeasureBlockError(blockIndex, block, error) {
15
15
  function recordLayoutComplete(reason = "manual") {
16
16
  globalThis.__folioLayoutInstrumentation?.onLayoutComplete?.({ reason });
17
17
  }
18
+ function recordLayoutStart(reason) {
19
+ globalThis.__folioLayoutInstrumentation?.onLayoutStart?.({ reason });
20
+ }
18
21
  function recordLayoutError(reason, error) {
19
22
  const message = error instanceof Error ? error.message : String(error);
20
23
  globalThis.__folioLayoutInstrumentation?.onLayoutError?.({
@@ -39,5 +42,11 @@ function recordHiddenEditorPhase(reason, phase, durationMs) {
39
42
  reason
40
43
  });
41
44
  }
45
+ function recordDocumentLoadPhase(phase, durationMs) {
46
+ globalThis.__folioLayoutInstrumentation?.onDocumentLoadPhase?.({
47
+ durationMs,
48
+ phase
49
+ });
50
+ }
42
51
  //#endregion
43
- export { recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError };
52
+ export { recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
@@ -1,6 +1,7 @@
1
- import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand } from "../types.js";
1
+ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, tableColumnsArePinned } from "../types.js";
2
2
  import { findClearLineY, measureParagraph } from "./measureParagraph.js";
3
3
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
4
+ import { getTextBoxGroupId } from "../textBoxGroup.js";
4
5
  import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
5
6
  import { bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
6
7
  //#region src/layout-engine/measure/measureBlocks.ts
@@ -71,12 +72,17 @@ function measureTableCellBlockVisualHeight(block, blockMeasure) {
71
72
  }
72
73
  let maxImageHeight = 0;
73
74
  for (const run of inlineImageRuns) maxImageHeight = Math.max(maxImageHeight, run.height);
75
+ if (inlineImageRuns.length === 1) {
76
+ const spacingBefore = paragraphBlock.attrs?.spacing?.before ?? 0;
77
+ const spacingAfter = paragraphBlock.attrs?.spacing?.after ?? 0;
78
+ return spacingBefore + maxImageHeight + spacingAfter;
79
+ }
74
80
  const spacingBefore = paragraphBlock.attrs?.spacing?.before ?? 0;
75
81
  const spacingAfter = paragraphBlock.attrs?.spacing?.after ?? 0;
76
82
  return spacingBefore + maxImageHeight + spacingAfter;
77
83
  }
78
- function getTableCellVerticalBorderHeight(cell) {
79
- return (cell?.borders?.top?.width ?? 0) + (cell?.borders?.bottom?.width ?? 0);
84
+ function getTableCellVerticalBorderHeight(cell, isFirstRow) {
85
+ return (isFirstRow ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
80
86
  }
81
87
  function measureTableBlock(tableBlock, contentWidth, fieldValues) {
82
88
  const DEFAULT_CELL_PADDING_X = 7;
@@ -113,6 +119,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
113
119
  while (occupied.has(colIdx)) colIdx++;
114
120
  }
115
121
  }
122
+ const columnsPinned = tableColumnsArePinned(tableBlock);
116
123
  const rows = tableBlock.rows.map((row, rowIdx) => {
117
124
  let columnIndex = 0;
118
125
  const occupied = occupiedColumnsPerRow.get(rowIdx) ?? /* @__PURE__ */ new Set();
@@ -128,7 +135,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
128
135
  const padLeft = cell.padding?.left ?? DEFAULT_CELL_PADDING_X;
129
136
  const padRight = cell.padding?.right ?? DEFAULT_CELL_PADDING_X;
130
137
  const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight);
131
- const measureWidth = cell.noWrap ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
138
+ const measureWidth = cell.noWrap === true && !columnsPinned ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
132
139
  const cellMeasure = {
133
140
  blocks: cell.blocks.map((b) => measureBlock(b, measureWidth, void 0, void 0, fieldValues)),
134
141
  width: cellWidth,
@@ -143,8 +150,19 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
143
150
  });
144
151
  for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
145
152
  const row = rows[rowIdx];
153
+ const sourceRow = tableBlock.rows[rowIdx];
154
+ if (sourceRow?.hidden) {
155
+ row.height = 0;
156
+ row.cells = sourceRow.cells.map(() => ({
157
+ blocks: [],
158
+ width: 0,
159
+ height: 0
160
+ }));
161
+ continue;
162
+ }
146
163
  const sourceRowCells = tableBlock.rows[rowIdx]?.cells;
147
164
  let maxCellHeightWithBorders = 0;
165
+ let maxBorderHeight = 0;
148
166
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
149
167
  const cell = row.cells[cellIdx];
150
168
  const sourceCell = sourceRowCells?.[cellIdx];
@@ -158,13 +176,14 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
158
176
  const padTop = sourceCell?.padding?.top ?? DEFAULT_CELL_PADDING_Y;
159
177
  const padBottom = sourceCell?.padding?.bottom ?? DEFAULT_CELL_PADDING_Y;
160
178
  cell.height += padTop + padBottom;
161
- maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + getTableCellVerticalBorderHeight(sourceCell));
179
+ const borderHeight = getTableCellVerticalBorderHeight(sourceCell, rowIdx === 0);
180
+ maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + borderHeight);
181
+ maxBorderHeight = Math.max(maxBorderHeight, borderHeight);
162
182
  }
163
- const sourceRow = tableBlock.rows[rowIdx];
164
183
  const explicitHeight = sourceRow?.height;
165
184
  const heightRule = sourceRow?.heightRule;
166
185
  if (explicitHeight && heightRule === "exact") row.height = explicitHeight;
167
- else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight);
186
+ else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxBorderHeight);
168
187
  else row.height = maxCellHeightWithBorders;
169
188
  }
170
189
  const totalHeight = rows.reduce((h, r) => h + r.height, 0);
@@ -183,6 +202,7 @@ function perBlockNumberValue(value, blockIndex, fallback) {
183
202
  }
184
203
  function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry) {
185
204
  const zones = [];
205
+ const textBoxGroupAnchors = /* @__PURE__ */ new Map();
186
206
  const defaultMarginTop = Array.isArray(marginTop) ? marginTop[0] ?? 0 : marginTop;
187
207
  const pageHeightInput = pageGeometry?.pageHeight ?? 0;
188
208
  const marginBottomInput = pageGeometry?.marginBottom ?? 0;
@@ -272,28 +292,31 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
272
292
  const block = blocks[blockIndex];
273
293
  if (block.kind !== "textBox") continue;
274
294
  const tb = block;
275
- if (!floatingTextBoxReservesBand(tb)) continue;
295
+ if (!floatingTextBoxReservesBand(tb) || tb.position === void 0) continue;
276
296
  const v = tb.position?.vertical;
277
- if (!isPageFrameRelativeAnchor(v?.relativeTo)) continue;
278
297
  const height = measureTextBoxBlock(tb).height;
279
298
  const distTop = tb.distTop ?? 0;
280
299
  const distBottom = tb.distBottom ?? 0;
281
300
  const blockMarginTop = perBlockNumberValue(marginTop, blockIndex, defaultMarginTop);
282
- const rawTop = bandTopContentY(v, {
301
+ const pageFrameRelative = isPageFrameRelativeAnchor(v?.relativeTo);
302
+ const rawTop = pageFrameRelative ? bandTopContentY(v, {
283
303
  pageHeight: perBlockNumberValue(pageHeightInput, blockIndex, defaultPageHeight),
284
304
  marginTop: blockMarginTop,
285
305
  marginBottom: perBlockNumberValue(marginBottomInput, blockIndex, defaultMarginBottom),
286
306
  boxHeight: height
287
- });
307
+ }) : emuToPixels(v?.posOffset ?? 0);
288
308
  const bottomY = rawTop + height + distBottom;
289
309
  if (bottomY <= 0) continue;
310
+ const groupId = getTextBoxGroupId(tb);
311
+ const anchorBlockIndex = groupId ? textBoxGroupAnchors.get(groupId) ?? blockIndex : blockIndex;
312
+ if (groupId && !textBoxGroupAnchors.has(groupId)) textBoxGroupAnchors.set(groupId, blockIndex);
290
313
  zones.push({
291
314
  leftMargin: 0,
292
315
  rightMargin: 0,
293
316
  topY: Math.max(0, rawTop - distTop),
294
317
  bottomY,
295
- anchorBlockIndex: blockIndex,
296
- isMarginRelative: true,
318
+ anchorBlockIndex,
319
+ ...pageFrameRelative ? { isMarginRelative: true } : {},
297
320
  fullWidthBlock: true
298
321
  });
299
322
  }
@@ -353,14 +376,15 @@ function measureTextBoxBlock(tb, fieldValues) {
353
376
  };
354
377
  }
355
378
  /**
356
- * `true` for a section break that starts a new page (`nextPage`/`evenPage`/
357
- * `oddPage`, or an unspecified type, which the layout treats as `nextPage`).
358
- * A `continuous` break stays on the current page, so it does not open a fresh
359
- * page frame. Used by `measureBlocks` to reset the running Y for a page-pinned
360
- * band that lands right after the break. eigenpal #694.
379
+ * `true` for a section break that explicitly starts a new page
380
+ * (`nextPage`/`evenPage`/`oddPage`). An absent type follows
381
+ * `scheduleSectionBreak` and stays continuous, so measurement must retain any
382
+ * active exclusion zones across it. Used by `measureBlocks` to reset the
383
+ * running Y for a page-pinned band that lands right after the break.
384
+ * eigenpal #694.
361
385
  */
362
386
  function isNextPageSectionBreak(block) {
363
- return block.kind === "sectionBreak" && block.type !== "continuous";
387
+ return block.kind === "sectionBreak" && block.type !== void 0 && block.type !== "continuous";
364
388
  }
365
389
  /**
366
390
  * Measure all blocks with floating image support.
@@ -21,6 +21,12 @@ const DEFAULT_FONT_SIZE = 11;
21
21
  const DEFAULT_FONT_FAMILY = "Calibri";
22
22
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
23
23
  const WIDTH_TOLERANCE = .5;
24
+ const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
25
+ const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
26
+ const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025;
27
+ const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
28
+ const DEFAULT_LIST_HANGING_INDENT_PX = 24;
29
+ const ALL_CAPS_RATIO_THRESHOLD = .8;
24
30
  /**
25
31
  * Find the longest prefix of `text` that fits within `maxWidth` pixels.
26
32
  * Returns the number of characters that fit (at least 1 if `forceMin` is true).
@@ -266,6 +272,18 @@ function hasFollowingTabOnLine(runs, tabIndex) {
266
272
  }
267
273
  return false;
268
274
  }
275
+ function hasPriorTabOnLine(runs, tabIndex) {
276
+ for (let i = tabIndex - 1; i >= 0; i--) {
277
+ const prior = runs[i];
278
+ if (!prior || isLineBreakRun(prior) || isImageRun(prior) && isBlockLayoutImageRun(prior)) break;
279
+ if (isTabRun(prior)) return true;
280
+ }
281
+ return false;
282
+ }
283
+ function canClampTabToRightEdge(alignment, currentLineWidth, hasPriorTab, followingWidth, availableWidth) {
284
+ if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE && (hasPriorTab || followingWidth <= availableWidth);
285
+ return true;
286
+ }
269
287
  /**
270
288
  * Width of the inline content preceding the first `.` in the runs that follow
271
289
  * a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
@@ -299,6 +317,33 @@ function measureDecimalPrefixWidthAfterTab(runs, tabIndex, fieldValues) {
299
317
  function isSpaceOrTab(char) {
300
318
  return char === " " || char === " ";
301
319
  }
320
+ function uppercaseLetterRatio(text) {
321
+ let letters = 0;
322
+ let uppercase = 0;
323
+ for (const char of text) {
324
+ const lower = char.toLocaleLowerCase();
325
+ const upper = char.toLocaleUpperCase();
326
+ if (lower === upper) continue;
327
+ letters++;
328
+ if (char === upper) uppercase++;
329
+ }
330
+ return letters === 0 ? 0 : uppercase / letters;
331
+ }
332
+ function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) {
333
+ 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));
339
+ }
340
+ const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
341
+ const hasTabRuns = block.runs.some(isTabRun);
342
+ 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;
344
+ 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;
346
+ }
302
347
  function trimTrailingSpacesAndTabs(text) {
303
348
  let end = text.length;
304
349
  while (end > 0) {
@@ -384,6 +429,7 @@ function measureParagraph(block, maxWidth, options) {
384
429
  const runs = block.runs;
385
430
  const attrs = block.attrs;
386
431
  const spacing = attrs?.spacing;
432
+ const isJustifiedParagraph = attrs?.alignment === "justify";
387
433
  const floatingZones = options?.floatingZones;
388
434
  const paragraphYOffset = options?.paragraphYOffset ?? 0;
389
435
  const indent = attrs?.indent;
@@ -477,6 +523,9 @@ function measureParagraph(block, maxWidth, options) {
477
523
  toRun: 0,
478
524
  toChar: 0,
479
525
  width: 0,
526
+ trailingWhitespaceWidth: 0,
527
+ regularSpaceCount: 0,
528
+ nonBreakingSpaceCount: 0,
480
529
  maxFontSize: DEFAULT_FONT_SIZE,
481
530
  maxFontMetrics: null,
482
531
  maxImageHeightPx: 0,
@@ -523,7 +572,7 @@ function measureParagraph(block, maxWidth, options) {
523
572
  fromChar: currentLine.fromChar,
524
573
  toRun: currentLine.toRun,
525
574
  toChar: currentLine.toChar,
526
- width: currentLine.width,
575
+ width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
527
576
  ...finalTypography
528
577
  };
529
578
  if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
@@ -543,17 +592,21 @@ function measureParagraph(block, maxWidth, options) {
543
592
  const estimatedLineHeight = ptToPx(DEFAULT_FONT_SIZE) * DEFAULT_LINE_HEIGHT_MULTIPLIER;
544
593
  skipObstructingFloats(estimatedLineHeight, bodyContentWidth);
545
594
  const floatingMargins = getFloatingMargins(cumulativeHeight, estimatedLineHeight, floatingZones, paragraphYOffset);
595
+ const adjustedWidth = Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth));
546
596
  currentLine = {
547
597
  fromRun: runIndex,
548
598
  fromChar: charIndex,
549
599
  toRun: runIndex,
550
600
  toChar: charIndex,
551
601
  width: 0,
602
+ trailingWhitespaceWidth: 0,
603
+ regularSpaceCount: 0,
604
+ nonBreakingSpaceCount: 0,
552
605
  maxFontSize: DEFAULT_FONT_SIZE,
553
606
  maxFontMetrics: null,
554
607
  maxImageHeightPx: 0,
555
608
  maxMathHeightPx: 0,
556
- availableWidth: Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth)),
609
+ availableWidth: adjustedWidth,
557
610
  leftOffset: floatingMargins.leftMargin,
558
611
  rightOffset: floatingMargins.rightMargin,
559
612
  ...floatingMargins.segments?.length ? { segmentZones: floatingMargins.segments } : {}
@@ -589,20 +642,23 @@ function measureParagraph(block, maxWidth, options) {
589
642
  const lineX = currentLine.width + currentLine.leftOffset;
590
643
  const isFirstLine = lines.length === 0;
591
644
  const contentX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + lineX;
592
- let tabWidth = calculateTabWidth(contentX, {
645
+ const tabResult = calculateTabWidth(contentX, {
593
646
  ...attrs?.tabs !== void 0 ? { explicitStops: attrs.tabs } : {},
647
+ ...attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: attrs.defaultTabStopTwips } : {},
594
648
  leftIndent: pixelsToTwips(indentLeft)
595
649
  }, {
596
650
  followingWidth,
597
651
  decimalPrefixWidth
598
- }).width;
652
+ });
653
+ let tabWidth = tabResult.width;
599
654
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
600
- if (!hasFollowingTabOnLine(runs, runIndex) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
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);
601
656
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
602
657
  startNewLine(runIndex, 0);
603
658
  updateMaxFont(style);
604
659
  }
605
660
  currentLine.width += tabWidth;
661
+ currentLine.trailingWhitespaceWidth = 0;
606
662
  currentLine.toRun = runIndex;
607
663
  currentLine.toChar = 1;
608
664
  continue;
@@ -632,6 +688,7 @@ function measureParagraph(block, maxWidth, options) {
632
688
  const imageFootprintPx = imageHeight + (run.distTop ?? 0) + (run.distBottom ?? 0);
633
689
  if (imageFootprintPx > currentLine.maxImageHeightPx) currentLine.maxImageHeightPx = imageFootprintPx;
634
690
  currentLine.width += imageWidth;
691
+ currentLine.trailingWhitespaceWidth = 0;
635
692
  currentLine.toRun = runIndex;
636
693
  currentLine.toChar = 1;
637
694
  continue;
@@ -646,6 +703,7 @@ function measureParagraph(block, maxWidth, options) {
646
703
  updateMaxFont(style);
647
704
  }
648
705
  currentLine.width += fieldWidth;
706
+ currentLine.trailingWhitespaceWidth = 0;
649
707
  currentLine.toRun = runIndex;
650
708
  currentLine.toChar = 1;
651
709
  continue;
@@ -666,6 +724,7 @@ function measureParagraph(block, maxWidth, options) {
666
724
  }
667
725
  if (mathFootprintPx > currentLine.maxMathHeightPx) currentLine.maxMathHeightPx = mathFootprintPx;
668
726
  currentLine.width += mathWidth;
727
+ currentLine.trailingWhitespaceWidth = 0;
669
728
  currentLine.toRun = runIndex;
670
729
  currentLine.toChar = 1;
671
730
  continue;
@@ -690,12 +749,17 @@ function measureParagraph(block, maxWidth, options) {
690
749
  break;
691
750
  }
692
751
  const word = text.slice(charIndex, nextBreak);
693
- const wordWidth = measureTextWidth(word, style);
694
- if (wordWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
752
+ const measuredWord = trimTrailingSpacesAndTabs(word);
753
+ const wordWidth = measureTextWidth(measuredWord, style);
754
+ const fullWordWidth = measureTextWidth(word, style);
755
+ const regularSpaces = measuredWord.split(" ").length - 1;
756
+ const nonBreakingSpaces = measuredWord.split("\xA0").length - 1;
757
+ const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyShrinkToleranceRatio(block, lines.length === 0, currentLine.regularSpaceCount + regularSpaces, currentLine.nonBreakingSpaceCount + nonBreakingSpaces)) : WIDTH_TOLERANCE;
758
+ if (wordWidth > currentLine.availableWidth + widthTolerance) {
695
759
  let chunkStart = 0;
696
- while (chunkStart < word.length) {
760
+ while (chunkStart < measuredWord.length) {
697
761
  const spaceLeft = currentLine.availableWidth - currentLine.width + WIDTH_TOLERANCE;
698
- let bestEnd = findMaxFittingLength(word.slice(chunkStart), style, spaceLeft);
762
+ let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft);
699
763
  if (bestEnd === 0) {
700
764
  if (currentLine.width > 0) {
701
765
  startNewLine(runIndex, charIndex + chunkStart);
@@ -705,26 +769,39 @@ function measureParagraph(block, maxWidth, options) {
705
769
  bestEnd = 1;
706
770
  }
707
771
  const chunkEnd = chunkStart + bestEnd;
708
- const chunkWidth = measureTextWidth(word.slice(chunkStart, chunkEnd), style);
772
+ const chunk = measuredWord.slice(chunkStart, chunkEnd);
773
+ const chunkWidth = measureTextWidth(chunk, style);
709
774
  currentLine.width += chunkWidth;
775
+ currentLine.trailingWhitespaceWidth = chunkWidth - measureTextWidth(trimTrailingSpacesAndTabs(chunk), style);
776
+ currentLine.regularSpaceCount += chunk.split(" ").length - 1;
777
+ currentLine.nonBreakingSpaceCount += chunk.split("\xA0").length - 1;
710
778
  currentLine.toRun = runIndex;
711
779
  currentLine.toChar = charIndex + chunkEnd;
712
780
  chunkStart = chunkEnd;
713
- if (chunkStart < word.length) {
781
+ if (chunkStart < measuredWord.length) {
714
782
  startNewLine(runIndex, charIndex + chunkStart);
715
783
  updateMaxFont(lineHeightStyle);
716
784
  }
717
785
  }
786
+ const trailingWhitespaceWidth = fullWordWidth - wordWidth;
787
+ currentLine.width += trailingWhitespaceWidth;
788
+ currentLine.trailingWhitespaceWidth = trailingWhitespaceWidth;
789
+ currentLine.regularSpaceCount += word.split(" ").length - 1;
790
+ currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
791
+ currentLine.toChar = nextBreak;
718
792
  charIndex = nextBreak;
719
793
  continue;
720
794
  }
721
795
  const rawGlueWidth = nextBreak === text.length && word.length > 0 && !isBreakChar(word[word.length - 1]) ? trailingGlueWidths[runIndex] ?? 0 : 0;
722
- const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + WIDTH_TOLERANCE ? rawGlueWidth : 0;
723
- if (currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
796
+ const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + widthTolerance ? rawGlueWidth : 0;
797
+ if (currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + widthTolerance) {
724
798
  startNewLine(runIndex, charIndex);
725
799
  updateMaxFont(lineHeightStyle);
726
800
  }
727
- currentLine.width += wordWidth;
801
+ currentLine.width += fullWordWidth;
802
+ currentLine.trailingWhitespaceWidth = fullWordWidth - wordWidth;
803
+ currentLine.regularSpaceCount += word.split(" ").length - 1;
804
+ currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1;
728
805
  currentLine.toRun = runIndex;
729
806
  currentLine.toChar = nextBreak;
730
807
  charIndex = nextBreak;
@@ -27,7 +27,8 @@ type PaginatorOptions = {
27
27
  w: number;
28
28
  h: number;
29
29
  }; /** Page margins. */
30
- margins: PageMargins;
30
+ margins: PageMargins; /** Swap left/right margins on even physical pages. */
31
+ mirrorMargins?: boolean;
31
32
  /**
32
33
  * Margins applied only to the very first page (page 1) of the paginator.
33
34
  * Used when a `<w:titlePg/>`-enabled section needs different margins for
@@ -46,7 +46,7 @@ function createPaginator(options) {
46
46
  }
47
47
  function currentPageUsesActiveLayout(state) {
48
48
  if (pendingPageSize || pendingMargins) return false;
49
- return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, margins);
49
+ return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number));
50
50
  }
51
51
  if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area");
52
52
  let columnWidth = calculateColumnWidth(pageSize.w, margins.left, margins.right, columns);
@@ -62,14 +62,20 @@ function createPaginator(options) {
62
62
  recalculateColumnWidth();
63
63
  }
64
64
  function getPageMargins(pageNumber) {
65
- return pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
65
+ const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
66
+ if (options.mirrorMargins === true && pageNumber % 2 === 0) return {
67
+ ...pageMargins,
68
+ left: pageMargins.right,
69
+ right: pageMargins.left
70
+ };
71
+ return pageMargins;
66
72
  }
67
73
  let columnRegionTop = margins.top;
68
74
  /**
69
75
  * Get X position for a given column index.
70
76
  */
71
77
  function getColumnX(columnIndex) {
72
- return margins.left + columnIndex * (columnWidth + columns.gap);
78
+ return (states.at(-1)?.page.margins.left ?? getPageMargins(1).left) + columnIndex * (columnWidth + columns.gap);
73
79
  }
74
80
  /**
75
81
  * Create a new page and add it to the list.
@@ -0,0 +1,7 @@
1
+ import { TextBoxBlock } from "./types.js";
2
+
3
+ //#region src/layout-engine/textBoxGroup.d.ts
4
+ declare const setTextBoxGroupId: (textBox: TextBoxBlock, groupId: string) => void;
5
+ declare const getTextBoxGroupId: (textBox: TextBoxBlock) => string | undefined;
6
+ //#endregion
7
+ export { getTextBoxGroupId, setTextBoxGroupId };
@@ -0,0 +1,8 @@
1
+ //#region src/layout-engine/textBoxGroup.ts
2
+ const textBoxGroupIds = /* @__PURE__ */ new WeakMap();
3
+ const setTextBoxGroupId = (textBox, groupId) => {
4
+ textBoxGroupIds.set(textBox, groupId);
5
+ };
6
+ const getTextBoxGroupId = (textBox) => textBoxGroupIds.get(textBox);
7
+ //#endregion
8
+ export { getTextBoxGroupId, setTextBoxGroupId };
@@ -3,7 +3,7 @@ import { ImagePosition, ImageWrap, SdtProperties, SdtType, TableWidthType } from
3
3
 
4
4
  //#region src/layout-engine/types.d.ts
5
5
  declare namespace types_d_exports {
6
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun };
6
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
7
7
  }
8
8
  /**
9
9
  * Unique identifier for a block in the document.
@@ -303,7 +303,9 @@ type ParagraphAttrs = {
303
303
  indent?: ParagraphIndent;
304
304
  keepNext?: boolean;
305
305
  keepLines?: boolean;
306
- pageBreakBefore?: boolean;
306
+ widowControl?: boolean;
307
+ pageBreakBefore?: boolean; /** Word's cached pagination hint (`w:lastRenderedPageBreak`). */
308
+ renderedPageBreakBefore?: boolean;
307
309
  styleId?: string;
308
310
  contextualSpacing?: boolean;
309
311
  /**
@@ -432,6 +434,7 @@ type TableRow = {
432
434
  height?: number;
433
435
  heightRule?: "auto" | "atLeast" | "exact";
434
436
  isHeader?: boolean;
437
+ hidden?: boolean;
435
438
  };
436
439
  /**
437
440
  * Floating table positioning info (pixel values).
@@ -457,7 +460,14 @@ type TableBlock = {
457
460
  rows: TableRow[];
458
461
  columnWidths?: number[]; /** Table width value (twips for dxa, 50ths of percent for pct). */
459
462
  width?: number; /** Table width type ('auto', 'pct', 'dxa', 'nil'). */
460
- widthType?: TableWidthType; /** Table horizontal alignment */
463
+ widthType?: TableWidthType;
464
+ /**
465
+ * Table layout algorithm (`w:tblLayout`). "fixed" pins columns to their grid
466
+ * widths; "autofit" (the default when absent) lets Word size columns to
467
+ * content. Consulted alongside `widthType` to decide whether `w:noWrap` cells
468
+ * can be honored (see `tableColumnsArePinned`).
469
+ */
470
+ layout?: "fixed" | "autofit"; /** Table horizontal alignment */
461
471
  justification?: "left" | "center" | "right"; /** Table indent from left margin (in pixels, from w:tblInd) */
462
472
  indent?: number; /** Right-to-left column order (w:bidiVisual): logical column 0 paints on the right. */
463
473
  bidi?: boolean; /** Floating table properties (pixel values). */
@@ -518,6 +528,7 @@ type SectionBreakBlock = {
518
528
  };
519
529
  type PageHeaderFooterRefs = {
520
530
  titlePg?: boolean;
531
+ evenAndOddHeaders?: boolean;
521
532
  headerDefault?: string;
522
533
  headerFirst?: string;
523
534
  headerEven?: string;
@@ -900,7 +911,8 @@ type LayoutOptions = {
900
911
  headerContentHeights?: HeaderFooterContentHeights; /** Footer content heights by variant. */
901
912
  footerContentHeights?: HeaderFooterContentHeights; /** Whether section has different first page header/footer. */
902
913
  titlePage?: boolean; /** Whether section has different even/odd headers/footers. */
903
- evenAndOddHeaders?: boolean; /** Per-page footnote reserved heights (pageNumber height in pixels). */
914
+ evenAndOddHeaders?: boolean; /** Swap left/right margins on even physical pages. */
915
+ mirrorMargins?: boolean; /** Per-page footnote reserved heights (pageNumber → height in pixels). */
904
916
  footnoteReservedHeights?: Map<number, number>;
905
917
  /**
906
918
  * Footnote content heights keyed by internal footnote id (the OOXML
@@ -967,10 +979,8 @@ type HeaderFooterContent = {
967
979
  visualTop?: number; /** Bottom-most visual extent relative to the nominal flow origin. */
968
980
  visualBottom?: number;
969
981
  /**
970
- * Flow-only bounds used by `computeHeaderFooterMarginExtender` to push
971
- * body margins clear of HF overflow. Excludes `behindDoc` images that
972
- * the renderer paints behind body content; including them would reserve
973
- * a full-page letterhead as body push-down.
982
+ * Flow-only bounds for consumers that need to distinguish normal
983
+ * header/footer flow from floating or `behindDoc` artwork.
974
984
  */
975
985
  marginPushTop?: number;
976
986
  marginPushBottom?: number;
@@ -1028,5 +1038,19 @@ declare function floatingTextBoxWrapsText(block: TextBoxFlowAttrs): boolean;
1028
1038
  * Ported from eigenpal #694.
1029
1039
  */
1030
1040
  declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean;
1041
+ /**
1042
+ * A table's columns are pinned when the layout engine cannot widen a column to
1043
+ * fit content that overflows it: either the layout is explicitly fixed
1044
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
1045
+ * `pct`) is fully consumed by the grid.
1046
+ *
1047
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
1048
+ * can only honor it by widening the column. When the columns are pinned it
1049
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
1050
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
1051
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
1052
+ * and the painter both consult this so their line count and `white-space` agree.
1053
+ */
1054
+ declare function tableColumnsArePinned(table: TableBlock): boolean;
1031
1055
  //#endregion
1032
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_d_exports };
1056
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
@@ -11,7 +11,8 @@ var types_exports = /* @__PURE__ */ __exportAll({
11
11
  floatingTextBoxWrapsText: () => floatingTextBoxWrapsText,
12
12
  isFloatingImageRun: () => isFloatingImageRun,
13
13
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
14
- isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun
14
+ isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
15
+ tableColumnsArePinned: () => tableColumnsArePinned
15
16
  });
16
17
  /** Default internal margins for text boxes (OOXML defaults in pixels) */
17
18
  const DEFAULT_TEXTBOX_MARGINS = {
@@ -96,5 +97,22 @@ function floatingTextBoxWrapsText(block) {
96
97
  function floatingTextBoxReservesBand(block) {
97
98
  return isFloatingTextBoxBlock(block) && block.wrapType === "topAndBottom";
98
99
  }
100
+ /**
101
+ * A table's columns are pinned when the layout engine cannot widen a column to
102
+ * fit content that overflows it: either the layout is explicitly fixed
103
+ * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/
104
+ * `pct`) is fully consumed by the grid.
105
+ *
106
+ * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word
107
+ * can only honor it by widening the column. When the columns are pinned it
108
+ * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table
109
+ * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column
110
+ * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement
111
+ * and the painter both consult this so their line count and `white-space` agree.
112
+ */
113
+ function tableColumnsArePinned(table) {
114
+ if (table.layout === "fixed") return true;
115
+ return table.widthType === "dxa" || table.widthType === "pct";
116
+ }
99
117
  //#endregion
100
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_exports };
118
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };