@beyondwork/docx-react-component 1.0.38 → 1.0.40
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 +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CommandExecutionContext,
|
|
5
|
+
EditorCommand,
|
|
6
|
+
EditorTransaction,
|
|
7
|
+
} from "../../core/commands/index.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CommandAppliedMeta,
|
|
10
|
+
DocumentRuntime,
|
|
11
|
+
Unsubscribe,
|
|
12
|
+
} from "../document-runtime.ts";
|
|
13
|
+
import {
|
|
14
|
+
createCommandEvent,
|
|
15
|
+
isBroadcastCommand,
|
|
16
|
+
isLocalOnlyCommand,
|
|
17
|
+
type CommandEvent,
|
|
18
|
+
} from "./event-types.ts";
|
|
19
|
+
|
|
20
|
+
export type RuntimeCommandAppliedListener = (
|
|
21
|
+
command: EditorCommand,
|
|
22
|
+
transaction: EditorTransaction,
|
|
23
|
+
context: CommandExecutionContext,
|
|
24
|
+
meta: CommandAppliedMeta,
|
|
25
|
+
) => void;
|
|
26
|
+
|
|
27
|
+
export interface RuntimeCommandAppliedBridge {
|
|
28
|
+
onCommandApplied: RuntimeCommandAppliedListener;
|
|
29
|
+
subscribe(listener: RuntimeCommandAppliedListener): Unsubscribe;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RuntimeCollabSyncOptions {
|
|
33
|
+
ydoc: Y.Doc;
|
|
34
|
+
runtime: DocumentRuntime;
|
|
35
|
+
authorId: string;
|
|
36
|
+
commandAppliedBridge: RuntimeCommandAppliedBridge;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RuntimeCollabSyncHandle {
|
|
40
|
+
destroy(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createRuntimeCommandAppliedBridge(): RuntimeCommandAppliedBridge {
|
|
44
|
+
const listeners = new Set<RuntimeCommandAppliedListener>();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
onCommandApplied(command, transaction, context, meta) {
|
|
48
|
+
for (const listener of [...listeners]) {
|
|
49
|
+
listener(command, transaction, context, meta);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRuntimeCollabSync(
|
|
62
|
+
options: RuntimeCollabSyncOptions,
|
|
63
|
+
): RuntimeCollabSyncHandle {
|
|
64
|
+
const { ydoc, runtime, authorId, commandAppliedBridge } = options;
|
|
65
|
+
const yEvents = ydoc.getArray<CommandEvent>("commandEvents");
|
|
66
|
+
const appliedEventIds = new Set<string>();
|
|
67
|
+
|
|
68
|
+
const unsubscribeCommandApplied = commandAppliedBridge.subscribe((command, _transaction, context, meta) => {
|
|
69
|
+
if (isLocalOnlyCommand(command)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!isBroadcastCommand(command)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const event = createCommandEvent({
|
|
77
|
+
command,
|
|
78
|
+
originClientId: ydoc.clientID,
|
|
79
|
+
authorId,
|
|
80
|
+
timestamp: context.timestamp,
|
|
81
|
+
context: {
|
|
82
|
+
documentMode: context.documentMode,
|
|
83
|
+
defaultAuthorId: context.defaultAuthorId,
|
|
84
|
+
preSelection: meta.preSelection,
|
|
85
|
+
activeStory: meta.activeStory,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
appliedEventIds.add(event.eventId);
|
|
90
|
+
yEvents.push([event]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
yEvents.observe(onYEventsChange);
|
|
94
|
+
|
|
95
|
+
for (const value of yEvents.toArray()) {
|
|
96
|
+
applyStartupEvent(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
destroy() {
|
|
101
|
+
unsubscribeCommandApplied();
|
|
102
|
+
yEvents.unobserve(onYEventsChange);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function onYEventsChange(event: Y.YArrayEvent<CommandEvent>): void {
|
|
107
|
+
for (const delta of event.changes.delta) {
|
|
108
|
+
if (!Array.isArray(delta.insert)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const value of delta.insert) {
|
|
113
|
+
applyObservedEvent(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function applyStartupEvent(value: unknown): void {
|
|
119
|
+
const event = asCommandEvent(value);
|
|
120
|
+
if (!event) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (appliedEventIds.has(event.eventId)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!isReplayableEvent(event)) {
|
|
127
|
+
appliedEventIds.add(event.eventId);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
appliedEventIds.add(event.eventId);
|
|
132
|
+
applyEventToRuntime(event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyObservedEvent(value: unknown): void {
|
|
136
|
+
const event = asCommandEvent(value);
|
|
137
|
+
if (!event) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (appliedEventIds.has(event.eventId)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!isReplayableEvent(event)) {
|
|
144
|
+
appliedEventIds.add(event.eventId);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
appliedEventIds.add(event.eventId);
|
|
149
|
+
if (event.originClientId === ydoc.clientID) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
applyEventToRuntime(event);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isReplayableEvent(event: CommandEvent): boolean {
|
|
156
|
+
if (isLocalOnlyCommand(event.command)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (!isBroadcastCommand(event.command)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyEventToRuntime(event: CommandEvent): void {
|
|
166
|
+
runtime.applyRemoteCommand(
|
|
167
|
+
event.command,
|
|
168
|
+
{
|
|
169
|
+
timestamp: event.timestamp,
|
|
170
|
+
documentMode: event.context.documentMode,
|
|
171
|
+
defaultAuthorId: event.context.defaultAuthorId ?? event.authorId,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
preSelection: event.context.preSelection,
|
|
175
|
+
activeStory: event.context.activeStory,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asCommandEvent(value: unknown): CommandEvent | null {
|
|
182
|
+
if (!isRecord(value)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value.eventId !== "string") {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value.originClientId !== "number" || !Number.isFinite(value.originClientId)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (typeof value.authorId !== "string") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const timestamp = normalizeTimestamp(value.timestamp);
|
|
195
|
+
if (timestamp === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const command = asEditorCommand(value.command);
|
|
200
|
+
const context = asCommandEventContext(value.context);
|
|
201
|
+
if (!command || !context) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
eventId: value.eventId,
|
|
207
|
+
originClientId: value.originClientId,
|
|
208
|
+
authorId: value.authorId,
|
|
209
|
+
timestamp,
|
|
210
|
+
command,
|
|
211
|
+
context,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeTimestamp(value: unknown): string | null {
|
|
216
|
+
if (typeof value === "string" && value.length > 0) {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
220
|
+
return new Date(value).toISOString();
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function asEditorCommand(value: unknown): EditorCommand | null {
|
|
226
|
+
if (!isRecord(value)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (typeof value.type !== "string") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return value as EditorCommand;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function asCommandEventContext(value: unknown): CommandEvent["context"] | null {
|
|
236
|
+
if (!isRecord(value)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
documentMode: isCommandDocumentMode(value.documentMode) ? value.documentMode : undefined,
|
|
242
|
+
defaultAuthorId: typeof value.defaultAuthorId === "string" ? value.defaultAuthorId : undefined,
|
|
243
|
+
preSelection: asSelectionSnapshot(value.preSelection),
|
|
244
|
+
activeStory: asStoryTarget(value.activeStory),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function asSelectionSnapshot(value: unknown): CommandEvent["context"]["preSelection"] {
|
|
249
|
+
if (!isRecord(value)) return undefined;
|
|
250
|
+
if (typeof value.anchor !== "number" || typeof value.head !== "number") return undefined;
|
|
251
|
+
if (typeof value.isCollapsed !== "boolean") return undefined;
|
|
252
|
+
if (!isRecord(value.activeRange)) return undefined;
|
|
253
|
+
return value as unknown as CommandEvent["context"]["preSelection"];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function asStoryTarget(value: unknown): CommandEvent["context"]["activeStory"] {
|
|
257
|
+
if (!isRecord(value)) return undefined;
|
|
258
|
+
if (typeof value.kind !== "string") return undefined;
|
|
259
|
+
return value as unknown as CommandEvent["context"]["activeStory"];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isCommandDocumentMode(
|
|
263
|
+
value: unknown,
|
|
264
|
+
): value is NonNullable<CommandEvent["context"]["documentMode"]> {
|
|
265
|
+
return value === "editing"
|
|
266
|
+
|| value === "suggesting"
|
|
267
|
+
|| value === "viewing"
|
|
268
|
+
|| value === "commenting";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
272
|
+
return typeof value === "object" && value !== null;
|
|
273
|
+
}
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
persistedSnapshotFromEditorSessionState,
|
|
83
83
|
} from "../api/session-state.ts";
|
|
84
84
|
import {
|
|
85
|
+
type CommandExecutionContext,
|
|
85
86
|
executeEditorCommand,
|
|
86
87
|
selectionChanged,
|
|
87
88
|
type CommandOrigin,
|
|
@@ -179,6 +180,8 @@ import {
|
|
|
179
180
|
setCaretAffinity as applyCaretAffinity,
|
|
180
181
|
setActivePageRegion as applyActivePageRegion,
|
|
181
182
|
setActiveObjectFrame as applyActiveObjectFrame,
|
|
183
|
+
setEditorRole as applyEditorRole,
|
|
184
|
+
setChromePin as applyChromePins,
|
|
182
185
|
createEditorViewStateSnapshot,
|
|
183
186
|
type ViewState,
|
|
184
187
|
} from "./view-state.ts";
|
|
@@ -229,6 +232,24 @@ export interface DocumentRuntime {
|
|
|
229
232
|
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
230
233
|
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
|
|
231
234
|
dispatch(command: EditorCommand): void;
|
|
235
|
+
/**
|
|
236
|
+
* Apply a command received from a remote collaborator. The command
|
|
237
|
+
* executes through `executeEditorCommand` exactly like a local dispatch,
|
|
238
|
+
* but:
|
|
239
|
+
* - `onCommandApplied` is NOT fired (no echo back to the network)
|
|
240
|
+
* - the local undo/redo history is NOT mutated (remote edits are
|
|
241
|
+
* not undoable by the local user)
|
|
242
|
+
* - local workflow/blocked-command checks are bypassed (the remote
|
|
243
|
+
* already performed them)
|
|
244
|
+
*
|
|
245
|
+
* Used by runtime-level collaboration sync to replay `CommandEvent`s
|
|
246
|
+
* from the shared event log.
|
|
247
|
+
*/
|
|
248
|
+
applyRemoteCommand(
|
|
249
|
+
command: EditorCommand,
|
|
250
|
+
context: CommandExecutionContext,
|
|
251
|
+
meta?: Partial<CommandAppliedMeta>,
|
|
252
|
+
): void;
|
|
232
253
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
233
254
|
undo(): void;
|
|
234
255
|
redo(): void;
|
|
@@ -255,6 +276,8 @@ export interface DocumentRuntime {
|
|
|
255
276
|
getProtectionSnapshot(): ProtectionSnapshot;
|
|
256
277
|
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
257
278
|
setZoom(level: ZoomLevel): void;
|
|
279
|
+
setEditorRole(role: import("./view-state.ts").ViewState["editorRole"]): void;
|
|
280
|
+
setChromePin(surface: import("../api/public-types.ts").ChromePinSurface, pin: import("../api/public-types.ts").PinState | null): void;
|
|
258
281
|
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
259
282
|
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
260
283
|
/**
|
|
@@ -319,6 +342,11 @@ export interface DocumentRuntime {
|
|
|
319
342
|
): RuntimeContextAnalyticsSnapshot | null;
|
|
320
343
|
}
|
|
321
344
|
|
|
345
|
+
export interface CommandAppliedMeta {
|
|
346
|
+
preSelection: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
347
|
+
activeStory: EditorStoryTarget;
|
|
348
|
+
}
|
|
349
|
+
|
|
322
350
|
export interface CreateDocumentRuntimeOptions {
|
|
323
351
|
documentId: string;
|
|
324
352
|
initialSessionState?: EditorSessionState;
|
|
@@ -338,6 +366,23 @@ export interface CreateDocumentRuntimeOptions {
|
|
|
338
366
|
onEvent?: (event: DocumentRuntimeEvent) => void;
|
|
339
367
|
onWarning?: (warning: EditorWarning) => void;
|
|
340
368
|
onError?: (error: EditorError) => void;
|
|
369
|
+
/**
|
|
370
|
+
* Fired AFTER a command has been applied locally via `dispatch()` or
|
|
371
|
+
* `applyActiveStoryTextCommand()`. Used by collaboration sync to
|
|
372
|
+
* broadcast the command to remote clients. NOT fired for remote commands
|
|
373
|
+
* applied via `applyRemoteCommand()` — this prevents echo loops.
|
|
374
|
+
*
|
|
375
|
+
* Not fired for:
|
|
376
|
+
* - `history.undo` / `history.redo` (applied via `applyHistory()` which
|
|
377
|
+
* does not pass through `commit()`'s new hook call site)
|
|
378
|
+
* - Remote replays applied via `applyRemoteCommand()`
|
|
379
|
+
*/
|
|
380
|
+
onCommandApplied?: (
|
|
381
|
+
command: EditorCommand,
|
|
382
|
+
transaction: EditorTransaction,
|
|
383
|
+
context: CommandExecutionContext,
|
|
384
|
+
meta: CommandAppliedMeta,
|
|
385
|
+
) => void;
|
|
341
386
|
initialViewState?: Partial<ViewState>;
|
|
342
387
|
protectionSnapshot?: ProtectionSnapshot;
|
|
343
388
|
}
|
|
@@ -451,6 +496,13 @@ export function createDocumentRuntime(
|
|
|
451
496
|
activeStory,
|
|
452
497
|
};
|
|
453
498
|
},
|
|
499
|
+
// R2 / scope-card-overlay P1 — surface metadata markup so
|
|
500
|
+
// `facet.getAllScopeCardModels()` can attach `IssueMetadataValue`
|
|
501
|
+
// to its scope without the chrome overlay having to re-fetch a
|
|
502
|
+
// separate snapshot. Reads through the same cached snapshot the
|
|
503
|
+
// runtime already builds for comment/revision/search consumers.
|
|
504
|
+
getWorkflowMarkupMetadata: () =>
|
|
505
|
+
getCachedWorkflowMarkupSnapshot().metadata,
|
|
454
506
|
});
|
|
455
507
|
renderKernelRef = createRenderKernel({
|
|
456
508
|
facet: layoutFacet,
|
|
@@ -1611,12 +1663,40 @@ export function createDocumentRuntime(
|
|
|
1611
1663
|
return;
|
|
1612
1664
|
}
|
|
1613
1665
|
try {
|
|
1614
|
-
const
|
|
1615
|
-
timestamp: command.origin?.timestamp ?? clock(),
|
|
1666
|
+
const context = {
|
|
1667
|
+
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1616
1668
|
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1617
1669
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1618
|
-
}
|
|
1670
|
+
} as const;
|
|
1671
|
+
const preSelection = commandSelection;
|
|
1672
|
+
const preActiveStory = activeStory;
|
|
1673
|
+
const transaction = executeEditorCommand(state, command, context);
|
|
1619
1674
|
commit(transaction);
|
|
1675
|
+
options.onCommandApplied?.(command, transaction, context, {
|
|
1676
|
+
preSelection,
|
|
1677
|
+
activeStory: preActiveStory,
|
|
1678
|
+
});
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
emitError(toRuntimeError(error));
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
applyRemoteCommand(command, context, meta) {
|
|
1684
|
+
try {
|
|
1685
|
+
if (command.type === "history.undo" || command.type === "history.redo") {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory)) {
|
|
1689
|
+
activeStory = meta.activeStory;
|
|
1690
|
+
storySelections.set(
|
|
1691
|
+
storyTargetKey(activeStory),
|
|
1692
|
+
meta.preSelection ?? state.selection,
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
const replayState = meta?.preSelection
|
|
1696
|
+
? { ...state, selection: meta.preSelection }
|
|
1697
|
+
: state;
|
|
1698
|
+
const transaction = executeEditorCommand(replayState, command, context);
|
|
1699
|
+
commitRemote(transaction);
|
|
1620
1700
|
} catch (error) {
|
|
1621
1701
|
emitError(toRuntimeError(error));
|
|
1622
1702
|
}
|
|
@@ -1890,6 +1970,20 @@ export function createDocumentRuntime(
|
|
|
1890
1970
|
listener();
|
|
1891
1971
|
}
|
|
1892
1972
|
},
|
|
1973
|
+
setEditorRole(role) {
|
|
1974
|
+
viewState = applyEditorRole(viewState, role);
|
|
1975
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1976
|
+
for (const listener of listeners) {
|
|
1977
|
+
listener();
|
|
1978
|
+
}
|
|
1979
|
+
},
|
|
1980
|
+
setChromePin(surface, pin) {
|
|
1981
|
+
viewState = applyChromePins(viewState, surface, pin);
|
|
1982
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1983
|
+
for (const listener of listeners) {
|
|
1984
|
+
listener();
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1893
1987
|
getPageLayoutSnapshot() {
|
|
1894
1988
|
return getCachedPageLayoutSnapshot(state, activeStory);
|
|
1895
1989
|
},
|
|
@@ -2346,13 +2440,21 @@ export function createDocumentRuntime(
|
|
|
2346
2440
|
}
|
|
2347
2441
|
|
|
2348
2442
|
function commit(transaction: EditorTransaction): void {
|
|
2349
|
-
const previous = state;
|
|
2350
|
-
|
|
2351
2443
|
if (transaction.historyBoundary === "push") {
|
|
2352
2444
|
history.past.push(state);
|
|
2353
2445
|
history.future = [];
|
|
2354
2446
|
}
|
|
2355
2447
|
|
|
2448
|
+
applyTransactionToState(transaction);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function commitRemote(transaction: EditorTransaction): void {
|
|
2452
|
+
applyTransactionToState(transaction);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function applyTransactionToState(transaction: EditorTransaction): void {
|
|
2456
|
+
const previous = state;
|
|
2457
|
+
|
|
2356
2458
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
2357
2459
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
2358
2460
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
@@ -2562,13 +2664,13 @@ export function createDocumentRuntime(
|
|
|
2562
2664
|
|
|
2563
2665
|
function applyTextCommandInActiveStory(
|
|
2564
2666
|
command: ActiveStoryTextCommand,
|
|
2565
|
-
|
|
2667
|
+
textOptions: {
|
|
2566
2668
|
selection?: EditorState["selection"];
|
|
2567
2669
|
blockedCommandName?: string;
|
|
2568
2670
|
} = {},
|
|
2569
2671
|
): TextCommandAck {
|
|
2570
2672
|
const opId = (command.origin as { opId?: string } | undefined)?.opId;
|
|
2571
|
-
const selection =
|
|
2673
|
+
const selection = textOptions.selection ?? state.selection;
|
|
2572
2674
|
if (
|
|
2573
2675
|
activeStory.kind !== "main" &&
|
|
2574
2676
|
getEffectiveDocumentMode(selection) === "suggesting" &&
|
|
@@ -2578,7 +2680,7 @@ export function createDocumentRuntime(
|
|
|
2578
2680
|
emit({
|
|
2579
2681
|
type: "command_blocked",
|
|
2580
2682
|
documentId: state.documentId,
|
|
2581
|
-
command:
|
|
2683
|
+
command: textOptions.blockedCommandName ?? command.type,
|
|
2582
2684
|
reasons: [{
|
|
2583
2685
|
code: "suggesting_unsupported",
|
|
2584
2686
|
message,
|
|
@@ -2597,7 +2699,7 @@ export function createDocumentRuntime(
|
|
|
2597
2699
|
emit({
|
|
2598
2700
|
type: "command_blocked",
|
|
2599
2701
|
documentId: state.documentId,
|
|
2600
|
-
command:
|
|
2702
|
+
command: textOptions.blockedCommandName ?? command.type,
|
|
2601
2703
|
reasons: blockedReasons,
|
|
2602
2704
|
});
|
|
2603
2705
|
return {
|
|
@@ -2608,7 +2710,7 @@ export function createDocumentRuntime(
|
|
|
2608
2710
|
};
|
|
2609
2711
|
}
|
|
2610
2712
|
|
|
2611
|
-
const timestamp = command.origin?.timestamp ?? clock();
|
|
2713
|
+
const timestamp = normalizeCommandTimestamp(command.origin?.timestamp) ?? clock();
|
|
2612
2714
|
const context = {
|
|
2613
2715
|
timestamp,
|
|
2614
2716
|
documentMode: getEffectiveDocumentMode(selection),
|
|
@@ -2621,9 +2723,15 @@ export function createDocumentRuntime(
|
|
|
2621
2723
|
selection,
|
|
2622
2724
|
};
|
|
2623
2725
|
|
|
2726
|
+
const preSelection = selection;
|
|
2727
|
+
const preActiveStory = activeStory;
|
|
2624
2728
|
if (activeStory.kind === "main") {
|
|
2625
2729
|
const mainTransaction = executeEditorCommand(baseState, command, context);
|
|
2626
2730
|
commit(mainTransaction);
|
|
2731
|
+
options.onCommandApplied?.(command, mainTransaction, context, {
|
|
2732
|
+
preSelection,
|
|
2733
|
+
activeStory: preActiveStory,
|
|
2734
|
+
});
|
|
2627
2735
|
return classifyAck({
|
|
2628
2736
|
command,
|
|
2629
2737
|
opId,
|
|
@@ -2682,16 +2790,17 @@ export function createDocumentRuntime(
|
|
|
2682
2790
|
activeStory,
|
|
2683
2791
|
),
|
|
2684
2792
|
};
|
|
2793
|
+
const broadcastCommand: EditorCommand = {
|
|
2794
|
+
type: "document.replace",
|
|
2795
|
+
document: nextDocumentWithReview,
|
|
2796
|
+
selection: localTransaction.nextState.selection,
|
|
2797
|
+
mapping: createEmptyMapping(),
|
|
2798
|
+
protectionSelection: selection,
|
|
2799
|
+
origin: command.origin,
|
|
2800
|
+
};
|
|
2685
2801
|
const fullTransaction = executeEditorCommand(
|
|
2686
2802
|
baseState,
|
|
2687
|
-
|
|
2688
|
-
type: "document.replace",
|
|
2689
|
-
document: nextDocumentWithReview,
|
|
2690
|
-
selection: localTransaction.nextState.selection,
|
|
2691
|
-
mapping: createEmptyMapping(),
|
|
2692
|
-
protectionSelection: selection,
|
|
2693
|
-
origin: command.origin,
|
|
2694
|
-
},
|
|
2803
|
+
broadcastCommand,
|
|
2695
2804
|
context,
|
|
2696
2805
|
);
|
|
2697
2806
|
|
|
@@ -2700,6 +2809,10 @@ export function createDocumentRuntime(
|
|
|
2700
2809
|
effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
|
|
2701
2810
|
};
|
|
2702
2811
|
commit(mergedTransaction);
|
|
2812
|
+
options.onCommandApplied?.(broadcastCommand, mergedTransaction, context, {
|
|
2813
|
+
preSelection,
|
|
2814
|
+
activeStory: preActiveStory,
|
|
2815
|
+
});
|
|
2703
2816
|
return classifyAck({
|
|
2704
2817
|
command,
|
|
2705
2818
|
opId,
|
|
@@ -2988,6 +3101,16 @@ function createEntityId(
|
|
|
2988
3101
|
return nextId;
|
|
2989
3102
|
}
|
|
2990
3103
|
|
|
3104
|
+
function normalizeCommandTimestamp(value: unknown): string | undefined {
|
|
3105
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3106
|
+
return value;
|
|
3107
|
+
}
|
|
3108
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3109
|
+
return new Date(value).toISOString();
|
|
3110
|
+
}
|
|
3111
|
+
return undefined;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
2991
3114
|
function finalizeState(
|
|
2992
3115
|
state: EditorState,
|
|
2993
3116
|
markDirty: boolean,
|
|
@@ -45,12 +45,35 @@ export interface DocxFontLoader {
|
|
|
45
45
|
refresh(input: FontLoaderInput): void;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface MinimalFontFace {
|
|
49
|
+
load(): Promise<MinimalFontFace>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface MinimalFontFaceDescriptors {
|
|
53
|
+
weight?: string;
|
|
54
|
+
style?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface MinimalFontFaceConstructor {
|
|
58
|
+
new (
|
|
59
|
+
family: string,
|
|
60
|
+
source: ArrayBuffer | ArrayBufferView | string,
|
|
61
|
+
descriptors?: MinimalFontFaceDescriptors,
|
|
62
|
+
): MinimalFontFace;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface MinimalFontFaceSet {
|
|
66
|
+
add(face: MinimalFontFace): void;
|
|
67
|
+
check(font: string): boolean;
|
|
68
|
+
ready: Promise<MinimalFontFaceSet>;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
72
|
+
const globalDocument = (globalThis as { document?: { fonts?: MinimalFontFaceSet } }).document;
|
|
49
73
|
const supported =
|
|
50
|
-
|
|
74
|
+
globalDocument !== undefined &&
|
|
51
75
|
typeof (globalThis as { FontFace?: unknown }).FontFace !== "undefined" &&
|
|
52
|
-
|
|
53
|
-
Boolean((document as Document & { fonts?: FontFaceSet }).fonts);
|
|
76
|
+
Boolean(globalDocument.fonts);
|
|
54
77
|
|
|
55
78
|
let current: FontLoaderInput = initial;
|
|
56
79
|
let readyPromise: Promise<void>;
|
|
@@ -58,7 +81,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
58
81
|
|
|
59
82
|
function run(input: FontLoaderInput): Promise<void> {
|
|
60
83
|
if (!supported) return Promise.resolve();
|
|
61
|
-
const fontSet =
|
|
84
|
+
const fontSet = globalDocument?.fonts;
|
|
62
85
|
if (!fontSet) return Promise.resolve();
|
|
63
86
|
|
|
64
87
|
const pending: Array<Promise<unknown>> = [];
|
|
@@ -70,10 +93,8 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
70
93
|
|
|
71
94
|
for (const [descriptor, data] of variantsOf(variants)) {
|
|
72
95
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
new (family: string, source: BufferSource, descriptors?: FontFaceDescriptors): FontFace;
|
|
76
|
-
};
|
|
96
|
+
const FontFaceCtor = (globalThis as { FontFace?: MinimalFontFaceConstructor }).FontFace;
|
|
97
|
+
if (!FontFaceCtor) continue;
|
|
77
98
|
const face = new FontFaceCtor(family, data, descriptor);
|
|
78
99
|
pending.push(
|
|
79
100
|
face.load().then((loaded) => {
|
|
@@ -88,8 +109,6 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
// Mark declared families as registered if the browser already resolves
|
|
92
|
-
// them (e.g. system fonts like Calibri, Arial).
|
|
93
112
|
for (const family of input.families) {
|
|
94
113
|
try {
|
|
95
114
|
const probe = `12px "${family.replace(/"/g, "'")}", serif`;
|
|
@@ -127,7 +146,7 @@ export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
|
127
146
|
|
|
128
147
|
function* variantsOf(
|
|
129
148
|
variants: EmbeddedFontBytes,
|
|
130
|
-
): IterableIterator<[
|
|
149
|
+
): IterableIterator<[MinimalFontFaceDescriptors, ArrayBuffer]> {
|
|
131
150
|
if (variants.regular) {
|
|
132
151
|
yield [{ weight: "400", style: "normal" }, variants.regular];
|
|
133
152
|
}
|
|
@@ -158,6 +158,8 @@ export {
|
|
|
158
158
|
type EmbeddedFontBytes,
|
|
159
159
|
} from "./docx-font-loader.ts";
|
|
160
160
|
|
|
161
|
+
export { createCanvasBackend } from "./measurement-backend-canvas.ts";
|
|
162
|
+
|
|
161
163
|
// ---------------------------------------------------------------------------
|
|
162
164
|
// Public facet (Phase 7)
|
|
163
165
|
// ---------------------------------------------------------------------------
|
|
@@ -46,11 +46,14 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
46
46
|
getAnchorRects: () => [],
|
|
47
47
|
getScopeRailSegments: () => [],
|
|
48
48
|
getAllScopeRailSegments: () => [],
|
|
49
|
+
getAllScopeCardModels: () => [],
|
|
49
50
|
getResolvedFormatting: () => null,
|
|
50
51
|
getResolvedRunFormatting: () => null,
|
|
51
52
|
getMeasurement: () => null,
|
|
52
53
|
getMeasurementFidelity: () => fidelity,
|
|
53
54
|
whenMeasurementReady: () => Promise.resolve(),
|
|
55
|
+
getFirstPageIndexForBlock: () => null,
|
|
56
|
+
swapMeasurementProvider: () => undefined,
|
|
54
57
|
getTableRenderPlan: () => null,
|
|
55
58
|
getDirtyFieldFamilies: () => [],
|
|
56
59
|
getFieldDirtinessReport: () => emptyReport,
|