@beyondwork/docx-react-component 1.0.18 → 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.
- package/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createEditorState,
|
|
3
|
-
createSelectionSnapshot,
|
|
4
3
|
createPersistedEditorSnapshot,
|
|
5
4
|
deriveDocumentStats,
|
|
5
|
+
createSelectionSnapshot,
|
|
6
6
|
type CanonicalDocumentEnvelope,
|
|
7
7
|
type CommentEntryRecord,
|
|
8
8
|
type CommentThreadRecord,
|
|
@@ -17,18 +17,47 @@ import type {
|
|
|
17
17
|
CommentSidebarSnapshot,
|
|
18
18
|
CommentSidebarThreadSnapshot,
|
|
19
19
|
CompatibilityReport,
|
|
20
|
+
DocumentMode,
|
|
21
|
+
DocumentNavigationSnapshot,
|
|
22
|
+
EditorSessionState,
|
|
20
23
|
EditorAnchorProjection,
|
|
21
24
|
EditorError,
|
|
25
|
+
EditorStoryTarget,
|
|
26
|
+
EditorViewStateSnapshot,
|
|
22
27
|
EditorWarning,
|
|
28
|
+
FieldEntrySnapshot,
|
|
29
|
+
FieldSnapshot,
|
|
30
|
+
HeaderFooterLinkPatch,
|
|
23
31
|
ExportDocxOptions,
|
|
24
32
|
ExportResult,
|
|
33
|
+
InteractionGuardSnapshot,
|
|
34
|
+
PageLayoutSnapshot,
|
|
25
35
|
PersistedEditorSnapshot,
|
|
36
|
+
ProtectionSnapshot,
|
|
26
37
|
RuntimeRenderSnapshot,
|
|
27
38
|
SelectionSnapshot,
|
|
39
|
+
StyleCatalogSnapshot,
|
|
40
|
+
TocRefreshOptions,
|
|
41
|
+
TocRefreshResult,
|
|
28
42
|
TrackedChangeEntrySnapshot,
|
|
29
43
|
TrackedChangesSnapshot,
|
|
44
|
+
UpdateFieldsOptions,
|
|
45
|
+
UpdateFieldsResult,
|
|
46
|
+
ViewMode,
|
|
47
|
+
WorkflowCandidateRange,
|
|
48
|
+
WorkflowCandidateRangeOptions,
|
|
49
|
+
WorkflowBlockedCommandReason,
|
|
50
|
+
WorkflowMarkupSnapshot,
|
|
51
|
+
WorkflowOverlay,
|
|
52
|
+
WorkflowScopeSnapshot,
|
|
53
|
+
WorkspaceMode,
|
|
30
54
|
WordReviewEditorEvent,
|
|
55
|
+
ZoomLevel,
|
|
31
56
|
} from "../api/public-types";
|
|
57
|
+
import {
|
|
58
|
+
editorSessionStateFromPersistedSnapshot,
|
|
59
|
+
persistedSnapshotFromEditorSessionState,
|
|
60
|
+
} from "../api/session-state.ts";
|
|
32
61
|
import {
|
|
33
62
|
executeEditorCommand,
|
|
34
63
|
selectionChanged,
|
|
@@ -39,11 +68,20 @@ import {
|
|
|
39
68
|
import { insertText } from "../core/commands/text-commands.ts";
|
|
40
69
|
import {
|
|
41
70
|
createDetachedAnchor,
|
|
71
|
+
createEmptyMapping,
|
|
42
72
|
createNodeAnchor,
|
|
43
73
|
createRangeAnchor,
|
|
74
|
+
mapRange,
|
|
75
|
+
MAIN_STORY_TARGET,
|
|
76
|
+
storyTargetsEqual,
|
|
44
77
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
45
78
|
} from "../core/selection/mapping.ts";
|
|
46
79
|
import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors.ts";
|
|
80
|
+
import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
|
|
81
|
+
import {
|
|
82
|
+
describeOpaqueFragment,
|
|
83
|
+
findOpaqueFragmentsIntersectingRange,
|
|
84
|
+
} from "../preservation/store.ts";
|
|
47
85
|
import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
|
|
48
86
|
import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
|
|
49
87
|
import {
|
|
@@ -53,21 +91,75 @@ import {
|
|
|
53
91
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
54
92
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
55
93
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
56
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
collectWorkflowMarkupSnapshot,
|
|
96
|
+
deriveWorkflowCandidateRangesFromMarkup,
|
|
97
|
+
} from "./workflow-markup.ts";
|
|
98
|
+
import {
|
|
99
|
+
createDocumentNavigationSnapshot,
|
|
100
|
+
findPageForOffset,
|
|
101
|
+
} from "./document-navigation.ts";
|
|
102
|
+
import {
|
|
103
|
+
buildPageLayoutSnapshot,
|
|
104
|
+
buildResolvedSections,
|
|
105
|
+
resolveActiveSection,
|
|
106
|
+
} from "./document-layout.ts";
|
|
107
|
+
import { normalizeHeaderFooterTarget } from "./story-context.ts";
|
|
108
|
+
import { storyTargetKey } from "./story-targeting.ts";
|
|
109
|
+
import {
|
|
110
|
+
createViewState,
|
|
111
|
+
setViewMode as applyViewMode,
|
|
112
|
+
setDocumentMode as applyDocumentMode,
|
|
113
|
+
setWorkspaceMode as applyWorkspaceMode,
|
|
114
|
+
setZoomLevel as applyZoomLevel,
|
|
115
|
+
setFocused as applyFocused,
|
|
116
|
+
setCaretAffinity as applyCaretAffinity,
|
|
117
|
+
setActivePageRegion as applyActivePageRegion,
|
|
118
|
+
setActiveObjectFrame as applyActiveObjectFrame,
|
|
119
|
+
createEditorViewStateSnapshot,
|
|
120
|
+
type ViewState,
|
|
121
|
+
} from "./view-state.ts";
|
|
122
|
+
import type {
|
|
123
|
+
BlockNode,
|
|
124
|
+
FieldNode,
|
|
125
|
+
FieldRefreshStatus,
|
|
126
|
+
InlineNode,
|
|
127
|
+
PageMargins,
|
|
128
|
+
ParagraphNode,
|
|
129
|
+
SubPartsCatalog,
|
|
130
|
+
} from "../model/canonical-document.ts";
|
|
131
|
+
import {
|
|
132
|
+
buildFieldRegistry,
|
|
133
|
+
isSupportedFieldFamily,
|
|
134
|
+
parseTocLevelRange,
|
|
135
|
+
resolveRefFieldText,
|
|
136
|
+
} from "../io/ooxml/parse-fields.ts";
|
|
137
|
+
import {
|
|
138
|
+
incrementInvalidationCounter,
|
|
139
|
+
recordPerfSample,
|
|
140
|
+
} from "../ui-tailwind/editor-surface/perf-probe.ts";
|
|
57
141
|
|
|
58
142
|
export type Unsubscribe = () => void;
|
|
59
143
|
|
|
144
|
+
type RuntimeReadySource = "docx" | "session" | "snapshot" | "datastore" | "canonical";
|
|
145
|
+
|
|
146
|
+
export type DocumentRuntimeEvent =
|
|
147
|
+
| (Omit<Extract<WordReviewEditorEvent, { type: "ready" }>, "source"> & {
|
|
148
|
+
source: RuntimeReadySource;
|
|
149
|
+
})
|
|
150
|
+
| Exclude<WordReviewEditorEvent, { type: "ready" }>;
|
|
151
|
+
|
|
60
152
|
export interface DocumentRuntime {
|
|
61
153
|
subscribe(listener: () => void): Unsubscribe;
|
|
62
|
-
subscribeToEvents(listener: (event:
|
|
154
|
+
subscribeToEvents(listener: (event: DocumentRuntimeEvent) => void): Unsubscribe;
|
|
63
155
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
64
|
-
|
|
156
|
+
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
65
157
|
dispatch(command: EditorCommand): void;
|
|
66
158
|
undo(): void;
|
|
67
159
|
redo(): void;
|
|
68
160
|
focus(): void;
|
|
69
161
|
blur(): void;
|
|
70
|
-
|
|
162
|
+
setDefaultAuthorId?(authorId?: string): void;
|
|
71
163
|
addComment(params: AddCommentParams): string;
|
|
72
164
|
openComment(commentId: string): void;
|
|
73
165
|
resolveComment(commentId: string): void;
|
|
@@ -78,30 +170,55 @@ export interface DocumentRuntime {
|
|
|
78
170
|
rejectChange(changeId: string): void;
|
|
79
171
|
acceptAllChanges(): void;
|
|
80
172
|
rejectAllChanges(): void;
|
|
173
|
+
openStory(target: EditorStoryTarget): boolean;
|
|
174
|
+
closeStory(): void;
|
|
175
|
+
getActiveStory(): EditorStoryTarget;
|
|
176
|
+
getViewState(): EditorViewStateSnapshot;
|
|
177
|
+
setViewMode(mode: ViewMode): void;
|
|
178
|
+
setDocumentMode(mode: DocumentMode): void;
|
|
179
|
+
getProtectionSnapshot(): ProtectionSnapshot;
|
|
180
|
+
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
181
|
+
setZoom(level: ZoomLevel): void;
|
|
182
|
+
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
183
|
+
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
184
|
+
getFieldSnapshot(): FieldSnapshot;
|
|
185
|
+
updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult;
|
|
186
|
+
updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult;
|
|
187
|
+
getSessionState(): EditorSessionState;
|
|
81
188
|
getPersistedSnapshot(): PersistedEditorSnapshot;
|
|
82
189
|
getCompatibilityReport(): CompatibilityReport;
|
|
83
190
|
getWarnings(): EditorWarning[];
|
|
84
191
|
exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
|
|
192
|
+
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
193
|
+
clearWorkflowOverlay(): void;
|
|
194
|
+
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
195
|
+
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
196
|
+
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
197
|
+
getWorkflowCandidateRanges(options?: WorkflowCandidateRangeOptions): WorkflowCandidateRange[];
|
|
198
|
+
replaceWorkflowMarkupText(markupId: string, text: string): void;
|
|
85
199
|
}
|
|
86
200
|
|
|
87
201
|
export interface CreateDocumentRuntimeOptions {
|
|
88
202
|
documentId: string;
|
|
203
|
+
initialSessionState?: EditorSessionState;
|
|
89
204
|
initialSnapshot?: PersistedEditorSnapshot;
|
|
90
205
|
initialCanonicalDocument?: CanonicalDocumentEnvelope;
|
|
91
206
|
sourceLabel?: string;
|
|
92
|
-
sourceKind?:
|
|
207
|
+
sourceKind?: RuntimeReadySource;
|
|
93
208
|
readOnly?: boolean;
|
|
94
209
|
editorBuild?: string;
|
|
95
210
|
defaultAuthorId?: string;
|
|
96
211
|
fatalError?: EditorError;
|
|
97
212
|
clock?: () => string;
|
|
98
213
|
exportDocx?: (
|
|
99
|
-
|
|
214
|
+
sessionState: EditorSessionState,
|
|
100
215
|
options?: ExportDocxOptions,
|
|
101
216
|
) => Promise<ExportResult>;
|
|
102
|
-
onEvent?: (event:
|
|
217
|
+
onEvent?: (event: DocumentRuntimeEvent) => void;
|
|
103
218
|
onWarning?: (warning: EditorWarning) => void;
|
|
104
219
|
onError?: (error: EditorError) => void;
|
|
220
|
+
initialViewState?: Partial<ViewState>;
|
|
221
|
+
protectionSnapshot?: ProtectionSnapshot;
|
|
105
222
|
}
|
|
106
223
|
|
|
107
224
|
interface HistoryState {
|
|
@@ -114,30 +231,624 @@ export function createDocumentRuntime(
|
|
|
114
231
|
): DocumentRuntime {
|
|
115
232
|
const clock = options.clock ?? (() => new Date().toISOString());
|
|
116
233
|
const editorBuild = options.editorBuild ?? "dev";
|
|
234
|
+
let defaultAuthorId = options.defaultAuthorId;
|
|
117
235
|
const sessionId = createSessionId(options.documentId, clock());
|
|
118
236
|
const listeners = new Set<() => void>();
|
|
119
|
-
const eventListeners = new Set<(event:
|
|
237
|
+
const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
|
|
120
238
|
const history: HistoryState = {
|
|
121
239
|
past: [],
|
|
122
240
|
future: [],
|
|
123
241
|
};
|
|
124
242
|
|
|
243
|
+
let activeStory: EditorStoryTarget = MAIN_STORY_TARGET;
|
|
244
|
+
const storySelections = new Map<string, EditorState["selection"]>();
|
|
245
|
+
let viewState: ViewState = createViewState(options.initialViewState);
|
|
246
|
+
let protectionSnapshot: ProtectionSnapshot =
|
|
247
|
+
options.protectionSnapshot ??
|
|
248
|
+
options.initialSessionState?.protectionSnapshot ??
|
|
249
|
+
options.initialSnapshot?.protectionSnapshot ?? {
|
|
250
|
+
hasDocumentProtection: false,
|
|
251
|
+
enforcementActive: false,
|
|
252
|
+
ranges: [],
|
|
253
|
+
enforcedRangeCount: 0,
|
|
254
|
+
preservedRangeCount: 0,
|
|
255
|
+
};
|
|
256
|
+
let workflowOverlay: WorkflowOverlay | null = null;
|
|
257
|
+
const initialPersistedSnapshot = options.initialSessionState
|
|
258
|
+
? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
|
|
259
|
+
savedAt: options.initialSessionState.updatedAt,
|
|
260
|
+
})
|
|
261
|
+
: options.initialSnapshot;
|
|
262
|
+
|
|
125
263
|
let state = createEditorState({
|
|
126
264
|
documentId: options.documentId,
|
|
127
265
|
sessionId,
|
|
128
266
|
sourceLabel: options.sourceLabel,
|
|
129
267
|
readOnly: options.readOnly,
|
|
130
|
-
persistedSnapshot:
|
|
268
|
+
persistedSnapshot: initialPersistedSnapshot as never,
|
|
131
269
|
canonicalDocument: options.initialCanonicalDocument,
|
|
132
270
|
fatalError: options.fatalError as never,
|
|
133
271
|
});
|
|
134
|
-
|
|
272
|
+
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
273
|
+
let cachedSurface:
|
|
274
|
+
| {
|
|
275
|
+
revisionToken: string;
|
|
276
|
+
activeStoryKey: string;
|
|
277
|
+
snapshot: RuntimeRenderSnapshot["surface"];
|
|
278
|
+
}
|
|
279
|
+
| undefined;
|
|
280
|
+
let cachedCompatibility:
|
|
281
|
+
| {
|
|
282
|
+
revisionToken: string;
|
|
283
|
+
warnings: EditorState["warnings"];
|
|
284
|
+
fatalError: EditorState["fatalError"];
|
|
285
|
+
report: RuntimeRenderSnapshot["compatibility"];
|
|
286
|
+
}
|
|
287
|
+
| undefined;
|
|
288
|
+
let cachedComments:
|
|
289
|
+
| {
|
|
290
|
+
comments: CanonicalDocumentEnvelope["review"]["comments"];
|
|
291
|
+
activeCommentId: EditorState["runtime"]["activeCommentId"];
|
|
292
|
+
snapshot: CommentSidebarSnapshot;
|
|
293
|
+
}
|
|
294
|
+
| undefined;
|
|
295
|
+
let cachedTrackedChanges:
|
|
296
|
+
| {
|
|
297
|
+
revisions: CanonicalDocumentEnvelope["review"]["revisions"];
|
|
298
|
+
plainText: string;
|
|
299
|
+
snapshot: TrackedChangesSnapshot;
|
|
300
|
+
}
|
|
301
|
+
| undefined;
|
|
302
|
+
let cachedPageLayout:
|
|
303
|
+
| {
|
|
304
|
+
revisionToken: string;
|
|
305
|
+
activeStoryKey: string;
|
|
306
|
+
activeSectionIndex: number | string;
|
|
307
|
+
snapshot: PageLayoutSnapshot | null;
|
|
308
|
+
}
|
|
309
|
+
| undefined;
|
|
310
|
+
let cachedNavigation:
|
|
311
|
+
| {
|
|
312
|
+
revisionToken: string;
|
|
313
|
+
activeStoryKey: string;
|
|
314
|
+
selectionHead: number;
|
|
315
|
+
snapshot: DocumentNavigationSnapshot;
|
|
316
|
+
}
|
|
317
|
+
| undefined;
|
|
318
|
+
let cachedViewStateSnapshot:
|
|
319
|
+
| {
|
|
320
|
+
revisionToken: string;
|
|
321
|
+
activeStoryKey: string;
|
|
322
|
+
selection: EditorState["selection"];
|
|
323
|
+
viewStateRef: ViewState;
|
|
324
|
+
pageLayout: PageLayoutSnapshot | null | undefined;
|
|
325
|
+
snapshot: EditorViewStateSnapshot;
|
|
326
|
+
}
|
|
327
|
+
| undefined;
|
|
328
|
+
let cachedInteractionGuardSnapshot:
|
|
329
|
+
| {
|
|
330
|
+
revisionToken: string;
|
|
331
|
+
activeStoryKey: string;
|
|
332
|
+
selection: EditorState["selection"];
|
|
333
|
+
readOnly: boolean;
|
|
334
|
+
documentMode: DocumentMode;
|
|
335
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
336
|
+
workflowOverlay: WorkflowOverlay | null;
|
|
337
|
+
snapshot: InteractionGuardSnapshot;
|
|
338
|
+
}
|
|
339
|
+
| undefined;
|
|
340
|
+
let cachedWorkflowScopeSnapshot:
|
|
341
|
+
| {
|
|
342
|
+
workflowOverlay: WorkflowOverlay;
|
|
343
|
+
interactionGuardSnapshot: InteractionGuardSnapshot;
|
|
344
|
+
snapshot: WorkflowScopeSnapshot;
|
|
345
|
+
}
|
|
346
|
+
| undefined;
|
|
347
|
+
|
|
348
|
+
function getCachedSurface(
|
|
349
|
+
document: CanonicalDocumentEnvelope,
|
|
350
|
+
nextActiveStory: EditorStoryTarget,
|
|
351
|
+
): RuntimeRenderSnapshot["surface"] {
|
|
352
|
+
const activeStoryKey = storyTargetKey(nextActiveStory);
|
|
353
|
+
if (
|
|
354
|
+
cachedSurface &&
|
|
355
|
+
cachedSurface.revisionToken === state.revisionToken &&
|
|
356
|
+
cachedSurface.activeStoryKey === activeStoryKey
|
|
357
|
+
) {
|
|
358
|
+
return cachedSurface.snapshot;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory);
|
|
362
|
+
recordPerfSample("snapshot.surface");
|
|
363
|
+
incrementInvalidationCounter("runtime.snapshot.surfaceMisses");
|
|
364
|
+
cachedSurface = {
|
|
365
|
+
revisionToken: state.revisionToken,
|
|
366
|
+
activeStoryKey,
|
|
367
|
+
snapshot,
|
|
368
|
+
};
|
|
369
|
+
return snapshot;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getCachedCompatibilityReport(
|
|
373
|
+
nextState: EditorState,
|
|
374
|
+
): RuntimeRenderSnapshot["compatibility"] {
|
|
375
|
+
if (
|
|
376
|
+
cachedCompatibility &&
|
|
377
|
+
cachedCompatibility.revisionToken === nextState.revisionToken &&
|
|
378
|
+
cachedCompatibility.warnings === nextState.warnings &&
|
|
379
|
+
cachedCompatibility.fatalError === nextState.fatalError
|
|
380
|
+
) {
|
|
381
|
+
return cachedCompatibility.report;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const derived = createDerivedCompatibility(nextState);
|
|
385
|
+
recordPerfSample("snapshot.compatibility");
|
|
386
|
+
incrementInvalidationCounter("runtime.snapshot.compatibilityMisses");
|
|
387
|
+
const report = {
|
|
388
|
+
blockExport: derived.blockExport,
|
|
389
|
+
blockExportReasons: listBlockExportReasons(derived),
|
|
390
|
+
warningCount: derived.warnings.length,
|
|
391
|
+
errorCount: derived.errors.length,
|
|
392
|
+
featureEntries: derived.featureEntries.map((entry) =>
|
|
393
|
+
toPublicCompatibilityFeatureEntry(entry),
|
|
394
|
+
),
|
|
395
|
+
};
|
|
396
|
+
cachedCompatibility = {
|
|
397
|
+
revisionToken: nextState.revisionToken,
|
|
398
|
+
warnings: nextState.warnings,
|
|
399
|
+
fatalError: nextState.fatalError,
|
|
400
|
+
report,
|
|
401
|
+
};
|
|
402
|
+
return report;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getCachedCommentSidebarSnapshot(nextState: EditorState): CommentSidebarSnapshot {
|
|
406
|
+
if (
|
|
407
|
+
cachedComments &&
|
|
408
|
+
cachedComments.comments === nextState.document.review.comments &&
|
|
409
|
+
cachedComments.activeCommentId === nextState.runtime.activeCommentId
|
|
410
|
+
) {
|
|
411
|
+
return cachedComments.snapshot;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const snapshot = toPublicCommentSidebarSnapshot(nextState);
|
|
415
|
+
cachedComments = {
|
|
416
|
+
comments: nextState.document.review.comments,
|
|
417
|
+
activeCommentId: nextState.runtime.activeCommentId,
|
|
418
|
+
snapshot,
|
|
419
|
+
};
|
|
420
|
+
return snapshot;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getCachedTrackedChangesSnapshot(
|
|
424
|
+
nextState: EditorState,
|
|
425
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
426
|
+
): TrackedChangesSnapshot {
|
|
427
|
+
const plainText = surface?.plainText ?? "";
|
|
428
|
+
if (
|
|
429
|
+
cachedTrackedChanges &&
|
|
430
|
+
cachedTrackedChanges.revisions === nextState.document.review.revisions &&
|
|
431
|
+
cachedTrackedChanges.plainText === plainText
|
|
432
|
+
) {
|
|
433
|
+
return cachedTrackedChanges.snapshot;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const snapshot = toPublicTrackedChangesSnapshot(nextState, plainText);
|
|
437
|
+
cachedTrackedChanges = {
|
|
438
|
+
revisions: nextState.document.review.revisions,
|
|
439
|
+
plainText,
|
|
440
|
+
snapshot,
|
|
441
|
+
};
|
|
442
|
+
return snapshot;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function getCachedDocumentNavigationSnapshot(
|
|
446
|
+
nextState: EditorState,
|
|
447
|
+
nextActiveStory: EditorStoryTarget,
|
|
448
|
+
): DocumentNavigationSnapshot {
|
|
449
|
+
const activeStoryKey = storyTargetKey(nextActiveStory);
|
|
450
|
+
if (
|
|
451
|
+
cachedNavigation &&
|
|
452
|
+
cachedNavigation.revisionToken === nextState.revisionToken &&
|
|
453
|
+
cachedNavigation.activeStoryKey === activeStoryKey
|
|
454
|
+
) {
|
|
455
|
+
if (cachedNavigation.selectionHead === nextState.selection.head) {
|
|
456
|
+
return cachedNavigation.snapshot;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const snapshot = createDocumentNavigationSnapshot(
|
|
460
|
+
nextState.document,
|
|
461
|
+
nextState.selection.head,
|
|
462
|
+
nextActiveStory,
|
|
463
|
+
);
|
|
464
|
+
if (
|
|
465
|
+
snapshot.activePageIndex === cachedNavigation.snapshot.activePageIndex &&
|
|
466
|
+
snapshot.activeSectionIndex === cachedNavigation.snapshot.activeSectionIndex
|
|
467
|
+
) {
|
|
468
|
+
cachedNavigation = {
|
|
469
|
+
revisionToken: nextState.revisionToken,
|
|
470
|
+
activeStoryKey,
|
|
471
|
+
selectionHead: nextState.selection.head,
|
|
472
|
+
snapshot: cachedNavigation.snapshot,
|
|
473
|
+
};
|
|
474
|
+
return cachedNavigation.snapshot;
|
|
475
|
+
}
|
|
476
|
+
cachedNavigation = {
|
|
477
|
+
revisionToken: nextState.revisionToken,
|
|
478
|
+
activeStoryKey,
|
|
479
|
+
selectionHead: nextState.selection.head,
|
|
480
|
+
snapshot,
|
|
481
|
+
};
|
|
482
|
+
return snapshot;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const snapshot = createDocumentNavigationSnapshot(
|
|
486
|
+
nextState.document,
|
|
487
|
+
nextState.selection.head,
|
|
488
|
+
nextActiveStory,
|
|
489
|
+
);
|
|
490
|
+
recordPerfSample("snapshot.navigation");
|
|
491
|
+
incrementInvalidationCounter("runtime.snapshot.navigationMisses");
|
|
492
|
+
cachedNavigation = {
|
|
493
|
+
revisionToken: nextState.revisionToken,
|
|
494
|
+
activeStoryKey,
|
|
495
|
+
selectionHead: nextState.selection.head,
|
|
496
|
+
snapshot,
|
|
497
|
+
};
|
|
498
|
+
return snapshot;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function resolvePageLayoutActiveSectionIndex(
|
|
502
|
+
nextState: EditorState,
|
|
503
|
+
nextActiveStory: EditorStoryTarget,
|
|
504
|
+
): number | string {
|
|
505
|
+
if (nextActiveStory.kind === "main") {
|
|
506
|
+
return getCachedDocumentNavigationSnapshot(nextState, nextActiveStory).activeSectionIndex;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if ("sectionIndex" in nextActiveStory && typeof nextActiveStory.sectionIndex === "number") {
|
|
510
|
+
return nextActiveStory.sectionIndex;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return storyTargetKey(nextActiveStory);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function getCachedPageLayoutSnapshot(
|
|
517
|
+
nextState: EditorState,
|
|
518
|
+
nextActiveStory: EditorStoryTarget,
|
|
519
|
+
): PageLayoutSnapshot | null {
|
|
520
|
+
const activeStoryKey = storyTargetKey(nextActiveStory);
|
|
521
|
+
const activeSectionIndex = resolvePageLayoutActiveSectionIndex(
|
|
522
|
+
nextState,
|
|
523
|
+
nextActiveStory,
|
|
524
|
+
);
|
|
525
|
+
if (
|
|
526
|
+
cachedPageLayout &&
|
|
527
|
+
cachedPageLayout.revisionToken === nextState.revisionToken &&
|
|
528
|
+
cachedPageLayout.activeStoryKey === activeStoryKey &&
|
|
529
|
+
cachedPageLayout.activeSectionIndex === activeSectionIndex
|
|
530
|
+
) {
|
|
531
|
+
return cachedPageLayout.snapshot;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const snapshot = derivePageLayoutSnapshot(nextState, nextActiveStory, storySelections);
|
|
535
|
+
cachedPageLayout = {
|
|
536
|
+
revisionToken: nextState.revisionToken,
|
|
537
|
+
activeStoryKey,
|
|
538
|
+
activeSectionIndex,
|
|
539
|
+
snapshot,
|
|
540
|
+
};
|
|
541
|
+
return snapshot;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function evaluateWorkflowBlockedReasons(
|
|
545
|
+
selection: EditorState["selection"],
|
|
546
|
+
commandType?: string,
|
|
547
|
+
): WorkflowBlockedCommandReason[] {
|
|
548
|
+
const reasons: WorkflowBlockedCommandReason[] = [];
|
|
549
|
+
const selectionBounds = {
|
|
550
|
+
from: Math.min(selection.anchor, selection.head),
|
|
551
|
+
to: Math.max(selection.anchor, selection.head),
|
|
552
|
+
};
|
|
553
|
+
const selectionRange = expandSelectionRange(selectionBounds);
|
|
554
|
+
const opaqueReason = deriveOpaqueWorkflowBlockedReason(selectionRange);
|
|
555
|
+
|
|
556
|
+
if (opaqueReason) {
|
|
557
|
+
reasons.push(opaqueReason);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (state.readOnly) {
|
|
561
|
+
reasons.push({
|
|
562
|
+
code: "document_read_only",
|
|
563
|
+
message: "Document is in read-only mode.",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (viewState.documentMode === "viewing") {
|
|
568
|
+
reasons.push({
|
|
569
|
+
code: "document_viewing_mode",
|
|
570
|
+
message: "Document is in viewing mode.",
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
isBlockedByProtection(protectionSnapshot, selection)
|
|
576
|
+
) {
|
|
577
|
+
reasons.push({
|
|
578
|
+
code: "protected_range",
|
|
579
|
+
message: "Selection falls within a protected range.",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (workflowOverlay) {
|
|
584
|
+
const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
|
|
585
|
+
const matchingScope = activeScopes.find((scope) => {
|
|
586
|
+
if (scope.anchor.kind === "detached") return false;
|
|
587
|
+
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
588
|
+
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
589
|
+
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (!matchingScope && workflowOverlay.scopes.length > 0) {
|
|
593
|
+
reasons.push({
|
|
594
|
+
code: "outside_workflow_scope",
|
|
595
|
+
message: "Selection is outside any active workflow scope.",
|
|
596
|
+
});
|
|
597
|
+
} else if (matchingScope) {
|
|
598
|
+
if (matchingScope.mode === "comment") {
|
|
599
|
+
const isCommentCommand =
|
|
600
|
+
commandType?.startsWith("comment.") ?? false;
|
|
601
|
+
if (!isCommentCommand) {
|
|
602
|
+
reasons.push({
|
|
603
|
+
code: "workflow_comment_only",
|
|
604
|
+
message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
|
|
605
|
+
scopeId: matchingScope.scopeId,
|
|
606
|
+
workItemId: matchingScope.workItemId,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
} else if (matchingScope.mode === "view") {
|
|
610
|
+
reasons.push({
|
|
611
|
+
code: "workflow_view_only",
|
|
612
|
+
message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
|
|
613
|
+
scopeId: matchingScope.scopeId,
|
|
614
|
+
workItemId: matchingScope.workItemId,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return reasons;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function expandSelectionRange(
|
|
624
|
+
range: { from: number; to: number },
|
|
625
|
+
): { from: number; to: number } {
|
|
626
|
+
return {
|
|
627
|
+
from: range.from,
|
|
628
|
+
to: range.to > range.from ? range.to : range.from + 1,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function deriveOpaqueWorkflowBlockedReason(
|
|
633
|
+
range: { from: number; to: number },
|
|
634
|
+
): WorkflowBlockedCommandReason | null {
|
|
635
|
+
const fragments = findOpaqueFragmentsIntersectingRange(
|
|
636
|
+
state.document.preservation,
|
|
637
|
+
range,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
if (fragments.length === 0) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const blockedImportFeatureKeys = new Set([
|
|
645
|
+
"alt-chunk",
|
|
646
|
+
"alternate-content",
|
|
647
|
+
"custom-xml",
|
|
648
|
+
]);
|
|
649
|
+
const blockedImportFragment =
|
|
650
|
+
fragments.find((fragment) =>
|
|
651
|
+
blockedImportFeatureKeys.has(describeOpaqueFragment(fragment).featureKey),
|
|
652
|
+
) ?? null;
|
|
653
|
+
const fragment = blockedImportFragment ?? fragments[0]!;
|
|
654
|
+
const descriptor = describeOpaqueFragment(fragment);
|
|
655
|
+
const isBlockedImport = blockedImportFragment !== null;
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
code: isBlockedImport ? "workflow_blocked_import" : "workflow_preserve_only",
|
|
659
|
+
message: isBlockedImport
|
|
660
|
+
? `${descriptor.label} remains a blocked import and cannot be edited.`
|
|
661
|
+
: `${descriptor.label} remains preserve-only and cannot be edited.`,
|
|
662
|
+
anchor: toPublicAnchorProjection(
|
|
663
|
+
createRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to, {
|
|
664
|
+
start: -1,
|
|
665
|
+
end: 1,
|
|
666
|
+
}),
|
|
667
|
+
),
|
|
668
|
+
storyTarget: activeStory,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
673
|
+
if (!workflowOverlay) return null;
|
|
674
|
+
const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
|
|
675
|
+
const activeItem = workflowOverlay.activeWorkItemId
|
|
676
|
+
? workflowOverlay.workItems?.find(
|
|
677
|
+
(item) => item.workItemId === workflowOverlay!.activeWorkItemId,
|
|
678
|
+
)
|
|
679
|
+
: undefined;
|
|
680
|
+
return {
|
|
681
|
+
overlayPresent: true,
|
|
682
|
+
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
683
|
+
activeWorkItem: activeItem,
|
|
684
|
+
scopes: workflowOverlay.scopes,
|
|
685
|
+
candidates: workflowOverlay.candidates ?? [],
|
|
686
|
+
blockedReasons,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
|
|
691
|
+
const activeWorkItemId = overlay.activeWorkItemId ?? null;
|
|
692
|
+
const activeWorkItemScopeIds =
|
|
693
|
+
activeWorkItemId === null
|
|
694
|
+
? null
|
|
695
|
+
: new Set(
|
|
696
|
+
overlay.workItems?.find((item) => item.workItemId === activeWorkItemId)?.scopeIds ?? [],
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return overlay.scopes.filter((scope) => {
|
|
700
|
+
const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
701
|
+
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (activeWorkItemId === null) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return (
|
|
710
|
+
scope.workItemId === activeWorkItemId ||
|
|
711
|
+
activeWorkItemScopeIds?.has(scope.scopeId) === true
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function getCachedInteractionGuardSnapshot(): InteractionGuardSnapshot {
|
|
717
|
+
const activeStoryKey = storyTargetKey(activeStory);
|
|
718
|
+
if (
|
|
719
|
+
cachedInteractionGuardSnapshot &&
|
|
720
|
+
cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
|
|
721
|
+
cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
|
|
722
|
+
cachedInteractionGuardSnapshot.selection === state.selection &&
|
|
723
|
+
cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
|
|
724
|
+
cachedInteractionGuardSnapshot.documentMode === viewState.documentMode &&
|
|
725
|
+
cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
|
|
726
|
+
cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay
|
|
727
|
+
) {
|
|
728
|
+
return cachedInteractionGuardSnapshot.snapshot;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const snapshot = {
|
|
732
|
+
blockedReasons: evaluateWorkflowBlockedReasons(state.selection),
|
|
733
|
+
};
|
|
734
|
+
cachedInteractionGuardSnapshot = {
|
|
735
|
+
revisionToken: state.revisionToken,
|
|
736
|
+
activeStoryKey,
|
|
737
|
+
selection: state.selection,
|
|
738
|
+
readOnly: state.readOnly,
|
|
739
|
+
documentMode: viewState.documentMode,
|
|
740
|
+
protectionSnapshot,
|
|
741
|
+
workflowOverlay,
|
|
742
|
+
snapshot,
|
|
743
|
+
};
|
|
744
|
+
return snapshot;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function getCachedWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
748
|
+
if (!workflowOverlay) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const interactionGuardSnapshot = getCachedInteractionGuardSnapshot();
|
|
753
|
+
if (
|
|
754
|
+
cachedWorkflowScopeSnapshot &&
|
|
755
|
+
cachedWorkflowScopeSnapshot.workflowOverlay === workflowOverlay &&
|
|
756
|
+
cachedWorkflowScopeSnapshot.interactionGuardSnapshot === interactionGuardSnapshot
|
|
757
|
+
) {
|
|
758
|
+
return cachedWorkflowScopeSnapshot.snapshot;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
762
|
+
cachedWorkflowScopeSnapshot = {
|
|
763
|
+
workflowOverlay,
|
|
764
|
+
interactionGuardSnapshot,
|
|
765
|
+
snapshot,
|
|
766
|
+
};
|
|
767
|
+
return snapshot;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function refreshRenderSnapshot(): RuntimeRenderSnapshot {
|
|
771
|
+
const surface = getCachedSurface(state.document, activeStory);
|
|
772
|
+
return {
|
|
773
|
+
documentId: state.documentId,
|
|
774
|
+
sessionId: state.sessionId,
|
|
775
|
+
sourceLabel: state.sourceLabel,
|
|
776
|
+
revisionToken: state.revisionToken,
|
|
777
|
+
isReady: state.phase === "ready",
|
|
778
|
+
isDirty: state.isDirty,
|
|
779
|
+
readOnly: state.readOnly,
|
|
780
|
+
documentMode: viewState.documentMode,
|
|
781
|
+
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
782
|
+
activeStory,
|
|
783
|
+
pageLayout: getCachedPageLayoutSnapshot(state, activeStory) ?? undefined,
|
|
784
|
+
documentStats: toPublicDocumentStats(state),
|
|
785
|
+
comments: getCachedCommentSidebarSnapshot(state),
|
|
786
|
+
trackedChanges: getCachedTrackedChangesSnapshot(state, surface),
|
|
787
|
+
compatibility: getCachedCompatibilityReport(state),
|
|
788
|
+
warnings: state.warnings.map((warning) => toPublicWarning(warning)),
|
|
789
|
+
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
790
|
+
commandState: {
|
|
791
|
+
canUndo: history.past.length > 0,
|
|
792
|
+
canRedo: history.future.length > 0,
|
|
793
|
+
readOnly: state.readOnly,
|
|
794
|
+
},
|
|
795
|
+
surface,
|
|
796
|
+
protectionSnapshot,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getCachedViewStateSnapshot(): EditorViewStateSnapshot {
|
|
801
|
+
const activeStoryKey = storyTargetKey(activeStory);
|
|
802
|
+
const pageLayout = cachedRenderSnapshot.pageLayout;
|
|
803
|
+
if (
|
|
804
|
+
cachedViewStateSnapshot &&
|
|
805
|
+
cachedViewStateSnapshot.revisionToken === state.revisionToken &&
|
|
806
|
+
cachedViewStateSnapshot.activeStoryKey === activeStoryKey &&
|
|
807
|
+
cachedViewStateSnapshot.selection === state.selection &&
|
|
808
|
+
cachedViewStateSnapshot.viewStateRef === viewState &&
|
|
809
|
+
cachedViewStateSnapshot.pageLayout === pageLayout
|
|
810
|
+
) {
|
|
811
|
+
return cachedViewStateSnapshot.snapshot;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const surface = cachedRenderSnapshot.surface;
|
|
815
|
+
const mainSurface =
|
|
816
|
+
activeStory.kind === "main"
|
|
817
|
+
? surface
|
|
818
|
+
: getCachedSurface(state.document, MAIN_STORY_TARGET);
|
|
819
|
+
const snapshot = createEditorViewStateSnapshot(
|
|
820
|
+
viewState,
|
|
821
|
+
activeStory,
|
|
822
|
+
toPublicSelectionSnapshot(state.selection, activeStory),
|
|
823
|
+
surface,
|
|
824
|
+
mainSurface,
|
|
825
|
+
pageLayout,
|
|
826
|
+
state.document.numbering,
|
|
827
|
+
);
|
|
828
|
+
cachedViewStateSnapshot = {
|
|
829
|
+
revisionToken: state.revisionToken,
|
|
830
|
+
activeStoryKey,
|
|
831
|
+
selection: state.selection,
|
|
832
|
+
viewStateRef: viewState,
|
|
833
|
+
pageLayout,
|
|
834
|
+
snapshot,
|
|
835
|
+
};
|
|
836
|
+
return snapshot;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
let cachedRenderSnapshot = refreshRenderSnapshot();
|
|
135
840
|
|
|
136
841
|
emit({
|
|
137
842
|
type: "ready",
|
|
138
843
|
documentId: state.documentId,
|
|
139
844
|
sessionId: state.sessionId,
|
|
140
|
-
source:
|
|
845
|
+
source:
|
|
846
|
+
options.sourceKind ??
|
|
847
|
+
(options.initialSessionState
|
|
848
|
+
? "session"
|
|
849
|
+
: options.initialSnapshot
|
|
850
|
+
? "snapshot"
|
|
851
|
+
: "canonical"),
|
|
141
852
|
stats: toPublicDocumentStats(state),
|
|
142
853
|
compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
|
|
143
854
|
comments: cachedRenderSnapshot.comments,
|
|
@@ -167,20 +878,34 @@ export function createDocumentRuntime(
|
|
|
167
878
|
getRenderSnapshot() {
|
|
168
879
|
return cachedRenderSnapshot;
|
|
169
880
|
},
|
|
170
|
-
getFormattingState() {
|
|
171
|
-
return getFormattingStateFromRenderSnapshot(cachedRenderSnapshot);
|
|
172
|
-
},
|
|
173
881
|
dispatch(command) {
|
|
882
|
+
if (isMutationCommand(command)) {
|
|
883
|
+
const blockedReasons = evaluateWorkflowBlockedReasons(
|
|
884
|
+
getCommandSelection(command, state.selection),
|
|
885
|
+
command.type,
|
|
886
|
+
);
|
|
887
|
+
if (blockedReasons.length > 0) {
|
|
888
|
+
emit({
|
|
889
|
+
type: "command_blocked",
|
|
890
|
+
documentId: state.documentId,
|
|
891
|
+
command: command.type,
|
|
892
|
+
reasons: blockedReasons,
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
174
898
|
if (command.type === "history.undo") {
|
|
899
|
+
if (viewState.documentMode === "viewing") return;
|
|
175
900
|
applyHistory("undo");
|
|
176
901
|
return;
|
|
177
902
|
}
|
|
178
903
|
|
|
179
904
|
if (command.type === "history.redo") {
|
|
905
|
+
if (viewState.documentMode === "viewing") return;
|
|
180
906
|
applyHistory("redo");
|
|
181
907
|
return;
|
|
182
908
|
}
|
|
183
|
-
|
|
184
909
|
try {
|
|
185
910
|
const transaction = executeEditorCommand(state, command, {
|
|
186
911
|
timestamp: command.origin?.timestamp ?? clock(),
|
|
@@ -203,6 +928,7 @@ export function createDocumentRuntime(
|
|
|
203
928
|
});
|
|
204
929
|
},
|
|
205
930
|
focus() {
|
|
931
|
+
viewState = applyFocused(viewState, true);
|
|
206
932
|
this.dispatch({
|
|
207
933
|
type: "runtime.focus",
|
|
208
934
|
focused: true,
|
|
@@ -210,18 +936,32 @@ export function createDocumentRuntime(
|
|
|
210
936
|
});
|
|
211
937
|
},
|
|
212
938
|
blur() {
|
|
939
|
+
viewState = applyFocused(viewState, false);
|
|
213
940
|
this.dispatch({
|
|
214
941
|
type: "runtime.focus",
|
|
215
942
|
focused: false,
|
|
216
943
|
origin: createOrigin("api", clock()),
|
|
217
944
|
});
|
|
218
945
|
},
|
|
946
|
+
setDefaultAuthorId(authorId) {
|
|
947
|
+
defaultAuthorId = authorId;
|
|
948
|
+
},
|
|
219
949
|
replaceText(text, target) {
|
|
220
950
|
try {
|
|
221
951
|
const timestamp = clock();
|
|
222
952
|
const selection = target
|
|
223
953
|
? createSelectionFromPublicAnchor(target)
|
|
224
954
|
: state.selection;
|
|
955
|
+
const blockedReasons = evaluateWorkflowBlockedReasons(selection, "text.insert");
|
|
956
|
+
if (blockedReasons.length > 0) {
|
|
957
|
+
emit({
|
|
958
|
+
type: "command_blocked",
|
|
959
|
+
documentId: state.documentId,
|
|
960
|
+
command: "replaceText",
|
|
961
|
+
reasons: blockedReasons,
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
225
965
|
const result = insertText(state.document, selection, text, { timestamp });
|
|
226
966
|
|
|
227
967
|
this.dispatch({
|
|
@@ -229,6 +969,7 @@ export function createDocumentRuntime(
|
|
|
229
969
|
document: result.document,
|
|
230
970
|
mapping: result.mapping,
|
|
231
971
|
selection: result.selection,
|
|
972
|
+
protectionSelection: selection,
|
|
232
973
|
origin: createOrigin("api", timestamp),
|
|
233
974
|
});
|
|
234
975
|
} catch (error) {
|
|
@@ -236,10 +977,16 @@ export function createDocumentRuntime(
|
|
|
236
977
|
}
|
|
237
978
|
},
|
|
238
979
|
addComment(params) {
|
|
980
|
+
if (viewState.documentMode === "viewing") {
|
|
981
|
+
throw new Error("Cannot add comments in viewing mode.");
|
|
982
|
+
}
|
|
239
983
|
const commentId = createEntityId("comment", state.document.review.comments, clock());
|
|
240
984
|
const anchor = params.anchor
|
|
241
985
|
? toInternalAnchorProjection(params.anchor)
|
|
242
986
|
: state.selection.activeRange;
|
|
987
|
+
const selection = params.anchor
|
|
988
|
+
? createSelectionFromPublicAnchor(params.anchor)
|
|
989
|
+
: state.selection;
|
|
243
990
|
if (!canCreateDocxCommentAnchor(state.document.content, anchor)) {
|
|
244
991
|
const message =
|
|
245
992
|
"DOCX comments must use a non-empty range that stays within a single paragraph.";
|
|
@@ -252,7 +999,7 @@ export function createDocumentRuntime(
|
|
|
252
999
|
});
|
|
253
1000
|
throw new Error(message);
|
|
254
1001
|
}
|
|
255
|
-
const authorId = params.authorId ??
|
|
1002
|
+
const authorId = params.authorId ?? defaultAuthorId ?? "unknown";
|
|
256
1003
|
const createdAt = clock();
|
|
257
1004
|
const entries: CommentEntryRecord[] = [
|
|
258
1005
|
{
|
|
@@ -281,6 +1028,7 @@ export function createDocumentRuntime(
|
|
|
281
1028
|
this.dispatch({
|
|
282
1029
|
type: "comment.add",
|
|
283
1030
|
comment,
|
|
1031
|
+
selection,
|
|
284
1032
|
origin: createOrigin("api", clock()),
|
|
285
1033
|
});
|
|
286
1034
|
|
|
@@ -297,7 +1045,7 @@ export function createDocumentRuntime(
|
|
|
297
1045
|
this.dispatch({
|
|
298
1046
|
type: "comment.resolve",
|
|
299
1047
|
commentId,
|
|
300
|
-
resolvedBy:
|
|
1048
|
+
resolvedBy: defaultAuthorId ?? "unknown",
|
|
301
1049
|
origin: createOrigin("api", clock()),
|
|
302
1050
|
});
|
|
303
1051
|
},
|
|
@@ -313,7 +1061,7 @@ export function createDocumentRuntime(
|
|
|
313
1061
|
type: "comment.add-reply",
|
|
314
1062
|
commentId,
|
|
315
1063
|
body,
|
|
316
|
-
authorId: authorId ??
|
|
1064
|
+
authorId: authorId ?? defaultAuthorId,
|
|
317
1065
|
origin: createOrigin("api", clock()),
|
|
318
1066
|
});
|
|
319
1067
|
},
|
|
@@ -351,13 +1099,132 @@ export function createDocumentRuntime(
|
|
|
351
1099
|
origin: createOrigin("api", clock()),
|
|
352
1100
|
});
|
|
353
1101
|
},
|
|
354
|
-
|
|
1102
|
+
openStory(target) {
|
|
1103
|
+
const normalizedTarget =
|
|
1104
|
+
target.kind === "header" || target.kind === "footer"
|
|
1105
|
+
? normalizeHeaderFooterTarget(
|
|
1106
|
+
state.document,
|
|
1107
|
+
target,
|
|
1108
|
+
cachedRenderSnapshot.pageLayout?.sectionIndex,
|
|
1109
|
+
) ?? target
|
|
1110
|
+
: target;
|
|
1111
|
+
if (storyTargetsEqual(activeStory, normalizedTarget)) {
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
if (!isValidStoryTarget(state, normalizedTarget)) {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
switchActiveStory(normalizedTarget);
|
|
1118
|
+
return true;
|
|
1119
|
+
},
|
|
1120
|
+
closeStory() {
|
|
1121
|
+
if (activeStory.kind === "main") {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
switchActiveStory(MAIN_STORY_TARGET);
|
|
1125
|
+
},
|
|
1126
|
+
getActiveStory() {
|
|
1127
|
+
return activeStory;
|
|
1128
|
+
},
|
|
1129
|
+
getViewState() {
|
|
1130
|
+
return getCachedViewStateSnapshot();
|
|
1131
|
+
},
|
|
1132
|
+
setViewMode(mode) {
|
|
1133
|
+
viewState = applyViewMode(viewState, mode);
|
|
1134
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1135
|
+
for (const listener of listeners) {
|
|
1136
|
+
listener();
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
setDocumentMode(mode) {
|
|
1140
|
+
viewState = applyDocumentMode(viewState, mode);
|
|
1141
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1142
|
+
for (const listener of listeners) {
|
|
1143
|
+
listener();
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
getProtectionSnapshot() {
|
|
1147
|
+
return cachedRenderSnapshot.protectionSnapshot;
|
|
1148
|
+
},
|
|
1149
|
+
setWorkspaceMode(mode) {
|
|
1150
|
+
viewState = applyWorkspaceMode(viewState, mode);
|
|
1151
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1152
|
+
for (const listener of listeners) {
|
|
1153
|
+
listener();
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
setZoom(level) {
|
|
1157
|
+
viewState = applyZoomLevel(viewState, level);
|
|
1158
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1159
|
+
for (const listener of listeners) {
|
|
1160
|
+
listener();
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
getPageLayoutSnapshot() {
|
|
1164
|
+
return getCachedPageLayoutSnapshot(state, activeStory);
|
|
1165
|
+
},
|
|
1166
|
+
getDocumentNavigationSnapshot() {
|
|
1167
|
+
return getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
1168
|
+
},
|
|
1169
|
+
getFieldSnapshot() {
|
|
1170
|
+
return buildFieldSnapshot(state.document);
|
|
1171
|
+
},
|
|
1172
|
+
updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult {
|
|
1173
|
+
const refreshed = refreshDocumentFields(
|
|
1174
|
+
state.document,
|
|
1175
|
+
state.selection.head,
|
|
1176
|
+
activeStory,
|
|
1177
|
+
options,
|
|
1178
|
+
);
|
|
1179
|
+
if (refreshed.changed) {
|
|
1180
|
+
this.dispatch({
|
|
1181
|
+
type: "document.replace",
|
|
1182
|
+
document: refreshed.document,
|
|
1183
|
+
mapping: createEmptyMapping(),
|
|
1184
|
+
protectionSelection: refreshed.protectionSelection,
|
|
1185
|
+
origin: createOrigin("api", clock()),
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const snapshot = buildFieldSnapshot(refreshed.document);
|
|
1189
|
+
return {
|
|
1190
|
+
totalCount: snapshot.totalCount,
|
|
1191
|
+
updatedCount: refreshed.updatedCount,
|
|
1192
|
+
preserveOnlyCount: snapshot.preserveOnlyCount,
|
|
1193
|
+
};
|
|
1194
|
+
},
|
|
1195
|
+
updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult {
|
|
1196
|
+
const refreshed = refreshDocumentTableOfContents(
|
|
1197
|
+
state.document,
|
|
1198
|
+
state.selection.head,
|
|
1199
|
+
activeStory,
|
|
1200
|
+
options,
|
|
1201
|
+
);
|
|
1202
|
+
if (refreshed.changed) {
|
|
1203
|
+
this.dispatch({
|
|
1204
|
+
type: "document.replace",
|
|
1205
|
+
document: refreshed.document,
|
|
1206
|
+
mapping: createEmptyMapping(),
|
|
1207
|
+
protectionSelection: refreshed.protectionSelection,
|
|
1208
|
+
origin: createOrigin("api", clock()),
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return refreshed.result;
|
|
1212
|
+
},
|
|
1213
|
+
getSessionState() {
|
|
355
1214
|
const compatibility = createDerivedCompatibility(state);
|
|
356
|
-
return
|
|
357
|
-
|
|
1215
|
+
return editorSessionStateFromPersistedSnapshot(
|
|
1216
|
+
createPersistedEditorSnapshot(state, {
|
|
1217
|
+
editorBuild,
|
|
1218
|
+
savedAt: clock(),
|
|
1219
|
+
compatibility,
|
|
1220
|
+
protectionSnapshot,
|
|
1221
|
+
}) as unknown as PersistedEditorSnapshot,
|
|
1222
|
+
);
|
|
1223
|
+
},
|
|
1224
|
+
getPersistedSnapshot() {
|
|
1225
|
+
return persistedSnapshotFromEditorSessionState(this.getSessionState(), {
|
|
358
1226
|
savedAt: clock(),
|
|
359
|
-
|
|
360
|
-
}) as unknown as PersistedEditorSnapshot;
|
|
1227
|
+
});
|
|
361
1228
|
},
|
|
362
1229
|
getCompatibilityReport() {
|
|
363
1230
|
return toPublicCompatibilityReport(createDerivedCompatibility(state));
|
|
@@ -381,14 +1248,7 @@ export function createDocumentRuntime(
|
|
|
381
1248
|
throw new Error(error.message);
|
|
382
1249
|
}
|
|
383
1250
|
|
|
384
|
-
const result = await options.exportDocx(
|
|
385
|
-
createPersistedEditorSnapshot(state, {
|
|
386
|
-
editorBuild,
|
|
387
|
-
savedAt: clock(),
|
|
388
|
-
compatibility: createDerivedCompatibility(state),
|
|
389
|
-
}) as unknown as PersistedEditorSnapshot,
|
|
390
|
-
exportOptions,
|
|
391
|
-
);
|
|
1251
|
+
const result = await options.exportDocx(this.getSessionState(), exportOptions);
|
|
392
1252
|
|
|
393
1253
|
emit({
|
|
394
1254
|
type: "export_completed",
|
|
@@ -398,6 +1258,75 @@ export function createDocumentRuntime(
|
|
|
398
1258
|
|
|
399
1259
|
return result;
|
|
400
1260
|
},
|
|
1261
|
+
setWorkflowOverlay(overlay) {
|
|
1262
|
+
workflowOverlay = structuredClone(overlay);
|
|
1263
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1264
|
+
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
1265
|
+
emit({
|
|
1266
|
+
type: "workflow_overlay_changed",
|
|
1267
|
+
documentId: state.documentId,
|
|
1268
|
+
snapshot,
|
|
1269
|
+
});
|
|
1270
|
+
if (workflowOverlay.activeWorkItemId !== undefined) {
|
|
1271
|
+
emit({
|
|
1272
|
+
type: "workflow_active_work_item_changed",
|
|
1273
|
+
documentId: state.documentId,
|
|
1274
|
+
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
for (const listener of listeners) {
|
|
1278
|
+
listener();
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
clearWorkflowOverlay() {
|
|
1282
|
+
workflowOverlay = null;
|
|
1283
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1284
|
+
emit({
|
|
1285
|
+
type: "workflow_active_work_item_changed",
|
|
1286
|
+
documentId: state.documentId,
|
|
1287
|
+
activeWorkItemId: null,
|
|
1288
|
+
});
|
|
1289
|
+
emit({
|
|
1290
|
+
type: "workflow_overlay_changed",
|
|
1291
|
+
documentId: state.documentId,
|
|
1292
|
+
snapshot: {
|
|
1293
|
+
overlayPresent: false,
|
|
1294
|
+
activeWorkItemId: null,
|
|
1295
|
+
scopes: [],
|
|
1296
|
+
candidates: [],
|
|
1297
|
+
blockedReasons: [],
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
for (const listener of listeners) {
|
|
1301
|
+
listener();
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
getWorkflowScopeSnapshot() {
|
|
1305
|
+
return getCachedWorkflowScopeSnapshot();
|
|
1306
|
+
},
|
|
1307
|
+
getInteractionGuardSnapshot() {
|
|
1308
|
+
return getCachedInteractionGuardSnapshot();
|
|
1309
|
+
},
|
|
1310
|
+
getWorkflowMarkupSnapshot() {
|
|
1311
|
+
return collectWorkflowMarkupSnapshot({
|
|
1312
|
+
renderSnapshot: this.getRenderSnapshot(),
|
|
1313
|
+
fieldSnapshot: this.getFieldSnapshot(),
|
|
1314
|
+
protectionSnapshot,
|
|
1315
|
+
preservation: state.document.preservation,
|
|
1316
|
+
});
|
|
1317
|
+
},
|
|
1318
|
+
getWorkflowCandidateRanges(options) {
|
|
1319
|
+
return deriveWorkflowCandidateRangesFromMarkup(this.getWorkflowMarkupSnapshot(), options);
|
|
1320
|
+
},
|
|
1321
|
+
replaceWorkflowMarkupText(markupId, text) {
|
|
1322
|
+
const target = this
|
|
1323
|
+
.getWorkflowMarkupSnapshot()
|
|
1324
|
+
.items.find((item) => item.markupId === markupId);
|
|
1325
|
+
if (!target || target.anchor.kind === "detached") {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this.replaceText(text, target.anchor);
|
|
1329
|
+
},
|
|
401
1330
|
};
|
|
402
1331
|
|
|
403
1332
|
function applyHistory(direction: "undo" | "redo"): void {
|
|
@@ -415,7 +1344,8 @@ export function createDocumentRuntime(
|
|
|
415
1344
|
// Undo/redo changes the document — must mint a new revisionToken so
|
|
416
1345
|
// autosave/export checkpoint dedup treats it as fresh content.
|
|
417
1346
|
state = finalizeState(target, true, clock());
|
|
418
|
-
|
|
1347
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
1348
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
419
1349
|
notify(previous, state, {
|
|
420
1350
|
nextState: state,
|
|
421
1351
|
mapping: { steps: [] },
|
|
@@ -436,8 +1366,10 @@ export function createDocumentRuntime(
|
|
|
436
1366
|
history.future = [];
|
|
437
1367
|
}
|
|
438
1368
|
|
|
1369
|
+
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
439
1370
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
440
|
-
|
|
1371
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
1372
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
441
1373
|
notify(previous, state, transaction);
|
|
442
1374
|
}
|
|
443
1375
|
|
|
@@ -458,7 +1390,7 @@ export function createDocumentRuntime(
|
|
|
458
1390
|
emit({
|
|
459
1391
|
type: "selection_changed",
|
|
460
1392
|
documentId: next.documentId,
|
|
461
|
-
selection: toPublicSelectionSnapshot(next.selection),
|
|
1393
|
+
selection: toPublicSelectionSnapshot(next.selection, activeStory),
|
|
462
1394
|
});
|
|
463
1395
|
}
|
|
464
1396
|
|
|
@@ -519,7 +1451,7 @@ export function createDocumentRuntime(
|
|
|
519
1451
|
}
|
|
520
1452
|
}
|
|
521
1453
|
|
|
522
|
-
function emit(event:
|
|
1454
|
+
function emit(event: DocumentRuntimeEvent): void {
|
|
523
1455
|
options.onEvent?.(event);
|
|
524
1456
|
for (const listener of eventListeners) {
|
|
525
1457
|
listener(event);
|
|
@@ -533,7 +1465,8 @@ export function createDocumentRuntime(
|
|
|
533
1465
|
fatalError: error.isFatal ? error : state.fatalError,
|
|
534
1466
|
};
|
|
535
1467
|
state = nextState;
|
|
536
|
-
|
|
1468
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
1469
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
537
1470
|
const publicError = toPublicError(error);
|
|
538
1471
|
options.onError?.(publicError);
|
|
539
1472
|
emit({
|
|
@@ -545,6 +1478,39 @@ export function createDocumentRuntime(
|
|
|
545
1478
|
listener();
|
|
546
1479
|
}
|
|
547
1480
|
}
|
|
1481
|
+
|
|
1482
|
+
function switchActiveStory(target: EditorStoryTarget): void {
|
|
1483
|
+
const previousStory = activeStory;
|
|
1484
|
+
const previousSelection = state.selection;
|
|
1485
|
+
storySelections.set(storyTargetKey(previousStory), previousSelection);
|
|
1486
|
+
|
|
1487
|
+
const restoredSelection =
|
|
1488
|
+
storySelections.get(storyTargetKey(target)) ?? createSelectionSnapshot(0, 0);
|
|
1489
|
+
activeStory = target;
|
|
1490
|
+
state = {
|
|
1491
|
+
...state,
|
|
1492
|
+
selection: restoredSelection,
|
|
1493
|
+
};
|
|
1494
|
+
storySelections.set(storyTargetKey(target), restoredSelection);
|
|
1495
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1496
|
+
|
|
1497
|
+
if (selectionChanged(previousSelection, restoredSelection)) {
|
|
1498
|
+
emit({
|
|
1499
|
+
type: "selection_changed",
|
|
1500
|
+
documentId: state.documentId,
|
|
1501
|
+
selection: toPublicSelectionSnapshot(restoredSelection, activeStory),
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
emit({
|
|
1506
|
+
type: "story_changed",
|
|
1507
|
+
documentId: state.documentId,
|
|
1508
|
+
activeStory,
|
|
1509
|
+
});
|
|
1510
|
+
for (const listener of listeners) {
|
|
1511
|
+
listener();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
548
1514
|
}
|
|
549
1515
|
|
|
550
1516
|
function createSessionId(documentId: string, timestamp: string): string {
|
|
@@ -620,47 +1586,6 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
620
1586
|
};
|
|
621
1587
|
}
|
|
622
1588
|
|
|
623
|
-
function createPublicRenderSnapshot(
|
|
624
|
-
state: EditorState,
|
|
625
|
-
history: HistoryState,
|
|
626
|
-
): RuntimeRenderSnapshot {
|
|
627
|
-
const compatibility = createDerivedCompatibility(state);
|
|
628
|
-
const surface = createEditorSurfaceSnapshot(state.document, state.selection);
|
|
629
|
-
const comments = toPublicCommentSidebarSnapshot(state);
|
|
630
|
-
const trackedChanges = toPublicTrackedChangesSnapshot(state, surface.plainText);
|
|
631
|
-
|
|
632
|
-
return {
|
|
633
|
-
documentId: state.documentId,
|
|
634
|
-
sessionId: state.sessionId,
|
|
635
|
-
sourceLabel: state.sourceLabel,
|
|
636
|
-
revisionToken: state.revisionToken,
|
|
637
|
-
isReady: state.phase === "ready",
|
|
638
|
-
isDirty: state.isDirty,
|
|
639
|
-
readOnly: state.readOnly,
|
|
640
|
-
selection: toPublicSelectionSnapshot(state.selection),
|
|
641
|
-
documentStats: toPublicDocumentStats(state),
|
|
642
|
-
comments,
|
|
643
|
-
trackedChanges,
|
|
644
|
-
compatibility: {
|
|
645
|
-
blockExport: compatibility.blockExport,
|
|
646
|
-
blockExportReasons: listBlockExportReasons(compatibility),
|
|
647
|
-
warningCount: compatibility.warnings.length,
|
|
648
|
-
errorCount: compatibility.errors.length,
|
|
649
|
-
featureEntries: compatibility.featureEntries.map((entry) =>
|
|
650
|
-
toPublicCompatibilityFeatureEntry(entry),
|
|
651
|
-
),
|
|
652
|
-
},
|
|
653
|
-
warnings: state.warnings.map((warning) => toPublicWarning(warning)),
|
|
654
|
-
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
655
|
-
commandState: {
|
|
656
|
-
canUndo: history.past.length > 0,
|
|
657
|
-
canRedo: history.future.length > 0,
|
|
658
|
-
readOnly: state.readOnly,
|
|
659
|
-
},
|
|
660
|
-
surface,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
1589
|
function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
665
1590
|
const stats = deriveDocumentStats(state);
|
|
666
1591
|
return {
|
|
@@ -673,12 +1598,14 @@ function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
|
673
1598
|
|
|
674
1599
|
function toPublicSelectionSnapshot(
|
|
675
1600
|
selection: EditorState["selection"],
|
|
1601
|
+
storyTarget?: EditorStoryTarget,
|
|
676
1602
|
): SelectionSnapshot {
|
|
677
1603
|
return {
|
|
678
1604
|
anchor: selection.anchor,
|
|
679
1605
|
head: selection.head,
|
|
680
1606
|
isCollapsed: selection.isCollapsed,
|
|
681
1607
|
activeRange: toPublicAnchorProjection(selection.activeRange),
|
|
1608
|
+
...(storyTarget && storyTarget.kind !== "main" ? { storyTarget } : {}),
|
|
682
1609
|
};
|
|
683
1610
|
}
|
|
684
1611
|
|
|
@@ -1026,6 +1953,755 @@ function summarizeRevisionExcerpt(
|
|
|
1026
1953
|
return collapsed.length > 96 ? `${collapsed.slice(0, 93)}...` : collapsed;
|
|
1027
1954
|
}
|
|
1028
1955
|
|
|
1956
|
+
function isValidStoryTarget(
|
|
1957
|
+
state: EditorState,
|
|
1958
|
+
target: EditorStoryTarget,
|
|
1959
|
+
): boolean {
|
|
1960
|
+
if (target.kind === "main") return true;
|
|
1961
|
+
const subParts = state.document.subParts;
|
|
1962
|
+
if (!subParts) return false;
|
|
1963
|
+
|
|
1964
|
+
switch (target.kind) {
|
|
1965
|
+
case "header":
|
|
1966
|
+
return Boolean(normalizeHeaderFooterTarget(state.document, target));
|
|
1967
|
+
case "footer":
|
|
1968
|
+
return Boolean(normalizeHeaderFooterTarget(state.document, target));
|
|
1969
|
+
case "footnote":
|
|
1970
|
+
return Boolean(subParts.footnoteCollection?.footnotes?.[target.noteId]);
|
|
1971
|
+
case "endnote":
|
|
1972
|
+
return Boolean(subParts.footnoteCollection?.endnotes?.[target.noteId]);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function derivePageLayoutSnapshot(
|
|
1977
|
+
state: EditorState,
|
|
1978
|
+
activeStory: EditorStoryTarget,
|
|
1979
|
+
storySelections?: ReadonlyMap<string, EditorState["selection"]>,
|
|
1980
|
+
): PageLayoutSnapshot | null {
|
|
1981
|
+
const subParts = state.document.subParts;
|
|
1982
|
+
const sections = buildResolvedSections(state.document);
|
|
1983
|
+
if (!subParts && sections.length === 0) {
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const activeSection = resolveActiveSection(
|
|
1988
|
+
state,
|
|
1989
|
+
activeStory,
|
|
1990
|
+
sections,
|
|
1991
|
+
storySelections,
|
|
1992
|
+
);
|
|
1993
|
+
return buildPageLayoutSnapshot(
|
|
1994
|
+
activeSection?.index ?? 0,
|
|
1995
|
+
activeSection?.properties ?? subParts?.finalSectionProperties,
|
|
1996
|
+
subParts,
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1029
2000
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1030
2001
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1031
2002
|
}
|
|
2003
|
+
|
|
2004
|
+
/** Commands that are safe in viewing mode (no document mutation). */
|
|
2005
|
+
const NON_MUTATION_COMMANDS = new Set([
|
|
2006
|
+
"selection.set",
|
|
2007
|
+
"runtime.set-read-only",
|
|
2008
|
+
"runtime.focus",
|
|
2009
|
+
"warning.add",
|
|
2010
|
+
"warning.clear",
|
|
2011
|
+
"comment.open",
|
|
2012
|
+
]);
|
|
2013
|
+
|
|
2014
|
+
function isMutationCommand(command: EditorCommand): boolean {
|
|
2015
|
+
return !NON_MUTATION_COMMANDS.has(command.type);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// ── Field snapshot helpers ──────────────────────────────────────────────────
|
|
2019
|
+
|
|
2020
|
+
function buildFieldSnapshot(document: CanonicalDocumentEnvelope): FieldSnapshot {
|
|
2021
|
+
const entries: FieldEntrySnapshot[] = [];
|
|
2022
|
+
let index = 0;
|
|
2023
|
+
for (const block of document.content.children) {
|
|
2024
|
+
index = collectFieldsFromBlock(block, entries, index);
|
|
2025
|
+
}
|
|
2026
|
+
index = collectFieldsFromSubParts(document.subParts, entries, index);
|
|
2027
|
+
const supportedCount = entries.filter((e) => e.supported).length;
|
|
2028
|
+
return {
|
|
2029
|
+
totalCount: entries.length,
|
|
2030
|
+
supportedCount,
|
|
2031
|
+
preserveOnlyCount: entries.length - supportedCount,
|
|
2032
|
+
fields: entries,
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function collectFieldsFromBlock(
|
|
2037
|
+
block: BlockNode,
|
|
2038
|
+
entries: FieldEntrySnapshot[],
|
|
2039
|
+
index: number,
|
|
2040
|
+
): number {
|
|
2041
|
+
if (block.type === "paragraph") {
|
|
2042
|
+
for (const child of block.children) {
|
|
2043
|
+
index = collectFieldsFromInline(child, entries, index);
|
|
2044
|
+
}
|
|
2045
|
+
} else if (block.type === "table") {
|
|
2046
|
+
for (const row of block.rows) {
|
|
2047
|
+
for (const cell of row.cells) {
|
|
2048
|
+
for (const child of cell.children) {
|
|
2049
|
+
index = collectFieldsFromBlock(child, entries, index);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2054
|
+
for (const child of block.children) {
|
|
2055
|
+
index = collectFieldsFromBlock(child, entries, index);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return index;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function collectFieldsFromInline(
|
|
2062
|
+
node: InlineNode,
|
|
2063
|
+
entries: FieldEntrySnapshot[],
|
|
2064
|
+
index: number,
|
|
2065
|
+
): number {
|
|
2066
|
+
if (node.type === "field") {
|
|
2067
|
+
const fieldFamily = node.fieldFamily ?? "UNKNOWN";
|
|
2068
|
+
const supported = isSupportedFieldFamily(fieldFamily);
|
|
2069
|
+
const displayText = extractFieldDisplayText(node);
|
|
2070
|
+
entries.push({
|
|
2071
|
+
index,
|
|
2072
|
+
fieldFamily,
|
|
2073
|
+
supported,
|
|
2074
|
+
instruction: node.instruction,
|
|
2075
|
+
fieldTarget: node.fieldTarget,
|
|
2076
|
+
refreshStatus: node.refreshStatus ?? (supported ? "stale" : "preserve-only"),
|
|
2077
|
+
displayText,
|
|
2078
|
+
});
|
|
2079
|
+
index++;
|
|
2080
|
+
// Also walk children — fields can contain nested fields
|
|
2081
|
+
for (const child of node.children) {
|
|
2082
|
+
index = collectFieldsFromInline(child, entries, index);
|
|
2083
|
+
}
|
|
2084
|
+
} else if (node.type === "hyperlink") {
|
|
2085
|
+
for (const child of node.children) {
|
|
2086
|
+
index = collectFieldsFromInline(child, entries, index);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return index;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function extractFieldDisplayText(field: FieldNode): string {
|
|
2093
|
+
return flattenInlineDisplayText(field.children);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function flattenInlineDisplayText(children: readonly InlineNode[]): string {
|
|
2097
|
+
return children
|
|
2098
|
+
.map((child) => {
|
|
2099
|
+
switch (child.type) {
|
|
2100
|
+
case "text":
|
|
2101
|
+
return child.text;
|
|
2102
|
+
case "tab":
|
|
2103
|
+
return "\t";
|
|
2104
|
+
case "hard_break":
|
|
2105
|
+
case "column_break":
|
|
2106
|
+
return "\n";
|
|
2107
|
+
case "hyperlink":
|
|
2108
|
+
case "field":
|
|
2109
|
+
return flattenInlineDisplayText(child.children);
|
|
2110
|
+
case "footnote_ref":
|
|
2111
|
+
return child.noteId;
|
|
2112
|
+
default:
|
|
2113
|
+
return "";
|
|
2114
|
+
}
|
|
2115
|
+
})
|
|
2116
|
+
.join("");
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function refreshDocumentFields(
|
|
2120
|
+
document: CanonicalDocumentEnvelope,
|
|
2121
|
+
selectionHead: number,
|
|
2122
|
+
activeStory: EditorStoryTarget,
|
|
2123
|
+
options?: UpdateFieldsOptions,
|
|
2124
|
+
): {
|
|
2125
|
+
document: CanonicalDocumentEnvelope;
|
|
2126
|
+
updatedCount: number;
|
|
2127
|
+
changed: boolean;
|
|
2128
|
+
protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
2129
|
+
} {
|
|
2130
|
+
const supportedOnly = options?.supportedOnly ?? true;
|
|
2131
|
+
const bookmarkMap = buildBookmarkNameMap(document);
|
|
2132
|
+
const paragraphs = collectParagraphContexts(document.content.children);
|
|
2133
|
+
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
2134
|
+
let updatedCount = 0;
|
|
2135
|
+
let changed = false;
|
|
2136
|
+
let changedFrom: number | undefined;
|
|
2137
|
+
let changedTo: number | undefined;
|
|
2138
|
+
|
|
2139
|
+
const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
|
|
2140
|
+
if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
|
|
2141
|
+
return field;
|
|
2142
|
+
}
|
|
2143
|
+
if (supportedOnly && field.fieldFamily === "TOC") {
|
|
2144
|
+
return field;
|
|
2145
|
+
}
|
|
2146
|
+
const display = resolveSupportedFieldDisplay(
|
|
2147
|
+
field,
|
|
2148
|
+
document,
|
|
2149
|
+
bookmarkMap,
|
|
2150
|
+
paragraphs,
|
|
2151
|
+
navigation,
|
|
2152
|
+
);
|
|
2153
|
+
if (!display) {
|
|
2154
|
+
return field;
|
|
2155
|
+
}
|
|
2156
|
+
updatedCount += 1;
|
|
2157
|
+
const nextField: FieldNode = {
|
|
2158
|
+
...field,
|
|
2159
|
+
children: buildInlineNodesFromDisplayText(display.displayText),
|
|
2160
|
+
refreshStatus: display.refreshStatus,
|
|
2161
|
+
};
|
|
2162
|
+
if (
|
|
2163
|
+
nextField.refreshStatus !== field.refreshStatus ||
|
|
2164
|
+
flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)
|
|
2165
|
+
) {
|
|
2166
|
+
changed = true;
|
|
2167
|
+
changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
|
|
2168
|
+
changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
|
|
2169
|
+
}
|
|
2170
|
+
return nextField;
|
|
2171
|
+
}).blocks;
|
|
2172
|
+
if (!changed) {
|
|
2173
|
+
return { document, updatedCount, changed: false };
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
2177
|
+
...document,
|
|
2178
|
+
content: {
|
|
2179
|
+
...document.content,
|
|
2180
|
+
children: nextChildren,
|
|
2181
|
+
},
|
|
2182
|
+
};
|
|
2183
|
+
const nextRegistry = buildFieldRegistry({
|
|
2184
|
+
content: nextDocument.content,
|
|
2185
|
+
styles: nextDocument.styles,
|
|
2186
|
+
subParts: nextDocument.subParts,
|
|
2187
|
+
});
|
|
2188
|
+
nextDocument.fieldRegistry = nextRegistry;
|
|
2189
|
+
let protectionSelection:
|
|
2190
|
+
| import("../core/state/editor-state.ts").SelectionSnapshot
|
|
2191
|
+
| undefined;
|
|
2192
|
+
if (changedFrom !== undefined && changedTo !== undefined) {
|
|
2193
|
+
protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
|
|
2194
|
+
}
|
|
2195
|
+
return {
|
|
2196
|
+
document: nextDocument,
|
|
2197
|
+
updatedCount,
|
|
2198
|
+
changed: true,
|
|
2199
|
+
...(protectionSelection ? { protectionSelection } : {}),
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function refreshDocumentTableOfContents(
|
|
2204
|
+
document: CanonicalDocumentEnvelope,
|
|
2205
|
+
selectionHead: number,
|
|
2206
|
+
activeStory: EditorStoryTarget,
|
|
2207
|
+
options?: TocRefreshOptions,
|
|
2208
|
+
): {
|
|
2209
|
+
document: CanonicalDocumentEnvelope;
|
|
2210
|
+
result: TocRefreshResult;
|
|
2211
|
+
changed: boolean;
|
|
2212
|
+
protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
2213
|
+
} {
|
|
2214
|
+
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
2215
|
+
let changed = false;
|
|
2216
|
+
let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
|
|
2217
|
+
let changedFrom: number | undefined;
|
|
2218
|
+
let changedTo: number | undefined;
|
|
2219
|
+
const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
|
|
2220
|
+
if (field.fieldFamily !== "TOC") {
|
|
2221
|
+
return field;
|
|
2222
|
+
}
|
|
2223
|
+
const levelRange = options?.maxLevel
|
|
2224
|
+
? { from: 1, to: options.maxLevel }
|
|
2225
|
+
: parseTocLevelRange(field.instruction);
|
|
2226
|
+
const entries = navigation.headings
|
|
2227
|
+
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
2228
|
+
.map((heading) => ({
|
|
2229
|
+
level: heading.level,
|
|
2230
|
+
text: heading.text,
|
|
2231
|
+
pageIndex: heading.pageIndex,
|
|
2232
|
+
}));
|
|
2233
|
+
if (resultEntries.length === 0) {
|
|
2234
|
+
resultEntries = entries;
|
|
2235
|
+
}
|
|
2236
|
+
const nextField: FieldNode = {
|
|
2237
|
+
...field,
|
|
2238
|
+
children: buildTocInlineNodes(entries),
|
|
2239
|
+
refreshStatus: "current",
|
|
2240
|
+
};
|
|
2241
|
+
if (flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)) {
|
|
2242
|
+
changed = true;
|
|
2243
|
+
changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
|
|
2244
|
+
changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
|
|
2245
|
+
}
|
|
2246
|
+
return nextField;
|
|
2247
|
+
}).blocks;
|
|
2248
|
+
if (!changed) {
|
|
2249
|
+
return {
|
|
2250
|
+
document,
|
|
2251
|
+
result: { entryCount: resultEntries.length, entries: resultEntries },
|
|
2252
|
+
changed: false,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
2257
|
+
...document,
|
|
2258
|
+
content: {
|
|
2259
|
+
...document.content,
|
|
2260
|
+
children: nextChildren,
|
|
2261
|
+
},
|
|
2262
|
+
};
|
|
2263
|
+
const nextRegistry = buildFieldRegistry({
|
|
2264
|
+
content: nextDocument.content,
|
|
2265
|
+
styles: nextDocument.styles,
|
|
2266
|
+
subParts: nextDocument.subParts,
|
|
2267
|
+
});
|
|
2268
|
+
nextDocument.fieldRegistry = nextRegistry.tocStructure
|
|
2269
|
+
? {
|
|
2270
|
+
...nextRegistry,
|
|
2271
|
+
tocStructure: {
|
|
2272
|
+
...nextRegistry.tocStructure,
|
|
2273
|
+
status: "current",
|
|
2274
|
+
},
|
|
2275
|
+
}
|
|
2276
|
+
: nextRegistry;
|
|
2277
|
+
let protectionSelection:
|
|
2278
|
+
| import("../core/state/editor-state.ts").SelectionSnapshot
|
|
2279
|
+
| undefined;
|
|
2280
|
+
if (changedFrom !== undefined && changedTo !== undefined) {
|
|
2281
|
+
protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
return {
|
|
2285
|
+
document: nextDocument,
|
|
2286
|
+
result: { entryCount: resultEntries.length, entries: resultEntries },
|
|
2287
|
+
changed: true,
|
|
2288
|
+
...(protectionSelection ? { protectionSelection } : {}),
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function refreshBlocksWithCursor(
|
|
2293
|
+
blocks: readonly BlockNode[],
|
|
2294
|
+
visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
|
|
2295
|
+
cursor = 0,
|
|
2296
|
+
previousParagraph = false,
|
|
2297
|
+
): {
|
|
2298
|
+
blocks: BlockNode[];
|
|
2299
|
+
cursor: number;
|
|
2300
|
+
previousParagraph: boolean;
|
|
2301
|
+
} {
|
|
2302
|
+
const nextBlocks = blocks.map((block) => {
|
|
2303
|
+
if (block.type === "paragraph") {
|
|
2304
|
+
const paragraphStart = previousParagraph ? cursor + 1 : cursor;
|
|
2305
|
+
const refreshedChildren = refreshInlineNodesWithCursor(
|
|
2306
|
+
block.children,
|
|
2307
|
+
visitField,
|
|
2308
|
+
paragraphStart,
|
|
2309
|
+
);
|
|
2310
|
+
cursor = paragraphStart + refreshedChildren.cursor;
|
|
2311
|
+
previousParagraph = true;
|
|
2312
|
+
return {
|
|
2313
|
+
...block,
|
|
2314
|
+
children: refreshedChildren.nodes,
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
if (block.type === "table") {
|
|
2318
|
+
cursor += 1;
|
|
2319
|
+
previousParagraph = false;
|
|
2320
|
+
return {
|
|
2321
|
+
...block,
|
|
2322
|
+
rows: block.rows.map((row) => ({
|
|
2323
|
+
...row,
|
|
2324
|
+
cells: row.cells.map((cell) => ({
|
|
2325
|
+
...cell,
|
|
2326
|
+
children: (() => {
|
|
2327
|
+
const refreshed = refreshBlocksWithCursor(cell.children, visitField, cursor, false);
|
|
2328
|
+
cursor = refreshed.cursor;
|
|
2329
|
+
return refreshed.blocks;
|
|
2330
|
+
})(),
|
|
2331
|
+
})),
|
|
2332
|
+
})),
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2336
|
+
const refreshed = refreshBlocksWithCursor(
|
|
2337
|
+
block.children,
|
|
2338
|
+
visitField,
|
|
2339
|
+
cursor,
|
|
2340
|
+
previousParagraph,
|
|
2341
|
+
);
|
|
2342
|
+
cursor = refreshed.cursor;
|
|
2343
|
+
previousParagraph = refreshed.previousParagraph;
|
|
2344
|
+
return {
|
|
2345
|
+
...block,
|
|
2346
|
+
children: refreshed.blocks,
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
cursor += 1;
|
|
2350
|
+
previousParagraph = false;
|
|
2351
|
+
return block;
|
|
2352
|
+
});
|
|
2353
|
+
return { blocks: nextBlocks, cursor, previousParagraph };
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function refreshInlineNodesWithCursor(
|
|
2357
|
+
nodes: readonly InlineNode[],
|
|
2358
|
+
visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
|
|
2359
|
+
cursor = 0,
|
|
2360
|
+
): {
|
|
2361
|
+
nodes: InlineNode[];
|
|
2362
|
+
cursor: number;
|
|
2363
|
+
} {
|
|
2364
|
+
const nextNodes = nodes.map((node) => {
|
|
2365
|
+
if (node.type === "field") {
|
|
2366
|
+
const fieldStart = cursor;
|
|
2367
|
+
const refreshedChildren = refreshInlineNodesWithCursor(node.children, visitField, cursor);
|
|
2368
|
+
const fieldLength = measureInlineNodes(node.children);
|
|
2369
|
+
cursor = fieldStart + fieldLength;
|
|
2370
|
+
return visitField({
|
|
2371
|
+
...node,
|
|
2372
|
+
children: refreshedChildren.nodes,
|
|
2373
|
+
}, {
|
|
2374
|
+
from: fieldStart,
|
|
2375
|
+
to: fieldStart + fieldLength,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
if (node.type === "hyperlink") {
|
|
2379
|
+
cursor += measureInlineNodes(node.children);
|
|
2380
|
+
return {
|
|
2381
|
+
...node,
|
|
2382
|
+
// Hyperlinks only contain text-like children in the canonical model.
|
|
2383
|
+
children: [...node.children],
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
cursor += measureInlineNode(node);
|
|
2387
|
+
return node;
|
|
2388
|
+
});
|
|
2389
|
+
return { nodes: nextNodes, cursor };
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
2393
|
+
if (text.length === 0) {
|
|
2394
|
+
return [];
|
|
2395
|
+
}
|
|
2396
|
+
const children: InlineNode[] = [];
|
|
2397
|
+
let buffer = "";
|
|
2398
|
+
const flushBuffer = () => {
|
|
2399
|
+
if (buffer.length > 0) {
|
|
2400
|
+
children.push({ type: "text", text: buffer });
|
|
2401
|
+
buffer = "";
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
for (const character of text) {
|
|
2405
|
+
if (character === "\t") {
|
|
2406
|
+
flushBuffer();
|
|
2407
|
+
children.push({ type: "tab" });
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (character === "\n") {
|
|
2411
|
+
flushBuffer();
|
|
2412
|
+
children.push({ type: "hard_break" });
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
buffer += character;
|
|
2416
|
+
}
|
|
2417
|
+
flushBuffer();
|
|
2418
|
+
return children;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function buildTocInlineNodes(
|
|
2422
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
2423
|
+
): InlineNode[] {
|
|
2424
|
+
const children: InlineNode[] = [];
|
|
2425
|
+
entries.forEach((entry, index) => {
|
|
2426
|
+
children.push({ type: "text", text: entry.text });
|
|
2427
|
+
children.push({ type: "tab" });
|
|
2428
|
+
children.push({ type: "text", text: String(entry.pageIndex + 1) });
|
|
2429
|
+
if (index < entries.length - 1) {
|
|
2430
|
+
children.push({ type: "hard_break" });
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
return children;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function collectFieldsFromSubParts(
|
|
2437
|
+
subParts: SubPartsCatalog | undefined,
|
|
2438
|
+
entries: FieldEntrySnapshot[],
|
|
2439
|
+
index: number,
|
|
2440
|
+
): number {
|
|
2441
|
+
if (!subParts) {
|
|
2442
|
+
return index;
|
|
2443
|
+
}
|
|
2444
|
+
let nextIndex = index;
|
|
2445
|
+
for (const header of subParts.headers) {
|
|
2446
|
+
for (const block of header.blocks) {
|
|
2447
|
+
nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
for (const footer of subParts.footers) {
|
|
2451
|
+
for (const block of footer.blocks) {
|
|
2452
|
+
nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (subParts.footnoteCollection) {
|
|
2456
|
+
for (const note of Object.values(subParts.footnoteCollection.footnotes)) {
|
|
2457
|
+
for (const block of note.blocks) {
|
|
2458
|
+
nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
for (const note of Object.values(subParts.footnoteCollection.endnotes)) {
|
|
2462
|
+
for (const block of note.blocks) {
|
|
2463
|
+
nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return nextIndex;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function resolveSupportedFieldDisplay(
|
|
2471
|
+
field: FieldNode,
|
|
2472
|
+
document: CanonicalDocumentEnvelope,
|
|
2473
|
+
bookmarkMap: Map<string, { bookmarkId: string; paragraphIndex: number }>,
|
|
2474
|
+
paragraphs: readonly ParagraphContext[],
|
|
2475
|
+
navigation: DocumentNavigationSnapshot,
|
|
2476
|
+
): { displayText: string; refreshStatus: FieldRefreshStatus } | undefined {
|
|
2477
|
+
if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
|
|
2478
|
+
return undefined;
|
|
2479
|
+
}
|
|
2480
|
+
if (!field.fieldTarget) {
|
|
2481
|
+
return field.fieldFamily === "TOC"
|
|
2482
|
+
? undefined
|
|
2483
|
+
: { displayText: "", refreshStatus: "unresolvable" };
|
|
2484
|
+
}
|
|
2485
|
+
if (field.fieldFamily === "REF") {
|
|
2486
|
+
const result = resolveRefFieldText(document, bookmarkMap, field.fieldTarget);
|
|
2487
|
+
return result
|
|
2488
|
+
? { displayText: result.text, refreshStatus: result.refreshStatus }
|
|
2489
|
+
: { displayText: "", refreshStatus: "unresolvable" };
|
|
2490
|
+
}
|
|
2491
|
+
const bookmark = bookmarkMap.get(field.fieldTarget);
|
|
2492
|
+
if (!bookmark) {
|
|
2493
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
2494
|
+
}
|
|
2495
|
+
if (field.fieldFamily === "PAGEREF") {
|
|
2496
|
+
const paragraph = paragraphs[bookmark.paragraphIndex];
|
|
2497
|
+
if (!paragraph) {
|
|
2498
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
2499
|
+
}
|
|
2500
|
+
const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
|
|
2501
|
+
return { displayText: String(pageIndex + 1), refreshStatus: "current" };
|
|
2502
|
+
}
|
|
2503
|
+
if (field.fieldFamily === "NOTEREF") {
|
|
2504
|
+
const paragraph = paragraphs[bookmark.paragraphIndex]?.paragraph;
|
|
2505
|
+
if (!paragraph) {
|
|
2506
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
2507
|
+
}
|
|
2508
|
+
const noteText = resolveNoteReferenceText(paragraph, bookmark.bookmarkId);
|
|
2509
|
+
return noteText
|
|
2510
|
+
? { displayText: noteText, refreshStatus: "current" }
|
|
2511
|
+
: { displayText: "", refreshStatus: "unresolvable" };
|
|
2512
|
+
}
|
|
2513
|
+
return undefined;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
interface ParagraphContext {
|
|
2517
|
+
paragraph: ParagraphNode;
|
|
2518
|
+
startOffset: number;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function collectParagraphContexts(blocks: readonly BlockNode[]): ParagraphContext[] {
|
|
2522
|
+
const paragraphs: ParagraphContext[] = [];
|
|
2523
|
+
collectParagraphContextsFromBlocks(blocks, paragraphs, 0, false);
|
|
2524
|
+
return paragraphs;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
function collectParagraphContextsFromBlocks(
|
|
2528
|
+
blocks: readonly BlockNode[],
|
|
2529
|
+
paragraphs: ParagraphContext[],
|
|
2530
|
+
cursor: number,
|
|
2531
|
+
previousParagraph: boolean,
|
|
2532
|
+
): { cursor: number; previousParagraph: boolean } {
|
|
2533
|
+
let nextCursor = cursor;
|
|
2534
|
+
let nextPreviousParagraph = previousParagraph;
|
|
2535
|
+
for (const block of blocks) {
|
|
2536
|
+
if (block.type === "paragraph") {
|
|
2537
|
+
if (nextPreviousParagraph) {
|
|
2538
|
+
nextCursor += 1;
|
|
2539
|
+
}
|
|
2540
|
+
paragraphs.push({ paragraph: block, startOffset: nextCursor });
|
|
2541
|
+
nextCursor += measureInlineNodes(block.children);
|
|
2542
|
+
nextPreviousParagraph = true;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
if (block.type === "table") {
|
|
2546
|
+
nextCursor += 1;
|
|
2547
|
+
nextPreviousParagraph = false;
|
|
2548
|
+
for (const row of block.rows) {
|
|
2549
|
+
for (const cell of row.cells) {
|
|
2550
|
+
const result = collectParagraphContextsFromBlocks(
|
|
2551
|
+
cell.children,
|
|
2552
|
+
paragraphs,
|
|
2553
|
+
nextCursor,
|
|
2554
|
+
false,
|
|
2555
|
+
);
|
|
2556
|
+
nextCursor = result.cursor;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2562
|
+
const result = collectParagraphContextsFromBlocks(
|
|
2563
|
+
block.children,
|
|
2564
|
+
paragraphs,
|
|
2565
|
+
nextCursor,
|
|
2566
|
+
nextPreviousParagraph,
|
|
2567
|
+
);
|
|
2568
|
+
nextCursor = result.cursor;
|
|
2569
|
+
nextPreviousParagraph = result.previousParagraph;
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
nextCursor += 1;
|
|
2573
|
+
nextPreviousParagraph = false;
|
|
2574
|
+
}
|
|
2575
|
+
return { cursor: nextCursor, previousParagraph: nextPreviousParagraph };
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function measureInlineNodes(nodes: readonly InlineNode[]): number {
|
|
2579
|
+
return nodes.reduce((size, node) => size + measureInlineNode(node), 0);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function measureInlineNode(node: InlineNode): number {
|
|
2583
|
+
switch (node.type) {
|
|
2584
|
+
case "text":
|
|
2585
|
+
return node.text.length;
|
|
2586
|
+
case "tab":
|
|
2587
|
+
case "hard_break":
|
|
2588
|
+
case "column_break":
|
|
2589
|
+
case "footnote_ref":
|
|
2590
|
+
case "image":
|
|
2591
|
+
case "opaque_inline":
|
|
2592
|
+
case "bookmark_start":
|
|
2593
|
+
case "bookmark_end":
|
|
2594
|
+
return 1;
|
|
2595
|
+
case "hyperlink":
|
|
2596
|
+
case "field":
|
|
2597
|
+
return measureInlineNodes(node.children);
|
|
2598
|
+
default:
|
|
2599
|
+
return 1;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function resolveNoteReferenceText(paragraph: ParagraphNode, bookmarkId: string): string | undefined {
|
|
2604
|
+
let inside = false;
|
|
2605
|
+
let sawBoundary = false;
|
|
2606
|
+
for (const child of paragraph.children) {
|
|
2607
|
+
if (child.type === "bookmark_start" && child.bookmarkId === bookmarkId) {
|
|
2608
|
+
inside = true;
|
|
2609
|
+
sawBoundary = true;
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
if (child.type === "bookmark_end" && child.bookmarkId === bookmarkId) {
|
|
2613
|
+
break;
|
|
2614
|
+
}
|
|
2615
|
+
if (!inside) {
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
if (child.type === "footnote_ref") {
|
|
2619
|
+
return child.noteId;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return sawBoundary ? undefined : undefined;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function getCommandSelection(
|
|
2626
|
+
command: EditorCommand,
|
|
2627
|
+
fallbackSelection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
2628
|
+
): import("../core/state/editor-state.ts").SelectionSnapshot {
|
|
2629
|
+
if ("protectionSelection" in command && command.protectionSelection) {
|
|
2630
|
+
return command.protectionSelection;
|
|
2631
|
+
}
|
|
2632
|
+
if ("selection" in command && command.selection) {
|
|
2633
|
+
return command.selection;
|
|
2634
|
+
}
|
|
2635
|
+
return fallbackSelection;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
function isBlockedByProtection(
|
|
2639
|
+
protection: ProtectionSnapshot,
|
|
2640
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
2641
|
+
): boolean {
|
|
2642
|
+
const enforcedRanges = protection.ranges.filter(
|
|
2643
|
+
(range): range is typeof range & { start: number; end: number } =>
|
|
2644
|
+
range.enforced && typeof range.start === "number" && typeof range.end === "number",
|
|
2645
|
+
);
|
|
2646
|
+
if (enforcedRanges.length === 0) {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
2650
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
2651
|
+
return !enforcedRanges.some((range) =>
|
|
2652
|
+
from >= range.start && to <= range.end,
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function remapProtectionSnapshot(
|
|
2657
|
+
protection: ProtectionSnapshot,
|
|
2658
|
+
mapping: import("../core/selection/mapping.ts").TransactionMapping,
|
|
2659
|
+
): ProtectionSnapshot {
|
|
2660
|
+
if (mapping.steps.length === 0 || protection.ranges.length === 0) {
|
|
2661
|
+
return protection;
|
|
2662
|
+
}
|
|
2663
|
+
let changed = false;
|
|
2664
|
+
const nextRanges = protection.ranges.map((range) => {
|
|
2665
|
+
if (
|
|
2666
|
+
!range.enforced ||
|
|
2667
|
+
typeof range.start !== "number" ||
|
|
2668
|
+
typeof range.end !== "number"
|
|
2669
|
+
) {
|
|
2670
|
+
return range;
|
|
2671
|
+
}
|
|
2672
|
+
const mapped = mapRange(
|
|
2673
|
+
{ from: range.start, to: range.end },
|
|
2674
|
+
{ start: -1, end: 1 },
|
|
2675
|
+
mapping,
|
|
2676
|
+
);
|
|
2677
|
+
if (mapped.kind === "detached") {
|
|
2678
|
+
changed = true;
|
|
2679
|
+
return {
|
|
2680
|
+
...range,
|
|
2681
|
+
start: undefined,
|
|
2682
|
+
end: undefined,
|
|
2683
|
+
enforced: false,
|
|
2684
|
+
enforcementReason:
|
|
2685
|
+
"preserve-only: permission range could not be remapped after runtime edits",
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
if (mapped.range.from !== range.start || mapped.range.to !== range.end) {
|
|
2689
|
+
changed = true;
|
|
2690
|
+
return {
|
|
2691
|
+
...range,
|
|
2692
|
+
start: mapped.range.from,
|
|
2693
|
+
end: mapped.range.to,
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
return range;
|
|
2697
|
+
});
|
|
2698
|
+
if (!changed) {
|
|
2699
|
+
return protection;
|
|
2700
|
+
}
|
|
2701
|
+
return {
|
|
2702
|
+
...protection,
|
|
2703
|
+
ranges: nextRanges,
|
|
2704
|
+
enforcedRangeCount: nextRanges.filter((range) => range.enforced).length,
|
|
2705
|
+
preservedRangeCount: nextRanges.filter((range) => !range.enforced).length,
|
|
2706
|
+
};
|
|
2707
|
+
}
|