@beyondwork/docx-react-component 1.0.57 → 1.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +1149 -8
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +120 -39
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +165 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +544 -35
- package/src/runtime/document-search.ts +176 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +293 -18
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorStoryTarget,
|
|
3
|
+
ScopeQueryFilter,
|
|
4
|
+
ScopeQueryResult,
|
|
5
|
+
WorkflowMetadataEntry,
|
|
6
|
+
WorkflowOverlay,
|
|
7
|
+
WorkflowScope,
|
|
8
|
+
WorkflowWorkItem,
|
|
9
|
+
} from "../api/public-types.ts";
|
|
10
|
+
import type { CanonicalDocument } from "../model/canonical-document.ts";
|
|
11
|
+
import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
|
|
12
|
+
import { collectScopeLocations } from "./scope-resolver.ts";
|
|
13
|
+
|
|
14
|
+
function storyTargetsEqual(a: EditorStoryTarget, b: EditorStoryTarget): boolean {
|
|
15
|
+
if (a.kind !== b.kind) return false;
|
|
16
|
+
switch (a.kind) {
|
|
17
|
+
case "main":
|
|
18
|
+
return true;
|
|
19
|
+
case "header":
|
|
20
|
+
case "footer":
|
|
21
|
+
return (
|
|
22
|
+
a.relationshipId === (b as typeof a).relationshipId &&
|
|
23
|
+
a.variant === (b as typeof a).variant &&
|
|
24
|
+
a.sectionIndex === (b as typeof a).sectionIndex
|
|
25
|
+
);
|
|
26
|
+
case "footnote":
|
|
27
|
+
case "endnote":
|
|
28
|
+
return a.noteId === (b as typeof a).noteId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MAIN_STORY: EditorStoryTarget = { kind: "main" };
|
|
33
|
+
|
|
34
|
+
export interface ScopeQueryInputs {
|
|
35
|
+
readonly overlay: WorkflowOverlay | null;
|
|
36
|
+
readonly entries: readonly WorkflowMetadataEntry[];
|
|
37
|
+
readonly document: Pick<CanonicalDocument, "content"> | CanonicalDocumentEnvelope;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Phase C §C2 helper — project an ordered list of scopeIds (already produced
|
|
42
|
+
* by a geometric walk like `findAllScopesAt` / `findScopesIntersecting`)
|
|
43
|
+
* into `ScopeQueryResult[]`. Preserves incoming order. Filters out scopeIds
|
|
44
|
+
* that are not in the overlay (e.g. orphan markers). Applies the §C8
|
|
45
|
+
* visibility filter via `includeHidden` / `includeInvisible`.
|
|
46
|
+
*/
|
|
47
|
+
export function projectScopeQueryResults(
|
|
48
|
+
inputs: ScopeQueryInputs,
|
|
49
|
+
scopeIds: readonly string[],
|
|
50
|
+
options: { includeHidden?: boolean; includeInvisible?: boolean } = {},
|
|
51
|
+
): ScopeQueryResult[] {
|
|
52
|
+
const overlay = inputs.overlay;
|
|
53
|
+
if (!overlay) return [];
|
|
54
|
+
const includeHidden = options.includeHidden === true;
|
|
55
|
+
const includeInvisible = options.includeInvisible === true;
|
|
56
|
+
|
|
57
|
+
const scopesById = new Map<string, WorkflowScope>();
|
|
58
|
+
for (const scope of overlay.scopes) scopesById.set(scope.scopeId, scope);
|
|
59
|
+
|
|
60
|
+
const entriesByScope = new Map<string, WorkflowMetadataEntry[]>();
|
|
61
|
+
for (const entry of inputs.entries) {
|
|
62
|
+
if (!entry.scopeId) continue;
|
|
63
|
+
let list = entriesByScope.get(entry.scopeId);
|
|
64
|
+
if (!list) {
|
|
65
|
+
list = [];
|
|
66
|
+
entriesByScope.set(entry.scopeId, list);
|
|
67
|
+
}
|
|
68
|
+
list.push(entry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workItemsById = new Map<string, WorkflowWorkItem>();
|
|
72
|
+
for (const item of overlay.workItems ?? []) {
|
|
73
|
+
workItemsById.set(item.workItemId, item);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const results: ScopeQueryResult[] = [];
|
|
77
|
+
for (const scopeId of scopeIds) {
|
|
78
|
+
const scope = scopesById.get(scopeId);
|
|
79
|
+
if (!scope) continue;
|
|
80
|
+
if (scope.visibility === "hidden" && !includeHidden) continue;
|
|
81
|
+
if (scope.visibility === "invisible" && !includeInvisible) continue;
|
|
82
|
+
const entries = entriesByScope.get(scopeId) ?? [];
|
|
83
|
+
const workItem = scope.workItemId
|
|
84
|
+
? workItemsById.get(scope.workItemId) ?? null
|
|
85
|
+
: null;
|
|
86
|
+
results.push({ scope, entries, workItem });
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pure §C1 projector — filter + join scopes with entries + workItem, ordered
|
|
93
|
+
* by start-marker document position (scopeId ASC tiebreak). Snapshot-based;
|
|
94
|
+
* no runtime state mutation.
|
|
95
|
+
*/
|
|
96
|
+
export function queryScopes(
|
|
97
|
+
inputs: ScopeQueryInputs,
|
|
98
|
+
filter: ScopeQueryFilter | undefined,
|
|
99
|
+
): ScopeQueryResult[] {
|
|
100
|
+
const overlay = inputs.overlay;
|
|
101
|
+
if (!overlay) return [];
|
|
102
|
+
|
|
103
|
+
const normalizedStoryFilter = filter?.storyTarget ?? MAIN_STORY;
|
|
104
|
+
const workItemIdSet = filter?.workItemIds ? new Set(filter.workItemIds) : null;
|
|
105
|
+
const modeSet = filter?.modes ? new Set(filter.modes) : null;
|
|
106
|
+
const domainSet = filter?.domains ? new Set(filter.domains) : null;
|
|
107
|
+
const labelPrefix = filter?.labelPrefix?.toLowerCase();
|
|
108
|
+
const metadataId = filter?.metadataId;
|
|
109
|
+
const hasValue = filter?.hasValue;
|
|
110
|
+
const limit = filter?.limit;
|
|
111
|
+
const includeHidden = filter?.includeHidden === true;
|
|
112
|
+
const includeInvisible = filter?.includeInvisible === true;
|
|
113
|
+
|
|
114
|
+
const entriesByScope = new Map<string, WorkflowMetadataEntry[]>();
|
|
115
|
+
for (const entry of inputs.entries) {
|
|
116
|
+
if (!entry.scopeId) continue;
|
|
117
|
+
let list = entriesByScope.get(entry.scopeId);
|
|
118
|
+
if (!list) {
|
|
119
|
+
list = [];
|
|
120
|
+
entriesByScope.set(entry.scopeId, list);
|
|
121
|
+
}
|
|
122
|
+
list.push(entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const workItemsById = new Map<string, WorkflowWorkItem>();
|
|
126
|
+
for (const item of overlay.workItems ?? []) {
|
|
127
|
+
workItemsById.set(item.workItemId, item);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const locations = collectScopeLocations(inputs.document);
|
|
131
|
+
|
|
132
|
+
const candidates: Array<{ scope: WorkflowScope; startPos: number }> = [];
|
|
133
|
+
for (const scope of overlay.scopes) {
|
|
134
|
+
// §C8: respect visibility flags. Missing field treated as "visible".
|
|
135
|
+
if (scope.visibility === "hidden" && !includeHidden) continue;
|
|
136
|
+
if (scope.visibility === "invisible" && !includeInvisible) continue;
|
|
137
|
+
|
|
138
|
+
if (normalizedStoryFilter !== "*") {
|
|
139
|
+
const scopeStory = scope.storyTarget ?? MAIN_STORY;
|
|
140
|
+
if (!storyTargetsEqual(scopeStory, normalizedStoryFilter)) continue;
|
|
141
|
+
}
|
|
142
|
+
if (modeSet && !modeSet.has(scope.mode)) continue;
|
|
143
|
+
if (domainSet && !(scope.domain && domainSet.has(scope.domain))) continue;
|
|
144
|
+
if (workItemIdSet) {
|
|
145
|
+
if (!scope.workItemId || !workItemIdSet.has(scope.workItemId)) continue;
|
|
146
|
+
}
|
|
147
|
+
if (labelPrefix) {
|
|
148
|
+
const label = scope.label?.toLowerCase() ?? "";
|
|
149
|
+
if (!label.startsWith(labelPrefix)) continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const scopeEntries = entriesByScope.get(scope.scopeId) ?? [];
|
|
153
|
+
|
|
154
|
+
if (metadataId && !scopeEntries.some((e) => e.metadataId === metadataId)) continue;
|
|
155
|
+
if (hasValue) {
|
|
156
|
+
const anyMatch = scopeEntries.some(
|
|
157
|
+
(e) => e.value !== undefined && hasValue(e.value, e),
|
|
158
|
+
);
|
|
159
|
+
if (!anyMatch) continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const loc = locations.get(scope.scopeId);
|
|
163
|
+
const startPos =
|
|
164
|
+
loc?.startPos ?? loc?.endPos ?? Number.POSITIVE_INFINITY;
|
|
165
|
+
candidates.push({ scope, startPos });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
candidates.sort((a, b) => {
|
|
169
|
+
if (a.startPos !== b.startPos) return a.startPos - b.startPos;
|
|
170
|
+
return a.scope.scopeId < b.scope.scopeId ? -1 : a.scope.scopeId > b.scope.scopeId ? 1 : 0;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const results: ScopeQueryResult[] = [];
|
|
174
|
+
for (const { scope } of candidates) {
|
|
175
|
+
if (limit !== undefined && results.length >= limit) break;
|
|
176
|
+
const entries = entriesByScope.get(scope.scopeId) ?? [];
|
|
177
|
+
const workItem = scope.workItemId
|
|
178
|
+
? workItemsById.get(scope.workItemId) ?? null
|
|
179
|
+
: null;
|
|
180
|
+
results.push({ scope, entries, workItem });
|
|
181
|
+
}
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
@@ -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")
|
|
@@ -146,3 +146,63 @@ export function findScopeAt(
|
|
|
146
146
|
}
|
|
147
147
|
return best;
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Phase C §C2 — every enclosing scope at `position`, ordered outermost →
|
|
152
|
+
* innermost (lowest startPos first; ties broken on scopeId ASC). Includes
|
|
153
|
+
* scopes that touch the position exactly (`startPos <= position <= endPos`).
|
|
154
|
+
* Companion to `findScopeAt`, which keeps the innermost-only contract.
|
|
155
|
+
*/
|
|
156
|
+
export function findAllScopesAt(
|
|
157
|
+
document: Pick<CanonicalDocument, "content"> | CanonicalDocumentEnvelope,
|
|
158
|
+
position: number,
|
|
159
|
+
): ResolvedScopeLocation[] {
|
|
160
|
+
const locations = collectScopeLocations(document);
|
|
161
|
+
const hits: ResolvedScopeLocation[] = [];
|
|
162
|
+
for (const [scopeId, loc] of locations) {
|
|
163
|
+
if (loc.startPos === undefined || loc.endPos === undefined) continue;
|
|
164
|
+
if (position < loc.startPos || position > loc.endPos) continue;
|
|
165
|
+
hits.push({ scopeId, startPos: loc.startPos, endPos: loc.endPos });
|
|
166
|
+
}
|
|
167
|
+
hits.sort((a, b) => {
|
|
168
|
+
if (a.startPos !== b.startPos) return a.startPos - b.startPos;
|
|
169
|
+
return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
|
|
170
|
+
});
|
|
171
|
+
return hits;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Phase C §C2 — every scope whose marker range intersects `[rangeFrom,
|
|
176
|
+
* rangeTo]`. `mode: "overlap"` (default) accepts any intersection including
|
|
177
|
+
* touching endpoints; `mode: "contain"` requires the scope's entire marker
|
|
178
|
+
* range to lie within `[rangeFrom, rangeTo]`. Deterministic order: startPos
|
|
179
|
+
* ASC, scopeId ASC.
|
|
180
|
+
*/
|
|
181
|
+
export function findScopesIntersecting(
|
|
182
|
+
document: Pick<CanonicalDocument, "content"> | CanonicalDocumentEnvelope,
|
|
183
|
+
rangeFrom: number,
|
|
184
|
+
rangeTo: number,
|
|
185
|
+
mode: "overlap" | "contain" = "overlap",
|
|
186
|
+
): ResolvedScopeLocation[] {
|
|
187
|
+
const from = Math.min(rangeFrom, rangeTo);
|
|
188
|
+
const to = Math.max(rangeFrom, rangeTo);
|
|
189
|
+
const locations = collectScopeLocations(document);
|
|
190
|
+
const hits: ResolvedScopeLocation[] = [];
|
|
191
|
+
for (const [scopeId, loc] of locations) {
|
|
192
|
+
if (loc.startPos === undefined || loc.endPos === undefined) continue;
|
|
193
|
+
const sFrom = Math.min(loc.startPos, loc.endPos);
|
|
194
|
+
const sTo = Math.max(loc.startPos, loc.endPos);
|
|
195
|
+
if (mode === "contain") {
|
|
196
|
+
if (sFrom < from || sTo > to) continue;
|
|
197
|
+
} else {
|
|
198
|
+
// overlap — any intersection including touching endpoints
|
|
199
|
+
if (sTo < from || sFrom > to) continue;
|
|
200
|
+
}
|
|
201
|
+
hits.push({ scopeId, startPos: loc.startPos, endPos: loc.endPos });
|
|
202
|
+
}
|
|
203
|
+
hits.sort((a, b) => {
|
|
204
|
+
if (a.startPos !== b.startPos) return a.startPos - b.startPos;
|
|
205
|
+
return a.scopeId < b.scopeId ? -1 : a.scopeId > b.scopeId ? 1 : 0;
|
|
206
|
+
});
|
|
207
|
+
return hits;
|
|
208
|
+
}
|
|
@@ -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
|
}
|