@beyondwork/docx-react-component 1.0.19 → 1.0.20

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 (70) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +850 -1315
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1422 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +51 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  46. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  47. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  48. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  49. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  50. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  51. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  52. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  53. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  54. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  55. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  56. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  57. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  58. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  59. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +174 -48
  60. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  61. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  62. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  63. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  64. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  65. package/src/ui-tailwind/theme/editor-theme.css +4 -0
  66. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  67. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  68. package/src/validation/compatibility-engine.ts +27 -4
  69. package/src/validation/compatibility-report.ts +1 -0
  70. package/src/validation/docx-comment-proof.ts +220 -0
@@ -7,23 +7,27 @@ import React, {
7
7
  useMemo,
8
8
  useRef,
9
9
  useState,
10
- useSyncExternalStore,
11
10
  } from "react";
12
11
 
13
12
  import type {
14
13
  AutosaveState,
15
14
  CompatibilityReport,
15
+ DocumentNavigationSnapshot,
16
+ EditorAnchorProjection,
16
17
  EditorDatastoreAdapter,
17
18
  EditorHostAdapter,
18
19
  EditorSessionState,
19
20
  ExportResult,
20
21
  EditorError,
21
22
  EditorStoryTarget,
23
+ EditorViewStateSnapshot,
22
24
  EditorWarning,
23
25
  ExportDocxOptions,
26
+ FieldSnapshot,
24
27
  FormattingAlignment,
25
28
  FormattingStateSnapshot,
26
29
  HeaderFooterLinkPatch,
30
+ InteractionGuardSnapshot,
27
31
  InsertImageOptions,
28
32
  InsertTableOptions,
29
33
  PageLayoutSnapshot,
@@ -37,14 +41,21 @@ import type {
37
41
  SelectionSnapshot as PublicSelectionSnapshot,
38
42
  StyleCatalogSnapshot,
39
43
  SurfaceBlockSnapshot,
44
+ TocRefreshResult,
45
+ UpdateFieldsResult,
40
46
  ViewMode as EditorViewMode,
47
+ WorkflowBlockedCommandReason,
48
+ WorkflowScopeSnapshot,
41
49
  WordReviewEditorEvent,
42
50
  WordReviewEditorProps,
43
51
  WordReviewEditorRef,
44
52
  WorkspaceMode,
45
53
  ZoomLevel,
46
54
  } from "../api/public-types";
47
- import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
55
+ import {
56
+ editorSessionStateFromPersistedSnapshot,
57
+ persistedSnapshotFromEditorSessionState,
58
+ } from "../api/session-state.ts";
48
59
  import {
49
60
  createDetachedAnchor,
50
61
  createNodeAnchor,
@@ -98,81 +109,66 @@ import {
98
109
  insertTable as insertTableInDocument,
99
110
  splitParagraph as splitParagraphInDocument,
100
111
  } from "../core/commands/text-commands.ts";
101
- import {
102
- createCanonicalDocumentId,
103
- type SelectionSnapshot as InternalSelectionSnapshot,
104
- } from "../core/state/editor-state.ts";
105
- import {
106
- createDocumentRuntime,
107
- type DocumentRuntime,
108
- } from "../runtime/document-runtime.ts";
112
+ import { type SelectionSnapshot as InternalSelectionSnapshot } from "../core/state/editor-state.ts";
109
113
  import {
110
114
  getStoryBlocks,
111
115
  replaceStoryBlocks,
112
116
  } from "../runtime/story-targeting.ts";
113
- import { loadDocxEditorSession } from "../io/docx-session.ts";
114
117
  import {
115
118
  decodePersistedSourcePackageBytes,
116
119
  hasValidPersistedSourcePackageDigest,
117
120
  } from "../io/source-package-provenance.ts";
121
+ import { readOpcPackage } from "../io/opc/package-reader.ts";
118
122
  import { deriveCapabilities } from "../runtime/session-capabilities";
119
123
  import { searchDocument } from "../runtime/document-search.ts";
120
124
  import {
121
- TwProseMirrorSurface,
125
+ createEditorViewStateSnapshot,
126
+ createViewState,
127
+ } from "../runtime/view-state.ts";
128
+ import {
122
129
  type TwProseMirrorSurfaceRef,
123
130
  } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
124
- import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
131
+ import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-state-from-snapshot";
132
+ import {
133
+ incrementInvalidationCounter,
134
+ recordPerfSample,
135
+ } from "../ui-tailwind/editor-surface/perf-probe.ts";
125
136
  import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail";
137
+ import {
138
+ selectMetaSlice,
139
+ selectReviewSlice,
140
+ selectStatusSlice,
141
+ selectSurfaceSlice,
142
+ selectToolbarSlice,
143
+ selectViewSlice,
144
+ shallowEqualRecord,
145
+ useRuntimeSnapshotSlice,
146
+ useRuntimeValue,
147
+ } from "./runtime-snapshot-selectors.ts";
126
148
  import type { MarkupDisplay } from "./headless/comment-decoration-model";
127
149
  import type {
128
150
  SelectionToolbarAnchor,
129
151
  SelectionToolbarModel,
130
152
  } from "./headless/selection-toolbar-model";
153
+ import { type EditorCommandBag, useCommandBag } from "./editor-command-bag.ts";
154
+ import {
155
+ type WordReviewEditorRuntime,
156
+ persistAndExport as persistAndExportFromBoundary,
157
+ persistSession as persistSessionFromBoundary,
158
+ rejectExportWhileLoading as rejectExportWhileLoadingFromBoundary,
159
+ useEditorRuntimeBoundary,
160
+ } from "./editor-runtime-boundary.ts";
131
161
  import {
132
162
  downloadExportResult,
133
163
  withExportDelivery,
134
164
  } from "./browser-export";
165
+ import { EditorShellView } from "./editor-shell-view.tsx";
166
+ import { EditorSurfaceController } from "./editor-surface-controller.tsx";
135
167
 
136
- interface ResolvedSource {
137
- source: "docx" | "session" | "snapshot";
138
- sourceLabel?: string;
139
- initialDocx?: Uint8Array | ArrayBuffer;
140
- initialSessionState?: EditorSessionState;
141
- initialSnapshot?: PersistedEditorSnapshot;
142
- }
143
-
144
- interface CreateRuntimeArgs {
145
- documentId: string;
146
- readOnly: boolean;
147
- source: ResolvedSource;
148
- initialViewState?: {
149
- workspaceMode?: WorkspaceMode;
150
- zoomLevel?: ZoomLevel;
151
- };
152
- hostAdapter?: EditorHostAdapter;
153
- datastore?: EditorDatastoreAdapter;
154
- currentUserId?: string;
155
- }
156
-
157
- interface RuntimeLifecycleHandlers {
158
- onEvent?: (event: WordReviewEditorEvent) => void;
159
- onWarning?: (warning: EditorWarning) => void;
160
- onError?: (error: EditorError) => void;
161
- }
162
-
163
- interface WordReviewEditorRuntime extends DocumentRuntime {
164
- getFatalError?(): EditorError | undefined;
165
- dispose?(): void;
166
- }
167
-
168
- type PackageBackedDocxSession = ReturnType<typeof loadDocxEditorSession>;
169
-
170
- interface SnapshotExportBarrier {
171
- reason:
172
- | "missing_source_package_provenance"
173
- | "invalid_source_package_provenance";
174
- message: string;
175
- }
168
+ export {
169
+ __createFallbackRuntime,
170
+ __resolveWordReviewEditorSource,
171
+ } from "./editor-runtime-boundary.ts";
176
172
 
177
173
  const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
178
174
  position: "absolute",
@@ -186,6 +182,15 @@ const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
186
182
  border: 0,
187
183
  };
188
184
 
185
+ const BROWSER_SAFE_PREVIEW_TYPES = new Set([
186
+ "image/png",
187
+ "image/jpeg",
188
+ "image/jpg",
189
+ "image/gif",
190
+ "image/webp",
191
+ "image/bmp",
192
+ ]);
193
+
189
194
  const ACCESSIBLE_REGION_ORDER = [
190
195
  "toolbar",
191
196
  "document",
@@ -392,13 +397,28 @@ export function __createWordReviewEditorRefBridge(
392
397
  runtime.closeStory();
393
398
  },
394
399
  getPageLayoutSnapshot: () => {
395
- return runtime.getPageLayoutSnapshot();
400
+ return clonePublicValue(runtime.getPageLayoutSnapshot());
396
401
  },
397
402
  getDocumentNavigationSnapshot: () => {
398
- return runtime.getDocumentNavigationSnapshot();
403
+ return clonePublicValue(runtime.getDocumentNavigationSnapshot());
404
+ },
405
+ getFieldSnapshot: () => {
406
+ return clonePublicValue(runtime.getFieldSnapshot());
407
+ },
408
+ updateFields: (options) => {
409
+ return runtime.updateFields(options);
410
+ },
411
+ updateTableOfContents: (options) => {
412
+ return runtime.updateTableOfContents(options);
399
413
  },
400
414
  getViewState: () => {
401
- return runtime.getViewState();
415
+ return clonePublicValue(runtime.getViewState());
416
+ },
417
+ setDocumentMode: (mode) => {
418
+ runtime.setDocumentMode(mode);
419
+ },
420
+ getProtectionSnapshot: () => {
421
+ return clonePublicValue(runtime.getProtectionSnapshot());
402
422
  },
403
423
  setWorkspaceMode: (mode) => {
404
424
  runtime.setWorkspaceMode(mode);
@@ -427,195 +447,28 @@ export function __createWordReviewEditorRefBridge(
427
447
  setImageFrame: (mediaId, offsets) => {
428
448
  applyRuntimeImageReposition(runtime, mediaId, offsets);
429
449
  },
430
- };
431
- }
432
-
433
- export async function __resolveWordReviewEditorSource(
434
- props: Pick<
435
- WordReviewEditorProps,
436
- | "documentId"
437
- | "hostAdapter"
438
- | "datastore"
439
- | "externalDocSource"
440
- | "initialDocx"
441
- | "initialSessionState"
442
- | "initialSnapshot"
443
- | "initialSourceLabel"
444
- | "loadRevision"
445
- | "loadSourcePolicy"
446
- >,
447
- ): Promise<ResolvedSource> {
448
- const explicitInitialCount =
449
- Number(Boolean(props.initialDocx)) +
450
- Number(Boolean(props.initialSessionState)) +
451
- Number(Boolean(props.initialSnapshot));
452
- if (explicitInitialCount > 1) {
453
- throw new Error(
454
- "Provide exactly one of initialDocx, initialSessionState, or initialSnapshot.",
455
- );
456
- }
457
-
458
- if (props.externalDocSource) {
459
- if (props.externalDocSource.kind === "docx") {
460
- return {
461
- source: "docx",
462
- initialDocx: props.externalDocSource.bytes,
463
- sourceLabel: props.externalDocSource.sourceLabel,
464
- };
465
- }
466
-
467
- if (props.externalDocSource.kind === "session") {
468
- return {
469
- source: "session",
470
- initialSessionState: props.externalDocSource.sessionState,
471
- sourceLabel:
472
- props.externalDocSource.sourceLabel ??
473
- props.externalDocSource.sessionState.sourcePackage?.sourceLabel,
474
- };
475
- }
476
-
477
- return {
478
- source: "snapshot",
479
- initialSnapshot: props.externalDocSource.snapshot,
480
- sourceLabel: props.externalDocSource.sourceLabel,
481
- };
482
- }
483
-
484
- if (props.initialSessionState) {
485
- return {
486
- source: "session",
487
- initialSessionState: props.initialSessionState,
488
- sourceLabel:
489
- props.initialSourceLabel ??
490
- props.initialSessionState.sourcePackage?.sourceLabel,
491
- };
492
- }
493
-
494
- if (props.initialSnapshot) {
495
- return {
496
- source: "snapshot",
497
- initialSnapshot: props.initialSnapshot,
498
- sourceLabel: props.initialSourceLabel,
499
- };
500
- }
501
-
502
- if (props.initialDocx) {
503
- return {
504
- source: "docx",
505
- initialDocx: props.initialDocx,
506
- sourceLabel: props.initialSourceLabel,
507
- };
508
- }
509
-
510
- const loader = props.hostAdapter?.load ?? props.datastore?.load;
511
- if (!loader) {
512
- throw new Error(
513
- `WordReviewEditor ${props.documentId} needs initialDocx, initialSessionState, initialSnapshot, or a host/datastore load source.`,
514
- );
515
- }
516
-
517
- const loadResult = await loader({
518
- documentId: props.documentId,
519
- loadRevision: props.loadRevision,
520
- loadSourcePolicy: props.loadSourcePolicy,
521
- });
522
-
523
- if (!loadResult.source) {
524
- throw new Error(
525
- `Host or datastore loader did not return a loadable source for ${props.documentId}.`,
526
- );
527
- }
528
-
529
- if (loadResult.source.kind === "docx") {
530
- return {
531
- source: "docx",
532
- initialDocx: loadResult.source.bytes,
533
- sourceLabel: loadResult.source.sourceLabel,
534
- };
535
- }
536
-
537
- if (loadResult.source.kind === "session") {
538
- return {
539
- source: "session",
540
- initialSessionState: loadResult.source.sessionState,
541
- sourceLabel:
542
- loadResult.source.sourceLabel ??
543
- loadResult.source.sessionState.sourcePackage?.sourceLabel,
544
- };
545
- }
546
-
547
- return {
548
- source: "snapshot",
549
- initialSnapshot: loadResult.source.snapshot,
550
- sourceLabel: loadResult.source.sourceLabel,
551
- };
552
- }
553
-
554
- export function __createFallbackRuntime(args: CreateRuntimeArgs): WordReviewEditorRuntime {
555
- return createRuntime(args);
556
- }
557
-
558
- function createRuntime(
559
- args: CreateRuntimeArgs,
560
- handlers: RuntimeLifecycleHandlers = {},
561
- ): WordReviewEditorRuntime {
562
- const docxSession = args.source.initialDocx
563
- ? loadDocxEditorSession({
564
- documentId: args.documentId,
565
- sourceLabel: args.source.sourceLabel,
566
- bytes: args.source.initialDocx,
567
- editorBuild: "dev",
568
- })
569
- : undefined;
570
- const snapshotExportResolution = !args.source.initialDocx
571
- ? resolvePackageBackedExportSession(args)
572
- : undefined;
573
- const initialSessionState =
574
- args.source.initialSessionState ??
575
- docxSession?.initialSessionState ??
576
- (args.source.initialSnapshot
577
- ? editorSessionStateFromPersistedSnapshot(args.source.initialSnapshot)
578
- : editorSessionStateFromPersistedSnapshot(
579
- createFallbackPersistedSnapshot(
580
- args.documentId,
581
- args.source.sourceLabel ?? "Generated shell snapshot",
582
- ),
583
- ));
584
- const runtimeSessionState = snapshotExportResolution?.barrier
585
- ? applySessionExportBarrier(initialSessionState, snapshotExportResolution.barrier)
586
- : initialSessionState;
587
-
588
- return createDocumentRuntime({
589
- documentId: args.documentId,
590
- initialSessionState: runtimeSessionState,
591
- sourceKind: args.source.source,
592
- sourceLabel: args.source.sourceLabel,
593
- initialViewState: args.initialViewState,
594
- readOnly: args.readOnly || docxSession?.readOnly,
595
- editorBuild: runtimeSessionState.editorBuild,
596
- fatalError: docxSession?.fatalError,
597
- exportDocx: async (sessionState, options) => {
598
- if (docxSession) {
599
- return docxSession.exportDocx(sessionState, options);
600
- }
601
-
602
- if (snapshotExportResolution?.session) {
603
- return snapshotExportResolution.session.exportDocx(sessionState, options);
604
- }
605
-
606
- throw createSnapshotExportBlockedError(
607
- args.documentId,
608
- snapshotExportResolution?.barrier ?? {
609
- reason: "missing_source_package_provenance",
610
- message:
611
- "DOCX export is blocked because this session does not carry embedded source package provenance.",
612
- },
613
- );
450
+ setWorkflowOverlay: (overlay) => {
451
+ runtime.setWorkflowOverlay(clonePublicValue(overlay));
614
452
  },
615
- onWarning: handlers.onWarning,
616
- onError: handlers.onError,
617
- defaultAuthorId: args.currentUserId,
618
- });
453
+ clearWorkflowOverlay: () => {
454
+ runtime.clearWorkflowOverlay();
455
+ },
456
+ getWorkflowScopeSnapshot: () => {
457
+ return clonePublicValue(runtime.getWorkflowScopeSnapshot());
458
+ },
459
+ getInteractionGuardSnapshot: () => {
460
+ return clonePublicValue(runtime.getInteractionGuardSnapshot());
461
+ },
462
+ getWorkflowMarkupSnapshot: () => {
463
+ return clonePublicValue(runtime.getWorkflowMarkupSnapshot());
464
+ },
465
+ getWorkflowCandidateRanges: (options) => {
466
+ return clonePublicValue(runtime.getWorkflowCandidateRanges(options));
467
+ },
468
+ replaceWorkflowMarkupText: (markupId, text) => {
469
+ runtime.replaceWorkflowMarkupText(markupId, text);
470
+ },
471
+ };
619
472
  }
620
473
 
621
474
  export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditorProps>(
@@ -633,7 +486,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
633
486
  initialSessionState,
634
487
  initialSnapshot,
635
488
  initialSourceLabel,
636
- markupDisplay = "simple",
489
+ markupDisplay,
637
490
  onError,
638
491
  onEvent,
639
492
  onWarning,
@@ -642,261 +495,161 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
642
495
  showReviewPanel = true,
643
496
  } = props;
644
497
 
645
- const [runtime, setRuntime] = useState<WordReviewEditorRuntime | null>(null);
646
- const [loadError, setLoadError] = useState<EditorError | null>(null);
647
498
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
648
499
  const [showTrackedChanges, setShowTrackedChanges] = useState(false);
649
500
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
650
501
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
651
502
  const [selectionToolbarDismissedKey, setSelectionToolbarDismissedKey] = useState<string | null>(null);
652
503
  const [selectionToolbarFocusWithin, setSelectionToolbarFocusWithin] = useState(false);
653
- const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
654
504
  const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
655
505
  const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
656
506
  const shellRef = useRef<HTMLDivElement | null>(null);
657
507
  const lastSelectionToolbarKeyRef = useRef<string | null>(null);
658
508
  const lastAnnouncedErrorIdRef = useRef<string | null>(null);
659
- const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
660
- const lastSavedRevisionTokenRef = useRef<string | null>(null);
661
- const hostAdapterRef = useRef(hostAdapter);
662
- const datastoreRef = useRef(datastore);
663
- const onEventRef = useRef(onEvent);
664
- const onWarningRef = useRef(onWarning);
665
- const onErrorRef = useRef(onError);
666
- const runtimeViewStateSeedRef = useRef<{
667
- workspaceMode: WorkspaceMode;
668
- zoomLevel: ZoomLevel;
669
- }>({
670
- workspaceMode: "canvas",
671
- zoomLevel: 100,
672
- });
673
- const initialSourceRef = useRef<{
674
- documentId: string;
675
- initialDocx?: Uint8Array | ArrayBuffer;
676
- initialSessionState?: EditorSessionState;
677
- initialSnapshot?: PersistedEditorSnapshot;
678
- initialSourceLabel?: string;
679
- } | null>(null);
680
-
681
- if (!initialSourceRef.current || initialSourceRef.current.documentId !== documentId) {
682
- initialSourceRef.current = {
683
- documentId,
684
- initialDocx,
685
- initialSessionState,
686
- initialSnapshot,
687
- initialSourceLabel,
688
- };
689
- }
690
-
691
- const stableInitialSource = initialSourceRef.current;
692
- const sourceReloadKey = externalDocSource
693
- ? `external:${externalDocSource.kind}:${externalDocumentRevision ?? "static"}`
694
- : stableInitialSource?.initialSessionState
695
- ? "initial-session"
696
- : stableInitialSource?.initialSnapshot
697
- ? "initial-snapshot"
698
- : stableInitialSource?.initialDocx
699
- ? "initial-docx"
700
- : hostAdapter
701
- ? `host-adapter:${loadRevision ?? "static"}`
702
- : `datastore:${loadRevision ?? "static"}`;
703
-
704
- useEffect(() => {
705
- hostAdapterRef.current = hostAdapter;
706
- datastoreRef.current = datastore;
707
- onEventRef.current = onEvent;
708
- onWarningRef.current = onWarning;
709
- onErrorRef.current = onError;
710
- }, [datastore, hostAdapter, onError, onEvent, onWarning]);
711
-
712
- useEffect(() => {
713
- let cancelled = false;
714
-
715
- async function loadRuntime(): Promise<void> {
716
- setLoadError(null);
717
-
718
- try {
719
- const source = await __resolveWordReviewEditorSource({
720
- documentId,
721
- hostAdapter: hostAdapterRef.current,
722
- datastore: datastoreRef.current,
723
- externalDocSource,
724
- initialDocx: stableInitialSource?.initialDocx,
725
- initialSessionState: stableInitialSource?.initialSessionState,
726
- initialSnapshot: stableInitialSource?.initialSnapshot,
727
- initialSourceLabel: stableInitialSource?.initialSourceLabel,
728
- loadRevision,
729
- loadSourcePolicy,
730
- });
731
-
732
- if (cancelled) {
733
- return;
734
- }
735
-
736
- runtimeRef.current?.dispose?.();
737
- const nextRuntime = createRuntime(
738
- {
739
- documentId,
740
- readOnly,
741
- source,
742
- initialViewState: runtimeViewStateSeedRef.current,
743
- hostAdapter: hostAdapterRef.current,
744
- datastore: datastoreRef.current,
745
- currentUserId: currentUser.userId,
746
- },
747
- {
748
- onWarning: onWarningRef.current,
749
- onError: onErrorRef.current,
750
- },
751
- );
752
- emitEditorEvent({
753
- hostAdapter: hostAdapterRef.current,
754
- datastore: datastoreRef.current,
755
- onEvent: onEventRef.current,
756
- event: createReadyEvent(nextRuntime, source.source),
757
- });
758
- runtimeRef.current = nextRuntime;
759
- setRuntime(nextRuntime);
760
- } catch (error) {
761
- if (cancelled) {
762
- return;
763
- }
764
-
765
- const normalized = normalizeEditorError(error);
766
- setLoadError(normalized);
767
- onErrorRef.current?.(normalized);
768
- emitEditorEvent({
769
- hostAdapter: hostAdapterRef.current,
770
- datastore: datastoreRef.current,
771
- onEvent: onEventRef.current,
772
- event: {
773
- type: "error",
774
- documentId,
775
- error: normalized,
776
- },
777
- });
778
- }
779
- }
780
-
781
- void loadRuntime();
782
-
783
- return () => {
784
- cancelled = true;
785
- };
786
- }, [
787
- documentId,
788
- readOnly,
789
- sourceReloadKey,
790
- ]);
791
-
792
- useEffect(() => {
793
- if (!runtime?.subscribeToEvents) {
794
- return;
795
- }
796
-
797
- return runtime.subscribeToEvents((event) => {
798
- if (event.type === "export_completed" || event.type === "ready") {
799
- return;
800
- }
801
- emitEditorEvent({
802
- hostAdapter: hostAdapterRef.current,
803
- datastore: datastoreRef.current,
804
- onEvent: onEventRef.current,
805
- event,
806
- });
807
- });
808
- }, [runtime]);
809
-
810
- useEffect(() => {
811
- return () => {
812
- if (autosaveTimerRef.current) {
813
- clearTimeout(autosaveTimerRef.current);
814
- autosaveTimerRef.current = null;
815
- }
816
- runtimeRef.current?.dispose?.();
817
- runtimeRef.current = null;
818
- };
819
- }, []);
820
-
821
- const optimisticRuntime = useMemo(
822
- () =>
823
- __createFallbackRuntime({
824
- documentId,
825
- readOnly,
826
- currentUserId: currentUser.userId,
827
- initialViewState: runtimeViewStateSeedRef.current,
828
- source: {
829
- source:
830
- initialSessionState
831
- ? "session"
832
- : "snapshot",
833
- initialSessionState:
834
- initialSessionState ??
835
- (initialSnapshot
836
- ? editorSessionStateFromPersistedSnapshot(initialSnapshot)
837
- : undefined),
838
- initialSnapshot:
839
- initialSnapshot ?? createFallbackPersistedSnapshot(documentId, initialSourceLabel),
840
- sourceLabel: guessSourceLabel(
841
- initialSourceLabel,
842
- initialSessionState,
843
- initialSnapshot,
844
- externalDocSource,
845
- ),
846
- },
847
- hostAdapter: hostAdapterRef.current,
848
- datastore: datastoreRef.current,
849
- }),
850
- [
851
- currentUser.userId,
852
- documentId,
853
- initialSessionState,
854
- initialSnapshot,
855
- initialSourceLabel,
856
- readOnly,
857
- hostAdapter,
858
- externalDocSource?.kind,
859
- externalDocSource?.sourceLabel,
860
- ],
509
+ const {
510
+ runtime,
511
+ loadError,
512
+ activeRuntime,
513
+ fallbackSnapshot,
514
+ loadingSessionState,
515
+ loadingViewState,
516
+ loadingNavigation,
517
+ hostAdapterRef,
518
+ datastoreRef,
519
+ onEventRef,
520
+ onWarningRef,
521
+ onErrorRef,
522
+ autosaveTimerRef,
523
+ lastSavedRevisionTokenRef,
524
+ runtimeViewStateSeedRef,
525
+ } = useEditorRuntimeBoundary(props);
526
+ const metaSlice = useRuntimeSnapshotSlice(
527
+ runtime,
528
+ fallbackSnapshot,
529
+ selectMetaSlice,
530
+ shallowEqualRecord,
861
531
  );
862
-
863
- const fallbackSnapshot = useMemo(
864
- () =>
865
- loadError
866
- ? createErrorSnapshot(documentId, loadError)
867
- : createLoadingSnapshot(
868
- documentId,
869
- readOnly,
870
- guessSourceLabel(
871
- initialSourceLabel,
872
- initialSessionState,
873
- initialSnapshot,
874
- externalDocSource,
875
- ),
876
- ),
532
+ const toolbarSlice = useRuntimeSnapshotSlice(
533
+ runtime,
534
+ fallbackSnapshot,
535
+ selectToolbarSlice,
536
+ shallowEqualRecord,
537
+ );
538
+ const surfaceSlice = useRuntimeSnapshotSlice(
539
+ runtime,
540
+ fallbackSnapshot,
541
+ selectSurfaceSlice,
542
+ shallowEqualRecord,
543
+ );
544
+ const reviewSlice = useRuntimeSnapshotSlice(
545
+ runtime,
546
+ fallbackSnapshot,
547
+ selectReviewSlice,
548
+ shallowEqualRecord,
549
+ );
550
+ const viewSlice = useRuntimeSnapshotSlice(
551
+ runtime,
552
+ fallbackSnapshot,
553
+ selectViewSlice,
554
+ shallowEqualRecord,
555
+ );
556
+ const statusSlice = useRuntimeSnapshotSlice(
557
+ runtime,
558
+ fallbackSnapshot,
559
+ selectStatusSlice,
560
+ shallowEqualRecord,
561
+ );
562
+ const snapshot = useMemo(
563
+ () => ({
564
+ documentId: metaSlice.documentId,
565
+ sessionId: metaSlice.sessionId,
566
+ sourceLabel: metaSlice.sourceLabel,
567
+ revisionToken: surfaceSlice.revisionToken,
568
+ isReady: toolbarSlice.isReady,
569
+ isDirty: statusSlice.isDirty,
570
+ readOnly: toolbarSlice.readOnly,
571
+ documentMode: viewSlice.documentMode,
572
+ selection: surfaceSlice.selection,
573
+ activeStory: viewSlice.activeStory,
574
+ pageLayout: viewSlice.pageLayout,
575
+ documentStats: statusSlice.documentStats,
576
+ comments: reviewSlice.comments,
577
+ trackedChanges: reviewSlice.trackedChanges,
578
+ compatibility: reviewSlice.compatibility,
579
+ warnings: statusSlice.warnings,
580
+ fatalError: statusSlice.fatalError,
581
+ commandState: toolbarSlice.commandState,
582
+ surface: surfaceSlice.surface,
583
+ protectionSnapshot: statusSlice.protectionSnapshot,
584
+ }),
877
585
  [
878
- documentId,
879
- externalDocSource,
880
- initialSessionState,
881
- initialSnapshot,
882
- initialSourceLabel,
883
- loadError,
884
- readOnly,
586
+ metaSlice.documentId,
587
+ metaSlice.sessionId,
588
+ metaSlice.sourceLabel,
589
+ surfaceSlice.revisionToken,
590
+ surfaceSlice.selection,
591
+ surfaceSlice.surface,
592
+ toolbarSlice.isReady,
593
+ toolbarSlice.readOnly,
594
+ toolbarSlice.commandState,
595
+ statusSlice.isDirty,
596
+ statusSlice.documentStats,
597
+ statusSlice.warnings,
598
+ statusSlice.fatalError,
599
+ statusSlice.protectionSnapshot,
600
+ reviewSlice.comments,
601
+ reviewSlice.trackedChanges,
602
+ reviewSlice.compatibility,
603
+ viewSlice.documentMode,
604
+ viewSlice.activeStory,
605
+ viewSlice.pageLayout,
885
606
  ],
886
607
  );
887
-
888
- const snapshot = useSyncExternalStore(
889
- (listener) => runtime?.subscribe(listener) ?? (() => undefined),
890
- () => runtime?.getRenderSnapshot() ?? fallbackSnapshot,
891
- () => runtime?.getRenderSnapshot() ?? fallbackSnapshot,
608
+ const viewState = useRuntimeValue(
609
+ runtime
610
+ ? {
611
+ subscribe: (listener) => runtime.subscribe(listener),
612
+ getValue: () => runtime.getViewState(),
613
+ }
614
+ : null,
615
+ loadingViewState,
892
616
  );
893
-
894
- const activeRuntime = runtime ?? optimisticRuntime;
895
- const viewState = activeRuntime.getViewState();
896
617
  const isPageWorkspace = viewState.workspaceMode === "page";
897
- const liveMarkupDisplay: MarkupDisplay = isPageWorkspace ? "all" : "clean";
898
- const canonicalDocument = activeRuntime.getSessionState().canonicalDocument;
899
- const documentNavigation = activeRuntime.getDocumentNavigationSnapshot();
618
+ const liveMarkupDisplay = __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
619
+ const documentNavigation = useRuntimeValue(
620
+ runtime
621
+ ? {
622
+ subscribe: (listener) => runtime.subscribe(listener),
623
+ getValue: () => runtime.getDocumentNavigationSnapshot(),
624
+ }
625
+ : null,
626
+ loadingNavigation,
627
+ );
628
+ const workflowScopeSnapshot = useRuntimeValue(
629
+ runtime
630
+ ? {
631
+ subscribe: (listener) => runtime.subscribe(listener),
632
+ getValue: () => runtime.getWorkflowScopeSnapshot(),
633
+ }
634
+ : null,
635
+ null,
636
+ workflowScopeSnapshotsEqual,
637
+ );
638
+ const interactionGuardSnapshot = useRuntimeValue(
639
+ runtime
640
+ ? {
641
+ subscribe: (listener) => runtime.subscribe(listener),
642
+ getValue: () => runtime.getInteractionGuardSnapshot(),
643
+ }
644
+ : null,
645
+ { blockedReasons: [] } satisfies InteractionGuardSnapshot,
646
+ interactionGuardSnapshotsEqual,
647
+ );
648
+ const sessionState = useMemo(
649
+ () => (runtime ? runtime.getSessionState() : loadingSessionState),
650
+ [loadingSessionState, runtime, snapshot.revisionToken],
651
+ );
652
+ const canonicalDocument = sessionState.canonicalDocument;
900
653
  const effectiveViewMode = deriveEditorViewMode(snapshot.readOnly, reviewMode);
901
654
 
902
655
  useEffect(() => {
@@ -910,6 +663,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
910
663
  };
911
664
  }, [viewState.workspaceMode, viewState.zoomLevel]);
912
665
 
666
+ useEffect(() => {
667
+ recordPerfSample("shell.render");
668
+ incrementInvalidationCounter("shell.rerenders");
669
+ }, [snapshot.revisionToken, snapshot.selection, viewState, documentNavigation]);
670
+
913
671
  useImperativeHandle(
914
672
  ref,
915
673
  () => ({
@@ -939,7 +697,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
939
697
  rejectAllChanges: () => activeRuntime.rejectAllChanges(),
940
698
  exportDocx: (options) =>
941
699
  runtime
942
- ? persistAndExport({
700
+ ? persistAndExportFromBoundary({
943
701
  hostAdapter: hostAdapterRef.current,
944
702
  datastore: datastoreRef.current,
945
703
  documentId,
@@ -950,7 +708,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
950
708
  lastSavedRevisionTokenRef,
951
709
  autosaveTimerRef,
952
710
  })
953
- : rejectExportWhileLoading({
711
+ : rejectExportWhileLoadingFromBoundary({
954
712
  documentId,
955
713
  hostAdapter: hostAdapterRef.current,
956
714
  datastore: datastoreRef.current,
@@ -1149,13 +907,28 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1149
907
  activeRuntime.closeStory();
1150
908
  },
1151
909
  getPageLayoutSnapshot: () => {
1152
- return activeRuntime.getPageLayoutSnapshot();
910
+ return clonePublicValue(activeRuntime.getPageLayoutSnapshot());
1153
911
  },
1154
912
  getDocumentNavigationSnapshot: () => {
1155
- return activeRuntime.getDocumentNavigationSnapshot();
913
+ return clonePublicValue(activeRuntime.getDocumentNavigationSnapshot());
914
+ },
915
+ getFieldSnapshot: () => {
916
+ return clonePublicValue(activeRuntime.getFieldSnapshot());
917
+ },
918
+ updateFields: (options) => {
919
+ return activeRuntime.updateFields(options);
920
+ },
921
+ updateTableOfContents: (options) => {
922
+ return activeRuntime.updateTableOfContents(options);
1156
923
  },
1157
924
  getViewState: () => {
1158
- return activeRuntime.getViewState();
925
+ return clonePublicValue(activeRuntime.getViewState());
926
+ },
927
+ setDocumentMode: (mode) => {
928
+ activeRuntime.setDocumentMode(mode);
929
+ },
930
+ getProtectionSnapshot: () => {
931
+ return clonePublicValue(activeRuntime.getProtectionSnapshot());
1159
932
  },
1160
933
  setWorkspaceMode: (mode) => {
1161
934
  activeRuntime.setWorkspaceMode(mode);
@@ -1184,6 +957,27 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1184
957
  setImageFrame: (mediaId, offsets) => {
1185
958
  applyRuntimeImageReposition(activeRuntime, mediaId, offsets);
1186
959
  },
960
+ setWorkflowOverlay: (overlay) => {
961
+ activeRuntime.setWorkflowOverlay(clonePublicValue(overlay));
962
+ },
963
+ clearWorkflowOverlay: () => {
964
+ activeRuntime.clearWorkflowOverlay();
965
+ },
966
+ getWorkflowScopeSnapshot: () => {
967
+ return clonePublicValue(activeRuntime.getWorkflowScopeSnapshot());
968
+ },
969
+ getInteractionGuardSnapshot: () => {
970
+ return clonePublicValue(activeRuntime.getInteractionGuardSnapshot());
971
+ },
972
+ getWorkflowMarkupSnapshot: () => {
973
+ return clonePublicValue(activeRuntime.getWorkflowMarkupSnapshot());
974
+ },
975
+ getWorkflowCandidateRanges: (options) => {
976
+ return clonePublicValue(activeRuntime.getWorkflowCandidateRanges(options));
977
+ },
978
+ replaceWorkflowMarkupText: (markupId, text) => {
979
+ activeRuntime.replaceWorkflowMarkupText(markupId, text);
980
+ },
1187
981
  }),
1188
982
  [activeRuntime, currentUser.userId, documentId, runtime],
1189
983
  );
@@ -1213,7 +1007,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1213
1007
 
1214
1008
  const debounceMs = props.autosave?.debounceMs ?? 800;
1215
1009
  if (debounceMs <= 0) {
1216
- void persistSession({
1010
+ void persistSessionFromBoundary({
1217
1011
  hostAdapter: hostAdapterRef.current,
1218
1012
  datastore: datastoreRef.current,
1219
1013
  documentId,
@@ -1227,7 +1021,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1227
1021
  }
1228
1022
 
1229
1023
  autosaveTimerRef.current = setTimeout(() => {
1230
- void persistSession({
1024
+ void persistSessionFromBoundary({
1231
1025
  hostAdapter: hostAdapterRef.current,
1232
1026
  datastore: datastoreRef.current,
1233
1027
  documentId,
@@ -1301,7 +1095,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1301
1095
 
1302
1096
  function exportCurrentDocument(): void {
1303
1097
  void (runtime
1304
- ? persistAndExport({
1098
+ ? persistAndExportFromBoundary({
1305
1099
  hostAdapter: hostAdapterRef.current,
1306
1100
  datastore: datastoreRef.current,
1307
1101
  documentId,
@@ -1311,7 +1105,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1311
1105
  lastSavedRevisionTokenRef,
1312
1106
  autosaveTimerRef,
1313
1107
  })
1314
- : rejectExportWhileLoading({
1108
+ : rejectExportWhileLoadingFromBoundary({
1315
1109
  documentId,
1316
1110
  hostAdapter: hostAdapterRef.current,
1317
1111
  datastore: datastoreRef.current,
@@ -1320,20 +1114,89 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1320
1114
  }));
1321
1115
  }
1322
1116
 
1323
- const derivedCapabilities = deriveCapabilities(snapshot, reviewMode);
1117
+ const derivedCapabilities = deriveCapabilities(
1118
+ snapshot,
1119
+ reviewMode,
1120
+ workflowScopeSnapshot,
1121
+ );
1324
1122
  const capabilities = showReviewPanel
1325
1123
  ? derivedCapabilities
1326
1124
  : { ...derivedCapabilities, reviewRailVisible: false };
1327
1125
  const formattingState = getFormattingStateFromRenderSnapshot(snapshot);
1328
1126
  const styleCatalog = useMemo(
1329
- () => getRuntimeStyleCatalog(activeRuntime),
1330
- [activeRuntime, snapshot.revisionToken],
1127
+ () => getRuntimeStyleCatalog(canonicalDocument.styles),
1128
+ [canonicalDocument.styles],
1331
1129
  );
1332
1130
  const diagnosticsModeMessage = getDiagnosticsModeMessage(loadError ?? snapshot.fatalError);
1333
1131
  const addCommentDisabledReason =
1334
1132
  !capabilities.canAddComment && !snapshot.selection.isCollapsed
1335
1133
  ? "Select text within one paragraph to add a DOCX comment."
1336
1134
  : undefined;
1135
+ const activeImageContext = useMemo(
1136
+ () =>
1137
+ buildActiveImageContext({
1138
+ canonicalDocument,
1139
+ selection: snapshot.selection,
1140
+ storyTarget: viewState.activeStory,
1141
+ surface: snapshot.surface,
1142
+ }),
1143
+ [canonicalDocument, snapshot.selection, snapshot.surface, viewState.activeStory],
1144
+ );
1145
+ const sourcePackage = sessionState.sourcePackage;
1146
+ const mediaPreviewCatalogKey = Object.values(canonicalDocument.media.items)
1147
+ .map((item) =>
1148
+ [
1149
+ item.mediaId,
1150
+ item.packagePartName,
1151
+ item.contentType ?? "",
1152
+ item.widthEmu ?? "",
1153
+ item.heightEmu ?? "",
1154
+ ].join(":"),
1155
+ )
1156
+ .sort()
1157
+ .join("|");
1158
+ const mediaPreviews = useMemo(() => {
1159
+ if (!sourcePackage) {
1160
+ return {} as Record<string, MediaPreviewDescriptor>;
1161
+ }
1162
+ try {
1163
+ const bytes = decodePersistedSourcePackageBytes(sourcePackage);
1164
+ if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
1165
+ return {} as Record<string, MediaPreviewDescriptor>;
1166
+ }
1167
+ const opc = readOpcPackage(bytes);
1168
+ const previews: Record<string, MediaPreviewDescriptor> = {};
1169
+ for (const item of Object.values(canonicalDocument.media.items)) {
1170
+ const contentType = item.contentType?.toLowerCase();
1171
+ const part = opc.parts.get(item.packagePartName);
1172
+ if (
1173
+ !part?.bytes ||
1174
+ !contentType ||
1175
+ !BROWSER_SAFE_PREVIEW_TYPES.has(contentType)
1176
+ ) {
1177
+ continue;
1178
+ }
1179
+ previews[item.mediaId] = {
1180
+ src: createImageDataUrl(contentType, part.bytes),
1181
+ ...(item.widthEmu !== undefined ? { widthEmu: item.widthEmu } : {}),
1182
+ ...(item.heightEmu !== undefined ? { heightEmu: item.heightEmu } : {}),
1183
+ };
1184
+ }
1185
+ return previews;
1186
+ } catch {
1187
+ return {} as Record<string, MediaPreviewDescriptor>;
1188
+ }
1189
+ }, [mediaPreviewCatalogKey, sourcePackage?.sha256Hex]);
1190
+ const activeObjectContext = useMemo(
1191
+ () =>
1192
+ viewState.activeObjectFrame && viewState.activeObjectFrame.kind !== "image"
1193
+ ? {
1194
+ kind: viewState.activeObjectFrame.kind,
1195
+ display: viewState.activeObjectFrame.display,
1196
+ }
1197
+ : null,
1198
+ [viewState.activeObjectFrame],
1199
+ );
1337
1200
  const selectionToolbar = buildSelectionToolbarModel({
1338
1201
  snapshot,
1339
1202
  viewState,
@@ -1609,131 +1472,202 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1609
1472
  },
1610
1473
  };
1611
1474
 
1475
+ const commands = useCommandBag<EditorCommandBag>({
1476
+ ...reviewCallbacks,
1477
+ onWorkspaceModeChange: (mode) => activeRuntime.setWorkspaceMode(mode),
1478
+ onZoomChange: (level) => activeRuntime.setZoom(level),
1479
+ onActiveRailTabChange: setActiveRailTab,
1480
+ onShowTrackedChangesChange: setShowTrackedChanges,
1481
+ onToggleBold: () =>
1482
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
1483
+ onToggleItalic: () =>
1484
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "italic" }),
1485
+ onToggleUnderline: () =>
1486
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "underline" }),
1487
+ onSetSelectionTextColor: (color) =>
1488
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-text-color", color }),
1489
+ onSetSelectionHighlightColor: (color) =>
1490
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-highlight-color", color }),
1491
+ onToggleStrikethrough: () =>
1492
+ applyRuntimeFormattingOperation(activeRuntime, {
1493
+ type: "toggle",
1494
+ mark: "strikethrough",
1495
+ }),
1496
+ onToggleSuperscript: () =>
1497
+ applyRuntimeFormattingOperation(activeRuntime, {
1498
+ type: "toggle",
1499
+ mark: "superscript",
1500
+ }),
1501
+ onToggleSubscript: () =>
1502
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "subscript" }),
1503
+ onSetFontFamily: (fontFamily) =>
1504
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-font-family", fontFamily }),
1505
+ onSetFontSize: (fontSize) =>
1506
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-font-size", size: fontSize }),
1507
+ onSetTextColor: (color) =>
1508
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-text-color", color }),
1509
+ onSetHighlightColor: (color) =>
1510
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-highlight-color", color }),
1511
+ onSetAlignment: (alignment) =>
1512
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-alignment", alignment }),
1513
+ onSetParagraphStyle: (styleId) => applyRuntimeParagraphStyle(activeRuntime, styleId),
1514
+ onOutdent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" }),
1515
+ onIndent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "indent" }),
1516
+ onInsertPageBreak: () => applyRuntimeInsertPageBreak(activeRuntime),
1517
+ onInsertTable: () => applyRuntimeInsertTable(activeRuntime, { rows: 3, columns: 3 }),
1518
+ onInsertSectionBreak: (type) => applyRuntimeInsertSectionBreak(activeRuntime, type),
1519
+ onInsertImage: (options) => applyRuntimeInsertImage(activeRuntime, options),
1520
+ onSetTableStyle: (styleId) => applyRuntimeTableStyle(activeRuntime, styleId),
1521
+ onAddRowBefore: () =>
1522
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1523
+ type: "add-row-before",
1524
+ }),
1525
+ onAddRowAfter: () =>
1526
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1527
+ type: "add-row-after",
1528
+ }),
1529
+ onAddColumnBefore: () =>
1530
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1531
+ type: "add-column-before",
1532
+ }),
1533
+ onAddColumnAfter: () =>
1534
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1535
+ type: "add-column-after",
1536
+ }),
1537
+ onDeleteRow: () =>
1538
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1539
+ type: "delete-row",
1540
+ }),
1541
+ onDeleteColumn: () =>
1542
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1543
+ type: "delete-column",
1544
+ }),
1545
+ onDeleteTable: () =>
1546
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1547
+ type: "delete-table",
1548
+ }),
1549
+ onMergeCells: () =>
1550
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1551
+ type: "merge-cells",
1552
+ }),
1553
+ onSplitCell: () =>
1554
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1555
+ type: "split-cell",
1556
+ }),
1557
+ onSetCellBackground: (color) =>
1558
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1559
+ type: "set-cell-background",
1560
+ color,
1561
+ }),
1562
+ onSetImageLayout: (mediaId, dimensions) =>
1563
+ applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
1564
+ onSetImageFrame: (mediaId, offsets) =>
1565
+ applyRuntimeImageReposition(activeRuntime, mediaId, offsets),
1566
+ onOpenHeaderStory: () =>
1567
+ openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
1568
+ onOpenFooterStory: () =>
1569
+ openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer"),
1570
+ onDeleteSectionBreak: (sectionIndex) =>
1571
+ applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
1572
+ onUpdateSectionLayout: (sectionIndex, patch) =>
1573
+ applyRuntimeUpdateSectionLayout(activeRuntime, sectionIndex, patch),
1574
+ onSetSectionPageNumbering: (sectionIndex, patch) =>
1575
+ applyRuntimeSetSectionPageNumbering(activeRuntime, sectionIndex, patch),
1576
+ onSetHeaderFooterLink: (sectionIndex, patch) =>
1577
+ applyRuntimeSetHeaderFooterLink(activeRuntime, sectionIndex, patch),
1578
+ onSetParagraphIndentation: (indentation) =>
1579
+ applyRuntimeParagraphIndentation(activeRuntime, indentation),
1580
+ onSetParagraphTabStops: (tabStops) =>
1581
+ applyRuntimeParagraphTabStops(activeRuntime, tabStops),
1582
+ onRestartNumbering: () =>
1583
+ applyRuntimeNumberingFlow(activeRuntime, { type: "restart" }),
1584
+ onContinueNumbering: () =>
1585
+ applyRuntimeNumberingFlow(activeRuntime, { type: "continue" }),
1586
+ onNavigateHeading: (headingId) => {
1587
+ const heading = documentNavigation.headings.find(
1588
+ (entry) => entry.headingId === headingId,
1589
+ );
1590
+ if (!heading) {
1591
+ return;
1592
+ }
1593
+ applyRuntimeSelection(
1594
+ activeRuntime,
1595
+ createCollapsedPublicSelection(heading.offset),
1596
+ );
1597
+ },
1598
+ });
1599
+
1600
+ const documentElement = (
1601
+ <EditorSurfaceController
1602
+ ref={surfaceRef}
1603
+ currentUser={currentUser}
1604
+ snapshot={snapshot}
1605
+ canonicalDocument={canonicalDocument}
1606
+ documentNavigation={documentNavigation}
1607
+ reviewMode={reviewMode}
1608
+ markupDisplay={liveMarkupDisplay}
1609
+ activeRevisionId={activeRevisionId}
1610
+ showTrackedChanges={showTrackedChanges}
1611
+ mediaPreviews={mediaPreviews}
1612
+ isPageWorkspace={isPageWorkspace}
1613
+ workflowScopes={workflowScopeSnapshot?.scopes}
1614
+ onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
1615
+ {...editorCallbacks}
1616
+ onCommentActivated={(commentId) => {
1617
+ activeRuntime.openComment(commentId);
1618
+ setActiveRailTab("comments");
1619
+ }}
1620
+ onRevisionActivated={(revisionId) => {
1621
+ setActiveRevisionId(revisionId);
1622
+ setActiveRailTab("changes");
1623
+ }}
1624
+ />
1625
+ );
1626
+
1612
1627
  return (
1613
- <div
1614
- ref={shellRef}
1615
- role="region"
1616
- aria-label={`Word review editor for ${snapshot.sourceLabel ?? documentId}`}
1617
- aria-describedby={`${accessibilityInstructionsId} ${accessibilityStatusId}${
1618
- diagnosticsModeMessage ? ` ${accessibilityAlertId}` : ""
1619
- }`}
1620
- className="relative h-full"
1621
- onKeyDownCapture={handleShellKeyDownCapture}
1622
- >
1623
- <p id={accessibilityInstructionsId} style={VISUALLY_HIDDEN_STYLES}>
1624
- Press F6 to move focus between the toolbar, document surface, review rail, and status bar.
1625
- </p>
1626
- <div
1627
- id={accessibilityStatusId}
1628
- role="status"
1629
- aria-live="polite"
1630
- aria-atomic="true"
1631
- style={VISUALLY_HIDDEN_STYLES}
1632
- >
1633
- {buildAccessibilityStatusMessage(snapshot, loadError ?? undefined)}
1634
- </div>
1635
- {diagnosticsModeMessage ? (
1636
- <div
1637
- id={accessibilityAlertId}
1638
- data-wre-alert="true"
1639
- role="alert"
1640
- aria-live="assertive"
1641
- aria-atomic="true"
1642
- tabIndex={-1}
1643
- className="border-b border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger"
1644
- >
1645
- {diagnosticsModeMessage}
1646
- </div>
1647
- ) : null}
1648
- <TwReviewWorkspace
1649
- snapshot={snapshot}
1650
- viewState={viewState}
1651
- currentUserId={currentUser.userId}
1652
- capabilities={capabilities}
1653
- documentNavigation={documentNavigation}
1654
- reviewMode={reviewMode}
1655
- workspaceMode={viewState.workspaceMode}
1656
- zoomLevel={viewState.zoomLevel}
1657
- formattingState={formattingState}
1658
- styleCatalog={styleCatalog}
1659
- activeRailTab={activeRailTab}
1660
- activeCommentId={snapshot.comments.activeCommentId}
1661
- activeRevisionId={activeRevisionId}
1662
- showTrackedChanges={showTrackedChanges}
1663
- selectionToolbar={shouldRenderSelectionToolbar ? selectionToolbar : null}
1664
- selectionToolbarAnchor={shouldRenderSelectionToolbar ? selectionToolbarAnchor : null}
1665
- onAddCommentFromSelection={addSelectionToolbarComment}
1666
- onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
1667
- onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
1668
- onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
1669
- selectionToolbarRef={selectionToolbarElementRef}
1670
- onWorkspaceModeChange={(mode) => activeRuntime.setWorkspaceMode(mode)}
1671
- onZoomChange={(level) => activeRuntime.setZoom(level)}
1672
- onActiveRailTabChange={setActiveRailTab}
1673
- onShowTrackedChangesChange={setShowTrackedChanges}
1674
- onToggleBold={() =>
1675
- applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" })}
1676
- onToggleItalic={() =>
1677
- applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "italic" })}
1678
- onToggleUnderline={() =>
1679
- applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "underline" })}
1680
- onSetParagraphStyle={(styleId) =>
1681
- applyRuntimeParagraphStyle(activeRuntime, styleId)}
1682
- onOutdent={() =>
1683
- applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" })}
1684
- onIndent={() =>
1685
- applyRuntimeFormattingOperation(activeRuntime, { type: "indent" })}
1686
- onOpenHeaderStory={() =>
1687
- openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header")}
1688
- onOpenFooterStory={() =>
1689
- openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer")}
1690
- onSetParagraphIndentation={(indentation) =>
1691
- applyRuntimeParagraphIndentation(activeRuntime, indentation)
1692
- }
1693
- onSetParagraphTabStops={(tabStops) =>
1694
- applyRuntimeParagraphTabStops(activeRuntime, tabStops)
1695
- }
1696
- onRestartNumbering={() => applyRuntimeNumberingFlow(activeRuntime, { type: "restart" })}
1697
- onContinueNumbering={() => applyRuntimeNumberingFlow(activeRuntime, { type: "continue" })}
1698
- onNavigateHeading={(headingId) => {
1699
- const heading = documentNavigation.headings.find(
1700
- (entry) => entry.headingId === headingId,
1701
- );
1702
- if (!heading) {
1703
- return;
1704
- }
1705
- applyRuntimeSelection(
1706
- activeRuntime,
1707
- createCollapsedPublicSelection(heading.offset),
1708
- );
1709
- }}
1710
- {...reviewCallbacks}
1711
- document={
1712
- <TwProseMirrorSurface
1713
- ref={surfaceRef}
1714
- currentUser={currentUser}
1715
- snapshot={snapshot}
1716
- canonicalDocument={canonicalDocument}
1717
- documentNavigation={documentNavigation}
1718
- reviewMode={reviewMode}
1719
- markupDisplay={liveMarkupDisplay}
1720
- activeRevisionId={activeRevisionId}
1721
- showTrackedChanges={showTrackedChanges}
1722
- isPageWorkspace={isPageWorkspace}
1723
- onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
1724
- {...editorCallbacks}
1725
- onCommentActivated={(commentId) => {
1726
- activeRuntime.openComment(commentId);
1727
- setActiveRailTab("comments");
1728
- }}
1729
- onRevisionActivated={(revisionId) => {
1730
- setActiveRevisionId(revisionId);
1731
- setActiveRailTab("changes");
1732
- }}
1733
- />
1734
- }
1735
- />
1736
- </div>
1628
+ <EditorShellView
1629
+ shellRef={shellRef}
1630
+ documentId={documentId}
1631
+ snapshot={snapshot}
1632
+ loadError={loadError}
1633
+ diagnosticsModeMessage={diagnosticsModeMessage}
1634
+ accessibilityInstructionsId={accessibilityInstructionsId}
1635
+ accessibilityStatusId={accessibilityStatusId}
1636
+ accessibilityAlertId={accessibilityAlertId}
1637
+ accessibilityStatusMessage={buildAccessibilityStatusMessage(
1638
+ snapshot,
1639
+ loadError ?? undefined,
1640
+ )}
1641
+ visuallyHiddenStyles={VISUALLY_HIDDEN_STYLES}
1642
+ onShellKeyDownCapture={handleShellKeyDownCapture}
1643
+ viewState={viewState}
1644
+ markupDisplay={liveMarkupDisplay}
1645
+ currentUserId={currentUser.userId}
1646
+ capabilities={capabilities}
1647
+ documentNavigation={documentNavigation}
1648
+ reviewMode={reviewMode}
1649
+ workspaceMode={viewState.workspaceMode}
1650
+ zoomLevel={viewState.zoomLevel}
1651
+ formattingState={formattingState}
1652
+ styleCatalog={styleCatalog}
1653
+ activeRailTab={activeRailTab}
1654
+ activeCommentId={snapshot.comments.activeCommentId}
1655
+ activeRevisionId={activeRevisionId}
1656
+ showTrackedChanges={showTrackedChanges}
1657
+ workflowScopeSnapshot={workflowScopeSnapshot}
1658
+ interactionGuardSnapshot={interactionGuardSnapshot}
1659
+ selectionToolbar={shouldRenderSelectionToolbar ? selectionToolbar : null}
1660
+ selectionToolbarAnchor={shouldRenderSelectionToolbar ? selectionToolbarAnchor : null}
1661
+ onAddCommentFromSelection={addSelectionToolbarComment}
1662
+ onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
1663
+ onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
1664
+ onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
1665
+ selectionToolbarRef={selectionToolbarElementRef}
1666
+ activeImageContext={activeImageContext}
1667
+ activeObjectContext={activeObjectContext}
1668
+ commands={commands}
1669
+ document={documentElement}
1670
+ />
1737
1671
  );
1738
1672
  },
1739
1673
  );
@@ -1772,9 +1706,14 @@ function applyRuntimeFormattingOperation(
1772
1706
  }
1773
1707
 
1774
1708
  function getRuntimeStyleCatalog(
1775
- runtime: WordReviewEditorRuntime,
1709
+ input:
1710
+ | WordReviewEditorRuntime
1711
+ | EditorSessionState["canonicalDocument"]["styles"],
1776
1712
  ): StyleCatalogSnapshot {
1777
- const styles = runtime.getSessionState().canonicalDocument.styles;
1713
+ const styles =
1714
+ "getSessionState" in input
1715
+ ? input.getSessionState().canonicalDocument.styles
1716
+ : input;
1778
1717
  const mapRecord = <
1779
1718
  T extends {
1780
1719
  styleId: string;
@@ -2233,24 +2172,28 @@ function applyRuntimeImageReposition(
2233
2172
  mediaId: string,
2234
2173
  offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
2235
2174
  ): void {
2236
- const snapshot = runtime.getRenderSnapshot();
2237
- if (!canApplyRuntimeMutation(snapshot)) {
2175
+ const context = getStoryMutationContext(runtime);
2176
+ if (!context) {
2238
2177
  return;
2239
2178
  }
2240
2179
 
2241
2180
  try {
2242
- const sessionState = runtime.getSessionState();
2243
2181
  const result = repositionFloatingImageInDocument(
2244
- sessionState.canonicalDocument,
2182
+ context.localDocument,
2245
2183
  mediaId,
2246
2184
  offsets,
2185
+ context.timestamp,
2186
+ );
2187
+ dispatchStoryMutationResult(
2188
+ runtime,
2189
+ context,
2190
+ {
2191
+ changed: true,
2192
+ document: result.document,
2193
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
2194
+ },
2195
+ context.timestamp,
2247
2196
  );
2248
- runtime.dispatch({
2249
- type: "document.replace",
2250
- document: result.document,
2251
- selection: toRuntimeSelectionSnapshot(snapshot.selection),
2252
- origin: { source: "api", timestamp: new Date().toISOString() },
2253
- });
2254
2197
  } catch {
2255
2198
  return;
2256
2199
  }
@@ -2758,6 +2701,13 @@ function normalizeRequestedSelection(
2758
2701
  );
2759
2702
  }
2760
2703
 
2704
+ export function __resolveLiveMarkupDisplay(
2705
+ requested: MarkupDisplay | undefined,
2706
+ isPageWorkspace: boolean,
2707
+ ): MarkupDisplay {
2708
+ return requested ?? (isPageWorkspace ? "all" : "clean");
2709
+ }
2710
+
2761
2711
  function createCollapsedPublicSelection(
2762
2712
  position: number,
2763
2713
  storyTarget?: EditorStoryTarget,
@@ -3008,60 +2958,6 @@ function guessSourceLabel(
3008
2958
  );
3009
2959
  }
3010
2960
 
3011
- function createLoadingSnapshot(
3012
- documentId: string,
3013
- readOnly: boolean,
3014
- sourceLabel?: string,
3015
- ): RuntimeRenderSnapshot {
3016
- return {
3017
- documentId,
3018
- sessionId: `${documentId}-loading`,
3019
- sourceLabel,
3020
- revisionToken: `${documentId}:loading`,
3021
- isReady: false,
3022
- isDirty: false,
3023
- readOnly,
3024
- selection: collapsedSelection(),
3025
- activeStory: { kind: "main" },
3026
- documentStats: {
3027
- storyLength: 0,
3028
- commentCount: 0,
3029
- revisionCount: 0,
3030
- opaqueFragmentCount: 0,
3031
- },
3032
- comments: {
3033
- openCommentIds: [],
3034
- resolvedCommentIds: [],
3035
- detachedCommentIds: [],
3036
- totalCount: 0,
3037
- threads: [],
3038
- },
3039
- trackedChanges: {
3040
- pendingChangeIds: [],
3041
- acceptedChangeIds: [],
3042
- rejectedChangeIds: [],
3043
- detachedChangeIds: [],
3044
- actionableChangeIds: [],
3045
- preserveOnlyChangeIds: [],
3046
- totalCount: 0,
3047
- revisions: [],
3048
- },
3049
- compatibility: {
3050
- blockExport: false,
3051
- blockExportReasons: [],
3052
- warningCount: 0,
3053
- errorCount: 0,
3054
- featureEntries: [],
3055
- },
3056
- warnings: [],
3057
- commandState: {
3058
- canUndo: false,
3059
- canRedo: false,
3060
- readOnly,
3061
- },
3062
- };
3063
- }
3064
-
3065
2961
  function deriveEditorViewMode(
3066
2962
  readOnly: boolean,
3067
2963
  reviewMode: WordReviewEditorProps["reviewMode"] = "review",
@@ -3072,636 +2968,6 @@ function deriveEditorViewMode(
3072
2968
  return reviewMode === "editing" ? "editing" : "review";
3073
2969
  }
3074
2970
 
3075
- function createErrorSnapshot(documentId: string, error: EditorError): RuntimeRenderSnapshot {
3076
- return {
3077
- ...createLoadingSnapshot(documentId, true),
3078
- isReady: true,
3079
- sessionId: `${documentId}-error`,
3080
- revisionToken: `${documentId}:error`,
3081
- compatibility: {
3082
- blockExport: true,
3083
- blockExportReasons: [error.message],
3084
- warningCount: 0,
3085
- errorCount: 1,
3086
- featureEntries: [],
3087
- },
3088
- fatalError: error,
3089
- };
3090
- }
3091
-
3092
- async function persistAndExport(input: {
3093
- hostAdapter?: EditorHostAdapter;
3094
- datastore?: EditorDatastoreAdapter;
3095
- documentId: string;
3096
- runtime: WordReviewEditorRuntime;
3097
- onError?: (error: EditorError) => void;
3098
- onEvent?: (event: WordReviewEditorEvent) => void;
3099
- options?: ExportDocxOptions;
3100
- lastSavedRevisionTokenRef: React.MutableRefObject<string | null>;
3101
- autosaveTimerRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>;
3102
- }): Promise<ExportResult> {
3103
- if (input.autosaveTimerRef.current) {
3104
- clearTimeout(input.autosaveTimerRef.current);
3105
- input.autosaveTimerRef.current = null;
3106
- }
3107
-
3108
- await persistSession({
3109
- hostAdapter: input.hostAdapter,
3110
- datastore: input.datastore,
3111
- documentId: input.documentId,
3112
- runtime: input.runtime,
3113
- isAutosave: false,
3114
- onError: input.onError,
3115
- onEvent: input.onEvent,
3116
- lastSavedRevisionTokenRef: input.lastSavedRevisionTokenRef,
3117
- });
3118
-
3119
- let result: ExportResult;
3120
- try {
3121
- result = await input.runtime.exportDocx(input.options);
3122
- } catch (error) {
3123
- const normalized = normalizeExportError(error, input.documentId, input.options);
3124
- input.onError?.(normalized);
3125
- emitEditorEvent({
3126
- hostAdapter: input.hostAdapter,
3127
- datastore: input.datastore,
3128
- onEvent: input.onEvent,
3129
- event: {
3130
- type: "error",
3131
- documentId: input.documentId,
3132
- error: normalized,
3133
- },
3134
- });
3135
- throw normalized;
3136
- }
3137
-
3138
- const saveExport = input.hostAdapter?.saveExport ?? input.datastore?.saveExport;
3139
- const saveExportSource = input.hostAdapter?.saveExport ? "host" : "datastore";
3140
- if (!saveExport) {
3141
- result = downloadExportResult(result);
3142
- emitEditorEvent({
3143
- hostAdapter: input.hostAdapter,
3144
- datastore: input.datastore,
3145
- onEvent: input.onEvent,
3146
- event: {
3147
- type: "export_completed",
3148
- documentId: input.documentId,
3149
- result,
3150
- },
3151
- });
3152
- return result;
3153
- }
3154
-
3155
- try {
3156
- const saveResult = await saveExport({
3157
- documentId: input.documentId,
3158
- result,
3159
- });
3160
- result = withExportDelivery(result, {
3161
- mode: "persisted-by-host",
3162
- savedAt: saveResult.savedAt,
3163
- });
3164
- } catch (error) {
3165
- const normalized = normalizeStorageError(error, {
3166
- message: "Export persisted bytes could not be stored.",
3167
- source: saveExportSource,
3168
- details: {
3169
- operation: "saveExport",
3170
- },
3171
- });
3172
- input.onError?.(normalized);
3173
- emitEditorEvent({
3174
- hostAdapter: input.hostAdapter,
3175
- datastore: input.datastore,
3176
- onEvent: input.onEvent,
3177
- event: {
3178
- type: "error",
3179
- documentId: input.documentId,
3180
- error: normalized,
3181
- },
3182
- });
3183
- result = withExportDelivery(result, {
3184
- mode: "exported-bytes-only",
3185
- });
3186
- }
3187
-
3188
- emitEditorEvent({
3189
- hostAdapter: input.hostAdapter,
3190
- datastore: input.datastore,
3191
- onEvent: input.onEvent,
3192
- event: {
3193
- type: "export_completed",
3194
- documentId: input.documentId,
3195
- result,
3196
- },
3197
- });
3198
-
3199
- return result;
3200
- }
3201
-
3202
- function rejectExportWhileLoading(input: {
3203
- documentId: string;
3204
- hostAdapter?: EditorHostAdapter;
3205
- datastore?: EditorDatastoreAdapter;
3206
- onError?: (error: EditorError) => void;
3207
- onEvent?: (event: WordReviewEditorEvent) => void;
3208
- }): Promise<never> {
3209
- const error: EditorError = {
3210
- errorId: "word-review-editor-loading-export",
3211
- code: "internal_invariant",
3212
- message: "WordReviewEditor is still loading and cannot export yet.",
3213
- isFatal: false,
3214
- source: "runtime",
3215
- };
3216
- input.onError?.(error);
3217
- emitEditorEvent({
3218
- hostAdapter: input.hostAdapter,
3219
- datastore: input.datastore,
3220
- onEvent: input.onEvent,
3221
- event: {
3222
- type: "error",
3223
- documentId: input.documentId,
3224
- error,
3225
- },
3226
- });
3227
- return Promise.reject(error);
3228
- }
3229
-
3230
- async function persistSession(input: {
3231
- hostAdapter?: EditorHostAdapter;
3232
- datastore?: EditorDatastoreAdapter;
3233
- documentId: string;
3234
- runtime: WordReviewEditorRuntime;
3235
- isAutosave: boolean;
3236
- onError?: (error: EditorError) => void;
3237
- onEvent?: (event: WordReviewEditorEvent) => void;
3238
- lastSavedRevisionTokenRef: React.MutableRefObject<string | null>;
3239
- }): Promise<void> {
3240
- const saveSession = input.hostAdapter?.saveSession;
3241
- const saveSnapshot = input.datastore?.saveSnapshot;
3242
- if (!saveSession && !saveSnapshot) {
3243
- return;
3244
- }
3245
-
3246
- const sessionState = input.runtime.getSessionState();
3247
- const snapshot = input.runtime.getPersistedSnapshot();
3248
- const revisionToken = input.runtime.getRenderSnapshot().revisionToken;
3249
-
3250
- if (input.isAutosave) {
3251
- emitEditorEvent({
3252
- hostAdapter: input.hostAdapter,
3253
- datastore: input.datastore,
3254
- onEvent: input.onEvent,
3255
- event: {
3256
- type: "autosave_state",
3257
- documentId: input.documentId,
3258
- state: {
3259
- status: "saving",
3260
- } satisfies AutosaveState,
3261
- },
3262
- });
3263
- }
3264
-
3265
- try {
3266
- const result = saveSession
3267
- ? await saveSession({
3268
- documentId: input.documentId,
3269
- sessionState,
3270
- isAutosave: input.isAutosave,
3271
- })
3272
- : await saveSnapshot!({
3273
- documentId: input.documentId,
3274
- snapshot,
3275
- isAutosave: input.isAutosave,
3276
- });
3277
- input.lastSavedRevisionTokenRef.current = revisionToken;
3278
- emitEditorEvent({
3279
- hostAdapter: input.hostAdapter,
3280
- datastore: input.datastore,
3281
- onEvent: input.onEvent,
3282
- event: saveSession
3283
- ? {
3284
- type: "session_saved",
3285
- documentId: input.documentId,
3286
- sessionState: input.runtime.getSessionState(),
3287
- savedAt: result.savedAt,
3288
- isAutosave: input.isAutosave,
3289
- }
3290
- : {
3291
- type: "snapshot_saved",
3292
- documentId: input.documentId,
3293
- snapshot: {
3294
- ...snapshot,
3295
- savedAt: result.savedAt,
3296
- },
3297
- isAutosave: input.isAutosave,
3298
- },
3299
- });
3300
- if (input.isAutosave) {
3301
- emitEditorEvent({
3302
- hostAdapter: input.hostAdapter,
3303
- datastore: input.datastore,
3304
- onEvent: input.onEvent,
3305
- event: {
3306
- type: "autosave_state",
3307
- documentId: input.documentId,
3308
- state: {
3309
- status: "saved",
3310
- savedAt: result.savedAt,
3311
- } satisfies AutosaveState,
3312
- },
3313
- });
3314
- }
3315
- } catch (error) {
3316
- const normalized = normalizeStorageError(error, {
3317
- message: input.isAutosave
3318
- ? saveSession
3319
- ? "Autosave failed while storing the editor session."
3320
- : "Autosave failed while storing the editor snapshot."
3321
- : saveSession
3322
- ? "Session save failed while preparing the export checkpoint."
3323
- : "Snapshot save failed while preparing the export checkpoint.",
3324
- source: saveSession ? "host" : "datastore",
3325
- details: {
3326
- operation: saveSession ? "saveSession" : "saveSnapshot",
3327
- isAutosave: input.isAutosave,
3328
- },
3329
- });
3330
- input.onError?.(normalized);
3331
- emitEditorEvent({
3332
- hostAdapter: input.hostAdapter,
3333
- datastore: input.datastore,
3334
- onEvent: input.onEvent,
3335
- event: {
3336
- type: "error",
3337
- documentId: input.documentId,
3338
- error: normalized,
3339
- },
3340
- });
3341
- if (input.isAutosave) {
3342
- emitEditorEvent({
3343
- hostAdapter: input.hostAdapter,
3344
- datastore: input.datastore,
3345
- onEvent: input.onEvent,
3346
- event: {
3347
- type: "autosave_state",
3348
- documentId: input.documentId,
3349
- state: {
3350
- status: "error",
3351
- error: normalized,
3352
- } satisfies AutosaveState,
3353
- },
3354
- });
3355
- }
3356
- if (!input.isAutosave) {
3357
- throw normalized;
3358
- }
3359
- }
3360
- }
3361
-
3362
- function emitEditorEvent(input: {
3363
- hostAdapter?: EditorHostAdapter;
3364
- datastore?: EditorDatastoreAdapter;
3365
- onEvent?: (event: WordReviewEditorEvent) => void;
3366
- event: WordReviewEditorEvent;
3367
- }): void {
3368
- input.onEvent?.(input.event);
3369
- const logEvent = input.hostAdapter?.logEvent ?? input.datastore?.logEvent;
3370
- logEvent?.({
3371
- type: input.event.type,
3372
- documentId: input.event.documentId,
3373
- detail: summarizeEventDetail(input.event),
3374
- });
3375
- }
3376
-
3377
- function summarizeEventDetail(
3378
- event: WordReviewEditorEvent,
3379
- ): Record<string, unknown> | undefined {
3380
- switch (event.type) {
3381
- case "dirty_changed":
3382
- return { isDirty: event.isDirty };
3383
- case "comment_added":
3384
- return { commentId: event.commentId };
3385
- case "comment_resolved":
3386
- return { commentId: event.commentId };
3387
- case "change_accepted":
3388
- case "change_rejected":
3389
- return { changeId: event.changeId };
3390
- case "warning_added":
3391
- return { warningId: event.warning.warningId, code: event.warning.code };
3392
- case "warning_cleared":
3393
- return { warningId: event.warningId, code: event.code };
3394
- case "error":
3395
- return { errorId: event.error.errorId, code: event.error.code };
3396
- case "autosave_state":
3397
- return { status: event.state.status };
3398
- case "snapshot_saved":
3399
- return { isAutosave: event.isAutosave, savedAt: event.snapshot.savedAt };
3400
- case "session_saved":
3401
- return { isAutosave: event.isAutosave, savedAt: event.savedAt };
3402
- case "export_completed":
3403
- return {
3404
- fileName: event.result.fileName,
3405
- deliveryMode: event.result.delivery?.mode,
3406
- savedAt: event.result.delivery?.savedAt,
3407
- };
3408
- case "story_changed":
3409
- return { activeStory: event.activeStory };
3410
- case "selection_changed":
3411
- return {
3412
- anchor: event.selection.anchor,
3413
- head: event.selection.head,
3414
- };
3415
- case "ready":
3416
- return {
3417
- source: event.source,
3418
- blockExport: event.compatibility.blockExport,
3419
- };
3420
- }
3421
- }
3422
-
3423
- function createReadyEvent(
3424
- runtime: Pick<WordReviewEditorRuntime, "getCompatibilityReport" | "getRenderSnapshot">,
3425
- source: "docx" | "session" | "snapshot",
3426
- ): Extract<WordReviewEditorEvent, { type: "ready" }> {
3427
- const snapshot = runtime.getRenderSnapshot();
3428
- return {
3429
- type: "ready",
3430
- documentId: snapshot.documentId,
3431
- sessionId: snapshot.sessionId,
3432
- source,
3433
- stats: snapshot.documentStats,
3434
- compatibility: runtime.getCompatibilityReport(),
3435
- comments: snapshot.comments,
3436
- trackedChanges: snapshot.trackedChanges,
3437
- };
3438
- }
3439
-
3440
- function normalizeStorageError(
3441
- error: unknown,
3442
- fallback: {
3443
- message: string;
3444
- source: "host" | "datastore";
3445
- details?: Record<string, unknown>;
3446
- },
3447
- ): EditorError {
3448
- if (
3449
- typeof error === "object" &&
3450
- error !== null &&
3451
- "errorId" in error &&
3452
- "code" in error &&
3453
- "message" in error
3454
- ) {
3455
- return error as EditorError;
3456
- }
3457
-
3458
- return {
3459
- errorId:
3460
- fallback.source === "host"
3461
- ? "word-review-editor-host"
3462
- : "word-review-editor-datastore",
3463
- code: "datastore_failed",
3464
- message: error instanceof Error ? error.message : fallback.message,
3465
- isFatal: false,
3466
- source: fallback.source === "host" ? "host" : "datastore",
3467
- details: fallback.details,
3468
- };
3469
- }
3470
-
3471
- function createFallbackSnapshot(args: CreateRuntimeArgs): RuntimeRenderSnapshot {
3472
- const initialSessionState =
3473
- args.source.initialSessionState ??
3474
- (args.source.initialSnapshot
3475
- ? editorSessionStateFromPersistedSnapshot(args.source.initialSnapshot)
3476
- : undefined);
3477
- const warnings = initialSessionState?.warningLog ?? [];
3478
- const compatibility = initialSessionState?.compatibility ?? emptyCompatibilityReport();
3479
-
3480
- return {
3481
- ...createLoadingSnapshot(args.documentId, args.readOnly, args.source.sourceLabel),
3482
- sessionId: `${args.documentId}-session`,
3483
- revisionToken: `${args.documentId}:0`,
3484
- isReady: true,
3485
- documentStats: {
3486
- storyLength: estimateStoryLength(initialSessionState ?? args.source.initialSnapshot),
3487
- commentCount: 0,
3488
- revisionCount: 0,
3489
- opaqueFragmentCount: 0,
3490
- },
3491
- compatibility: {
3492
- blockExport: compatibility.blockExport,
3493
- blockExportReasons: [],
3494
- warningCount: compatibility.warnings.length,
3495
- errorCount: compatibility.errors.length,
3496
- featureEntries: compatibility.featureEntries,
3497
- },
3498
- warnings,
3499
- };
3500
- }
3501
-
3502
- function createFallbackPersistedSnapshot(
3503
- documentId: string,
3504
- label = "Generated shell snapshot",
3505
- ): PersistedEditorSnapshot {
3506
- const docId = createCanonicalDocumentId(documentId);
3507
- return {
3508
- snapshotVersion: "persisted-editor-snapshot/2",
3509
- schemaVersion: "cds/1.0.0",
3510
- documentId,
3511
- docId,
3512
- createdAt: "1970-01-01T00:00:00.000Z",
3513
- updatedAt: "1970-01-01T00:00:00.000Z",
3514
- savedAt: "1970-01-01T00:00:00.000Z",
3515
- editorBuild: label,
3516
- canonicalDocument: {
3517
- schemaVersion: "cds/1.0.0",
3518
- docId,
3519
- createdAt: "1970-01-01T00:00:00.000Z",
3520
- updatedAt: "1970-01-01T00:00:00.000Z",
3521
- metadata: {
3522
- customProperties: {},
3523
- },
3524
- styles: {
3525
- paragraphs: {},
3526
- characters: {},
3527
- tables: {},
3528
- },
3529
- numbering: {
3530
- abstractDefinitions: {},
3531
- instances: {},
3532
- },
3533
- media: {
3534
- items: {},
3535
- },
3536
- content: {
3537
- type: "doc",
3538
- children: [{ type: "paragraph", children: [] }],
3539
- },
3540
- review: {
3541
- comments: {},
3542
- revisions: {},
3543
- },
3544
- preservation: {
3545
- opaqueFragments: {},
3546
- packageParts: {},
3547
- },
3548
- diagnostics: {
3549
- warnings: [],
3550
- errors: [],
3551
- },
3552
- },
3553
- compatibility: emptyCompatibilityReport(),
3554
- warningLog: [],
3555
- };
3556
- }
3557
-
3558
- function emptyCompatibilityReport(): CompatibilityReport {
3559
- return {
3560
- reportVersion: "compatibility-report/1",
3561
- generatedAt: "1970-01-01T00:00:00.000Z",
3562
- blockExport: false,
3563
- featureEntries: [],
3564
- warnings: [],
3565
- errors: [],
3566
- };
3567
- }
3568
-
3569
- function resolvePackageBackedExportSession(args: CreateRuntimeArgs): {
3570
- session?: PackageBackedDocxSession;
3571
- barrier?: SnapshotExportBarrier;
3572
- } {
3573
- const sourcePackage =
3574
- args.source.initialSessionState?.sourcePackage ??
3575
- args.source.initialSnapshot?.sourcePackage;
3576
- if (!sourcePackage) {
3577
- return {
3578
- barrier: {
3579
- reason: "missing_source_package_provenance",
3580
- message:
3581
- "DOCX export is blocked because this session was loaded without embedded source package provenance.",
3582
- },
3583
- };
3584
- }
3585
-
3586
- try {
3587
- const bytes = decodePersistedSourcePackageBytes(sourcePackage);
3588
- if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
3589
- return {
3590
- barrier: {
3591
- reason: "invalid_source_package_provenance",
3592
- message:
3593
- "DOCX export is blocked because the embedded source package provenance failed its integrity check.",
3594
- },
3595
- };
3596
- }
3597
-
3598
- const session = loadDocxEditorSession({
3599
- documentId: args.documentId,
3600
- sourceLabel: sourcePackage.sourceLabel ?? args.source.sourceLabel,
3601
- bytes,
3602
- editorBuild:
3603
- args.source.initialSessionState?.editorBuild ??
3604
- args.source.initialSnapshot?.editorBuild ??
3605
- "dev",
3606
- });
3607
- if (session.readOnly || session.fatalError) {
3608
- return {
3609
- barrier: {
3610
- reason: "invalid_source_package_provenance",
3611
- message:
3612
- "DOCX export is blocked because the embedded source package provenance is no longer loadable as a valid package-backed session.",
3613
- },
3614
- };
3615
- }
3616
-
3617
- return { session };
3618
- } catch {
3619
- return {
3620
- barrier: {
3621
- reason: "invalid_source_package_provenance",
3622
- message:
3623
- "DOCX export is blocked because the embedded source package provenance could not be decoded into a package-backed session.",
3624
- },
3625
- };
3626
- }
3627
- }
3628
-
3629
- function applySessionExportBarrier(
3630
- sessionState: EditorSessionState,
3631
- barrier: SnapshotExportBarrier,
3632
- ): EditorSessionState {
3633
- const featureEntryId = `feature:source-package-provenance:${barrier.reason}`;
3634
- const featureEntries = sessionState.compatibility.featureEntries.some(
3635
- (entry) => entry.featureEntryId === featureEntryId,
3636
- )
3637
- ? sessionState.compatibility.featureEntries
3638
- : [
3639
- ...sessionState.compatibility.featureEntries,
3640
- {
3641
- featureEntryId,
3642
- featureKey: "source-package-provenance",
3643
- featureClass: "unsupported-fatal" as const,
3644
- message: barrier.message,
3645
- details: {
3646
- reason: barrier.reason,
3647
- },
3648
- },
3649
- ];
3650
-
3651
- return {
3652
- ...sessionState,
3653
- compatibility: {
3654
- ...sessionState.compatibility,
3655
- blockExport: true,
3656
- featureEntries,
3657
- },
3658
- };
3659
- }
3660
-
3661
- function createSnapshotExportBlockedError(
3662
- documentId: string,
3663
- barrier: SnapshotExportBarrier,
3664
- ): EditorError {
3665
- return {
3666
- errorId: `${documentId}:export:${barrier.reason}`,
3667
- code: "export_failed",
3668
- message: barrier.message,
3669
- isFatal: false,
3670
- source: "export",
3671
- details: {
3672
- reason: barrier.reason,
3673
- },
3674
- };
3675
- }
3676
-
3677
- function normalizeExportError(
3678
- error: unknown,
3679
- documentId: string,
3680
- options?: ExportDocxOptions,
3681
- ): EditorError {
3682
- if (
3683
- typeof error === "object" &&
3684
- error !== null &&
3685
- "errorId" in error &&
3686
- "code" in error &&
3687
- "message" in error
3688
- ) {
3689
- return error as EditorError;
3690
- }
3691
-
3692
- return {
3693
- errorId: `${documentId}:export:failed`,
3694
- code: "export_failed",
3695
- message:
3696
- error instanceof Error ? error.message : "DOCX export failed for an unknown reason.",
3697
- isFatal: false,
3698
- source: "export",
3699
- details: {
3700
- requestedOptions: options ?? {},
3701
- },
3702
- };
3703
- }
3704
-
3705
2971
  function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
3706
2972
  return {
3707
2973
  anchor: selection.anchor,
@@ -3825,6 +3091,102 @@ function selectionToolbarAnchorsEqual(
3825
3091
  );
3826
3092
  }
3827
3093
 
3094
+ function workflowScopeSnapshotsEqual(
3095
+ left: WorkflowScopeSnapshot | null,
3096
+ right: WorkflowScopeSnapshot | null,
3097
+ ): boolean {
3098
+ if (left === right) {
3099
+ return true;
3100
+ }
3101
+ if (!left || !right) {
3102
+ return false;
3103
+ }
3104
+ return (
3105
+ left.overlayPresent === right.overlayPresent &&
3106
+ left.activeWorkItemId === right.activeWorkItemId &&
3107
+ left.activeWorkItem === right.activeWorkItem &&
3108
+ left.scopes === right.scopes &&
3109
+ left.candidates === right.candidates &&
3110
+ workflowBlockedReasonsEqual(left.blockedReasons, right.blockedReasons)
3111
+ );
3112
+ }
3113
+
3114
+ function interactionGuardSnapshotsEqual(
3115
+ left: InteractionGuardSnapshot,
3116
+ right: InteractionGuardSnapshot,
3117
+ ): boolean {
3118
+ if (left === right) {
3119
+ return true;
3120
+ }
3121
+ return workflowBlockedReasonsEqual(left.blockedReasons, right.blockedReasons);
3122
+ }
3123
+
3124
+ function workflowBlockedReasonsEqual(
3125
+ left: readonly WorkflowBlockedCommandReason[],
3126
+ right: readonly WorkflowBlockedCommandReason[],
3127
+ ): boolean {
3128
+ if (left === right) {
3129
+ return true;
3130
+ }
3131
+ if (left.length !== right.length) {
3132
+ return false;
3133
+ }
3134
+ for (let index = 0; index < left.length; index += 1) {
3135
+ if (!workflowBlockedReasonEqual(left[index]!, right[index]!)) {
3136
+ return false;
3137
+ }
3138
+ }
3139
+ return true;
3140
+ }
3141
+
3142
+ function workflowBlockedReasonEqual(
3143
+ left: WorkflowBlockedCommandReason,
3144
+ right: WorkflowBlockedCommandReason,
3145
+ ): boolean {
3146
+ return (
3147
+ left.code === right.code &&
3148
+ left.message === right.message &&
3149
+ left.scopeId === right.scopeId &&
3150
+ left.workItemId === right.workItemId &&
3151
+ editorAnchorProjectionEqual(left.anchor, right.anchor) &&
3152
+ storyTargetsEqual(left.storyTarget, right.storyTarget)
3153
+ );
3154
+ }
3155
+
3156
+ function editorAnchorProjectionEqual(
3157
+ left: EditorAnchorProjection | undefined,
3158
+ right: EditorAnchorProjection | undefined,
3159
+ ): boolean {
3160
+ if (left === right) {
3161
+ return true;
3162
+ }
3163
+ if (!left || !right || left.kind !== right.kind) {
3164
+ return false;
3165
+ }
3166
+
3167
+ switch (left.kind) {
3168
+ case "range":
3169
+ return (
3170
+ right.kind === "range" &&
3171
+ left.from === right.from &&
3172
+ left.to === right.to &&
3173
+ left.assoc.start === right.assoc.start &&
3174
+ left.assoc.end === right.assoc.end
3175
+ );
3176
+ case "node":
3177
+ return right.kind === "node" && left.at === right.at && left.assoc === right.assoc;
3178
+ case "detached":
3179
+ return (
3180
+ right.kind === "detached" &&
3181
+ left.lastKnownRange.from === right.lastKnownRange.from &&
3182
+ left.lastKnownRange.to === right.lastKnownRange.to &&
3183
+ left.reason === right.reason
3184
+ );
3185
+ }
3186
+
3187
+ return false;
3188
+ }
3189
+
3828
3190
  function createSelectionToolbarSelectionKey(
3829
3191
  selection: RuntimeRenderSnapshot["selection"],
3830
3192
  activeStory: EditorStoryTarget,
@@ -3948,3 +3310,176 @@ function createSelectionToolbarListBadge(
3948
3310
  label: viewState.activeListContext.isOrdered ? "Numbered list" : "Bulleted list",
3949
3311
  };
3950
3312
  }
3313
+
3314
+ function buildActiveImageContext(args: {
3315
+ canonicalDocument: PersistedEditorSnapshot["canonicalDocument"];
3316
+ selection: RuntimeRenderSnapshot["selection"];
3317
+ storyTarget: EditorStoryTarget;
3318
+ surface?: RuntimeRenderSnapshot["surface"];
3319
+ }): {
3320
+ mediaId: string;
3321
+ display: "inline" | "floating";
3322
+ widthEmu?: number;
3323
+ heightEmu?: number;
3324
+ horizontalOffsetEmu?: number;
3325
+ verticalOffsetEmu?: number;
3326
+ } | null {
3327
+ const imageSegment = findSelectedImageSegment(args.surface, args.selection);
3328
+ if (!imageSegment) {
3329
+ return null;
3330
+ }
3331
+
3332
+ const storyBlocks = getStoryBlocks(args.canonicalDocument, args.storyTarget);
3333
+ const imageNode = findImageNodeByMediaId(storyBlocks, imageSegment.mediaId);
3334
+ const mediaItem = args.canonicalDocument.media.items[imageSegment.mediaId];
3335
+
3336
+ return {
3337
+ mediaId: imageSegment.mediaId,
3338
+ display: imageSegment.display === "floating" ? "floating" : "inline",
3339
+ widthEmu: mediaItem?.widthEmu,
3340
+ heightEmu: mediaItem?.heightEmu,
3341
+ horizontalOffsetEmu: imageNode?.floating?.horizontalPosition?.offset,
3342
+ verticalOffsetEmu: imageNode?.floating?.verticalPosition?.offset,
3343
+ };
3344
+ }
3345
+
3346
+ function findSelectedImageSegment(
3347
+ surface: RuntimeRenderSnapshot["surface"] | undefined,
3348
+ selection: RuntimeRenderSnapshot["selection"],
3349
+ ): Extract<NonNullable<RuntimeRenderSnapshot["surface"]>["blocks"][number], { kind: "paragraph" }>["segments"][number] & {
3350
+ kind: "image";
3351
+ } | null {
3352
+ if (!surface) {
3353
+ return null;
3354
+ }
3355
+
3356
+ const position =
3357
+ selection.activeRange.kind === "node" ? selection.activeRange.at : selection.head;
3358
+
3359
+ return findSelectedImageSegmentInBlocks(surface.blocks, position);
3360
+ }
3361
+
3362
+ function findSelectedImageSegmentInBlocks(
3363
+ blocks: readonly SurfaceBlockSnapshot[],
3364
+ position: number,
3365
+ ): Extract<NonNullable<RuntimeRenderSnapshot["surface"]>["blocks"][number], { kind: "paragraph" }>["segments"][number] & {
3366
+ kind: "image";
3367
+ } | null {
3368
+ for (const block of blocks) {
3369
+ if (position < block.from || position > block.to) {
3370
+ continue;
3371
+ }
3372
+
3373
+ if (block.kind === "paragraph") {
3374
+ const imageSegment = block.segments.find(
3375
+ (segment) => segment.kind === "image" && position >= segment.from && position <= segment.to,
3376
+ );
3377
+ if (imageSegment && imageSegment.kind === "image") {
3378
+ return imageSegment;
3379
+ }
3380
+ continue;
3381
+ }
3382
+
3383
+ if (block.kind === "table") {
3384
+ for (const row of block.rows) {
3385
+ for (const cell of row.cells) {
3386
+ const match = findSelectedImageSegmentInBlocks(cell.content, position);
3387
+ if (match) {
3388
+ return match;
3389
+ }
3390
+ }
3391
+ }
3392
+ continue;
3393
+ }
3394
+
3395
+ if (block.kind === "sdt_block") {
3396
+ const match = findSelectedImageSegmentInBlocks(block.children, position);
3397
+ if (match) {
3398
+ return match;
3399
+ }
3400
+ }
3401
+ }
3402
+
3403
+ return null;
3404
+ }
3405
+
3406
+ function findImageNodeByMediaId(blocks: readonly unknown[], mediaId: string): {
3407
+ floating?: {
3408
+ horizontalPosition?: { offset?: number };
3409
+ verticalPosition?: { offset?: number };
3410
+ };
3411
+ } | null {
3412
+ for (const block of blocks) {
3413
+ const match = findImageNodeInValue(block, mediaId);
3414
+ if (match) {
3415
+ return match;
3416
+ }
3417
+ }
3418
+ return null;
3419
+ }
3420
+
3421
+ function findImageNodeInValue(
3422
+ value: unknown,
3423
+ mediaId: string,
3424
+ ): {
3425
+ floating?: {
3426
+ horizontalPosition?: { offset?: number };
3427
+ verticalPosition?: { offset?: number };
3428
+ };
3429
+ } | null {
3430
+ if (!value || typeof value !== "object") {
3431
+ return null;
3432
+ }
3433
+
3434
+ const record = value as {
3435
+ type?: string;
3436
+ mediaId?: string;
3437
+ children?: unknown[];
3438
+ rows?: Array<{ cells?: Array<{ children?: unknown[] }> }>;
3439
+ floating?: {
3440
+ horizontalPosition?: { offset?: number };
3441
+ verticalPosition?: { offset?: number };
3442
+ };
3443
+ };
3444
+
3445
+ if (record.type === "image" && record.mediaId === mediaId) {
3446
+ return record;
3447
+ }
3448
+
3449
+ if (Array.isArray(record.children)) {
3450
+ for (const child of record.children) {
3451
+ const match = findImageNodeInValue(child, mediaId);
3452
+ if (match) {
3453
+ return match;
3454
+ }
3455
+ }
3456
+ }
3457
+
3458
+ if (Array.isArray(record.rows)) {
3459
+ for (const row of record.rows) {
3460
+ for (const cell of row.cells ?? []) {
3461
+ for (const child of cell.children ?? []) {
3462
+ const match = findImageNodeInValue(child, mediaId);
3463
+ if (match) {
3464
+ return match;
3465
+ }
3466
+ }
3467
+ }
3468
+ }
3469
+ }
3470
+
3471
+ return null;
3472
+ }
3473
+
3474
+ function createImageDataUrl(contentType: string, bytes: Uint8Array): string {
3475
+ const base64 = bytesToBase64(bytes);
3476
+ return `data:${contentType};base64,${base64}`;
3477
+ }
3478
+
3479
+ function bytesToBase64(bytes: Uint8Array): string {
3480
+ let binary = "";
3481
+ for (let index = 0; index < bytes.length; index += 1) {
3482
+ binary += String.fromCharCode(bytes[index] ?? 0);
3483
+ }
3484
+ return btoa(binary);
3485
+ }