@beyondwork/docx-react-component 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- 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 +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -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 +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +850 -1315
- 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/revision-decoration-model.ts +4 -4
- 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-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +174 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
CommentSidebarSnapshot,
|
|
18
18
|
CommentSidebarThreadSnapshot,
|
|
19
19
|
CompatibilityReport,
|
|
20
|
+
DocumentMode,
|
|
20
21
|
DocumentNavigationSnapshot,
|
|
21
22
|
EditorSessionState,
|
|
22
23
|
EditorAnchorProjection,
|
|
@@ -24,17 +25,31 @@ import type {
|
|
|
24
25
|
EditorStoryTarget,
|
|
25
26
|
EditorViewStateSnapshot,
|
|
26
27
|
EditorWarning,
|
|
28
|
+
FieldEntrySnapshot,
|
|
29
|
+
FieldSnapshot,
|
|
27
30
|
HeaderFooterLinkPatch,
|
|
28
31
|
ExportDocxOptions,
|
|
29
32
|
ExportResult,
|
|
33
|
+
InteractionGuardSnapshot,
|
|
30
34
|
PageLayoutSnapshot,
|
|
31
35
|
PersistedEditorSnapshot,
|
|
36
|
+
ProtectionSnapshot,
|
|
32
37
|
RuntimeRenderSnapshot,
|
|
33
38
|
SelectionSnapshot,
|
|
34
39
|
StyleCatalogSnapshot,
|
|
40
|
+
TocRefreshOptions,
|
|
41
|
+
TocRefreshResult,
|
|
35
42
|
TrackedChangeEntrySnapshot,
|
|
36
43
|
TrackedChangesSnapshot,
|
|
44
|
+
UpdateFieldsOptions,
|
|
45
|
+
UpdateFieldsResult,
|
|
37
46
|
ViewMode,
|
|
47
|
+
WorkflowCandidateRange,
|
|
48
|
+
WorkflowCandidateRangeOptions,
|
|
49
|
+
WorkflowBlockedCommandReason,
|
|
50
|
+
WorkflowMarkupSnapshot,
|
|
51
|
+
WorkflowOverlay,
|
|
52
|
+
WorkflowScopeSnapshot,
|
|
38
53
|
WorkspaceMode,
|
|
39
54
|
WordReviewEditorEvent,
|
|
40
55
|
ZoomLevel,
|
|
@@ -53,13 +68,20 @@ import {
|
|
|
53
68
|
import { insertText } from "../core/commands/text-commands.ts";
|
|
54
69
|
import {
|
|
55
70
|
createDetachedAnchor,
|
|
71
|
+
createEmptyMapping,
|
|
56
72
|
createNodeAnchor,
|
|
57
73
|
createRangeAnchor,
|
|
74
|
+
mapRange,
|
|
58
75
|
MAIN_STORY_TARGET,
|
|
59
76
|
storyTargetsEqual,
|
|
60
77
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
61
78
|
} from "../core/selection/mapping.ts";
|
|
62
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";
|
|
63
85
|
import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
|
|
64
86
|
import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
|
|
65
87
|
import {
|
|
@@ -69,7 +91,14 @@ import {
|
|
|
69
91
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
70
92
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
71
93
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
72
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
collectWorkflowMarkupSnapshot,
|
|
96
|
+
deriveWorkflowCandidateRangesFromMarkup,
|
|
97
|
+
} from "./workflow-markup.ts";
|
|
98
|
+
import {
|
|
99
|
+
createDocumentNavigationSnapshot,
|
|
100
|
+
findPageForOffset,
|
|
101
|
+
} from "./document-navigation.ts";
|
|
73
102
|
import {
|
|
74
103
|
buildPageLayoutSnapshot,
|
|
75
104
|
buildResolvedSections,
|
|
@@ -80,6 +109,7 @@ import { storyTargetKey } from "./story-targeting.ts";
|
|
|
80
109
|
import {
|
|
81
110
|
createViewState,
|
|
82
111
|
setViewMode as applyViewMode,
|
|
112
|
+
setDocumentMode as applyDocumentMode,
|
|
83
113
|
setWorkspaceMode as applyWorkspaceMode,
|
|
84
114
|
setZoomLevel as applyZoomLevel,
|
|
85
115
|
setFocused as applyFocused,
|
|
@@ -89,7 +119,25 @@ import {
|
|
|
89
119
|
createEditorViewStateSnapshot,
|
|
90
120
|
type ViewState,
|
|
91
121
|
} from "./view-state.ts";
|
|
92
|
-
import type {
|
|
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";
|
|
93
141
|
|
|
94
142
|
export type Unsubscribe = () => void;
|
|
95
143
|
|
|
@@ -111,6 +159,7 @@ export interface DocumentRuntime {
|
|
|
111
159
|
redo(): void;
|
|
112
160
|
focus(): void;
|
|
113
161
|
blur(): void;
|
|
162
|
+
setDefaultAuthorId?(authorId?: string): void;
|
|
114
163
|
addComment(params: AddCommentParams): string;
|
|
115
164
|
openComment(commentId: string): void;
|
|
116
165
|
resolveComment(commentId: string): void;
|
|
@@ -126,15 +175,27 @@ export interface DocumentRuntime {
|
|
|
126
175
|
getActiveStory(): EditorStoryTarget;
|
|
127
176
|
getViewState(): EditorViewStateSnapshot;
|
|
128
177
|
setViewMode(mode: ViewMode): void;
|
|
178
|
+
setDocumentMode(mode: DocumentMode): void;
|
|
179
|
+
getProtectionSnapshot(): ProtectionSnapshot;
|
|
129
180
|
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
130
181
|
setZoom(level: ZoomLevel): void;
|
|
131
182
|
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
132
183
|
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
184
|
+
getFieldSnapshot(): FieldSnapshot;
|
|
185
|
+
updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult;
|
|
186
|
+
updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult;
|
|
133
187
|
getSessionState(): EditorSessionState;
|
|
134
188
|
getPersistedSnapshot(): PersistedEditorSnapshot;
|
|
135
189
|
getCompatibilityReport(): CompatibilityReport;
|
|
136
190
|
getWarnings(): EditorWarning[];
|
|
137
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;
|
|
138
199
|
}
|
|
139
200
|
|
|
140
201
|
export interface CreateDocumentRuntimeOptions {
|
|
@@ -157,6 +218,7 @@ export interface CreateDocumentRuntimeOptions {
|
|
|
157
218
|
onWarning?: (warning: EditorWarning) => void;
|
|
158
219
|
onError?: (error: EditorError) => void;
|
|
159
220
|
initialViewState?: Partial<ViewState>;
|
|
221
|
+
protectionSnapshot?: ProtectionSnapshot;
|
|
160
222
|
}
|
|
161
223
|
|
|
162
224
|
interface HistoryState {
|
|
@@ -169,6 +231,7 @@ export function createDocumentRuntime(
|
|
|
169
231
|
): DocumentRuntime {
|
|
170
232
|
const clock = options.clock ?? (() => new Date().toISOString());
|
|
171
233
|
const editorBuild = options.editorBuild ?? "dev";
|
|
234
|
+
let defaultAuthorId = options.defaultAuthorId;
|
|
172
235
|
const sessionId = createSessionId(options.documentId, clock());
|
|
173
236
|
const listeners = new Set<() => void>();
|
|
174
237
|
const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
|
|
@@ -180,6 +243,17 @@ export function createDocumentRuntime(
|
|
|
180
243
|
let activeStory: EditorStoryTarget = MAIN_STORY_TARGET;
|
|
181
244
|
const storySelections = new Map<string, EditorState["selection"]>();
|
|
182
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;
|
|
183
257
|
const initialPersistedSnapshot = options.initialSessionState
|
|
184
258
|
? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
|
|
185
259
|
savedAt: options.initialSessionState.updatedAt,
|
|
@@ -196,7 +270,573 @@ export function createDocumentRuntime(
|
|
|
196
270
|
fatalError: options.fatalError as never,
|
|
197
271
|
});
|
|
198
272
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
199
|
-
let
|
|
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();
|
|
200
840
|
|
|
201
841
|
emit({
|
|
202
842
|
type: "ready",
|
|
@@ -239,16 +879,33 @@ export function createDocumentRuntime(
|
|
|
239
879
|
return cachedRenderSnapshot;
|
|
240
880
|
},
|
|
241
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
|
+
|
|
242
898
|
if (command.type === "history.undo") {
|
|
899
|
+
if (viewState.documentMode === "viewing") return;
|
|
243
900
|
applyHistory("undo");
|
|
244
901
|
return;
|
|
245
902
|
}
|
|
246
903
|
|
|
247
904
|
if (command.type === "history.redo") {
|
|
905
|
+
if (viewState.documentMode === "viewing") return;
|
|
248
906
|
applyHistory("redo");
|
|
249
907
|
return;
|
|
250
908
|
}
|
|
251
|
-
|
|
252
909
|
try {
|
|
253
910
|
const transaction = executeEditorCommand(state, command, {
|
|
254
911
|
timestamp: command.origin?.timestamp ?? clock(),
|
|
@@ -286,12 +943,25 @@ export function createDocumentRuntime(
|
|
|
286
943
|
origin: createOrigin("api", clock()),
|
|
287
944
|
});
|
|
288
945
|
},
|
|
946
|
+
setDefaultAuthorId(authorId) {
|
|
947
|
+
defaultAuthorId = authorId;
|
|
948
|
+
},
|
|
289
949
|
replaceText(text, target) {
|
|
290
950
|
try {
|
|
291
951
|
const timestamp = clock();
|
|
292
952
|
const selection = target
|
|
293
953
|
? createSelectionFromPublicAnchor(target)
|
|
294
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
|
+
}
|
|
295
965
|
const result = insertText(state.document, selection, text, { timestamp });
|
|
296
966
|
|
|
297
967
|
this.dispatch({
|
|
@@ -299,6 +969,7 @@ export function createDocumentRuntime(
|
|
|
299
969
|
document: result.document,
|
|
300
970
|
mapping: result.mapping,
|
|
301
971
|
selection: result.selection,
|
|
972
|
+
protectionSelection: selection,
|
|
302
973
|
origin: createOrigin("api", timestamp),
|
|
303
974
|
});
|
|
304
975
|
} catch (error) {
|
|
@@ -306,10 +977,16 @@ export function createDocumentRuntime(
|
|
|
306
977
|
}
|
|
307
978
|
},
|
|
308
979
|
addComment(params) {
|
|
980
|
+
if (viewState.documentMode === "viewing") {
|
|
981
|
+
throw new Error("Cannot add comments in viewing mode.");
|
|
982
|
+
}
|
|
309
983
|
const commentId = createEntityId("comment", state.document.review.comments, clock());
|
|
310
984
|
const anchor = params.anchor
|
|
311
985
|
? toInternalAnchorProjection(params.anchor)
|
|
312
986
|
: state.selection.activeRange;
|
|
987
|
+
const selection = params.anchor
|
|
988
|
+
? createSelectionFromPublicAnchor(params.anchor)
|
|
989
|
+
: state.selection;
|
|
313
990
|
if (!canCreateDocxCommentAnchor(state.document.content, anchor)) {
|
|
314
991
|
const message =
|
|
315
992
|
"DOCX comments must use a non-empty range that stays within a single paragraph.";
|
|
@@ -322,7 +999,7 @@ export function createDocumentRuntime(
|
|
|
322
999
|
});
|
|
323
1000
|
throw new Error(message);
|
|
324
1001
|
}
|
|
325
|
-
const authorId = params.authorId ??
|
|
1002
|
+
const authorId = params.authorId ?? defaultAuthorId ?? "unknown";
|
|
326
1003
|
const createdAt = clock();
|
|
327
1004
|
const entries: CommentEntryRecord[] = [
|
|
328
1005
|
{
|
|
@@ -351,6 +1028,7 @@ export function createDocumentRuntime(
|
|
|
351
1028
|
this.dispatch({
|
|
352
1029
|
type: "comment.add",
|
|
353
1030
|
comment,
|
|
1031
|
+
selection,
|
|
354
1032
|
origin: createOrigin("api", clock()),
|
|
355
1033
|
});
|
|
356
1034
|
|
|
@@ -367,7 +1045,7 @@ export function createDocumentRuntime(
|
|
|
367
1045
|
this.dispatch({
|
|
368
1046
|
type: "comment.resolve",
|
|
369
1047
|
commentId,
|
|
370
|
-
resolvedBy:
|
|
1048
|
+
resolvedBy: defaultAuthorId ?? "unknown",
|
|
371
1049
|
origin: createOrigin("api", clock()),
|
|
372
1050
|
});
|
|
373
1051
|
},
|
|
@@ -383,7 +1061,7 @@ export function createDocumentRuntime(
|
|
|
383
1061
|
type: "comment.add-reply",
|
|
384
1062
|
commentId,
|
|
385
1063
|
body,
|
|
386
|
-
authorId: authorId ??
|
|
1064
|
+
authorId: authorId ?? defaultAuthorId,
|
|
387
1065
|
origin: createOrigin("api", clock()),
|
|
388
1066
|
});
|
|
389
1067
|
},
|
|
@@ -449,51 +1127,88 @@ export function createDocumentRuntime(
|
|
|
449
1127
|
return activeStory;
|
|
450
1128
|
},
|
|
451
1129
|
getViewState() {
|
|
452
|
-
|
|
453
|
-
const mainSurface =
|
|
454
|
-
activeStory.kind === "main"
|
|
455
|
-
? surface
|
|
456
|
-
: createEditorSurfaceSnapshot(state.document, state.selection, MAIN_STORY_TARGET);
|
|
457
|
-
return createEditorViewStateSnapshot(
|
|
458
|
-
viewState,
|
|
459
|
-
activeStory,
|
|
460
|
-
toPublicSelectionSnapshot(state.selection, activeStory),
|
|
461
|
-
surface,
|
|
462
|
-
mainSurface,
|
|
463
|
-
cachedRenderSnapshot.pageLayout,
|
|
464
|
-
state.document.numbering,
|
|
465
|
-
);
|
|
1130
|
+
return getCachedViewStateSnapshot();
|
|
466
1131
|
},
|
|
467
1132
|
setViewMode(mode) {
|
|
468
1133
|
viewState = applyViewMode(viewState, mode);
|
|
469
|
-
cachedRenderSnapshot =
|
|
1134
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1135
|
+
for (const listener of listeners) {
|
|
1136
|
+
listener();
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
setDocumentMode(mode) {
|
|
1140
|
+
viewState = applyDocumentMode(viewState, mode);
|
|
1141
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
470
1142
|
for (const listener of listeners) {
|
|
471
1143
|
listener();
|
|
472
1144
|
}
|
|
473
1145
|
},
|
|
1146
|
+
getProtectionSnapshot() {
|
|
1147
|
+
return cachedRenderSnapshot.protectionSnapshot;
|
|
1148
|
+
},
|
|
474
1149
|
setWorkspaceMode(mode) {
|
|
475
1150
|
viewState = applyWorkspaceMode(viewState, mode);
|
|
476
|
-
cachedRenderSnapshot =
|
|
1151
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
477
1152
|
for (const listener of listeners) {
|
|
478
1153
|
listener();
|
|
479
1154
|
}
|
|
480
1155
|
},
|
|
481
1156
|
setZoom(level) {
|
|
482
1157
|
viewState = applyZoomLevel(viewState, level);
|
|
483
|
-
cachedRenderSnapshot =
|
|
1158
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
484
1159
|
for (const listener of listeners) {
|
|
485
1160
|
listener();
|
|
486
1161
|
}
|
|
487
1162
|
},
|
|
488
1163
|
getPageLayoutSnapshot() {
|
|
489
|
-
return
|
|
1164
|
+
return getCachedPageLayoutSnapshot(state, activeStory);
|
|
490
1165
|
},
|
|
491
1166
|
getDocumentNavigationSnapshot() {
|
|
492
|
-
return
|
|
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(
|
|
493
1197
|
state.document,
|
|
494
1198
|
state.selection.head,
|
|
495
1199
|
activeStory,
|
|
1200
|
+
options,
|
|
496
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;
|
|
497
1212
|
},
|
|
498
1213
|
getSessionState() {
|
|
499
1214
|
const compatibility = createDerivedCompatibility(state);
|
|
@@ -502,6 +1217,7 @@ export function createDocumentRuntime(
|
|
|
502
1217
|
editorBuild,
|
|
503
1218
|
savedAt: clock(),
|
|
504
1219
|
compatibility,
|
|
1220
|
+
protectionSnapshot,
|
|
505
1221
|
}) as unknown as PersistedEditorSnapshot,
|
|
506
1222
|
);
|
|
507
1223
|
},
|
|
@@ -542,6 +1258,75 @@ export function createDocumentRuntime(
|
|
|
542
1258
|
|
|
543
1259
|
return result;
|
|
544
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
|
+
},
|
|
545
1330
|
};
|
|
546
1331
|
|
|
547
1332
|
function applyHistory(direction: "undo" | "redo"): void {
|
|
@@ -560,7 +1345,7 @@ export function createDocumentRuntime(
|
|
|
560
1345
|
// autosave/export checkpoint dedup treats it as fresh content.
|
|
561
1346
|
state = finalizeState(target, true, clock());
|
|
562
1347
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
563
|
-
cachedRenderSnapshot =
|
|
1348
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
564
1349
|
notify(previous, state, {
|
|
565
1350
|
nextState: state,
|
|
566
1351
|
mapping: { steps: [] },
|
|
@@ -581,9 +1366,10 @@ export function createDocumentRuntime(
|
|
|
581
1366
|
history.future = [];
|
|
582
1367
|
}
|
|
583
1368
|
|
|
1369
|
+
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
584
1370
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
585
1371
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
586
|
-
cachedRenderSnapshot =
|
|
1372
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
587
1373
|
notify(previous, state, transaction);
|
|
588
1374
|
}
|
|
589
1375
|
|
|
@@ -680,7 +1466,7 @@ export function createDocumentRuntime(
|
|
|
680
1466
|
};
|
|
681
1467
|
state = nextState;
|
|
682
1468
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
683
|
-
cachedRenderSnapshot =
|
|
1469
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
684
1470
|
const publicError = toPublicError(error);
|
|
685
1471
|
options.onError?.(publicError);
|
|
686
1472
|
emit({
|
|
@@ -706,7 +1492,7 @@ export function createDocumentRuntime(
|
|
|
706
1492
|
selection: restoredSelection,
|
|
707
1493
|
};
|
|
708
1494
|
storySelections.set(storyTargetKey(target), restoredSelection);
|
|
709
|
-
cachedRenderSnapshot =
|
|
1495
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
710
1496
|
|
|
711
1497
|
if (selectionChanged(previousSelection, restoredSelection)) {
|
|
712
1498
|
emit({
|
|
@@ -800,51 +1586,6 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
800
1586
|
};
|
|
801
1587
|
}
|
|
802
1588
|
|
|
803
|
-
function createPublicRenderSnapshot(
|
|
804
|
-
state: EditorState,
|
|
805
|
-
history: HistoryState,
|
|
806
|
-
activeStory: EditorStoryTarget,
|
|
807
|
-
): RuntimeRenderSnapshot {
|
|
808
|
-
const compatibility = createDerivedCompatibility(state);
|
|
809
|
-
const surface = createEditorSurfaceSnapshot(state.document, state.selection, activeStory);
|
|
810
|
-
const comments = toPublicCommentSidebarSnapshot(state);
|
|
811
|
-
const trackedChanges = toPublicTrackedChangesSnapshot(state, surface.plainText);
|
|
812
|
-
const pageLayout = derivePageLayoutSnapshot(state, activeStory);
|
|
813
|
-
|
|
814
|
-
return {
|
|
815
|
-
documentId: state.documentId,
|
|
816
|
-
sessionId: state.sessionId,
|
|
817
|
-
sourceLabel: state.sourceLabel,
|
|
818
|
-
revisionToken: state.revisionToken,
|
|
819
|
-
isReady: state.phase === "ready",
|
|
820
|
-
isDirty: state.isDirty,
|
|
821
|
-
readOnly: state.readOnly,
|
|
822
|
-
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
823
|
-
activeStory,
|
|
824
|
-
pageLayout: pageLayout ?? undefined,
|
|
825
|
-
documentStats: toPublicDocumentStats(state),
|
|
826
|
-
comments,
|
|
827
|
-
trackedChanges,
|
|
828
|
-
compatibility: {
|
|
829
|
-
blockExport: compatibility.blockExport,
|
|
830
|
-
blockExportReasons: listBlockExportReasons(compatibility),
|
|
831
|
-
warningCount: compatibility.warnings.length,
|
|
832
|
-
errorCount: compatibility.errors.length,
|
|
833
|
-
featureEntries: compatibility.featureEntries.map((entry) =>
|
|
834
|
-
toPublicCompatibilityFeatureEntry(entry),
|
|
835
|
-
),
|
|
836
|
-
},
|
|
837
|
-
warnings: state.warnings.map((warning) => toPublicWarning(warning)),
|
|
838
|
-
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
839
|
-
commandState: {
|
|
840
|
-
canUndo: history.past.length > 0,
|
|
841
|
-
canRedo: history.future.length > 0,
|
|
842
|
-
readOnly: state.readOnly,
|
|
843
|
-
},
|
|
844
|
-
surface,
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
1589
|
function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
849
1590
|
const stats = deriveDocumentStats(state);
|
|
850
1591
|
return {
|
|
@@ -1259,3 +2000,708 @@ function derivePageLayoutSnapshot(
|
|
|
1259
2000
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1260
2001
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1261
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
|
+
}
|