@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.
- package/package.json +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- 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
|
-
?
|
|
120
|
-
:
|
|
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
|
+
}
|