@beyondwork/docx-react-component 1.0.46 → 1.0.48
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 +115 -1
- package/src/compare/diff-engine.ts +4 -0
- package/src/core/commands/add-scope.ts +257 -0
- package/src/core/commands/formatting-commands.ts +2 -0
- package/src/core/commands/index.ts +120 -1
- package/src/core/schema/text-schema.ts +95 -1
- package/src/core/state/text-transaction.ts +17 -5
- package/src/io/chart-preview-resolver.ts +27 -0
- package/src/io/docx-session.ts +219 -2
- package/src/io/export/serialize-main-document.ts +37 -0
- package/src/io/export/serialize-settings.ts +421 -0
- package/src/io/export/serialize-styles.ts +10 -0
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/chart/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +813 -0
- package/src/io/ooxml/chart/parse-series.ts +570 -0
- package/src/io/ooxml/chart/resolve-color.ts +251 -0
- package/src/io/ooxml/chart/types.ts +420 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +87 -2
- package/src/io/ooxml/parse-main-document.ts +115 -1
- package/src/io/ooxml/parse-scope-markers.ts +184 -0
- package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
- package/src/io/ooxml/parse-settings.ts +97 -1
- package/src/io/ooxml/parse-styles.ts +65 -0
- package/src/io/ooxml/parse-theme.ts +2 -127
- package/src/io/ooxml/workflow-payload.ts +27 -0
- package/src/io/ooxml/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +94 -0
- package/src/model/scope-markers.ts +144 -0
- package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
- package/src/runtime/collab/checkpoint-election.ts +75 -0
- package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
- package/src/runtime/collab/checkpoint-store.ts +115 -0
- package/src/runtime/collab/event-types.ts +37 -5
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +404 -1
- package/src/runtime/document-runtime.ts +221 -16
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/layout/layout-engine-version.ts +27 -2
- package/src/runtime/prerender/cache-envelope.ts +19 -7
- package/src/runtime/prerender/cache-key.ts +25 -14
- package/src/runtime/prerender/canonical-document-hash.ts +63 -0
- package/src/runtime/prerender/customxml-cache.ts +211 -0
- package/src/runtime/prerender/customxml-probe.ts +78 -0
- package/src/runtime/prerender/prerender-document.ts +74 -7
- package/src/runtime/scope-resolver.ts +148 -0
- package/src/runtime/scope-tag-registry.ts +10 -0
- package/src/runtime/surface-projection.ts +8 -1
- package/src/runtime/text-ack-range.ts +3 -3
- package/src/ui/WordReviewEditor.tsx +30 -0
- package/src/ui/editor-runtime-boundary.ts +6 -1
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
AddCommentParams,
|
|
17
17
|
AddCommentReplyResult,
|
|
18
18
|
AddCommentResult,
|
|
19
|
+
AddScopeParams,
|
|
20
|
+
AddScopeResult,
|
|
19
21
|
CommentSidebarSnapshot,
|
|
20
22
|
CommentSidebarThreadSnapshot,
|
|
21
23
|
CompatibilityReport,
|
|
@@ -74,6 +76,7 @@ import type {
|
|
|
74
76
|
WorkflowMetadataSnapshot,
|
|
75
77
|
WorkflowMarkupSnapshot,
|
|
76
78
|
WorkflowOverlay,
|
|
79
|
+
WorkflowScope,
|
|
77
80
|
WorkflowScopeSnapshot,
|
|
78
81
|
WorkspaceMode,
|
|
79
82
|
WordReviewEditorEvent,
|
|
@@ -116,6 +119,11 @@ import {
|
|
|
116
119
|
} from "../review/store/revision-store.ts";
|
|
117
120
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
118
121
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
122
|
+
import { resolveScope } from "./scope-resolver.ts";
|
|
123
|
+
import {
|
|
124
|
+
insertScopeMarkers,
|
|
125
|
+
removeScopeMarkers,
|
|
126
|
+
} from "../core/commands/add-scope.ts";
|
|
119
127
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
120
128
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
121
129
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
@@ -291,6 +299,9 @@ export interface DocumentRuntime {
|
|
|
291
299
|
reopenComment(commentId: string): void;
|
|
292
300
|
addCommentReply(commentId: string, body: string, authorId?: string): AddCommentReplyResult;
|
|
293
301
|
editCommentBody(commentId: string, body: string): void;
|
|
302
|
+
addScope(params: AddScopeParams): AddScopeResult;
|
|
303
|
+
getScope(scopeId: string): WorkflowScope | null;
|
|
304
|
+
removeScope(scopeId: string): void;
|
|
294
305
|
acceptChange(changeId: string): void;
|
|
295
306
|
rejectChange(changeId: string): void;
|
|
296
307
|
acceptAllChanges(): void;
|
|
@@ -406,6 +417,7 @@ export interface DocumentRuntime {
|
|
|
406
417
|
export interface CommandAppliedMeta {
|
|
407
418
|
preSelection: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
408
419
|
activeStory: EditorStoryTarget;
|
|
420
|
+
priorDocument: CanonicalDocumentEnvelope;
|
|
409
421
|
}
|
|
410
422
|
|
|
411
423
|
export interface CreateDocumentRuntimeOptions {
|
|
@@ -2034,6 +2046,16 @@ export function createDocumentRuntime(
|
|
|
2034
2046
|
}
|
|
2035
2047
|
});
|
|
2036
2048
|
|
|
2049
|
+
// R5 scratch snapshot: single pre-allocated object reused for every
|
|
2050
|
+
// `applyRemoteCommand(cmd, ctx, meta)` call with a `meta.preSelection`
|
|
2051
|
+
// override. Avoids the per-remote-command `{ ...cachedRenderSnapshot }`
|
|
2052
|
+
// + `{ ...state }` allocations that CLAUDE.md rule 4 warns against.
|
|
2053
|
+
// Mutated in place below — callers MUST NOT hold references to it
|
|
2054
|
+
// across dispatches; `executeEditorCommand` + `commitRemote` consume
|
|
2055
|
+
// synchronously within `applyRemoteCommand`, so this is safe today.
|
|
2056
|
+
const r5ScratchReplayState: typeof state = { ...state };
|
|
2057
|
+
const r5ScratchReplaySnapshot: typeof cachedRenderSnapshot = { ...cachedRenderSnapshot };
|
|
2058
|
+
|
|
2037
2059
|
return {
|
|
2038
2060
|
subscribe(listener) {
|
|
2039
2061
|
listeners.add(listener);
|
|
@@ -2111,6 +2133,7 @@ export function createDocumentRuntime(
|
|
|
2111
2133
|
options.onCommandApplied?.(command, noopTransaction, context, {
|
|
2112
2134
|
preSelection: state.selection,
|
|
2113
2135
|
activeStory,
|
|
2136
|
+
priorDocument: state.document,
|
|
2114
2137
|
});
|
|
2115
2138
|
return;
|
|
2116
2139
|
}
|
|
@@ -2124,11 +2147,13 @@ export function createDocumentRuntime(
|
|
|
2124
2147
|
} as const;
|
|
2125
2148
|
const preSelection = commandSelection;
|
|
2126
2149
|
const preActiveStory = activeStory;
|
|
2150
|
+
const priorDocument = state.document;
|
|
2127
2151
|
const transaction = executeEditorCommand(state, command, context);
|
|
2128
2152
|
commit(transaction);
|
|
2129
2153
|
options.onCommandApplied?.(command, transaction, context, {
|
|
2130
2154
|
preSelection,
|
|
2131
2155
|
activeStory: preActiveStory,
|
|
2156
|
+
priorDocument,
|
|
2132
2157
|
});
|
|
2133
2158
|
} catch (error) {
|
|
2134
2159
|
emitError(toRuntimeError(error));
|
|
@@ -2143,28 +2168,56 @@ export function createDocumentRuntime(
|
|
|
2143
2168
|
applyRuntimeStateOverlayCommand(command);
|
|
2144
2169
|
return;
|
|
2145
2170
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2171
|
+
// Story-target isolation: the remote's `meta.activeStory` scopes
|
|
2172
|
+
// the replay (so the command lands in the intended region of the
|
|
2173
|
+
// shared document), but must NEVER overwrite the local user's
|
|
2174
|
+
// closure-level `activeStory`. Before P11 this assignment stole
|
|
2175
|
+
// focus every time a remote event arrived for a different story —
|
|
2176
|
+
// a local user authoring in a header would get yanked into the
|
|
2177
|
+
// main body as soon as a peer edited main.
|
|
2178
|
+
const replayStory = meta?.activeStory ?? activeStory;
|
|
2179
|
+
const crossStoryReplay = Boolean(
|
|
2180
|
+
meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory),
|
|
2181
|
+
);
|
|
2182
|
+
let replayState: typeof state;
|
|
2183
|
+
let replaySnapshot: typeof cachedRenderSnapshot;
|
|
2184
|
+
if (meta?.preSelection) {
|
|
2185
|
+
// Refresh scratch with current `state` / `cachedRenderSnapshot` (both
|
|
2186
|
+
// mutate on every commit), then override only the selection fields.
|
|
2187
|
+
Object.assign(r5ScratchReplayState, state);
|
|
2188
|
+
r5ScratchReplayState.selection = meta.preSelection;
|
|
2189
|
+
Object.assign(r5ScratchReplaySnapshot, cachedRenderSnapshot);
|
|
2190
|
+
r5ScratchReplaySnapshot.selection = toPublicSelectionSnapshot(meta.preSelection, replayStory);
|
|
2191
|
+
replayState = r5ScratchReplayState;
|
|
2192
|
+
replaySnapshot = r5ScratchReplaySnapshot;
|
|
2193
|
+
} else {
|
|
2194
|
+
replayState = state;
|
|
2195
|
+
replaySnapshot = cachedRenderSnapshot;
|
|
2152
2196
|
}
|
|
2153
|
-
const replayState = meta?.preSelection
|
|
2154
|
-
? { ...state, selection: meta.preSelection }
|
|
2155
|
-
: state;
|
|
2156
|
-
const replaySnapshot = meta?.preSelection
|
|
2157
|
-
? {
|
|
2158
|
-
...cachedRenderSnapshot,
|
|
2159
|
-
selection: toPublicSelectionSnapshot(meta.preSelection, activeStory),
|
|
2160
|
-
}
|
|
2161
|
-
: cachedRenderSnapshot;
|
|
2162
2197
|
const replayContext = {
|
|
2163
2198
|
...context,
|
|
2164
2199
|
renderSnapshot: replaySnapshot,
|
|
2165
2200
|
};
|
|
2166
2201
|
const transaction = executeEditorCommand(replayState, command, replayContext);
|
|
2167
|
-
|
|
2202
|
+
if (crossStoryReplay) {
|
|
2203
|
+
// Cross-story replay: the transaction's resulting selection is
|
|
2204
|
+
// in the remote story's region. Don't leak that into local
|
|
2205
|
+
// `state.selection` — preserve the pre-replay local selection
|
|
2206
|
+
// so the local caret stays where the user is focused. The
|
|
2207
|
+
// document delta still applies; only the selection is filtered.
|
|
2208
|
+
// Full position-mapping of the local cursor through the remote
|
|
2209
|
+
// edit is a separate P11 sub-bullet (Awareness cursor transaction
|
|
2210
|
+
// mapping).
|
|
2211
|
+
commitRemote({
|
|
2212
|
+
...transaction,
|
|
2213
|
+
nextState: {
|
|
2214
|
+
...transaction.nextState,
|
|
2215
|
+
selection: state.selection,
|
|
2216
|
+
},
|
|
2217
|
+
});
|
|
2218
|
+
} else {
|
|
2219
|
+
commitRemote(transaction);
|
|
2220
|
+
}
|
|
2168
2221
|
} catch (error) {
|
|
2169
2222
|
emitError(toRuntimeError(error));
|
|
2170
2223
|
}
|
|
@@ -2358,6 +2411,155 @@ export function createDocumentRuntime(
|
|
|
2358
2411
|
origin: createOrigin("api", clock()),
|
|
2359
2412
|
});
|
|
2360
2413
|
},
|
|
2414
|
+
addScope(params): AddScopeResult {
|
|
2415
|
+
const scopeId =
|
|
2416
|
+
params.scopeId ??
|
|
2417
|
+
`scope-${clock().replace(/[^0-9]/gu, "")}-${Math.floor(Math.random() * 1e6)}`;
|
|
2418
|
+
const anchor =
|
|
2419
|
+
params.anchor.kind === "range"
|
|
2420
|
+
? { from: params.anchor.from, to: params.anchor.to }
|
|
2421
|
+
: null;
|
|
2422
|
+
|
|
2423
|
+
if (!anchor) {
|
|
2424
|
+
return {
|
|
2425
|
+
scopeId,
|
|
2426
|
+
anchor: params.anchor,
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const { document: nextDocument } = insertScopeMarkers(state.document, {
|
|
2431
|
+
scopeId,
|
|
2432
|
+
from: anchor.from,
|
|
2433
|
+
to: anchor.to,
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
if (nextDocument !== state.document) {
|
|
2437
|
+
this.dispatch({
|
|
2438
|
+
type: "document.replace",
|
|
2439
|
+
document: nextDocument,
|
|
2440
|
+
origin: createOrigin("api", clock()),
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const resolved = resolveScope(state.document, scopeId);
|
|
2445
|
+
const publicAnchor: EditorAnchorProjection =
|
|
2446
|
+
resolved && resolved.kind === "range"
|
|
2447
|
+
? resolved
|
|
2448
|
+
: {
|
|
2449
|
+
kind: "range",
|
|
2450
|
+
from: anchor.from,
|
|
2451
|
+
to: anchor.to,
|
|
2452
|
+
assoc: { start: -1, end: 1 },
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
const currentOverlay: WorkflowOverlay = workflowOverlay ?? {
|
|
2456
|
+
overlayVersion: "workflow-overlay/1",
|
|
2457
|
+
scopes: [],
|
|
2458
|
+
};
|
|
2459
|
+
const existingScopes = currentOverlay.scopes.filter(
|
|
2460
|
+
(existing) => existing.scopeId !== scopeId,
|
|
2461
|
+
);
|
|
2462
|
+
const scope: WorkflowScope = {
|
|
2463
|
+
scopeId,
|
|
2464
|
+
mode: params.mode ?? "comment",
|
|
2465
|
+
anchor: publicAnchor,
|
|
2466
|
+
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
2467
|
+
...(params.label ? { label: params.label } : {}),
|
|
2468
|
+
};
|
|
2469
|
+
this.dispatch({
|
|
2470
|
+
type: "workflow.set-overlay",
|
|
2471
|
+
overlay: {
|
|
2472
|
+
...currentOverlay,
|
|
2473
|
+
scopes: [...existingScopes, scope],
|
|
2474
|
+
},
|
|
2475
|
+
origin: createOrigin("api", clock()),
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
if (params.persistence && params.persistence !== "runtime-only") {
|
|
2479
|
+
const entry: WorkflowMetadataEntry = {
|
|
2480
|
+
entryId: `scope-metadata-${scopeId}`,
|
|
2481
|
+
metadataId: "workflow.scope",
|
|
2482
|
+
anchor: publicAnchor,
|
|
2483
|
+
scopeId,
|
|
2484
|
+
value:
|
|
2485
|
+
params.persistence === "document-metadata"
|
|
2486
|
+
? { ...(params.metadata?.value ?? {}), label: params.label }
|
|
2487
|
+
: params.metadata?.value,
|
|
2488
|
+
metadataPersistence:
|
|
2489
|
+
params.persistence === "session" ? "external" : "internal",
|
|
2490
|
+
};
|
|
2491
|
+
this.dispatch({
|
|
2492
|
+
type: "workflow.set-metadata-entries",
|
|
2493
|
+
entries: [...(workflowMetadataEntries ?? []), entry],
|
|
2494
|
+
origin: createOrigin("api", clock()),
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
return {
|
|
2499
|
+
scopeId,
|
|
2500
|
+
anchor: publicAnchor,
|
|
2501
|
+
};
|
|
2502
|
+
},
|
|
2503
|
+
getScope(scopeId) {
|
|
2504
|
+
const resolved = resolveScope(state.document, scopeId);
|
|
2505
|
+
if (!resolved) {
|
|
2506
|
+
const stored = workflowOverlay?.scopes.find((s) => s.scopeId === scopeId);
|
|
2507
|
+
return stored ?? null;
|
|
2508
|
+
}
|
|
2509
|
+
const stored = workflowOverlay?.scopes.find((s) => s.scopeId === scopeId);
|
|
2510
|
+
if (!stored) {
|
|
2511
|
+
return {
|
|
2512
|
+
scopeId,
|
|
2513
|
+
mode: "comment",
|
|
2514
|
+
anchor: resolved,
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
return {
|
|
2518
|
+
...stored,
|
|
2519
|
+
anchor: resolved,
|
|
2520
|
+
};
|
|
2521
|
+
},
|
|
2522
|
+
removeScope(scopeId) {
|
|
2523
|
+
// Step 1: drop the scope from the overlay FIRST. If the scope's mode was
|
|
2524
|
+
// "comment" / "view" the workflow-blocked-reasons gate in `dispatch`
|
|
2525
|
+
// would otherwise refuse the subsequent `document.replace` with
|
|
2526
|
+
// `workflow_comment_only` / `workflow_view_only`. Overlay commands are
|
|
2527
|
+
// routed through `applyRuntimeStateOverlayCommand` and bypass that gate.
|
|
2528
|
+
if (workflowOverlay) {
|
|
2529
|
+
const nextScopes = workflowOverlay.scopes.filter(
|
|
2530
|
+
(scope) => scope.scopeId !== scopeId,
|
|
2531
|
+
);
|
|
2532
|
+
if (nextScopes.length !== workflowOverlay.scopes.length) {
|
|
2533
|
+
this.dispatch({
|
|
2534
|
+
type: "workflow.set-overlay",
|
|
2535
|
+
overlay: { ...workflowOverlay, scopes: nextScopes },
|
|
2536
|
+
origin: createOrigin("api", clock()),
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
// Step 2: now that the scope is gone, strip the markers from the doc.
|
|
2541
|
+
const nextDocument = removeScopeMarkers(state.document, scopeId);
|
|
2542
|
+
if (nextDocument !== state.document) {
|
|
2543
|
+
this.dispatch({
|
|
2544
|
+
type: "document.replace",
|
|
2545
|
+
document: nextDocument,
|
|
2546
|
+
origin: createOrigin("api", clock()),
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
// Step 3: clear any customXml-persisted metadata entries.
|
|
2550
|
+
if (workflowMetadataEntries) {
|
|
2551
|
+
const nextEntries = workflowMetadataEntries.filter(
|
|
2552
|
+
(entry) => entry.scopeId !== scopeId,
|
|
2553
|
+
);
|
|
2554
|
+
if (nextEntries.length !== workflowMetadataEntries.length) {
|
|
2555
|
+
this.dispatch({
|
|
2556
|
+
type: "workflow.set-metadata-entries",
|
|
2557
|
+
entries: nextEntries,
|
|
2558
|
+
origin: createOrigin("api", clock()),
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
},
|
|
2361
2563
|
acceptChange(changeId) {
|
|
2362
2564
|
this.dispatch({
|
|
2363
2565
|
type: "change.accept",
|
|
@@ -3283,12 +3485,14 @@ export function createDocumentRuntime(
|
|
|
3283
3485
|
|
|
3284
3486
|
const preSelection = selection;
|
|
3285
3487
|
const preActiveStory = activeStory;
|
|
3488
|
+
const priorDocument = state.document;
|
|
3286
3489
|
if (activeStory.kind === "main") {
|
|
3287
3490
|
const mainTransaction = executeEditorCommand(baseState, command, context);
|
|
3288
3491
|
commit(mainTransaction);
|
|
3289
3492
|
options.onCommandApplied?.(command, mainTransaction, context, {
|
|
3290
3493
|
preSelection,
|
|
3291
3494
|
activeStory: preActiveStory,
|
|
3495
|
+
priorDocument,
|
|
3292
3496
|
});
|
|
3293
3497
|
return classifyAck({
|
|
3294
3498
|
command,
|
|
@@ -3370,6 +3574,7 @@ export function createDocumentRuntime(
|
|
|
3370
3574
|
options.onCommandApplied?.(broadcastCommand, mergedTransaction, context, {
|
|
3371
3575
|
preSelection,
|
|
3372
3576
|
activeStory: preActiveStory,
|
|
3577
|
+
priorDocument,
|
|
3373
3578
|
});
|
|
3374
3579
|
return classifyAck({
|
|
3375
3580
|
command,
|
|
@@ -45,7 +45,8 @@ export type CapabilityCategory =
|
|
|
45
45
|
| "tracked-changes"
|
|
46
46
|
| "comments"
|
|
47
47
|
| "structure"
|
|
48
|
-
| "system"
|
|
48
|
+
| "system"
|
|
49
|
+
| "workflow";
|
|
49
50
|
|
|
50
51
|
export interface CapabilityShortcut {
|
|
51
52
|
/** Canonical Windows / Linux binding string (e.g. "Ctrl+B", "Shift+Tab"). */
|
|
@@ -301,87 +302,99 @@ export const EDITOR_CAPABILITIES: readonly EditorCapability[] = [
|
|
|
301
302
|
shortcut: { winLinux: "Ctrl+0", mac: "Cmd+0" },
|
|
302
303
|
hostEvent: "onZoomRequested",
|
|
303
304
|
},
|
|
304
|
-
|
|
305
|
-
// ---------------------------------------------------------------
|
|
306
|
-
// Blocked — Word shortcuts the mounted editor does not implement
|
|
307
|
-
// ---------------------------------------------------------------
|
|
308
305
|
{
|
|
309
|
-
id: "
|
|
310
|
-
kind: "
|
|
306
|
+
id: "shortcut.replace",
|
|
307
|
+
kind: "host-delegated",
|
|
311
308
|
category: "navigation",
|
|
312
309
|
label: "Find and replace",
|
|
313
310
|
shortcut: { winLinux: "Ctrl+H", mac: "Ctrl+H" },
|
|
314
|
-
|
|
315
|
-
code: UNSUPPORTED_SURFACE,
|
|
316
|
-
message: "Replace shortcuts are not supported in the mounted editor yet.",
|
|
317
|
-
},
|
|
311
|
+
hostEvent: "onReplaceRequested",
|
|
318
312
|
},
|
|
319
313
|
{
|
|
320
|
-
id: "
|
|
321
|
-
kind: "
|
|
314
|
+
id: "shortcut.go-to",
|
|
315
|
+
kind: "host-delegated",
|
|
322
316
|
category: "navigation",
|
|
323
|
-
label: "Go to",
|
|
317
|
+
label: "Go to page / bookmark / line",
|
|
324
318
|
shortcut: { winLinux: "Ctrl+G", mac: "Cmd+Option+G" },
|
|
325
|
-
|
|
326
|
-
code: UNSUPPORTED_SURFACE,
|
|
327
|
-
message: "Go To shortcuts are not supported in the mounted editor yet.",
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
id: "toggleTrackChanges",
|
|
332
|
-
kind: "blocked",
|
|
333
|
-
category: "tracked-changes",
|
|
334
|
-
label: "Toggle track-changes authoring mode",
|
|
335
|
-
shortcut: { winLinux: "Ctrl+Shift+E", mac: "Cmd+Shift+E" },
|
|
336
|
-
blockReason: {
|
|
337
|
-
code: UNSUPPORTED_SURFACE,
|
|
338
|
-
message: "Track changes authoring shortcuts are not supported in the mounted editor.",
|
|
339
|
-
},
|
|
319
|
+
hostEvent: "onGoToRequested",
|
|
340
320
|
},
|
|
341
321
|
{
|
|
342
|
-
id: "
|
|
343
|
-
kind: "
|
|
322
|
+
id: "shortcut.spell",
|
|
323
|
+
kind: "host-delegated",
|
|
344
324
|
category: "system",
|
|
345
325
|
label: "Check spelling",
|
|
346
326
|
shortcut: { winLinux: "F7", mac: "F7" },
|
|
347
|
-
|
|
348
|
-
code: UNSUPPORTED_SURFACE,
|
|
349
|
-
message: "Spelling shortcuts are not supported in the mounted editor.",
|
|
350
|
-
},
|
|
327
|
+
hostEvent: "onSpellRequested",
|
|
351
328
|
},
|
|
352
329
|
{
|
|
353
|
-
id: "
|
|
354
|
-
kind: "
|
|
330
|
+
id: "shortcut.thesaurus",
|
|
331
|
+
kind: "host-delegated",
|
|
355
332
|
category: "system",
|
|
356
333
|
label: "Open thesaurus",
|
|
357
334
|
shortcut: { winLinux: "Shift+F7", mac: "Shift+F7" },
|
|
358
|
-
|
|
359
|
-
code: UNSUPPORTED_SURFACE,
|
|
360
|
-
message: "Thesaurus shortcuts are not supported in the mounted editor.",
|
|
361
|
-
},
|
|
335
|
+
hostEvent: "onThesaurusRequested",
|
|
362
336
|
},
|
|
363
337
|
{
|
|
364
|
-
id: "
|
|
365
|
-
kind: "
|
|
338
|
+
id: "shortcut.extend-selection",
|
|
339
|
+
kind: "host-delegated",
|
|
366
340
|
category: "selection",
|
|
367
341
|
label: "Extend-selection mode",
|
|
368
342
|
shortcut: { winLinux: "F8", mac: "F8" },
|
|
369
|
-
|
|
370
|
-
code: UNSUPPORTED_SURFACE,
|
|
371
|
-
message: "Extend-selection shortcuts are not supported in the mounted editor.",
|
|
372
|
-
},
|
|
343
|
+
hostEvent: "onExtendSelectionRequested",
|
|
373
344
|
},
|
|
374
345
|
{
|
|
375
|
-
id: "
|
|
376
|
-
kind: "
|
|
346
|
+
id: "shortcut.last-edit",
|
|
347
|
+
kind: "host-delegated",
|
|
377
348
|
category: "navigation",
|
|
378
349
|
label: "Return to last edit",
|
|
379
350
|
shortcut: { winLinux: "Shift+F5", mac: "Shift+F5" },
|
|
351
|
+
hostEvent: "onLastEditRequested",
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------
|
|
355
|
+
// Blocked — Word shortcuts the mounted editor does not implement
|
|
356
|
+
// ---------------------------------------------------------------
|
|
357
|
+
{
|
|
358
|
+
id: "toggleTrackChanges",
|
|
359
|
+
kind: "blocked",
|
|
360
|
+
category: "tracked-changes",
|
|
361
|
+
label: "Toggle track-changes authoring mode",
|
|
362
|
+
shortcut: { winLinux: "Ctrl+Shift+E", mac: "Cmd+Shift+E" },
|
|
380
363
|
blockReason: {
|
|
381
364
|
code: UNSUPPORTED_SURFACE,
|
|
382
|
-
message: "
|
|
365
|
+
message: "Track changes authoring shortcuts are not supported in the mounted editor.",
|
|
383
366
|
},
|
|
384
367
|
},
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------
|
|
370
|
+
// Workflow ref-methods (supported, no shortcut)
|
|
371
|
+
//
|
|
372
|
+
// These entries document ref-method API surface that has no
|
|
373
|
+
// keyboard binding — `WordReviewEditorRef.addScope` / `getScope`
|
|
374
|
+
// / `removeScope`, shipped as S1. They appear in the capability
|
|
375
|
+
// table so host integrations can introspect the full contract
|
|
376
|
+
// without parsing multiple sources. Do not remove the `kind:
|
|
377
|
+
// "supported"` discipline — they are runtime-owned mutations, not
|
|
378
|
+
// host-delegated.
|
|
379
|
+
// ---------------------------------------------------------------
|
|
380
|
+
{
|
|
381
|
+
id: "scope.add",
|
|
382
|
+
kind: "supported",
|
|
383
|
+
category: "workflow",
|
|
384
|
+
label: "Attach a workflow scope to a selection or range (ref method)",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "scope.get",
|
|
388
|
+
kind: "supported",
|
|
389
|
+
category: "workflow",
|
|
390
|
+
label: "Resolve a scopeId to a live WorkflowScope with current anchor (ref method)",
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "scope.remove",
|
|
394
|
+
kind: "supported",
|
|
395
|
+
category: "workflow",
|
|
396
|
+
label: "Remove a scope's markers + metadata record (ref method)",
|
|
397
|
+
},
|
|
385
398
|
];
|
|
386
399
|
|
|
387
400
|
/**
|
|
@@ -24,8 +24,26 @@
|
|
|
24
24
|
* internal cachedGraph + cachedKey without triggering fullRebuild.
|
|
25
25
|
* Does not change geometry — but the public interface changed, so
|
|
26
26
|
* persisted envelopes MUST re-derive their cacheKey under 3.
|
|
27
|
+
* 4 — PR #188 `fix(export)` bumped the version to satisfy the
|
|
28
|
+
* `src/runtime/layout/**` gate, even though the font-loader
|
|
29
|
+
* type-def restoration intended for that PR did not survive the
|
|
30
|
+
* squash-merge. Runtime and cached geometry unchanged.
|
|
31
|
+
* 5 — PR #187 `joakim/commentevents` restores the local `Minimal*`
|
|
32
|
+
* font-loader type defs under `src/runtime/layout/docx-font-loader.ts`
|
|
33
|
+
* (re-application of previously-shipped PRs #162/#163 which a
|
|
34
|
+
* subsequent merge reverted) so downstream consumers whose
|
|
35
|
+
* TS `lib` does not expose `FontFaceSet.add` can type-check the
|
|
36
|
+
* package, and adds the `comments_changed` event plumbing in
|
|
37
|
+
* runtime domains outside `src/runtime/layout/**`. TypeScript-
|
|
38
|
+
* surface-only from the layout engine's perspective: no cached
|
|
39
|
+
* geometry or cache-key derivation changes. The bump exists
|
|
40
|
+
* solely to satisfy the
|
|
41
|
+
* `scripts/ci-check-layout-engine-version.mjs` gate because a
|
|
42
|
+
* file under `src/runtime/layout/**` changed. Safe to treat
|
|
43
|
+
* versions 3, 4, and 5 as cache-compatible if a migration ever
|
|
44
|
+
* needs to collapse them.
|
|
27
45
|
*/
|
|
28
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
46
|
+
export const LAYOUT_ENGINE_VERSION = 5 as const;
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
49
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -37,5 +55,12 @@ export const LAYOUT_ENGINE_VERSION = 3 as const;
|
|
|
37
55
|
* 1 — initial envelope shape: { schemaVersion, engineVersion,
|
|
38
56
|
* fontFingerprint, structuralHash, graph, surface }. Ships with
|
|
39
57
|
* L7 Phase 2.5 Plan A.
|
|
58
|
+
* 2 — L7 Phase 2.5 Plan B: adds `canonicalDocument` + `canonicalDocumentHash`
|
|
59
|
+
* fields so the receiving client can skip the DOCX parse entirely on
|
|
60
|
+
* cache hit. `canonicalDocumentHash` is also a 5th input to the cache
|
|
61
|
+
* key so any state mutation (styles, metadata, comments, preservation)
|
|
62
|
+
* correctly invalidates. v1 envelopes are rejected on load under v2 —
|
|
63
|
+
* no corruption path exists because schemaVersion is the top-level
|
|
64
|
+
* discriminator.
|
|
40
65
|
*/
|
|
41
|
-
export const LAYCACHE_SCHEMA_VERSION =
|
|
66
|
+
export const LAYCACHE_SCHEMA_VERSION = 2 as const;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { EditorSurfaceSnapshot } from "../../api/public-types";
|
|
2
|
+
import type { CanonicalDocument } from "../../model/canonical-document.ts";
|
|
2
3
|
import type { RuntimePageGraph } from "../layout/page-graph.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* L7 Phase 2.5
|
|
6
|
+
* L7 Phase 2.5 — prerender cache envelope shape.
|
|
6
7
|
*
|
|
7
|
-
* The envelope is the unit written to IndexedDB (Plan A) and —
|
|
8
|
-
*
|
|
9
|
-
* must agree on this shape: the prerender pipeline that populates
|
|
10
|
-
* the warm-path loader that rehydrates it.
|
|
8
|
+
* The envelope is the unit written to IndexedDB (Plan A) and — under Plan B
|
|
9
|
+
* (schema v2) — to the `laycache` customXml editor-state namespace. Two
|
|
10
|
+
* consumers must agree on this shape: the prerender pipeline that populates
|
|
11
|
+
* it, and the warm-path loader that rehydrates it.
|
|
11
12
|
*
|
|
12
13
|
* Load-time invariants checked by consumers before trusting the envelope:
|
|
13
14
|
* - `schemaVersion === LAYCACHE_SCHEMA_VERSION` — bump invalidates
|
|
@@ -15,15 +16,26 @@ import type { RuntimePageGraph } from "../layout/page-graph.ts";
|
|
|
15
16
|
* - `graph.revision === 0` — canonical marker
|
|
16
17
|
*
|
|
17
18
|
* The envelope MUST be structured-clone-safe because IndexedDB and Plan B's
|
|
18
|
-
* customXml path both rely on structured-clone semantics. Keep fields
|
|
19
|
-
* JSON-serializable primitives, plain objects, or arrays — no class
|
|
19
|
+
* customXml path both rely on structured-clone / JSON semantics. Keep fields
|
|
20
|
+
* as JSON-serializable primitives, plain objects, or arrays — no class
|
|
20
21
|
* instances, functions, or symbols.
|
|
22
|
+
*
|
|
23
|
+
* Plan B additions (schema v2):
|
|
24
|
+
* - `canonicalDocument` — the full parsed model, so the warm-path loader
|
|
25
|
+
* can skip `parseMainDocumentXml` + `createImportedCanonicalDocument` +
|
|
26
|
+
* `buildCompatibilityReport` + `createImportedSnapshot` on cache hit.
|
|
27
|
+
* Saves ~584 ms of the 976 ms cold-upload on `extra-large` CCEP.
|
|
28
|
+
* - `canonicalDocumentHash` — sha256 of sorted-keys JSON. Also the 5th
|
|
29
|
+
* input to `deriveCacheKey`, so style/metadata/comment/preservation
|
|
30
|
+
* mutations correctly invalidate the cache.
|
|
21
31
|
*/
|
|
22
32
|
export interface CacheEnvelope {
|
|
23
33
|
readonly schemaVersion: number;
|
|
24
34
|
readonly engineVersion: number;
|
|
25
35
|
readonly fontFingerprint: string;
|
|
26
36
|
readonly structuralHash: string;
|
|
37
|
+
readonly canonicalDocumentHash: string;
|
|
27
38
|
readonly graph: RuntimePageGraph;
|
|
28
39
|
readonly surface: EditorSurfaceSnapshot;
|
|
40
|
+
readonly canonicalDocument: CanonicalDocument;
|
|
29
41
|
}
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L7 Phase 2.5
|
|
2
|
+
* L7 Phase 2.5 — prerender cache-key derivation.
|
|
3
3
|
*
|
|
4
4
|
* The cache key is the composite identity the IndexedDB (Plan A) and
|
|
5
5
|
* customXml (Plan B) backends index on. It has five inputs:
|
|
6
6
|
*
|
|
7
|
-
* 1. structuralHash(blocks)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2. fontFingerprint
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
7
|
+
* 1. structuralHash(blocks) — sha256 of the ordered kind:blockId list.
|
|
8
|
+
* Stable across text-only edits; changes on
|
|
9
|
+
* insert/delete/reorder because blockIds are
|
|
10
|
+
* kind-counter pairs (paragraph-5 stays 5
|
|
11
|
+
* under typing, shifts to paragraph-6 after
|
|
12
|
+
* an insert).
|
|
13
|
+
* 2. fontFingerprint — identifies the measurement-backend + font-
|
|
14
|
+
* metric source. "empirical-backend" in
|
|
15
|
+
* Plan A; a real font-derived string after
|
|
16
|
+
* Phase 8.
|
|
17
|
+
* 3. engineVersion — LAYOUT_ENGINE_VERSION from src/runtime/
|
|
18
|
+
* layout/layout-engine-version.ts. Bumped by
|
|
19
|
+
* CI gate on any layout/render shape change.
|
|
20
|
+
* 4. schemaVersion — LAYCACHE_SCHEMA_VERSION for envelope
|
|
21
|
+
* format.
|
|
22
|
+
* 5. canonicalDocumentHash — (Plan B, schema v2) sha256 of sorted-keys
|
|
23
|
+
* JSON of the CanonicalDocument. Catches
|
|
24
|
+
* non-structural mutations (styles,
|
|
25
|
+
* metadata, comments, preservation) that
|
|
26
|
+
* `structuralHash` alone misses. Computed
|
|
27
|
+
* via `computeCanonicalDocumentHash()` from
|
|
28
|
+
* `./canonical-document-hash.ts`.
|
|
20
29
|
*
|
|
21
30
|
* Returns a 64-char lower-case hex digest. Uses the Web Crypto API
|
|
22
31
|
* (globalThis.crypto.subtle), available in Node 18+ and all target browsers —
|
|
@@ -33,6 +42,7 @@ export interface CacheKeyInputs {
|
|
|
33
42
|
readonly fontFingerprint: string;
|
|
34
43
|
readonly engineVersion: string | number;
|
|
35
44
|
readonly schemaVersion: number;
|
|
45
|
+
readonly canonicalDocumentHash: string;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
const BLOCK_SEPARATOR = "\u0000";
|
|
@@ -61,6 +71,7 @@ export async function deriveCacheKey(inputs: CacheKeyInputs): Promise<string> {
|
|
|
61
71
|
inputs.fontFingerprint,
|
|
62
72
|
String(inputs.engineVersion),
|
|
63
73
|
String(inputs.schemaVersion),
|
|
74
|
+
inputs.canonicalDocumentHash,
|
|
64
75
|
].join(FIELD_SEPARATOR);
|
|
65
76
|
return sha256Hex(composite);
|
|
66
77
|
}
|