@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
@@ -1,4 +1,13 @@
1
- import type { NumberingCatalog, NumberingLevelDefinition, NumberingLevelOverride } from "../../model/canonical-document.ts";
1
+ import type {
2
+ NumberingCatalog,
3
+ NumberingLevelDefinition,
4
+ NumberingLevelParagraphGeometry,
5
+ NumberingLevelOverride,
6
+ NumberingLevelOverrideDefinition,
7
+ ParagraphSpacing,
8
+ ParagraphIndentation,
9
+ TabStop,
10
+ } from "../../model/canonical-document.ts";
2
11
 
3
12
  export interface ParsedParagraphNumberingReference {
4
13
  paragraphIndex: number;
@@ -136,32 +145,12 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
136
145
  if (level === undefined) {
137
146
  continue;
138
147
  }
148
+ const definition = readLevelDefinition(child, level);
149
+ if (!definition) {
150
+ continue;
151
+ }
139
152
 
140
- const startNode = findChildElementOptional(child, "start");
141
- const formatNode = findChildElementOptional(child, "numFmt");
142
- const textNode = findChildElementOptional(child, "lvlText");
143
- const paragraphStyleNode = findChildElementOptional(child, "pStyle");
144
- const rawStart = startNode?.attributes["w:val"] ?? startNode?.attributes.val;
145
- const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
146
- const format = formatNode?.attributes["w:val"] ?? formatNode?.attributes.val ?? "decimal";
147
- const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val ?? `%${level + 1}.`;
148
- const paragraphStyleId =
149
- paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
150
- const isLegalNode = findChildElementOptional(child, "isLgl");
151
- const isLegalNumbering = isLegalNode !== undefined;
152
- const suffixNode = findChildElementOptional(child, "suff");
153
- const suffixVal = suffixNode?.attributes["w:val"] ?? suffixNode?.attributes.val;
154
- const suffix = suffixVal === "space" || suffixVal === "nothing" ? suffixVal : suffixVal === "tab" ? "tab" : undefined;
155
-
156
- levels.push({
157
- level,
158
- format,
159
- text,
160
- ...(startAt !== undefined ? { startAt } : {}),
161
- ...(paragraphStyleId ? { paragraphStyleId } : {}),
162
- ...(isLegalNumbering ? { isLegalNumbering } : {}),
163
- ...(suffix ? { suffix } : {}),
164
- });
153
+ levels.push(definition);
165
154
  }
166
155
 
167
156
  return levels.sort((left, right) => left.level - right.level);
@@ -184,16 +173,342 @@ function readOverrides(numNode: XmlElementNode): NumberingLevelOverride[] {
184
173
  const startOverrideNode = findChildElementOptional(child, "startOverride");
185
174
  const rawStart = startOverrideNode?.attributes["w:val"] ?? startOverrideNode?.attributes.val;
186
175
  const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
176
+ const levelDefinitionNode = findChildElementOptional(child, "lvl");
177
+ const levelDefinition = levelDefinitionNode
178
+ ? readLevelOverrideDefinition(levelDefinitionNode, level)
179
+ : undefined;
187
180
 
188
181
  overrides.push({
189
182
  level,
190
183
  ...(startAt !== undefined ? { startAt } : {}),
184
+ ...(levelDefinition ? { levelDefinition } : {}),
191
185
  });
192
186
  }
193
187
 
194
188
  return overrides.sort((left, right) => left.level - right.level);
195
189
  }
196
190
 
191
+ function readLevelDefinition(
192
+ levelNode: XmlElementNode,
193
+ fallbackLevel?: number,
194
+ ): NumberingLevelDefinition | undefined {
195
+ const rawLevel = levelNode.attributes["w:ilvl"] ?? levelNode.attributes.ilvl;
196
+ const level = rawLevel === undefined ? fallbackLevel : parseInteger(rawLevel);
197
+ if (level === undefined) {
198
+ return undefined;
199
+ }
200
+
201
+ const startNode = findChildElementOptional(levelNode, "start");
202
+ const formatNode = findChildElementOptional(levelNode, "numFmt");
203
+ const textNode = findChildElementOptional(levelNode, "lvlText");
204
+ const paragraphStyleNode = findChildElementOptional(levelNode, "pStyle");
205
+ const rawStart = startNode?.attributes["w:val"] ?? startNode?.attributes.val;
206
+ const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
207
+ const format = formatNode?.attributes["w:val"] ?? formatNode?.attributes.val ?? "decimal";
208
+ const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val ?? `%${level + 1}.`;
209
+ const paragraphStyleId =
210
+ paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
211
+ const isLegalNode = findChildElementOptional(levelNode, "isLgl");
212
+ const isLegalNumbering = readIsLegalNumberingValue(isLegalNode);
213
+ const suffixNode = findChildElementOptional(levelNode, "suff");
214
+ const suffixVal = suffixNode?.attributes["w:val"] ?? suffixNode?.attributes.val;
215
+ const suffix =
216
+ suffixVal === "space" || suffixVal === "nothing"
217
+ ? suffixVal
218
+ : suffixVal === "tab"
219
+ ? "tab"
220
+ : undefined;
221
+ const paragraphGeometry = readLevelParagraphGeometry(levelNode);
222
+
223
+ return {
224
+ level,
225
+ format,
226
+ text,
227
+ ...(startAt !== undefined ? { startAt } : {}),
228
+ ...(paragraphStyleId ? { paragraphStyleId } : {}),
229
+ ...(isLegalNumbering ? { isLegalNumbering: true } : {}),
230
+ ...(suffix ? { suffix } : {}),
231
+ ...(paragraphGeometry ? { paragraphGeometry } : {}),
232
+ };
233
+ }
234
+
235
+ function readLevelOverrideDefinition(
236
+ levelNode: XmlElementNode,
237
+ fallbackLevel?: number,
238
+ ): NumberingLevelOverrideDefinition | undefined {
239
+ const rawLevel = levelNode.attributes["w:ilvl"] ?? levelNode.attributes.ilvl;
240
+ const level = rawLevel === undefined ? fallbackLevel : parseInteger(rawLevel);
241
+ if (level === undefined) {
242
+ return undefined;
243
+ }
244
+
245
+ const startNode = findChildElementOptional(levelNode, "start");
246
+ const formatNode = findChildElementOptional(levelNode, "numFmt");
247
+ const textNode = findChildElementOptional(levelNode, "lvlText");
248
+ const paragraphStyleNode = findChildElementOptional(levelNode, "pStyle");
249
+ const rawStart = startNode?.attributes["w:val"] ?? startNode?.attributes.val;
250
+ const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
251
+ const format = formatNode?.attributes["w:val"] ?? formatNode?.attributes.val;
252
+ const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val;
253
+ const paragraphStyleId =
254
+ paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
255
+ const isLegalNode = findChildElementOptional(levelNode, "isLgl");
256
+ const isLegalNumbering = readIsLegalNumberingValue(isLegalNode);
257
+ const suffixNode = findChildElementOptional(levelNode, "suff");
258
+ const suffixVal = suffixNode?.attributes["w:val"] ?? suffixNode?.attributes.val;
259
+ const suffix =
260
+ suffixVal === "space" || suffixVal === "nothing"
261
+ ? suffixVal
262
+ : suffixVal === "tab"
263
+ ? "tab"
264
+ : undefined;
265
+ const paragraphGeometry = readLevelParagraphGeometry(levelNode);
266
+
267
+ const hasExplicitFields =
268
+ startAt !== undefined ||
269
+ format !== undefined ||
270
+ text !== undefined ||
271
+ paragraphStyleId !== undefined ||
272
+ isLegalNumbering !== undefined ||
273
+ suffix !== undefined ||
274
+ paragraphGeometry !== undefined;
275
+
276
+ if (!hasExplicitFields) {
277
+ return undefined;
278
+ }
279
+
280
+ return {
281
+ level,
282
+ ...(startAt !== undefined ? { startAt } : {}),
283
+ ...(format !== undefined ? { format } : {}),
284
+ ...(text !== undefined ? { text } : {}),
285
+ ...(paragraphStyleId ? { paragraphStyleId } : {}),
286
+ ...(isLegalNumbering !== undefined ? { isLegalNumbering } : {}),
287
+ ...(suffix ? { suffix } : {}),
288
+ ...(paragraphGeometry ? { paragraphGeometry } : {}),
289
+ };
290
+ }
291
+
292
+ function readLevelParagraphGeometry(
293
+ levelNode: XmlElementNode,
294
+ ): NumberingLevelDefinition["paragraphGeometry"] | undefined {
295
+ const paragraphPropertiesNode = findChildElementOptional(levelNode, "pPr");
296
+ const justification = readLevelJustification(levelNode);
297
+ const spacing = paragraphPropertiesNode ? readParagraphSpacing(paragraphPropertiesNode) : undefined;
298
+ const indentation = paragraphPropertiesNode
299
+ ? readParagraphIndentation(paragraphPropertiesNode)
300
+ : undefined;
301
+ const tabStops = paragraphPropertiesNode ? readParagraphTabStops(paragraphPropertiesNode) : undefined;
302
+
303
+ if (!justification && !spacing && !indentation && (!tabStops || tabStops.length === 0)) {
304
+ return undefined;
305
+ }
306
+
307
+ return {
308
+ ...(justification ? { justification } : {}),
309
+ ...(spacing ? { spacing } : {}),
310
+ ...(indentation ? { indentation } : {}),
311
+ ...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
312
+ };
313
+ }
314
+
315
+ function readLevelJustification(
316
+ levelNode: XmlElementNode,
317
+ ): NumberingLevelParagraphGeometry["justification"] {
318
+ const justificationNode = findChildElementOptional(levelNode, "lvlJc");
319
+ if (!justificationNode) {
320
+ return undefined;
321
+ }
322
+
323
+ const rawValue = (
324
+ justificationNode.attributes["w:val"] ??
325
+ justificationNode.attributes.val ??
326
+ ""
327
+ ).toLowerCase();
328
+
329
+ if (rawValue === "start") {
330
+ return "left";
331
+ }
332
+ if (rawValue === "end") {
333
+ return "right";
334
+ }
335
+ if (
336
+ rawValue === "left" ||
337
+ rawValue === "center" ||
338
+ rawValue === "right" ||
339
+ rawValue === "both" ||
340
+ rawValue === "distribute"
341
+ ) {
342
+ return rawValue;
343
+ }
344
+
345
+ return undefined;
346
+ }
347
+
348
+ function readParagraphIndentation(node: XmlElementNode): ParagraphIndentation | undefined {
349
+ const indNode = findChildElementOptional(node, "ind");
350
+ if (!indNode) {
351
+ return undefined;
352
+ }
353
+
354
+ const indentation: ParagraphIndentation = {};
355
+ const left =
356
+ indNode.attributes["w:start"] ??
357
+ indNode.attributes.start ??
358
+ indNode.attributes["w:left"] ??
359
+ indNode.attributes.left;
360
+ const right =
361
+ indNode.attributes["w:end"] ??
362
+ indNode.attributes.end ??
363
+ indNode.attributes["w:right"] ??
364
+ indNode.attributes.right;
365
+ const firstLine = indNode.attributes["w:firstLine"] ?? indNode.attributes.firstLine;
366
+ const hanging = indNode.attributes["w:hanging"] ?? indNode.attributes.hanging;
367
+
368
+ if (left !== undefined) {
369
+ const value = Number.parseInt(left, 10);
370
+ if (Number.isFinite(value)) indentation.left = value;
371
+ }
372
+ if (right !== undefined) {
373
+ const value = Number.parseInt(right, 10);
374
+ if (Number.isFinite(value)) indentation.right = value;
375
+ }
376
+ if (firstLine !== undefined) {
377
+ const value = Number.parseInt(firstLine, 10);
378
+ if (Number.isFinite(value)) indentation.firstLine = value;
379
+ }
380
+ if (hanging !== undefined) {
381
+ const value = Number.parseInt(hanging, 10);
382
+ if (Number.isFinite(value)) indentation.hanging = value;
383
+ }
384
+
385
+ if (
386
+ indentation.left === undefined &&
387
+ indentation.right === undefined &&
388
+ indentation.firstLine === undefined &&
389
+ indentation.hanging === undefined
390
+ ) {
391
+ return undefined;
392
+ }
393
+
394
+ return indentation;
395
+ }
396
+
397
+ function readParagraphSpacing(node: XmlElementNode): ParagraphSpacing | undefined {
398
+ const spacingNode = findChildElementOptional(node, "spacing");
399
+ if (!spacingNode) {
400
+ return undefined;
401
+ }
402
+
403
+ const spacing: ParagraphSpacing = {};
404
+ const before = spacingNode.attributes["w:before"] ?? spacingNode.attributes.before;
405
+ const after = spacingNode.attributes["w:after"] ?? spacingNode.attributes.after;
406
+ const line = spacingNode.attributes["w:line"] ?? spacingNode.attributes.line;
407
+ const lineRule = spacingNode.attributes["w:lineRule"] ?? spacingNode.attributes.lineRule;
408
+
409
+ if (before !== undefined) {
410
+ const value = Number.parseInt(before, 10);
411
+ if (Number.isFinite(value)) spacing.before = value;
412
+ }
413
+ if (after !== undefined) {
414
+ const value = Number.parseInt(after, 10);
415
+ if (Number.isFinite(value)) spacing.after = value;
416
+ }
417
+ if (line !== undefined) {
418
+ const value = Number.parseInt(line, 10);
419
+ if (Number.isFinite(value)) spacing.line = value;
420
+ }
421
+ if (lineRule !== undefined) {
422
+ const normalized = lineRule.toLowerCase();
423
+ if (normalized === "auto" || normalized === "exact") {
424
+ spacing.lineRule = normalized;
425
+ } else if (normalized === "atleast") {
426
+ spacing.lineRule = "atLeast";
427
+ }
428
+ }
429
+
430
+ if (
431
+ spacing.before === undefined &&
432
+ spacing.after === undefined &&
433
+ spacing.line === undefined &&
434
+ spacing.lineRule === undefined
435
+ ) {
436
+ return undefined;
437
+ }
438
+
439
+ return spacing;
440
+ }
441
+
442
+ function readParagraphTabStops(node: XmlElementNode): TabStop[] | undefined {
443
+ const tabsNode = findChildElementOptional(node, "tabs");
444
+ if (!tabsNode) {
445
+ return undefined;
446
+ }
447
+
448
+ const tabStops: TabStop[] = [];
449
+ for (const child of tabsNode.children) {
450
+ if (child.type !== "element" || localName(child.name) !== "tab") {
451
+ continue;
452
+ }
453
+
454
+ const pos = child.attributes["w:pos"] ?? child.attributes.pos;
455
+ const val = (child.attributes["w:val"] ?? child.attributes.val ?? "left").toLowerCase();
456
+ const leader = (child.attributes["w:leader"] ?? child.attributes.leader ?? "none").toLowerCase();
457
+ if (pos === undefined) {
458
+ continue;
459
+ }
460
+
461
+ const position = Number.parseInt(pos, 10);
462
+ if (!Number.isFinite(position)) {
463
+ continue;
464
+ }
465
+
466
+ const align = (["left", "center", "right", "decimal", "num", "bar", "clear"] as const).includes(
467
+ val as TabStop["align"],
468
+ )
469
+ ? (val as TabStop["align"])
470
+ : "left";
471
+ const leaderValue =
472
+ leader === "none" ||
473
+ leader === "dot" ||
474
+ leader === "hyphen" ||
475
+ leader === "underscore" ||
476
+ leader === "heavy"
477
+ ? (leader as Exclude<TabStop["leader"], "middleDot">)
478
+ : leader === "middledot"
479
+ ? "middleDot"
480
+ : undefined;
481
+
482
+ tabStops.push({
483
+ position,
484
+ align,
485
+ ...(leaderValue && leaderValue !== "none" ? { leader: leaderValue } : {}),
486
+ });
487
+ }
488
+
489
+ return tabStops.length > 0 ? tabStops : undefined;
490
+ }
491
+
492
+ function readIsLegalNumberingValue(
493
+ isLegalNode: XmlElementNode | undefined,
494
+ ): boolean | undefined {
495
+ if (!isLegalNode) {
496
+ return undefined;
497
+ }
498
+
499
+ const rawValue = (
500
+ isLegalNode.attributes["w:val"] ??
501
+ isLegalNode.attributes.val ??
502
+ "1"
503
+ ).toLowerCase();
504
+
505
+ if (rawValue === "0" || rawValue === "false" || rawValue === "off") {
506
+ return false;
507
+ }
508
+
509
+ return true;
510
+ }
511
+
197
512
  function findChildElement(node: XmlElementNode, childLocalName: string): XmlElementNode {
198
513
  const child = findChildElementOptional(node, childLocalName);
199
514
  if (!child) {
@@ -106,11 +106,24 @@ export function parseRevisionsFromDocumentXml(
106
106
  if (childType !== "p") {
107
107
  if (childType === "tbl") {
108
108
  parseTblPropertyRevisions(child, cursor, state);
109
+ const nested = parseNestedParagraphContainers(
110
+ child.children,
111
+ paragraphIndex,
112
+ cursor,
113
+ previousWasParagraph,
114
+ state,
115
+ );
116
+ paragraphIndex = nested.paragraphIndex;
117
+ cursor = nested.cursor;
118
+ previousWasParagraph = nested.previousWasParagraph;
109
119
  } else if (childType === "sectPr") {
110
120
  parseSectPrRevisions(child, cursor, state);
121
+ cursor += 1;
122
+ previousWasParagraph = false;
123
+ } else {
124
+ cursor += 1;
125
+ previousWasParagraph = false;
111
126
  }
112
- cursor += 1;
113
- previousWasParagraph = false;
114
127
  continue;
115
128
  }
116
129
 
@@ -140,6 +153,106 @@ export function parseRevisionsFromDocumentXml(
140
153
  };
141
154
  }
142
155
 
156
+ function parseNestedParagraphContainers(
157
+ nodes: XmlNode[],
158
+ paragraphIndex: number,
159
+ cursor: number,
160
+ previousWasParagraph: boolean,
161
+ state: ParseState,
162
+ ): {
163
+ paragraphIndex: number;
164
+ cursor: number;
165
+ previousWasParagraph: boolean;
166
+ } {
167
+ let nextParagraphIndex = paragraphIndex;
168
+ let nextCursor = cursor;
169
+ let nextPreviousWasParagraph = previousWasParagraph;
170
+
171
+ for (const node of nodes) {
172
+ if (node.type !== "element") {
173
+ continue;
174
+ }
175
+
176
+ const type = localName(node.name);
177
+ if (type === "p") {
178
+ nextParagraphIndex += 1;
179
+ const paragraphBoundary = state.boundaries[nextParagraphIndex];
180
+ if (!paragraphBoundary) {
181
+ continue;
182
+ }
183
+ parseParagraphMarkRevisions(node, paragraphBoundary, state);
184
+ walkParagraphContent(node.children, nextParagraphIndex, state, () => nextCursor, (next) => {
185
+ nextCursor = next;
186
+ });
187
+ nextPreviousWasParagraph = false;
188
+ continue;
189
+ }
190
+
191
+ if (
192
+ type === "tbl" ||
193
+ type === "tr" ||
194
+ type === "tc" ||
195
+ type === "sdtContent" ||
196
+ type === "customXml" ||
197
+ type === "smartTag"
198
+ ) {
199
+ const nested = parseNestedParagraphContainers(
200
+ node.children,
201
+ nextParagraphIndex,
202
+ nextCursor,
203
+ nextPreviousWasParagraph,
204
+ state,
205
+ );
206
+ nextParagraphIndex = nested.paragraphIndex;
207
+ nextCursor = nested.cursor;
208
+ nextPreviousWasParagraph = nested.previousWasParagraph;
209
+ continue;
210
+ }
211
+
212
+ if (
213
+ type === "tblPr" ||
214
+ type === "tblGrid" ||
215
+ type === "gridCol" ||
216
+ type === "trPr" ||
217
+ type === "tcPr"
218
+ ) {
219
+ continue;
220
+ }
221
+
222
+ nextCursor += 1;
223
+ nextPreviousWasParagraph = false;
224
+ }
225
+
226
+ return {
227
+ paragraphIndex: nextParagraphIndex,
228
+ cursor: nextCursor,
229
+ previousWasParagraph: nextPreviousWasParagraph,
230
+ };
231
+ }
232
+
233
+ export function parseRevisionsFromStoryXml(
234
+ storyXml: string,
235
+ ): ParsedRevisionsResult {
236
+ const xmlDeclMatch = storyXml.match(/^<\?xml[\s\S]*?\?>\s*/u);
237
+ const withoutDecl = xmlDeclMatch ? storyXml.slice(xmlDeclMatch[0].length) : storyXml;
238
+ const rootMatch = withoutDecl.match(/^<([A-Za-z0-9:._-]+)([^>]*)>([\s\S]*)<\/\1>\s*$/u);
239
+ if (!rootMatch) {
240
+ return {
241
+ revisions: [],
242
+ preservedMarkup: [],
243
+ diagnostics: [],
244
+ boundaries: [],
245
+ };
246
+ }
247
+
248
+ const innerXml = rootMatch[3] ?? "";
249
+ const wrapped =
250
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
251
+ `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
252
+ `<w:body>${innerXml}</w:body></w:document>`;
253
+ return parseRevisionsFromDocumentXml(wrapped);
254
+ }
255
+
143
256
  function parseParagraphMarkRevisions(
144
257
  paragraph: XmlElementNode,
145
258
  boundary: RevisionParagraphBoundary,
@@ -225,6 +338,7 @@ function parseParagraphMarkRevisions(
225
338
  createdAt: metadata.createdAt,
226
339
  metadata: {
227
340
  source: "import",
341
+ semanticKind: "structural-change",
228
342
  importedRevisionForm:
229
343
  type === "ins" ? "paragraph-insertion" : "paragraph-deletion",
230
344
  originalRevisionType: `paragraph-${type}`,
@@ -444,7 +558,7 @@ function walkContentNode(
444
558
  .filter((child): child is XmlElementNode["children"][number] & { type: "text" } => child.type === "text")
445
559
  .map((child) => child.text)
446
560
  .join("");
447
- setCursor(getCursor() + text.length);
561
+ setCursor(getCursor() + Array.from(text).length);
448
562
  return;
449
563
  }
450
564
  case "tab":
@@ -914,7 +1028,7 @@ function measureStoryLength(node: XmlNode): number {
914
1028
  case "delInstrText":
915
1029
  return node.children
916
1030
  .filter((child): child is XmlElementNode["children"][number] & { type: "text" } => child.type === "text")
917
- .map((child) => child.text.length)
1031
+ .map((child) => Array.from(child.text).length)
918
1032
  .reduce((total, length) => total + length, 0);
919
1033
  case "tab":
920
1034
  case "br":