@beyondwork/docx-react-component 1.0.57 → 1.0.59
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 +1 -1
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +1149 -8
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +120 -39
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +165 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +544 -35
- package/src/runtime/document-search.ts +176 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +293 -18
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -86,6 +86,13 @@ import type {
|
|
|
86
86
|
WorkflowOverlay,
|
|
87
87
|
WorkflowScope,
|
|
88
88
|
WorkflowScopeSnapshot,
|
|
89
|
+
ScopeQueryFilter,
|
|
90
|
+
ScopeQueryResult,
|
|
91
|
+
ScopeVisibility,
|
|
92
|
+
ScopeChromeVisibilityState,
|
|
93
|
+
SearchOptions,
|
|
94
|
+
TextStyleFilter,
|
|
95
|
+
WorkflowScopeMode,
|
|
89
96
|
WorkspaceMode,
|
|
90
97
|
WordReviewEditorEvent,
|
|
91
98
|
ZoomLevel,
|
|
@@ -136,7 +143,17 @@ import {
|
|
|
136
143
|
} from "../review/store/revision-store.ts";
|
|
137
144
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
138
145
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
139
|
-
import {
|
|
146
|
+
import { createSurfaceNodeSelectionProbe } from "./selection/post-edit-validator.ts";
|
|
147
|
+
import {
|
|
148
|
+
collectScopeLocations,
|
|
149
|
+
findAllScopesAt,
|
|
150
|
+
findScopesIntersecting,
|
|
151
|
+
resolveScope,
|
|
152
|
+
} from "./scope-resolver.ts";
|
|
153
|
+
import {
|
|
154
|
+
projectScopeQueryResults,
|
|
155
|
+
queryScopes as runQueryScopes,
|
|
156
|
+
} from "./query-scopes.ts";
|
|
140
157
|
import {
|
|
141
158
|
insertScopeMarkers,
|
|
142
159
|
removeScopeMarkers,
|
|
@@ -144,6 +161,10 @@ import {
|
|
|
144
161
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
145
162
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
146
163
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
164
|
+
import {
|
|
165
|
+
findTextMatches,
|
|
166
|
+
findTextWithStyleMatches,
|
|
167
|
+
} from "./document-search.ts";
|
|
147
168
|
import {
|
|
148
169
|
collectWorkflowMarkupSnapshot,
|
|
149
170
|
deriveWorkflowCandidateRangesFromMarkup,
|
|
@@ -409,6 +430,15 @@ export interface DocumentRuntime {
|
|
|
409
430
|
*/
|
|
410
431
|
applyRemoteCommandBatch(envelopes: ReadonlyArray<RemoteCommandEnvelope>): void;
|
|
411
432
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
433
|
+
/**
|
|
434
|
+
* Emit a fire-and-forget `warning_added` event + `onWarning` callback for
|
|
435
|
+
* a host-layer no-op (e.g. `applyRuntimeDeleteComment` invoked with an
|
|
436
|
+
* unknown id). The warning is NOT persisted to `state.warnings` or the
|
|
437
|
+
* compatibility report — same semantics as a reducer-emitted
|
|
438
|
+
* `effects.transientWarnings` entry, exposed here for host-side paths
|
|
439
|
+
* that never reach a reducer.
|
|
440
|
+
*/
|
|
441
|
+
emitTransientWarning(warning: InternalEditorWarning): void;
|
|
412
442
|
undo(): void;
|
|
413
443
|
redo(): void;
|
|
414
444
|
focus(): void;
|
|
@@ -419,11 +449,33 @@ export interface DocumentRuntime {
|
|
|
419
449
|
openComment(commentId: string): void;
|
|
420
450
|
resolveComment(commentId: string): void;
|
|
421
451
|
reopenComment(commentId: string): void;
|
|
422
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Append a reply entry to an existing thread. Returns the minted
|
|
454
|
+
* `{ commentId, entryId }` on success. Returns `null` when the thread
|
|
455
|
+
* is unknown, resolved, or detached — in that case a
|
|
456
|
+
* `review_target_not_found` warning fires on `onWarning` / `warning_added`
|
|
457
|
+
* with structured `details.op = "addCommentReply"` and `details.reason`
|
|
458
|
+
* ∈ `{ "comment_unknown", "comment_status" }`.
|
|
459
|
+
*/
|
|
460
|
+
addCommentReply(
|
|
461
|
+
commentId: string,
|
|
462
|
+
body: string,
|
|
463
|
+
authorId?: string,
|
|
464
|
+
): AddCommentReplyResult | null;
|
|
423
465
|
editCommentBody(commentId: string, body: string): void;
|
|
424
466
|
addScope(params: AddScopeParams): AddScopeResult;
|
|
425
467
|
getScope(scopeId: string): WorkflowScope | null;
|
|
426
468
|
removeScope(scopeId: string): void;
|
|
469
|
+
/** §C8 — Add a scope with visibility: "invisible" atomically. */
|
|
470
|
+
addInvisibleScope(params: Omit<AddScopeParams, "mode"> & { mode?: WorkflowScopeMode }): AddScopeResult;
|
|
471
|
+
/** §C8 — Set a scope's visibility (collab-replicated). */
|
|
472
|
+
setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
|
|
473
|
+
/** §C8 — Get a scope's current visibility (absent = "visible"). */
|
|
474
|
+
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
475
|
+
/** §C7 — Set local chrome visibility state (never collab-replicated). */
|
|
476
|
+
setScopeChromeVisibility(state: ScopeChromeVisibilityState): void;
|
|
477
|
+
/** §C7 — Get local chrome visibility state (default: { mode: "all" }). */
|
|
478
|
+
getScopeChromeVisibility(): ScopeChromeVisibilityState;
|
|
427
479
|
acceptChange(changeId: string): void;
|
|
428
480
|
rejectChange(changeId: string): void;
|
|
429
481
|
acceptAllChanges(): void;
|
|
@@ -504,6 +556,44 @@ export interface DocumentRuntime {
|
|
|
504
556
|
setWorkflowMetadataEntries(entries: WorkflowMetadataEntry[]): void;
|
|
505
557
|
clearWorkflowMetadataEntries(): void;
|
|
506
558
|
getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
|
|
559
|
+
/**
|
|
560
|
+
* Phase C §C1 — snapshot-based filter + join projection. See
|
|
561
|
+
* `WordReviewEditorRef.queryScopes` for contract.
|
|
562
|
+
*/
|
|
563
|
+
queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[];
|
|
564
|
+
/** Phase C §C1 — live subscription; returns unsubscribe fn. */
|
|
565
|
+
subscribeToScopeQuery(
|
|
566
|
+
filter: ScopeQueryFilter,
|
|
567
|
+
callback: (results: ScopeQueryResult[]) => void,
|
|
568
|
+
): () => void;
|
|
569
|
+
/** Phase C §C4 — text search + style filter. */
|
|
570
|
+
findAllText(query: string, options?: SearchOptions): EditorAnchorProjection[];
|
|
571
|
+
findTextWithStyle(
|
|
572
|
+
query: string,
|
|
573
|
+
filter: TextStyleFilter,
|
|
574
|
+
options?: SearchOptions,
|
|
575
|
+
): EditorAnchorProjection[];
|
|
576
|
+
selectTextWithStyle(
|
|
577
|
+
query: string,
|
|
578
|
+
filter: TextStyleFilter,
|
|
579
|
+
options?: SearchOptions,
|
|
580
|
+
): number;
|
|
581
|
+
/**
|
|
582
|
+
* Phase C §C2 — geometric scope queries. See `WordReviewEditorRef`
|
|
583
|
+
* for contract. Non-range anchors yield `[]`.
|
|
584
|
+
*/
|
|
585
|
+
findScopesAt(
|
|
586
|
+
position: EditorAnchorProjection,
|
|
587
|
+
options?: { includeHidden?: boolean; includeInvisible?: boolean },
|
|
588
|
+
): ScopeQueryResult[];
|
|
589
|
+
findScopesIntersecting(
|
|
590
|
+
range: EditorAnchorProjection,
|
|
591
|
+
options?: {
|
|
592
|
+
includeHidden?: boolean;
|
|
593
|
+
includeInvisible?: boolean;
|
|
594
|
+
mode?: "overlap" | "contain";
|
|
595
|
+
},
|
|
596
|
+
): ScopeQueryResult[];
|
|
507
597
|
setHostAnnotationOverlay(overlay: HostAnnotationOverlay): void;
|
|
508
598
|
clearHostAnnotationOverlay(): void;
|
|
509
599
|
getHostAnnotationSnapshot(): HostAnnotationSnapshot;
|
|
@@ -783,6 +873,8 @@ export function createDocumentRuntime(
|
|
|
783
873
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
784
874
|
?? [];
|
|
785
875
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
876
|
+
// §C7 — local view-state for scope chrome visibility; never collab-replicated.
|
|
877
|
+
let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
|
|
786
878
|
// P13 Slice B: shared workflow state from the collab Y.Map "workflow".
|
|
787
879
|
// Set via setSharedWorkflowState(); observed by evaluateWorkflowBlockedReasons.
|
|
788
880
|
let sharedWorkflowState: SharedWorkflowState | null = null;
|
|
@@ -925,6 +1017,10 @@ export function createDocumentRuntime(
|
|
|
925
1017
|
let cachedCompatibility:
|
|
926
1018
|
| {
|
|
927
1019
|
revisionToken: string;
|
|
1020
|
+
/** Block count at the time of the last full rebuild. O(1) proxy for structural stability. */
|
|
1021
|
+
blockCount: number;
|
|
1022
|
+
/** Warning count at time of rebuild — warnings array gets remapped on every edit. */
|
|
1023
|
+
warningCount: number;
|
|
928
1024
|
warnings: EditorState["warnings"];
|
|
929
1025
|
fatalError: EditorState["fatalError"];
|
|
930
1026
|
report: RuntimeRenderSnapshot["compatibility"];
|
|
@@ -1075,6 +1171,14 @@ export function createDocumentRuntime(
|
|
|
1075
1171
|
snapshot: WorkflowMarkupSnapshot;
|
|
1076
1172
|
}
|
|
1077
1173
|
| undefined;
|
|
1174
|
+
// Keyed on block count + subParts identity (not revisionToken) — fields only
|
|
1175
|
+
// change when blocks are inserted/deleted or subParts (headers/footers) change,
|
|
1176
|
+
// NOT on text-only edits. Cleared explicitly by updateFields() for field-refresh.
|
|
1177
|
+
let cachedFieldSnapshotEntry: {
|
|
1178
|
+
blockCount: number;
|
|
1179
|
+
subParts: CanonicalDocumentEnvelope["subParts"];
|
|
1180
|
+
snapshot: FieldSnapshot;
|
|
1181
|
+
} | null = null;
|
|
1078
1182
|
const cachedContextAnalyticsSnapshots = new Map<
|
|
1079
1183
|
string,
|
|
1080
1184
|
{
|
|
@@ -1120,16 +1224,53 @@ export function createDocumentRuntime(
|
|
|
1120
1224
|
return snapshot;
|
|
1121
1225
|
}
|
|
1122
1226
|
|
|
1227
|
+
function getCachedFieldSnapshot(document: CanonicalDocumentEnvelope): FieldSnapshot {
|
|
1228
|
+
const blockCount = document.content.children.length;
|
|
1229
|
+
if (
|
|
1230
|
+
cachedFieldSnapshotEntry &&
|
|
1231
|
+
cachedFieldSnapshotEntry.blockCount === blockCount &&
|
|
1232
|
+
cachedFieldSnapshotEntry.subParts === document.subParts
|
|
1233
|
+
) {
|
|
1234
|
+
return cachedFieldSnapshotEntry.snapshot;
|
|
1235
|
+
}
|
|
1236
|
+
const snapshot = buildFieldSnapshot(document);
|
|
1237
|
+
cachedFieldSnapshotEntry = { blockCount, subParts: document.subParts, snapshot };
|
|
1238
|
+
return snapshot;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1123
1241
|
function getCachedCompatibilityReport(
|
|
1124
1242
|
nextState: EditorState,
|
|
1125
1243
|
): RuntimeRenderSnapshot["compatibility"] {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1244
|
+
const blockCount = nextState.document.content.children.length;
|
|
1245
|
+
const warningCount = nextState.warnings?.length ?? 0;
|
|
1246
|
+
if (cachedCompatibility) {
|
|
1247
|
+
// Fast path 1: same revisionToken (selection move, surface-only refresh).
|
|
1248
|
+
if (
|
|
1249
|
+
cachedCompatibility.revisionToken === nextState.revisionToken &&
|
|
1250
|
+
cachedCompatibility.warnings === nextState.warnings &&
|
|
1251
|
+
cachedCompatibility.fatalError === nextState.fatalError
|
|
1252
|
+
) {
|
|
1253
|
+
return cachedCompatibility.report;
|
|
1254
|
+
}
|
|
1255
|
+
// Fast path 2: revisionToken changed but block structure and warning
|
|
1256
|
+
// count are stable (text-only edit). buildCompatibilityReport reads
|
|
1257
|
+
// block types, review counts, preservation, and warnings — NOT run text.
|
|
1258
|
+
// Block count + warning count + fatalError identity is an O(1) proxy:
|
|
1259
|
+
// if these are stable, the compatibility report output is unchanged.
|
|
1260
|
+
// `warnings` array gets remapped to a new reference on every commit via
|
|
1261
|
+
// remapReviewStateAfterContentChange, so we can't use reference equality.
|
|
1262
|
+
if (
|
|
1263
|
+
cachedCompatibility.blockCount === blockCount &&
|
|
1264
|
+
cachedCompatibility.warningCount === warningCount &&
|
|
1265
|
+
cachedCompatibility.fatalError === nextState.fatalError
|
|
1266
|
+
) {
|
|
1267
|
+
cachedCompatibility = {
|
|
1268
|
+
...cachedCompatibility,
|
|
1269
|
+
revisionToken: nextState.revisionToken,
|
|
1270
|
+
warnings: nextState.warnings,
|
|
1271
|
+
};
|
|
1272
|
+
return cachedCompatibility.report;
|
|
1273
|
+
}
|
|
1133
1274
|
}
|
|
1134
1275
|
|
|
1135
1276
|
const derived = createDerivedCompatibility(nextState);
|
|
@@ -1146,6 +1287,8 @@ export function createDocumentRuntime(
|
|
|
1146
1287
|
};
|
|
1147
1288
|
cachedCompatibility = {
|
|
1148
1289
|
revisionToken: nextState.revisionToken,
|
|
1290
|
+
blockCount,
|
|
1291
|
+
warningCount,
|
|
1149
1292
|
warnings: nextState.warnings,
|
|
1150
1293
|
fatalError: nextState.fatalError,
|
|
1151
1294
|
report,
|
|
@@ -1392,8 +1535,13 @@ export function createDocumentRuntime(
|
|
|
1392
1535
|
if (normalizedWorkflowOverlay) {
|
|
1393
1536
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
1394
1537
|
const activeScopes = getEffectiveWorkflowScopes(normalizedWorkflowOverlay);
|
|
1538
|
+
// §C8: invisible non-view scopes are transparent to the interaction guard.
|
|
1539
|
+
// Don't count them toward the "outside_workflow_scope" threshold.
|
|
1540
|
+
const guardingScopes = activeScopes.filter(
|
|
1541
|
+
(s) => !(s.visibility === "invisible" && s.mode !== "view"),
|
|
1542
|
+
);
|
|
1395
1543
|
|
|
1396
|
-
if (!matchingScope &&
|
|
1544
|
+
if (!matchingScope && guardingScopes.length > 0) {
|
|
1397
1545
|
reasons.push({
|
|
1398
1546
|
code: "outside_workflow_scope",
|
|
1399
1547
|
message: "Selection is outside any active workflow scope.",
|
|
@@ -1424,24 +1572,60 @@ export function createDocumentRuntime(
|
|
|
1424
1572
|
return reasons;
|
|
1425
1573
|
}
|
|
1426
1574
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1575
|
+
// §C6 — most-restrictive-wins ordering for overlap layering.
|
|
1576
|
+
const MODE_RESTRICTIVENESS: Record<WorkflowScopeMode, number> = {
|
|
1577
|
+
edit: 0,
|
|
1578
|
+
suggest: 1,
|
|
1579
|
+
comment: 2,
|
|
1580
|
+
view: 3,
|
|
1581
|
+
};
|
|
1433
1582
|
|
|
1583
|
+
/**
|
|
1584
|
+
* §C6 — Collect all guard-eligible scopes that contain `selection`,
|
|
1585
|
+
* sorted outermost→innermost (startPos ASC, endPos DESC, scopeId ASC).
|
|
1586
|
+
* Excludes invisible non-view scopes per §C8.
|
|
1587
|
+
*/
|
|
1588
|
+
function buildMatchingScopeStack(
|
|
1589
|
+
selection: EditorState["selection"],
|
|
1590
|
+
): WorkflowOverlay["scopes"] {
|
|
1591
|
+
if (!workflowOverlay) return [];
|
|
1434
1592
|
const selectionBounds = {
|
|
1435
1593
|
from: Math.min(selection.anchor, selection.head),
|
|
1436
1594
|
to: Math.max(selection.anchor, selection.head),
|
|
1437
1595
|
};
|
|
1438
1596
|
const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
|
|
1439
|
-
|
|
1597
|
+
const matching = activeScopes.filter((scope) => {
|
|
1598
|
+
// §C8
|
|
1599
|
+
if (scope.visibility === "invisible" && scope.mode !== "view") return false;
|
|
1440
1600
|
if (scope.anchor.kind === "detached") return false;
|
|
1441
1601
|
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
1442
1602
|
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
1443
1603
|
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
1444
|
-
})
|
|
1604
|
+
});
|
|
1605
|
+
// Outermost first: startPos ASC, endPos DESC (wider span = outer), scopeId ASC tiebreak
|
|
1606
|
+
matching.sort((a, b) => {
|
|
1607
|
+
const aFrom = a.anchor.kind === "range" ? a.anchor.from : (a.anchor as { at: number }).at;
|
|
1608
|
+
const bFrom = b.anchor.kind === "range" ? b.anchor.from : (b.anchor as { at: number }).at;
|
|
1609
|
+
if (aFrom !== bFrom) return aFrom - bFrom;
|
|
1610
|
+
const aTo = a.anchor.kind === "range" ? a.anchor.to : (a.anchor as { at: number }).at;
|
|
1611
|
+
const bTo = b.anchor.kind === "range" ? b.anchor.to : (b.anchor as { at: number }).at;
|
|
1612
|
+
if (aTo !== bTo) return bTo - aTo; // wider first
|
|
1613
|
+
return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
|
|
1614
|
+
});
|
|
1615
|
+
return matching;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function getMatchingWorkflowScope(
|
|
1619
|
+
selection: EditorState["selection"],
|
|
1620
|
+
): WorkflowOverlay["scopes"][number] | null {
|
|
1621
|
+
const stack = buildMatchingScopeStack(selection);
|
|
1622
|
+
if (stack.length === 0) return null;
|
|
1623
|
+
// §C6 — most-restrictive scope wins across all overlapping scopes.
|
|
1624
|
+
return stack.reduce((best, scope) =>
|
|
1625
|
+
(MODE_RESTRICTIVENESS[scope.mode] ?? 0) > (MODE_RESTRICTIVENESS[best.mode] ?? 0)
|
|
1626
|
+
? scope
|
|
1627
|
+
: best,
|
|
1628
|
+
);
|
|
1445
1629
|
}
|
|
1446
1630
|
|
|
1447
1631
|
function getEffectiveDocumentMode(
|
|
@@ -1860,6 +2044,7 @@ export function createDocumentRuntime(
|
|
|
1860
2044
|
|
|
1861
2045
|
const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
|
|
1862
2046
|
const matchingScope = getMatchingWorkflowScope(state.selection);
|
|
2047
|
+
const scopeStack = buildMatchingScopeStack(state.selection);
|
|
1863
2048
|
const primaryBlockedReason = blockedReasons[0];
|
|
1864
2049
|
const effectiveMode = primaryBlockedReason
|
|
1865
2050
|
? (
|
|
@@ -1872,10 +2057,19 @@ export function createDocumentRuntime(
|
|
|
1872
2057
|
: getEffectiveDocumentMode(state.selection) === "suggesting"
|
|
1873
2058
|
? "suggest"
|
|
1874
2059
|
: matchingScope?.mode ?? "edit";
|
|
2060
|
+
const matchedScopeStack: InteractionGuardSnapshot["matchedScopeStack"] =
|
|
2061
|
+
scopeStack.length > 0
|
|
2062
|
+
? scopeStack.map((s) => ({
|
|
2063
|
+
scopeId: s.scopeId,
|
|
2064
|
+
mode: s.mode,
|
|
2065
|
+
visibility: s.visibility ?? "visible",
|
|
2066
|
+
}))
|
|
2067
|
+
: undefined;
|
|
1875
2068
|
const snapshot: InteractionGuardSnapshot = {
|
|
1876
2069
|
effectiveMode,
|
|
1877
2070
|
...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
|
|
1878
2071
|
...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
|
|
2072
|
+
...(matchedScopeStack ? { matchedScopeStack } : {}),
|
|
1879
2073
|
targetAccess:
|
|
1880
2074
|
effectiveMode === "edit"
|
|
1881
2075
|
? "direct-edit"
|
|
@@ -1960,7 +2154,7 @@ export function createDocumentRuntime(
|
|
|
1960
2154
|
|
|
1961
2155
|
const snapshot = collectWorkflowMarkupSnapshot({
|
|
1962
2156
|
renderSnapshot: cachedRenderSnapshot,
|
|
1963
|
-
fieldSnapshot:
|
|
2157
|
+
fieldSnapshot: getCachedFieldSnapshot(state.document),
|
|
1964
2158
|
protectionSnapshot,
|
|
1965
2159
|
preservation: state.document.preservation,
|
|
1966
2160
|
workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
|
|
@@ -2215,6 +2409,7 @@ export function createDocumentRuntime(
|
|
|
2215
2409
|
},
|
|
2216
2410
|
surface,
|
|
2217
2411
|
protectionSnapshot,
|
|
2412
|
+
grabbedObjectId: grabState.objectId,
|
|
2218
2413
|
};
|
|
2219
2414
|
}
|
|
2220
2415
|
|
|
@@ -2264,6 +2459,7 @@ export function createDocumentRuntime(
|
|
|
2264
2459
|
cachedWorkflowScopeSnapshot = undefined;
|
|
2265
2460
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
2266
2461
|
cachedWorkflowMarkupSnapshot = undefined;
|
|
2462
|
+
cachedFieldSnapshotEntry = null;
|
|
2267
2463
|
cachedContextAnalyticsSnapshots.clear();
|
|
2268
2464
|
lastEmittedContextAnalyticsSnapshots = undefined;
|
|
2269
2465
|
}
|
|
@@ -2280,8 +2476,11 @@ export function createDocumentRuntime(
|
|
|
2280
2476
|
document,
|
|
2281
2477
|
};
|
|
2282
2478
|
if (previousDocument.subParts !== document.subParts) {
|
|
2283
|
-
|
|
2284
|
-
|
|
2479
|
+
const fontInput = collectFontLoaderInput(document);
|
|
2480
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previousDocument).families)) {
|
|
2481
|
+
fontLoader.refresh(fontInput);
|
|
2482
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2483
|
+
}
|
|
2285
2484
|
}
|
|
2286
2485
|
invalidateDerivedRuntimeCaches();
|
|
2287
2486
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
@@ -2440,6 +2639,15 @@ export function createDocumentRuntime(
|
|
|
2440
2639
|
reasons,
|
|
2441
2640
|
});
|
|
2442
2641
|
},
|
|
2642
|
+
emitTransientWarning(warning) {
|
|
2643
|
+
const publicWarning = toPublicWarning(warning);
|
|
2644
|
+
emit({
|
|
2645
|
+
type: "warning_added",
|
|
2646
|
+
documentId: state.documentId,
|
|
2647
|
+
warning: publicWarning,
|
|
2648
|
+
});
|
|
2649
|
+
options.onWarning?.(publicWarning);
|
|
2650
|
+
},
|
|
2443
2651
|
dispatch(command) {
|
|
2444
2652
|
const commandSelection = getCommandSelection(command, state.selection);
|
|
2445
2653
|
if (isMutationCommand(command)) {
|
|
@@ -3050,7 +3258,7 @@ export function createDocumentRuntime(
|
|
|
3050
3258
|
});
|
|
3051
3259
|
},
|
|
3052
3260
|
addCommentReply(commentId, body, authorId) {
|
|
3053
|
-
const
|
|
3261
|
+
const priorCount =
|
|
3054
3262
|
state.document.review.comments[commentId]?.entries?.length ?? 0;
|
|
3055
3263
|
this.dispatch({
|
|
3056
3264
|
type: "comment.add-reply",
|
|
@@ -3059,8 +3267,16 @@ export function createDocumentRuntime(
|
|
|
3059
3267
|
authorId: authorId ?? defaultAuthorId,
|
|
3060
3268
|
origin: createOrigin("api", clock()),
|
|
3061
3269
|
});
|
|
3062
|
-
|
|
3063
|
-
|
|
3270
|
+
// Read post-dispatch state. The reducer skips silently on unknown /
|
|
3271
|
+
// resolved / detached threads and emits a `review_target_not_found`
|
|
3272
|
+
// transient warning from `effects.transientWarnings` — callers who
|
|
3273
|
+
// need to know about the skip listen on `onWarning`.
|
|
3274
|
+
const entries = state.document.review.comments[commentId]?.entries ?? [];
|
|
3275
|
+
if (entries.length <= priorCount) {
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
const last = entries[entries.length - 1]!;
|
|
3279
|
+
return { commentId, entryId: last.entryId };
|
|
3064
3280
|
},
|
|
3065
3281
|
editCommentBody(commentId, body) {
|
|
3066
3282
|
this.dispatch({
|
|
@@ -3217,6 +3433,38 @@ export function createDocumentRuntime(
|
|
|
3217
3433
|
}
|
|
3218
3434
|
}
|
|
3219
3435
|
},
|
|
3436
|
+
addInvisibleScope(params) {
|
|
3437
|
+
const result = this.addScope({
|
|
3438
|
+
...params,
|
|
3439
|
+
mode: params.mode ?? "comment",
|
|
3440
|
+
});
|
|
3441
|
+
this.setScopeVisibility(result.scopeId, "invisible");
|
|
3442
|
+
return result;
|
|
3443
|
+
},
|
|
3444
|
+
setScopeVisibility(scopeId, visibility) {
|
|
3445
|
+
if (!workflowOverlay) return;
|
|
3446
|
+
const idx = workflowOverlay.scopes.findIndex((s) => s.scopeId === scopeId);
|
|
3447
|
+
if (idx === -1) return;
|
|
3448
|
+
const nextScopes = workflowOverlay.scopes.map((s) =>
|
|
3449
|
+
s.scopeId === scopeId ? { ...s, visibility } : s,
|
|
3450
|
+
);
|
|
3451
|
+
this.dispatch({
|
|
3452
|
+
type: "workflow.set-overlay",
|
|
3453
|
+
overlay: { ...workflowOverlay, scopes: nextScopes },
|
|
3454
|
+
origin: createOrigin("api", clock()),
|
|
3455
|
+
});
|
|
3456
|
+
},
|
|
3457
|
+
getScopeVisibility(scopeId): ScopeVisibility {
|
|
3458
|
+
if (!workflowOverlay) return "visible";
|
|
3459
|
+
const scope = workflowOverlay.scopes.find((s) => s.scopeId === scopeId);
|
|
3460
|
+
return scope?.visibility ?? "visible";
|
|
3461
|
+
},
|
|
3462
|
+
setScopeChromeVisibility(state) {
|
|
3463
|
+
scopeChromeVisibilityState = state;
|
|
3464
|
+
},
|
|
3465
|
+
getScopeChromeVisibility(): ScopeChromeVisibilityState {
|
|
3466
|
+
return scopeChromeVisibilityState;
|
|
3467
|
+
},
|
|
3220
3468
|
acceptChange(changeId) {
|
|
3221
3469
|
this.dispatch({
|
|
3222
3470
|
type: "change.accept",
|
|
@@ -3349,7 +3597,11 @@ export function createDocumentRuntime(
|
|
|
3349
3597
|
getFootnoteResolver(): FootnoteResolver | undefined {
|
|
3350
3598
|
const collection = state.document.subParts?.footnoteCollection;
|
|
3351
3599
|
if (!collection) return undefined;
|
|
3352
|
-
return createFootnoteResolver(
|
|
3600
|
+
return createFootnoteResolver(
|
|
3601
|
+
collection,
|
|
3602
|
+
collectSectionPropertiesInOrder(state.document),
|
|
3603
|
+
state.document,
|
|
3604
|
+
);
|
|
3353
3605
|
},
|
|
3354
3606
|
layout: layoutFacet,
|
|
3355
3607
|
getCurrentLocation() {
|
|
@@ -3496,6 +3748,9 @@ export function createDocumentRuntime(
|
|
|
3496
3748
|
options,
|
|
3497
3749
|
);
|
|
3498
3750
|
if (refreshed.changed) {
|
|
3751
|
+
// Field display text changed — clear field snapshot cache so the
|
|
3752
|
+
// next getCachedFieldSnapshot call rebuilds with fresh displayText.
|
|
3753
|
+
cachedFieldSnapshotEntry = null;
|
|
3499
3754
|
this.dispatch({
|
|
3500
3755
|
type: "document.replace",
|
|
3501
3756
|
document: refreshed.document,
|
|
@@ -3690,6 +3945,102 @@ export function createDocumentRuntime(
|
|
|
3690
3945
|
getWorkflowMetadataSnapshot() {
|
|
3691
3946
|
return deriveWorkflowMetadataSnapshot();
|
|
3692
3947
|
},
|
|
3948
|
+
queryScopes(filter) {
|
|
3949
|
+
return runQueryScopes(
|
|
3950
|
+
{
|
|
3951
|
+
overlay: workflowOverlay,
|
|
3952
|
+
entries: workflowMetadataEntries,
|
|
3953
|
+
document: state.document,
|
|
3954
|
+
},
|
|
3955
|
+
filter,
|
|
3956
|
+
);
|
|
3957
|
+
},
|
|
3958
|
+
subscribeToScopeQuery(filter, callback) {
|
|
3959
|
+
const buildKey = (results: ScopeQueryResult[]) =>
|
|
3960
|
+
results
|
|
3961
|
+
.map(
|
|
3962
|
+
(r) =>
|
|
3963
|
+
`${r.scope.scopeId}:${r.scope.version ?? 0}:${r.scope.visibility ?? "visible"}`,
|
|
3964
|
+
)
|
|
3965
|
+
.join(",");
|
|
3966
|
+
|
|
3967
|
+
let lastKey = "";
|
|
3968
|
+
const fire = () => {
|
|
3969
|
+
const results = this.queryScopes(filter);
|
|
3970
|
+
const key = buildKey(results);
|
|
3971
|
+
if (key !== lastKey) {
|
|
3972
|
+
lastKey = key;
|
|
3973
|
+
callback(results);
|
|
3974
|
+
}
|
|
3975
|
+
};
|
|
3976
|
+
|
|
3977
|
+
// Immediate initial fire
|
|
3978
|
+
fire();
|
|
3979
|
+
|
|
3980
|
+
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
3981
|
+
const unsub = this.subscribe(() => {
|
|
3982
|
+
if (pendingTimer !== null) return;
|
|
3983
|
+
pendingTimer = setTimeout(() => {
|
|
3984
|
+
pendingTimer = null;
|
|
3985
|
+
fire();
|
|
3986
|
+
}, 0);
|
|
3987
|
+
});
|
|
3988
|
+
|
|
3989
|
+
return () => {
|
|
3990
|
+
unsub();
|
|
3991
|
+
if (pendingTimer !== null) {
|
|
3992
|
+
clearTimeout(pendingTimer);
|
|
3993
|
+
pendingTimer = null;
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
},
|
|
3997
|
+
findAllText(query, options) {
|
|
3998
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
3999
|
+
return findTextMatches(state.document, sel, query, options ?? {});
|
|
4000
|
+
},
|
|
4001
|
+
findTextWithStyle(query, filter, options) {
|
|
4002
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
4003
|
+
return findTextWithStyleMatches(state.document, sel, query, filter, options ?? {});
|
|
4004
|
+
},
|
|
4005
|
+
selectTextWithStyle(query, filter, options) {
|
|
4006
|
+
const sel = makePublicSelectionSnapshot(state.selection.anchor, state.selection.head);
|
|
4007
|
+
const hits = findTextWithStyleMatches(state.document, sel, query, filter, options ?? {});
|
|
4008
|
+
if (hits.length > 0 && hits[0]) {
|
|
4009
|
+
const first = hits[0];
|
|
4010
|
+
if (first.kind === "range") {
|
|
4011
|
+
this.dispatch({
|
|
4012
|
+
type: "selection.set",
|
|
4013
|
+
selection: createSelectionSnapshot(first.from, first.to),
|
|
4014
|
+
origin: createOrigin("api", clock()),
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
return hits.length;
|
|
4019
|
+
},
|
|
4020
|
+
findScopesAt(position, options) {
|
|
4021
|
+
const pos =
|
|
4022
|
+
position.kind === "range"
|
|
4023
|
+
? position.from
|
|
4024
|
+
: position.kind === "node"
|
|
4025
|
+
? position.at
|
|
4026
|
+
: null;
|
|
4027
|
+
if (pos === null) return [];
|
|
4028
|
+
const hits = findAllScopesAt(state.document, pos);
|
|
4029
|
+
return projectScopeQueryResults(
|
|
4030
|
+
{ overlay: workflowOverlay, entries: workflowMetadataEntries, document: state.document },
|
|
4031
|
+
hits.map((h) => h.scopeId),
|
|
4032
|
+
options,
|
|
4033
|
+
);
|
|
4034
|
+
},
|
|
4035
|
+
findScopesIntersecting(range, options) {
|
|
4036
|
+
if (range.kind !== "range") return [];
|
|
4037
|
+
const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
|
|
4038
|
+
return projectScopeQueryResults(
|
|
4039
|
+
{ overlay: workflowOverlay, entries: workflowMetadataEntries, document: state.document },
|
|
4040
|
+
hits.map((h) => h.scopeId),
|
|
4041
|
+
options,
|
|
4042
|
+
);
|
|
4043
|
+
},
|
|
3693
4044
|
setHostAnnotationOverlay(overlay) {
|
|
3694
4045
|
this.dispatch({
|
|
3695
4046
|
type: "host-annotation.set-overlay",
|
|
@@ -3858,6 +4209,38 @@ export function createDocumentRuntime(
|
|
|
3858
4209
|
}
|
|
3859
4210
|
|
|
3860
4211
|
function applyTransactionToState(transaction: EditorTransaction): void {
|
|
4212
|
+
// Pure-no-op short-circuit: when a reducer skipped without emitting any
|
|
4213
|
+
// observable effect AND the selection is identical, skip finalizeState
|
|
4214
|
+
// / invalidate / refresh / notify entirely. This keeps hot paths (e.g.
|
|
4215
|
+
// host loops that query invalid ids) cheap — no clock allocation, no
|
|
4216
|
+
// listener loop, no snapshot rebuild for nothing. Transient-warning
|
|
4217
|
+
// commits (the `review_target_not_found` path) carry
|
|
4218
|
+
// `transientWarnings.length > 0` and MUST fall through so `notify`
|
|
4219
|
+
// can emit them. Selection-only dispatches (e.g. `selection.set`) also
|
|
4220
|
+
// fall through because `nextState.selection !== state.selection`.
|
|
4221
|
+
const effects = transaction.effects;
|
|
4222
|
+
const selectionUnchanged = transaction.nextState.selection === state.selection;
|
|
4223
|
+
const isPureNoop =
|
|
4224
|
+
!transaction.markDirty &&
|
|
4225
|
+
transaction.historyBoundary === "skip" &&
|
|
4226
|
+
transaction.mapping.steps.length === 0 &&
|
|
4227
|
+
selectionUnchanged &&
|
|
4228
|
+
effects.warningsAdded.length === 0 &&
|
|
4229
|
+
effects.warningsCleared.length === 0 &&
|
|
4230
|
+
(effects.transientWarnings?.length ?? 0) === 0 &&
|
|
4231
|
+
!effects.commentAdded &&
|
|
4232
|
+
!effects.commentResolved &&
|
|
4233
|
+
!effects.commentReopened &&
|
|
4234
|
+
!effects.commentReplyAdded &&
|
|
4235
|
+
!effects.commentBodyEdited &&
|
|
4236
|
+
!effects.changeAccepted &&
|
|
4237
|
+
!effects.changeRejected &&
|
|
4238
|
+
!effects.revisionAuthored &&
|
|
4239
|
+
!effects.commandBlocked;
|
|
4240
|
+
if (isPureNoop) {
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
|
|
3861
4244
|
const previous = state;
|
|
3862
4245
|
|
|
3863
4246
|
const tApply0 = performance.now();
|
|
@@ -3881,8 +4264,11 @@ export function createDocumentRuntime(
|
|
|
3881
4264
|
}
|
|
3882
4265
|
}
|
|
3883
4266
|
if (previous.document.subParts !== state.document.subParts) {
|
|
3884
|
-
|
|
3885
|
-
|
|
4267
|
+
const fontInput = collectFontLoaderInput(state.document);
|
|
4268
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previous.document).families)) {
|
|
4269
|
+
fontLoader.refresh(fontInput);
|
|
4270
|
+
layoutEngine.invalidateMeasurementCache();
|
|
4271
|
+
}
|
|
3886
4272
|
}
|
|
3887
4273
|
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
3888
4274
|
|
|
@@ -3912,10 +4298,16 @@ export function createDocumentRuntime(
|
|
|
3912
4298
|
// which is optional in the public API for shape-only reasons — the helper
|
|
3913
4299
|
// itself always returns a defined snapshot.
|
|
3914
4300
|
const surfaceForValidation = getCachedSurface(state.document, activeStory)!;
|
|
4301
|
+
const validationOptions = state.selection.activeRange.kind === "node"
|
|
4302
|
+
? {
|
|
4303
|
+
isValidNodeTarget: createSurfaceNodeSelectionProbe(surfaceForValidation),
|
|
4304
|
+
}
|
|
4305
|
+
: undefined;
|
|
3915
4306
|
const validatedSelection = validateSelectionAgainstDocument(
|
|
3916
4307
|
state.document,
|
|
3917
4308
|
state.selection,
|
|
3918
4309
|
surfaceForValidation.storySize,
|
|
4310
|
+
validationOptions,
|
|
3919
4311
|
);
|
|
3920
4312
|
if (validatedSelection !== state.selection) {
|
|
3921
4313
|
state = { ...state, selection: validatedSelection };
|
|
@@ -4117,6 +4509,23 @@ export function createDocumentRuntime(
|
|
|
4117
4509
|
options.onWarning?.(publicWarning);
|
|
4118
4510
|
}
|
|
4119
4511
|
|
|
4512
|
+
if (transaction.effects.transientWarnings) {
|
|
4513
|
+
// Fire-and-forget diagnostics (e.g. `review_target_not_found` on a
|
|
4514
|
+
// silent-skip reducer). Surfaces on `warning_added` + `onWarning`
|
|
4515
|
+
// like any other warning, but never touches `state.warnings` or the
|
|
4516
|
+
// compatibility report, so repeated no-op calls (e.g. a host
|
|
4517
|
+
// looping `resolveComment` over stale ids) do not accumulate entries.
|
|
4518
|
+
for (const warning of transaction.effects.transientWarnings) {
|
|
4519
|
+
const publicWarning = toPublicWarning(warning);
|
|
4520
|
+
emit({
|
|
4521
|
+
type: "warning_added",
|
|
4522
|
+
documentId: next.documentId,
|
|
4523
|
+
warning: publicWarning,
|
|
4524
|
+
});
|
|
4525
|
+
options.onWarning?.(publicWarning);
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4120
4529
|
for (const cleared of transaction.effects.warningsCleared) {
|
|
4121
4530
|
emit({
|
|
4122
4531
|
type: "warning_cleared",
|
|
@@ -4366,9 +4775,16 @@ export function createDocumentRuntime(
|
|
|
4366
4775
|
base: EditorTransaction["effects"],
|
|
4367
4776
|
local: EditorTransaction["effects"],
|
|
4368
4777
|
): EditorTransaction["effects"] {
|
|
4778
|
+
const baseTransient = base.transientWarnings ?? [];
|
|
4779
|
+
const localTransient = local.transientWarnings ?? [];
|
|
4780
|
+
const mergedTransient =
|
|
4781
|
+
baseTransient.length + localTransient.length === 0
|
|
4782
|
+
? undefined
|
|
4783
|
+
: [...baseTransient, ...localTransient];
|
|
4369
4784
|
return {
|
|
4370
4785
|
warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
|
|
4371
4786
|
warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
|
|
4787
|
+
...(mergedTransient ? { transientWarnings: mergedTransient } : {}),
|
|
4372
4788
|
commentAdded: base.commentAdded ?? local.commentAdded,
|
|
4373
4789
|
commentResolved: base.commentResolved ?? local.commentResolved,
|
|
4374
4790
|
commentReopened: base.commentReopened ?? local.commentReopened,
|
|
@@ -4912,7 +5328,13 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
4912
5328
|
function toStructuredRuntimeException<T extends InternalEditorError>(
|
|
4913
5329
|
error: T,
|
|
4914
5330
|
): Error & T {
|
|
4915
|
-
|
|
5331
|
+
const exception = Object.assign(new Error(error.message), error);
|
|
5332
|
+
// Set `name` so host wrappers that must branch across process / bundle
|
|
5333
|
+
// boundaries (e.g. the gRPC handlers in `vendor/beyondwork/src/ts/docx-api`)
|
|
5334
|
+
// can discriminate structured editor errors from generic `Error` objects
|
|
5335
|
+
// via `error.name === "EditorError"` — `instanceof` is unreliable there.
|
|
5336
|
+
exception.name = "EditorError";
|
|
5337
|
+
return exception;
|
|
4916
5338
|
}
|
|
4917
5339
|
|
|
4918
5340
|
function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
@@ -5010,11 +5432,14 @@ function extractSelectionFragment(
|
|
|
5010
5432
|
}
|
|
5011
5433
|
|
|
5012
5434
|
/**
|
|
5013
|
-
* Collect the stable ids of comment threads whose
|
|
5014
|
-
*
|
|
5015
|
-
*
|
|
5016
|
-
* the thread
|
|
5017
|
-
*
|
|
5435
|
+
* Collect the stable ids of comment threads whose host-observable state
|
|
5436
|
+
* differs between two commits. A reference-identity diff was too noisy —
|
|
5437
|
+
* any `remapCommentStore` pass (fired on every text edit that touches a
|
|
5438
|
+
* thread's anchor region) rebuilt the thread objects and would have marked
|
|
5439
|
+
* every thread as "changed" even when only their anchor numerics shifted
|
|
5440
|
+
* in a way that doesn't affect what the host renders. The semantic diff
|
|
5441
|
+
* below compares the fields hosts actually consume: status, anchor shape,
|
|
5442
|
+
* entry count + ordering, resolution metadata, and warning ids.
|
|
5018
5443
|
*/
|
|
5019
5444
|
function diffCommentMapKeys(
|
|
5020
5445
|
previous: CanonicalDocumentEnvelope["review"]["comments"],
|
|
@@ -5022,14 +5447,65 @@ function diffCommentMapKeys(
|
|
|
5022
5447
|
): string[] {
|
|
5023
5448
|
const changed = new Set<string>();
|
|
5024
5449
|
for (const id of Object.keys(previous)) {
|
|
5025
|
-
if (
|
|
5450
|
+
if (!next[id]) changed.add(id);
|
|
5026
5451
|
}
|
|
5027
5452
|
for (const id of Object.keys(next)) {
|
|
5028
|
-
|
|
5453
|
+
const prev = previous[id];
|
|
5454
|
+
if (!prev) {
|
|
5455
|
+
changed.add(id);
|
|
5456
|
+
continue;
|
|
5457
|
+
}
|
|
5458
|
+
if (prev === next[id]) continue;
|
|
5459
|
+
if (!semanticallyEqualCommentThreads(prev, next[id]!)) changed.add(id);
|
|
5029
5460
|
}
|
|
5030
5461
|
return Array.from(changed);
|
|
5031
5462
|
}
|
|
5032
5463
|
|
|
5464
|
+
function semanticallyEqualCommentThreads(
|
|
5465
|
+
prev: CanonicalDocumentEnvelope["review"]["comments"][string],
|
|
5466
|
+
next: CanonicalDocumentEnvelope["review"]["comments"][string],
|
|
5467
|
+
): boolean {
|
|
5468
|
+
if (prev.status !== next.status) return false;
|
|
5469
|
+
if (prev.isResolved !== next.isResolved) return false;
|
|
5470
|
+
if (prev.resolvedAt !== next.resolvedAt) return false;
|
|
5471
|
+
if (prev.resolution?.resolvedAt !== next.resolution?.resolvedAt) return false;
|
|
5472
|
+
if (prev.resolution?.resolvedBy !== next.resolution?.resolvedBy) return false;
|
|
5473
|
+
if (prev.body !== next.body) return false;
|
|
5474
|
+
if ((prev.entries?.length ?? 0) !== (next.entries?.length ?? 0)) return false;
|
|
5475
|
+
const prevEntries = prev.entries ?? [];
|
|
5476
|
+
const nextEntries = next.entries ?? [];
|
|
5477
|
+
for (let i = 0; i < prevEntries.length; i++) {
|
|
5478
|
+
const pe = prevEntries[i]!;
|
|
5479
|
+
const ne = nextEntries[i]!;
|
|
5480
|
+
if (pe.entryId !== ne.entryId) return false;
|
|
5481
|
+
if (pe.body !== ne.body) return false;
|
|
5482
|
+
if (pe.authorId !== ne.authorId) return false;
|
|
5483
|
+
}
|
|
5484
|
+
const prevWarnings = prev.warningIds ?? [];
|
|
5485
|
+
const nextWarnings = next.warningIds ?? [];
|
|
5486
|
+
if (prevWarnings.length !== nextWarnings.length) return false;
|
|
5487
|
+
for (let i = 0; i < prevWarnings.length; i++) {
|
|
5488
|
+
if (prevWarnings[i] !== nextWarnings[i]) return false;
|
|
5489
|
+
}
|
|
5490
|
+
const prevAnchor = prev.anchor;
|
|
5491
|
+
const nextAnchor = next.anchor;
|
|
5492
|
+
if (prevAnchor === nextAnchor) return true;
|
|
5493
|
+
if (prevAnchor.kind !== nextAnchor.kind) return false;
|
|
5494
|
+
if (prevAnchor.kind === "range" && nextAnchor.kind === "range") {
|
|
5495
|
+
return (
|
|
5496
|
+
prevAnchor.range.from === nextAnchor.range.from &&
|
|
5497
|
+
prevAnchor.range.to === nextAnchor.range.to
|
|
5498
|
+
);
|
|
5499
|
+
}
|
|
5500
|
+
if (prevAnchor.kind === "node" && nextAnchor.kind === "node") {
|
|
5501
|
+
return prevAnchor.at === nextAnchor.at;
|
|
5502
|
+
}
|
|
5503
|
+
if (prevAnchor.kind === "detached" && nextAnchor.kind === "detached") {
|
|
5504
|
+
return prevAnchor.reason === nextAnchor.reason;
|
|
5505
|
+
}
|
|
5506
|
+
return false;
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5033
5509
|
function toPublicCompatibilityReport(
|
|
5034
5510
|
report: InternalCompatibilityReport,
|
|
5035
5511
|
): CompatibilityReport {
|
|
@@ -5443,6 +5919,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
5443
5919
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5444
5920
|
}
|
|
5445
5921
|
|
|
5922
|
+
function makePublicSelectionSnapshot(anchor: number, head: number): SelectionSnapshot {
|
|
5923
|
+
const from = Math.min(anchor, head);
|
|
5924
|
+
const to = Math.max(anchor, head);
|
|
5925
|
+
return {
|
|
5926
|
+
anchor,
|
|
5927
|
+
head,
|
|
5928
|
+
isCollapsed: anchor === head,
|
|
5929
|
+
activeRange: {
|
|
5930
|
+
kind: "range",
|
|
5931
|
+
from,
|
|
5932
|
+
to,
|
|
5933
|
+
assoc: { start: -1, end: 1 },
|
|
5934
|
+
},
|
|
5935
|
+
};
|
|
5936
|
+
}
|
|
5937
|
+
|
|
5446
5938
|
/** Commands that are safe in viewing mode (no document mutation). */
|
|
5447
5939
|
const NON_MUTATION_COMMANDS = new Set([
|
|
5448
5940
|
"selection.set",
|
|
@@ -5459,6 +5951,9 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
5459
5951
|
"workflow.clear-metadata-entries",
|
|
5460
5952
|
"host-annotation.set-overlay",
|
|
5461
5953
|
"host-annotation.clear-overlay",
|
|
5954
|
+
// API-level full-document replacement (scope markers, protection refresh,
|
|
5955
|
+
// collab replay). Not user-initiated — must bypass the workflow guard.
|
|
5956
|
+
"document.replace",
|
|
5462
5957
|
]);
|
|
5463
5958
|
|
|
5464
5959
|
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
@@ -6559,6 +7054,17 @@ const fontLoaderInputCache = new WeakMap<
|
|
|
6559
7054
|
WeakMap<object, { families: readonly string[] }>
|
|
6560
7055
|
>();
|
|
6561
7056
|
|
|
7057
|
+
function fontFamiliesEqual(
|
|
7058
|
+
a: readonly string[],
|
|
7059
|
+
b: readonly string[],
|
|
7060
|
+
): boolean {
|
|
7061
|
+
if (a.length !== b.length) return false;
|
|
7062
|
+
// Both arrays are Set-derived (no duplicates); sort for order-independence.
|
|
7063
|
+
const sortedA = [...a].sort();
|
|
7064
|
+
const sortedB = [...b].sort();
|
|
7065
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
7066
|
+
}
|
|
7067
|
+
|
|
6562
7068
|
function collectFontLoaderInput(
|
|
6563
7069
|
document: CanonicalDocumentEnvelope,
|
|
6564
7070
|
): { families: readonly string[] } {
|
|
@@ -6611,6 +7117,9 @@ function collectFontLoaderInputUncached(
|
|
|
6611
7117
|
/** Test-only export of the uncached walk so memoization tests can spy on it. */
|
|
6612
7118
|
export const __collectFontLoaderInputUncached = collectFontLoaderInputUncached;
|
|
6613
7119
|
|
|
7120
|
+
/** Test-only export of the font-family set equality helper. */
|
|
7121
|
+
export const __fontFamiliesEqual = fontFamiliesEqual;
|
|
7122
|
+
|
|
6614
7123
|
/**
|
|
6615
7124
|
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
6616
7125
|
* the platform supports it and fonts have resolved. Errors are swallowed
|