@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.
Files changed (40) hide show
  1. package/README.md +8 -2
  2. package/package.json +35 -21
  3. package/src/api/public-types.ts +103 -1
  4. package/src/core/commands/formatting-commands.ts +742 -0
  5. package/src/core/commands/image-commands.ts +84 -2
  6. package/src/core/commands/structural-helpers.ts +309 -0
  7. package/src/core/commands/table-structure-commands.ts +721 -0
  8. package/src/core/commands/text-commands.ts +166 -1
  9. package/src/core/state/editor-state.ts +318 -9
  10. package/src/formats/xlsx/io/parse-sheet.ts +177 -7
  11. package/src/formats/xlsx/io/parse-styles.ts +2 -0
  12. package/src/formats/xlsx/io/xlsx-session.ts +18 -12
  13. package/src/formats/xlsx/model/sheet.ts +81 -1
  14. package/src/formats/xlsx/model/workbook.ts +10 -6
  15. package/src/io/docx-session.ts +392 -22
  16. package/src/io/export/export-session.ts +55 -0
  17. package/src/io/export/serialize-footnotes.ts +5 -20
  18. package/src/io/export/serialize-headers-footers.ts +5 -31
  19. package/src/io/export/serialize-main-document.ts +78 -5
  20. package/src/io/normalize/normalize-text.ts +90 -1
  21. package/src/io/ooxml/parse-footnotes.ts +68 -5
  22. package/src/io/ooxml/parse-headers-footers.ts +67 -9
  23. package/src/io/ooxml/parse-main-document.ts +169 -6
  24. package/src/io/opc/package-reader.ts +3 -3
  25. package/src/io/source-package-provenance.ts +241 -0
  26. package/src/model/canonical-document.ts +450 -2
  27. package/src/model/cds-1.0.0.ts +5 -2
  28. package/src/model/snapshot.ts +190 -19
  29. package/src/preservation/package-preservation.ts +0 -7
  30. package/src/runtime/document-runtime.ts +7 -1
  31. package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
  32. package/src/runtime/surface-projection.ts +200 -17
  33. package/src/runtime/table-commands.ts +79 -0
  34. package/src/runtime/table-schema.ts +9 -0
  35. package/src/ui/WordReviewEditor.tsx +708 -16
  36. package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
  37. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +73 -7
  38. package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
  39. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
  40. 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 { createCanonicalDocumentId } from "../core/state/editor-state.ts";
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 { exportSnapshotToMinimalDocx } from "../io/export/minimal-docx.ts";
38
- import { DOCX_MIME_TYPE } from "../io/opc/docx-package.ts";
58
+ import {
59
+ decodePersistedSourcePackageBytes,
60
+ hasValidPersistedSourcePackageDigest,
61
+ } from "../io/source-package-provenance.ts";
39
62
  import { deriveCapabilities } from "../runtime/session-capabilities";
40
- import { TwProseMirrorSurface } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
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: initialSnapshot.editorBuild,
411
+ editorBuild: runtimeSnapshot.editorBuild,
245
412
  fatalError: docxSession?.fatalError,
246
- exportDocx: async (snapshot, options) =>
247
- docxSession
248
- ? docxSession.exportDocx(snapshot, options)
249
- : {
250
- bytes: exportSnapshotToMinimalDocx(snapshot),
251
- mimeType: DOCX_MIME_TYPE,
252
- fileName: options?.fileName ?? `${args.documentId}.docx`,
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
- const result = await input.runtime.exportDocx(input.options);
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/1",
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,