@beyondwork/docx-react-component 1.0.17 → 1.0.19
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 +32 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- 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 +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- 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/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- 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 +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- 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-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- 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/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -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,32 @@ import type {
|
|
|
17
17
|
CommentSidebarSnapshot,
|
|
18
18
|
CommentSidebarThreadSnapshot,
|
|
19
19
|
CompatibilityReport,
|
|
20
|
+
DocumentNavigationSnapshot,
|
|
21
|
+
EditorSessionState,
|
|
20
22
|
EditorAnchorProjection,
|
|
21
23
|
EditorError,
|
|
24
|
+
EditorStoryTarget,
|
|
25
|
+
EditorViewStateSnapshot,
|
|
22
26
|
EditorWarning,
|
|
27
|
+
HeaderFooterLinkPatch,
|
|
23
28
|
ExportDocxOptions,
|
|
24
29
|
ExportResult,
|
|
30
|
+
PageLayoutSnapshot,
|
|
25
31
|
PersistedEditorSnapshot,
|
|
26
32
|
RuntimeRenderSnapshot,
|
|
27
33
|
SelectionSnapshot,
|
|
34
|
+
StyleCatalogSnapshot,
|
|
28
35
|
TrackedChangeEntrySnapshot,
|
|
29
36
|
TrackedChangesSnapshot,
|
|
37
|
+
ViewMode,
|
|
38
|
+
WorkspaceMode,
|
|
30
39
|
WordReviewEditorEvent,
|
|
40
|
+
ZoomLevel,
|
|
31
41
|
} from "../api/public-types";
|
|
42
|
+
import {
|
|
43
|
+
editorSessionStateFromPersistedSnapshot,
|
|
44
|
+
persistedSnapshotFromEditorSessionState,
|
|
45
|
+
} from "../api/session-state.ts";
|
|
32
46
|
import {
|
|
33
47
|
executeEditorCommand,
|
|
34
48
|
selectionChanged,
|
|
@@ -41,6 +55,8 @@ import {
|
|
|
41
55
|
createDetachedAnchor,
|
|
42
56
|
createNodeAnchor,
|
|
43
57
|
createRangeAnchor,
|
|
58
|
+
MAIN_STORY_TARGET,
|
|
59
|
+
storyTargetsEqual,
|
|
44
60
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
45
61
|
} from "../core/selection/mapping.ts";
|
|
46
62
|
import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors.ts";
|
|
@@ -53,21 +69,48 @@ import {
|
|
|
53
69
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
54
70
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
55
71
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
56
|
-
import {
|
|
72
|
+
import { createDocumentNavigationSnapshot } from "./document-navigation.ts";
|
|
73
|
+
import {
|
|
74
|
+
buildPageLayoutSnapshot,
|
|
75
|
+
buildResolvedSections,
|
|
76
|
+
resolveActiveSection,
|
|
77
|
+
} from "./document-layout.ts";
|
|
78
|
+
import { normalizeHeaderFooterTarget } from "./story-context.ts";
|
|
79
|
+
import { storyTargetKey } from "./story-targeting.ts";
|
|
80
|
+
import {
|
|
81
|
+
createViewState,
|
|
82
|
+
setViewMode as applyViewMode,
|
|
83
|
+
setWorkspaceMode as applyWorkspaceMode,
|
|
84
|
+
setZoomLevel as applyZoomLevel,
|
|
85
|
+
setFocused as applyFocused,
|
|
86
|
+
setCaretAffinity as applyCaretAffinity,
|
|
87
|
+
setActivePageRegion as applyActivePageRegion,
|
|
88
|
+
setActiveObjectFrame as applyActiveObjectFrame,
|
|
89
|
+
createEditorViewStateSnapshot,
|
|
90
|
+
type ViewState,
|
|
91
|
+
} from "./view-state.ts";
|
|
92
|
+
import type { PageMargins } from "../model/canonical-document.ts";
|
|
57
93
|
|
|
58
94
|
export type Unsubscribe = () => void;
|
|
59
95
|
|
|
96
|
+
type RuntimeReadySource = "docx" | "session" | "snapshot" | "datastore" | "canonical";
|
|
97
|
+
|
|
98
|
+
export type DocumentRuntimeEvent =
|
|
99
|
+
| (Omit<Extract<WordReviewEditorEvent, { type: "ready" }>, "source"> & {
|
|
100
|
+
source: RuntimeReadySource;
|
|
101
|
+
})
|
|
102
|
+
| Exclude<WordReviewEditorEvent, { type: "ready" }>;
|
|
103
|
+
|
|
60
104
|
export interface DocumentRuntime {
|
|
61
105
|
subscribe(listener: () => void): Unsubscribe;
|
|
62
|
-
subscribeToEvents(listener: (event:
|
|
106
|
+
subscribeToEvents(listener: (event: DocumentRuntimeEvent) => void): Unsubscribe;
|
|
63
107
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
64
|
-
|
|
108
|
+
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
65
109
|
dispatch(command: EditorCommand): void;
|
|
66
110
|
undo(): void;
|
|
67
111
|
redo(): void;
|
|
68
112
|
focus(): void;
|
|
69
113
|
blur(): void;
|
|
70
|
-
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
71
114
|
addComment(params: AddCommentParams): string;
|
|
72
115
|
openComment(commentId: string): void;
|
|
73
116
|
resolveComment(commentId: string): void;
|
|
@@ -78,6 +121,16 @@ export interface DocumentRuntime {
|
|
|
78
121
|
rejectChange(changeId: string): void;
|
|
79
122
|
acceptAllChanges(): void;
|
|
80
123
|
rejectAllChanges(): void;
|
|
124
|
+
openStory(target: EditorStoryTarget): boolean;
|
|
125
|
+
closeStory(): void;
|
|
126
|
+
getActiveStory(): EditorStoryTarget;
|
|
127
|
+
getViewState(): EditorViewStateSnapshot;
|
|
128
|
+
setViewMode(mode: ViewMode): void;
|
|
129
|
+
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
130
|
+
setZoom(level: ZoomLevel): void;
|
|
131
|
+
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
132
|
+
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
133
|
+
getSessionState(): EditorSessionState;
|
|
81
134
|
getPersistedSnapshot(): PersistedEditorSnapshot;
|
|
82
135
|
getCompatibilityReport(): CompatibilityReport;
|
|
83
136
|
getWarnings(): EditorWarning[];
|
|
@@ -86,22 +139,24 @@ export interface DocumentRuntime {
|
|
|
86
139
|
|
|
87
140
|
export interface CreateDocumentRuntimeOptions {
|
|
88
141
|
documentId: string;
|
|
142
|
+
initialSessionState?: EditorSessionState;
|
|
89
143
|
initialSnapshot?: PersistedEditorSnapshot;
|
|
90
144
|
initialCanonicalDocument?: CanonicalDocumentEnvelope;
|
|
91
145
|
sourceLabel?: string;
|
|
92
|
-
sourceKind?:
|
|
146
|
+
sourceKind?: RuntimeReadySource;
|
|
93
147
|
readOnly?: boolean;
|
|
94
148
|
editorBuild?: string;
|
|
95
149
|
defaultAuthorId?: string;
|
|
96
150
|
fatalError?: EditorError;
|
|
97
151
|
clock?: () => string;
|
|
98
152
|
exportDocx?: (
|
|
99
|
-
|
|
153
|
+
sessionState: EditorSessionState,
|
|
100
154
|
options?: ExportDocxOptions,
|
|
101
155
|
) => Promise<ExportResult>;
|
|
102
|
-
onEvent?: (event:
|
|
156
|
+
onEvent?: (event: DocumentRuntimeEvent) => void;
|
|
103
157
|
onWarning?: (warning: EditorWarning) => void;
|
|
104
158
|
onError?: (error: EditorError) => void;
|
|
159
|
+
initialViewState?: Partial<ViewState>;
|
|
105
160
|
}
|
|
106
161
|
|
|
107
162
|
interface HistoryState {
|
|
@@ -116,28 +171,44 @@ export function createDocumentRuntime(
|
|
|
116
171
|
const editorBuild = options.editorBuild ?? "dev";
|
|
117
172
|
const sessionId = createSessionId(options.documentId, clock());
|
|
118
173
|
const listeners = new Set<() => void>();
|
|
119
|
-
const eventListeners = new Set<(event:
|
|
174
|
+
const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
|
|
120
175
|
const history: HistoryState = {
|
|
121
176
|
past: [],
|
|
122
177
|
future: [],
|
|
123
178
|
};
|
|
124
179
|
|
|
180
|
+
let activeStory: EditorStoryTarget = MAIN_STORY_TARGET;
|
|
181
|
+
const storySelections = new Map<string, EditorState["selection"]>();
|
|
182
|
+
let viewState: ViewState = createViewState(options.initialViewState);
|
|
183
|
+
const initialPersistedSnapshot = options.initialSessionState
|
|
184
|
+
? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
|
|
185
|
+
savedAt: options.initialSessionState.updatedAt,
|
|
186
|
+
})
|
|
187
|
+
: options.initialSnapshot;
|
|
188
|
+
|
|
125
189
|
let state = createEditorState({
|
|
126
190
|
documentId: options.documentId,
|
|
127
191
|
sessionId,
|
|
128
192
|
sourceLabel: options.sourceLabel,
|
|
129
193
|
readOnly: options.readOnly,
|
|
130
|
-
persistedSnapshot:
|
|
194
|
+
persistedSnapshot: initialPersistedSnapshot as never,
|
|
131
195
|
canonicalDocument: options.initialCanonicalDocument,
|
|
132
196
|
fatalError: options.fatalError as never,
|
|
133
197
|
});
|
|
134
|
-
|
|
198
|
+
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
199
|
+
let cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
135
200
|
|
|
136
201
|
emit({
|
|
137
202
|
type: "ready",
|
|
138
203
|
documentId: state.documentId,
|
|
139
204
|
sessionId: state.sessionId,
|
|
140
|
-
source:
|
|
205
|
+
source:
|
|
206
|
+
options.sourceKind ??
|
|
207
|
+
(options.initialSessionState
|
|
208
|
+
? "session"
|
|
209
|
+
: options.initialSnapshot
|
|
210
|
+
? "snapshot"
|
|
211
|
+
: "canonical"),
|
|
141
212
|
stats: toPublicDocumentStats(state),
|
|
142
213
|
compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
|
|
143
214
|
comments: cachedRenderSnapshot.comments,
|
|
@@ -167,9 +238,6 @@ export function createDocumentRuntime(
|
|
|
167
238
|
getRenderSnapshot() {
|
|
168
239
|
return cachedRenderSnapshot;
|
|
169
240
|
},
|
|
170
|
-
getFormattingState() {
|
|
171
|
-
return getFormattingStateFromRenderSnapshot(cachedRenderSnapshot);
|
|
172
|
-
},
|
|
173
241
|
dispatch(command) {
|
|
174
242
|
if (command.type === "history.undo") {
|
|
175
243
|
applyHistory("undo");
|
|
@@ -203,6 +271,7 @@ export function createDocumentRuntime(
|
|
|
203
271
|
});
|
|
204
272
|
},
|
|
205
273
|
focus() {
|
|
274
|
+
viewState = applyFocused(viewState, true);
|
|
206
275
|
this.dispatch({
|
|
207
276
|
type: "runtime.focus",
|
|
208
277
|
focused: true,
|
|
@@ -210,6 +279,7 @@ export function createDocumentRuntime(
|
|
|
210
279
|
});
|
|
211
280
|
},
|
|
212
281
|
blur() {
|
|
282
|
+
viewState = applyFocused(viewState, false);
|
|
213
283
|
this.dispatch({
|
|
214
284
|
type: "runtime.focus",
|
|
215
285
|
focused: false,
|
|
@@ -351,13 +421,94 @@ export function createDocumentRuntime(
|
|
|
351
421
|
origin: createOrigin("api", clock()),
|
|
352
422
|
});
|
|
353
423
|
},
|
|
354
|
-
|
|
424
|
+
openStory(target) {
|
|
425
|
+
const normalizedTarget =
|
|
426
|
+
target.kind === "header" || target.kind === "footer"
|
|
427
|
+
? normalizeHeaderFooterTarget(
|
|
428
|
+
state.document,
|
|
429
|
+
target,
|
|
430
|
+
cachedRenderSnapshot.pageLayout?.sectionIndex,
|
|
431
|
+
) ?? target
|
|
432
|
+
: target;
|
|
433
|
+
if (storyTargetsEqual(activeStory, normalizedTarget)) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
if (!isValidStoryTarget(state, normalizedTarget)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
switchActiveStory(normalizedTarget);
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
closeStory() {
|
|
443
|
+
if (activeStory.kind === "main") {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
switchActiveStory(MAIN_STORY_TARGET);
|
|
447
|
+
},
|
|
448
|
+
getActiveStory() {
|
|
449
|
+
return activeStory;
|
|
450
|
+
},
|
|
451
|
+
getViewState() {
|
|
452
|
+
const surface = cachedRenderSnapshot.surface;
|
|
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
|
+
);
|
|
466
|
+
},
|
|
467
|
+
setViewMode(mode) {
|
|
468
|
+
viewState = applyViewMode(viewState, mode);
|
|
469
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
470
|
+
for (const listener of listeners) {
|
|
471
|
+
listener();
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
setWorkspaceMode(mode) {
|
|
475
|
+
viewState = applyWorkspaceMode(viewState, mode);
|
|
476
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
477
|
+
for (const listener of listeners) {
|
|
478
|
+
listener();
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
setZoom(level) {
|
|
482
|
+
viewState = applyZoomLevel(viewState, level);
|
|
483
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
484
|
+
for (const listener of listeners) {
|
|
485
|
+
listener();
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
getPageLayoutSnapshot() {
|
|
489
|
+
return derivePageLayoutSnapshot(state, activeStory, storySelections);
|
|
490
|
+
},
|
|
491
|
+
getDocumentNavigationSnapshot() {
|
|
492
|
+
return createDocumentNavigationSnapshot(
|
|
493
|
+
state.document,
|
|
494
|
+
state.selection.head,
|
|
495
|
+
activeStory,
|
|
496
|
+
);
|
|
497
|
+
},
|
|
498
|
+
getSessionState() {
|
|
355
499
|
const compatibility = createDerivedCompatibility(state);
|
|
356
|
-
return
|
|
357
|
-
|
|
500
|
+
return editorSessionStateFromPersistedSnapshot(
|
|
501
|
+
createPersistedEditorSnapshot(state, {
|
|
502
|
+
editorBuild,
|
|
503
|
+
savedAt: clock(),
|
|
504
|
+
compatibility,
|
|
505
|
+
}) as unknown as PersistedEditorSnapshot,
|
|
506
|
+
);
|
|
507
|
+
},
|
|
508
|
+
getPersistedSnapshot() {
|
|
509
|
+
return persistedSnapshotFromEditorSessionState(this.getSessionState(), {
|
|
358
510
|
savedAt: clock(),
|
|
359
|
-
|
|
360
|
-
}) as unknown as PersistedEditorSnapshot;
|
|
511
|
+
});
|
|
361
512
|
},
|
|
362
513
|
getCompatibilityReport() {
|
|
363
514
|
return toPublicCompatibilityReport(createDerivedCompatibility(state));
|
|
@@ -381,14 +532,7 @@ export function createDocumentRuntime(
|
|
|
381
532
|
throw new Error(error.message);
|
|
382
533
|
}
|
|
383
534
|
|
|
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
|
-
);
|
|
535
|
+
const result = await options.exportDocx(this.getSessionState(), exportOptions);
|
|
392
536
|
|
|
393
537
|
emit({
|
|
394
538
|
type: "export_completed",
|
|
@@ -415,7 +559,8 @@ export function createDocumentRuntime(
|
|
|
415
559
|
// Undo/redo changes the document — must mint a new revisionToken so
|
|
416
560
|
// autosave/export checkpoint dedup treats it as fresh content.
|
|
417
561
|
state = finalizeState(target, true, clock());
|
|
418
|
-
|
|
562
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
563
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
419
564
|
notify(previous, state, {
|
|
420
565
|
nextState: state,
|
|
421
566
|
mapping: { steps: [] },
|
|
@@ -437,7 +582,8 @@ export function createDocumentRuntime(
|
|
|
437
582
|
}
|
|
438
583
|
|
|
439
584
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
440
|
-
|
|
585
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
586
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
441
587
|
notify(previous, state, transaction);
|
|
442
588
|
}
|
|
443
589
|
|
|
@@ -458,7 +604,7 @@ export function createDocumentRuntime(
|
|
|
458
604
|
emit({
|
|
459
605
|
type: "selection_changed",
|
|
460
606
|
documentId: next.documentId,
|
|
461
|
-
selection: toPublicSelectionSnapshot(next.selection),
|
|
607
|
+
selection: toPublicSelectionSnapshot(next.selection, activeStory),
|
|
462
608
|
});
|
|
463
609
|
}
|
|
464
610
|
|
|
@@ -519,7 +665,7 @@ export function createDocumentRuntime(
|
|
|
519
665
|
}
|
|
520
666
|
}
|
|
521
667
|
|
|
522
|
-
function emit(event:
|
|
668
|
+
function emit(event: DocumentRuntimeEvent): void {
|
|
523
669
|
options.onEvent?.(event);
|
|
524
670
|
for (const listener of eventListeners) {
|
|
525
671
|
listener(event);
|
|
@@ -533,7 +679,8 @@ export function createDocumentRuntime(
|
|
|
533
679
|
fatalError: error.isFatal ? error : state.fatalError,
|
|
534
680
|
};
|
|
535
681
|
state = nextState;
|
|
536
|
-
|
|
682
|
+
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
683
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
537
684
|
const publicError = toPublicError(error);
|
|
538
685
|
options.onError?.(publicError);
|
|
539
686
|
emit({
|
|
@@ -545,6 +692,39 @@ export function createDocumentRuntime(
|
|
|
545
692
|
listener();
|
|
546
693
|
}
|
|
547
694
|
}
|
|
695
|
+
|
|
696
|
+
function switchActiveStory(target: EditorStoryTarget): void {
|
|
697
|
+
const previousStory = activeStory;
|
|
698
|
+
const previousSelection = state.selection;
|
|
699
|
+
storySelections.set(storyTargetKey(previousStory), previousSelection);
|
|
700
|
+
|
|
701
|
+
const restoredSelection =
|
|
702
|
+
storySelections.get(storyTargetKey(target)) ?? createSelectionSnapshot(0, 0);
|
|
703
|
+
activeStory = target;
|
|
704
|
+
state = {
|
|
705
|
+
...state,
|
|
706
|
+
selection: restoredSelection,
|
|
707
|
+
};
|
|
708
|
+
storySelections.set(storyTargetKey(target), restoredSelection);
|
|
709
|
+
cachedRenderSnapshot = createPublicRenderSnapshot(state, history, activeStory);
|
|
710
|
+
|
|
711
|
+
if (selectionChanged(previousSelection, restoredSelection)) {
|
|
712
|
+
emit({
|
|
713
|
+
type: "selection_changed",
|
|
714
|
+
documentId: state.documentId,
|
|
715
|
+
selection: toPublicSelectionSnapshot(restoredSelection, activeStory),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
emit({
|
|
720
|
+
type: "story_changed",
|
|
721
|
+
documentId: state.documentId,
|
|
722
|
+
activeStory,
|
|
723
|
+
});
|
|
724
|
+
for (const listener of listeners) {
|
|
725
|
+
listener();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
548
728
|
}
|
|
549
729
|
|
|
550
730
|
function createSessionId(documentId: string, timestamp: string): string {
|
|
@@ -623,11 +803,13 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
623
803
|
function createPublicRenderSnapshot(
|
|
624
804
|
state: EditorState,
|
|
625
805
|
history: HistoryState,
|
|
806
|
+
activeStory: EditorStoryTarget,
|
|
626
807
|
): RuntimeRenderSnapshot {
|
|
627
808
|
const compatibility = createDerivedCompatibility(state);
|
|
628
|
-
const surface = createEditorSurfaceSnapshot(state.document, state.selection);
|
|
809
|
+
const surface = createEditorSurfaceSnapshot(state.document, state.selection, activeStory);
|
|
629
810
|
const comments = toPublicCommentSidebarSnapshot(state);
|
|
630
811
|
const trackedChanges = toPublicTrackedChangesSnapshot(state, surface.plainText);
|
|
812
|
+
const pageLayout = derivePageLayoutSnapshot(state, activeStory);
|
|
631
813
|
|
|
632
814
|
return {
|
|
633
815
|
documentId: state.documentId,
|
|
@@ -637,7 +819,9 @@ function createPublicRenderSnapshot(
|
|
|
637
819
|
isReady: state.phase === "ready",
|
|
638
820
|
isDirty: state.isDirty,
|
|
639
821
|
readOnly: state.readOnly,
|
|
640
|
-
selection: toPublicSelectionSnapshot(state.selection),
|
|
822
|
+
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
823
|
+
activeStory,
|
|
824
|
+
pageLayout: pageLayout ?? undefined,
|
|
641
825
|
documentStats: toPublicDocumentStats(state),
|
|
642
826
|
comments,
|
|
643
827
|
trackedChanges,
|
|
@@ -673,12 +857,14 @@ function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
|
673
857
|
|
|
674
858
|
function toPublicSelectionSnapshot(
|
|
675
859
|
selection: EditorState["selection"],
|
|
860
|
+
storyTarget?: EditorStoryTarget,
|
|
676
861
|
): SelectionSnapshot {
|
|
677
862
|
return {
|
|
678
863
|
anchor: selection.anchor,
|
|
679
864
|
head: selection.head,
|
|
680
865
|
isCollapsed: selection.isCollapsed,
|
|
681
866
|
activeRange: toPublicAnchorProjection(selection.activeRange),
|
|
867
|
+
...(storyTarget && storyTarget.kind !== "main" ? { storyTarget } : {}),
|
|
682
868
|
};
|
|
683
869
|
}
|
|
684
870
|
|
|
@@ -1026,6 +1212,50 @@ function summarizeRevisionExcerpt(
|
|
|
1026
1212
|
return collapsed.length > 96 ? `${collapsed.slice(0, 93)}...` : collapsed;
|
|
1027
1213
|
}
|
|
1028
1214
|
|
|
1215
|
+
function isValidStoryTarget(
|
|
1216
|
+
state: EditorState,
|
|
1217
|
+
target: EditorStoryTarget,
|
|
1218
|
+
): boolean {
|
|
1219
|
+
if (target.kind === "main") return true;
|
|
1220
|
+
const subParts = state.document.subParts;
|
|
1221
|
+
if (!subParts) return false;
|
|
1222
|
+
|
|
1223
|
+
switch (target.kind) {
|
|
1224
|
+
case "header":
|
|
1225
|
+
return Boolean(normalizeHeaderFooterTarget(state.document, target));
|
|
1226
|
+
case "footer":
|
|
1227
|
+
return Boolean(normalizeHeaderFooterTarget(state.document, target));
|
|
1228
|
+
case "footnote":
|
|
1229
|
+
return Boolean(subParts.footnoteCollection?.footnotes?.[target.noteId]);
|
|
1230
|
+
case "endnote":
|
|
1231
|
+
return Boolean(subParts.footnoteCollection?.endnotes?.[target.noteId]);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function derivePageLayoutSnapshot(
|
|
1236
|
+
state: EditorState,
|
|
1237
|
+
activeStory: EditorStoryTarget,
|
|
1238
|
+
storySelections?: ReadonlyMap<string, EditorState["selection"]>,
|
|
1239
|
+
): PageLayoutSnapshot | null {
|
|
1240
|
+
const subParts = state.document.subParts;
|
|
1241
|
+
const sections = buildResolvedSections(state.document);
|
|
1242
|
+
if (!subParts && sections.length === 0) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const activeSection = resolveActiveSection(
|
|
1247
|
+
state,
|
|
1248
|
+
activeStory,
|
|
1249
|
+
sections,
|
|
1250
|
+
storySelections,
|
|
1251
|
+
);
|
|
1252
|
+
return buildPageLayoutSnapshot(
|
|
1253
|
+
activeSection?.index ?? 0,
|
|
1254
|
+
activeSection?.properties ?? subParts?.finalSectionProperties,
|
|
1255
|
+
subParts,
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1029
1259
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1030
1260
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1031
1261
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentNavigationSnapshot,
|
|
3
|
+
EditorStoryTarget,
|
|
4
|
+
SearchOptions,
|
|
5
|
+
SearchResultSnapshot,
|
|
6
|
+
SelectionSnapshot,
|
|
7
|
+
} from "../api/public-types";
|
|
8
|
+
import {
|
|
9
|
+
MAIN_STORY_TARGET,
|
|
10
|
+
storyTargetsEqual,
|
|
11
|
+
} from "../core/selection/mapping.ts";
|
|
12
|
+
import {
|
|
13
|
+
createSelectionSnapshot,
|
|
14
|
+
type CanonicalDocumentEnvelope,
|
|
15
|
+
} from "../core/state/editor-state.ts";
|
|
16
|
+
import {
|
|
17
|
+
searchSecondaryStories,
|
|
18
|
+
searchSurfaceBlocks,
|
|
19
|
+
} from "../core/search/search-text.ts";
|
|
20
|
+
import { findPageForOffset } from "./document-navigation.ts";
|
|
21
|
+
import {
|
|
22
|
+
buildResolvedSections,
|
|
23
|
+
resolveSectionForStoryTarget,
|
|
24
|
+
} from "./document-layout.ts";
|
|
25
|
+
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
26
|
+
|
|
27
|
+
export function searchDocument(
|
|
28
|
+
document: CanonicalDocumentEnvelope,
|
|
29
|
+
selection: SelectionSnapshot,
|
|
30
|
+
activeStory: EditorStoryTarget,
|
|
31
|
+
navigation: DocumentNavigationSnapshot,
|
|
32
|
+
query: string,
|
|
33
|
+
options: SearchOptions = {},
|
|
34
|
+
): SearchResultSnapshot[] {
|
|
35
|
+
const normalizedQuery = query.trim();
|
|
36
|
+
if (!normalizedQuery) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
41
|
+
document,
|
|
42
|
+
createSelectionSnapshot(selection.anchor, selection.head),
|
|
43
|
+
MAIN_STORY_TARGET,
|
|
44
|
+
);
|
|
45
|
+
const sections = buildResolvedSections(document);
|
|
46
|
+
const combined: SearchResultSnapshot[] = [];
|
|
47
|
+
|
|
48
|
+
for (const match of searchSurfaceBlocks(mainSurface.blocks, normalizedQuery, options)) {
|
|
49
|
+
const pageIndex = findPageForOffset(navigation.pages, match.from);
|
|
50
|
+
combined.push({
|
|
51
|
+
resultId: `search-main-${combined.length}`,
|
|
52
|
+
anchor: {
|
|
53
|
+
kind: "range",
|
|
54
|
+
from: match.from,
|
|
55
|
+
to: match.to,
|
|
56
|
+
assoc: {
|
|
57
|
+
start: -1,
|
|
58
|
+
end: 1,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
excerpt: match.excerpt,
|
|
62
|
+
isActive: false,
|
|
63
|
+
storyTarget: MAIN_STORY_TARGET,
|
|
64
|
+
sectionIndex: navigation.pages[pageIndex]?.sectionIndex ?? 0,
|
|
65
|
+
pageIndex,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const match of searchSecondaryStories(
|
|
70
|
+
mainSurface.secondaryStories,
|
|
71
|
+
normalizedQuery,
|
|
72
|
+
options,
|
|
73
|
+
)) {
|
|
74
|
+
const section = resolveSectionForStoryTarget(
|
|
75
|
+
document,
|
|
76
|
+
sections,
|
|
77
|
+
match.storyTarget,
|
|
78
|
+
);
|
|
79
|
+
const pageIndex =
|
|
80
|
+
section === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: navigation.pages.find((page) => page.sectionIndex === section.index)
|
|
83
|
+
?.pageIndex ?? 0;
|
|
84
|
+
|
|
85
|
+
combined.push({
|
|
86
|
+
resultId: `search-secondary-${combined.length}`,
|
|
87
|
+
anchor: {
|
|
88
|
+
kind: "range",
|
|
89
|
+
from: match.from,
|
|
90
|
+
to: match.to,
|
|
91
|
+
assoc: {
|
|
92
|
+
start: -1,
|
|
93
|
+
end: 1,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
excerpt: match.excerpt,
|
|
97
|
+
isActive: false,
|
|
98
|
+
storyTarget: match.storyTarget,
|
|
99
|
+
...(section ? { sectionIndex: section.index } : {}),
|
|
100
|
+
...(pageIndex !== undefined ? { pageIndex } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const limited = combined.slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
105
|
+
const activeIndex = getActiveSearchResultIndex(limited, selection, activeStory);
|
|
106
|
+
|
|
107
|
+
return limited.map((result, index) => ({
|
|
108
|
+
...result,
|
|
109
|
+
isActive: index === activeIndex,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getActiveSearchResultIndex(
|
|
114
|
+
results: readonly SearchResultSnapshot[],
|
|
115
|
+
selection: SelectionSnapshot,
|
|
116
|
+
activeStory: EditorStoryTarget,
|
|
117
|
+
): number {
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
123
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
124
|
+
const activeIndex = results.findIndex((result) => {
|
|
125
|
+
if (!result.storyTarget || !storyTargetsEqual(result.storyTarget, activeStory)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result.anchor.kind !== "range") {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (selectionFrom === selectionTo) {
|
|
134
|
+
return (
|
|
135
|
+
selectionFrom >= result.anchor.from && selectionFrom <= result.anchor.to
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
selectionFrom < result.anchor.to && selectionTo > result.anchor.from
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return activeIndex >= 0 ? activeIndex : 0;
|
|
145
|
+
}
|