@beyondwork/docx-react-component 1.0.58 → 1.0.60
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 +980 -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 +4 -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/workflow-payload.ts +6 -1
- 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 +5 -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 +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- 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 +108 -10
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EditorAnchorProjection,
|
|
2
3
|
EditorStoryTarget,
|
|
3
4
|
ScopeQueryFilter,
|
|
4
5
|
ScopeQueryResult,
|
|
@@ -31,10 +32,97 @@ function storyTargetsEqual(a: EditorStoryTarget, b: EditorStoryTarget): boolean
|
|
|
31
32
|
|
|
32
33
|
const MAIN_STORY: EditorStoryTarget = { kind: "main" };
|
|
33
34
|
|
|
35
|
+
function workflowAnchorsEqual(
|
|
36
|
+
left: EditorAnchorProjection,
|
|
37
|
+
right: EditorAnchorProjection,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (left.kind !== right.kind) return false;
|
|
40
|
+
switch (left.kind) {
|
|
41
|
+
case "range":
|
|
42
|
+
return (
|
|
43
|
+
right.kind === "range" &&
|
|
44
|
+
left.from === right.from &&
|
|
45
|
+
left.to === right.to &&
|
|
46
|
+
left.assoc.start === right.assoc.start &&
|
|
47
|
+
left.assoc.end === right.assoc.end
|
|
48
|
+
);
|
|
49
|
+
case "node":
|
|
50
|
+
return right.kind === "node" && left.at === right.at;
|
|
51
|
+
case "detached":
|
|
52
|
+
return (
|
|
53
|
+
right.kind === "detached" &&
|
|
54
|
+
left.reason === right.reason &&
|
|
55
|
+
left.lastKnownRange.from === right.lastKnownRange.from &&
|
|
56
|
+
left.lastKnownRange.to === right.lastKnownRange.to
|
|
57
|
+
);
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildScopeIdCounts(overlay: WorkflowOverlay): Map<string, number> {
|
|
64
|
+
const counts = new Map<string, number>();
|
|
65
|
+
for (const scope of overlay.scopes) {
|
|
66
|
+
counts.set(scope.scopeId, (counts.get(scope.scopeId) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
return counts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeScopeAnchor(
|
|
72
|
+
scope: WorkflowScope,
|
|
73
|
+
scopeIdCounts: ReadonlyMap<string, number>,
|
|
74
|
+
locations: ReadonlyMap<string, { startPos?: number; endPos?: number }>,
|
|
75
|
+
markerBackedScopeIds: ReadonlySet<string>,
|
|
76
|
+
): WorkflowScope {
|
|
77
|
+
if ((scopeIdCounts.get(scope.scopeId) ?? 0) !== 1) {
|
|
78
|
+
return scope;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const location = locations.get(scope.scopeId);
|
|
82
|
+
let nextAnchor: EditorAnchorProjection | null = null;
|
|
83
|
+
if (
|
|
84
|
+
location &&
|
|
85
|
+
location.startPos !== undefined &&
|
|
86
|
+
location.endPos !== undefined
|
|
87
|
+
) {
|
|
88
|
+
nextAnchor = {
|
|
89
|
+
kind: "range",
|
|
90
|
+
from: Math.min(location.startPos, location.endPos),
|
|
91
|
+
to: Math.max(location.startPos, location.endPos),
|
|
92
|
+
assoc: { start: -1, end: 1 },
|
|
93
|
+
};
|
|
94
|
+
} else if (markerBackedScopeIds.has(scope.scopeId)) {
|
|
95
|
+
const lastKnownRange =
|
|
96
|
+
scope.anchor.kind === "range"
|
|
97
|
+
? { from: scope.anchor.from, to: scope.anchor.to }
|
|
98
|
+
: scope.anchor.kind === "node"
|
|
99
|
+
? { from: scope.anchor.at, to: scope.anchor.at }
|
|
100
|
+
: scope.anchor.lastKnownRange;
|
|
101
|
+
nextAnchor = {
|
|
102
|
+
kind: "detached",
|
|
103
|
+
reason:
|
|
104
|
+
location && (location.startPos !== undefined || location.endPos !== undefined)
|
|
105
|
+
? "deleted"
|
|
106
|
+
: "invalidatedByStructureChange",
|
|
107
|
+
lastKnownRange,
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return scope;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return workflowAnchorsEqual(scope.anchor, nextAnchor)
|
|
114
|
+
? scope
|
|
115
|
+
: {
|
|
116
|
+
...scope,
|
|
117
|
+
anchor: nextAnchor,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
34
121
|
export interface ScopeQueryInputs {
|
|
35
122
|
readonly overlay: WorkflowOverlay | null;
|
|
36
123
|
readonly entries: readonly WorkflowMetadataEntry[];
|
|
37
124
|
readonly document: Pick<CanonicalDocument, "content"> | CanonicalDocumentEnvelope;
|
|
125
|
+
readonly markerBackedScopeIds?: ReadonlySet<string>;
|
|
38
126
|
}
|
|
39
127
|
|
|
40
128
|
/**
|
|
@@ -53,9 +141,17 @@ export function projectScopeQueryResults(
|
|
|
53
141
|
if (!overlay) return [];
|
|
54
142
|
const includeHidden = options.includeHidden === true;
|
|
55
143
|
const includeInvisible = options.includeInvisible === true;
|
|
144
|
+
const scopeIdCounts = buildScopeIdCounts(overlay);
|
|
145
|
+
const locations = collectScopeLocations(inputs.document);
|
|
146
|
+
const markerBackedScopeIds = inputs.markerBackedScopeIds ?? new Set<string>();
|
|
56
147
|
|
|
57
148
|
const scopesById = new Map<string, WorkflowScope>();
|
|
58
|
-
for (const scope of overlay.scopes)
|
|
149
|
+
for (const scope of overlay.scopes) {
|
|
150
|
+
scopesById.set(
|
|
151
|
+
scope.scopeId,
|
|
152
|
+
normalizeScopeAnchor(scope, scopeIdCounts, locations, markerBackedScopeIds),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
59
155
|
|
|
60
156
|
const entriesByScope = new Map<string, WorkflowMetadataEntry[]>();
|
|
61
157
|
for (const entry of inputs.entries) {
|
|
@@ -77,9 +173,8 @@ export function projectScopeQueryResults(
|
|
|
77
173
|
for (const scopeId of scopeIds) {
|
|
78
174
|
const scope = scopesById.get(scopeId);
|
|
79
175
|
if (!scope) continue;
|
|
80
|
-
|
|
81
|
-
if (visibility === "
|
|
82
|
-
if (visibility === "invisible" && !includeInvisible) continue;
|
|
176
|
+
if (scope.visibility === "hidden" && !includeHidden) continue;
|
|
177
|
+
if (scope.visibility === "invisible" && !includeInvisible) continue;
|
|
83
178
|
const entries = entriesByScope.get(scopeId) ?? [];
|
|
84
179
|
const workItem = scope.workItemId
|
|
85
180
|
? workItemsById.get(scope.workItemId) ?? null
|
|
@@ -129,14 +224,14 @@ export function queryScopes(
|
|
|
129
224
|
}
|
|
130
225
|
|
|
131
226
|
const locations = collectScopeLocations(inputs.document);
|
|
227
|
+
const scopeIdCounts = buildScopeIdCounts(overlay);
|
|
228
|
+
const markerBackedScopeIds = inputs.markerBackedScopeIds ?? new Set<string>();
|
|
132
229
|
|
|
133
230
|
const candidates: Array<{ scope: WorkflowScope; startPos: number }> = [];
|
|
134
231
|
for (const scope of overlay.scopes) {
|
|
135
|
-
// §C8
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (visibility === "hidden" && !includeHidden) continue;
|
|
139
|
-
if (visibility === "invisible" && !includeInvisible) continue;
|
|
232
|
+
// §C8: respect visibility flags. Missing field treated as "visible".
|
|
233
|
+
if (scope.visibility === "hidden" && !includeHidden) continue;
|
|
234
|
+
if (scope.visibility === "invisible" && !includeInvisible) continue;
|
|
140
235
|
|
|
141
236
|
if (normalizedStoryFilter !== "*") {
|
|
142
237
|
const scopeStory = scope.storyTarget ?? MAIN_STORY;
|
|
@@ -165,7 +260,10 @@ export function queryScopes(
|
|
|
165
260
|
const loc = locations.get(scope.scopeId);
|
|
166
261
|
const startPos =
|
|
167
262
|
loc?.startPos ?? loc?.endPos ?? Number.POSITIVE_INFINITY;
|
|
168
|
-
candidates.push({
|
|
263
|
+
candidates.push({
|
|
264
|
+
scope: normalizeScopeAnchor(scope, scopeIdCounts, locations, markerBackedScopeIds),
|
|
265
|
+
startPos,
|
|
266
|
+
});
|
|
169
267
|
}
|
|
170
268
|
|
|
171
269
|
candidates.sort((a, b) => {
|
|
@@ -199,8 +199,24 @@ function resolveNumberingGeometry(
|
|
|
199
199
|
paragraph: Pick<ParagraphNode, "spacing" | "indentation" | "tabStops"> | undefined,
|
|
200
200
|
levelRunProperties: CanonicalRunFormatting | undefined,
|
|
201
201
|
): ResolvedNumberingGeometry {
|
|
202
|
+
// LibreOffice SyncIndentWithList pattern: when a numbered paragraph's w:ind has
|
|
203
|
+
// hanging === left (e.g. left=851 hanging=851), it is a stale Word-written snapshot
|
|
204
|
+
// meaning "no custom indent beyond the list style" — use level geometry directly.
|
|
205
|
+
// This also covers the prior CO3.8 heuristic (para.left === level.left) since Word
|
|
206
|
+
// typically writes hanging=left in that situation too.
|
|
207
|
+
const paraInd = paragraph?.indentation;
|
|
208
|
+
const levelInd = levelGeometry?.indentation;
|
|
209
|
+
const isDegenerateParaInd =
|
|
210
|
+
paraInd !== undefined &&
|
|
211
|
+
paraInd.hanging !== undefined &&
|
|
212
|
+
paraInd.left !== undefined &&
|
|
213
|
+
paraInd.hanging === paraInd.left;
|
|
214
|
+
|
|
215
|
+
const indentation = isDegenerateParaInd
|
|
216
|
+
? levelInd ? { ...levelInd } : undefined
|
|
217
|
+
: mergeParagraphIndentation(levelInd, paraInd);
|
|
218
|
+
|
|
202
219
|
const spacing = mergeParagraphSpacing(levelGeometry?.spacing, paragraph?.spacing);
|
|
203
|
-
const indentation = mergeParagraphIndentation(levelGeometry?.indentation, paragraph?.indentation);
|
|
204
220
|
const tabStops = paragraph?.tabStops?.length
|
|
205
221
|
? cloneTabStops(paragraph.tabStops)
|
|
206
222
|
: levelGeometry?.tabStops?.length
|
|
@@ -244,12 +260,27 @@ function mergeParagraphIndentation(
|
|
|
244
260
|
return undefined;
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
const out: ParagraphIndentation = {};
|
|
264
|
+
|
|
265
|
+
const left = override?.left ?? base?.left;
|
|
266
|
+
if (left !== undefined) out.left = left;
|
|
267
|
+
|
|
268
|
+
const right = override?.right ?? base?.right;
|
|
269
|
+
if (right !== undefined) out.right = right;
|
|
270
|
+
|
|
271
|
+
// ECMA-376 §17.3.1.12: firstLine and hanging are mutually exclusive on a single
|
|
272
|
+
// w:ind element. When override specifies either, it wins exclusively.
|
|
273
|
+
const overrideTouchesFLorH =
|
|
274
|
+
override?.firstLine !== undefined || override?.hanging !== undefined;
|
|
275
|
+
if (overrideTouchesFLorH) {
|
|
276
|
+
if (override?.hanging !== undefined) out.hanging = override.hanging;
|
|
277
|
+
else if (override?.firstLine !== undefined) out.firstLine = override.firstLine;
|
|
278
|
+
} else {
|
|
279
|
+
if (base?.hanging !== undefined) out.hanging = base.hanging;
|
|
280
|
+
else if (base?.firstLine !== undefined) out.firstLine = base.firstLine;
|
|
281
|
+
}
|
|
251
282
|
|
|
252
|
-
return Object.keys(
|
|
283
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
253
284
|
}
|
|
254
285
|
|
|
255
286
|
function cloneTabStops(tabStops: readonly TabStop[]): TabStop[] {
|
|
@@ -37,10 +37,12 @@ export interface RevisionRuntimeCommandResult extends RevisionRuntimeState {
|
|
|
37
37
|
export function applyRevisionRuntimeCommand(
|
|
38
38
|
options: ApplyRevisionRuntimeCommandOptions,
|
|
39
39
|
): RevisionRuntimeCommandResult {
|
|
40
|
-
const
|
|
40
|
+
const rawRevisionIds = isSingleRevisionReviewCommand(options.command)
|
|
41
41
|
? [options.command.revisionId]
|
|
42
42
|
: listBatchRevisionIds(options.state.store);
|
|
43
43
|
|
|
44
|
+
const revisionIds = expandLinkedMovePartners(rawRevisionIds, options.state.store);
|
|
45
|
+
|
|
44
46
|
const outcomes: RevisionActionOutcome[] = [];
|
|
45
47
|
const mappings: Array<{ revisionId: string; mapping: TransactionMapping; steps: number }> = [];
|
|
46
48
|
const appliedRevisionIds: string[] = [];
|
|
@@ -92,6 +94,30 @@ export function applyRevisionRuntimeCommand(
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function expandLinkedMovePartners(
|
|
98
|
+
revisionIds: readonly string[],
|
|
99
|
+
store: RevisionStore,
|
|
100
|
+
): string[] {
|
|
101
|
+
const expanded = new Set<string>(revisionIds);
|
|
102
|
+
for (const revisionId of revisionIds) {
|
|
103
|
+
const revision = store.revisions[revisionId];
|
|
104
|
+
const partner = revision?.metadata.moveData?.linkedRevisionId;
|
|
105
|
+
if (!partner) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const partnerRevision = store.revisions[partner];
|
|
109
|
+
// Only pull the partner in if it's still eligible. Partners that have
|
|
110
|
+
// already been accepted/rejected/detached don't belong in the
|
|
111
|
+
// current-intent batch — adding them would surface a cosmetic
|
|
112
|
+
// "already-resolved" skip in `effects.skippedRevisions` that the user
|
|
113
|
+
// never targeted.
|
|
114
|
+
if (partnerRevision && partnerRevision.status === "active") {
|
|
115
|
+
expanded.add(partner);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...expanded];
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
function listBatchRevisionIds(store: RevisionStore): string[] {
|
|
96
122
|
return Object.values(store.revisions)
|
|
97
123
|
.filter((revision) => revision.status === "active")
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
2
2
|
import type { SelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
3
|
+
import type {
|
|
4
|
+
EditorSurfaceSnapshot,
|
|
5
|
+
SurfaceBlockSnapshot,
|
|
6
|
+
SurfaceInlineSegment,
|
|
7
|
+
} from "../../api/public-types.ts";
|
|
8
|
+
|
|
9
|
+
export interface SelectionValidationOptions {
|
|
10
|
+
isValidNodeTarget?: (at: number) => boolean;
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* Snap a selection to a valid position relative to the document.
|
|
@@ -16,9 +25,9 @@ import type { SelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
|
16
25
|
* POST-mutation `surface.storySize`, primed via
|
|
17
26
|
* `getCachedSurface(state.document, activeStory).storySize`).
|
|
18
27
|
*
|
|
19
|
-
* NodeAnchor invalidation
|
|
20
|
-
*
|
|
21
|
-
* are returned unchanged (identity).
|
|
28
|
+
* NodeAnchor invalidation stays caller-driven: pass a cheap node-target probe
|
|
29
|
+
* when the caller already has the post-mutation surface. Without that probe,
|
|
30
|
+
* NodeAnchor selections are returned unchanged (identity).
|
|
22
31
|
*
|
|
23
32
|
* @param document The post-mutation canonical document. Currently
|
|
24
33
|
* unused except for the deferred NodeAnchor branch;
|
|
@@ -42,11 +51,28 @@ export function validateSelectionAgainstDocument(
|
|
|
42
51
|
document: CanonicalDocumentEnvelope,
|
|
43
52
|
selection: SelectionSnapshot,
|
|
44
53
|
maxOffset: number,
|
|
54
|
+
options?: SelectionValidationOptions,
|
|
45
55
|
): SelectionSnapshot {
|
|
46
56
|
if (selection.activeRange.kind === "node") {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
if (
|
|
58
|
+
options?.isValidNodeTarget &&
|
|
59
|
+
!options.isValidNodeTarget(selection.activeRange.at)
|
|
60
|
+
) {
|
|
61
|
+
const collapsed = clamp(selection.activeRange.at, 0, maxOffset);
|
|
62
|
+
return {
|
|
63
|
+
anchor: collapsed,
|
|
64
|
+
head: collapsed,
|
|
65
|
+
isCollapsed: true,
|
|
66
|
+
activeRange: {
|
|
67
|
+
kind: "range",
|
|
68
|
+
range: { from: collapsed, to: collapsed },
|
|
69
|
+
assoc: {
|
|
70
|
+
start: selection.activeRange.assoc,
|
|
71
|
+
end: selection.activeRange.assoc,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
50
76
|
return selection;
|
|
51
77
|
}
|
|
52
78
|
|
|
@@ -75,3 +101,31 @@ export function validateSelectionAgainstDocument(
|
|
|
75
101
|
function clamp(n: number, lo: number, hi: number): number {
|
|
76
102
|
return n < lo ? lo : n > hi ? hi : n;
|
|
77
103
|
}
|
|
104
|
+
|
|
105
|
+
export function createSurfaceNodeSelectionProbe(
|
|
106
|
+
surface: EditorSurfaceSnapshot,
|
|
107
|
+
): (at: number) => boolean {
|
|
108
|
+
return (at) => surface.blocks.some((block) => blockHasNodeTarget(block, at));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function blockHasNodeTarget(block: SurfaceBlockSnapshot, at: number): boolean {
|
|
112
|
+
switch (block.kind) {
|
|
113
|
+
case "paragraph":
|
|
114
|
+
return block.segments.some((segment) => isNodeSelectableSegment(segment) && segment.from === at);
|
|
115
|
+
case "table":
|
|
116
|
+
return (
|
|
117
|
+
block.from === at ||
|
|
118
|
+
block.rows.some((row) =>
|
|
119
|
+
row.cells.some((cell) => cell.content.some((child) => blockHasNodeTarget(child, at))),
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
case "sdt_block":
|
|
123
|
+
return block.from === at || block.children.some((child) => blockHasNodeTarget(child, at));
|
|
124
|
+
case "opaque_block":
|
|
125
|
+
return block.from === at;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isNodeSelectableSegment(segment: SurfaceInlineSegment): boolean {
|
|
130
|
+
return segment.kind !== "text" && segment.kind !== "tab" && segment.kind !== "hard_break";
|
|
131
|
+
}
|
|
@@ -25,7 +25,11 @@ import type {
|
|
|
25
25
|
import type { StructuralMutationResult } from "../../core/commands/structural-helpers.ts";
|
|
26
26
|
import type { TextCommandContext } from "../../core/commands/text-commands.ts";
|
|
27
27
|
import { countLogicalPositions, parseTextStory } from "../../core/schema/text-schema.ts";
|
|
28
|
-
import {
|
|
28
|
+
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
29
|
+
import {
|
|
30
|
+
createSurfaceNodeSelectionProbe,
|
|
31
|
+
validateSelectionAgainstDocument,
|
|
32
|
+
} from "../selection/post-edit-validator.ts";
|
|
29
33
|
import { applyFragmentInsert } from "./fragment-insert.ts";
|
|
30
34
|
|
|
31
35
|
export interface StructureLayer {
|
|
@@ -58,9 +62,21 @@ function validateResult(result: StructuralMutationResult): StructuralMutationRes
|
|
|
58
62
|
if (!result.changed) {
|
|
59
63
|
return result;
|
|
60
64
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const surface = result.selection.activeRange.kind === "node"
|
|
66
|
+
? createEditorSurfaceSnapshot(result.document, result.selection)
|
|
67
|
+
: null;
|
|
68
|
+
const options = surface
|
|
69
|
+
? { isValidNodeTarget: createSurfaceNodeSelectionProbe(surface) }
|
|
70
|
+
: undefined;
|
|
71
|
+
const maxOffset = surface
|
|
72
|
+
? surface.storySize
|
|
73
|
+
: countLogicalPositions(parseTextStory(result.document.content).units);
|
|
74
|
+
const validated = validateSelectionAgainstDocument(
|
|
75
|
+
result.document,
|
|
76
|
+
result.selection,
|
|
77
|
+
maxOffset,
|
|
78
|
+
options,
|
|
79
|
+
);
|
|
64
80
|
if (validated === result.selection) {
|
|
65
81
|
return result;
|
|
66
82
|
}
|