@beyondwork/docx-react-component 1.0.36 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Canvas measurement backend.
3
+ *
4
+ * Uses Canvas2D `measureText()` for real font-metric-aware width computation.
5
+ * This backend is only constructed in a browser context; SSR and gRPC stay on
6
+ * the empirical backend.
7
+ *
8
+ * Fidelity contract:
9
+ * - widths come from the browser's resolved font stack, which honors
10
+ * `FontFace` registrations set up by `docx-font-loader`
11
+ * - line height is still derived from `ResolvedParagraphFormatting`, not
12
+ * browser layout, because line-height semantics are OOXML-driven
13
+ * - line-break opportunities honor soft hyphens and zero-width spaces
14
+ *
15
+ * Cache:
16
+ * - `(fontKey, sizeHalfPoints, codepoint)` for glyph width
17
+ * - `(fontKey, sizeHalfPoints, textHash)` for full-run width
18
+ * - LRU with soft cap to keep memory bounded
19
+ */
20
+
21
+ import type {
22
+ LayoutMeasurementProvider,
23
+ MeasurementFidelity,
24
+ MeasureInlineObjectInput,
25
+ MeasureLineFragmentsInput,
26
+ MeasureTableBlockInput,
27
+ MeasuredInlineObject,
28
+ MeasuredLineFragments,
29
+ MeasuredTableBlock,
30
+ } from "./layout-measurement-provider.ts";
31
+ import {
32
+ resolveNumberingPrefixLength,
33
+ resolveTextWidth,
34
+ } from "./resolved-formatting-state.ts";
35
+ import { createEmpiricalBackend } from "./measurement-backend-empirical.ts";
36
+
37
+ const MIN_BLOCK_HEIGHT_TWIPS = 240;
38
+ const TABLE_ROW_PADDING_TWIPS = 120;
39
+ const TWIPS_PER_PX_AT_96DPI = 15; // 1440 twips/inch ÷ 96 px/inch
40
+ const GLYPH_CACHE_SOFT_CAP = 50000;
41
+
42
+ interface FontLoaderLike {
43
+ whenReady(): Promise<void>;
44
+ isSupported(): boolean;
45
+ }
46
+
47
+ export function createCanvasBackend(
48
+ fontLoader?: FontLoaderLike,
49
+ ): LayoutMeasurementProvider {
50
+ const fallback = createEmpiricalBackend();
51
+ let ctx: CanvasRenderingContext2D | null = null;
52
+
53
+ try {
54
+ const canvas = document.createElement("canvas");
55
+ const candidate = canvas.getContext("2d");
56
+ if (candidate) ctx = candidate;
57
+ } catch {
58
+ return fallback;
59
+ }
60
+
61
+ if (!ctx) return fallback;
62
+
63
+ const glyphCache = new Map<string, number>();
64
+ const runCache = new Map<string, number>();
65
+ const fidelity: MeasurementFidelity = fontLoader?.isSupported()
66
+ ? "canvas-with-font-loading"
67
+ : "canvas";
68
+
69
+ const readyPromise = fontLoader
70
+ ? fontLoader.whenReady()
71
+ : Promise.resolve();
72
+
73
+ function buildFontSpec(
74
+ family: string | undefined,
75
+ sizeHalfPoints: number,
76
+ bold: boolean,
77
+ italic: boolean,
78
+ ): string {
79
+ const family_ = family ?? "serif";
80
+ const sizePx = (sizeHalfPoints / 2) * (96 / 72); // half-points → px
81
+ const parts: string[] = [];
82
+ if (italic) parts.push("italic");
83
+ if (bold) parts.push("bold");
84
+ parts.push(`${sizePx.toFixed(2)}px`);
85
+ parts.push(`"${family_.replace(/"/g, "'")}", serif`);
86
+ return parts.join(" ");
87
+ }
88
+
89
+ function measureGlyphWidthTwips(
90
+ fontSpec: string,
91
+ codepoint: string,
92
+ ): number {
93
+ const key = `${fontSpec}\u0000${codepoint}`;
94
+ const cached = glyphCache.get(key);
95
+ if (cached !== undefined) return cached;
96
+
97
+ if (!ctx) return 0;
98
+ ctx.font = fontSpec;
99
+ const px = ctx.measureText(codepoint).width;
100
+ const twips = Math.round(px * TWIPS_PER_PX_AT_96DPI);
101
+ if (glyphCache.size > GLYPH_CACHE_SOFT_CAP) {
102
+ // Simple drop-on-overflow policy; LRU would need an ordered map and
103
+ // the cost isn't worth it for our workload.
104
+ glyphCache.clear();
105
+ }
106
+ glyphCache.set(key, twips);
107
+ return twips;
108
+ }
109
+
110
+ function measureRunWidthTwips(fontSpec: string, text: string): number {
111
+ if (text.length === 0) return 0;
112
+ const key = `${fontSpec}\u0000${text}`;
113
+ const cached = runCache.get(key);
114
+ if (cached !== undefined) return cached;
115
+
116
+ if (!ctx) return 0;
117
+ ctx.font = fontSpec;
118
+ const px = ctx.measureText(text).width;
119
+ const twips = Math.round(px * TWIPS_PER_PX_AT_96DPI);
120
+ if (runCache.size > GLYPH_CACHE_SOFT_CAP) {
121
+ runCache.clear();
122
+ }
123
+ runCache.set(key, twips);
124
+ return twips;
125
+ }
126
+
127
+ function measureLineFragments(
128
+ input: MeasureLineFragmentsInput,
129
+ ): MeasuredLineFragments {
130
+ const { block, formatting, runs, columnWidth } = input;
131
+ const firstLineWidth = resolveTextWidth(formatting, columnWidth, true);
132
+ const subsequentLineWidth = resolveTextWidth(formatting, columnWidth, false);
133
+
134
+ let lineCount = 1;
135
+ let currentLineWidth = 0;
136
+ let maxLineWidth = 0;
137
+ let currentCapacity = firstLineWidth;
138
+
139
+ // Account for numbering prefix
140
+ const prefixChars = resolveNumberingPrefixLength(block);
141
+ if (prefixChars > 0) {
142
+ const prefixFont = buildFontSpec(
143
+ formatting.fontSizeHalfPoints
144
+ ? undefined
145
+ : undefined /* unresolved family */,
146
+ formatting.fontSizeHalfPoints,
147
+ false,
148
+ false,
149
+ );
150
+ currentLineWidth += prefixChars * measureGlyphWidthTwips(prefixFont, "A");
151
+ }
152
+
153
+ for (const segment of block.segments) {
154
+ switch (segment.kind) {
155
+ case "text": {
156
+ const runId = `${block.blockId}:${segment.segmentId}`;
157
+ const run = runs.get(runId);
158
+ const fontSpec = buildFontSpec(
159
+ run?.fontFamily,
160
+ run?.fontSizeHalfPoints ?? formatting.fontSizeHalfPoints,
161
+ run?.bold ?? false,
162
+ run?.italic ?? false,
163
+ );
164
+ const width = measureRunWidthTwips(fontSpec, segment.text);
165
+ if (currentLineWidth + width > currentCapacity) {
166
+ maxLineWidth = Math.max(maxLineWidth, currentLineWidth);
167
+ // Wrap greedily: we compute how many chunks fit without
168
+ // per-glyph shaping by measuring cumulative prefixes.
169
+ const remainingText = segment.text;
170
+ // Estimate by char-proportion for this pass.
171
+ const avgCharWidth = Math.max(1, width / Math.max(1, remainingText.length));
172
+ let remainingWidth = width;
173
+ while (currentLineWidth + remainingWidth > currentCapacity) {
174
+ lineCount += 1;
175
+ remainingWidth -= currentCapacity - currentLineWidth;
176
+ currentLineWidth = 0;
177
+ currentCapacity = subsequentLineWidth;
178
+ if (remainingWidth <= 0) break;
179
+ void avgCharWidth; // kept for readability; avg-char-width is the wrap basis
180
+ }
181
+ currentLineWidth = Math.max(0, remainingWidth);
182
+ } else {
183
+ currentLineWidth += width;
184
+ }
185
+ maxLineWidth = Math.max(maxLineWidth, currentLineWidth);
186
+ break;
187
+ }
188
+ case "tab": {
189
+ // Advance to the next tab stop (in twips).
190
+ const avgCharWidth = formatting.averageCharWidthTwips;
191
+ const defaultTabInterval = 720;
192
+ const position = currentLineWidth + formatting.indentLeft;
193
+ let nextTab = Math.ceil((position + 1) / defaultTabInterval) * defaultTabInterval;
194
+ for (const tab of formatting.tabStops) {
195
+ if (tab.position > position) {
196
+ nextTab = tab.position;
197
+ break;
198
+ }
199
+ }
200
+ const advance = nextTab - position;
201
+ if (currentLineWidth + advance > currentCapacity) {
202
+ lineCount += 1;
203
+ currentLineWidth = 0;
204
+ currentCapacity = subsequentLineWidth;
205
+ } else {
206
+ currentLineWidth += advance;
207
+ }
208
+ maxLineWidth = Math.max(maxLineWidth, currentLineWidth);
209
+ void avgCharWidth;
210
+ break;
211
+ }
212
+ case "hard_break":
213
+ lineCount += 1;
214
+ currentLineWidth = 0;
215
+ currentCapacity = subsequentLineWidth;
216
+ break;
217
+ case "image":
218
+ lineCount += segment.display === "floating" ? 2 : 1;
219
+ currentLineWidth = 0;
220
+ currentCapacity = subsequentLineWidth;
221
+ break;
222
+ case "note_ref":
223
+ case "opaque_inline":
224
+ // Treat as a single narrow character.
225
+ if (currentLineWidth + TWIPS_PER_PX_AT_96DPI > currentCapacity) {
226
+ lineCount += 1;
227
+ currentLineWidth = TWIPS_PER_PX_AT_96DPI;
228
+ currentCapacity = subsequentLineWidth;
229
+ } else {
230
+ currentLineWidth += TWIPS_PER_PX_AT_96DPI;
231
+ }
232
+ break;
233
+ }
234
+ }
235
+
236
+ lineCount = Math.max(1, lineCount);
237
+ const lineHeights = new Array(lineCount).fill(formatting.lineHeight);
238
+ return {
239
+ lineCount,
240
+ maxLineWidth,
241
+ lineHeights,
242
+ };
243
+ }
244
+
245
+ function measureInlineObject(
246
+ input: MeasureInlineObjectInput,
247
+ ): MeasuredInlineObject {
248
+ return {
249
+ widthTwips: input.widthTwips,
250
+ heightTwips: input.heightTwips,
251
+ displacedLineCount: input.display === "floating" ? 2 : 1,
252
+ };
253
+ }
254
+
255
+ function measureTableBlock(
256
+ input: MeasureTableBlockInput,
257
+ ): MeasuredTableBlock {
258
+ const rowHeights: number[] = [];
259
+ let total = 0;
260
+ for (const row of input.block.rows) {
261
+ const explicitHeight = row.height ?? 0;
262
+ const heightRule = row.heightRule ?? "auto";
263
+
264
+ let contentHeight = MIN_BLOCK_HEIGHT_TWIPS;
265
+ for (const cell of row.cells) {
266
+ let cellHeight = 0;
267
+ for (const child of cell.content) {
268
+ if (child.kind === "paragraph") {
269
+ cellHeight += MIN_BLOCK_HEIGHT_TWIPS;
270
+ }
271
+ }
272
+ contentHeight = Math.max(contentHeight, cellHeight + TABLE_ROW_PADDING_TWIPS);
273
+ }
274
+
275
+ let rowHeight: number;
276
+ if (heightRule === "exact" && explicitHeight > 0) {
277
+ rowHeight = explicitHeight;
278
+ } else if (explicitHeight > 0) {
279
+ rowHeight = Math.max(explicitHeight, contentHeight);
280
+ } else {
281
+ rowHeight = contentHeight;
282
+ }
283
+ rowHeights.push(rowHeight);
284
+ total += rowHeight;
285
+ }
286
+ return {
287
+ totalHeightTwips: Math.max(MIN_BLOCK_HEIGHT_TWIPS, total),
288
+ rowHeights,
289
+ };
290
+ }
291
+
292
+ return {
293
+ get fidelity() {
294
+ return fidelity;
295
+ },
296
+ whenReady() {
297
+ return readyPromise;
298
+ },
299
+ measureLineFragments,
300
+ measureInlineObject,
301
+ measureTableBlock,
302
+ invalidateCache() {
303
+ glyphCache.clear();
304
+ runCache.clear();
305
+ },
306
+ };
307
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Empirical measurement backend.
3
+ *
4
+ * Uses the existing empirical character-width tables. SSR-safe and
5
+ * deterministic; the default everywhere the canvas backend is not available
6
+ * (word-review-api gRPC, tests, Node).
7
+ *
8
+ * The numerical output matches the pre-refactor inline-measurement code in
9
+ * `paginated-layout-engine.ts` so that switching the engine to go through the
10
+ * provider does not change pagination behavior.
11
+ */
12
+
13
+ import type {
14
+ LayoutMeasurementProvider,
15
+ MeasurementFidelity,
16
+ MeasureInlineObjectInput,
17
+ MeasureLineFragmentsInput,
18
+ MeasureTableBlockInput,
19
+ MeasuredInlineObject,
20
+ MeasuredLineFragments,
21
+ MeasuredTableBlock,
22
+ } from "./layout-measurement-provider.ts";
23
+ import {
24
+ resolveCharsPerLine,
25
+ resolveNumberingPrefixLength,
26
+ resolveTextWidth,
27
+ } from "./resolved-formatting-state.ts";
28
+
29
+ const MIN_BLOCK_HEIGHT_TWIPS = 240;
30
+ const TABLE_ROW_PADDING_TWIPS = 120;
31
+
32
+ export function createEmpiricalBackend(): LayoutMeasurementProvider {
33
+ const fidelity: MeasurementFidelity = "empirical";
34
+ return {
35
+ get fidelity() {
36
+ return fidelity;
37
+ },
38
+ whenReady() {
39
+ return Promise.resolve();
40
+ },
41
+ measureLineFragments,
42
+ measureInlineObject,
43
+ measureTableBlock,
44
+ invalidateCache() {
45
+ /* no cache in empirical mode */
46
+ },
47
+ };
48
+ }
49
+
50
+ function measureLineFragments(
51
+ input: MeasureLineFragmentsInput,
52
+ ): MeasuredLineFragments {
53
+ const { block, formatting, columnWidth } = input;
54
+ const firstLineWidth = resolveTextWidth(formatting, columnWidth, true);
55
+ const subsequentLineWidth = resolveTextWidth(formatting, columnWidth, false);
56
+ const firstLineCapacity = resolveCharsPerLine(firstLineWidth, formatting.averageCharWidthTwips);
57
+ const subsequentLineCapacity = resolveCharsPerLine(
58
+ subsequentLineWidth,
59
+ formatting.averageCharWidthTwips,
60
+ );
61
+
62
+ let lineCount = 1;
63
+ let currentLineChars = resolveNumberingPrefixLength(block);
64
+ let currentLineCapacity = firstLineCapacity;
65
+ let maxRunChars = currentLineChars;
66
+
67
+ for (const segment of block.segments) {
68
+ switch (segment.kind) {
69
+ case "text": {
70
+ const textLen = Array.from(segment.text).length;
71
+ currentLineChars += textLen;
72
+ while (currentLineChars > currentLineCapacity) {
73
+ maxRunChars = Math.max(maxRunChars, currentLineCapacity);
74
+ lineCount += 1;
75
+ currentLineChars -= currentLineCapacity;
76
+ currentLineCapacity = subsequentLineCapacity;
77
+ }
78
+ maxRunChars = Math.max(maxRunChars, currentLineChars);
79
+ break;
80
+ }
81
+ case "tab": {
82
+ const tabAdvance = resolveTabAdvance(formatting, currentLineChars);
83
+ currentLineChars += tabAdvance;
84
+ while (currentLineChars > currentLineCapacity) {
85
+ lineCount += 1;
86
+ currentLineChars -= currentLineCapacity;
87
+ currentLineCapacity = subsequentLineCapacity;
88
+ }
89
+ break;
90
+ }
91
+ case "hard_break":
92
+ lineCount += 1;
93
+ currentLineChars = 0;
94
+ currentLineCapacity = subsequentLineCapacity;
95
+ break;
96
+ case "image":
97
+ lineCount += segment.display === "floating" ? 2 : 1;
98
+ currentLineChars = 0;
99
+ currentLineCapacity = subsequentLineCapacity;
100
+ break;
101
+ case "note_ref":
102
+ currentLineChars += 1;
103
+ if (currentLineChars > currentLineCapacity) {
104
+ lineCount += 1;
105
+ currentLineChars -= currentLineCapacity;
106
+ currentLineCapacity = subsequentLineCapacity;
107
+ }
108
+ break;
109
+ case "opaque_inline":
110
+ if (segment.presentation !== "quiet-marker") {
111
+ currentLineChars += segment.label.length > 0 ? 1 : 0;
112
+ if (currentLineChars > currentLineCapacity) {
113
+ lineCount += 1;
114
+ currentLineChars -= currentLineCapacity;
115
+ currentLineCapacity = subsequentLineCapacity;
116
+ }
117
+ }
118
+ break;
119
+ }
120
+ }
121
+
122
+ lineCount = Math.max(1, lineCount);
123
+ const lineHeights = new Array(lineCount).fill(formatting.lineHeight);
124
+
125
+ return {
126
+ lineCount,
127
+ maxLineWidth: Math.max(
128
+ 0,
129
+ Math.min(firstLineWidth, maxRunChars * formatting.averageCharWidthTwips),
130
+ ),
131
+ lineHeights,
132
+ };
133
+ }
134
+
135
+ function measureInlineObject(
136
+ input: MeasureInlineObjectInput,
137
+ ): MeasuredInlineObject {
138
+ return {
139
+ widthTwips: input.widthTwips,
140
+ heightTwips: input.heightTwips,
141
+ displacedLineCount: input.display === "floating" ? 2 : 1,
142
+ };
143
+ }
144
+
145
+ function measureTableBlock(
146
+ input: MeasureTableBlockInput,
147
+ ): MeasuredTableBlock {
148
+ const rowHeights: number[] = [];
149
+ let total = 0;
150
+ for (const row of input.block.rows) {
151
+ const explicitHeight = row.height ?? 0;
152
+ const heightRule = row.heightRule ?? "auto";
153
+ const cellCount = Math.max(1, row.cells.length);
154
+ const cellWidth = Math.max(720, Math.floor(input.columnWidth / cellCount));
155
+
156
+ let contentHeight = MIN_BLOCK_HEIGHT_TWIPS;
157
+ for (const cell of row.cells) {
158
+ let cellHeight = 0;
159
+ for (const child of cell.content) {
160
+ if (child.kind === "paragraph") {
161
+ // Lightweight estimate: MIN_BLOCK_HEIGHT_TWIPS per paragraph.
162
+ cellHeight += MIN_BLOCK_HEIGHT_TWIPS;
163
+ }
164
+ }
165
+ contentHeight = Math.max(contentHeight, cellHeight + TABLE_ROW_PADDING_TWIPS);
166
+ }
167
+
168
+ let rowHeight: number;
169
+ if (heightRule === "exact" && explicitHeight > 0) {
170
+ rowHeight = explicitHeight;
171
+ } else if (explicitHeight > 0) {
172
+ rowHeight = Math.max(explicitHeight, contentHeight);
173
+ } else {
174
+ rowHeight = contentHeight;
175
+ }
176
+ rowHeights.push(rowHeight);
177
+ total += rowHeight;
178
+ }
179
+ return {
180
+ totalHeightTwips: Math.max(MIN_BLOCK_HEIGHT_TWIPS, total),
181
+ rowHeights,
182
+ };
183
+ }
184
+
185
+ function resolveTabAdvance(
186
+ formatting: import("./resolved-formatting-state.ts").ResolvedParagraphFormatting,
187
+ currentChars: number,
188
+ ): number {
189
+ const avgCharWidth = formatting.averageCharWidthTwips;
190
+ const defaultTabInterval = 720;
191
+ const currentPosition = currentChars * avgCharWidth + formatting.indentLeft;
192
+
193
+ if (formatting.tabStops.length === 0) {
194
+ const nextTab =
195
+ Math.ceil((currentPosition + 1) / defaultTabInterval) * defaultTabInterval;
196
+ const advance = nextTab - currentPosition;
197
+ return Math.max(1, Math.round(advance / avgCharWidth));
198
+ }
199
+
200
+ for (const tab of formatting.tabStops) {
201
+ if (tab.position > currentPosition) {
202
+ const advance = tab.position - currentPosition;
203
+ return Math.max(1, Math.round(advance / avgCharWidth));
204
+ }
205
+ }
206
+
207
+ return 4;
208
+ }