@beyondwork/docx-react-component 1.0.47 → 1.0.49
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 +16 -11
- package/package.json +30 -41
- package/src/api/public-types.ts +199 -13
- 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 +9 -1
- package/src/core/commands/text-commands.ts +3 -1
- package/src/core/schema/text-schema.ts +95 -1
- package/src/core/selection/anchor-conversion.ts +112 -0
- package/src/core/selection/review-anchors.ts +108 -3
- package/src/core/state/text-transaction.ts +103 -7
- package/src/internal/harness-debug-ports.ts +168 -0
- package/src/io/chart-preview-resolver.ts +59 -1
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +46 -0
- package/src/io/export/serialize-paragraph-formatting.ts +8 -0
- package/src/io/export/serialize-run-formatting.ts +10 -1
- 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/chart-style-table.ts +543 -0
- package/src/io/ooxml/chart/color-palette.ts +101 -0
- package/src/io/ooxml/chart/compose-series-color.ts +147 -0
- package/src/io/ooxml/chart/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
- package/src/io/ooxml/chart/parse-series.ts +635 -0
- package/src/io/ooxml/chart/resolve-color.ts +261 -0
- package/src/io/ooxml/chart/types.ts +439 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +90 -2
- package/src/io/ooxml/parse-main-document.ts +156 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
- package/src/io/ooxml/parse-run-formatting.ts +49 -0
- 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/property-grab-bag.ts +211 -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 +160 -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 +27 -0
- package/src/runtime/collab/index.ts +29 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +330 -0
- package/src/runtime/collab/workflow-shared.ts +247 -0
- package/src/runtime/document-locations.ts +1 -9
- package/src/runtime/document-outline.ts +1 -9
- package/src/runtime/document-runtime.ts +288 -65
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- 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 +102 -37
- package/src/runtime/theme-color-resolver.ts +188 -0
- package/src/runtime/workflow-markup.ts +7 -18
- package/src/ui/WordReviewEditor.tsx +48 -2
- package/src/ui/editor-runtime-boundary.ts +42 -1
- package/src/ui/headless/selection-helpers.ts +10 -23
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
- package/src/ui/unsupported-previews-policy.ts +23 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
|
@@ -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,
|
|
@@ -28,6 +30,7 @@ import type {
|
|
|
28
30
|
DocumentTextToken,
|
|
29
31
|
EditorSessionState,
|
|
30
32
|
EditorAnchorProjection,
|
|
33
|
+
TextFormattingDirective,
|
|
31
34
|
EditorError,
|
|
32
35
|
EditorStoryTarget,
|
|
33
36
|
EditorViewStateSnapshot,
|
|
@@ -74,6 +77,7 @@ import type {
|
|
|
74
77
|
WorkflowMetadataSnapshot,
|
|
75
78
|
WorkflowMarkupSnapshot,
|
|
76
79
|
WorkflowOverlay,
|
|
80
|
+
WorkflowScope,
|
|
77
81
|
WorkflowScopeSnapshot,
|
|
78
82
|
WorkspaceMode,
|
|
79
83
|
WordReviewEditorEvent,
|
|
@@ -101,7 +105,14 @@ import {
|
|
|
101
105
|
storyTargetsEqual,
|
|
102
106
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
103
107
|
} from "../core/selection/mapping.ts";
|
|
104
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
toInternalAnchorProjection,
|
|
110
|
+
toPublicAnchorProjection,
|
|
111
|
+
} from "../core/selection/anchor-conversion.ts";
|
|
112
|
+
import {
|
|
113
|
+
commentAnchorRejectionReason,
|
|
114
|
+
snapCommentAnchorAwayFromTable,
|
|
115
|
+
} from "../core/selection/review-anchors.ts";
|
|
105
116
|
import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
|
|
106
117
|
import {
|
|
107
118
|
describeOpaqueFragment,
|
|
@@ -116,6 +127,11 @@ import {
|
|
|
116
127
|
} from "../review/store/revision-store.ts";
|
|
117
128
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
118
129
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
130
|
+
import { resolveScope } from "./scope-resolver.ts";
|
|
131
|
+
import {
|
|
132
|
+
insertScopeMarkers,
|
|
133
|
+
removeScopeMarkers,
|
|
134
|
+
} from "../core/commands/add-scope.ts";
|
|
119
135
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
120
136
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
121
137
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
@@ -224,6 +240,7 @@ import type {
|
|
|
224
240
|
} from "../api/editor-state-types.ts";
|
|
225
241
|
import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
|
|
226
242
|
import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
|
|
243
|
+
import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
|
|
227
244
|
|
|
228
245
|
/** Internal extension of ExportDocxOptions that threads the collected
|
|
229
246
|
* editorState payload from the runtime to the docx serializer. */
|
|
@@ -257,7 +274,7 @@ export interface DocumentRuntime {
|
|
|
257
274
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
258
275
|
getCanonicalDocument(): CanonicalDocumentEnvelope;
|
|
259
276
|
getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
|
|
260
|
-
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
277
|
+
replaceText(text: string, target?: EditorAnchorProjection, formatting?: TextFormattingDirective): void;
|
|
261
278
|
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
|
|
262
279
|
dispatch(command: EditorCommand): void;
|
|
263
280
|
/**
|
|
@@ -291,6 +308,9 @@ export interface DocumentRuntime {
|
|
|
291
308
|
reopenComment(commentId: string): void;
|
|
292
309
|
addCommentReply(commentId: string, body: string, authorId?: string): AddCommentReplyResult;
|
|
293
310
|
editCommentBody(commentId: string, body: string): void;
|
|
311
|
+
addScope(params: AddScopeParams): AddScopeResult;
|
|
312
|
+
getScope(scopeId: string): WorkflowScope | null;
|
|
313
|
+
removeScope(scopeId: string): void;
|
|
294
314
|
acceptChange(changeId: string): void;
|
|
295
315
|
rejectChange(changeId: string): void;
|
|
296
316
|
acceptAllChanges(): void;
|
|
@@ -348,6 +368,7 @@ export interface DocumentRuntime {
|
|
|
348
368
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
349
369
|
clearWorkflowOverlay(): void;
|
|
350
370
|
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
371
|
+
setSharedWorkflowState(state: SharedWorkflowState | null): void;
|
|
351
372
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
352
373
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
353
374
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
@@ -618,6 +639,9 @@ export function createDocumentRuntime(
|
|
|
618
639
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
619
640
|
?? [];
|
|
620
641
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
642
|
+
// P13 Slice B: shared workflow state from the collab Y.Map "workflow".
|
|
643
|
+
// Set via setSharedWorkflowState(); observed by evaluateWorkflowBlockedReasons.
|
|
644
|
+
let sharedWorkflowState: SharedWorkflowState | null = null;
|
|
621
645
|
const initialPersistedSnapshot = options.initialSessionState
|
|
622
646
|
? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
|
|
623
647
|
savedAt: options.initialSessionState.updatedAt,
|
|
@@ -862,6 +886,7 @@ export function createDocumentRuntime(
|
|
|
862
886
|
documentMode: DocumentMode;
|
|
863
887
|
protectionSnapshot: ProtectionSnapshot;
|
|
864
888
|
workflowOverlay: WorkflowOverlay | null;
|
|
889
|
+
sharedWorkflowState: SharedWorkflowState | null;
|
|
865
890
|
snapshot: InteractionGuardSnapshot;
|
|
866
891
|
}
|
|
867
892
|
| undefined;
|
|
@@ -1129,6 +1154,29 @@ export function createDocumentRuntime(
|
|
|
1129
1154
|
commandType?: string,
|
|
1130
1155
|
): WorkflowBlockedCommandReason[] {
|
|
1131
1156
|
const reasons: WorkflowBlockedCommandReason[] = [];
|
|
1157
|
+
// P13 Slice B: shared lockedMode overrides all other scope checks when
|
|
1158
|
+
// non-editing. Short-circuit: no other scope reason applies when the round
|
|
1159
|
+
// is locked (the round state supersedes scope/overlay-level gating).
|
|
1160
|
+
// Emit a reason code whose effectiveMode mapping matches the mode intent:
|
|
1161
|
+
// "commenting" → workflow_comment_only (maps to effectiveMode: "comment")
|
|
1162
|
+
// "viewing" → workflow_view_only (maps to effectiveMode: "view")
|
|
1163
|
+
// "suggesting" → workflow_round_locked (no existing mapping; stays "blocked"
|
|
1164
|
+
// for this slice — full suggesting-mode semantics will be a
|
|
1165
|
+
// future slice that hooks getEffectiveDocumentMode instead).
|
|
1166
|
+
if (sharedWorkflowState?.lockedMode && sharedWorkflowState.lockedMode !== "editing") {
|
|
1167
|
+
const lockedMode = sharedWorkflowState.lockedMode;
|
|
1168
|
+
const code: WorkflowBlockedCommandReason["code"] =
|
|
1169
|
+
lockedMode === "commenting"
|
|
1170
|
+
? "workflow_comment_only"
|
|
1171
|
+
: lockedMode === "viewing"
|
|
1172
|
+
? "workflow_view_only"
|
|
1173
|
+
: "workflow_round_locked";
|
|
1174
|
+
reasons.push({
|
|
1175
|
+
code,
|
|
1176
|
+
message: `Round is locked to ${lockedMode} mode.`,
|
|
1177
|
+
});
|
|
1178
|
+
return reasons;
|
|
1179
|
+
}
|
|
1132
1180
|
const selectionBounds = {
|
|
1133
1181
|
from: Math.min(selection.anchor, selection.head),
|
|
1134
1182
|
to: Math.max(selection.anchor, selection.head),
|
|
@@ -1541,7 +1589,8 @@ export function createDocumentRuntime(
|
|
|
1541
1589
|
cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
|
|
1542
1590
|
cachedInteractionGuardSnapshot.documentMode === viewState.documentMode &&
|
|
1543
1591
|
cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
|
|
1544
|
-
cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay
|
|
1592
|
+
cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay &&
|
|
1593
|
+
cachedInteractionGuardSnapshot.sharedWorkflowState === sharedWorkflowState
|
|
1545
1594
|
) {
|
|
1546
1595
|
return cachedInteractionGuardSnapshot.snapshot;
|
|
1547
1596
|
}
|
|
@@ -1602,6 +1651,7 @@ export function createDocumentRuntime(
|
|
|
1602
1651
|
documentMode: viewState.documentMode,
|
|
1603
1652
|
protectionSnapshot,
|
|
1604
1653
|
workflowOverlay,
|
|
1654
|
+
sharedWorkflowState,
|
|
1605
1655
|
snapshot,
|
|
1606
1656
|
};
|
|
1607
1657
|
return snapshot;
|
|
@@ -2035,6 +2085,16 @@ export function createDocumentRuntime(
|
|
|
2035
2085
|
}
|
|
2036
2086
|
});
|
|
2037
2087
|
|
|
2088
|
+
// R5 scratch snapshot: single pre-allocated object reused for every
|
|
2089
|
+
// `applyRemoteCommand(cmd, ctx, meta)` call with a `meta.preSelection`
|
|
2090
|
+
// override. Avoids the per-remote-command `{ ...cachedRenderSnapshot }`
|
|
2091
|
+
// + `{ ...state }` allocations that CLAUDE.md rule 4 warns against.
|
|
2092
|
+
// Mutated in place below — callers MUST NOT hold references to it
|
|
2093
|
+
// across dispatches; `executeEditorCommand` + `commitRemote` consume
|
|
2094
|
+
// synchronously within `applyRemoteCommand`, so this is safe today.
|
|
2095
|
+
const r5ScratchReplayState: typeof state = { ...state };
|
|
2096
|
+
const r5ScratchReplaySnapshot: typeof cachedRenderSnapshot = { ...cachedRenderSnapshot };
|
|
2097
|
+
|
|
2038
2098
|
return {
|
|
2039
2099
|
subscribe(listener) {
|
|
2040
2100
|
listeners.add(listener);
|
|
@@ -2147,28 +2207,56 @@ export function createDocumentRuntime(
|
|
|
2147
2207
|
applyRuntimeStateOverlayCommand(command);
|
|
2148
2208
|
return;
|
|
2149
2209
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2210
|
+
// Story-target isolation: the remote's `meta.activeStory` scopes
|
|
2211
|
+
// the replay (so the command lands in the intended region of the
|
|
2212
|
+
// shared document), but must NEVER overwrite the local user's
|
|
2213
|
+
// closure-level `activeStory`. Before P11 this assignment stole
|
|
2214
|
+
// focus every time a remote event arrived for a different story —
|
|
2215
|
+
// a local user authoring in a header would get yanked into the
|
|
2216
|
+
// main body as soon as a peer edited main.
|
|
2217
|
+
const replayStory = meta?.activeStory ?? activeStory;
|
|
2218
|
+
const crossStoryReplay = Boolean(
|
|
2219
|
+
meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory),
|
|
2220
|
+
);
|
|
2221
|
+
let replayState: typeof state;
|
|
2222
|
+
let replaySnapshot: typeof cachedRenderSnapshot;
|
|
2223
|
+
if (meta?.preSelection) {
|
|
2224
|
+
// Refresh scratch with current `state` / `cachedRenderSnapshot` (both
|
|
2225
|
+
// mutate on every commit), then override only the selection fields.
|
|
2226
|
+
Object.assign(r5ScratchReplayState, state);
|
|
2227
|
+
r5ScratchReplayState.selection = meta.preSelection;
|
|
2228
|
+
Object.assign(r5ScratchReplaySnapshot, cachedRenderSnapshot);
|
|
2229
|
+
r5ScratchReplaySnapshot.selection = toPublicSelectionSnapshot(meta.preSelection, replayStory);
|
|
2230
|
+
replayState = r5ScratchReplayState;
|
|
2231
|
+
replaySnapshot = r5ScratchReplaySnapshot;
|
|
2232
|
+
} else {
|
|
2233
|
+
replayState = state;
|
|
2234
|
+
replaySnapshot = cachedRenderSnapshot;
|
|
2156
2235
|
}
|
|
2157
|
-
const replayState = meta?.preSelection
|
|
2158
|
-
? { ...state, selection: meta.preSelection }
|
|
2159
|
-
: state;
|
|
2160
|
-
const replaySnapshot = meta?.preSelection
|
|
2161
|
-
? {
|
|
2162
|
-
...cachedRenderSnapshot,
|
|
2163
|
-
selection: toPublicSelectionSnapshot(meta.preSelection, activeStory),
|
|
2164
|
-
}
|
|
2165
|
-
: cachedRenderSnapshot;
|
|
2166
2236
|
const replayContext = {
|
|
2167
2237
|
...context,
|
|
2168
2238
|
renderSnapshot: replaySnapshot,
|
|
2169
2239
|
};
|
|
2170
2240
|
const transaction = executeEditorCommand(replayState, command, replayContext);
|
|
2171
|
-
|
|
2241
|
+
if (crossStoryReplay) {
|
|
2242
|
+
// Cross-story replay: the transaction's resulting selection is
|
|
2243
|
+
// in the remote story's region. Don't leak that into local
|
|
2244
|
+
// `state.selection` — preserve the pre-replay local selection
|
|
2245
|
+
// so the local caret stays where the user is focused. The
|
|
2246
|
+
// document delta still applies; only the selection is filtered.
|
|
2247
|
+
// Full position-mapping of the local cursor through the remote
|
|
2248
|
+
// edit is a separate P11 sub-bullet (Awareness cursor transaction
|
|
2249
|
+
// mapping).
|
|
2250
|
+
commitRemote({
|
|
2251
|
+
...transaction,
|
|
2252
|
+
nextState: {
|
|
2253
|
+
...transaction.nextState,
|
|
2254
|
+
selection: state.selection,
|
|
2255
|
+
},
|
|
2256
|
+
});
|
|
2257
|
+
} else {
|
|
2258
|
+
commitRemote(transaction);
|
|
2259
|
+
}
|
|
2172
2260
|
} catch (error) {
|
|
2173
2261
|
emitError(toRuntimeError(error));
|
|
2174
2262
|
}
|
|
@@ -2207,13 +2295,14 @@ export function createDocumentRuntime(
|
|
|
2207
2295
|
getDefaultAuthorId() {
|
|
2208
2296
|
return defaultAuthorId;
|
|
2209
2297
|
},
|
|
2210
|
-
replaceText(text, target) {
|
|
2298
|
+
replaceText(text, target, formatting) {
|
|
2211
2299
|
try {
|
|
2212
2300
|
const timestamp = clock();
|
|
2213
2301
|
applyTextCommandInActiveStory(
|
|
2214
2302
|
{
|
|
2215
2303
|
type: "text.insert",
|
|
2216
2304
|
text,
|
|
2305
|
+
...(formatting ? { formatting } : {}),
|
|
2217
2306
|
origin: createOrigin("api", timestamp),
|
|
2218
2307
|
},
|
|
2219
2308
|
{
|
|
@@ -2260,22 +2349,39 @@ export function createDocumentRuntime(
|
|
|
2260
2349
|
throw toStructuredRuntimeException(error);
|
|
2261
2350
|
}
|
|
2262
2351
|
const commentId = createEntityId("comment", state.document.review.comments, clock());
|
|
2263
|
-
|
|
2352
|
+
let anchor = params.anchor
|
|
2264
2353
|
? toInternalAnchorProjection(params.anchor)
|
|
2265
2354
|
: state.selection.activeRange;
|
|
2266
|
-
|
|
2355
|
+
let selection = params.anchor
|
|
2267
2356
|
? createSelectionFromPublicAnchor(params.anchor)
|
|
2268
2357
|
: state.selection;
|
|
2269
|
-
if (
|
|
2358
|
+
if (params.snapToSafeBoundary === true) {
|
|
2359
|
+
const snapped = snapCommentAnchorAwayFromTable(
|
|
2360
|
+
cachedRenderSnapshot.surface,
|
|
2361
|
+
anchor,
|
|
2362
|
+
);
|
|
2363
|
+
if (snapped !== null && snapped !== anchor) {
|
|
2364
|
+
anchor = snapped;
|
|
2365
|
+
selection = createSelectionFromPublicAnchor(toPublicAnchorProjection(snapped));
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const rejectionReason = commentAnchorRejectionReason(
|
|
2369
|
+
cachedRenderSnapshot.surface,
|
|
2370
|
+
anchor,
|
|
2371
|
+
);
|
|
2372
|
+
if (rejectionReason !== null) {
|
|
2373
|
+
const message =
|
|
2374
|
+
rejectionReason === "comment_anchor_table_adjacent"
|
|
2375
|
+
? "DOCX comments cannot currently anchor mid-run within a paragraph adjacent to a table boundary — snap the range to a paragraph or word boundary and retry."
|
|
2376
|
+
: "DOCX comments must use a non-empty range that stays within a single story and does not cross table or opaque-fragment boundaries.";
|
|
2270
2377
|
const error: InternalEditorError = {
|
|
2271
2378
|
errorId: createSessionId("comment-anchor", clock()),
|
|
2272
2379
|
code: "validation_failed",
|
|
2273
2380
|
isFatal: false,
|
|
2274
|
-
message
|
|
2275
|
-
"DOCX comments must use a non-empty range that stays within a single story and does not cross table or opaque-fragment boundaries.",
|
|
2381
|
+
message,
|
|
2276
2382
|
source: "runtime",
|
|
2277
2383
|
details: {
|
|
2278
|
-
reason:
|
|
2384
|
+
reason: rejectionReason,
|
|
2279
2385
|
},
|
|
2280
2386
|
};
|
|
2281
2387
|
emitError(error);
|
|
@@ -2362,6 +2468,155 @@ export function createDocumentRuntime(
|
|
|
2362
2468
|
origin: createOrigin("api", clock()),
|
|
2363
2469
|
});
|
|
2364
2470
|
},
|
|
2471
|
+
addScope(params): AddScopeResult {
|
|
2472
|
+
const scopeId =
|
|
2473
|
+
params.scopeId ??
|
|
2474
|
+
`scope-${clock().replace(/[^0-9]/gu, "")}-${Math.floor(Math.random() * 1e6)}`;
|
|
2475
|
+
const anchor =
|
|
2476
|
+
params.anchor.kind === "range"
|
|
2477
|
+
? { from: params.anchor.from, to: params.anchor.to }
|
|
2478
|
+
: null;
|
|
2479
|
+
|
|
2480
|
+
if (!anchor) {
|
|
2481
|
+
return {
|
|
2482
|
+
scopeId,
|
|
2483
|
+
anchor: params.anchor,
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
const { document: nextDocument } = insertScopeMarkers(state.document, {
|
|
2488
|
+
scopeId,
|
|
2489
|
+
from: anchor.from,
|
|
2490
|
+
to: anchor.to,
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
if (nextDocument !== state.document) {
|
|
2494
|
+
this.dispatch({
|
|
2495
|
+
type: "document.replace",
|
|
2496
|
+
document: nextDocument,
|
|
2497
|
+
origin: createOrigin("api", clock()),
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
const resolved = resolveScope(state.document, scopeId);
|
|
2502
|
+
const publicAnchor: EditorAnchorProjection =
|
|
2503
|
+
resolved && resolved.kind === "range"
|
|
2504
|
+
? resolved
|
|
2505
|
+
: {
|
|
2506
|
+
kind: "range",
|
|
2507
|
+
from: anchor.from,
|
|
2508
|
+
to: anchor.to,
|
|
2509
|
+
assoc: { start: -1, end: 1 },
|
|
2510
|
+
};
|
|
2511
|
+
|
|
2512
|
+
const currentOverlay: WorkflowOverlay = workflowOverlay ?? {
|
|
2513
|
+
overlayVersion: "workflow-overlay/1",
|
|
2514
|
+
scopes: [],
|
|
2515
|
+
};
|
|
2516
|
+
const existingScopes = currentOverlay.scopes.filter(
|
|
2517
|
+
(existing) => existing.scopeId !== scopeId,
|
|
2518
|
+
);
|
|
2519
|
+
const scope: WorkflowScope = {
|
|
2520
|
+
scopeId,
|
|
2521
|
+
mode: params.mode ?? "comment",
|
|
2522
|
+
anchor: publicAnchor,
|
|
2523
|
+
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
2524
|
+
...(params.label ? { label: params.label } : {}),
|
|
2525
|
+
};
|
|
2526
|
+
this.dispatch({
|
|
2527
|
+
type: "workflow.set-overlay",
|
|
2528
|
+
overlay: {
|
|
2529
|
+
...currentOverlay,
|
|
2530
|
+
scopes: [...existingScopes, scope],
|
|
2531
|
+
},
|
|
2532
|
+
origin: createOrigin("api", clock()),
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
if (params.persistence && params.persistence !== "runtime-only") {
|
|
2536
|
+
const entry: WorkflowMetadataEntry = {
|
|
2537
|
+
entryId: `scope-metadata-${scopeId}`,
|
|
2538
|
+
metadataId: "workflow.scope",
|
|
2539
|
+
anchor: publicAnchor,
|
|
2540
|
+
scopeId,
|
|
2541
|
+
value:
|
|
2542
|
+
params.persistence === "document-metadata"
|
|
2543
|
+
? { ...(params.metadata?.value ?? {}), label: params.label }
|
|
2544
|
+
: params.metadata?.value,
|
|
2545
|
+
metadataPersistence:
|
|
2546
|
+
params.persistence === "session" ? "external" : "internal",
|
|
2547
|
+
};
|
|
2548
|
+
this.dispatch({
|
|
2549
|
+
type: "workflow.set-metadata-entries",
|
|
2550
|
+
entries: [...(workflowMetadataEntries ?? []), entry],
|
|
2551
|
+
origin: createOrigin("api", clock()),
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
return {
|
|
2556
|
+
scopeId,
|
|
2557
|
+
anchor: publicAnchor,
|
|
2558
|
+
};
|
|
2559
|
+
},
|
|
2560
|
+
getScope(scopeId) {
|
|
2561
|
+
const resolved = resolveScope(state.document, scopeId);
|
|
2562
|
+
if (!resolved) {
|
|
2563
|
+
const stored = workflowOverlay?.scopes.find((s) => s.scopeId === scopeId);
|
|
2564
|
+
return stored ?? null;
|
|
2565
|
+
}
|
|
2566
|
+
const stored = workflowOverlay?.scopes.find((s) => s.scopeId === scopeId);
|
|
2567
|
+
if (!stored) {
|
|
2568
|
+
return {
|
|
2569
|
+
scopeId,
|
|
2570
|
+
mode: "comment",
|
|
2571
|
+
anchor: resolved,
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
return {
|
|
2575
|
+
...stored,
|
|
2576
|
+
anchor: resolved,
|
|
2577
|
+
};
|
|
2578
|
+
},
|
|
2579
|
+
removeScope(scopeId) {
|
|
2580
|
+
// Step 1: drop the scope from the overlay FIRST. If the scope's mode was
|
|
2581
|
+
// "comment" / "view" the workflow-blocked-reasons gate in `dispatch`
|
|
2582
|
+
// would otherwise refuse the subsequent `document.replace` with
|
|
2583
|
+
// `workflow_comment_only` / `workflow_view_only`. Overlay commands are
|
|
2584
|
+
// routed through `applyRuntimeStateOverlayCommand` and bypass that gate.
|
|
2585
|
+
if (workflowOverlay) {
|
|
2586
|
+
const nextScopes = workflowOverlay.scopes.filter(
|
|
2587
|
+
(scope) => scope.scopeId !== scopeId,
|
|
2588
|
+
);
|
|
2589
|
+
if (nextScopes.length !== workflowOverlay.scopes.length) {
|
|
2590
|
+
this.dispatch({
|
|
2591
|
+
type: "workflow.set-overlay",
|
|
2592
|
+
overlay: { ...workflowOverlay, scopes: nextScopes },
|
|
2593
|
+
origin: createOrigin("api", clock()),
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
// Step 2: now that the scope is gone, strip the markers from the doc.
|
|
2598
|
+
const nextDocument = removeScopeMarkers(state.document, scopeId);
|
|
2599
|
+
if (nextDocument !== state.document) {
|
|
2600
|
+
this.dispatch({
|
|
2601
|
+
type: "document.replace",
|
|
2602
|
+
document: nextDocument,
|
|
2603
|
+
origin: createOrigin("api", clock()),
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
// Step 3: clear any customXml-persisted metadata entries.
|
|
2607
|
+
if (workflowMetadataEntries) {
|
|
2608
|
+
const nextEntries = workflowMetadataEntries.filter(
|
|
2609
|
+
(entry) => entry.scopeId !== scopeId,
|
|
2610
|
+
);
|
|
2611
|
+
if (nextEntries.length !== workflowMetadataEntries.length) {
|
|
2612
|
+
this.dispatch({
|
|
2613
|
+
type: "workflow.set-metadata-entries",
|
|
2614
|
+
entries: nextEntries,
|
|
2615
|
+
origin: createOrigin("api", clock()),
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2365
2620
|
acceptChange(changeId) {
|
|
2366
2621
|
this.dispatch({
|
|
2367
2622
|
type: "change.accept",
|
|
@@ -2756,6 +3011,13 @@ export function createDocumentRuntime(
|
|
|
2756
3011
|
getWorkflowOverlay() {
|
|
2757
3012
|
return workflowOverlay;
|
|
2758
3013
|
},
|
|
3014
|
+
setSharedWorkflowState(state) {
|
|
3015
|
+
if (state === sharedWorkflowState) return;
|
|
3016
|
+
sharedWorkflowState = state;
|
|
3017
|
+
// Invalidate guard/scope caches so next derivation reflects the new state.
|
|
3018
|
+
cachedInteractionGuardSnapshot = undefined;
|
|
3019
|
+
cachedWorkflowScopeSnapshot = undefined;
|
|
3020
|
+
},
|
|
2759
3021
|
getWorkflowScopeSnapshot() {
|
|
2760
3022
|
return getCachedWorkflowScopeSnapshot();
|
|
2761
3023
|
},
|
|
@@ -3954,45 +4216,6 @@ function toPublicSelectionSnapshot(
|
|
|
3954
4216
|
};
|
|
3955
4217
|
}
|
|
3956
4218
|
|
|
3957
|
-
function toPublicAnchorProjection(
|
|
3958
|
-
anchor: InternalEditorAnchorProjection,
|
|
3959
|
-
): EditorAnchorProjection {
|
|
3960
|
-
switch (anchor.kind) {
|
|
3961
|
-
case "range":
|
|
3962
|
-
return {
|
|
3963
|
-
kind: "range",
|
|
3964
|
-
from: anchor.range.from,
|
|
3965
|
-
to: anchor.range.to,
|
|
3966
|
-
assoc: anchor.assoc,
|
|
3967
|
-
};
|
|
3968
|
-
case "node":
|
|
3969
|
-
return {
|
|
3970
|
-
kind: "node",
|
|
3971
|
-
at: anchor.at,
|
|
3972
|
-
assoc: anchor.assoc,
|
|
3973
|
-
};
|
|
3974
|
-
case "detached":
|
|
3975
|
-
return {
|
|
3976
|
-
kind: "detached",
|
|
3977
|
-
lastKnownRange: anchor.lastKnownRange,
|
|
3978
|
-
reason: anchor.reason,
|
|
3979
|
-
};
|
|
3980
|
-
}
|
|
3981
|
-
}
|
|
3982
|
-
|
|
3983
|
-
function toInternalAnchorProjection(
|
|
3984
|
-
anchor: EditorAnchorProjection,
|
|
3985
|
-
): InternalEditorAnchorProjection {
|
|
3986
|
-
switch (anchor.kind) {
|
|
3987
|
-
case "range":
|
|
3988
|
-
return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
|
|
3989
|
-
case "node":
|
|
3990
|
-
return createNodeAnchor(anchor.at, anchor.assoc);
|
|
3991
|
-
case "detached":
|
|
3992
|
-
return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
|
|
3993
|
-
}
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
4219
|
function createSelectionFromPublicAnchor(
|
|
3997
4220
|
anchor: EditorAnchorProjection,
|
|
3998
4221
|
): import("../core/state/editor-state.ts").SelectionSnapshot {
|
|
@@ -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
|
/**
|