@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
@@ -16,6 +16,11 @@ export interface SerializedRuntimeRevisionsResult {
16
16
  skippedRevisionIds: string[];
17
17
  }
18
18
 
19
+ const STORY_WRAPPER_PREFIX =
20
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
21
+ `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>`;
22
+ const STORY_WRAPPER_SUFFIX = `</w:body></w:document>`;
23
+
19
24
  export function serializeRuntimeRevisionsIntoDocumentXml(
20
25
  documentXml: string,
21
26
  revisions: readonly RevisionRecord[],
@@ -34,6 +39,17 @@ export function serializeRuntimeRevisionsIntoDocumentXml(
34
39
  continue;
35
40
  }
36
41
 
42
+ if (revision.kind === "property-change") {
43
+ const propertyChangeReplacement = createPropertyChangeReplacement(documentXml, boundaries, revision);
44
+ if (!propertyChangeReplacement) {
45
+ skippedRevisionIds.push(revision.revisionId);
46
+ continue;
47
+ }
48
+ replacements.push(propertyChangeReplacement);
49
+ serializedRevisionIds.push(revision.revisionId);
50
+ continue;
51
+ }
52
+
37
53
  if (revision.kind !== "insertion" && revision.kind !== "deletion") {
38
54
  continue;
39
55
  }
@@ -89,6 +105,136 @@ export function serializeRuntimeRevisionsIntoDocumentXml(
89
105
  };
90
106
  }
91
107
 
108
+ function createPropertyChangeReplacement(
109
+ documentXml: string,
110
+ boundaries: readonly RevisionParagraphBoundary[],
111
+ revision: RevisionRecord,
112
+ ): XmlReplacement | undefined {
113
+ const propertyChange = revision.metadata.propertyChangeData;
114
+ if (!propertyChange) {
115
+ return undefined;
116
+ }
117
+ if (propertyChange.xmlTag === "pPrChange") {
118
+ return createParagraphPropertyChangeReplacement(documentXml, boundaries, revision, propertyChange.beforeXml);
119
+ }
120
+ if (propertyChange.xmlTag === "rPrChange") {
121
+ return createRunPropertyChangeReplacement(documentXml, boundaries, revision, propertyChange.beforeXml);
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ function createParagraphPropertyChangeReplacement(
127
+ documentXml: string,
128
+ boundaries: readonly RevisionParagraphBoundary[],
129
+ revision: RevisionRecord,
130
+ beforeXml: string,
131
+ ): XmlReplacement | undefined {
132
+ if (revision.anchor.kind !== "range") {
133
+ return undefined;
134
+ }
135
+ const paragraphBoundary = findParagraphBoundaryForRange(
136
+ boundaries,
137
+ revision.anchor.range.from,
138
+ revision.anchor.range.to,
139
+ );
140
+ if (!paragraphBoundary) {
141
+ return undefined;
142
+ }
143
+ const insertionIndex = findClosingTagInsertionIndex(
144
+ documentXml,
145
+ paragraphBoundary.paragraphPropertiesStart,
146
+ paragraphBoundary.paragraphPropertiesEnd,
147
+ "w:pPr",
148
+ );
149
+ if (insertionIndex === undefined) {
150
+ return undefined;
151
+ }
152
+ return {
153
+ start: insertionIndex,
154
+ end: insertionIndex,
155
+ replacement: `<w:pPrChange${serializeRevisionAttributes(revision)}>${beforeXml}</w:pPrChange>`,
156
+ };
157
+ }
158
+
159
+ function createRunPropertyChangeReplacement(
160
+ documentXml: string,
161
+ boundaries: readonly RevisionParagraphBoundary[],
162
+ revision: RevisionRecord,
163
+ beforeXml: string,
164
+ ): XmlReplacement | undefined {
165
+ if (revision.anchor.kind !== "range") {
166
+ return undefined;
167
+ }
168
+ const paragraphBoundary = findParagraphBoundaryForRange(
169
+ boundaries,
170
+ revision.anchor.range.from,
171
+ revision.anchor.range.to,
172
+ );
173
+ if (!paragraphBoundary) {
174
+ return undefined;
175
+ }
176
+ const startIndex = paragraphBoundary.boundaries.get(revision.anchor.range.from);
177
+ const endIndex = paragraphBoundary.boundaries.get(revision.anchor.range.to);
178
+ if (startIndex === undefined || endIndex === undefined || endIndex < startIndex) {
179
+ return undefined;
180
+ }
181
+ const runXml = documentXml.slice(startIndex, endIndex);
182
+ if (!/^<w:r[\s>][\s\S]*<\/w:r>$/u.test(runXml)) {
183
+ return undefined;
184
+ }
185
+ const changeXml = `<w:rPrChange${serializeRevisionAttributes(revision)}>${beforeXml}</w:rPrChange>`;
186
+ let replacement: string;
187
+ if (/<w:rPr[\s>][\s\S]*<\/w:rPr>/u.test(runXml)) {
188
+ replacement = runXml.replace(/<\/w:rPr>/u, `${changeXml}</w:rPr>`);
189
+ } else {
190
+ replacement = runXml.replace(/^(<w:r[\s>][^>]*>)/u, `$1<w:rPr>${changeXml}</w:rPr>`);
191
+ }
192
+ if (replacement === runXml) {
193
+ return undefined;
194
+ }
195
+ return {
196
+ start: startIndex,
197
+ end: endIndex,
198
+ replacement,
199
+ };
200
+ }
201
+
202
+ export function serializeRuntimeRevisionsIntoStoryXml(
203
+ storyXml: string,
204
+ revisions: readonly RevisionRecord[],
205
+ ): SerializedRuntimeRevisionsResult {
206
+ if (revisions.length === 0) {
207
+ return {
208
+ documentXml: storyXml,
209
+ serializedRevisionIds: [],
210
+ skippedRevisionIds: [],
211
+ };
212
+ }
213
+
214
+ const xmlDeclMatch = storyXml.match(/^<\?xml[\s\S]*?\?>\s*/u);
215
+ const xmlDecl = xmlDeclMatch?.[0] ?? "";
216
+ const withoutDecl = xmlDeclMatch ? storyXml.slice(xmlDecl.length) : storyXml;
217
+ const rootMatch = withoutDecl.match(/^<([A-Za-z0-9:._-]+)([^>]*)>([\s\S]*)<\/\1>\s*$/u);
218
+ if (!rootMatch) {
219
+ return serializeRuntimeRevisionsIntoDocumentXml(storyXml, revisions);
220
+ }
221
+
222
+ const [, tagName, attrs = "", innerXml = ""] = rootMatch;
223
+ const wrapped = `${STORY_WRAPPER_PREFIX}${innerXml}${STORY_WRAPPER_SUFFIX}`;
224
+ const serialized = serializeRuntimeRevisionsIntoDocumentXml(wrapped, revisions);
225
+ return {
226
+ documentXml:
227
+ `${xmlDecl}<${tagName}${attrs}>` +
228
+ serialized.documentXml.slice(
229
+ STORY_WRAPPER_PREFIX.length,
230
+ serialized.documentXml.length - STORY_WRAPPER_SUFFIX.length,
231
+ ) +
232
+ `</${tagName}>`,
233
+ serializedRevisionIds: serialized.serializedRevisionIds,
234
+ skippedRevisionIds: serialized.skippedRevisionIds,
235
+ };
236
+ }
237
+
92
238
  function createRangeRevisionReplacement(
93
239
  documentXml: string,
94
240
  boundaries: readonly RevisionParagraphBoundary[],
@@ -110,14 +256,18 @@ function createRangeRevisionReplacement(
110
256
  }
111
257
 
112
258
  const xml = documentXml.slice(startIndex, endIndex);
259
+ const { leadingMarkers, coreXml, trailingMarkers } = peelZeroWidthEdgeMarkers(xml);
260
+ if (coreXml.length === 0) {
261
+ return undefined;
262
+ }
113
263
  const attributes = serializeRevisionAttributes(revision);
114
264
  return {
115
265
  start: startIndex,
116
266
  end: endIndex,
117
267
  replacement:
118
268
  revision.kind === "insertion"
119
- ? `<w:ins${attributes}>${xml}</w:ins>`
120
- : `<w:del${attributes}>${convertRunsToDeletedContent(xml)}</w:del>`,
269
+ ? `${leadingMarkers}<w:ins${attributes}>${coreXml}</w:ins>${trailingMarkers}`
270
+ : `${leadingMarkers}<w:del${attributes}>${convertRunsToDeletedContent(coreXml)}</w:del>${trailingMarkers}`,
121
271
  };
122
272
  }
123
273
 
@@ -214,6 +364,50 @@ function serializeRevisionAttributes(revision: RevisionRecord): string {
214
364
  .join("");
215
365
  }
216
366
 
367
+ function peelZeroWidthEdgeMarkers(xml: string): {
368
+ leadingMarkers: string;
369
+ coreXml: string;
370
+ trailingMarkers: string;
371
+ } {
372
+ const markerPattern =
373
+ /^(?:\s|<(?:\w+:)?(?:bookmarkStart|bookmarkEnd|permStart|permEnd|commentRangeStart|commentRangeEnd|proofErr|lastRenderedPageBreak)\b[^>]*\/>)+$/u;
374
+ let coreXml = xml;
375
+ let leadingMarkers = "";
376
+ let trailingMarkers = "";
377
+
378
+ while (true) {
379
+ const match = coreXml.match(/^\s*(<(?:\w+:)?(?:bookmarkStart|bookmarkEnd|permStart|permEnd|commentRangeStart|commentRangeEnd|proofErr|lastRenderedPageBreak)\b[^>]*\/>)/u);
380
+ if (!match) {
381
+ break;
382
+ }
383
+ leadingMarkers += match[0];
384
+ coreXml = coreXml.slice(match[0].length);
385
+ }
386
+
387
+ while (true) {
388
+ const match = coreXml.match(/(<(?:\w+:)?(?:bookmarkStart|bookmarkEnd|permStart|permEnd|commentRangeStart|commentRangeEnd|proofErr|lastRenderedPageBreak)\b[^>]*\/>)\s*$/u);
389
+ if (!match) {
390
+ break;
391
+ }
392
+ trailingMarkers = match[0] + trailingMarkers;
393
+ coreXml = coreXml.slice(0, coreXml.length - match[0].length);
394
+ }
395
+
396
+ if (markerPattern.test(coreXml)) {
397
+ return {
398
+ leadingMarkers: xml,
399
+ coreXml: "",
400
+ trailingMarkers: "",
401
+ };
402
+ }
403
+
404
+ return {
405
+ leadingMarkers,
406
+ coreXml,
407
+ trailingMarkers,
408
+ };
409
+ }
410
+
217
411
  function findClosingTagInsertionIndex(
218
412
  documentXml: string,
219
413
  start: number | undefined,
@@ -0,0 +1,252 @@
1
+ import type {
2
+ BlockNode,
3
+ HyperlinkNode,
4
+ InlineNode,
5
+ ParagraphNode,
6
+ TableCellNode,
7
+ TableNode,
8
+ } from "../../model/canonical-document.ts";
9
+ import type { RevisionRecord } from "../../review/store/revision-types.ts";
10
+
11
+ interface CursorState {
12
+ value: number;
13
+ }
14
+
15
+ type InsertionRangeRevision = RevisionRecord & {
16
+ kind: "insertion";
17
+ anchor: Extract<RevisionRecord["anchor"], { kind: "range" }>;
18
+ };
19
+
20
+ function isInsertionRangeRevision(revision: RevisionRecord): revision is InsertionRangeRevision {
21
+ return (
22
+ revision.status === "active" &&
23
+ revision.kind === "insertion" &&
24
+ revision.anchor.kind === "range"
25
+ );
26
+ }
27
+
28
+ export function splitStoryBlocksForRuntimeRevisions(
29
+ blocks: readonly BlockNode[],
30
+ revisions: readonly RevisionRecord[],
31
+ ): BlockNode[] {
32
+ const insertionRevisions = revisions.filter(isInsertionRangeRevision);
33
+ if (insertionRevisions.length === 0) {
34
+ return blocks.map(cloneBlock);
35
+ }
36
+
37
+ const cursor: CursorState = { value: 0 };
38
+ return processBlockSequence(blocks, insertionRevisions, cursor);
39
+ }
40
+
41
+ function processBlockSequence(
42
+ blocks: readonly BlockNode[],
43
+ revisions: readonly InsertionRangeRevision[],
44
+ cursor: CursorState,
45
+ ): BlockNode[] {
46
+ const nextBlocks: BlockNode[] = [];
47
+
48
+ for (let index = 0; index < blocks.length; index += 1) {
49
+ const block = blocks[index]!;
50
+ switch (block.type) {
51
+ case "paragraph":
52
+ nextBlocks.push(processParagraph(block, revisions, cursor));
53
+ if (index < blocks.length - 1) {
54
+ cursor.value += 1;
55
+ }
56
+ break;
57
+ case "table":
58
+ nextBlocks.push(processTable(block, revisions, cursor));
59
+ cursor.value += 1;
60
+ break;
61
+ case "sdt":
62
+ case "custom_xml":
63
+ nextBlocks.push({
64
+ ...block,
65
+ children: processBlockSequence(block.children, revisions, cursor),
66
+ });
67
+ break;
68
+ default:
69
+ nextBlocks.push(cloneBlock(block));
70
+ cursor.value += 1;
71
+ }
72
+ }
73
+
74
+ return nextBlocks;
75
+ }
76
+
77
+ function processTable(
78
+ table: TableNode,
79
+ revisions: readonly InsertionRangeRevision[],
80
+ cursor: CursorState,
81
+ ): TableNode {
82
+ return {
83
+ ...table,
84
+ rows: table.rows.map((row) => ({
85
+ ...row,
86
+ cells: row.cells.map((cell) => processTableCell(cell, revisions, cursor)),
87
+ })),
88
+ };
89
+ }
90
+
91
+ function processTableCell(
92
+ cell: TableCellNode,
93
+ revisions: readonly InsertionRangeRevision[],
94
+ cursor: CursorState,
95
+ ): TableCellNode {
96
+ return {
97
+ ...cell,
98
+ children: processBlockSequence(cell.children, revisions, cursor),
99
+ };
100
+ }
101
+
102
+ function processParagraph(
103
+ paragraph: ParagraphNode,
104
+ revisions: readonly InsertionRangeRevision[],
105
+ cursor: CursorState,
106
+ ): ParagraphNode {
107
+ return {
108
+ ...paragraph,
109
+ children: processInlineSequence(
110
+ paragraph.children as readonly InlineNode[],
111
+ revisions,
112
+ cursor,
113
+ ) as ParagraphNode["children"],
114
+ };
115
+ }
116
+
117
+ function processInlineSequence(
118
+ children: readonly InlineNode[],
119
+ revisions: readonly InsertionRangeRevision[],
120
+ cursor: CursorState,
121
+ ): InlineNode[] {
122
+ const nextChildren: InlineNode[] = [];
123
+
124
+ for (const child of children) {
125
+ switch (child.type) {
126
+ case "text": {
127
+ const start = cursor.value;
128
+ const end = start + Array.from(child.text).length;
129
+ const splitPoints = collectSplitPoints(start, end, revisions);
130
+ if (splitPoints.length === 0) {
131
+ nextChildren.push({ ...child });
132
+ } else {
133
+ const chars = Array.from(child.text);
134
+ let previous = 0;
135
+ for (const point of splitPoints) {
136
+ const localEnd = point - start;
137
+ if (localEnd > previous) {
138
+ nextChildren.push({
139
+ ...child,
140
+ text: chars.slice(previous, localEnd).join(""),
141
+ });
142
+ }
143
+ previous = localEnd;
144
+ }
145
+ if (previous < chars.length) {
146
+ nextChildren.push({
147
+ ...child,
148
+ text: chars.slice(previous).join(""),
149
+ });
150
+ }
151
+ }
152
+ cursor.value = end;
153
+ break;
154
+ }
155
+ case "hyperlink":
156
+ nextChildren.push(processHyperlink(child, revisions, cursor));
157
+ break;
158
+ case "field":
159
+ nextChildren.push({
160
+ ...child,
161
+ children: processInlineSequence(child.children, revisions, cursor),
162
+ });
163
+ break;
164
+ case "tab":
165
+ case "hard_break":
166
+ case "footnote_ref":
167
+ case "bookmark_start":
168
+ case "bookmark_end":
169
+ nextChildren.push({ ...child });
170
+ cursor.value += inlineLength(child);
171
+ break;
172
+ default:
173
+ nextChildren.push({ ...child });
174
+ cursor.value += inlineLength(child);
175
+ }
176
+ }
177
+
178
+ return nextChildren;
179
+ }
180
+
181
+ function processHyperlink(
182
+ node: HyperlinkNode,
183
+ revisions: readonly InsertionRangeRevision[],
184
+ cursor: CursorState,
185
+ ): HyperlinkNode {
186
+ return {
187
+ ...node,
188
+ children: processInlineSequence(
189
+ node.children as readonly InlineNode[],
190
+ revisions,
191
+ cursor,
192
+ ) as HyperlinkNode["children"],
193
+ };
194
+ }
195
+
196
+ function collectSplitPoints(
197
+ start: number,
198
+ end: number,
199
+ revisions: readonly InsertionRangeRevision[],
200
+ ): number[] {
201
+ const points = new Set<number>();
202
+ for (const revision of revisions) {
203
+ const from = revision.anchor.range.from;
204
+ const to = revision.anchor.range.to;
205
+ if (from > start && from < end) {
206
+ points.add(from);
207
+ }
208
+ if (to > start && to < end) {
209
+ points.add(to);
210
+ }
211
+ }
212
+ return [...points].sort((left, right) => left - right);
213
+ }
214
+
215
+ function inlineLength(node: InlineNode): number {
216
+ switch (node.type) {
217
+ case "text":
218
+ return Array.from(node.text).length;
219
+ case "tab":
220
+ case "hard_break":
221
+ case "footnote_ref":
222
+ return 1;
223
+ case "hyperlink":
224
+ case "field":
225
+ return node.children.reduce((total, child) => total + inlineLength(child), 0);
226
+ default:
227
+ return 0;
228
+ }
229
+ }
230
+
231
+ function cloneBlock(block: BlockNode): BlockNode {
232
+ switch (block.type) {
233
+ case "paragraph":
234
+ return { ...block, children: block.children.map((child) => ({ ...child })) };
235
+ case "table":
236
+ return {
237
+ ...block,
238
+ rows: block.rows.map((row) => ({
239
+ ...row,
240
+ cells: row.cells.map((cell) => ({
241
+ ...cell,
242
+ children: cell.children.map(cloneBlock),
243
+ })),
244
+ })),
245
+ };
246
+ case "sdt":
247
+ case "custom_xml":
248
+ return { ...block, children: block.children.map(cloneBlock) };
249
+ default:
250
+ return { ...block };
251
+ }
252
+ }