@beyondwork/docx-react-component 1.0.28 → 1.0.30

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/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -0,0 +1,137 @@
1
+ import type { SnapshotRefreshHints, WordReviewEditorEvent } from "../api/public-types";
2
+
3
+ export function describeEventImpact(
4
+ event: WordReviewEditorEvent,
5
+ ): SnapshotRefreshHints {
6
+ switch (event.type) {
7
+ case "selection_changed":
8
+ return {
9
+ invalidate: ["selection", "locations", "contextAnalytics"],
10
+ staleTargets: ["selection_only"],
11
+ changeKinds: ["selection"],
12
+ };
13
+ case "story_changed":
14
+ return {
15
+ invalidate: ["selection", "render", "locations", "outline", "sections", "contextAnalytics"],
16
+ staleTargets: ["anchors"],
17
+ changeKinds: ["selection", "structure"],
18
+ };
19
+ case "workflow_overlay_changed":
20
+ case "workflow_active_work_item_changed":
21
+ return {
22
+ invalidate: [
23
+ "workflowScope",
24
+ "workflowMarkup",
25
+ "locations",
26
+ "reviewWork",
27
+ "chunks",
28
+ "contextAnalytics",
29
+ ],
30
+ staleTargets: ["anchors"],
31
+ changeKinds: ["workflow"],
32
+ };
33
+ case "workflow_metadata_changed":
34
+ return {
35
+ invalidate: ["workflowMarkup", "render", "locations", "reviewWork", "chunks"],
36
+ staleTargets: ["anchors"],
37
+ changeKinds: ["workflow"],
38
+ };
39
+ case "change_authored":
40
+ case "change_accepted":
41
+ case "change_rejected":
42
+ case "suggestion_authored":
43
+ case "suggestion_updated":
44
+ case "comment_added":
45
+ case "comment_resolved":
46
+ return {
47
+ invalidate: [
48
+ "render",
49
+ "comments",
50
+ "trackedChanges",
51
+ "navigation",
52
+ "outline",
53
+ "toc",
54
+ "sections",
55
+ "locations",
56
+ "reviewWork",
57
+ "chunks",
58
+ "textStream",
59
+ "contextAnalytics",
60
+ ],
61
+ staleTargets: ["search_results", "anchors"],
62
+ changeKinds: ["content", "review", "structure"],
63
+ };
64
+ case "warning_added":
65
+ case "warning_cleared":
66
+ case "error":
67
+ return {
68
+ invalidate: ["render", "contextAnalytics"],
69
+ staleTargets: ["none"],
70
+ changeKinds: ["content"],
71
+ };
72
+ case "snapshot_saved":
73
+ return {
74
+ invalidate: [],
75
+ staleTargets: ["none"],
76
+ changeKinds: ["checkpoint"],
77
+ checkpointType: "snapshot",
78
+ };
79
+ case "session_saved":
80
+ return {
81
+ invalidate: [],
82
+ staleTargets: ["none"],
83
+ changeKinds: ["checkpoint"],
84
+ checkpointType: "session",
85
+ };
86
+ case "export_completed":
87
+ return {
88
+ invalidate: [],
89
+ staleTargets: ["none"],
90
+ changeKinds: ["checkpoint"],
91
+ checkpointType: "export",
92
+ };
93
+ case "command_blocked":
94
+ return {
95
+ invalidate: ["workflowScope", "locations", "contextAnalytics"],
96
+ staleTargets: ["anchors"],
97
+ changeKinds: ["workflow"],
98
+ };
99
+ case "ready":
100
+ return {
101
+ invalidate: [
102
+ "render",
103
+ "navigation",
104
+ "outline",
105
+ "toc",
106
+ "sections",
107
+ "locations",
108
+ "reviewWork",
109
+ "chunks",
110
+ "textStream",
111
+ "contextAnalytics",
112
+ ],
113
+ staleTargets: ["none"],
114
+ changeKinds: ["structure"],
115
+ };
116
+ case "context_analytics_changed":
117
+ return {
118
+ invalidate: ["contextAnalytics"],
119
+ staleTargets: ["none"],
120
+ changeKinds: ["selection", "content", "review", "workflow"],
121
+ };
122
+ case "dirty_changed":
123
+ case "autosave_state":
124
+ case "host_annotation_overlay_changed":
125
+ return {
126
+ invalidate: ["render"],
127
+ staleTargets: ["none"],
128
+ changeKinds: ["checkpoint"],
129
+ };
130
+ }
131
+
132
+ return {
133
+ invalidate: ["render"],
134
+ staleTargets: ["none"],
135
+ changeKinds: ["content"],
136
+ };
137
+ }
@@ -3,6 +3,11 @@ import type {
3
3
  NumberingLevelDefinition,
4
4
  ParagraphNode,
5
5
  } from "../model/canonical-document.ts";
6
+ import {
7
+ DEFAULT_NUMBERING_START_AT,
8
+ resolveNumberingDefinitionSet,
9
+ type ResolvedNumberingGeometry,
10
+ } from "./resolved-numbering-geometry.ts";
6
11
 
7
12
  interface NumberingSequenceState {
8
13
  counters: Array<number | undefined>;
@@ -10,56 +15,83 @@ interface NumberingSequenceState {
10
15
  }
11
16
 
12
17
  export interface NumberingPrefixResult {
13
- text: string;
18
+ text: string | null;
19
+ level: number;
20
+ format: string;
21
+ startAt: number;
14
22
  suffix?: "tab" | "space" | "nothing";
23
+ paragraphStyleId?: string;
24
+ isLegalNumbering?: boolean;
25
+ geometry: ResolvedNumberingGeometry;
15
26
  }
16
27
 
17
28
  export interface NumberingPrefixResolver {
18
29
  resolve(numbering: ParagraphNode["numbering"] | undefined): string | null;
19
- resolveDetailed(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null;
30
+ resolveDetailed(
31
+ numbering: ParagraphNode["numbering"] | undefined,
32
+ paragraph?: ParagraphNode,
33
+ ): NumberingPrefixResult | null;
34
+ resolveParagraph(paragraph: ParagraphNode): NumberingPrefixResult | null;
20
35
  }
21
36
 
22
- const DEFAULT_START_AT = 1;
23
-
24
37
  export function createNumberingPrefixResolver(
25
38
  catalog: NumberingCatalog,
26
39
  ): NumberingPrefixResolver {
27
40
  const sequenceStates = new Map<string, NumberingSequenceState>();
28
41
 
29
- function resolveInternal(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null {
30
- if (!numbering) {
42
+ function resolveInternal(
43
+ numbering: ParagraphNode["numbering"] | undefined,
44
+ paragraph?: ParagraphNode,
45
+ ): NumberingPrefixResult | null {
46
+ const resolved = resolveNumberingDefinitionSet(
47
+ catalog,
48
+ paragraph?.numbering ?? numbering,
49
+ paragraph,
50
+ );
51
+ if (!resolved) {
31
52
  return null;
32
53
  }
33
54
 
34
- const instance = catalog.instances[numbering.numberingInstanceId];
35
- if (!instance) {
55
+ const resolvedNumbering = paragraph?.numbering ?? numbering;
56
+ if (!resolvedNumbering) {
36
57
  return null;
37
58
  }
38
59
 
39
- const definition = catalog.abstractDefinitions[instance.abstractNumberingId];
40
- if (!definition) {
41
- return null;
42
- }
60
+ const sequenceState = getSequenceState(sequenceStates, resolvedNumbering.numberingInstanceId);
61
+ advanceSequence(sequenceState, resolved.effectiveLevel.level, resolved.effectiveLevels);
43
62
 
44
- const levelDefinitions = new Map(
45
- definition.levels.map((level) => [level.level, level] as const),
63
+ // When isLegalNumbering is true, use decimal format for all referenced levels
64
+ const effectiveLevelDefs = resolved.effectiveLevel.isLegalNumbering
65
+ ? new Map(
66
+ Array.from(resolved.effectiveLevels.entries()).map(([level, definition]) => [
67
+ level,
68
+ { ...definition, format: "decimal" },
69
+ ]),
70
+ )
71
+ : resolved.effectiveLevels;
72
+
73
+ const text = renderLevelText(
74
+ resolved.effectiveLevel.text,
75
+ sequenceState.counters,
76
+ effectiveLevelDefs,
46
77
  );
47
- const levelDefinition = levelDefinitions.get(numbering.level);
48
- if (!levelDefinition || levelDefinition.format === "none") {
78
+ if (resolved.effectiveLevel.format !== "none" && text === null) {
49
79
  return null;
50
80
  }
51
-
52
- const sequenceState = getSequenceState(sequenceStates, numbering.numberingInstanceId);
53
- advanceSequence(sequenceState, numbering.level, levelDefinitions, instance.overrides);
54
-
55
- // When isLegalNumbering is true, use decimal format for all referenced levels
56
- const effectiveLevelDefs = levelDefinition.isLegalNumbering
57
- ? new Map(Array.from(levelDefinitions.entries()).map(([k, v]) => [k, { ...v, format: "decimal" }]))
58
- : levelDefinitions;
59
-
60
- const text = renderLevelText(levelDefinition.text, sequenceState.counters, effectiveLevelDefs);
61
- if (text === null) return null;
62
- return { text, suffix: levelDefinition.suffix };
81
+ const visibleText = resolved.effectiveLevel.format === "none" ? null : text;
82
+
83
+ return {
84
+ text: visibleText,
85
+ level: resolved.effectiveLevel.level,
86
+ format: resolved.effectiveLevel.format,
87
+ startAt: resolved.effectiveLevel.startAt ?? DEFAULT_NUMBERING_START_AT,
88
+ ...(resolved.effectiveLevel.suffix ? { suffix: resolved.effectiveLevel.suffix } : {}),
89
+ ...(resolved.effectiveLevel.paragraphStyleId
90
+ ? { paragraphStyleId: resolved.effectiveLevel.paragraphStyleId }
91
+ : {}),
92
+ ...(resolved.effectiveLevel.isLegalNumbering ? { isLegalNumbering: true } : {}),
93
+ geometry: resolved.geometry,
94
+ };
63
95
  }
64
96
 
65
97
  return {
@@ -67,8 +99,11 @@ export function createNumberingPrefixResolver(
67
99
  const result = resolveInternal(numbering);
68
100
  return result?.text ?? null;
69
101
  },
70
- resolveDetailed(numbering) {
71
- return resolveInternal(numbering);
102
+ resolveDetailed(numbering, paragraph) {
103
+ return resolveInternal(numbering, paragraph);
104
+ },
105
+ resolveParagraph(paragraph) {
106
+ return resolveInternal(paragraph.numbering, paragraph);
72
107
  },
73
108
  };
74
109
  }
@@ -94,13 +129,12 @@ function advanceSequence(
94
129
  state: NumberingSequenceState,
95
130
  currentLevel: number,
96
131
  levelDefinitions: Map<number, NumberingLevelDefinition>,
97
- overrides: NumberingCatalog["instances"][string]["overrides"],
98
132
  ): void {
99
133
  if (state.lastLevel !== null && currentLevel <= state.lastLevel) {
100
134
  state.counters.length = currentLevel + 1;
101
135
  }
102
136
 
103
- const startAt = getLevelStartAt(currentLevel, levelDefinitions, overrides);
137
+ const startAt = getLevelStartAt(currentLevel, levelDefinitions);
104
138
  const currentValue = state.counters[currentLevel];
105
139
  state.counters[currentLevel] =
106
140
  currentValue === undefined ? startAt : currentValue + 1;
@@ -110,14 +144,8 @@ function advanceSequence(
110
144
  function getLevelStartAt(
111
145
  level: number,
112
146
  levelDefinitions: Map<number, NumberingLevelDefinition>,
113
- overrides: NumberingCatalog["instances"][string]["overrides"],
114
147
  ): number {
115
- const override = overrides.find((entry) => entry.level === level);
116
- if (override?.startAt !== undefined) {
117
- return override.startAt;
118
- }
119
-
120
- return levelDefinitions.get(level)?.startAt ?? DEFAULT_START_AT;
148
+ return levelDefinitions.get(level)?.startAt ?? DEFAULT_NUMBERING_START_AT;
121
149
  }
122
150
 
123
151
  function renderLevelText(
@@ -38,10 +38,11 @@ export function estimateParagraphHeight(
38
38
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
39
39
  columnWidth: number,
40
40
  ): number {
41
+ const spacing = resolveParagraphSpacing(block);
41
42
  const lineHeight = estimateParagraphLineHeight(block);
42
43
  const lineCount = estimateParagraphLineCount(block, columnWidth);
43
- const spacingBefore = block.spacing?.before ?? 0;
44
- const spacingAfter = block.spacing?.after ?? 0;
44
+ const spacingBefore = spacing?.before ?? 0;
45
+ const spacingAfter = spacing?.after ?? 0;
45
46
  return Math.max(
46
47
  MIN_BLOCK_HEIGHT_TWIPS,
47
48
  lineHeight * lineCount + spacingBefore + spacingAfter,
@@ -51,7 +52,7 @@ export function estimateParagraphHeight(
51
52
  export function estimateParagraphLineHeight(
52
53
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
53
54
  ): number {
54
- const explicitLine = block.spacing?.line;
55
+ const explicitLine = resolveParagraphSpacing(block)?.line;
55
56
  if (typeof explicitLine === "number" && explicitLine > 0) {
56
57
  return explicitLine;
57
58
  }
@@ -75,36 +76,55 @@ export function estimateParagraphLineCount(
75
76
  columnWidth: number,
76
77
  ): number {
77
78
  const averageCharWidth = estimateAverageCharWidth(block);
78
- const charsPerLine = Math.max(12, Math.floor(columnWidth / averageCharWidth));
79
+ const lineMetrics = resolveParagraphLineMetrics(block, columnWidth, averageCharWidth);
79
80
  let lineCount = 1;
80
- let currentLineChars = estimatedPrefixLength(block);
81
+ let currentLineChars = lineMetrics.initialLineChars;
82
+ let currentLineCapacity = lineMetrics.firstLineCapacity;
81
83
 
82
84
  for (const segment of block.segments) {
83
85
  switch (segment.kind) {
84
86
  case "text":
85
87
  currentLineChars += Array.from(segment.text).length;
86
- while (currentLineChars > charsPerLine) {
88
+ while (currentLineChars > currentLineCapacity) {
87
89
  lineCount += 1;
88
- currentLineChars -= charsPerLine;
90
+ currentLineChars -= currentLineCapacity;
91
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
89
92
  }
90
93
  break;
91
94
  case "tab":
92
95
  currentLineChars += 4;
96
+ while (currentLineChars > currentLineCapacity) {
97
+ lineCount += 1;
98
+ currentLineChars -= currentLineCapacity;
99
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
100
+ }
93
101
  break;
94
102
  case "hard_break":
95
103
  lineCount += 1;
96
104
  currentLineChars = 0;
105
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
97
106
  break;
98
107
  case "image":
99
108
  lineCount += Math.max(1, Math.round(segment.display === "floating" ? 2 : 1));
100
109
  currentLineChars = 0;
110
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
101
111
  break;
102
112
  case "note_ref":
103
113
  currentLineChars += 1;
114
+ while (currentLineChars > currentLineCapacity) {
115
+ lineCount += 1;
116
+ currentLineChars -= currentLineCapacity;
117
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
118
+ }
104
119
  break;
105
120
  case "opaque_inline":
106
121
  if (segment.presentation !== "quiet-marker") {
107
122
  currentLineChars += segment.label.length > 0 ? 1 : 0;
123
+ while (currentLineChars > currentLineCapacity) {
124
+ lineCount += 1;
125
+ currentLineChars -= currentLineCapacity;
126
+ currentLineCapacity = lineMetrics.subsequentLineCapacity;
127
+ }
108
128
  }
109
129
  break;
110
130
  }
@@ -198,6 +218,79 @@ function estimateAverageCharWidth(
198
218
  return Math.max(96, Math.round(fontSizePoints * 12));
199
219
  }
200
220
 
221
+ function resolveParagraphSpacing(
222
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
223
+ ): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["spacing"] | undefined {
224
+ return block.resolvedNumbering?.geometry.spacing ?? block.spacing;
225
+ }
226
+
227
+ function resolveParagraphLineMetrics(
228
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
229
+ columnWidth: number,
230
+ averageCharWidth: number,
231
+ ): {
232
+ initialLineChars: number;
233
+ firstLineCapacity: number;
234
+ subsequentLineCapacity: number;
235
+ } {
236
+ const textColumn =
237
+ block.resolvedNumbering?.geometry.textColumn ??
238
+ deriveParagraphTextColumn(block.indentation);
239
+ const hasResolvedTextColumn = Boolean(textColumn);
240
+ const baseLineWidth = resolveTextColumnWidth(columnWidth, textColumn);
241
+ const firstLineWidth =
242
+ textColumn?.firstLine && textColumn.firstLine > 0
243
+ ? Math.max(averageCharWidth, baseLineWidth - textColumn.firstLine)
244
+ : baseLineWidth;
245
+
246
+ return {
247
+ initialLineChars: hasResolvedTextColumn ? 0 : estimatedPrefixLength(block),
248
+ firstLineCapacity: resolveCharsPerLine(firstLineWidth, averageCharWidth),
249
+ subsequentLineCapacity: resolveCharsPerLine(baseLineWidth, averageCharWidth),
250
+ };
251
+ }
252
+
253
+ function deriveParagraphTextColumn(
254
+ indentation: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"],
255
+ ): { start: number; right?: number; firstLine?: number; hanging?: number } | undefined {
256
+ if (!indentation) {
257
+ return undefined;
258
+ }
259
+ const hanging =
260
+ indentation.hanging ??
261
+ (typeof indentation.firstLine === "number" && indentation.firstLine < 0
262
+ ? Math.abs(indentation.firstLine)
263
+ : undefined);
264
+ const hasGeometry =
265
+ indentation.left !== undefined ||
266
+ indentation.right !== undefined ||
267
+ indentation.firstLine !== undefined ||
268
+ hanging !== undefined;
269
+ if (!hasGeometry) {
270
+ return undefined;
271
+ }
272
+ return {
273
+ start: indentation.left ?? 0,
274
+ ...(indentation.right !== undefined ? { right: indentation.right } : {}),
275
+ ...(indentation.firstLine !== undefined ? { firstLine: indentation.firstLine } : {}),
276
+ ...(hanging !== undefined ? { hanging } : {}),
277
+ };
278
+ }
279
+
280
+ function resolveTextColumnWidth(
281
+ columnWidth: number,
282
+ textColumn: { start: number; right?: number; firstLine?: number; hanging?: number } | undefined,
283
+ ): number {
284
+ if (!textColumn) {
285
+ return columnWidth;
286
+ }
287
+ return Math.max(1, columnWidth - textColumn.start - (textColumn.right ?? 0));
288
+ }
289
+
290
+ function resolveCharsPerLine(width: number, averageCharWidth: number): number {
291
+ return Math.max(1, Math.floor(width / averageCharWidth));
292
+ }
293
+
201
294
  function estimatedPrefixLength(
202
295
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
203
296
  ): number {