@beyondwork/docx-react-component 1.0.11 → 1.0.13
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/README.md +8 -2
- package/package.json +35 -21
- package/src/api/public-types.ts +103 -1
- package/src/core/commands/formatting-commands.ts +742 -0
- package/src/core/commands/image-commands.ts +84 -2
- package/src/core/commands/structural-helpers.ts +309 -0
- package/src/core/commands/table-structure-commands.ts +721 -0
- package/src/core/commands/text-commands.ts +166 -1
- package/src/core/state/editor-state.ts +318 -9
- package/src/formats/xlsx/io/parse-sheet.ts +177 -7
- package/src/formats/xlsx/io/parse-styles.ts +2 -0
- package/src/formats/xlsx/io/xlsx-session.ts +18 -12
- package/src/formats/xlsx/model/sheet.ts +81 -1
- package/src/formats/xlsx/model/workbook.ts +10 -6
- package/src/io/docx-session.ts +392 -22
- package/src/io/export/export-session.ts +55 -0
- package/src/io/export/serialize-footnotes.ts +5 -20
- package/src/io/export/serialize-headers-footers.ts +5 -31
- package/src/io/export/serialize-main-document.ts +78 -5
- package/src/io/normalize/normalize-text.ts +90 -1
- package/src/io/ooxml/parse-footnotes.ts +68 -5
- package/src/io/ooxml/parse-headers-footers.ts +67 -9
- package/src/io/ooxml/parse-main-document.ts +169 -6
- package/src/io/opc/package-reader.ts +3 -3
- package/src/io/source-package-provenance.ts +241 -0
- package/src/model/canonical-document.ts +450 -2
- package/src/model/cds-1.0.0.ts +5 -2
- package/src/model/snapshot.ts +190 -19
- package/src/preservation/package-preservation.ts +0 -7
- package/src/runtime/document-runtime.ts +7 -1
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
- package/src/runtime/surface-projection.ts +200 -17
- package/src/runtime/table-commands.ts +79 -0
- package/src/runtime/table-schema.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +708 -16
- package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +73 -7
- package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
- package/src/validation/compatibility-engine.ts +208 -0
|
@@ -14,9 +14,14 @@ import type {
|
|
|
14
14
|
CompatibilityReport,
|
|
15
15
|
EditorError,
|
|
16
16
|
EditorWarning,
|
|
17
|
+
FormattingAlignment,
|
|
17
18
|
ExportDocxOptions,
|
|
19
|
+
InsertImageOptions,
|
|
20
|
+
InsertTableOptions,
|
|
18
21
|
PersistedEditorSnapshot,
|
|
19
22
|
RuntimeRenderSnapshot,
|
|
23
|
+
SearchOptions,
|
|
24
|
+
SearchResultSnapshot,
|
|
20
25
|
SelectionSnapshot as PublicSelectionSnapshot,
|
|
21
26
|
ExportResult,
|
|
22
27
|
WordReviewEditorEvent,
|
|
@@ -27,17 +32,42 @@ import {
|
|
|
27
32
|
createDetachedAnchor,
|
|
28
33
|
createNodeAnchor,
|
|
29
34
|
createRangeAnchor,
|
|
35
|
+
type TransactionMapping,
|
|
30
36
|
} from "../core/selection/mapping.ts";
|
|
31
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
applyFormattingOperationToDocument,
|
|
39
|
+
getFormattingStateFromRenderSnapshot,
|
|
40
|
+
} from "../core/commands/formatting-commands.ts";
|
|
41
|
+
import { insertImage as insertImageInDocument } from "../core/commands/image-commands.ts";
|
|
42
|
+
import {
|
|
43
|
+
applyTableStructureOperation,
|
|
44
|
+
} from "../core/commands/table-structure-commands.ts";
|
|
45
|
+
import {
|
|
46
|
+
insertPageBreak as insertPageBreakInDocument,
|
|
47
|
+
insertTable as insertTableInDocument,
|
|
48
|
+
} from "../core/commands/text-commands.ts";
|
|
49
|
+
import {
|
|
50
|
+
createCanonicalDocumentId,
|
|
51
|
+
type SelectionSnapshot as InternalSelectionSnapshot,
|
|
52
|
+
} from "../core/state/editor-state.ts";
|
|
32
53
|
import {
|
|
33
54
|
createDocumentRuntime,
|
|
34
55
|
type DocumentRuntime,
|
|
35
56
|
} from "../runtime/document-runtime.ts";
|
|
36
57
|
import { loadDocxEditorSession } from "../io/docx-session.ts";
|
|
37
|
-
import {
|
|
38
|
-
|
|
58
|
+
import {
|
|
59
|
+
decodePersistedSourcePackageBytes,
|
|
60
|
+
hasValidPersistedSourcePackageDigest,
|
|
61
|
+
} from "../io/source-package-provenance.ts";
|
|
39
62
|
import { deriveCapabilities } from "../runtime/session-capabilities";
|
|
40
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
createSearchExcerpt,
|
|
65
|
+
findSearchMatches,
|
|
66
|
+
} from "../ui-tailwind/editor-surface/search-plugin";
|
|
67
|
+
import {
|
|
68
|
+
TwProseMirrorSurface,
|
|
69
|
+
type TwProseMirrorSurfaceRef,
|
|
70
|
+
} from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
|
|
41
71
|
import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
|
|
42
72
|
import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail";
|
|
43
73
|
import type { ViewMode } from "../ui-tailwind/toolbar/tw-toolbar";
|
|
@@ -69,6 +99,15 @@ interface WordReviewEditorRuntime extends DocumentRuntime {
|
|
|
69
99
|
dispose?(): void;
|
|
70
100
|
}
|
|
71
101
|
|
|
102
|
+
type PackageBackedDocxSession = ReturnType<typeof loadDocxEditorSession>;
|
|
103
|
+
|
|
104
|
+
interface SnapshotExportBarrier {
|
|
105
|
+
reason:
|
|
106
|
+
| "missing_source_package_provenance"
|
|
107
|
+
| "invalid_source_package_provenance";
|
|
108
|
+
message: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
72
111
|
const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
|
|
73
112
|
position: "absolute",
|
|
74
113
|
width: "1px",
|
|
@@ -92,6 +131,7 @@ type AccessibleRegionId = (typeof ACCESSIBLE_REGION_ORDER)[number];
|
|
|
92
131
|
|
|
93
132
|
export function __createWordReviewEditorRefBridge(
|
|
94
133
|
runtime: WordReviewEditorRuntime,
|
|
134
|
+
mountedSurface?: TwProseMirrorSurfaceRef | null,
|
|
95
135
|
): WordReviewEditorRef {
|
|
96
136
|
return {
|
|
97
137
|
focus: () => runtime.focus(),
|
|
@@ -114,6 +154,127 @@ export function __createWordReviewEditorRefBridge(
|
|
|
114
154
|
getWarnings: () => runtime.getWarnings(),
|
|
115
155
|
getComments: () => runtime.getRenderSnapshot().comments,
|
|
116
156
|
getTrackedChanges: () => runtime.getRenderSnapshot().trackedChanges,
|
|
157
|
+
getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
|
|
158
|
+
toggleBold: () => {
|
|
159
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
|
|
160
|
+
},
|
|
161
|
+
toggleItalic: () => {
|
|
162
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "italic" });
|
|
163
|
+
},
|
|
164
|
+
toggleUnderline: () => {
|
|
165
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "underline" });
|
|
166
|
+
},
|
|
167
|
+
toggleStrikethrough: () => {
|
|
168
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
169
|
+
type: "toggle",
|
|
170
|
+
mark: "strikethrough",
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
toggleSuperscript: () => {
|
|
174
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
175
|
+
type: "toggle",
|
|
176
|
+
mark: "superscript",
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
toggleSubscript: () => {
|
|
180
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "subscript" });
|
|
181
|
+
},
|
|
182
|
+
setFontFamily: (fontFamily) => {
|
|
183
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
184
|
+
type: "set-font-family",
|
|
185
|
+
fontFamily,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
setFontSize: (size) => {
|
|
189
|
+
applyRuntimeFormattingOperation(runtime, { type: "set-font-size", size });
|
|
190
|
+
},
|
|
191
|
+
setTextColor: (color) => {
|
|
192
|
+
applyRuntimeFormattingOperation(runtime, { type: "set-text-color", color });
|
|
193
|
+
},
|
|
194
|
+
setHighlightColor: (color) => {
|
|
195
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
196
|
+
type: "set-highlight-color",
|
|
197
|
+
color,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
setAlignment: (alignment) => {
|
|
201
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
202
|
+
type: "set-alignment",
|
|
203
|
+
alignment,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
indent: () => {
|
|
207
|
+
applyRuntimeFormattingOperation(runtime, { type: "indent" });
|
|
208
|
+
},
|
|
209
|
+
outdent: () => {
|
|
210
|
+
applyRuntimeFormattingOperation(runtime, { type: "outdent" });
|
|
211
|
+
},
|
|
212
|
+
insertPageBreak: () => {
|
|
213
|
+
applyRuntimeInsertPageBreak(runtime);
|
|
214
|
+
},
|
|
215
|
+
insertTable: (options) => {
|
|
216
|
+
applyRuntimeInsertTable(runtime, options);
|
|
217
|
+
},
|
|
218
|
+
insertImage: (options) => {
|
|
219
|
+
applyRuntimeInsertImage(runtime, options);
|
|
220
|
+
},
|
|
221
|
+
addRowBefore: () => {
|
|
222
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
223
|
+
type: "add-row-before",
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
addRowAfter: () => {
|
|
227
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
228
|
+
type: "add-row-after",
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
addColumnBefore: () => {
|
|
232
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
233
|
+
type: "add-column-before",
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
addColumnAfter: () => {
|
|
237
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
238
|
+
type: "add-column-after",
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
deleteRow: () => {
|
|
242
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
243
|
+
type: "delete-row",
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
deleteColumn: () => {
|
|
247
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
248
|
+
type: "delete-column",
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
deleteTable: () => {
|
|
252
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
253
|
+
type: "delete-table",
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
mergeCells: () => {
|
|
257
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
258
|
+
type: "merge-cells",
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
splitCell: () => {
|
|
262
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
263
|
+
type: "split-cell",
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
setCellBackground: (color) => {
|
|
267
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
268
|
+
type: "set-cell-background",
|
|
269
|
+
color,
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
search: (query, options) =>
|
|
273
|
+
mountedSurface?.search(query, options) ??
|
|
274
|
+
searchSnapshotSurface(runtime.getRenderSnapshot(), query, options),
|
|
275
|
+
clearSearch: () => {
|
|
276
|
+
mountedSurface?.clearSearch();
|
|
277
|
+
},
|
|
117
278
|
scrollToRevision: (revisionId: string) => {
|
|
118
279
|
const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
119
280
|
(r) => r.revisionId === revisionId,
|
|
@@ -227,6 +388,9 @@ function createRuntime(
|
|
|
227
388
|
editorBuild: "dev",
|
|
228
389
|
})
|
|
229
390
|
: undefined;
|
|
391
|
+
const snapshotExportResolution = !args.source.initialDocx
|
|
392
|
+
? resolveSnapshotExportSession(args)
|
|
393
|
+
: undefined;
|
|
230
394
|
const initialSnapshot =
|
|
231
395
|
args.source.initialSnapshot ??
|
|
232
396
|
docxSession?.initialSnapshot ??
|
|
@@ -234,23 +398,36 @@ function createRuntime(
|
|
|
234
398
|
args.documentId,
|
|
235
399
|
args.source.sourceLabel ?? "Generated shell snapshot",
|
|
236
400
|
);
|
|
401
|
+
const runtimeSnapshot = snapshotExportResolution?.barrier
|
|
402
|
+
? applySnapshotExportBarrier(initialSnapshot, snapshotExportResolution.barrier)
|
|
403
|
+
: initialSnapshot;
|
|
237
404
|
|
|
238
405
|
return createDocumentRuntime({
|
|
239
406
|
documentId: args.documentId,
|
|
240
|
-
initialSnapshot,
|
|
407
|
+
initialSnapshot: runtimeSnapshot,
|
|
241
408
|
sourceKind: args.source.source,
|
|
242
409
|
sourceLabel: args.source.sourceLabel,
|
|
243
410
|
readOnly: args.readOnly || docxSession?.readOnly,
|
|
244
|
-
editorBuild:
|
|
411
|
+
editorBuild: runtimeSnapshot.editorBuild,
|
|
245
412
|
fatalError: docxSession?.fatalError,
|
|
246
|
-
exportDocx: async (snapshot, options) =>
|
|
247
|
-
docxSession
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
413
|
+
exportDocx: async (snapshot, options) => {
|
|
414
|
+
if (docxSession) {
|
|
415
|
+
return docxSession.exportDocx(snapshot, options);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (snapshotExportResolution?.session) {
|
|
419
|
+
return snapshotExportResolution.session.exportDocx(snapshot, options);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
throw createSnapshotExportBlockedError(
|
|
423
|
+
args.documentId,
|
|
424
|
+
snapshotExportResolution?.barrier ?? {
|
|
425
|
+
reason: "missing_source_package_provenance",
|
|
426
|
+
message:
|
|
427
|
+
"DOCX export is blocked because this snapshot does not carry embedded source package provenance.",
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
},
|
|
254
431
|
onWarning: handlers.onWarning,
|
|
255
432
|
onError: handlers.onError,
|
|
256
433
|
defaultAuthorId: args.currentUserId,
|
|
@@ -285,6 +462,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
285
462
|
const [showTrackedChanges, setShowTrackedChanges] = useState(false);
|
|
286
463
|
const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
|
|
287
464
|
const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
|
|
465
|
+
const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
|
|
288
466
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
289
467
|
const lastAnnouncedErrorIdRef = useRef<string | null>(null);
|
|
290
468
|
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -520,6 +698,146 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
520
698
|
getWarnings: () => activeRuntime.getWarnings(),
|
|
521
699
|
getComments: () => activeRuntime.getRenderSnapshot().comments,
|
|
522
700
|
getTrackedChanges: () => activeRuntime.getRenderSnapshot().trackedChanges,
|
|
701
|
+
getFormattingState: () =>
|
|
702
|
+
getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
|
|
703
|
+
toggleBold: () => {
|
|
704
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
705
|
+
type: "toggle",
|
|
706
|
+
mark: "bold",
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
toggleItalic: () => {
|
|
710
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
711
|
+
type: "toggle",
|
|
712
|
+
mark: "italic",
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
toggleUnderline: () => {
|
|
716
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
717
|
+
type: "toggle",
|
|
718
|
+
mark: "underline",
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
toggleStrikethrough: () => {
|
|
722
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
723
|
+
type: "toggle",
|
|
724
|
+
mark: "strikethrough",
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
toggleSuperscript: () => {
|
|
728
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
729
|
+
type: "toggle",
|
|
730
|
+
mark: "superscript",
|
|
731
|
+
});
|
|
732
|
+
},
|
|
733
|
+
toggleSubscript: () => {
|
|
734
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
735
|
+
type: "toggle",
|
|
736
|
+
mark: "subscript",
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
setFontFamily: (fontFamily) => {
|
|
740
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
741
|
+
type: "set-font-family",
|
|
742
|
+
fontFamily,
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
setFontSize: (size) => {
|
|
746
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
747
|
+
type: "set-font-size",
|
|
748
|
+
size,
|
|
749
|
+
});
|
|
750
|
+
},
|
|
751
|
+
setTextColor: (color) => {
|
|
752
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
753
|
+
type: "set-text-color",
|
|
754
|
+
color,
|
|
755
|
+
});
|
|
756
|
+
},
|
|
757
|
+
setHighlightColor: (color) => {
|
|
758
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
759
|
+
type: "set-highlight-color",
|
|
760
|
+
color,
|
|
761
|
+
});
|
|
762
|
+
},
|
|
763
|
+
setAlignment: (alignment) => {
|
|
764
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
765
|
+
type: "set-alignment",
|
|
766
|
+
alignment,
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
indent: () => {
|
|
770
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "indent" });
|
|
771
|
+
},
|
|
772
|
+
outdent: () => {
|
|
773
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" });
|
|
774
|
+
},
|
|
775
|
+
insertPageBreak: () => {
|
|
776
|
+
applyRuntimeInsertPageBreak(activeRuntime);
|
|
777
|
+
},
|
|
778
|
+
insertTable: (options) => {
|
|
779
|
+
applyRuntimeInsertTable(activeRuntime, options);
|
|
780
|
+
},
|
|
781
|
+
insertImage: (options) => {
|
|
782
|
+
applyRuntimeInsertImage(activeRuntime, options);
|
|
783
|
+
},
|
|
784
|
+
addRowBefore: () => {
|
|
785
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
786
|
+
type: "add-row-before",
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
addRowAfter: () => {
|
|
790
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
791
|
+
type: "add-row-after",
|
|
792
|
+
});
|
|
793
|
+
},
|
|
794
|
+
addColumnBefore: () => {
|
|
795
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
796
|
+
type: "add-column-before",
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
addColumnAfter: () => {
|
|
800
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
801
|
+
type: "add-column-after",
|
|
802
|
+
});
|
|
803
|
+
},
|
|
804
|
+
deleteRow: () => {
|
|
805
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
806
|
+
type: "delete-row",
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
deleteColumn: () => {
|
|
810
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
811
|
+
type: "delete-column",
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
deleteTable: () => {
|
|
815
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
816
|
+
type: "delete-table",
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
mergeCells: () => {
|
|
820
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
821
|
+
type: "merge-cells",
|
|
822
|
+
});
|
|
823
|
+
},
|
|
824
|
+
splitCell: () => {
|
|
825
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
826
|
+
type: "split-cell",
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
setCellBackground: (color) => {
|
|
830
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
831
|
+
type: "set-cell-background",
|
|
832
|
+
color,
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
search: (query, options) =>
|
|
836
|
+
surfaceRef.current?.search(query, options) ??
|
|
837
|
+
searchSnapshotSurface(activeRuntime.getRenderSnapshot(), query, options),
|
|
838
|
+
clearSearch: () => {
|
|
839
|
+
surfaceRef.current?.clearSearch();
|
|
840
|
+
},
|
|
523
841
|
scrollToRevision: (revisionId: string) => {
|
|
524
842
|
const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
525
843
|
(r) => r.revisionId === revisionId,
|
|
@@ -835,6 +1153,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
835
1153
|
{...reviewCallbacks}
|
|
836
1154
|
document={
|
|
837
1155
|
<TwProseMirrorSurface
|
|
1156
|
+
ref={surfaceRef}
|
|
838
1157
|
currentUser={currentUser}
|
|
839
1158
|
snapshot={snapshot}
|
|
840
1159
|
reviewMode={reviewMode}
|
|
@@ -858,6 +1177,231 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
858
1177
|
},
|
|
859
1178
|
);
|
|
860
1179
|
|
|
1180
|
+
function applyRuntimeFormattingOperation(
|
|
1181
|
+
runtime: WordReviewEditorRuntime,
|
|
1182
|
+
operation:
|
|
1183
|
+
| { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
|
|
1184
|
+
| { type: "set-font-family"; fontFamily: string | null }
|
|
1185
|
+
| { type: "set-font-size"; size: number | null }
|
|
1186
|
+
| { type: "set-text-color"; color: string | null }
|
|
1187
|
+
| { type: "set-highlight-color"; color: string | null }
|
|
1188
|
+
| { type: "set-alignment"; alignment: FormattingAlignment }
|
|
1189
|
+
| { type: "indent" }
|
|
1190
|
+
| { type: "outdent" },
|
|
1191
|
+
): void {
|
|
1192
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1193
|
+
if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const result = applyFormattingOperationToDocument(
|
|
1198
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1199
|
+
snapshot,
|
|
1200
|
+
operation,
|
|
1201
|
+
);
|
|
1202
|
+
if (!result.changed) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
runtime.dispatch({
|
|
1207
|
+
type: "document.replace",
|
|
1208
|
+
document: result.document,
|
|
1209
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1210
|
+
origin: {
|
|
1211
|
+
source: "api",
|
|
1212
|
+
timestamp: new Date().toISOString(),
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
|
|
1218
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1219
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const timestamp = new Date().toISOString();
|
|
1224
|
+
const result = insertPageBreakInDocument(
|
|
1225
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1226
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1227
|
+
{ timestamp },
|
|
1228
|
+
);
|
|
1229
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function applyRuntimeInsertTable(
|
|
1233
|
+
runtime: WordReviewEditorRuntime,
|
|
1234
|
+
options: InsertTableOptions,
|
|
1235
|
+
): void {
|
|
1236
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1237
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const timestamp = new Date().toISOString();
|
|
1242
|
+
const result = insertTableInDocument(
|
|
1243
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1244
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1245
|
+
options,
|
|
1246
|
+
{ timestamp },
|
|
1247
|
+
);
|
|
1248
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function applyRuntimeInsertImage(
|
|
1252
|
+
runtime: WordReviewEditorRuntime,
|
|
1253
|
+
options: InsertImageOptions,
|
|
1254
|
+
): void {
|
|
1255
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1256
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const timestamp = new Date().toISOString();
|
|
1261
|
+
try {
|
|
1262
|
+
const result = insertImageInDocument(
|
|
1263
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1264
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1265
|
+
options.data,
|
|
1266
|
+
options.mimeType,
|
|
1267
|
+
options.width,
|
|
1268
|
+
options.height,
|
|
1269
|
+
{
|
|
1270
|
+
timestamp,
|
|
1271
|
+
altText: options.altText,
|
|
1272
|
+
},
|
|
1273
|
+
);
|
|
1274
|
+
dispatchRuntimeDocumentMutation(runtime, {
|
|
1275
|
+
changed: true,
|
|
1276
|
+
document: result.document,
|
|
1277
|
+
selection: result.selection,
|
|
1278
|
+
mapping: result.mapping,
|
|
1279
|
+
}, timestamp);
|
|
1280
|
+
} catch {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function applyRuntimeTableStructureOperation(
|
|
1286
|
+
runtime: WordReviewEditorRuntime,
|
|
1287
|
+
mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
|
|
1288
|
+
operation:
|
|
1289
|
+
| { type: "add-row-before" }
|
|
1290
|
+
| { type: "add-row-after" }
|
|
1291
|
+
| { type: "add-column-before" }
|
|
1292
|
+
| { type: "add-column-after" }
|
|
1293
|
+
| { type: "delete-row" }
|
|
1294
|
+
| { type: "delete-column" }
|
|
1295
|
+
| { type: "delete-table" }
|
|
1296
|
+
| { type: "merge-cells" }
|
|
1297
|
+
| { type: "split-cell" }
|
|
1298
|
+
| { type: "set-cell-background"; color: string },
|
|
1299
|
+
): void {
|
|
1300
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1301
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const timestamp = new Date().toISOString();
|
|
1306
|
+
const result = applyTableStructureOperation(
|
|
1307
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1308
|
+
snapshot,
|
|
1309
|
+
mountedSurface?.getTableSelection() ?? null,
|
|
1310
|
+
operation,
|
|
1311
|
+
);
|
|
1312
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function canApplyRuntimeMutation(snapshot: RuntimeRenderSnapshot): boolean {
|
|
1316
|
+
return snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function dispatchRuntimeDocumentMutation(
|
|
1320
|
+
runtime: WordReviewEditorRuntime,
|
|
1321
|
+
result: {
|
|
1322
|
+
changed: boolean;
|
|
1323
|
+
document: PersistedEditorSnapshot["canonicalDocument"];
|
|
1324
|
+
selection: InternalSelectionSnapshot;
|
|
1325
|
+
mapping?: TransactionMapping;
|
|
1326
|
+
},
|
|
1327
|
+
timestamp: string,
|
|
1328
|
+
): void {
|
|
1329
|
+
if (!result.changed) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
runtime.dispatch({
|
|
1334
|
+
type: "document.replace",
|
|
1335
|
+
document: {
|
|
1336
|
+
...result.document,
|
|
1337
|
+
updatedAt: timestamp,
|
|
1338
|
+
},
|
|
1339
|
+
mapping: result.mapping,
|
|
1340
|
+
selection: result.selection,
|
|
1341
|
+
origin: {
|
|
1342
|
+
source: "api",
|
|
1343
|
+
timestamp,
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function searchSnapshotSurface(
|
|
1349
|
+
snapshot: RuntimeRenderSnapshot,
|
|
1350
|
+
query: string,
|
|
1351
|
+
options: SearchOptions = {},
|
|
1352
|
+
): SearchResultSnapshot[] {
|
|
1353
|
+
const normalizedQuery = query.trim();
|
|
1354
|
+
if (!normalizedQuery || !snapshot.surface) {
|
|
1355
|
+
return [];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const rawResults = findSearchMatches(
|
|
1359
|
+
snapshot.surface.plainText,
|
|
1360
|
+
normalizedQuery,
|
|
1361
|
+
options,
|
|
1362
|
+
).slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
1363
|
+
const activeResultIndex = getActiveSearchResultIndex(rawResults, snapshot.selection);
|
|
1364
|
+
|
|
1365
|
+
return rawResults.map((result, index) => ({
|
|
1366
|
+
resultId: `search-result-${index}`,
|
|
1367
|
+
anchor: {
|
|
1368
|
+
kind: "range",
|
|
1369
|
+
from: result.from,
|
|
1370
|
+
to: result.to,
|
|
1371
|
+
assoc: {
|
|
1372
|
+
start: -1,
|
|
1373
|
+
end: 1,
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
excerpt: createSearchExcerpt(
|
|
1377
|
+
snapshot.surface?.plainText ?? "",
|
|
1378
|
+
result.from,
|
|
1379
|
+
result.to,
|
|
1380
|
+
),
|
|
1381
|
+
isActive: index === activeResultIndex,
|
|
1382
|
+
}));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function getActiveSearchResultIndex(
|
|
1386
|
+
results: Array<{ from: number; to: number }>,
|
|
1387
|
+
selection: PublicSelectionSnapshot,
|
|
1388
|
+
): number {
|
|
1389
|
+
if (results.length === 0) {
|
|
1390
|
+
return -1;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
1394
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
1395
|
+
const activeIndex = results.findIndex((result) => {
|
|
1396
|
+
if (selectionFrom === selectionTo) {
|
|
1397
|
+
return selectionFrom >= result.from && selectionFrom <= result.to;
|
|
1398
|
+
}
|
|
1399
|
+
return selectionFrom < result.to && selectionTo > result.from;
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
return activeIndex >= 0 ? activeIndex : 0;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
861
1405
|
function applyRegionAttributes(shell: HTMLElement): void {
|
|
862
1406
|
const toolbar = shell.querySelector<HTMLElement>("header");
|
|
863
1407
|
if (toolbar) {
|
|
@@ -1006,6 +1550,7 @@ function guessSourceLabel(
|
|
|
1006
1550
|
return (
|
|
1007
1551
|
externalDocSource?.sourceLabel ??
|
|
1008
1552
|
initialSourceLabel ??
|
|
1553
|
+
initialSnapshot?.sourcePackage?.sourceLabel ??
|
|
1009
1554
|
initialSnapshot?.editorBuild ??
|
|
1010
1555
|
undefined
|
|
1011
1556
|
);
|
|
@@ -1106,7 +1651,23 @@ async function persistAndExport(input: {
|
|
|
1106
1651
|
lastSavedRevisionTokenRef: input.lastSavedRevisionTokenRef,
|
|
1107
1652
|
});
|
|
1108
1653
|
|
|
1109
|
-
|
|
1654
|
+
let result: ExportResult;
|
|
1655
|
+
try {
|
|
1656
|
+
result = await input.runtime.exportDocx(input.options);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
const normalized = normalizeExportError(error, input.documentId, input.options);
|
|
1659
|
+
input.onError?.(normalized);
|
|
1660
|
+
emitEditorEvent({
|
|
1661
|
+
datastore: input.datastore,
|
|
1662
|
+
onEvent: input.onEvent,
|
|
1663
|
+
event: {
|
|
1664
|
+
type: "error",
|
|
1665
|
+
documentId: input.documentId,
|
|
1666
|
+
error: normalized,
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
throw normalized;
|
|
1670
|
+
}
|
|
1110
1671
|
|
|
1111
1672
|
if (!input.datastore) {
|
|
1112
1673
|
return result;
|
|
@@ -1397,7 +1958,7 @@ function createFallbackPersistedSnapshot(
|
|
|
1397
1958
|
): PersistedEditorSnapshot {
|
|
1398
1959
|
const docId = createCanonicalDocumentId(documentId);
|
|
1399
1960
|
return {
|
|
1400
|
-
snapshotVersion: "persisted-editor-snapshot/
|
|
1961
|
+
snapshotVersion: "persisted-editor-snapshot/2",
|
|
1401
1962
|
schemaVersion: "cds/1.0.0",
|
|
1402
1963
|
documentId,
|
|
1403
1964
|
docId,
|
|
@@ -1458,6 +2019,137 @@ function emptyCompatibilityReport(): CompatibilityReport {
|
|
|
1458
2019
|
};
|
|
1459
2020
|
}
|
|
1460
2021
|
|
|
2022
|
+
function resolveSnapshotExportSession(args: CreateRuntimeArgs): {
|
|
2023
|
+
session?: PackageBackedDocxSession;
|
|
2024
|
+
barrier?: SnapshotExportBarrier;
|
|
2025
|
+
} {
|
|
2026
|
+
const sourcePackage = args.source.initialSnapshot?.sourcePackage;
|
|
2027
|
+
if (!sourcePackage) {
|
|
2028
|
+
return {
|
|
2029
|
+
barrier: {
|
|
2030
|
+
reason: "missing_source_package_provenance",
|
|
2031
|
+
message:
|
|
2032
|
+
"DOCX export is blocked because this snapshot was loaded without embedded source package provenance.",
|
|
2033
|
+
},
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
try {
|
|
2038
|
+
const bytes = decodePersistedSourcePackageBytes(sourcePackage);
|
|
2039
|
+
if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
|
|
2040
|
+
return {
|
|
2041
|
+
barrier: {
|
|
2042
|
+
reason: "invalid_source_package_provenance",
|
|
2043
|
+
message:
|
|
2044
|
+
"DOCX export is blocked because the embedded source package provenance failed its integrity check.",
|
|
2045
|
+
},
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
const session = loadDocxEditorSession({
|
|
2050
|
+
documentId: args.documentId,
|
|
2051
|
+
sourceLabel: sourcePackage.sourceLabel ?? args.source.sourceLabel,
|
|
2052
|
+
bytes,
|
|
2053
|
+
editorBuild: args.source.initialSnapshot?.editorBuild ?? "dev",
|
|
2054
|
+
});
|
|
2055
|
+
if (session.readOnly || session.fatalError) {
|
|
2056
|
+
return {
|
|
2057
|
+
barrier: {
|
|
2058
|
+
reason: "invalid_source_package_provenance",
|
|
2059
|
+
message:
|
|
2060
|
+
"DOCX export is blocked because the embedded source package provenance is no longer loadable as a valid package-backed session.",
|
|
2061
|
+
},
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return { session };
|
|
2066
|
+
} catch {
|
|
2067
|
+
return {
|
|
2068
|
+
barrier: {
|
|
2069
|
+
reason: "invalid_source_package_provenance",
|
|
2070
|
+
message:
|
|
2071
|
+
"DOCX export is blocked because the embedded source package provenance could not be decoded into a package-backed session.",
|
|
2072
|
+
},
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function applySnapshotExportBarrier(
|
|
2078
|
+
snapshot: PersistedEditorSnapshot,
|
|
2079
|
+
barrier: SnapshotExportBarrier,
|
|
2080
|
+
): PersistedEditorSnapshot {
|
|
2081
|
+
const featureEntryId = `feature:source-package-provenance:${barrier.reason}`;
|
|
2082
|
+
const featureEntries = snapshot.compatibility.featureEntries.some(
|
|
2083
|
+
(entry) => entry.featureEntryId === featureEntryId,
|
|
2084
|
+
)
|
|
2085
|
+
? snapshot.compatibility.featureEntries
|
|
2086
|
+
: [
|
|
2087
|
+
...snapshot.compatibility.featureEntries,
|
|
2088
|
+
{
|
|
2089
|
+
featureEntryId,
|
|
2090
|
+
featureKey: "source-package-provenance",
|
|
2091
|
+
featureClass: "unsupported-fatal" as const,
|
|
2092
|
+
message: barrier.message,
|
|
2093
|
+
details: {
|
|
2094
|
+
reason: barrier.reason,
|
|
2095
|
+
},
|
|
2096
|
+
},
|
|
2097
|
+
];
|
|
2098
|
+
|
|
2099
|
+
return {
|
|
2100
|
+
...snapshot,
|
|
2101
|
+
compatibility: {
|
|
2102
|
+
...snapshot.compatibility,
|
|
2103
|
+
blockExport: true,
|
|
2104
|
+
featureEntries,
|
|
2105
|
+
},
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function createSnapshotExportBlockedError(
|
|
2110
|
+
documentId: string,
|
|
2111
|
+
barrier: SnapshotExportBarrier,
|
|
2112
|
+
): EditorError {
|
|
2113
|
+
return {
|
|
2114
|
+
errorId: `${documentId}:export:${barrier.reason}`,
|
|
2115
|
+
code: "export_failed",
|
|
2116
|
+
message: barrier.message,
|
|
2117
|
+
isFatal: false,
|
|
2118
|
+
source: "export",
|
|
2119
|
+
details: {
|
|
2120
|
+
reason: barrier.reason,
|
|
2121
|
+
},
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function normalizeExportError(
|
|
2126
|
+
error: unknown,
|
|
2127
|
+
documentId: string,
|
|
2128
|
+
options?: ExportDocxOptions,
|
|
2129
|
+
): EditorError {
|
|
2130
|
+
if (
|
|
2131
|
+
typeof error === "object" &&
|
|
2132
|
+
error !== null &&
|
|
2133
|
+
"errorId" in error &&
|
|
2134
|
+
"code" in error &&
|
|
2135
|
+
"message" in error
|
|
2136
|
+
) {
|
|
2137
|
+
return error as EditorError;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
return {
|
|
2141
|
+
errorId: `${documentId}:export:failed`,
|
|
2142
|
+
code: "export_failed",
|
|
2143
|
+
message:
|
|
2144
|
+
error instanceof Error ? error.message : "DOCX export failed for an unknown reason.",
|
|
2145
|
+
isFatal: false,
|
|
2146
|
+
source: "export",
|
|
2147
|
+
details: {
|
|
2148
|
+
requestedOptions: options ?? {},
|
|
2149
|
+
},
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1461
2153
|
function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
|
|
1462
2154
|
return {
|
|
1463
2155
|
anchor: selection.anchor,
|