@beyondwork/docx-react-component 1.0.19 → 1.0.21

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 (71) hide show
  1. package/package.json +44 -25
  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 +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -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/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. 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,22 @@ 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
+ WorkflowMarkupSnapshot,
49
+ WorkflowScopeSnapshot,
41
50
  WordReviewEditorEvent,
42
51
  WordReviewEditorProps,
43
52
  WordReviewEditorRef,
44
53
  WorkspaceMode,
45
54
  ZoomLevel,
46
55
  } from "../api/public-types";
47
- import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
56
+ import {
57
+ editorSessionStateFromPersistedSnapshot,
58
+ persistedSnapshotFromEditorSessionState,
59
+ } from "../api/session-state.ts";
48
60
  import {
49
61
  createDetachedAnchor,
50
62
  createNodeAnchor,
@@ -98,81 +110,67 @@ import {
98
110
  insertTable as insertTableInDocument,
99
111
  splitParagraph as splitParagraphInDocument,
100
112
  } 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";
113
+ import { type SelectionSnapshot as InternalSelectionSnapshot } from "../core/state/editor-state.ts";
109
114
  import {
110
115
  getStoryBlocks,
111
116
  replaceStoryBlocks,
112
117
  } from "../runtime/story-targeting.ts";
113
- import { loadDocxEditorSession } from "../io/docx-session.ts";
114
118
  import {
115
119
  decodePersistedSourcePackageBytes,
116
120
  hasValidPersistedSourcePackageDigest,
117
121
  } from "../io/source-package-provenance.ts";
122
+ import { readOpcPackage } from "../io/opc/package-reader.ts";
118
123
  import { deriveCapabilities } from "../runtime/session-capabilities";
119
124
  import { searchDocument } from "../runtime/document-search.ts";
120
125
  import {
121
- TwProseMirrorSurface,
126
+ createEditorViewStateSnapshot,
127
+ createViewState,
128
+ } from "../runtime/view-state.ts";
129
+ import {
122
130
  type TwProseMirrorSurfaceRef,
123
131
  } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
124
- import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
132
+ import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-state-from-snapshot";
133
+ import {
134
+ incrementInvalidationCounter,
135
+ recordPerfSample,
136
+ } from "../ui-tailwind/editor-surface/perf-probe.ts";
125
137
  import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail";
138
+ import {
139
+ selectMetaSlice,
140
+ selectReviewSlice,
141
+ selectStatusSlice,
142
+ selectSurfaceSlice,
143
+ selectToolbarSlice,
144
+ selectViewSlice,
145
+ shallowEqualRecord,
146
+ useRuntimeSnapshotSlice,
147
+ useRuntimeValue,
148
+ } from "./runtime-snapshot-selectors.ts";
126
149
  import type { MarkupDisplay } from "./headless/comment-decoration-model";
127
150
  import type {
128
151
  SelectionToolbarAnchor,
129
152
  SelectionToolbarModel,
130
153
  } from "./headless/selection-toolbar-model";
154
+ import { type EditorCommandBag, useCommandBag } from "./editor-command-bag.ts";
155
+ import { deriveVisibleWorkflowBlockedRails } from "./workflow-surface-blocked-rails.ts";
156
+ import {
157
+ type WordReviewEditorRuntime,
158
+ persistAndExport as persistAndExportFromBoundary,
159
+ persistSession as persistSessionFromBoundary,
160
+ rejectExportWhileLoading as rejectExportWhileLoadingFromBoundary,
161
+ useEditorRuntimeBoundary,
162
+ } from "./editor-runtime-boundary.ts";
131
163
  import {
132
164
  downloadExportResult,
133
165
  withExportDelivery,
134
166
  } from "./browser-export";
167
+ import { EditorShellView } from "./editor-shell-view.tsx";
168
+ import { EditorSurfaceController } from "./editor-surface-controller.tsx";
135
169
 
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
- }
170
+ export {
171
+ __createFallbackRuntime,
172
+ __resolveWordReviewEditorSource,
173
+ } from "./editor-runtime-boundary.ts";
176
174
 
177
175
  const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
178
176
  position: "absolute",
@@ -186,6 +184,15 @@ const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
186
184
  border: 0,
187
185
  };
188
186
 
187
+ const BROWSER_SAFE_PREVIEW_TYPES = new Set([
188
+ "image/png",
189
+ "image/jpeg",
190
+ "image/jpg",
191
+ "image/gif",
192
+ "image/webp",
193
+ "image/bmp",
194
+ ]);
195
+
189
196
  const ACCESSIBLE_REGION_ORDER = [
190
197
  "toolbar",
191
198
  "document",
@@ -392,13 +399,28 @@ export function __createWordReviewEditorRefBridge(
392
399
  runtime.closeStory();
393
400
  },
394
401
  getPageLayoutSnapshot: () => {
395
- return runtime.getPageLayoutSnapshot();
402
+ return clonePublicValue(runtime.getPageLayoutSnapshot());
396
403
  },
397
404
  getDocumentNavigationSnapshot: () => {
398
- return runtime.getDocumentNavigationSnapshot();
405
+ return clonePublicValue(runtime.getDocumentNavigationSnapshot());
406
+ },
407
+ getFieldSnapshot: () => {
408
+ return clonePublicValue(runtime.getFieldSnapshot());
409
+ },
410
+ updateFields: (options) => {
411
+ return runtime.updateFields(options);
412
+ },
413
+ updateTableOfContents: (options) => {
414
+ return runtime.updateTableOfContents(options);
399
415
  },
400
416
  getViewState: () => {
401
- return runtime.getViewState();
417
+ return clonePublicValue(runtime.getViewState());
418
+ },
419
+ setDocumentMode: (mode) => {
420
+ runtime.setDocumentMode(mode);
421
+ },
422
+ getProtectionSnapshot: () => {
423
+ return clonePublicValue(runtime.getProtectionSnapshot());
402
424
  },
403
425
  setWorkspaceMode: (mode) => {
404
426
  runtime.setWorkspaceMode(mode);
@@ -427,195 +449,28 @@ export function __createWordReviewEditorRefBridge(
427
449
  setImageFrame: (mediaId, offsets) => {
428
450
  applyRuntimeImageReposition(runtime, mediaId, offsets);
429
451
  },
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
- );
452
+ setWorkflowOverlay: (overlay) => {
453
+ runtime.setWorkflowOverlay(clonePublicValue(overlay));
614
454
  },
615
- onWarning: handlers.onWarning,
616
- onError: handlers.onError,
617
- defaultAuthorId: args.currentUserId,
618
- });
455
+ clearWorkflowOverlay: () => {
456
+ runtime.clearWorkflowOverlay();
457
+ },
458
+ getWorkflowScopeSnapshot: () => {
459
+ return clonePublicValue(runtime.getWorkflowScopeSnapshot());
460
+ },
461
+ getInteractionGuardSnapshot: () => {
462
+ return clonePublicValue(runtime.getInteractionGuardSnapshot());
463
+ },
464
+ getWorkflowMarkupSnapshot: () => {
465
+ return clonePublicValue(runtime.getWorkflowMarkupSnapshot());
466
+ },
467
+ getWorkflowCandidateRanges: (options) => {
468
+ return clonePublicValue(runtime.getWorkflowCandidateRanges(options));
469
+ },
470
+ replaceWorkflowMarkupText: (markupId, text) => {
471
+ runtime.replaceWorkflowMarkupText(markupId, text);
472
+ },
473
+ };
619
474
  }
620
475
 
621
476
  export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditorProps>(
@@ -633,7 +488,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
633
488
  initialSessionState,
634
489
  initialSnapshot,
635
490
  initialSourceLabel,
636
- markupDisplay = "simple",
491
+ markupDisplay,
637
492
  onError,
638
493
  onEvent,
639
494
  onWarning,
@@ -642,261 +497,169 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
642
497
  showReviewPanel = true,
643
498
  } = props;
644
499
 
645
- const [runtime, setRuntime] = useState<WordReviewEditorRuntime | null>(null);
646
- const [loadError, setLoadError] = useState<EditorError | null>(null);
647
500
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
648
501
  const [showTrackedChanges, setShowTrackedChanges] = useState(false);
649
502
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
650
503
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
651
504
  const [selectionToolbarDismissedKey, setSelectionToolbarDismissedKey] = useState<string | null>(null);
652
505
  const [selectionToolbarFocusWithin, setSelectionToolbarFocusWithin] = useState(false);
653
- const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
654
506
  const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
655
507
  const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
656
508
  const shellRef = useRef<HTMLDivElement | null>(null);
657
509
  const lastSelectionToolbarKeyRef = useRef<string | null>(null);
658
510
  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
- ],
511
+ const {
512
+ runtime,
513
+ loadError,
514
+ activeRuntime,
515
+ fallbackSnapshot,
516
+ loadingSessionState,
517
+ loadingViewState,
518
+ loadingNavigation,
519
+ hostAdapterRef,
520
+ datastoreRef,
521
+ onEventRef,
522
+ onWarningRef,
523
+ onErrorRef,
524
+ autosaveTimerRef,
525
+ lastSavedRevisionTokenRef,
526
+ runtimeViewStateSeedRef,
527
+ } = useEditorRuntimeBoundary(props);
528
+ const metaSlice = useRuntimeSnapshotSlice(
529
+ runtime,
530
+ fallbackSnapshot,
531
+ selectMetaSlice,
532
+ shallowEqualRecord,
861
533
  );
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
- ),
534
+ const toolbarSlice = useRuntimeSnapshotSlice(
535
+ runtime,
536
+ fallbackSnapshot,
537
+ selectToolbarSlice,
538
+ shallowEqualRecord,
539
+ );
540
+ const surfaceSlice = useRuntimeSnapshotSlice(
541
+ runtime,
542
+ fallbackSnapshot,
543
+ selectSurfaceSlice,
544
+ shallowEqualRecord,
545
+ );
546
+ const reviewSlice = useRuntimeSnapshotSlice(
547
+ runtime,
548
+ fallbackSnapshot,
549
+ selectReviewSlice,
550
+ shallowEqualRecord,
551
+ );
552
+ const viewSlice = useRuntimeSnapshotSlice(
553
+ runtime,
554
+ fallbackSnapshot,
555
+ selectViewSlice,
556
+ shallowEqualRecord,
557
+ );
558
+ const statusSlice = useRuntimeSnapshotSlice(
559
+ runtime,
560
+ fallbackSnapshot,
561
+ selectStatusSlice,
562
+ shallowEqualRecord,
563
+ );
564
+ const snapshot = useMemo(
565
+ () => ({
566
+ documentId: metaSlice.documentId,
567
+ sessionId: metaSlice.sessionId,
568
+ sourceLabel: metaSlice.sourceLabel,
569
+ revisionToken: surfaceSlice.revisionToken,
570
+ isReady: toolbarSlice.isReady,
571
+ isDirty: statusSlice.isDirty,
572
+ readOnly: toolbarSlice.readOnly,
573
+ documentMode: viewSlice.documentMode,
574
+ selection: surfaceSlice.selection,
575
+ activeStory: viewSlice.activeStory,
576
+ pageLayout: viewSlice.pageLayout,
577
+ documentStats: statusSlice.documentStats,
578
+ comments: reviewSlice.comments,
579
+ trackedChanges: reviewSlice.trackedChanges,
580
+ compatibility: reviewSlice.compatibility,
581
+ warnings: statusSlice.warnings,
582
+ fatalError: statusSlice.fatalError,
583
+ commandState: toolbarSlice.commandState,
584
+ surface: surfaceSlice.surface,
585
+ protectionSnapshot: statusSlice.protectionSnapshot,
586
+ }),
877
587
  [
878
- documentId,
879
- externalDocSource,
880
- initialSessionState,
881
- initialSnapshot,
882
- initialSourceLabel,
883
- loadError,
884
- readOnly,
588
+ metaSlice.documentId,
589
+ metaSlice.sessionId,
590
+ metaSlice.sourceLabel,
591
+ surfaceSlice.revisionToken,
592
+ surfaceSlice.selection,
593
+ surfaceSlice.surface,
594
+ toolbarSlice.isReady,
595
+ toolbarSlice.readOnly,
596
+ toolbarSlice.commandState,
597
+ statusSlice.isDirty,
598
+ statusSlice.documentStats,
599
+ statusSlice.warnings,
600
+ statusSlice.fatalError,
601
+ statusSlice.protectionSnapshot,
602
+ reviewSlice.comments,
603
+ reviewSlice.trackedChanges,
604
+ reviewSlice.compatibility,
605
+ viewSlice.documentMode,
606
+ viewSlice.activeStory,
607
+ viewSlice.pageLayout,
885
608
  ],
886
609
  );
887
-
888
- const snapshot = useSyncExternalStore(
889
- (listener) => runtime?.subscribe(listener) ?? (() => undefined),
890
- () => runtime?.getRenderSnapshot() ?? fallbackSnapshot,
891
- () => runtime?.getRenderSnapshot() ?? fallbackSnapshot,
610
+ const viewState = useRuntimeValue(
611
+ runtime
612
+ ? {
613
+ subscribe: (listener) => runtime.subscribe(listener),
614
+ getValue: () => runtime.getViewState(),
615
+ }
616
+ : null,
617
+ loadingViewState,
892
618
  );
893
-
894
- const activeRuntime = runtime ?? optimisticRuntime;
895
- const viewState = activeRuntime.getViewState();
896
619
  const isPageWorkspace = viewState.workspaceMode === "page";
897
- const liveMarkupDisplay: MarkupDisplay = isPageWorkspace ? "all" : "clean";
898
- const canonicalDocument = activeRuntime.getSessionState().canonicalDocument;
899
- const documentNavigation = activeRuntime.getDocumentNavigationSnapshot();
620
+ const liveMarkupDisplay = __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
621
+ const documentNavigation = useRuntimeValue(
622
+ runtime
623
+ ? {
624
+ subscribe: (listener) => runtime.subscribe(listener),
625
+ getValue: () => runtime.getDocumentNavigationSnapshot(),
626
+ }
627
+ : null,
628
+ loadingNavigation,
629
+ );
630
+ const workflowScopeSnapshot = useRuntimeValue(
631
+ runtime
632
+ ? {
633
+ subscribe: (listener) => runtime.subscribe(listener),
634
+ getValue: () => runtime.getWorkflowScopeSnapshot(),
635
+ }
636
+ : null,
637
+ null,
638
+ workflowScopeSnapshotsEqual,
639
+ );
640
+ const interactionGuardSnapshot = useRuntimeValue(
641
+ runtime
642
+ ? {
643
+ subscribe: (listener) => runtime.subscribe(listener),
644
+ getValue: () => runtime.getInteractionGuardSnapshot(),
645
+ }
646
+ : null,
647
+ { blockedReasons: [] } satisfies InteractionGuardSnapshot,
648
+ interactionGuardSnapshotsEqual,
649
+ );
650
+ const workflowMarkupSnapshot = useMemo(
651
+ () => (runtime ? runtime.getWorkflowMarkupSnapshot() : null),
652
+ [runtime, snapshot.revisionToken],
653
+ );
654
+ const workflowBlockedRails = useMemo(
655
+ () => deriveVisibleWorkflowBlockedRails(snapshot.surface, workflowMarkupSnapshot),
656
+ [snapshot.surface, workflowMarkupSnapshot],
657
+ );
658
+ const sessionState = useMemo(
659
+ () => (runtime ? runtime.getSessionState() : loadingSessionState),
660
+ [loadingSessionState, runtime, snapshot.revisionToken],
661
+ );
662
+ const canonicalDocument = sessionState.canonicalDocument;
900
663
  const effectiveViewMode = deriveEditorViewMode(snapshot.readOnly, reviewMode);
901
664
 
902
665
  useEffect(() => {
@@ -910,6 +673,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
910
673
  };
911
674
  }, [viewState.workspaceMode, viewState.zoomLevel]);
912
675
 
676
+ useEffect(() => {
677
+ recordPerfSample("shell.render");
678
+ incrementInvalidationCounter("shell.rerenders");
679
+ }, [snapshot.revisionToken, snapshot.selection, viewState, documentNavigation]);
680
+
913
681
  useImperativeHandle(
914
682
  ref,
915
683
  () => ({
@@ -939,7 +707,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
939
707
  rejectAllChanges: () => activeRuntime.rejectAllChanges(),
940
708
  exportDocx: (options) =>
941
709
  runtime
942
- ? persistAndExport({
710
+ ? persistAndExportFromBoundary({
943
711
  hostAdapter: hostAdapterRef.current,
944
712
  datastore: datastoreRef.current,
945
713
  documentId,
@@ -950,7 +718,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
950
718
  lastSavedRevisionTokenRef,
951
719
  autosaveTimerRef,
952
720
  })
953
- : rejectExportWhileLoading({
721
+ : rejectExportWhileLoadingFromBoundary({
954
722
  documentId,
955
723
  hostAdapter: hostAdapterRef.current,
956
724
  datastore: datastoreRef.current,
@@ -1149,13 +917,28 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1149
917
  activeRuntime.closeStory();
1150
918
  },
1151
919
  getPageLayoutSnapshot: () => {
1152
- return activeRuntime.getPageLayoutSnapshot();
920
+ return clonePublicValue(activeRuntime.getPageLayoutSnapshot());
1153
921
  },
1154
922
  getDocumentNavigationSnapshot: () => {
1155
- return activeRuntime.getDocumentNavigationSnapshot();
923
+ return clonePublicValue(activeRuntime.getDocumentNavigationSnapshot());
924
+ },
925
+ getFieldSnapshot: () => {
926
+ return clonePublicValue(activeRuntime.getFieldSnapshot());
927
+ },
928
+ updateFields: (options) => {
929
+ return activeRuntime.updateFields(options);
930
+ },
931
+ updateTableOfContents: (options) => {
932
+ return activeRuntime.updateTableOfContents(options);
1156
933
  },
1157
934
  getViewState: () => {
1158
- return activeRuntime.getViewState();
935
+ return clonePublicValue(activeRuntime.getViewState());
936
+ },
937
+ setDocumentMode: (mode) => {
938
+ activeRuntime.setDocumentMode(mode);
939
+ },
940
+ getProtectionSnapshot: () => {
941
+ return clonePublicValue(activeRuntime.getProtectionSnapshot());
1159
942
  },
1160
943
  setWorkspaceMode: (mode) => {
1161
944
  activeRuntime.setWorkspaceMode(mode);
@@ -1184,6 +967,27 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1184
967
  setImageFrame: (mediaId, offsets) => {
1185
968
  applyRuntimeImageReposition(activeRuntime, mediaId, offsets);
1186
969
  },
970
+ setWorkflowOverlay: (overlay) => {
971
+ activeRuntime.setWorkflowOverlay(clonePublicValue(overlay));
972
+ },
973
+ clearWorkflowOverlay: () => {
974
+ activeRuntime.clearWorkflowOverlay();
975
+ },
976
+ getWorkflowScopeSnapshot: () => {
977
+ return clonePublicValue(activeRuntime.getWorkflowScopeSnapshot());
978
+ },
979
+ getInteractionGuardSnapshot: () => {
980
+ return clonePublicValue(activeRuntime.getInteractionGuardSnapshot());
981
+ },
982
+ getWorkflowMarkupSnapshot: () => {
983
+ return clonePublicValue(activeRuntime.getWorkflowMarkupSnapshot());
984
+ },
985
+ getWorkflowCandidateRanges: (options) => {
986
+ return clonePublicValue(activeRuntime.getWorkflowCandidateRanges(options));
987
+ },
988
+ replaceWorkflowMarkupText: (markupId, text) => {
989
+ activeRuntime.replaceWorkflowMarkupText(markupId, text);
990
+ },
1187
991
  }),
1188
992
  [activeRuntime, currentUser.userId, documentId, runtime],
1189
993
  );
@@ -1213,7 +1017,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1213
1017
 
1214
1018
  const debounceMs = props.autosave?.debounceMs ?? 800;
1215
1019
  if (debounceMs <= 0) {
1216
- void persistSession({
1020
+ void persistSessionFromBoundary({
1217
1021
  hostAdapter: hostAdapterRef.current,
1218
1022
  datastore: datastoreRef.current,
1219
1023
  documentId,
@@ -1227,7 +1031,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1227
1031
  }
1228
1032
 
1229
1033
  autosaveTimerRef.current = setTimeout(() => {
1230
- void persistSession({
1034
+ void persistSessionFromBoundary({
1231
1035
  hostAdapter: hostAdapterRef.current,
1232
1036
  datastore: datastoreRef.current,
1233
1037
  documentId,
@@ -1301,7 +1105,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1301
1105
 
1302
1106
  function exportCurrentDocument(): void {
1303
1107
  void (runtime
1304
- ? persistAndExport({
1108
+ ? persistAndExportFromBoundary({
1305
1109
  hostAdapter: hostAdapterRef.current,
1306
1110
  datastore: datastoreRef.current,
1307
1111
  documentId,
@@ -1311,7 +1115,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1311
1115
  lastSavedRevisionTokenRef,
1312
1116
  autosaveTimerRef,
1313
1117
  })
1314
- : rejectExportWhileLoading({
1118
+ : rejectExportWhileLoadingFromBoundary({
1315
1119
  documentId,
1316
1120
  hostAdapter: hostAdapterRef.current,
1317
1121
  datastore: datastoreRef.current,
@@ -1320,20 +1124,89 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1320
1124
  }));
1321
1125
  }
1322
1126
 
1323
- const derivedCapabilities = deriveCapabilities(snapshot, reviewMode);
1127
+ const derivedCapabilities = deriveCapabilities(
1128
+ snapshot,
1129
+ reviewMode,
1130
+ workflowScopeSnapshot,
1131
+ );
1324
1132
  const capabilities = showReviewPanel
1325
1133
  ? derivedCapabilities
1326
1134
  : { ...derivedCapabilities, reviewRailVisible: false };
1327
1135
  const formattingState = getFormattingStateFromRenderSnapshot(snapshot);
1328
1136
  const styleCatalog = useMemo(
1329
- () => getRuntimeStyleCatalog(activeRuntime),
1330
- [activeRuntime, snapshot.revisionToken],
1137
+ () => getRuntimeStyleCatalog(canonicalDocument.styles),
1138
+ [canonicalDocument.styles],
1331
1139
  );
1332
1140
  const diagnosticsModeMessage = getDiagnosticsModeMessage(loadError ?? snapshot.fatalError);
1333
1141
  const addCommentDisabledReason =
1334
1142
  !capabilities.canAddComment && !snapshot.selection.isCollapsed
1335
1143
  ? "Select text within one paragraph to add a DOCX comment."
1336
1144
  : undefined;
1145
+ const activeImageContext = useMemo(
1146
+ () =>
1147
+ buildActiveImageContext({
1148
+ canonicalDocument,
1149
+ selection: snapshot.selection,
1150
+ storyTarget: viewState.activeStory,
1151
+ surface: snapshot.surface,
1152
+ }),
1153
+ [canonicalDocument, snapshot.selection, snapshot.surface, viewState.activeStory],
1154
+ );
1155
+ const sourcePackage = sessionState.sourcePackage;
1156
+ const mediaPreviewCatalogKey = Object.values(canonicalDocument.media.items)
1157
+ .map((item) =>
1158
+ [
1159
+ item.mediaId,
1160
+ item.packagePartName,
1161
+ item.contentType ?? "",
1162
+ item.widthEmu ?? "",
1163
+ item.heightEmu ?? "",
1164
+ ].join(":"),
1165
+ )
1166
+ .sort()
1167
+ .join("|");
1168
+ const mediaPreviews = useMemo(() => {
1169
+ if (!sourcePackage) {
1170
+ return {} as Record<string, MediaPreviewDescriptor>;
1171
+ }
1172
+ try {
1173
+ const bytes = decodePersistedSourcePackageBytes(sourcePackage);
1174
+ if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
1175
+ return {} as Record<string, MediaPreviewDescriptor>;
1176
+ }
1177
+ const opc = readOpcPackage(bytes);
1178
+ const previews: Record<string, MediaPreviewDescriptor> = {};
1179
+ for (const item of Object.values(canonicalDocument.media.items)) {
1180
+ const contentType = item.contentType?.toLowerCase();
1181
+ const part = opc.parts.get(item.packagePartName);
1182
+ if (
1183
+ !part?.bytes ||
1184
+ !contentType ||
1185
+ !BROWSER_SAFE_PREVIEW_TYPES.has(contentType)
1186
+ ) {
1187
+ continue;
1188
+ }
1189
+ previews[item.mediaId] = {
1190
+ src: createImageDataUrl(contentType, part.bytes),
1191
+ ...(item.widthEmu !== undefined ? { widthEmu: item.widthEmu } : {}),
1192
+ ...(item.heightEmu !== undefined ? { heightEmu: item.heightEmu } : {}),
1193
+ };
1194
+ }
1195
+ return previews;
1196
+ } catch {
1197
+ return {} as Record<string, MediaPreviewDescriptor>;
1198
+ }
1199
+ }, [mediaPreviewCatalogKey, sourcePackage?.sha256Hex]);
1200
+ const activeObjectContext = useMemo(
1201
+ () =>
1202
+ viewState.activeObjectFrame && viewState.activeObjectFrame.kind !== "image"
1203
+ ? {
1204
+ kind: viewState.activeObjectFrame.kind,
1205
+ display: viewState.activeObjectFrame.display,
1206
+ }
1207
+ : null,
1208
+ [viewState.activeObjectFrame],
1209
+ );
1337
1210
  const selectionToolbar = buildSelectionToolbarModel({
1338
1211
  snapshot,
1339
1212
  viewState,
@@ -1609,131 +1482,204 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1609
1482
  },
1610
1483
  };
1611
1484
 
1485
+ const commands = useCommandBag<EditorCommandBag>({
1486
+ ...reviewCallbacks,
1487
+ onWorkspaceModeChange: (mode) => activeRuntime.setWorkspaceMode(mode),
1488
+ onZoomChange: (level) => activeRuntime.setZoom(level),
1489
+ onActiveRailTabChange: setActiveRailTab,
1490
+ onShowTrackedChangesChange: setShowTrackedChanges,
1491
+ onToggleBold: () =>
1492
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
1493
+ onToggleItalic: () =>
1494
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "italic" }),
1495
+ onToggleUnderline: () =>
1496
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "underline" }),
1497
+ onSetSelectionTextColor: (color) =>
1498
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-text-color", color }),
1499
+ onSetSelectionHighlightColor: (color) =>
1500
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-highlight-color", color }),
1501
+ onToggleStrikethrough: () =>
1502
+ applyRuntimeFormattingOperation(activeRuntime, {
1503
+ type: "toggle",
1504
+ mark: "strikethrough",
1505
+ }),
1506
+ onToggleSuperscript: () =>
1507
+ applyRuntimeFormattingOperation(activeRuntime, {
1508
+ type: "toggle",
1509
+ mark: "superscript",
1510
+ }),
1511
+ onToggleSubscript: () =>
1512
+ applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "subscript" }),
1513
+ onSetFontFamily: (fontFamily) =>
1514
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-font-family", fontFamily }),
1515
+ onSetFontSize: (fontSize) =>
1516
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-font-size", size: fontSize }),
1517
+ onSetTextColor: (color) =>
1518
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-text-color", color }),
1519
+ onSetHighlightColor: (color) =>
1520
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-highlight-color", color }),
1521
+ onSetAlignment: (alignment) =>
1522
+ applyRuntimeFormattingOperation(activeRuntime, { type: "set-alignment", alignment }),
1523
+ onSetParagraphStyle: (styleId) => applyRuntimeParagraphStyle(activeRuntime, styleId),
1524
+ onOutdent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" }),
1525
+ onIndent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "indent" }),
1526
+ onInsertPageBreak: () => applyRuntimeInsertPageBreak(activeRuntime),
1527
+ onInsertTable: () => applyRuntimeInsertTable(activeRuntime, { rows: 3, columns: 3 }),
1528
+ onInsertSectionBreak: (type) => applyRuntimeInsertSectionBreak(activeRuntime, type),
1529
+ onInsertImage: (options) => applyRuntimeInsertImage(activeRuntime, options),
1530
+ onSetTableStyle: (styleId) => applyRuntimeTableStyle(activeRuntime, styleId),
1531
+ onAddRowBefore: () =>
1532
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1533
+ type: "add-row-before",
1534
+ }),
1535
+ onAddRowAfter: () =>
1536
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1537
+ type: "add-row-after",
1538
+ }),
1539
+ onAddColumnBefore: () =>
1540
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1541
+ type: "add-column-before",
1542
+ }),
1543
+ onAddColumnAfter: () =>
1544
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1545
+ type: "add-column-after",
1546
+ }),
1547
+ onDeleteRow: () =>
1548
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1549
+ type: "delete-row",
1550
+ }),
1551
+ onDeleteColumn: () =>
1552
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1553
+ type: "delete-column",
1554
+ }),
1555
+ onDeleteTable: () =>
1556
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1557
+ type: "delete-table",
1558
+ }),
1559
+ onMergeCells: () =>
1560
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1561
+ type: "merge-cells",
1562
+ }),
1563
+ onSplitCell: () =>
1564
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1565
+ type: "split-cell",
1566
+ }),
1567
+ onSetCellBackground: (color) =>
1568
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
1569
+ type: "set-cell-background",
1570
+ color,
1571
+ }),
1572
+ onSetImageLayout: (mediaId, dimensions) =>
1573
+ applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
1574
+ onSetImageFrame: (mediaId, offsets) =>
1575
+ applyRuntimeImageReposition(activeRuntime, mediaId, offsets),
1576
+ onOpenHeaderStory: () =>
1577
+ openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
1578
+ onOpenFooterStory: () =>
1579
+ openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer"),
1580
+ onDeleteSectionBreak: (sectionIndex) =>
1581
+ applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
1582
+ onUpdateSectionLayout: (sectionIndex, patch) =>
1583
+ applyRuntimeUpdateSectionLayout(activeRuntime, sectionIndex, patch),
1584
+ onSetSectionPageNumbering: (sectionIndex, patch) =>
1585
+ applyRuntimeSetSectionPageNumbering(activeRuntime, sectionIndex, patch),
1586
+ onSetHeaderFooterLink: (sectionIndex, patch) =>
1587
+ applyRuntimeSetHeaderFooterLink(activeRuntime, sectionIndex, patch),
1588
+ onSetParagraphIndentation: (indentation) =>
1589
+ applyRuntimeParagraphIndentation(activeRuntime, indentation),
1590
+ onSetParagraphTabStops: (tabStops) =>
1591
+ applyRuntimeParagraphTabStops(activeRuntime, tabStops),
1592
+ onRestartNumbering: () =>
1593
+ applyRuntimeNumberingFlow(activeRuntime, { type: "restart" }),
1594
+ onContinueNumbering: () =>
1595
+ applyRuntimeNumberingFlow(activeRuntime, { type: "continue" }),
1596
+ onNavigateHeading: (headingId) => {
1597
+ const heading = documentNavigation.headings.find(
1598
+ (entry) => entry.headingId === headingId,
1599
+ );
1600
+ if (!heading) {
1601
+ return;
1602
+ }
1603
+ applyRuntimeSelection(
1604
+ activeRuntime,
1605
+ createCollapsedPublicSelection(heading.offset),
1606
+ );
1607
+ },
1608
+ });
1609
+
1610
+ const documentElement = (
1611
+ <EditorSurfaceController
1612
+ ref={surfaceRef}
1613
+ currentUser={currentUser}
1614
+ snapshot={snapshot}
1615
+ canonicalDocument={canonicalDocument}
1616
+ documentNavigation={documentNavigation}
1617
+ reviewMode={reviewMode}
1618
+ markupDisplay={liveMarkupDisplay}
1619
+ activeRevisionId={activeRevisionId}
1620
+ showTrackedChanges={showTrackedChanges}
1621
+ mediaPreviews={mediaPreviews}
1622
+ isPageWorkspace={isPageWorkspace}
1623
+ workflowScopes={workflowScopeSnapshot?.scopes}
1624
+ workflowCandidates={workflowScopeSnapshot?.candidates}
1625
+ workflowBlockedReasons={workflowBlockedRails}
1626
+ onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
1627
+ {...editorCallbacks}
1628
+ onCommentActivated={(commentId) => {
1629
+ activeRuntime.openComment(commentId);
1630
+ setActiveRailTab("comments");
1631
+ }}
1632
+ onRevisionActivated={(revisionId) => {
1633
+ setActiveRevisionId(revisionId);
1634
+ setActiveRailTab("changes");
1635
+ }}
1636
+ />
1637
+ );
1638
+
1612
1639
  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>
1640
+ <EditorShellView
1641
+ shellRef={shellRef}
1642
+ documentId={documentId}
1643
+ snapshot={snapshot}
1644
+ loadError={loadError}
1645
+ diagnosticsModeMessage={diagnosticsModeMessage}
1646
+ accessibilityInstructionsId={accessibilityInstructionsId}
1647
+ accessibilityStatusId={accessibilityStatusId}
1648
+ accessibilityAlertId={accessibilityAlertId}
1649
+ accessibilityStatusMessage={buildAccessibilityStatusMessage(
1650
+ snapshot,
1651
+ loadError ?? undefined,
1652
+ )}
1653
+ visuallyHiddenStyles={VISUALLY_HIDDEN_STYLES}
1654
+ onShellKeyDownCapture={handleShellKeyDownCapture}
1655
+ viewState={viewState}
1656
+ markupDisplay={liveMarkupDisplay}
1657
+ currentUserId={currentUser.userId}
1658
+ capabilities={capabilities}
1659
+ documentNavigation={documentNavigation}
1660
+ reviewMode={reviewMode}
1661
+ workspaceMode={viewState.workspaceMode}
1662
+ zoomLevel={viewState.zoomLevel}
1663
+ formattingState={formattingState}
1664
+ styleCatalog={styleCatalog}
1665
+ activeRailTab={activeRailTab}
1666
+ activeCommentId={snapshot.comments.activeCommentId}
1667
+ activeRevisionId={activeRevisionId}
1668
+ showTrackedChanges={showTrackedChanges}
1669
+ workflowScopeSnapshot={workflowScopeSnapshot}
1670
+ interactionGuardSnapshot={interactionGuardSnapshot}
1671
+ selectionToolbar={shouldRenderSelectionToolbar ? selectionToolbar : null}
1672
+ selectionToolbarAnchor={shouldRenderSelectionToolbar ? selectionToolbarAnchor : null}
1673
+ onAddCommentFromSelection={addSelectionToolbarComment}
1674
+ onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
1675
+ onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
1676
+ onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
1677
+ selectionToolbarRef={selectionToolbarElementRef}
1678
+ activeImageContext={activeImageContext}
1679
+ activeObjectContext={activeObjectContext}
1680
+ commands={commands}
1681
+ document={documentElement}
1682
+ />
1737
1683
  );
1738
1684
  },
1739
1685
  );
@@ -1772,9 +1718,14 @@ function applyRuntimeFormattingOperation(
1772
1718
  }
1773
1719
 
1774
1720
  function getRuntimeStyleCatalog(
1775
- runtime: WordReviewEditorRuntime,
1721
+ input:
1722
+ | WordReviewEditorRuntime
1723
+ | EditorSessionState["canonicalDocument"]["styles"],
1776
1724
  ): StyleCatalogSnapshot {
1777
- const styles = runtime.getSessionState().canonicalDocument.styles;
1725
+ const styles =
1726
+ "getSessionState" in input
1727
+ ? input.getSessionState().canonicalDocument.styles
1728
+ : input;
1778
1729
  const mapRecord = <
1779
1730
  T extends {
1780
1731
  styleId: string;
@@ -1965,6 +1916,13 @@ function applyRuntimeInsertSectionBreak(
1965
1916
  if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
1966
1917
  return;
1967
1918
  }
1919
+ if (snapshot.documentMode === "suggesting") {
1920
+ runtime.emitBlockedCommand("insertSectionBreak", [{
1921
+ code: "unsupported_surface",
1922
+ message: "Section break insertion is not supported in suggesting mode.",
1923
+ }]);
1924
+ return;
1925
+ }
1968
1926
 
1969
1927
  const sessionState = runtime.getSessionState();
1970
1928
  const timestamp = new Date().toISOString();
@@ -2136,6 +2094,14 @@ function applyRuntimeSetHeaderFooterLink(
2136
2094
  }
2137
2095
 
2138
2096
  function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
2097
+ const snapshot = runtime.getRenderSnapshot();
2098
+ if (snapshot.documentMode === "suggesting") {
2099
+ runtime.emitBlockedCommand("insertPageBreak", [{
2100
+ code: "unsupported_surface",
2101
+ message: "Page break insertion is not supported in suggesting mode.",
2102
+ }]);
2103
+ return;
2104
+ }
2139
2105
  const context = getStoryMutationContext(runtime);
2140
2106
  if (!context) {
2141
2107
  return;
@@ -2153,6 +2119,14 @@ function applyRuntimeInsertTable(
2153
2119
  runtime: WordReviewEditorRuntime,
2154
2120
  options: InsertTableOptions,
2155
2121
  ): void {
2122
+ const snapshot = runtime.getRenderSnapshot();
2123
+ if (snapshot.documentMode === "suggesting") {
2124
+ runtime.emitBlockedCommand("insertTable", [{
2125
+ code: "unsupported_surface",
2126
+ message: "Table insertion is not supported in suggesting mode.",
2127
+ }]);
2128
+ return;
2129
+ }
2156
2130
  const context = getStoryMutationContext(runtime);
2157
2131
  if (!context) {
2158
2132
  return;
@@ -2171,6 +2145,14 @@ function applyRuntimeInsertImage(
2171
2145
  runtime: WordReviewEditorRuntime,
2172
2146
  options: InsertImageOptions,
2173
2147
  ): void {
2148
+ const snapshot = runtime.getRenderSnapshot();
2149
+ if (snapshot.documentMode === "suggesting") {
2150
+ runtime.emitBlockedCommand("insertImage", [{
2151
+ code: "unsupported_surface",
2152
+ message: "Image insertion is not supported in suggesting mode.",
2153
+ }]);
2154
+ return;
2155
+ }
2174
2156
  const context = getStoryMutationContext(runtime);
2175
2157
  if (!context) {
2176
2158
  return;
@@ -2209,6 +2191,13 @@ function applyRuntimeImageResize(
2209
2191
  if (!canApplyRuntimeMutation(snapshot)) {
2210
2192
  return;
2211
2193
  }
2194
+ if (snapshot.documentMode === "suggesting") {
2195
+ runtime.emitBlockedCommand("setImageLayout", [{
2196
+ code: "unsupported_surface",
2197
+ message: "Image resize is not supported in suggesting mode.",
2198
+ }]);
2199
+ return;
2200
+ }
2212
2201
 
2213
2202
  try {
2214
2203
  const sessionState = runtime.getSessionState();
@@ -2234,23 +2223,35 @@ function applyRuntimeImageReposition(
2234
2223
  offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
2235
2224
  ): void {
2236
2225
  const snapshot = runtime.getRenderSnapshot();
2237
- if (!canApplyRuntimeMutation(snapshot)) {
2226
+ if (snapshot.documentMode === "suggesting") {
2227
+ runtime.emitBlockedCommand("setImageFrame", [{
2228
+ code: "unsupported_surface",
2229
+ message: "Image reposition is not supported in suggesting mode.",
2230
+ }]);
2231
+ return;
2232
+ }
2233
+ const context = getStoryMutationContext(runtime);
2234
+ if (!context) {
2238
2235
  return;
2239
2236
  }
2240
2237
 
2241
2238
  try {
2242
- const sessionState = runtime.getSessionState();
2243
2239
  const result = repositionFloatingImageInDocument(
2244
- sessionState.canonicalDocument,
2240
+ context.localDocument,
2245
2241
  mediaId,
2246
2242
  offsets,
2243
+ context.timestamp,
2244
+ );
2245
+ dispatchStoryMutationResult(
2246
+ runtime,
2247
+ context,
2248
+ {
2249
+ changed: true,
2250
+ document: result.document,
2251
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
2252
+ },
2253
+ context.timestamp,
2247
2254
  );
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
2255
  } catch {
2255
2256
  return;
2256
2257
  }
@@ -2274,6 +2275,14 @@ function applyRuntimeTableStructureOperation(
2274
2275
  | { type: "split-cell" }
2275
2276
  | { type: "set-cell-background"; color: string },
2276
2277
  ): void {
2278
+ const snapshot = runtime.getRenderSnapshot();
2279
+ if (snapshot.documentMode === "suggesting") {
2280
+ runtime.emitBlockedCommand(`table.${operation.type}`, [{
2281
+ code: "unsupported_surface",
2282
+ message: `Table operation "${operation.type}" is not supported in suggesting mode.`,
2283
+ }]);
2284
+ return;
2285
+ }
2277
2286
  const context = getStoryMutationContext(runtime);
2278
2287
  if (!context) {
2279
2288
  return;
@@ -2758,6 +2767,13 @@ function normalizeRequestedSelection(
2758
2767
  );
2759
2768
  }
2760
2769
 
2770
+ export function __resolveLiveMarkupDisplay(
2771
+ requested: MarkupDisplay | undefined,
2772
+ isPageWorkspace: boolean,
2773
+ ): MarkupDisplay {
2774
+ return requested ?? (isPageWorkspace ? "all" : "clean");
2775
+ }
2776
+
2761
2777
  function createCollapsedPublicSelection(
2762
2778
  position: number,
2763
2779
  storyTarget?: EditorStoryTarget,
@@ -3008,60 +3024,6 @@ function guessSourceLabel(
3008
3024
  );
3009
3025
  }
3010
3026
 
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
3027
  function deriveEditorViewMode(
3066
3028
  readOnly: boolean,
3067
3029
  reviewMode: WordReviewEditorProps["reviewMode"] = "review",
@@ -3072,636 +3034,6 @@ function deriveEditorViewMode(
3072
3034
  return reviewMode === "editing" ? "editing" : "review";
3073
3035
  }
3074
3036
 
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
3037
  function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
3706
3038
  return {
3707
3039
  anchor: selection.anchor,
@@ -3825,6 +3157,102 @@ function selectionToolbarAnchorsEqual(
3825
3157
  );
3826
3158
  }
3827
3159
 
3160
+ function workflowScopeSnapshotsEqual(
3161
+ left: WorkflowScopeSnapshot | null,
3162
+ right: WorkflowScopeSnapshot | null,
3163
+ ): boolean {
3164
+ if (left === right) {
3165
+ return true;
3166
+ }
3167
+ if (!left || !right) {
3168
+ return false;
3169
+ }
3170
+ return (
3171
+ left.overlayPresent === right.overlayPresent &&
3172
+ left.activeWorkItemId === right.activeWorkItemId &&
3173
+ left.activeWorkItem === right.activeWorkItem &&
3174
+ left.scopes === right.scopes &&
3175
+ left.candidates === right.candidates &&
3176
+ workflowBlockedReasonsEqual(left.blockedReasons, right.blockedReasons)
3177
+ );
3178
+ }
3179
+
3180
+ function interactionGuardSnapshotsEqual(
3181
+ left: InteractionGuardSnapshot,
3182
+ right: InteractionGuardSnapshot,
3183
+ ): boolean {
3184
+ if (left === right) {
3185
+ return true;
3186
+ }
3187
+ return workflowBlockedReasonsEqual(left.blockedReasons, right.blockedReasons);
3188
+ }
3189
+
3190
+ function workflowBlockedReasonsEqual(
3191
+ left: readonly WorkflowBlockedCommandReason[],
3192
+ right: readonly WorkflowBlockedCommandReason[],
3193
+ ): boolean {
3194
+ if (left === right) {
3195
+ return true;
3196
+ }
3197
+ if (left.length !== right.length) {
3198
+ return false;
3199
+ }
3200
+ for (let index = 0; index < left.length; index += 1) {
3201
+ if (!workflowBlockedReasonEqual(left[index]!, right[index]!)) {
3202
+ return false;
3203
+ }
3204
+ }
3205
+ return true;
3206
+ }
3207
+
3208
+ function workflowBlockedReasonEqual(
3209
+ left: WorkflowBlockedCommandReason,
3210
+ right: WorkflowBlockedCommandReason,
3211
+ ): boolean {
3212
+ return (
3213
+ left.code === right.code &&
3214
+ left.message === right.message &&
3215
+ left.scopeId === right.scopeId &&
3216
+ left.workItemId === right.workItemId &&
3217
+ editorAnchorProjectionEqual(left.anchor, right.anchor) &&
3218
+ storyTargetsEqual(left.storyTarget, right.storyTarget)
3219
+ );
3220
+ }
3221
+
3222
+ function editorAnchorProjectionEqual(
3223
+ left: EditorAnchorProjection | undefined,
3224
+ right: EditorAnchorProjection | undefined,
3225
+ ): boolean {
3226
+ if (left === right) {
3227
+ return true;
3228
+ }
3229
+ if (!left || !right || left.kind !== right.kind) {
3230
+ return false;
3231
+ }
3232
+
3233
+ switch (left.kind) {
3234
+ case "range":
3235
+ return (
3236
+ right.kind === "range" &&
3237
+ left.from === right.from &&
3238
+ left.to === right.to &&
3239
+ left.assoc.start === right.assoc.start &&
3240
+ left.assoc.end === right.assoc.end
3241
+ );
3242
+ case "node":
3243
+ return right.kind === "node" && left.at === right.at && left.assoc === right.assoc;
3244
+ case "detached":
3245
+ return (
3246
+ right.kind === "detached" &&
3247
+ left.lastKnownRange.from === right.lastKnownRange.from &&
3248
+ left.lastKnownRange.to === right.lastKnownRange.to &&
3249
+ left.reason === right.reason
3250
+ );
3251
+ }
3252
+
3253
+ return false;
3254
+ }
3255
+
3828
3256
  function createSelectionToolbarSelectionKey(
3829
3257
  selection: RuntimeRenderSnapshot["selection"],
3830
3258
  activeStory: EditorStoryTarget,
@@ -3948,3 +3376,176 @@ function createSelectionToolbarListBadge(
3948
3376
  label: viewState.activeListContext.isOrdered ? "Numbered list" : "Bulleted list",
3949
3377
  };
3950
3378
  }
3379
+
3380
+ function buildActiveImageContext(args: {
3381
+ canonicalDocument: PersistedEditorSnapshot["canonicalDocument"];
3382
+ selection: RuntimeRenderSnapshot["selection"];
3383
+ storyTarget: EditorStoryTarget;
3384
+ surface?: RuntimeRenderSnapshot["surface"];
3385
+ }): {
3386
+ mediaId: string;
3387
+ display: "inline" | "floating";
3388
+ widthEmu?: number;
3389
+ heightEmu?: number;
3390
+ horizontalOffsetEmu?: number;
3391
+ verticalOffsetEmu?: number;
3392
+ } | null {
3393
+ const imageSegment = findSelectedImageSegment(args.surface, args.selection);
3394
+ if (!imageSegment) {
3395
+ return null;
3396
+ }
3397
+
3398
+ const storyBlocks = getStoryBlocks(args.canonicalDocument, args.storyTarget);
3399
+ const imageNode = findImageNodeByMediaId(storyBlocks, imageSegment.mediaId);
3400
+ const mediaItem = args.canonicalDocument.media.items[imageSegment.mediaId];
3401
+
3402
+ return {
3403
+ mediaId: imageSegment.mediaId,
3404
+ display: imageSegment.display === "floating" ? "floating" : "inline",
3405
+ widthEmu: mediaItem?.widthEmu,
3406
+ heightEmu: mediaItem?.heightEmu,
3407
+ horizontalOffsetEmu: imageNode?.floating?.horizontalPosition?.offset,
3408
+ verticalOffsetEmu: imageNode?.floating?.verticalPosition?.offset,
3409
+ };
3410
+ }
3411
+
3412
+ function findSelectedImageSegment(
3413
+ surface: RuntimeRenderSnapshot["surface"] | undefined,
3414
+ selection: RuntimeRenderSnapshot["selection"],
3415
+ ): Extract<NonNullable<RuntimeRenderSnapshot["surface"]>["blocks"][number], { kind: "paragraph" }>["segments"][number] & {
3416
+ kind: "image";
3417
+ } | null {
3418
+ if (!surface) {
3419
+ return null;
3420
+ }
3421
+
3422
+ const position =
3423
+ selection.activeRange.kind === "node" ? selection.activeRange.at : selection.head;
3424
+
3425
+ return findSelectedImageSegmentInBlocks(surface.blocks, position);
3426
+ }
3427
+
3428
+ function findSelectedImageSegmentInBlocks(
3429
+ blocks: readonly SurfaceBlockSnapshot[],
3430
+ position: number,
3431
+ ): Extract<NonNullable<RuntimeRenderSnapshot["surface"]>["blocks"][number], { kind: "paragraph" }>["segments"][number] & {
3432
+ kind: "image";
3433
+ } | null {
3434
+ for (const block of blocks) {
3435
+ if (position < block.from || position > block.to) {
3436
+ continue;
3437
+ }
3438
+
3439
+ if (block.kind === "paragraph") {
3440
+ const imageSegment = block.segments.find(
3441
+ (segment) => segment.kind === "image" && position >= segment.from && position <= segment.to,
3442
+ );
3443
+ if (imageSegment && imageSegment.kind === "image") {
3444
+ return imageSegment;
3445
+ }
3446
+ continue;
3447
+ }
3448
+
3449
+ if (block.kind === "table") {
3450
+ for (const row of block.rows) {
3451
+ for (const cell of row.cells) {
3452
+ const match = findSelectedImageSegmentInBlocks(cell.content, position);
3453
+ if (match) {
3454
+ return match;
3455
+ }
3456
+ }
3457
+ }
3458
+ continue;
3459
+ }
3460
+
3461
+ if (block.kind === "sdt_block") {
3462
+ const match = findSelectedImageSegmentInBlocks(block.children, position);
3463
+ if (match) {
3464
+ return match;
3465
+ }
3466
+ }
3467
+ }
3468
+
3469
+ return null;
3470
+ }
3471
+
3472
+ function findImageNodeByMediaId(blocks: readonly unknown[], mediaId: string): {
3473
+ floating?: {
3474
+ horizontalPosition?: { offset?: number };
3475
+ verticalPosition?: { offset?: number };
3476
+ };
3477
+ } | null {
3478
+ for (const block of blocks) {
3479
+ const match = findImageNodeInValue(block, mediaId);
3480
+ if (match) {
3481
+ return match;
3482
+ }
3483
+ }
3484
+ return null;
3485
+ }
3486
+
3487
+ function findImageNodeInValue(
3488
+ value: unknown,
3489
+ mediaId: string,
3490
+ ): {
3491
+ floating?: {
3492
+ horizontalPosition?: { offset?: number };
3493
+ verticalPosition?: { offset?: number };
3494
+ };
3495
+ } | null {
3496
+ if (!value || typeof value !== "object") {
3497
+ return null;
3498
+ }
3499
+
3500
+ const record = value as {
3501
+ type?: string;
3502
+ mediaId?: string;
3503
+ children?: unknown[];
3504
+ rows?: Array<{ cells?: Array<{ children?: unknown[] }> }>;
3505
+ floating?: {
3506
+ horizontalPosition?: { offset?: number };
3507
+ verticalPosition?: { offset?: number };
3508
+ };
3509
+ };
3510
+
3511
+ if (record.type === "image" && record.mediaId === mediaId) {
3512
+ return record;
3513
+ }
3514
+
3515
+ if (Array.isArray(record.children)) {
3516
+ for (const child of record.children) {
3517
+ const match = findImageNodeInValue(child, mediaId);
3518
+ if (match) {
3519
+ return match;
3520
+ }
3521
+ }
3522
+ }
3523
+
3524
+ if (Array.isArray(record.rows)) {
3525
+ for (const row of record.rows) {
3526
+ for (const cell of row.cells ?? []) {
3527
+ for (const child of cell.children ?? []) {
3528
+ const match = findImageNodeInValue(child, mediaId);
3529
+ if (match) {
3530
+ return match;
3531
+ }
3532
+ }
3533
+ }
3534
+ }
3535
+ }
3536
+
3537
+ return null;
3538
+ }
3539
+
3540
+ function createImageDataUrl(contentType: string, bytes: Uint8Array): string {
3541
+ const base64 = bytesToBase64(bytes);
3542
+ return `data:${contentType};base64,${base64}`;
3543
+ }
3544
+
3545
+ function bytesToBase64(bytes: Uint8Array): string {
3546
+ let binary = "";
3547
+ for (let index = 0; index < bytes.length; index += 1) {
3548
+ binary += String.fromCharCode(bytes[index] ?? 0);
3549
+ }
3550
+ return btoa(binary);
3551
+ }