@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,318 @@
1
+ interface TableWidthLike {
2
+ value: number;
3
+ type: string;
4
+ }
5
+
6
+ interface BorderSpecLike {
7
+ value?: string;
8
+ size?: number;
9
+ space?: number;
10
+ color?: string;
11
+ }
12
+
13
+ interface TableBordersLike {
14
+ top?: BorderSpecLike;
15
+ left?: BorderSpecLike;
16
+ bottom?: BorderSpecLike;
17
+ right?: BorderSpecLike;
18
+ insideH?: BorderSpecLike;
19
+ insideV?: BorderSpecLike;
20
+ }
21
+
22
+ interface TableCellMarginsLike {
23
+ top?: number;
24
+ left?: number;
25
+ bottom?: number;
26
+ right?: number;
27
+ }
28
+
29
+ interface TableLookLike {
30
+ val?: string;
31
+ firstRow?: boolean;
32
+ lastRow?: boolean;
33
+ firstColumn?: boolean;
34
+ lastColumn?: boolean;
35
+ noHBand?: boolean;
36
+ noVBand?: boolean;
37
+ }
38
+
39
+ interface CellShadingLike {
40
+ val?: string;
41
+ color?: string;
42
+ fill?: string;
43
+ }
44
+
45
+ interface TablePropertiesLike {
46
+ propertiesXml?: string;
47
+ styleId?: string;
48
+ width?: TableWidthLike;
49
+ alignment?: string;
50
+ borders?: TableBordersLike;
51
+ cellMargins?: TableCellMarginsLike;
52
+ tblLook?: TableLookLike;
53
+ }
54
+
55
+ interface TableRowPropertiesLike {
56
+ propertiesXml?: string;
57
+ gridBefore?: number;
58
+ widthBefore?: TableWidthLike;
59
+ gridAfter?: number;
60
+ widthAfter?: TableWidthLike;
61
+ height?: number;
62
+ heightRule?: string;
63
+ isHeader?: boolean;
64
+ }
65
+
66
+ interface TableCellPropertiesLike {
67
+ propertiesXml?: string;
68
+ width?: TableWidthLike;
69
+ gridSpan?: number;
70
+ verticalMerge?: "restart" | "continue";
71
+ borders?: TableBordersLike;
72
+ shading?: CellShadingLike;
73
+ verticalAlign?: string;
74
+ }
75
+
76
+ interface PropertyStripSpec {
77
+ pairedTags?: string[];
78
+ selfClosingTags?: string[];
79
+ }
80
+
81
+ const TABLE_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
82
+ pairedTags: ["w:tblBorders", "w:tblCellMar"],
83
+ selfClosingTags: ["w:tblStyle", "w:tblW", "w:jc", "w:tblLook"],
84
+ };
85
+
86
+ const ROW_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
87
+ selfClosingTags: ["w:gridBefore", "w:wBefore", "w:gridAfter", "w:wAfter", "w:trHeight", "w:tblHeader"],
88
+ };
89
+
90
+ const CELL_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
91
+ pairedTags: ["w:tcBorders"],
92
+ selfClosingTags: ["w:tcW", "w:gridSpan", "w:vMerge", "w:shd", "w:vAlign"],
93
+ };
94
+
95
+ export function serializeTablePropertiesXml(table: TablePropertiesLike): string {
96
+ return mergePropertiesXml(
97
+ "w:tblPr",
98
+ table.propertiesXml,
99
+ buildTablePropertiesInnerXml(table),
100
+ TABLE_PROPERTY_STRIP_SPEC,
101
+ );
102
+ }
103
+
104
+ export function serializeTableRowPropertiesXml(row: TableRowPropertiesLike): string {
105
+ return mergePropertiesXml(
106
+ "w:trPr",
107
+ row.propertiesXml,
108
+ buildTableRowPropertiesInnerXml(row),
109
+ ROW_PROPERTY_STRIP_SPEC,
110
+ );
111
+ }
112
+
113
+ export function serializeTableCellPropertiesXml(cell: TableCellPropertiesLike): string {
114
+ return mergePropertiesXml(
115
+ "w:tcPr",
116
+ cell.propertiesXml,
117
+ buildTableCellPropertiesInnerXml(cell),
118
+ CELL_PROPERTY_STRIP_SPEC,
119
+ );
120
+ }
121
+
122
+ function mergePropertiesXml(
123
+ tagName: "w:tblPr" | "w:trPr" | "w:tcPr",
124
+ existingXml: string | undefined,
125
+ supportedInnerXml: string,
126
+ stripSpec: PropertyStripSpec,
127
+ ): string {
128
+ const preservedInnerXml = stripKnownProperties(
129
+ extractWrappedChildren(tagName, existingXml),
130
+ stripSpec,
131
+ );
132
+ const mergedInnerXml = [supportedInnerXml, preservedInnerXml]
133
+ .filter((part) => part.length > 0)
134
+ .join("");
135
+ return mergedInnerXml.length > 0 ? `<${tagName}>${mergedInnerXml}</${tagName}>` : "";
136
+ }
137
+
138
+ function extractWrappedChildren(
139
+ tagName: "w:tblPr" | "w:trPr" | "w:tcPr",
140
+ xml: string | undefined,
141
+ ): string {
142
+ if (!xml) {
143
+ return "";
144
+ }
145
+ const trimmed = xml.trim();
146
+ const wrapped = new RegExp(`^<${tagName}\\b[^>]*>([\\s\\S]*)</${tagName}>$`, "u").exec(trimmed);
147
+ return wrapped?.[1] ?? trimmed;
148
+ }
149
+
150
+ function stripKnownProperties(xml: string, stripSpec: PropertyStripSpec): string {
151
+ let nextXml = xml;
152
+ for (const tagName of stripSpec.pairedTags ?? []) {
153
+ nextXml = nextXml.replace(
154
+ new RegExp(`<${tagName}\\b[^>]*>[\\s\\S]*?</${tagName}>`, "gu"),
155
+ "",
156
+ );
157
+ }
158
+ for (const tagName of stripSpec.selfClosingTags ?? []) {
159
+ nextXml = nextXml.replace(new RegExp(`<${tagName}\\b[^>]*/>`, "gu"), "");
160
+ }
161
+ return nextXml.trim();
162
+ }
163
+
164
+ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
165
+ const children: string[] = [];
166
+ if (table.styleId) {
167
+ children.push(`<w:tblStyle w:val="${escapeAttribute(table.styleId)}"/>`);
168
+ }
169
+ if (table.width) {
170
+ children.push(serializeWidth("tblW", table.width));
171
+ }
172
+ if (table.alignment) {
173
+ children.push(`<w:jc w:val="${escapeAttribute(table.alignment)}"/>`);
174
+ }
175
+ if (table.borders) {
176
+ const bordersXml = serializeBorders(table.borders);
177
+ if (bordersXml) {
178
+ children.push(`<w:tblBorders>${bordersXml}</w:tblBorders>`);
179
+ }
180
+ }
181
+ if (table.cellMargins) {
182
+ const marginsXml = serializeTableCellMargins(table.cellMargins);
183
+ if (marginsXml) {
184
+ children.push(`<w:tblCellMar>${marginsXml}</w:tblCellMar>`);
185
+ }
186
+ }
187
+ if (table.tblLook) {
188
+ const tblLookXml = serializeTableLook(table.tblLook);
189
+ if (tblLookXml) {
190
+ children.push(tblLookXml);
191
+ }
192
+ }
193
+ return children.join("");
194
+ }
195
+
196
+ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
197
+ const children: string[] = [];
198
+ if (row.gridBefore !== undefined) {
199
+ children.push(`<w:gridBefore w:val="${row.gridBefore}"/>`);
200
+ }
201
+ if (row.widthBefore) {
202
+ children.push(`<w:wBefore w:w="${row.widthBefore.value}" w:type="${row.widthBefore.type}"/>`);
203
+ }
204
+ if (row.gridAfter !== undefined) {
205
+ children.push(`<w:gridAfter w:val="${row.gridAfter}"/>`);
206
+ }
207
+ if (row.widthAfter) {
208
+ children.push(`<w:wAfter w:w="${row.widthAfter.value}" w:type="${row.widthAfter.type}"/>`);
209
+ }
210
+ if (row.height !== undefined) {
211
+ const hRuleAttr = row.heightRule ? ` w:hRule="${escapeAttribute(row.heightRule)}"` : "";
212
+ children.push(`<w:trHeight w:val="${row.height}"${hRuleAttr}/>`);
213
+ }
214
+ if (row.isHeader !== undefined) {
215
+ children.push(row.isHeader ? `<w:tblHeader/>` : `<w:tblHeader w:val="0"/>`);
216
+ }
217
+ return children.join("");
218
+ }
219
+
220
+ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
221
+ const children: string[] = [];
222
+ if (cell.width) {
223
+ children.push(serializeWidth("tcW", cell.width));
224
+ }
225
+ if (cell.gridSpan && cell.gridSpan > 1) {
226
+ children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
227
+ }
228
+ if (cell.verticalMerge) {
229
+ children.push(
230
+ cell.verticalMerge === "restart"
231
+ ? `<w:vMerge w:val="restart"/>`
232
+ : `<w:vMerge/>`,
233
+ );
234
+ }
235
+ if (cell.borders) {
236
+ const bordersXml = serializeBorders(cell.borders);
237
+ if (bordersXml) {
238
+ children.push(`<w:tcBorders>${bordersXml}</w:tcBorders>`);
239
+ }
240
+ }
241
+ if (cell.shading) {
242
+ const shadingXml = serializeCellShading(cell.shading);
243
+ if (shadingXml) {
244
+ children.push(shadingXml);
245
+ }
246
+ }
247
+ if (cell.verticalAlign) {
248
+ children.push(`<w:vAlign w:val="${escapeAttribute(cell.verticalAlign)}"/>`);
249
+ }
250
+ return children.join("");
251
+ }
252
+
253
+ function serializeWidth(elementName: "tblW" | "tcW", width: TableWidthLike): string {
254
+ return `<w:${elementName} w:w="${width.value}" w:type="${escapeAttribute(width.type)}"/>`;
255
+ }
256
+
257
+ function serializeBorders(borders: TableBordersLike): string {
258
+ const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
259
+ return sides
260
+ .filter((side) => borders[side] !== undefined)
261
+ .map((side) => serializeBorderSpec(side, borders[side]!))
262
+ .join("");
263
+ }
264
+
265
+ function serializeBorderSpec(elementName: string, border: BorderSpecLike): string {
266
+ const attrs: string[] = [];
267
+ if (border.value) attrs.push(`w:val="${escapeAttribute(border.value)}"`);
268
+ if (border.size !== undefined) attrs.push(`w:sz="${border.size}"`);
269
+ if (border.space !== undefined) attrs.push(`w:space="${border.space}"`);
270
+ if (border.color) attrs.push(`w:color="${escapeAttribute(border.color)}"`);
271
+ return attrs.length > 0 ? `<w:${elementName} ${attrs.join(" ")}/>` : "";
272
+ }
273
+
274
+ function serializeTableCellMargins(margins: TableCellMarginsLike): string {
275
+ const parts: string[] = [];
276
+ if (margins.top !== undefined) parts.push(`<w:top w:w="${margins.top}" w:type="dxa"/>`);
277
+ if (margins.left !== undefined) parts.push(`<w:left w:w="${margins.left}" w:type="dxa"/>`);
278
+ if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${margins.bottom}" w:type="dxa"/>`);
279
+ if (margins.right !== undefined) parts.push(`<w:right w:w="${margins.right}" w:type="dxa"/>`);
280
+ return parts.join("");
281
+ }
282
+
283
+ function serializeTableLook(tblLook: TableLookLike): string {
284
+ const attrs: string[] = [];
285
+ if (tblLook.val) {
286
+ attrs.push(`w:val="${escapeAttribute(tblLook.val)}"`);
287
+ }
288
+ for (const [key, attr] of [
289
+ ["firstRow", "w:firstRow"],
290
+ ["lastRow", "w:lastRow"],
291
+ ["firstColumn", "w:firstColumn"],
292
+ ["lastColumn", "w:lastColumn"],
293
+ ["noHBand", "w:noHBand"],
294
+ ["noVBand", "w:noVBand"],
295
+ ] as const) {
296
+ const value = tblLook[key];
297
+ if (value !== undefined) {
298
+ attrs.push(`${attr}="${value ? "1" : "0"}"`);
299
+ }
300
+ }
301
+ return attrs.length > 0 ? `<w:tblLook ${attrs.join(" ")}/>` : "";
302
+ }
303
+
304
+ function serializeCellShading(shading: CellShadingLike): string {
305
+ const attrs: string[] = [];
306
+ if (shading.val) attrs.push(`w:val="${escapeAttribute(shading.val)}"`);
307
+ if (shading.color) attrs.push(`w:color="${escapeAttribute(shading.color)}"`);
308
+ if (shading.fill) attrs.push(`w:fill="${escapeAttribute(shading.fill)}"`);
309
+ return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
310
+ }
311
+
312
+ function escapeAttribute(value: string): string {
313
+ return value
314
+ .replace(/&/gu, "&amp;")
315
+ .replace(/"/gu, "&quot;")
316
+ .replace(/</gu, "&lt;")
317
+ .replace(/>/gu, "&gt;");
318
+ }
@@ -199,6 +199,11 @@ function normalizeTable(
199
199
  ...(table.propertiesXml ? { propertiesXml: table.propertiesXml } : {}),
200
200
  gridColumns: table.gridColumns,
201
201
  rows,
202
+ ...(table.width ? { width: table.width } : {}),
203
+ ...(table.alignment ? { alignment: table.alignment } : {}),
204
+ ...(table.borders ? { borders: table.borders } : {}),
205
+ ...(table.cellMargins ? { cellMargins: table.cellMargins } : {}),
206
+ ...(table.tblLook ? { tblLook: table.tblLook } : {}),
202
207
  };
203
208
  }
204
209
 
@@ -215,6 +220,9 @@ function normalizeTableRow(
215
220
  ...(row.widthBefore ? { widthBefore: row.widthBefore } : {}),
216
221
  ...(row.gridAfter !== undefined ? { gridAfter: row.gridAfter } : {}),
217
222
  ...(row.widthAfter ? { widthAfter: row.widthAfter } : {}),
223
+ ...(row.height !== undefined ? { height: row.height } : {}),
224
+ ...(row.heightRule ? { heightRule: row.heightRule } : {}),
225
+ ...(row.isHeader !== undefined ? { isHeader: row.isHeader } : {}),
218
226
  cells,
219
227
  };
220
228
  }
@@ -237,6 +245,10 @@ function normalizeTableCell(
237
245
  ...(cell.propertiesXml ? { propertiesXml: cell.propertiesXml } : {}),
238
246
  ...(cell.gridSpan ? { gridSpan: cell.gridSpan } : {}),
239
247
  ...(cell.verticalMerge ? { verticalMerge: cell.verticalMerge } : {}),
248
+ ...(cell.width ? { width: cell.width } : {}),
249
+ ...(cell.borders ? { borders: cell.borders } : {}),
250
+ ...(cell.shading ? { shading: cell.shading } : {}),
251
+ ...(cell.verticalAlign ? { verticalAlign: cell.verticalAlign } : {}),
240
252
  children,
241
253
  };
242
254
  }
@@ -262,6 +274,7 @@ function normalizeCustomXml(
262
274
  type: "custom_xml",
263
275
  ...(block.uri ? { uri: block.uri } : {}),
264
276
  ...(block.element ? { element: block.element } : {}),
277
+ ...(block.rawXml ? { rawXml: block.rawXml } : {}),
265
278
  children: block.children.flatMap((child) => normalizeBlocks(child, state, packagePartName)),
266
279
  };
267
280
  }
@@ -321,7 +334,7 @@ function normalizeInlineChildren(
321
334
  ...(node.marks && node.marks.length > 0 ? { marks: node.marks } : {}),
322
335
  });
323
336
  }
324
- state.cursor += node.text.length;
337
+ state.cursor += Array.from(node.text).length;
325
338
  break;
326
339
  }
327
340
  case "tab":
@@ -431,6 +444,7 @@ function normalizeInlineChildren(
431
444
  ? normalizeInlineChildren(node.children, state, packagePartName)
432
445
  : normalizeFieldContentXml(node.contentXml ?? "");
433
446
  state.cursor = cursorBeforeField;
447
+ const renderedLength = measureNormalizedInlineDisplayLength(fieldChildren);
434
448
  normalized.push({
435
449
  type: "field",
436
450
  fieldType: node.fieldType,
@@ -440,7 +454,7 @@ function normalizeInlineChildren(
440
454
  ...(classification.target ? { fieldTarget: classification.target } : {}),
441
455
  refreshStatus: classification.supported ? "stale" : "preserve-only",
442
456
  });
443
- state.cursor += fieldChildren.length > 0 ? fieldChildren.length : 1;
457
+ state.cursor += renderedLength > 0 ? renderedLength : 1;
444
458
  break;
445
459
  }
446
460
  }
@@ -538,7 +552,7 @@ function normalizeHyperlink(node: ParsedHyperlinkNode): {
538
552
  function measureHyperlink(node: ParsedHyperlinkNode): number {
539
553
  return node.children.reduce<number>((size, child) => {
540
554
  if (child.type === "text") {
541
- return size + child.text.length;
555
+ return size + Array.from(child.text).length;
542
556
  }
543
557
  return size + 1;
544
558
  }, 0);
@@ -637,3 +651,20 @@ function normalizeFieldContentXml(contentXml: string | undefined): InlineNode[]
637
651
 
638
652
  return children;
639
653
  }
654
+
655
+ function measureNormalizedInlineDisplayLength(nodes: InlineNode[]): number {
656
+ return nodes.reduce((total, node) => {
657
+ switch (node.type) {
658
+ case "text":
659
+ return total + Array.from(node.text).length;
660
+ case "hyperlink":
661
+ case "field":
662
+ return total + measureNormalizedInlineDisplayLength(node.children);
663
+ case "bookmark_start":
664
+ case "bookmark_end":
665
+ return total;
666
+ default:
667
+ return total + 1;
668
+ }
669
+ }, 0);
670
+ }
@@ -164,6 +164,9 @@ export function parseCommentsFromOoxml(
164
164
  source: "import",
165
165
  rootOoxmlCommentId: rootDefinition.commentId,
166
166
  rootParaId: rootDefinition.paraId,
167
+ detachedReason: "incomplete-markers",
168
+ actionabilityNote:
169
+ "Re-anchoring requires the host to supply a valid range. The comment body and thread are preserved for display.",
167
170
  },
168
171
  }),
169
172
  );
@@ -194,6 +197,9 @@ export function parseCommentsFromOoxml(
194
197
  source: "import",
195
198
  rootOoxmlCommentId: rootDefinition.commentId,
196
199
  rootParaId: rootDefinition.paraId,
200
+ detachedReason: "multi-paragraph",
201
+ actionabilityNote:
202
+ "The comment thread and body are preserved. Operators see the thread in the sidebar but cannot navigate to an inline highlight.",
197
203
  },
198
204
  }),
199
205
  );
@@ -11,6 +11,22 @@ import type {
11
11
  TableRowNode,
12
12
  TextMark,
13
13
  } from "../../model/canonical-document.ts";
14
+ import {
15
+ readCellBorders,
16
+ readCellShading,
17
+ readCellVerticalAlign,
18
+ readCellWidth,
19
+ readGridColumns as readSharedGridColumns,
20
+ readRowHeight,
21
+ readRowHeightRule,
22
+ readRowIsHeader,
23
+ readTableAlignment,
24
+ readTableBorders,
25
+ readTableCellMargins,
26
+ readTableLook,
27
+ readTableStyleId,
28
+ readTableWidth,
29
+ } from "./parse-tables.ts";
14
30
 
15
31
  // ---- XML node types (inline, no external dep) ----
16
32
 
@@ -19,11 +35,15 @@ interface XmlElementNode {
19
35
  name: string;
20
36
  attributes: Record<string, string>;
21
37
  children: XmlNode[];
38
+ start: number;
39
+ end: number;
22
40
  }
23
41
 
24
42
  interface XmlTextNode {
25
43
  type: "text";
26
44
  text: string;
45
+ start: number;
46
+ end: number;
27
47
  }
28
48
 
29
49
  type XmlNode = XmlElementNode | XmlTextNode;
@@ -656,6 +676,11 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
656
676
  const rows: TableRowNode[] = [];
657
677
  let propertiesXml: string | undefined;
658
678
  let styleId: string | undefined;
679
+ let width: TableNode["width"];
680
+ let alignment: TableNode["alignment"];
681
+ let borders: TableNode["borders"];
682
+ let cellMargins: TableNode["cellMargins"];
683
+ let tblLook: TableNode["tblLook"];
659
684
 
660
685
  for (const child of tblElement.children) {
661
686
  if (child.type !== "element") continue;
@@ -663,8 +688,12 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
663
688
 
664
689
  if (name === "tblPr") {
665
690
  propertiesXml = serializeElementToXml(child);
666
- const pStyle = findChildElementOptional(child, "tblStyle");
667
- styleId = pStyle?.attributes["w:val"] ?? pStyle?.attributes.val;
691
+ styleId = readTableStyleId(child);
692
+ width = readTableWidth(child);
693
+ alignment = readTableAlignment(child);
694
+ borders = readTableBorders(child);
695
+ cellMargins = readTableCellMargins(child);
696
+ tblLook = readTableLook(child);
668
697
  } else if (name === "tblGrid") {
669
698
  gridColumns = readGridColumns(child);
670
699
  } else if (name === "tr") {
@@ -678,24 +707,24 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
678
707
  ...(propertiesXml ? { propertiesXml } : {}),
679
708
  gridColumns,
680
709
  rows,
710
+ ...(width ? { width } : {}),
711
+ ...(alignment ? { alignment } : {}),
712
+ ...(borders ? { borders } : {}),
713
+ ...(cellMargins ? { cellMargins } : {}),
714
+ ...(tblLook ? { tblLook } : {}),
681
715
  };
682
716
  }
683
717
 
684
718
  function readGridColumns(tblGrid: XmlElementNode): number[] {
685
- const columns: number[] = [];
686
- for (const child of tblGrid.children) {
687
- if (child.type !== "element") continue;
688
- if (localName(child.name) === "gridCol") {
689
- const w = child.attributes["w:w"] ?? child.attributes.w ?? "0";
690
- columns.push(Number.parseInt(w, 10) || 0);
691
- }
692
- }
693
- return columns;
719
+ return readSharedGridColumns(tblGrid);
694
720
  }
695
721
 
696
722
  function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
697
723
  const cells: TableCellNode[] = [];
698
724
  let propertiesXml: string | undefined;
725
+ let height: TableRowNode["height"];
726
+ let heightRule: TableRowNode["heightRule"];
727
+ let isHeader: TableRowNode["isHeader"];
699
728
 
700
729
  for (const child of trElement.children) {
701
730
  if (child.type !== "element") continue;
@@ -703,6 +732,9 @@ function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
703
732
 
704
733
  if (name === "trPr") {
705
734
  propertiesXml = serializeElementToXml(child);
735
+ height = readRowHeight(child);
736
+ heightRule = readRowHeightRule(child);
737
+ isHeader = readRowIsHeader(child);
706
738
  } else if (name === "tc") {
707
739
  cells.push(parseSimpleTableCell(child));
708
740
  }
@@ -711,6 +743,9 @@ function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
711
743
  return {
712
744
  type: "table_row",
713
745
  ...(propertiesXml ? { propertiesXml } : {}),
746
+ ...(height !== undefined ? { height } : {}),
747
+ ...(heightRule ? { heightRule } : {}),
748
+ ...(isHeader !== undefined ? { isHeader } : {}),
714
749
  cells,
715
750
  };
716
751
  }
@@ -720,6 +755,10 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
720
755
  let propertiesXml: string | undefined;
721
756
  let gridSpan: number | undefined;
722
757
  let verticalMerge: "restart" | "continue" | undefined;
758
+ let width: TableCellNode["width"];
759
+ let borders: TableCellNode["borders"];
760
+ let shading: TableCellNode["shading"];
761
+ let verticalAlign: TableCellNode["verticalAlign"];
723
762
 
724
763
  for (const child of tcElement.children) {
725
764
  if (child.type !== "element") continue;
@@ -736,6 +775,10 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
736
775
  const vmVal = vmEl.attributes["w:val"] ?? vmEl.attributes.val ?? "continue";
737
776
  verticalMerge = vmVal === "restart" ? "restart" : "continue";
738
777
  }
778
+ width = readCellWidth(child);
779
+ borders = readCellBorders(child);
780
+ shading = readCellShading(child);
781
+ verticalAlign = readCellVerticalAlign(child);
739
782
  } else if (name === "p") {
740
783
  children.push(parseParagraphElement(child));
741
784
  }
@@ -746,6 +789,10 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
746
789
  ...(propertiesXml ? { propertiesXml } : {}),
747
790
  ...(gridSpan ? { gridSpan } : {}),
748
791
  ...(verticalMerge ? { verticalMerge } : {}),
792
+ ...(width ? { width } : {}),
793
+ ...(borders ? { borders } : {}),
794
+ ...(shading ? { shading } : {}),
795
+ ...(verticalAlign ? { verticalAlign } : {}),
749
796
  children: children.length > 0 ? children : [{ type: "paragraph", children: [] }],
750
797
  };
751
798
  }
@@ -791,6 +838,8 @@ function parseXml(xml: string): XmlElementNode {
791
838
  name: "__root__",
792
839
  attributes: {},
793
840
  children: [],
841
+ start: 0,
842
+ end: xml.length,
794
843
  };
795
844
  const stack: XmlElementNode[] = [root];
796
845
  let cursor = 0;
@@ -814,6 +863,8 @@ function parseXml(xml: string): XmlElementNode {
814
863
  stack[stack.length - 1]?.children.push({
815
864
  type: "text",
816
865
  text: xml.slice(cursor + 9, textEnd),
866
+ start: cursor,
867
+ end: end >= 0 ? end + 3 : xml.length,
817
868
  });
818
869
  cursor = end >= 0 ? end + 3 : xml.length;
819
870
  continue;
@@ -824,7 +875,7 @@ function parseXml(xml: string): XmlElementNode {
824
875
  const end = nextTag >= 0 ? nextTag : xml.length;
825
876
  const text = decodeXmlEntities(xml.slice(cursor, end));
826
877
  if (text.trim().length > 0 || (text.length > 0 && stack.length > 1)) {
827
- stack[stack.length - 1]?.children.push({ type: "text", text });
878
+ stack[stack.length - 1]?.children.push({ type: "text", text, start: cursor, end });
828
879
  }
829
880
  cursor = end;
830
881
  continue;
@@ -833,7 +884,10 @@ function parseXml(xml: string): XmlElementNode {
833
884
  if (xml[cursor + 1] === "/") {
834
885
  const end = xml.indexOf(">", cursor);
835
886
  if (end < 0) break;
836
- stack.pop();
887
+ const current = stack.pop();
888
+ if (current) {
889
+ current.end = end + 1;
890
+ }
837
891
  cursor = end + 1;
838
892
  continue;
839
893
  }
@@ -855,6 +909,8 @@ function parseXml(xml: string): XmlElementNode {
855
909
  name: tagName,
856
910
  attributes,
857
911
  children: [],
912
+ start: cursor,
913
+ end: tagEnd + 1,
858
914
  };
859
915
 
860
916
  stack[stack.length - 1]?.children.push(element);