@beyondwork/docx-react-component 1.0.58 → 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 +2 -2
- 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 +978 -10
- 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 +72 -42
- 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 +159 -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 +476 -34
- package/src/runtime/document-search.ts +115 -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 +5 -8
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- 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 +290 -21
- 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 +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -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 +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -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 +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -88,6 +88,11 @@ import type {
|
|
|
88
88
|
WorkflowScopeSnapshot,
|
|
89
89
|
ScopeQueryFilter,
|
|
90
90
|
ScopeQueryResult,
|
|
91
|
+
ScopeVisibility,
|
|
92
|
+
ScopeChromeVisibilityState,
|
|
93
|
+
SearchOptions,
|
|
94
|
+
TextStyleFilter,
|
|
95
|
+
WorkflowScopeMode,
|
|
91
96
|
WorkspaceMode,
|
|
92
97
|
WordReviewEditorEvent,
|
|
93
98
|
ZoomLevel,
|
|
@@ -138,6 +143,7 @@ import {
|
|
|
138
143
|
} from "../review/store/revision-store.ts";
|
|
139
144
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
140
145
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
146
|
+
import { createSurfaceNodeSelectionProbe } from "./selection/post-edit-validator.ts";
|
|
141
147
|
import {
|
|
142
148
|
collectScopeLocations,
|
|
143
149
|
findAllScopesAt,
|
|
@@ -155,6 +161,10 @@ import {
|
|
|
155
161
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
156
162
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
157
163
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
164
|
+
import {
|
|
165
|
+
findTextMatches,
|
|
166
|
+
findTextWithStyleMatches,
|
|
167
|
+
} from "./document-search.ts";
|
|
158
168
|
import {
|
|
159
169
|
collectWorkflowMarkupSnapshot,
|
|
160
170
|
deriveWorkflowCandidateRangesFromMarkup,
|
|
@@ -420,6 +430,15 @@ export interface DocumentRuntime {
|
|
|
420
430
|
*/
|
|
421
431
|
applyRemoteCommandBatch(envelopes: ReadonlyArray<RemoteCommandEnvelope>): void;
|
|
422
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;
|
|
423
442
|
undo(): void;
|
|
424
443
|
redo(): void;
|
|
425
444
|
focus(): void;
|
|
@@ -430,11 +449,33 @@ export interface DocumentRuntime {
|
|
|
430
449
|
openComment(commentId: string): void;
|
|
431
450
|
resolveComment(commentId: string): void;
|
|
432
451
|
reopenComment(commentId: string): void;
|
|
433
|
-
|
|
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;
|
|
434
465
|
editCommentBody(commentId: string, body: string): void;
|
|
435
466
|
addScope(params: AddScopeParams): AddScopeResult;
|
|
436
467
|
getScope(scopeId: string): WorkflowScope | null;
|
|
437
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;
|
|
438
479
|
acceptChange(changeId: string): void;
|
|
439
480
|
rejectChange(changeId: string): void;
|
|
440
481
|
acceptAllChanges(): void;
|
|
@@ -520,6 +561,23 @@ export interface DocumentRuntime {
|
|
|
520
561
|
* `WordReviewEditorRef.queryScopes` for contract.
|
|
521
562
|
*/
|
|
522
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;
|
|
523
581
|
/**
|
|
524
582
|
* Phase C §C2 — geometric scope queries. See `WordReviewEditorRef`
|
|
525
583
|
* for contract. Non-range anchors yield `[]`.
|
|
@@ -815,6 +873,8 @@ export function createDocumentRuntime(
|
|
|
815
873
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
816
874
|
?? [];
|
|
817
875
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
876
|
+
// §C7 — local view-state for scope chrome visibility; never collab-replicated.
|
|
877
|
+
let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
|
|
818
878
|
// P13 Slice B: shared workflow state from the collab Y.Map "workflow".
|
|
819
879
|
// Set via setSharedWorkflowState(); observed by evaluateWorkflowBlockedReasons.
|
|
820
880
|
let sharedWorkflowState: SharedWorkflowState | null = null;
|
|
@@ -957,6 +1017,10 @@ export function createDocumentRuntime(
|
|
|
957
1017
|
let cachedCompatibility:
|
|
958
1018
|
| {
|
|
959
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;
|
|
960
1024
|
warnings: EditorState["warnings"];
|
|
961
1025
|
fatalError: EditorState["fatalError"];
|
|
962
1026
|
report: RuntimeRenderSnapshot["compatibility"];
|
|
@@ -1107,6 +1171,14 @@ export function createDocumentRuntime(
|
|
|
1107
1171
|
snapshot: WorkflowMarkupSnapshot;
|
|
1108
1172
|
}
|
|
1109
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;
|
|
1110
1182
|
const cachedContextAnalyticsSnapshots = new Map<
|
|
1111
1183
|
string,
|
|
1112
1184
|
{
|
|
@@ -1152,16 +1224,53 @@ export function createDocumentRuntime(
|
|
|
1152
1224
|
return snapshot;
|
|
1153
1225
|
}
|
|
1154
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
|
+
|
|
1155
1241
|
function getCachedCompatibilityReport(
|
|
1156
1242
|
nextState: EditorState,
|
|
1157
1243
|
): RuntimeRenderSnapshot["compatibility"] {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
1274
|
}
|
|
1166
1275
|
|
|
1167
1276
|
const derived = createDerivedCompatibility(nextState);
|
|
@@ -1178,6 +1287,8 @@ export function createDocumentRuntime(
|
|
|
1178
1287
|
};
|
|
1179
1288
|
cachedCompatibility = {
|
|
1180
1289
|
revisionToken: nextState.revisionToken,
|
|
1290
|
+
blockCount,
|
|
1291
|
+
warningCount,
|
|
1181
1292
|
warnings: nextState.warnings,
|
|
1182
1293
|
fatalError: nextState.fatalError,
|
|
1183
1294
|
report,
|
|
@@ -1424,8 +1535,13 @@ export function createDocumentRuntime(
|
|
|
1424
1535
|
if (normalizedWorkflowOverlay) {
|
|
1425
1536
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
1426
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
|
+
);
|
|
1427
1543
|
|
|
1428
|
-
if (!matchingScope &&
|
|
1544
|
+
if (!matchingScope && guardingScopes.length > 0) {
|
|
1429
1545
|
reasons.push({
|
|
1430
1546
|
code: "outside_workflow_scope",
|
|
1431
1547
|
message: "Selection is outside any active workflow scope.",
|
|
@@ -1456,24 +1572,60 @@ export function createDocumentRuntime(
|
|
|
1456
1572
|
return reasons;
|
|
1457
1573
|
}
|
|
1458
1574
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
};
|
|
1465
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 [];
|
|
1466
1592
|
const selectionBounds = {
|
|
1467
1593
|
from: Math.min(selection.anchor, selection.head),
|
|
1468
1594
|
to: Math.max(selection.anchor, selection.head),
|
|
1469
1595
|
};
|
|
1470
1596
|
const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
|
|
1471
|
-
|
|
1597
|
+
const matching = activeScopes.filter((scope) => {
|
|
1598
|
+
// §C8
|
|
1599
|
+
if (scope.visibility === "invisible" && scope.mode !== "view") return false;
|
|
1472
1600
|
if (scope.anchor.kind === "detached") return false;
|
|
1473
1601
|
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
1474
1602
|
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
1475
1603
|
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
1476
|
-
})
|
|
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
|
+
);
|
|
1477
1629
|
}
|
|
1478
1630
|
|
|
1479
1631
|
function getEffectiveDocumentMode(
|
|
@@ -1892,6 +2044,7 @@ export function createDocumentRuntime(
|
|
|
1892
2044
|
|
|
1893
2045
|
const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
|
|
1894
2046
|
const matchingScope = getMatchingWorkflowScope(state.selection);
|
|
2047
|
+
const scopeStack = buildMatchingScopeStack(state.selection);
|
|
1895
2048
|
const primaryBlockedReason = blockedReasons[0];
|
|
1896
2049
|
const effectiveMode = primaryBlockedReason
|
|
1897
2050
|
? (
|
|
@@ -1904,10 +2057,19 @@ export function createDocumentRuntime(
|
|
|
1904
2057
|
: getEffectiveDocumentMode(state.selection) === "suggesting"
|
|
1905
2058
|
? "suggest"
|
|
1906
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;
|
|
1907
2068
|
const snapshot: InteractionGuardSnapshot = {
|
|
1908
2069
|
effectiveMode,
|
|
1909
2070
|
...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
|
|
1910
2071
|
...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
|
|
2072
|
+
...(matchedScopeStack ? { matchedScopeStack } : {}),
|
|
1911
2073
|
targetAccess:
|
|
1912
2074
|
effectiveMode === "edit"
|
|
1913
2075
|
? "direct-edit"
|
|
@@ -1992,7 +2154,7 @@ export function createDocumentRuntime(
|
|
|
1992
2154
|
|
|
1993
2155
|
const snapshot = collectWorkflowMarkupSnapshot({
|
|
1994
2156
|
renderSnapshot: cachedRenderSnapshot,
|
|
1995
|
-
fieldSnapshot:
|
|
2157
|
+
fieldSnapshot: getCachedFieldSnapshot(state.document),
|
|
1996
2158
|
protectionSnapshot,
|
|
1997
2159
|
preservation: state.document.preservation,
|
|
1998
2160
|
workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
|
|
@@ -2297,6 +2459,7 @@ export function createDocumentRuntime(
|
|
|
2297
2459
|
cachedWorkflowScopeSnapshot = undefined;
|
|
2298
2460
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
2299
2461
|
cachedWorkflowMarkupSnapshot = undefined;
|
|
2462
|
+
cachedFieldSnapshotEntry = null;
|
|
2300
2463
|
cachedContextAnalyticsSnapshots.clear();
|
|
2301
2464
|
lastEmittedContextAnalyticsSnapshots = undefined;
|
|
2302
2465
|
}
|
|
@@ -2313,8 +2476,11 @@ export function createDocumentRuntime(
|
|
|
2313
2476
|
document,
|
|
2314
2477
|
};
|
|
2315
2478
|
if (previousDocument.subParts !== document.subParts) {
|
|
2316
|
-
|
|
2317
|
-
|
|
2479
|
+
const fontInput = collectFontLoaderInput(document);
|
|
2480
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previousDocument).families)) {
|
|
2481
|
+
fontLoader.refresh(fontInput);
|
|
2482
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2483
|
+
}
|
|
2318
2484
|
}
|
|
2319
2485
|
invalidateDerivedRuntimeCaches();
|
|
2320
2486
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
@@ -2473,6 +2639,15 @@ export function createDocumentRuntime(
|
|
|
2473
2639
|
reasons,
|
|
2474
2640
|
});
|
|
2475
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
|
+
},
|
|
2476
2651
|
dispatch(command) {
|
|
2477
2652
|
const commandSelection = getCommandSelection(command, state.selection);
|
|
2478
2653
|
if (isMutationCommand(command)) {
|
|
@@ -3083,7 +3258,7 @@ export function createDocumentRuntime(
|
|
|
3083
3258
|
});
|
|
3084
3259
|
},
|
|
3085
3260
|
addCommentReply(commentId, body, authorId) {
|
|
3086
|
-
const
|
|
3261
|
+
const priorCount =
|
|
3087
3262
|
state.document.review.comments[commentId]?.entries?.length ?? 0;
|
|
3088
3263
|
this.dispatch({
|
|
3089
3264
|
type: "comment.add-reply",
|
|
@@ -3092,8 +3267,16 @@ export function createDocumentRuntime(
|
|
|
3092
3267
|
authorId: authorId ?? defaultAuthorId,
|
|
3093
3268
|
origin: createOrigin("api", clock()),
|
|
3094
3269
|
});
|
|
3095
|
-
|
|
3096
|
-
|
|
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 };
|
|
3097
3280
|
},
|
|
3098
3281
|
editCommentBody(commentId, body) {
|
|
3099
3282
|
this.dispatch({
|
|
@@ -3250,6 +3433,38 @@ export function createDocumentRuntime(
|
|
|
3250
3433
|
}
|
|
3251
3434
|
}
|
|
3252
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
|
+
},
|
|
3253
3468
|
acceptChange(changeId) {
|
|
3254
3469
|
this.dispatch({
|
|
3255
3470
|
type: "change.accept",
|
|
@@ -3382,7 +3597,11 @@ export function createDocumentRuntime(
|
|
|
3382
3597
|
getFootnoteResolver(): FootnoteResolver | undefined {
|
|
3383
3598
|
const collection = state.document.subParts?.footnoteCollection;
|
|
3384
3599
|
if (!collection) return undefined;
|
|
3385
|
-
return createFootnoteResolver(
|
|
3600
|
+
return createFootnoteResolver(
|
|
3601
|
+
collection,
|
|
3602
|
+
collectSectionPropertiesInOrder(state.document),
|
|
3603
|
+
state.document,
|
|
3604
|
+
);
|
|
3386
3605
|
},
|
|
3387
3606
|
layout: layoutFacet,
|
|
3388
3607
|
getCurrentLocation() {
|
|
@@ -3529,6 +3748,9 @@ export function createDocumentRuntime(
|
|
|
3529
3748
|
options,
|
|
3530
3749
|
);
|
|
3531
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;
|
|
3532
3754
|
this.dispatch({
|
|
3533
3755
|
type: "document.replace",
|
|
3534
3756
|
document: refreshed.document,
|
|
@@ -3733,6 +3955,68 @@ export function createDocumentRuntime(
|
|
|
3733
3955
|
filter,
|
|
3734
3956
|
);
|
|
3735
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
|
+
},
|
|
3736
4020
|
findScopesAt(position, options) {
|
|
3737
4021
|
const pos =
|
|
3738
4022
|
position.kind === "range"
|
|
@@ -3925,6 +4209,38 @@ export function createDocumentRuntime(
|
|
|
3925
4209
|
}
|
|
3926
4210
|
|
|
3927
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
|
+
|
|
3928
4244
|
const previous = state;
|
|
3929
4245
|
|
|
3930
4246
|
const tApply0 = performance.now();
|
|
@@ -3948,8 +4264,11 @@ export function createDocumentRuntime(
|
|
|
3948
4264
|
}
|
|
3949
4265
|
}
|
|
3950
4266
|
if (previous.document.subParts !== state.document.subParts) {
|
|
3951
|
-
|
|
3952
|
-
|
|
4267
|
+
const fontInput = collectFontLoaderInput(state.document);
|
|
4268
|
+
if (!fontFamiliesEqual(fontInput.families, collectFontLoaderInput(previous.document).families)) {
|
|
4269
|
+
fontLoader.refresh(fontInput);
|
|
4270
|
+
layoutEngine.invalidateMeasurementCache();
|
|
4271
|
+
}
|
|
3953
4272
|
}
|
|
3954
4273
|
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
3955
4274
|
|
|
@@ -3979,10 +4298,16 @@ export function createDocumentRuntime(
|
|
|
3979
4298
|
// which is optional in the public API for shape-only reasons — the helper
|
|
3980
4299
|
// itself always returns a defined snapshot.
|
|
3981
4300
|
const surfaceForValidation = getCachedSurface(state.document, activeStory)!;
|
|
4301
|
+
const validationOptions = state.selection.activeRange.kind === "node"
|
|
4302
|
+
? {
|
|
4303
|
+
isValidNodeTarget: createSurfaceNodeSelectionProbe(surfaceForValidation),
|
|
4304
|
+
}
|
|
4305
|
+
: undefined;
|
|
3982
4306
|
const validatedSelection = validateSelectionAgainstDocument(
|
|
3983
4307
|
state.document,
|
|
3984
4308
|
state.selection,
|
|
3985
4309
|
surfaceForValidation.storySize,
|
|
4310
|
+
validationOptions,
|
|
3986
4311
|
);
|
|
3987
4312
|
if (validatedSelection !== state.selection) {
|
|
3988
4313
|
state = { ...state, selection: validatedSelection };
|
|
@@ -4184,6 +4509,23 @@ export function createDocumentRuntime(
|
|
|
4184
4509
|
options.onWarning?.(publicWarning);
|
|
4185
4510
|
}
|
|
4186
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
|
+
|
|
4187
4529
|
for (const cleared of transaction.effects.warningsCleared) {
|
|
4188
4530
|
emit({
|
|
4189
4531
|
type: "warning_cleared",
|
|
@@ -4433,9 +4775,16 @@ export function createDocumentRuntime(
|
|
|
4433
4775
|
base: EditorTransaction["effects"],
|
|
4434
4776
|
local: EditorTransaction["effects"],
|
|
4435
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];
|
|
4436
4784
|
return {
|
|
4437
4785
|
warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
|
|
4438
4786
|
warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
|
|
4787
|
+
...(mergedTransient ? { transientWarnings: mergedTransient } : {}),
|
|
4439
4788
|
commentAdded: base.commentAdded ?? local.commentAdded,
|
|
4440
4789
|
commentResolved: base.commentResolved ?? local.commentResolved,
|
|
4441
4790
|
commentReopened: base.commentReopened ?? local.commentReopened,
|
|
@@ -4979,7 +5328,13 @@ function toRuntimeError(error: unknown): InternalEditorError {
|
|
|
4979
5328
|
function toStructuredRuntimeException<T extends InternalEditorError>(
|
|
4980
5329
|
error: T,
|
|
4981
5330
|
): Error & T {
|
|
4982
|
-
|
|
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;
|
|
4983
5338
|
}
|
|
4984
5339
|
|
|
4985
5340
|
function toPublicDocumentStats(state: Pick<EditorState, "document">) {
|
|
@@ -5077,11 +5432,14 @@ function extractSelectionFragment(
|
|
|
5077
5432
|
}
|
|
5078
5433
|
|
|
5079
5434
|
/**
|
|
5080
|
-
* Collect the stable ids of comment threads whose
|
|
5081
|
-
*
|
|
5082
|
-
*
|
|
5083
|
-
* the thread
|
|
5084
|
-
*
|
|
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.
|
|
5085
5443
|
*/
|
|
5086
5444
|
function diffCommentMapKeys(
|
|
5087
5445
|
previous: CanonicalDocumentEnvelope["review"]["comments"],
|
|
@@ -5089,14 +5447,65 @@ function diffCommentMapKeys(
|
|
|
5089
5447
|
): string[] {
|
|
5090
5448
|
const changed = new Set<string>();
|
|
5091
5449
|
for (const id of Object.keys(previous)) {
|
|
5092
|
-
if (
|
|
5450
|
+
if (!next[id]) changed.add(id);
|
|
5093
5451
|
}
|
|
5094
5452
|
for (const id of Object.keys(next)) {
|
|
5095
|
-
|
|
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);
|
|
5096
5460
|
}
|
|
5097
5461
|
return Array.from(changed);
|
|
5098
5462
|
}
|
|
5099
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
|
+
|
|
5100
5509
|
function toPublicCompatibilityReport(
|
|
5101
5510
|
report: InternalCompatibilityReport,
|
|
5102
5511
|
): CompatibilityReport {
|
|
@@ -5510,6 +5919,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
5510
5919
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5511
5920
|
}
|
|
5512
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
|
+
|
|
5513
5938
|
/** Commands that are safe in viewing mode (no document mutation). */
|
|
5514
5939
|
const NON_MUTATION_COMMANDS = new Set([
|
|
5515
5940
|
"selection.set",
|
|
@@ -5526,6 +5951,9 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
5526
5951
|
"workflow.clear-metadata-entries",
|
|
5527
5952
|
"host-annotation.set-overlay",
|
|
5528
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",
|
|
5529
5957
|
]);
|
|
5530
5958
|
|
|
5531
5959
|
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
@@ -6626,6 +7054,17 @@ const fontLoaderInputCache = new WeakMap<
|
|
|
6626
7054
|
WeakMap<object, { families: readonly string[] }>
|
|
6627
7055
|
>();
|
|
6628
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
|
+
|
|
6629
7068
|
function collectFontLoaderInput(
|
|
6630
7069
|
document: CanonicalDocumentEnvelope,
|
|
6631
7070
|
): { families: readonly string[] } {
|
|
@@ -6678,6 +7117,9 @@ function collectFontLoaderInputUncached(
|
|
|
6678
7117
|
/** Test-only export of the uncached walk so memoization tests can spy on it. */
|
|
6679
7118
|
export const __collectFontLoaderInputUncached = collectFontLoaderInputUncached;
|
|
6680
7119
|
|
|
7120
|
+
/** Test-only export of the font-family set equality helper. */
|
|
7121
|
+
export const __fontFamiliesEqual = fontFamiliesEqual;
|
|
7122
|
+
|
|
6681
7123
|
/**
|
|
6682
7124
|
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
6683
7125
|
* the platform supports it and fonts have resolved. Errors are swallowed
|