@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,409 @@
1
+ import type {
2
+ BorderSpec,
3
+ CellShading,
4
+ TableBorders,
5
+ TableCellBorders,
6
+ TableCellMargins,
7
+ TableLook,
8
+ TableNode,
9
+ TableStyleConditionalRegion,
10
+ TableStyleDefinition,
11
+ TableStyleFormatting,
12
+ TableWidth,
13
+ } from "../model/canonical-document.ts";
14
+
15
+ export interface ResolvedTableCellStyle {
16
+ width?: TableWidth;
17
+ borders?: TableCellBorders;
18
+ shading?: CellShading;
19
+ verticalAlign?: "top" | "center" | "bottom";
20
+ activeConditionalRegions: TableStyleConditionalRegion[];
21
+ }
22
+
23
+ export interface ResolvedTableRowStyle {
24
+ height?: number;
25
+ heightRule?: "auto" | "atLeast" | "exact";
26
+ isHeader?: boolean;
27
+ activeConditionalRegions: TableStyleConditionalRegion[];
28
+ }
29
+
30
+ export interface ResolvedTableStyleResolution {
31
+ rawTblLook?: TableLook;
32
+ effectiveTblLook: TableLook;
33
+ table?: TableStyleFormatting["table"];
34
+ rows: Array<{
35
+ style: ResolvedTableRowStyle;
36
+ cells: ResolvedTableCellStyle[];
37
+ }>;
38
+ }
39
+
40
+ const DEFAULT_EFFECTIVE_TABLE_LOOK: TableLook = {
41
+ val: "04A0",
42
+ firstRow: true,
43
+ lastRow: false,
44
+ firstColumn: true,
45
+ lastColumn: false,
46
+ noHBand: false,
47
+ noVBand: true,
48
+ };
49
+
50
+ const CONDITIONAL_REGION_ORDER: TableStyleConditionalRegion[] = [
51
+ "band1Horz",
52
+ "band2Horz",
53
+ "band1Vert",
54
+ "band2Vert",
55
+ "firstColumn",
56
+ "lastColumn",
57
+ "firstRow",
58
+ "lastRow",
59
+ ];
60
+
61
+ export function resolveTableStyleResolution(
62
+ table: TableNode,
63
+ tableStyles: Record<string, TableStyleDefinition>,
64
+ ): ResolvedTableStyleResolution {
65
+ const resolvedStyle = table.styleId ? resolveTableStyleDefinition(table.styleId, tableStyles) : {};
66
+ const effectiveTblLook = resolveEffectiveTableLook(table.tblLook, resolvedStyle.formatting?.table?.tblLook);
67
+ const columnCount = table.gridColumns.length > 0
68
+ ? table.gridColumns.length
69
+ : table.rows.reduce(
70
+ (max, row) =>
71
+ Math.max(
72
+ max,
73
+ (row.gridBefore ?? 0) +
74
+ row.cells.reduce((sum, cell) => sum + (cell.gridSpan ?? 1), 0) +
75
+ (row.gridAfter ?? 0),
76
+ ),
77
+ 0,
78
+ );
79
+
80
+ const rows = table.rows.map((row, rowIndex) => {
81
+ const rowRegions = getActiveRowRegions(rowIndex, table.rows.length, effectiveTblLook);
82
+ const rowStyleFormatting = mergeStyleFormattingRow(
83
+ resolvedStyle.formatting?.row,
84
+ rowRegions.map((region) => resolvedStyle.conditionalFormatting?.[region]?.row),
85
+ {
86
+ ...(row.height !== undefined ? { height: row.height } : {}),
87
+ ...(row.heightRule ? { heightRule: row.heightRule } : {}),
88
+ ...(row.isHeader !== undefined ? { isHeader: row.isHeader } : {}),
89
+ },
90
+ );
91
+
92
+ let columnCursor = row.gridBefore ?? 0;
93
+ const cells = row.cells.map((cell) => {
94
+ const startColumn = columnCursor;
95
+ const span = cell.gridSpan ?? 1;
96
+ const endColumn = startColumn + span - 1;
97
+ columnCursor += span;
98
+
99
+ const cellRegions = getActiveCellRegions(
100
+ rowRegions,
101
+ startColumn,
102
+ endColumn,
103
+ columnCount,
104
+ effectiveTblLook,
105
+ );
106
+ const cellStyleFormatting = mergeStyleFormattingCell(
107
+ resolvedStyle.formatting?.cell,
108
+ cellRegions.map((region) => resolvedStyle.conditionalFormatting?.[region]?.cell),
109
+ {
110
+ ...(cell.width ? { width: cell.width } : {}),
111
+ ...(cell.borders ? { borders: cell.borders } : {}),
112
+ ...(cell.shading ? { shading: cell.shading } : {}),
113
+ ...(cell.verticalAlign ? { verticalAlign: cell.verticalAlign } : {}),
114
+ },
115
+ );
116
+
117
+ return {
118
+ ...(cellStyleFormatting ?? {}),
119
+ activeConditionalRegions: cellRegions,
120
+ };
121
+ });
122
+
123
+ return {
124
+ style: {
125
+ ...(rowStyleFormatting ?? {}),
126
+ activeConditionalRegions: rowRegions,
127
+ },
128
+ cells,
129
+ };
130
+ });
131
+
132
+ const tableFormatting = mergeTableFormatting(
133
+ resolvedStyle.formatting?.table,
134
+ {
135
+ ...(table.width ? { width: table.width } : {}),
136
+ ...(table.alignment ? { alignment: table.alignment } : {}),
137
+ ...(table.borders ? { borders: table.borders } : {}),
138
+ ...(table.cellMargins ? { cellMargins: table.cellMargins } : {}),
139
+ ...(table.tblLook ? { tblLook: table.tblLook } : {}),
140
+ },
141
+ );
142
+
143
+ return {
144
+ ...(table.tblLook ? { rawTblLook: table.tblLook } : {}),
145
+ effectiveTblLook,
146
+ ...(tableFormatting ? { table: tableFormatting } : {}),
147
+ rows,
148
+ };
149
+ }
150
+
151
+ function resolveTableStyleDefinition(
152
+ styleId: string,
153
+ tableStyles: Record<string, TableStyleDefinition>,
154
+ visited = new Set<string>(),
155
+ ): {
156
+ formatting?: TableStyleFormatting;
157
+ conditionalFormatting?: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>>;
158
+ } {
159
+ if (visited.has(styleId)) {
160
+ return {};
161
+ }
162
+ visited.add(styleId);
163
+
164
+ const style = tableStyles[styleId];
165
+ if (!style) {
166
+ return {};
167
+ }
168
+
169
+ const inherited = style.basedOn
170
+ ? resolveTableStyleDefinition(style.basedOn, tableStyles, visited)
171
+ : {};
172
+
173
+ return {
174
+ formatting: mergeWholeStyleFormatting(inherited.formatting, style.formatting),
175
+ conditionalFormatting: mergeConditionalFormatting(
176
+ inherited.conditionalFormatting,
177
+ style.conditionalFormatting,
178
+ ),
179
+ };
180
+ }
181
+
182
+ function resolveEffectiveTableLook(rawTblLook: TableLook | undefined, styleTblLook: TableLook | undefined): TableLook {
183
+ const decodedStyleLook = decodeTableLookMask(styleTblLook?.val);
184
+ const decodedRawLook = decodeTableLookMask(rawTblLook?.val);
185
+ return {
186
+ ...DEFAULT_EFFECTIVE_TABLE_LOOK,
187
+ ...(decodedStyleLook ?? {}),
188
+ ...(styleTblLook ?? {}),
189
+ ...(decodedRawLook ?? {}),
190
+ ...(rawTblLook ?? {}),
191
+ };
192
+ }
193
+
194
+ function decodeTableLookMask(val: string | undefined): TableLook | undefined {
195
+ if (!val) {
196
+ return undefined;
197
+ }
198
+
199
+ const mask = Number.parseInt(val, 16);
200
+ if (Number.isNaN(mask)) {
201
+ return { val };
202
+ }
203
+
204
+ return {
205
+ val,
206
+ firstRow: (mask & 0x0020) !== 0,
207
+ lastRow: (mask & 0x0040) !== 0,
208
+ firstColumn: (mask & 0x0080) !== 0,
209
+ lastColumn: (mask & 0x0100) !== 0,
210
+ noHBand: (mask & 0x0200) !== 0,
211
+ noVBand: (mask & 0x0400) !== 0,
212
+ };
213
+ }
214
+
215
+ function getActiveRowRegions(
216
+ rowIndex: number,
217
+ rowCount: number,
218
+ effectiveTblLook: TableLook,
219
+ ): TableStyleConditionalRegion[] {
220
+ const active = new Set<TableStyleConditionalRegion>();
221
+
222
+ if (effectiveTblLook.firstRow && rowIndex === 0) {
223
+ active.add("firstRow");
224
+ }
225
+ if (effectiveTblLook.lastRow && rowIndex === rowCount - 1) {
226
+ active.add("lastRow");
227
+ }
228
+
229
+ if (!effectiveTblLook.noHBand) {
230
+ const bandStart = effectiveTblLook.firstRow ? 1 : 0;
231
+ const bandEndExclusive = effectiveTblLook.lastRow ? rowCount - 1 : rowCount;
232
+ if (rowIndex >= bandStart && rowIndex < bandEndExclusive) {
233
+ const bandIndex = rowIndex - bandStart;
234
+ active.add(bandIndex % 2 === 0 ? "band1Horz" : "band2Horz");
235
+ }
236
+ }
237
+
238
+ return CONDITIONAL_REGION_ORDER.filter((region) => active.has(region));
239
+ }
240
+
241
+ function getActiveCellRegions(
242
+ rowRegions: TableStyleConditionalRegion[],
243
+ startColumn: number,
244
+ endColumn: number,
245
+ columnCount: number,
246
+ effectiveTblLook: TableLook,
247
+ ): TableStyleConditionalRegion[] {
248
+ const active = new Set<TableStyleConditionalRegion>(rowRegions);
249
+
250
+ if (effectiveTblLook.firstColumn && startColumn === 0) {
251
+ active.add("firstColumn");
252
+ }
253
+ if (effectiveTblLook.lastColumn && endColumn === columnCount - 1) {
254
+ active.add("lastColumn");
255
+ }
256
+
257
+ if (!effectiveTblLook.noVBand) {
258
+ const bandStart = effectiveTblLook.firstColumn ? 1 : 0;
259
+ const bandEndExclusive = effectiveTblLook.lastColumn ? columnCount - 1 : columnCount;
260
+ if (startColumn >= bandStart && startColumn < bandEndExclusive) {
261
+ const bandIndex = startColumn - bandStart;
262
+ active.add(bandIndex % 2 === 0 ? "band1Vert" : "band2Vert");
263
+ }
264
+ }
265
+
266
+ return CONDITIONAL_REGION_ORDER.filter((region) => active.has(region));
267
+ }
268
+
269
+ function mergeConditionalFormatting(
270
+ base: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> | undefined,
271
+ override: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> | undefined,
272
+ ): Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> | undefined {
273
+ if (!base && !override) {
274
+ return undefined;
275
+ }
276
+
277
+ const merged: Partial<Record<TableStyleConditionalRegion, TableStyleFormatting>> = {};
278
+ for (const region of CONDITIONAL_REGION_ORDER) {
279
+ const formatting = mergeWholeStyleFormatting(base?.[region], override?.[region]);
280
+ if (formatting) {
281
+ merged[region] = formatting;
282
+ }
283
+ }
284
+ return Object.keys(merged).length > 0 ? merged : undefined;
285
+ }
286
+
287
+ function mergeWholeStyleFormatting(
288
+ base: TableStyleFormatting | undefined,
289
+ override: TableStyleFormatting | undefined,
290
+ ): TableStyleFormatting | undefined {
291
+ if (!base && !override) {
292
+ return undefined;
293
+ }
294
+
295
+ const merged: TableStyleFormatting = {};
296
+ const table = mergeTableFormatting(base?.table, override?.table);
297
+ const row = mergeRowFormatting(base?.row, override?.row);
298
+ const cell = mergeCellFormatting(base?.cell, override?.cell);
299
+
300
+ if (table) merged.table = table;
301
+ if (row) merged.row = row;
302
+ if (cell) merged.cell = cell;
303
+
304
+ return Object.keys(merged).length > 0 ? merged : undefined;
305
+ }
306
+
307
+ function mergeStyleFormattingRow(
308
+ base: TableStyleFormatting["row"],
309
+ conditionals: Array<TableStyleFormatting["row"] | undefined>,
310
+ direct: TableStyleFormatting["row"],
311
+ ): TableStyleFormatting["row"] {
312
+ const styleResolved = conditionals.reduce(
313
+ (current, formatting) => mergeRowFormatting(current, formatting),
314
+ base,
315
+ );
316
+ return mergeRowFormatting(styleResolved, direct);
317
+ }
318
+
319
+ function mergeStyleFormattingCell(
320
+ base: TableStyleFormatting["cell"],
321
+ conditionals: Array<TableStyleFormatting["cell"] | undefined>,
322
+ direct: TableStyleFormatting["cell"],
323
+ ): TableStyleFormatting["cell"] {
324
+ const styleResolved = conditionals.reduce(
325
+ (current, formatting) => mergeCellFormatting(current, formatting),
326
+ base,
327
+ );
328
+ return mergeCellFormatting(styleResolved, direct);
329
+ }
330
+
331
+ function mergeTableFormatting(
332
+ base: TableStyleFormatting["table"],
333
+ override: TableStyleFormatting["table"],
334
+ ): TableStyleFormatting["table"] {
335
+ if (!base && !override) {
336
+ return undefined;
337
+ }
338
+ return {
339
+ ...(base ?? {}),
340
+ ...(override ?? {}),
341
+ borders: mergeBorderMap(base?.borders, override?.borders),
342
+ cellMargins: mergePlainObject(base?.cellMargins, override?.cellMargins),
343
+ tblLook: mergeTableLook(base?.tblLook, override?.tblLook),
344
+ };
345
+ }
346
+
347
+ function mergeRowFormatting(
348
+ base: TableStyleFormatting["row"],
349
+ override: TableStyleFormatting["row"],
350
+ ): TableStyleFormatting["row"] {
351
+ if (!base && !override) {
352
+ return undefined;
353
+ }
354
+ return { ...(base ?? {}), ...(override ?? {}) };
355
+ }
356
+
357
+ function mergeCellFormatting(
358
+ base: TableStyleFormatting["cell"],
359
+ override: TableStyleFormatting["cell"],
360
+ ): TableStyleFormatting["cell"] {
361
+ if (!base && !override) {
362
+ return undefined;
363
+ }
364
+ return {
365
+ ...(base ?? {}),
366
+ ...(override ?? {}),
367
+ borders: mergeBorderMap(base?.borders, override?.borders),
368
+ shading: mergePlainObject(base?.shading, override?.shading),
369
+ };
370
+ }
371
+
372
+ function mergeBorderMap<T extends TableBorders | TableCellBorders>(
373
+ base: T | undefined,
374
+ override: T | undefined,
375
+ ): T | undefined {
376
+ if (!base && !override) {
377
+ return undefined;
378
+ }
379
+
380
+ const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
381
+ const merged = {} as T;
382
+ for (const side of sides) {
383
+ const spec = mergePlainObject(base?.[side], override?.[side]) as BorderSpec | undefined;
384
+ if (spec) {
385
+ merged[side] = spec as T[typeof side];
386
+ }
387
+ }
388
+ return Object.keys(merged).length > 0 ? merged : undefined;
389
+ }
390
+
391
+ function mergePlainObject<T extends object>(base: T | undefined, override: T | undefined): T | undefined {
392
+ if (!base && !override) {
393
+ return undefined;
394
+ }
395
+ return { ...(base ?? {}), ...(override ?? {}) } as T;
396
+ }
397
+
398
+ function mergeTableLook(base: TableLook | undefined, override: TableLook | undefined): TableLook | undefined {
399
+ if (!base && !override) {
400
+ return undefined;
401
+ }
402
+ if (!override) {
403
+ return base ? { ...base } : undefined;
404
+ }
405
+ if (override.val !== undefined) {
406
+ return { ...override };
407
+ }
408
+ return { ...(base ?? {}), ...override };
409
+ }
@@ -168,7 +168,7 @@ export function deriveLayoutMeasurement(
168
168
  ? selectionOrPosition.activeRange.at
169
169
  : selectionOrPosition.head;
170
170
  const block = surface ? findBlockAtPosition(surface.blocks, selectionHead) : null;
171
- const tabStops = block?.kind === "paragraph" && block.tabStops ? block.tabStops : [];
171
+ const tabStops = resolveMeasurementTabStops(block);
172
172
  const listMarkerLane = deriveListMarkerLane(block);
173
173
 
174
174
  return {
@@ -469,9 +469,25 @@ function deriveListMarkerLane(
469
469
  block: SurfaceBlockSnapshot | null,
470
470
  ): { indent: number; markerWidth: number } | null {
471
471
  if (!block || block.kind !== "paragraph" || !block.numbering) return null;
472
+ const resolvedMarkerLane = block.resolvedNumbering?.geometry.markerLane;
473
+ if (resolvedMarkerLane) {
474
+ return {
475
+ indent: resolvedMarkerLane.textStart,
476
+ markerWidth: resolvedMarkerLane.width,
477
+ };
478
+ }
472
479
  const indent = block.indentation?.hanging ?? block.indentation?.left ?? 360;
473
480
  return {
474
481
  indent,
475
482
  markerWidth: Math.min(indent, 360),
476
483
  };
477
484
  }
485
+
486
+ function resolveMeasurementTabStops(
487
+ block: SurfaceBlockSnapshot | null,
488
+ ): Array<{ pos: number; val?: string; leader?: string }> {
489
+ if (!block || block.kind !== "paragraph") {
490
+ return [];
491
+ }
492
+ return block.resolvedNumbering?.geometry.tabStops ?? block.tabStops ?? [];
493
+ }
@@ -12,6 +12,8 @@ import type {
12
12
  WorkflowCandidateRangeOptions,
13
13
  WorkflowFieldMarkup,
14
14
  WorkflowHighlightMarkup,
15
+ WorkflowMetadataMarkup,
16
+ WorkflowMetadataSnapshot,
15
17
  WorkflowMarkupItem,
16
18
  WorkflowMarkupSnapshot,
17
19
  WorkflowOpaqueFragmentMarkup,
@@ -21,18 +23,18 @@ import type {
21
23
  } from "../api/public-types";
22
24
  import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
23
25
  import { searchSurfaceBlocks } from "../core/search/search-text.ts";
24
- import { describeOpaqueFragment } from "../preservation/store.ts";
26
+ import { describeOpaqueFragment, isBlockedImportFeatureKey } from "../preservation/store.ts";
25
27
  import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
26
28
 
27
- const BLOCKED_IMPORT_FEATURE_KEYS = new Set(["alt-chunk", "alternate-content", "custom-xml"]);
28
-
29
29
  export function collectWorkflowMarkupSnapshot(input: {
30
30
  renderSnapshot: RuntimeRenderSnapshot;
31
31
  fieldSnapshot: FieldSnapshot;
32
32
  protectionSnapshot: ProtectionSnapshot;
33
33
  preservation: CanonicalDocumentEnvelope["preservation"];
34
+ workflowMetadataSnapshot?: WorkflowMetadataSnapshot;
34
35
  }): WorkflowMarkupSnapshot {
35
36
  const highlights: WorkflowHighlightMarkup[] = [];
37
+ const metadata = collectWorkflowMetadataMarkup(input.workflowMetadataSnapshot);
36
38
  const fields: WorkflowFieldMarkup[] = [];
37
39
  const opaqueFragments: WorkflowOpaqueFragmentMarkup[] = [];
38
40
  const surface = input.renderSnapshot.surface;
@@ -113,6 +115,7 @@ export function collectWorkflowMarkupSnapshot(input: {
113
115
 
114
116
  const items: WorkflowMarkupItem[] = [
115
117
  ...highlights,
118
+ ...metadata,
116
119
  ...comments,
117
120
  ...revisions,
118
121
  ...fields,
@@ -124,6 +127,7 @@ export function collectWorkflowMarkupSnapshot(input: {
124
127
  totalCount: items.length,
125
128
  items,
126
129
  highlights,
130
+ metadata,
127
131
  comments,
128
132
  revisions,
129
133
  fields,
@@ -132,6 +136,42 @@ export function collectWorkflowMarkupSnapshot(input: {
132
136
  };
133
137
  }
134
138
 
139
+ function collectWorkflowMetadataMarkup(
140
+ snapshot?: WorkflowMetadataSnapshot,
141
+ ): WorkflowMetadataMarkup[] {
142
+ if (!snapshot) {
143
+ return [];
144
+ }
145
+
146
+ const definitionsById = new Map(
147
+ snapshot.definitions.map((definition) => [definition.metadataId, definition] as const),
148
+ );
149
+
150
+ return snapshot.entries.flatMap((entry) => {
151
+ const definition = definitionsById.get(entry.metadataId);
152
+ if (!definition) {
153
+ return [];
154
+ }
155
+
156
+ return [{
157
+ markupId: `metadata:${entry.entryId}`,
158
+ kind: "metadata",
159
+ entryId: entry.entryId,
160
+ metadataId: entry.metadataId,
161
+ anchor: entry.anchor,
162
+ storyTarget: entry.storyTarget,
163
+ label: definition.label,
164
+ excerpt: definition.kind,
165
+ color: definition.color,
166
+ icon: definition.icon,
167
+ persistence: definition.persistence,
168
+ value: entry.value,
169
+ scopeId: entry.scopeId,
170
+ workItemId: entry.workItemId,
171
+ } satisfies WorkflowMetadataMarkup];
172
+ });
173
+ }
174
+
135
175
  export function deriveWorkflowCandidateRangesFromMarkup(
136
176
  snapshot: WorkflowMarkupSnapshot,
137
177
  options: WorkflowCandidateRangeOptions = {},
@@ -184,10 +224,6 @@ function collectSurfaceMarkup(
184
224
 
185
225
  const fragment = preservation.opaqueFragments[block.fragmentId];
186
226
  const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
187
- const blockedReasonCode =
188
- fragment && BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor?.featureKey ?? "")
189
- ? "workflow_blocked_import"
190
- : "workflow_preserve_only";
191
227
  opaqueFragments.push({
192
228
  markupId: `opaque:${block.fragmentId}`,
193
229
  kind: "opaque_fragment",
@@ -198,7 +234,11 @@ function collectSurfaceMarkup(
198
234
  label: block.label,
199
235
  excerpt: block.detail,
200
236
  detail: block.detail,
201
- blockedReasonCode,
237
+ blockedReasonCode:
238
+ block.blockedReasonCode ??
239
+ (fragment && descriptor && isBlockedImportFeatureKey(descriptor.featureKey)
240
+ ? "workflow_blocked_import"
241
+ : "workflow_preserve_only"),
202
242
  });
203
243
  }
204
244
  }
@@ -227,10 +267,6 @@ function collectSegmentMarkup(
227
267
  if (segment.kind === "opaque_inline") {
228
268
  const fragment = preservation.opaqueFragments[segment.fragmentId];
229
269
  const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
230
- const blockedReasonCode =
231
- fragment && BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor?.featureKey ?? "")
232
- ? "workflow_blocked_import"
233
- : "workflow_preserve_only";
234
270
  opaqueFragments.push({
235
271
  markupId: `opaque:${segment.fragmentId}`,
236
272
  kind: "opaque_fragment",
@@ -241,7 +277,11 @@ function collectSegmentMarkup(
241
277
  label: segment.label,
242
278
  excerpt: segment.detail,
243
279
  detail: segment.detail,
244
- blockedReasonCode,
280
+ blockedReasonCode:
281
+ segment.blockedReasonCode ??
282
+ (fragment && descriptor && isBlockedImportFeatureKey(descriptor.featureKey)
283
+ ? "workflow_blocked_import"
284
+ : "workflow_preserve_only"),
245
285
  });
246
286
  }
247
287
  }
@@ -308,7 +348,7 @@ function collectOpaqueFragmentMarkup(
308
348
  )
309
349
  .map((fragment) => {
310
350
  const descriptor = describeOpaqueFragment(fragment);
311
- const blockedReasonCode = BLOCKED_IMPORT_FEATURE_KEYS.has(descriptor.featureKey)
351
+ const blockedReasonCode = isBlockedImportFeatureKey(descriptor.featureKey)
312
352
  ? "workflow_blocked_import"
313
353
  : "workflow_preserve_only";
314
354