@beyondwork/docx-react-component 1.0.59 → 1.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +33 -44
- package/src/api/public-types.ts +43 -0
- package/src/core/state/editor-state.ts +2 -0
- package/src/io/docx-session.ts +167 -8
- package/src/io/export/serialize-footnotes.ts +36 -5
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +25 -18
- package/src/io/export/serialize-paragraph-formatting.ts +6 -0
- package/src/io/export/serialize-settings.ts +130 -3
- package/src/io/normalize/normalize-text.ts +8 -4
- package/src/io/ooxml/parse-footnotes.ts +11 -0
- package/src/io/ooxml/parse-headers-footers.ts +117 -42
- package/src/io/ooxml/parse-main-document.ts +20 -8
- package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
- package/src/io/ooxml/parse-settings.ts +91 -1
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/model/canonical-document.ts +36 -2
- package/src/model/snapshot.ts +2 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +2 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +9 -0
- package/src/runtime/document-runtime.ts +770 -21
- package/src/runtime/footnote-resolver.ts +32 -8
- package/src/runtime/layout/layout-engine-version.ts +7 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
- package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
- package/src/runtime/layout/paginated-layout-engine.ts +41 -8
- package/src/runtime/layout/resolved-formatting-document.ts +11 -9
- package/src/runtime/layout/resolved-formatting-state.ts +4 -0
- package/src/runtime/numbering-prefix.ts +26 -2
- package/src/runtime/query-scopes.ts +103 -2
- package/src/runtime/surface-projection.ts +75 -14
- package/src/runtime/table-schema.ts +26 -0
- package/src/ui/WordReviewEditor.tsx +25 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
AddCommentResult,
|
|
24
24
|
AddScopeParams,
|
|
25
25
|
AddScopeResult,
|
|
26
|
+
ClearHighlightOptions,
|
|
26
27
|
CommentSidebarSnapshot,
|
|
27
28
|
CommentSidebarThreadSnapshot,
|
|
28
29
|
CompatibilityReport,
|
|
@@ -150,6 +151,7 @@ import {
|
|
|
150
151
|
findScopesIntersecting,
|
|
151
152
|
resolveScope,
|
|
152
153
|
} from "./scope-resolver.ts";
|
|
154
|
+
import { buildDiagnosticFromLegacyWarningCode } from "./diagnostics/build-diagnostic.ts";
|
|
153
155
|
import {
|
|
154
156
|
projectScopeQueryResults,
|
|
155
157
|
queryScopes as runQueryScopes,
|
|
@@ -158,6 +160,7 @@ import {
|
|
|
158
160
|
insertScopeMarkers,
|
|
159
161
|
removeScopeMarkers,
|
|
160
162
|
} from "../core/commands/add-scope.ts";
|
|
163
|
+
import { applyFormattingOperationToDocument } from "../core/commands/formatting-commands.ts";
|
|
161
164
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
162
165
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
163
166
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
@@ -480,6 +483,22 @@ export interface DocumentRuntime {
|
|
|
480
483
|
rejectChange(changeId: string): void;
|
|
481
484
|
acceptAllChanges(): void;
|
|
482
485
|
rejectAllChanges(): void;
|
|
486
|
+
/**
|
|
487
|
+
* Clears the highlight (background color) on a range in the currently
|
|
488
|
+
* active story. The caller's selection is NOT moved.
|
|
489
|
+
*
|
|
490
|
+
* When `options.range` is omitted, the current document selection is used.
|
|
491
|
+
*
|
|
492
|
+
* Tracked-changes / suggesting mode is honored: in suggesting mode the
|
|
493
|
+
* clear is recorded as an `rPrChange` property-change suggestion when the
|
|
494
|
+
* resolved range is a single bounded text segment, and is reported via
|
|
495
|
+
* `command_blocked` when the range spans multiple segments.
|
|
496
|
+
*
|
|
497
|
+
* When `options.expandToFullHighlight` is `true`, the resolved range is
|
|
498
|
+
* grown outward before clearing to cover the entire contiguous highlighted
|
|
499
|
+
* span it touches. Expansion stops at paragraph and table-cell boundaries.
|
|
500
|
+
*/
|
|
501
|
+
clearHighlight(options?: ClearHighlightOptions): void;
|
|
483
502
|
openStory(target: EditorStoryTarget): boolean;
|
|
484
503
|
closeStory(): void;
|
|
485
504
|
getActiveStory(): EditorStoryTarget;
|
|
@@ -872,6 +891,7 @@ export function createDocumentRuntime(
|
|
|
872
891
|
options.initialSessionState?.workflowMetadata?.entries
|
|
873
892
|
?? options.initialSnapshot?.workflowMetadata?.entries
|
|
874
893
|
?? [];
|
|
894
|
+
let markerBackedScopeIds = new Set<string>();
|
|
875
895
|
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
876
896
|
// §C7 — local view-state for scope chrome visibility; never collab-replicated.
|
|
877
897
|
let scopeChromeVisibilityState: ScopeChromeVisibilityState = { mode: "all" };
|
|
@@ -909,6 +929,32 @@ export function createDocumentRuntime(
|
|
|
909
929
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
910
930
|
lastHeadingFingerprint = computeHeadingFingerprint(state.document);
|
|
911
931
|
|
|
932
|
+
function syncMarkerBackedScopeIds(
|
|
933
|
+
document: CanonicalDocumentEnvelope,
|
|
934
|
+
overlay: WorkflowOverlay | null,
|
|
935
|
+
): void {
|
|
936
|
+
const presentScopeIds = new Set(collectScopeLocations(document).keys());
|
|
937
|
+
if (!overlay) {
|
|
938
|
+
markerBackedScopeIds = presentScopeIds;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const overlayScopeIds = new Set(overlay.scopes.map((scope) => scope.scopeId));
|
|
942
|
+
const next = new Set<string>();
|
|
943
|
+
for (const scopeId of markerBackedScopeIds) {
|
|
944
|
+
if (overlayScopeIds.has(scopeId)) {
|
|
945
|
+
next.add(scopeId);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const scopeId of presentScopeIds) {
|
|
949
|
+
if (overlayScopeIds.has(scopeId)) {
|
|
950
|
+
next.add(scopeId);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
markerBackedScopeIds = next;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
957
|
+
|
|
912
958
|
// Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
|
|
913
959
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
914
960
|
// (content, styles, subParts). It is the single internal source of truth
|
|
@@ -1924,19 +1970,37 @@ export function createDocumentRuntime(
|
|
|
1924
1970
|
return scope;
|
|
1925
1971
|
}
|
|
1926
1972
|
const location = locations.get(scope.scopeId);
|
|
1973
|
+
const isMarkerBacked = markerBackedScopeIds.has(scope.scopeId);
|
|
1974
|
+
let nextAnchor: EditorAnchorProjection | null = null;
|
|
1927
1975
|
if (
|
|
1928
|
-
|
|
1929
|
-
location.startPos
|
|
1930
|
-
location.endPos
|
|
1976
|
+
location &&
|
|
1977
|
+
location.startPos !== undefined &&
|
|
1978
|
+
location.endPos !== undefined
|
|
1931
1979
|
) {
|
|
1980
|
+
nextAnchor = {
|
|
1981
|
+
kind: "range",
|
|
1982
|
+
from: Math.min(location.startPos, location.endPos),
|
|
1983
|
+
to: Math.max(location.startPos, location.endPos),
|
|
1984
|
+
assoc: { start: -1, end: 1 },
|
|
1985
|
+
};
|
|
1986
|
+
} else if (isMarkerBacked) {
|
|
1987
|
+
const lastKnownRange =
|
|
1988
|
+
scope.anchor.kind === "range"
|
|
1989
|
+
? { from: scope.anchor.from, to: scope.anchor.to }
|
|
1990
|
+
: scope.anchor.kind === "node"
|
|
1991
|
+
? { from: scope.anchor.at, to: scope.anchor.at }
|
|
1992
|
+
: scope.anchor.lastKnownRange;
|
|
1993
|
+
nextAnchor = {
|
|
1994
|
+
kind: "detached",
|
|
1995
|
+
reason:
|
|
1996
|
+
location && (location.startPos !== undefined || location.endPos !== undefined)
|
|
1997
|
+
? "deleted"
|
|
1998
|
+
: "invalidatedByStructureChange",
|
|
1999
|
+
lastKnownRange,
|
|
2000
|
+
};
|
|
2001
|
+
} else {
|
|
1932
2002
|
return scope;
|
|
1933
2003
|
}
|
|
1934
|
-
const nextAnchor: EditorAnchorProjection = {
|
|
1935
|
-
kind: "range",
|
|
1936
|
-
from: Math.min(location.startPos, location.endPos),
|
|
1937
|
-
to: Math.max(location.startPos, location.endPos),
|
|
1938
|
-
assoc: { start: -1, end: 1 },
|
|
1939
|
-
};
|
|
1940
2004
|
if (workflowAnchorsEqual(scope.anchor, nextAnchor)) {
|
|
1941
2005
|
return scope;
|
|
1942
2006
|
}
|
|
@@ -1966,6 +2030,161 @@ export function createDocumentRuntime(
|
|
|
1966
2030
|
return normalizeWorkflowOverlayForDocument(state.document, workflowOverlay);
|
|
1967
2031
|
}
|
|
1968
2032
|
|
|
2033
|
+
function buildWarningSignature(warning: InternalEditorWarning): string {
|
|
2034
|
+
return JSON.stringify({
|
|
2035
|
+
code: warning.code,
|
|
2036
|
+
severity: warning.severity,
|
|
2037
|
+
message: warning.message,
|
|
2038
|
+
source: warning.source,
|
|
2039
|
+
featureEntryId: warning.featureEntryId ?? null,
|
|
2040
|
+
details: warning.details ?? null,
|
|
2041
|
+
affectedAnchor: warning.affectedAnchor ?? null,
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function mergeDetachedWorkflowScopeWarnings(
|
|
2046
|
+
overlay: WorkflowOverlay | null,
|
|
2047
|
+
existingWarnings: readonly InternalEditorWarning[],
|
|
2048
|
+
): {
|
|
2049
|
+
nextWarnings: InternalEditorWarning[];
|
|
2050
|
+
added: InternalEditorWarning[];
|
|
2051
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
2052
|
+
} {
|
|
2053
|
+
const detachedScopesById = new Map<string, WorkflowScope>();
|
|
2054
|
+
for (const scope of overlay?.scopes ?? []) {
|
|
2055
|
+
if (scope.anchor.kind === "detached") {
|
|
2056
|
+
detachedScopesById.set(scope.scopeId, scope);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const retainedWarnings = existingWarnings.filter(
|
|
2061
|
+
(warning) => warning.code !== "workflow_scope_invalidated",
|
|
2062
|
+
);
|
|
2063
|
+
const existingDetachedWarnings = existingWarnings.filter(
|
|
2064
|
+
(warning) => warning.code === "workflow_scope_invalidated",
|
|
2065
|
+
);
|
|
2066
|
+
const existingById = new Map(
|
|
2067
|
+
existingDetachedWarnings.map((warning) => [warning.warningId, warning] as const),
|
|
2068
|
+
);
|
|
2069
|
+
const desiredById = new Map(
|
|
2070
|
+
[...detachedScopesById.values()].map((scope) => {
|
|
2071
|
+
const warning = createInvalidatedWorkflowScopeWarning(scope);
|
|
2072
|
+
return [warning.warningId, warning] as const;
|
|
2073
|
+
}),
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
const added: InternalEditorWarning[] = [];
|
|
2077
|
+
const cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }> = [];
|
|
2078
|
+
|
|
2079
|
+
for (const [warningId, existingWarning] of existingById) {
|
|
2080
|
+
const desiredWarning = desiredById.get(warningId);
|
|
2081
|
+
if (!desiredWarning) {
|
|
2082
|
+
cleared.push({ warningId, code: existingWarning.code });
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (buildWarningSignature(existingWarning) !== buildWarningSignature(desiredWarning)) {
|
|
2086
|
+
cleared.push({ warningId, code: existingWarning.code });
|
|
2087
|
+
added.push(desiredWarning);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
for (const [warningId, desiredWarning] of desiredById) {
|
|
2092
|
+
if (!existingById.has(warningId)) {
|
|
2093
|
+
added.push(desiredWarning);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
return {
|
|
2098
|
+
nextWarnings: [...retainedWarnings, ...desiredById.values()],
|
|
2099
|
+
added,
|
|
2100
|
+
cleared,
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function createInvalidatedWorkflowScopeWarning(
|
|
2105
|
+
scope: WorkflowScope,
|
|
2106
|
+
): InternalEditorWarning {
|
|
2107
|
+
const anchor = scope.anchor.kind === "detached" ? scope.anchor : null;
|
|
2108
|
+
const subject = scope.label
|
|
2109
|
+
? `Workflow scope "${scope.label}" (${scope.scopeId})`
|
|
2110
|
+
: `Workflow scope ${scope.scopeId}`;
|
|
2111
|
+
const reasonPhrase =
|
|
2112
|
+
anchor?.reason === "deleted"
|
|
2113
|
+
? "its anchored text was deleted"
|
|
2114
|
+
: anchor?.reason === "invalidatedByStructureChange"
|
|
2115
|
+
? "document structure changed around it"
|
|
2116
|
+
: "its anchor could not be resolved unambiguously";
|
|
2117
|
+
const modePhrase =
|
|
2118
|
+
scope.mode === "view"
|
|
2119
|
+
? "read-only enforcement"
|
|
2120
|
+
: `${scope.mode} enforcement`;
|
|
2121
|
+
|
|
2122
|
+
return {
|
|
2123
|
+
warningId: `warning:workflow-scope-invalidated:${scope.scopeId}`,
|
|
2124
|
+
code: "workflow_scope_invalidated",
|
|
2125
|
+
severity: "warning",
|
|
2126
|
+
message: `${subject} was invalidated because ${reasonPhrase}. Reapply the scope before relying on ${modePhrase}.`,
|
|
2127
|
+
source: "runtime",
|
|
2128
|
+
affectedAnchor: anchor ? toInternalAnchorProjection(anchor) : undefined,
|
|
2129
|
+
diagnostic: buildDiagnosticFromLegacyWarningCode("workflow_scope_invalidated", {
|
|
2130
|
+
diagnosticId: `warning-diag:workflow-scope-invalidated:${scope.scopeId}`,
|
|
2131
|
+
technical: {
|
|
2132
|
+
message: `${subject} lost its trusted anchor and is now detached.`,
|
|
2133
|
+
source: "runtime",
|
|
2134
|
+
},
|
|
2135
|
+
details: {
|
|
2136
|
+
scopeId: scope.scopeId,
|
|
2137
|
+
label: scope.label,
|
|
2138
|
+
mode: scope.mode,
|
|
2139
|
+
reason: anchor?.reason,
|
|
2140
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2141
|
+
storyTarget: scope.storyTarget,
|
|
2142
|
+
reapplySuggested: true,
|
|
2143
|
+
},
|
|
2144
|
+
affectedAnchor: anchor ? scope.anchor : undefined,
|
|
2145
|
+
llmMetadata: {
|
|
2146
|
+
userSummary: `${subject} is no longer attached to trusted document content. Reapply the scope before relying on it.`,
|
|
2147
|
+
remediation: {
|
|
2148
|
+
kind: "fallback",
|
|
2149
|
+
suggestion:
|
|
2150
|
+
"Locate the intended text using warning.details.scopeId and warning.details.lastKnownRange, then call addScope again for the repaired range.",
|
|
2151
|
+
},
|
|
2152
|
+
recoveryClass: "requires-input",
|
|
2153
|
+
echoedInput: {
|
|
2154
|
+
scopeId: scope.scopeId,
|
|
2155
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
}),
|
|
2159
|
+
details: {
|
|
2160
|
+
scopeId: scope.scopeId,
|
|
2161
|
+
label: scope.label,
|
|
2162
|
+
mode: scope.mode,
|
|
2163
|
+
reason: anchor?.reason,
|
|
2164
|
+
lastKnownRange: anchor?.lastKnownRange,
|
|
2165
|
+
storyTarget: scope.storyTarget,
|
|
2166
|
+
reapplySuggested: true,
|
|
2167
|
+
actionabilityNote:
|
|
2168
|
+
"Resolve the intended text again, then reapply the scope; the previous anchor is no longer trusted.",
|
|
2169
|
+
},
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function syncDetachedWorkflowScopeWarningsInState(): {
|
|
2174
|
+
added: InternalEditorWarning[];
|
|
2175
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
2176
|
+
} {
|
|
2177
|
+
const { nextWarnings, added, cleared } = mergeDetachedWorkflowScopeWarnings(
|
|
2178
|
+
getNormalizedWorkflowOverlay(),
|
|
2179
|
+
state.warnings,
|
|
2180
|
+
);
|
|
2181
|
+
if (added.length === 0 && cleared.length === 0) {
|
|
2182
|
+
return { added, cleared };
|
|
2183
|
+
}
|
|
2184
|
+
state = { ...state, warnings: nextWarnings };
|
|
2185
|
+
return { added, cleared };
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1969
2188
|
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
1970
2189
|
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1971
2190
|
if (!normalizedWorkflowOverlay) return null;
|
|
@@ -2529,6 +2748,7 @@ export function createDocumentRuntime(
|
|
|
2529
2748
|
return snapshot;
|
|
2530
2749
|
}
|
|
2531
2750
|
|
|
2751
|
+
syncDetachedWorkflowScopeWarningsInState();
|
|
2532
2752
|
let cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2533
2753
|
|
|
2534
2754
|
emit({
|
|
@@ -3351,17 +3571,29 @@ export function createDocumentRuntime(
|
|
|
3351
3571
|
});
|
|
3352
3572
|
|
|
3353
3573
|
if (params.persistence && params.persistence !== "runtime-only") {
|
|
3574
|
+
const requestedMetadata = params.metadata ?? {};
|
|
3575
|
+
const entryPersistence =
|
|
3576
|
+
requestedMetadata.metadataPersistence ??
|
|
3577
|
+
(params.persistence === "session" ? "external" : "internal");
|
|
3354
3578
|
const entry: WorkflowMetadataEntry = {
|
|
3355
|
-
entryId: `scope-metadata-${scopeId}`,
|
|
3356
|
-
metadataId: "workflow.scope",
|
|
3579
|
+
entryId: requestedMetadata.entryId ?? `scope-metadata-${scopeId}`,
|
|
3580
|
+
metadataId: requestedMetadata.metadataId ?? "workflow.scope",
|
|
3357
3581
|
anchor: publicAnchor,
|
|
3582
|
+
...(params.storyTarget ? { storyTarget: params.storyTarget } : {}),
|
|
3358
3583
|
scopeId,
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3584
|
+
...(requestedMetadata.workItemId ? { workItemId: requestedMetadata.workItemId } : {}),
|
|
3585
|
+
...(requestedMetadata.value !== undefined
|
|
3586
|
+
? { value: requestedMetadata.value }
|
|
3587
|
+
: params.persistence === "document-metadata" && params.label
|
|
3588
|
+
? { value: { label: params.label } }
|
|
3589
|
+
: {}),
|
|
3590
|
+
metadataPersistence: entryPersistence,
|
|
3591
|
+
...(requestedMetadata.storageRef !== undefined
|
|
3592
|
+
? { storageRef: requestedMetadata.storageRef }
|
|
3593
|
+
: {}),
|
|
3594
|
+
...(requestedMetadata.metadataVersion !== undefined
|
|
3595
|
+
? { metadataVersion: requestedMetadata.metadataVersion }
|
|
3596
|
+
: {}),
|
|
3365
3597
|
};
|
|
3366
3598
|
this.dispatch({
|
|
3367
3599
|
type: "workflow.set-metadata-entries",
|
|
@@ -3491,6 +3723,143 @@ export function createDocumentRuntime(
|
|
|
3491
3723
|
origin: createOrigin("api", clock()),
|
|
3492
3724
|
});
|
|
3493
3725
|
},
|
|
3726
|
+
clearHighlight(options) {
|
|
3727
|
+
const resolvedRange = resolveClearHighlightRange(
|
|
3728
|
+
options?.range,
|
|
3729
|
+
state.selection,
|
|
3730
|
+
);
|
|
3731
|
+
if (!resolvedRange) {
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (viewState.documentMode === "viewing") {
|
|
3735
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3736
|
+
code: "document_viewing_mode",
|
|
3737
|
+
message: "Cannot clear highlight in viewing mode.",
|
|
3738
|
+
}]);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
const surfaceBlocks = getActiveStorySurfaceBlocks();
|
|
3743
|
+
if (!surfaceBlocks) {
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
const inputFrom = Math.min(resolvedRange.from, resolvedRange.to);
|
|
3748
|
+
const inputTo = Math.max(resolvedRange.from, resolvedRange.to);
|
|
3749
|
+
let targetFrom = inputFrom;
|
|
3750
|
+
let targetTo = inputTo;
|
|
3751
|
+
if (options?.expandToFullHighlight === true) {
|
|
3752
|
+
const expanded = expandRangeToHighlightExtent(
|
|
3753
|
+
surfaceBlocks,
|
|
3754
|
+
inputFrom,
|
|
3755
|
+
inputTo,
|
|
3756
|
+
);
|
|
3757
|
+
targetFrom = expanded.from;
|
|
3758
|
+
targetTo = expanded.to;
|
|
3759
|
+
}
|
|
3760
|
+
if (targetFrom === targetTo) {
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
const activeStoryDocument =
|
|
3765
|
+
activeStory.kind === "main"
|
|
3766
|
+
? state.document
|
|
3767
|
+
: {
|
|
3768
|
+
...state.document,
|
|
3769
|
+
content: {
|
|
3770
|
+
type: "doc" as const,
|
|
3771
|
+
children: [...getStoryBlocks(state.document, activeStory)],
|
|
3772
|
+
},
|
|
3773
|
+
};
|
|
3774
|
+
const syntheticSnapshot: RuntimeRenderSnapshot = {
|
|
3775
|
+
...cachedRenderSnapshot,
|
|
3776
|
+
...(activeStory.kind === "main" ? {} : { activeStory: MAIN_STORY_TARGET }),
|
|
3777
|
+
selection: {
|
|
3778
|
+
anchor: targetFrom,
|
|
3779
|
+
head: targetTo,
|
|
3780
|
+
isCollapsed: false,
|
|
3781
|
+
activeRange: {
|
|
3782
|
+
kind: "range",
|
|
3783
|
+
from: targetFrom,
|
|
3784
|
+
to: targetTo,
|
|
3785
|
+
assoc: { start: -1, end: 1 },
|
|
3786
|
+
},
|
|
3787
|
+
},
|
|
3788
|
+
};
|
|
3789
|
+
|
|
3790
|
+
const suggesting =
|
|
3791
|
+
getEffectiveDocumentMode(state.selection) === "suggesting";
|
|
3792
|
+
if (suggesting) {
|
|
3793
|
+
if (activeStory.kind !== "main") {
|
|
3794
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3795
|
+
code: "suggesting_unsupported",
|
|
3796
|
+
message: `"clearHighlight" is not supported in suggesting mode for this story.`,
|
|
3797
|
+
}]);
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const segment = findSingleSelectedTextSegment(syntheticSnapshot);
|
|
3801
|
+
if (!segment) {
|
|
3802
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3803
|
+
code: "suggesting_unsupported",
|
|
3804
|
+
message: `"clearHighlight" requires one bounded text segment in suggesting mode.`,
|
|
3805
|
+
}]);
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
const beforeXml = buildRunPropertyBeforeXml(segment);
|
|
3809
|
+
const mutation = applyFormattingOperationToDocument(
|
|
3810
|
+
activeStoryDocument,
|
|
3811
|
+
syntheticSnapshot,
|
|
3812
|
+
{ type: "set-highlight-color", color: null },
|
|
3813
|
+
);
|
|
3814
|
+
if (!mutation.changed) {
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
const timestamp = clock();
|
|
3818
|
+
const nextDocument = appendPropertyChangeSuggestion(
|
|
3819
|
+
mutation.document,
|
|
3820
|
+
{ from: segment.from, to: segment.to },
|
|
3821
|
+
{
|
|
3822
|
+
originalRevisionType: "rPrChange",
|
|
3823
|
+
xmlTag: "rPrChange",
|
|
3824
|
+
beforeXml,
|
|
3825
|
+
semanticKind: "formatting-change",
|
|
3826
|
+
storyTarget: activeStory,
|
|
3827
|
+
authorId: defaultAuthorId ?? undefined,
|
|
3828
|
+
},
|
|
3829
|
+
timestamp,
|
|
3830
|
+
);
|
|
3831
|
+
this.dispatch({
|
|
3832
|
+
type: "document.replace",
|
|
3833
|
+
document: nextDocument,
|
|
3834
|
+
mapping: createEmptyMapping(),
|
|
3835
|
+
origin: createOrigin("api", timestamp),
|
|
3836
|
+
});
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
const result = applyFormattingOperationToDocument(
|
|
3841
|
+
activeStoryDocument,
|
|
3842
|
+
syntheticSnapshot,
|
|
3843
|
+
{ type: "set-highlight-color", color: null },
|
|
3844
|
+
);
|
|
3845
|
+
if (!result.changed) {
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const nextDocument =
|
|
3849
|
+
activeStory.kind === "main"
|
|
3850
|
+
? result.document
|
|
3851
|
+
: replaceStoryBlocks(
|
|
3852
|
+
state.document,
|
|
3853
|
+
activeStory,
|
|
3854
|
+
result.document.content.children,
|
|
3855
|
+
);
|
|
3856
|
+
this.dispatch({
|
|
3857
|
+
type: "document.replace",
|
|
3858
|
+
document: nextDocument,
|
|
3859
|
+
mapping: createEmptyMapping(),
|
|
3860
|
+
origin: createOrigin("api", clock()),
|
|
3861
|
+
});
|
|
3862
|
+
},
|
|
3494
3863
|
openStory(target) {
|
|
3495
3864
|
const normalizedTarget =
|
|
3496
3865
|
target.kind === "header" || target.kind === "footer"
|
|
@@ -3601,6 +3970,7 @@ export function createDocumentRuntime(
|
|
|
3601
3970
|
collection,
|
|
3602
3971
|
collectSectionPropertiesInOrder(state.document),
|
|
3603
3972
|
state.document,
|
|
3973
|
+
state.document.subParts?.settings,
|
|
3604
3974
|
);
|
|
3605
3975
|
},
|
|
3606
3976
|
layout: layoutFacet,
|
|
@@ -3951,16 +4321,42 @@ export function createDocumentRuntime(
|
|
|
3951
4321
|
overlay: workflowOverlay,
|
|
3952
4322
|
entries: workflowMetadataEntries,
|
|
3953
4323
|
document: state.document,
|
|
4324
|
+
markerBackedScopeIds,
|
|
3954
4325
|
},
|
|
3955
4326
|
filter,
|
|
3956
4327
|
);
|
|
3957
4328
|
},
|
|
3958
4329
|
subscribeToScopeQuery(filter, callback) {
|
|
4330
|
+
const buildAnchorKey = (anchor: EditorAnchorProjection): string => {
|
|
4331
|
+
switch (anchor.kind) {
|
|
4332
|
+
case "range":
|
|
4333
|
+
return `range:${anchor.from}:${anchor.to}:${anchor.assoc.start}:${anchor.assoc.end}`;
|
|
4334
|
+
case "node":
|
|
4335
|
+
return `node:${anchor.at}`;
|
|
4336
|
+
case "detached":
|
|
4337
|
+
return `detached:${anchor.reason}:${anchor.lastKnownRange.from}:${anchor.lastKnownRange.to}`;
|
|
4338
|
+
default:
|
|
4339
|
+
return "unknown";
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
|
|
3959
4343
|
const buildKey = (results: ScopeQueryResult[]) =>
|
|
3960
4344
|
results
|
|
3961
4345
|
.map(
|
|
3962
4346
|
(r) =>
|
|
3963
|
-
|
|
4347
|
+
[
|
|
4348
|
+
r.scope.scopeId,
|
|
4349
|
+
r.scope.version ?? 0,
|
|
4350
|
+
r.scope.visibility ?? "visible",
|
|
4351
|
+
buildAnchorKey(r.scope.anchor),
|
|
4352
|
+
r.workItem?.workItemId ?? "",
|
|
4353
|
+
r.entries
|
|
4354
|
+
.map(
|
|
4355
|
+
(entry) =>
|
|
4356
|
+
`${entry.entryId}:${entry.metadataVersion ?? 0}:${buildAnchorKey(entry.anchor)}`,
|
|
4357
|
+
)
|
|
4358
|
+
.join("|"),
|
|
4359
|
+
].join(":"),
|
|
3964
4360
|
)
|
|
3965
4361
|
.join(",");
|
|
3966
4362
|
|
|
@@ -4027,7 +4423,12 @@ export function createDocumentRuntime(
|
|
|
4027
4423
|
if (pos === null) return [];
|
|
4028
4424
|
const hits = findAllScopesAt(state.document, pos);
|
|
4029
4425
|
return projectScopeQueryResults(
|
|
4030
|
-
{
|
|
4426
|
+
{
|
|
4427
|
+
overlay: workflowOverlay,
|
|
4428
|
+
entries: workflowMetadataEntries,
|
|
4429
|
+
document: state.document,
|
|
4430
|
+
markerBackedScopeIds,
|
|
4431
|
+
},
|
|
4031
4432
|
hits.map((h) => h.scopeId),
|
|
4032
4433
|
options,
|
|
4033
4434
|
);
|
|
@@ -4036,7 +4437,12 @@ export function createDocumentRuntime(
|
|
|
4036
4437
|
if (range.kind !== "range") return [];
|
|
4037
4438
|
const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
|
|
4038
4439
|
return projectScopeQueryResults(
|
|
4039
|
-
{
|
|
4440
|
+
{
|
|
4441
|
+
overlay: workflowOverlay,
|
|
4442
|
+
entries: workflowMetadataEntries,
|
|
4443
|
+
document: state.document,
|
|
4444
|
+
markerBackedScopeIds,
|
|
4445
|
+
},
|
|
4040
4446
|
hits.map((h) => h.scopeId),
|
|
4041
4447
|
options,
|
|
4042
4448
|
);
|
|
@@ -4249,6 +4655,8 @@ export function createDocumentRuntime(
|
|
|
4249
4655
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
4250
4656
|
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
4251
4657
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
4658
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4659
|
+
const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4252
4660
|
|
|
4253
4661
|
const tInvalidate0 = performance.now();
|
|
4254
4662
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
@@ -4319,7 +4727,20 @@ export function createDocumentRuntime(
|
|
|
4319
4727
|
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
4320
4728
|
|
|
4321
4729
|
const tNotify0 = performance.now();
|
|
4322
|
-
notify(previous, state,
|
|
4730
|
+
notify(previous, state, {
|
|
4731
|
+
...transaction,
|
|
4732
|
+
effects: {
|
|
4733
|
+
...transaction.effects,
|
|
4734
|
+
warningsAdded: [
|
|
4735
|
+
...transaction.effects.warningsAdded,
|
|
4736
|
+
...detachedWorkflowScopeWarnings.added,
|
|
4737
|
+
],
|
|
4738
|
+
warningsCleared: [
|
|
4739
|
+
...transaction.effects.warningsCleared,
|
|
4740
|
+
...detachedWorkflowScopeWarnings.cleared,
|
|
4741
|
+
],
|
|
4742
|
+
},
|
|
4743
|
+
});
|
|
4323
4744
|
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
4324
4745
|
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
4325
4746
|
}
|
|
@@ -4948,10 +5369,18 @@ export function createDocumentRuntime(
|
|
|
4948
5369
|
function applyRuntimeStateOverlayCommand(
|
|
4949
5370
|
command: RuntimeStateOverlayCommand,
|
|
4950
5371
|
): void {
|
|
5372
|
+
let detachedWorkflowScopeWarnings:
|
|
5373
|
+
| {
|
|
5374
|
+
added: InternalEditorWarning[];
|
|
5375
|
+
cleared: Array<{ warningId: string; code: InternalEditorWarning["code"] }>;
|
|
5376
|
+
}
|
|
5377
|
+
| null = null;
|
|
4951
5378
|
switch (command.type) {
|
|
4952
5379
|
case "workflow.set-overlay": {
|
|
4953
5380
|
workflowOverlay = structuredClone(command.overlay);
|
|
5381
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4954
5382
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5383
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4955
5384
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4956
5385
|
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
4957
5386
|
emit({
|
|
@@ -4970,7 +5399,9 @@ export function createDocumentRuntime(
|
|
|
4970
5399
|
}
|
|
4971
5400
|
case "workflow.clear-overlay": {
|
|
4972
5401
|
workflowOverlay = null;
|
|
5402
|
+
syncMarkerBackedScopeIds(state.document, workflowOverlay);
|
|
4973
5403
|
cachedNormalizedWorkflowOverlay = undefined;
|
|
5404
|
+
detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
4974
5405
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4975
5406
|
emit({
|
|
4976
5407
|
type: "workflow_active_work_item_changed",
|
|
@@ -5045,6 +5476,25 @@ export function createDocumentRuntime(
|
|
|
5045
5476
|
break;
|
|
5046
5477
|
}
|
|
5047
5478
|
}
|
|
5479
|
+
if (detachedWorkflowScopeWarnings) {
|
|
5480
|
+
for (const warning of detachedWorkflowScopeWarnings.added) {
|
|
5481
|
+
const publicWarning = toPublicWarning(warning);
|
|
5482
|
+
emit({
|
|
5483
|
+
type: "warning_added",
|
|
5484
|
+
documentId: state.documentId,
|
|
5485
|
+
warning: publicWarning,
|
|
5486
|
+
});
|
|
5487
|
+
options.onWarning?.(publicWarning);
|
|
5488
|
+
}
|
|
5489
|
+
for (const cleared of detachedWorkflowScopeWarnings.cleared) {
|
|
5490
|
+
emit({
|
|
5491
|
+
type: "warning_cleared",
|
|
5492
|
+
documentId: state.documentId,
|
|
5493
|
+
warningId: cleared.warningId,
|
|
5494
|
+
code: cleared.code,
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5048
5498
|
for (const listener of listeners) {
|
|
5049
5499
|
listener();
|
|
5050
5500
|
}
|
|
@@ -5533,11 +5983,42 @@ function toPublicCompatibilityFeatureEntry(
|
|
|
5533
5983
|
}
|
|
5534
5984
|
|
|
5535
5985
|
function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
|
|
5986
|
+
const diagnostic =
|
|
5987
|
+
warning.diagnostic ??
|
|
5988
|
+
(() => {
|
|
5989
|
+
switch (warning.code) {
|
|
5990
|
+
case "unsupported_ooxml_preserved":
|
|
5991
|
+
case "unsupported_ooxml_locked":
|
|
5992
|
+
case "import_normalized":
|
|
5993
|
+
case "export_roundtrip_risk":
|
|
5994
|
+
case "comment_anchor_detached":
|
|
5995
|
+
case "revision_anchor_detached":
|
|
5996
|
+
case "workflow_scope_invalidated":
|
|
5997
|
+
case "large_document_degraded":
|
|
5998
|
+
case "font_substitution":
|
|
5999
|
+
case "image_missing":
|
|
6000
|
+
return buildDiagnosticFromLegacyWarningCode(warning.code, {
|
|
6001
|
+
diagnosticId: `warning-diag:${warning.warningId}`,
|
|
6002
|
+
emittedAt: new Date(0).toISOString(),
|
|
6003
|
+
technical: {
|
|
6004
|
+
message: warning.message,
|
|
6005
|
+
source: warning.source,
|
|
6006
|
+
},
|
|
6007
|
+
details: warning.details,
|
|
6008
|
+
affectedAnchor: warning.affectedAnchor
|
|
6009
|
+
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
6010
|
+
: undefined,
|
|
6011
|
+
});
|
|
6012
|
+
default:
|
|
6013
|
+
return undefined;
|
|
6014
|
+
}
|
|
6015
|
+
})();
|
|
5536
6016
|
return {
|
|
5537
6017
|
...warning,
|
|
5538
6018
|
affectedAnchor: warning.affectedAnchor
|
|
5539
6019
|
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
5540
6020
|
: undefined,
|
|
6021
|
+
diagnostic,
|
|
5541
6022
|
};
|
|
5542
6023
|
}
|
|
5543
6024
|
|
|
@@ -7143,3 +7624,271 @@ async function upgradeMeasurementProvider(
|
|
|
7143
7624
|
// fall through — the empirical backend remains in place
|
|
7144
7625
|
}
|
|
7145
7626
|
}
|
|
7627
|
+
|
|
7628
|
+
function rangesOverlap(
|
|
7629
|
+
leftFrom: number,
|
|
7630
|
+
leftTo: number,
|
|
7631
|
+
rightFrom: number,
|
|
7632
|
+
rightTo: number,
|
|
7633
|
+
): boolean {
|
|
7634
|
+
return leftFrom < rightTo && rightFrom < leftTo;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
function resolveClearHighlightRange(
|
|
7638
|
+
inputRange: EditorAnchorProjection | undefined,
|
|
7639
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
7640
|
+
): { from: number; to: number } | null {
|
|
7641
|
+
if (inputRange !== undefined) {
|
|
7642
|
+
if (inputRange.kind !== "range") return null;
|
|
7643
|
+
return { from: inputRange.from, to: inputRange.to };
|
|
7644
|
+
}
|
|
7645
|
+
const active = selection.activeRange;
|
|
7646
|
+
if (active.kind !== "range") return null;
|
|
7647
|
+
return { from: active.range.from, to: active.range.to };
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
function expandRangeToHighlightExtent(
|
|
7651
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7652
|
+
inputFrom: number,
|
|
7653
|
+
inputTo: number,
|
|
7654
|
+
): { from: number; to: number } {
|
|
7655
|
+
let from = inputFrom;
|
|
7656
|
+
let to = inputTo;
|
|
7657
|
+
forEachParagraphBlock(blocks, (paragraph) => {
|
|
7658
|
+
if (!rangesOverlap(from, to, paragraph.from, paragraph.to)) {
|
|
7659
|
+
return;
|
|
7660
|
+
}
|
|
7661
|
+
const textSegments = paragraph.segments.filter(
|
|
7662
|
+
(segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
|
|
7663
|
+
segment.kind === "text",
|
|
7664
|
+
);
|
|
7665
|
+
const expanded = expandRangeWithinParagraph(textSegments, inputFrom, inputTo);
|
|
7666
|
+
if (expanded.from < from) from = expanded.from;
|
|
7667
|
+
if (expanded.to > to) to = expanded.to;
|
|
7668
|
+
});
|
|
7669
|
+
return { from, to };
|
|
7670
|
+
}
|
|
7671
|
+
|
|
7672
|
+
function expandRangeWithinParagraph(
|
|
7673
|
+
segments: Array<Extract<SurfaceInlineSegment, { kind: "text" }>>,
|
|
7674
|
+
inputFrom: number,
|
|
7675
|
+
inputTo: number,
|
|
7676
|
+
): { from: number; to: number } {
|
|
7677
|
+
let from = inputFrom;
|
|
7678
|
+
let to = inputTo;
|
|
7679
|
+
let touchedLeftIndex = -1;
|
|
7680
|
+
let touchedRightIndex = -1;
|
|
7681
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
7682
|
+
const segment = segments[i]!;
|
|
7683
|
+
if (!isHighlightedSegment(segment)) continue;
|
|
7684
|
+
if (rangesOverlap(inputFrom, inputTo, segment.from, segment.to)) {
|
|
7685
|
+
if (touchedLeftIndex === -1) touchedLeftIndex = i;
|
|
7686
|
+
touchedRightIndex = i;
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
if (touchedLeftIndex === -1) {
|
|
7690
|
+
return { from, to };
|
|
7691
|
+
}
|
|
7692
|
+
for (let i = touchedLeftIndex; i >= 0; i -= 1) {
|
|
7693
|
+
const segment = segments[i]!;
|
|
7694
|
+
if (!isHighlightedSegment(segment)) break;
|
|
7695
|
+
if (segment.to < from) break;
|
|
7696
|
+
if (segment.from < from) from = segment.from;
|
|
7697
|
+
}
|
|
7698
|
+
for (let i = touchedRightIndex; i < segments.length; i += 1) {
|
|
7699
|
+
const segment = segments[i]!;
|
|
7700
|
+
if (!isHighlightedSegment(segment)) break;
|
|
7701
|
+
if (segment.from > to) break;
|
|
7702
|
+
if (segment.to > to) to = segment.to;
|
|
7703
|
+
}
|
|
7704
|
+
return { from, to };
|
|
7705
|
+
}
|
|
7706
|
+
|
|
7707
|
+
function isHighlightedSegment(
|
|
7708
|
+
segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
|
|
7709
|
+
): boolean {
|
|
7710
|
+
const bg = segment.markAttrs?.backgroundColor;
|
|
7711
|
+
return typeof bg === "string" && bg.length > 0;
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
function forEachParagraphBlock(
|
|
7715
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7716
|
+
visit: (paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
|
|
7717
|
+
): void {
|
|
7718
|
+
for (const block of blocks) {
|
|
7719
|
+
if (block.kind === "paragraph") {
|
|
7720
|
+
visit(block);
|
|
7721
|
+
continue;
|
|
7722
|
+
}
|
|
7723
|
+
if (block.kind === "table") {
|
|
7724
|
+
for (const row of block.rows) {
|
|
7725
|
+
for (const cell of row.cells) {
|
|
7726
|
+
forEachParagraphBlock(cell.content, visit);
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
continue;
|
|
7730
|
+
}
|
|
7731
|
+
if (block.kind === "sdt_block") {
|
|
7732
|
+
forEachParagraphBlock(block.children, visit);
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7737
|
+
function findSingleSelectedTextSegment(
|
|
7738
|
+
snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
|
|
7739
|
+
): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
|
|
7740
|
+
if (
|
|
7741
|
+
!snapshot.surface ||
|
|
7742
|
+
snapshot.selection.activeRange.kind !== "range" ||
|
|
7743
|
+
snapshot.selection.isCollapsed
|
|
7744
|
+
) {
|
|
7745
|
+
return null;
|
|
7746
|
+
}
|
|
7747
|
+
const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
|
|
7748
|
+
const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
|
|
7749
|
+
const segments = collectSelectedTextSegments(
|
|
7750
|
+
snapshot.surface.blocks,
|
|
7751
|
+
selectionFrom,
|
|
7752
|
+
selectionTo,
|
|
7753
|
+
);
|
|
7754
|
+
if (segments.length !== 1) {
|
|
7755
|
+
return null;
|
|
7756
|
+
}
|
|
7757
|
+
const [segment] = segments;
|
|
7758
|
+
if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
|
|
7759
|
+
return null;
|
|
7760
|
+
}
|
|
7761
|
+
return segment;
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
function collectSelectedTextSegments(
|
|
7765
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7766
|
+
selectionFrom: number,
|
|
7767
|
+
selectionTo: number,
|
|
7768
|
+
output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
|
|
7769
|
+
): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
|
|
7770
|
+
for (const block of blocks) {
|
|
7771
|
+
if (block.kind === "paragraph") {
|
|
7772
|
+
for (const segment of block.segments) {
|
|
7773
|
+
if (
|
|
7774
|
+
segment.kind === "text" &&
|
|
7775
|
+
rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
|
|
7776
|
+
) {
|
|
7777
|
+
output.push(segment);
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
continue;
|
|
7781
|
+
}
|
|
7782
|
+
if (block.kind === "table") {
|
|
7783
|
+
for (const row of block.rows) {
|
|
7784
|
+
for (const cell of row.cells) {
|
|
7785
|
+
collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
if (block.kind === "sdt_block") {
|
|
7791
|
+
collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
return output;
|
|
7795
|
+
}
|
|
7796
|
+
|
|
7797
|
+
function buildRunPropertyBeforeXml(
|
|
7798
|
+
segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
|
|
7799
|
+
): string {
|
|
7800
|
+
const parts: string[] = [];
|
|
7801
|
+
const marks = new Set(segment.marks ?? []);
|
|
7802
|
+
if (marks.has("bold")) parts.push("<w:b/>");
|
|
7803
|
+
if (marks.has("italic")) parts.push("<w:i/>");
|
|
7804
|
+
if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
|
|
7805
|
+
if (marks.has("strikethrough")) parts.push("<w:strike/>");
|
|
7806
|
+
if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
|
|
7807
|
+
if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
|
|
7808
|
+
if (segment.markAttrs?.fontFamily) {
|
|
7809
|
+
parts.push(
|
|
7810
|
+
`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`,
|
|
7811
|
+
);
|
|
7812
|
+
}
|
|
7813
|
+
if (segment.markAttrs?.fontSize !== undefined) {
|
|
7814
|
+
parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
|
|
7815
|
+
}
|
|
7816
|
+
if (segment.markAttrs?.textColor) {
|
|
7817
|
+
parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
|
|
7818
|
+
}
|
|
7819
|
+
if (segment.markAttrs?.backgroundColor) {
|
|
7820
|
+
parts.push(
|
|
7821
|
+
`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`,
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7824
|
+
return `<w:rPr>${parts.join("")}</w:rPr>`;
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
function escapeAttributeXml(value: string): string {
|
|
7828
|
+
return value
|
|
7829
|
+
.replace(/&/g, "&")
|
|
7830
|
+
.replace(/</g, "<")
|
|
7831
|
+
.replace(/>/g, ">")
|
|
7832
|
+
.replace(/"/g, """);
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
function appendPropertyChangeSuggestion(
|
|
7836
|
+
document: CanonicalDocumentEnvelope,
|
|
7837
|
+
anchor: { from: number; to: number },
|
|
7838
|
+
input: {
|
|
7839
|
+
originalRevisionType: "rPrChange" | "pPrChange";
|
|
7840
|
+
xmlTag: "rPrChange" | "pPrChange";
|
|
7841
|
+
beforeXml: string;
|
|
7842
|
+
semanticKind: "formatting-change" | "paragraph-property-change";
|
|
7843
|
+
storyTarget: EditorStoryTarget;
|
|
7844
|
+
authorId?: string;
|
|
7845
|
+
},
|
|
7846
|
+
timestamp: string,
|
|
7847
|
+
): CanonicalDocumentEnvelope {
|
|
7848
|
+
const existing = document.review.revisions;
|
|
7849
|
+
const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
|
|
7850
|
+
const resolvedAuthorId = input.authorId ?? "unknown";
|
|
7851
|
+
return {
|
|
7852
|
+
...document,
|
|
7853
|
+
review: {
|
|
7854
|
+
...document.review,
|
|
7855
|
+
revisions: {
|
|
7856
|
+
...existing,
|
|
7857
|
+
[changeId]: {
|
|
7858
|
+
changeId,
|
|
7859
|
+
kind: "property-change",
|
|
7860
|
+
anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
|
|
7861
|
+
authorId: resolvedAuthorId,
|
|
7862
|
+
createdAt: timestamp,
|
|
7863
|
+
warningIds: [],
|
|
7864
|
+
metadata: {
|
|
7865
|
+
source: "runtime",
|
|
7866
|
+
storyTarget: input.storyTarget,
|
|
7867
|
+
suggestionId: changeId,
|
|
7868
|
+
semanticKind: input.semanticKind,
|
|
7869
|
+
originalRevisionType: input.originalRevisionType,
|
|
7870
|
+
propertyChangeData: {
|
|
7871
|
+
xmlTag: input.xmlTag,
|
|
7872
|
+
beforeXml: input.beforeXml,
|
|
7873
|
+
},
|
|
7874
|
+
},
|
|
7875
|
+
status: "open",
|
|
7876
|
+
},
|
|
7877
|
+
},
|
|
7878
|
+
},
|
|
7879
|
+
};
|
|
7880
|
+
}
|
|
7881
|
+
|
|
7882
|
+
function createRuntimeSuggestionChangeId(
|
|
7883
|
+
existing: CanonicalDocumentEnvelope["review"]["revisions"],
|
|
7884
|
+
timestamp: string,
|
|
7885
|
+
): string {
|
|
7886
|
+
const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
|
|
7887
|
+
let counter = Object.keys(existing).length + 1;
|
|
7888
|
+
let candidate = `${base}-p${counter}`;
|
|
7889
|
+
while (existing[candidate]) {
|
|
7890
|
+
counter += 1;
|
|
7891
|
+
candidate = `${base}-p${counter}`;
|
|
7892
|
+
}
|
|
7893
|
+
return candidate;
|
|
7894
|
+
}
|