@beyondwork/docx-react-component 1.0.79 → 1.0.81
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 +1 -1
- package/src/api/public-types.ts +28 -21
- package/src/api/v3/ai/resolve.ts +13 -7
- package/src/api/v3/runtime/workflow.ts +0 -9
- package/src/api/v3/ui/chrome-composition.ts +10 -2
- package/src/core/commands/add-scope.ts +110 -84
- package/src/runtime/formatting/formatting-types.ts +16 -0
- package/src/runtime/formatting/revision-display.ts +16 -10
- package/src/runtime/scopes/compile-scope-bundle.ts +9 -1
- package/src/runtime/scopes/compile-scope.ts +16 -0
- package/src/runtime/scopes/enumerate-scopes.ts +116 -3
- package/src/runtime/scopes/replaceability.ts +16 -0
- package/src/runtime/scopes/replacement/apply.ts +13 -3
- package/src/runtime/scopes/resolve-reference.ts +5 -0
- package/src/runtime/scopes/scope-kinds/scope.ts +87 -0
- package/src/runtime/scopes/scope-range.ts +11 -0
- package/src/runtime/workflow/coordinator.ts +3 -6
- package/src/runtime/workflow/scope-writer.ts +5 -26
- package/src/ui/WordReviewEditor.tsx +62 -3
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +10 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +153 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
- package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
- package/src/ui-tailwind/review-workspace/types.ts +1 -0
- package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
- package/src/ui-tailwind/theme/editor-theme.css +10 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +114 -14
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.81",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -1211,6 +1211,18 @@ export type SurfaceInlineSegment =
|
|
|
1211
1211
|
* is enforced by `test/runtime/formatting/production-boundary.test.ts`.
|
|
1212
1212
|
*/
|
|
1213
1213
|
revisionDisplay?: {
|
|
1214
|
+
/**
|
|
1215
|
+
* Identity of the attached revision. Render consumers emit
|
|
1216
|
+
* `data-revision-id` and route sidebar scroll-to-revision off
|
|
1217
|
+
* this field rather than re-deriving from the review store.
|
|
1218
|
+
*/
|
|
1219
|
+
revisionId: string;
|
|
1220
|
+
/**
|
|
1221
|
+
* The revision's kind. Mirrors `RevisionRecord.kind` on the
|
|
1222
|
+
* canonical document. Render consumers branch on this (e.g.
|
|
1223
|
+
* widget-bracket placement in suggestions mode).
|
|
1224
|
+
*/
|
|
1225
|
+
kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
|
|
1214
1226
|
markupMode: "clean" | "simple" | "all";
|
|
1215
1227
|
hidden?: boolean;
|
|
1216
1228
|
strikethrough?: boolean;
|
|
@@ -2517,23 +2529,20 @@ export interface AddScopeResult {
|
|
|
2517
2529
|
/**
|
|
2518
2530
|
* Structured diagnostic for the marker-plant attempt. Omitted on the
|
|
2519
2531
|
* happy path for back-compat; populated with `planted: false` + a
|
|
2520
|
-
* typed reason when the plant was refused (
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
*
|
|
2532
|
+
* typed reason when the plant was refused (non-paragraph target,
|
|
2533
|
+
* out-of-bounds, empty-document). Cross-paragraph ranges plant
|
|
2534
|
+
* successfully as of 2026-04-24 — see the multi-paragraph-scopes
|
|
2535
|
+
* slice. Callers that previously assumed every returned `scopeId`
|
|
2536
|
+
* was live should migrate to checking this field.
|
|
2524
2537
|
*/
|
|
2525
2538
|
plantStatus?: {
|
|
2526
2539
|
readonly planted: false;
|
|
2527
2540
|
readonly reason:
|
|
2528
|
-
| "cross-paragraph-range"
|
|
2529
2541
|
| "non-paragraph-target"
|
|
2530
2542
|
| "range-out-of-bounds"
|
|
2531
2543
|
| "empty-document";
|
|
2532
2544
|
readonly requestedFrom: number;
|
|
2533
2545
|
readonly requestedTo: number;
|
|
2534
|
-
/** Present on `cross-paragraph-range`. */
|
|
2535
|
-
readonly fromBlockIndex?: number;
|
|
2536
|
-
readonly toBlockIndex?: number;
|
|
2537
2546
|
/** Present on `non-paragraph-target`. */
|
|
2538
2547
|
readonly blockIndex?: number;
|
|
2539
2548
|
readonly blockKind?: string;
|
|
@@ -5602,17 +5611,15 @@ export interface WordReviewEditorProps {
|
|
|
5602
5611
|
import("../ui/headless/chrome-registry").SelectionToolRegistryEntry
|
|
5603
5612
|
>;
|
|
5604
5613
|
/**
|
|
5605
|
-
*
|
|
5606
|
-
*
|
|
5607
|
-
*
|
|
5608
|
-
*
|
|
5609
|
-
*
|
|
5610
|
-
*
|
|
5611
|
-
*
|
|
5612
|
-
*
|
|
5613
|
-
*
|
|
5614
|
-
* palette). Required for the default editor to expose the
|
|
5615
|
-
* Phase C.γ surfaces shipped in `5fce913a`.
|
|
5614
|
+
* Optional host-callback extension bag for workspace command chrome.
|
|
5615
|
+
* The default `<WordReviewEditor />` path now mounts
|
|
5616
|
+
* `TwWorkspaceChromeHost` with product-backed commands for formatting,
|
|
5617
|
+
* paragraph/list actions, comments, and table insertion/structure.
|
|
5618
|
+
* Supplying this bag overrides or extends those defaults for host-owned
|
|
5619
|
+
* actions such as custom table properties, hyperlink handling, or
|
|
5620
|
+
* object metadata. Actions without a wired callback are hidden from
|
|
5621
|
+
* every surface — progressive disclosure per `designsystem.md §2.1
|
|
5622
|
+
* principle 4`.
|
|
5616
5623
|
*/
|
|
5617
5624
|
editorActionHost?: import("../ui-tailwind/chrome/editor-action-registry").EditorActionHostCallbacks;
|
|
5618
5625
|
/**
|
|
@@ -5628,8 +5635,8 @@ export interface WordReviewEditorProps {
|
|
|
5628
5635
|
/**
|
|
5629
5636
|
* Suppress the global Ctrl/Cmd+K palette listener — e.g. when a
|
|
5630
5637
|
* higher-priority modal captures keyboard focus. Defaults to
|
|
5631
|
-
* `false` (palette listener is active
|
|
5632
|
-
*
|
|
5638
|
+
* `false` (the default product command palette listener is active
|
|
5639
|
+
* unless this prop suppresses it).
|
|
5633
5640
|
*/
|
|
5634
5641
|
commandPaletteDisabled?: boolean;
|
|
5635
5642
|
/**
|
package/src/api/v3/ai/resolve.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @endStateApi v3 — `ai.resolve` family.
|
|
3
3
|
*
|
|
4
|
-
* Slice 3 of refactor/08
|
|
5
|
-
* `live-with-adapter
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Slice 3 of refactor/08 graduated `resolveReference` to
|
|
5
|
+
* `live-with-adapter`. Post-KI-P9 (2026-04-24) the reference union
|
|
6
|
+
* carries **three durable reference kinds**: `scope-id`, `semantic-path`,
|
|
7
|
+
* and `natural-language`. Positional queries (`offset` / `range`) were
|
|
8
|
+
* split into `queryScopeAtPosition(at)` / `queryScopeInRange(from, to)`
|
|
9
|
+
* one-shot APIs that return `ScopeHandle | null` directly — the
|
|
10
|
+
* type-system split prevents a positional hint from being cached or
|
|
11
|
+
* round-tripped as a reference across a mutation.
|
|
12
|
+
*
|
|
13
|
+
* Offset / range lookups still honor precise nested-kind ranges (fields,
|
|
14
|
+
* table rows, table cells) via the scope-compiler's specificity
|
|
15
|
+
* tie-breaker — see `docs/architecture/08-semantic-scope-compiler.md`
|
|
16
|
+
* §"Resolve-reference precision" for the specificity table.
|
|
11
17
|
*
|
|
12
18
|
* The `natural-language` hint remains **partial by design**. The live
|
|
13
19
|
* path is a deterministic substring matcher over:
|
|
@@ -161,14 +161,11 @@ export type CreateScopeFromAnchorResult =
|
|
|
161
161
|
| "from-negative"
|
|
162
162
|
| "to-less-than-from"
|
|
163
163
|
| "range-exceeds-story-length"
|
|
164
|
-
| "cross-paragraph-range"
|
|
165
164
|
| "non-paragraph-target"
|
|
166
165
|
| "empty-document";
|
|
167
166
|
readonly from: number;
|
|
168
167
|
readonly to: number;
|
|
169
168
|
readonly storyLength: number;
|
|
170
|
-
readonly fromBlockIndex?: number;
|
|
171
|
-
readonly toBlockIndex?: number;
|
|
172
169
|
readonly blockIndex?: number;
|
|
173
170
|
readonly blockKind?: string;
|
|
174
171
|
/** Agent-actionable single-sentence explanation. Safe to surface to LLM tool replies as-is. */
|
|
@@ -540,12 +537,6 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
540
537
|
from: adapterResult.from,
|
|
541
538
|
to: adapterResult.to,
|
|
542
539
|
storyLength: adapterResult.storyLength,
|
|
543
|
-
...(adapterResult.fromBlockIndex !== undefined
|
|
544
|
-
? { fromBlockIndex: adapterResult.fromBlockIndex }
|
|
545
|
-
: {}),
|
|
546
|
-
...(adapterResult.toBlockIndex !== undefined
|
|
547
|
-
? { toBlockIndex: adapterResult.toBlockIndex }
|
|
548
|
-
: {}),
|
|
549
540
|
...(adapterResult.blockIndex !== undefined
|
|
550
541
|
? { blockIndex: adapterResult.blockIndex }
|
|
551
542
|
: {}),
|
|
@@ -131,6 +131,7 @@ export interface ChromeCompositionInput {
|
|
|
131
131
|
| "simple"
|
|
132
132
|
| "all";
|
|
133
133
|
readonly activeRailTab?: EditorRailTab | null;
|
|
134
|
+
readonly railOpen?: boolean;
|
|
134
135
|
readonly pinnedRailTabs?: ReadonlySet<EditorRailTab>;
|
|
135
136
|
readonly density?: ChromeDensity;
|
|
136
137
|
readonly containerWidth?: number;
|
|
@@ -255,13 +256,18 @@ function resolveVisibleRailTabs(
|
|
|
255
256
|
railOpen: boolean,
|
|
256
257
|
diagnosticsSignal: DiagnosticsSignal,
|
|
257
258
|
pinned: ReadonlySet<EditorRailTab>,
|
|
259
|
+
mode: EditorChromeMode,
|
|
258
260
|
): ReadonlySet<EditorRailTab> {
|
|
259
261
|
const visible = new Set<EditorRailTab>();
|
|
260
262
|
if (railOpen) {
|
|
261
263
|
visible.add("comments");
|
|
262
264
|
visible.add("changes");
|
|
263
265
|
visible.add("workflow");
|
|
264
|
-
if (
|
|
266
|
+
if (
|
|
267
|
+
diagnosticsSignal.severity !== "none" ||
|
|
268
|
+
diagnosticsSignal.count > 0 ||
|
|
269
|
+
mode === "more"
|
|
270
|
+
) {
|
|
265
271
|
visible.add("health");
|
|
266
272
|
}
|
|
267
273
|
}
|
|
@@ -294,11 +300,13 @@ export function resolveChromeComposition(
|
|
|
294
300
|
const density: ChromeDensity = input.density ?? "standard";
|
|
295
301
|
const pinnedRailTabs: ReadonlySet<EditorRailTab> =
|
|
296
302
|
input.pinnedRailTabs ?? new Set<EditorRailTab>();
|
|
297
|
-
const railOpen =
|
|
303
|
+
const railOpen =
|
|
304
|
+
input.railOpen ?? (options.showReviewRail && visibility.reviewRail);
|
|
298
305
|
const visibleTabs = resolveVisibleRailTabs(
|
|
299
306
|
railOpen,
|
|
300
307
|
diagnosticsSignal,
|
|
301
308
|
pinnedRailTabs,
|
|
309
|
+
mode,
|
|
302
310
|
);
|
|
303
311
|
|
|
304
312
|
// Default active tab: honor caller; otherwise land on the mode-appropriate tab.
|
|
@@ -16,6 +16,10 @@ import type { CanonicalDocumentEnvelope } from "../state/editor-state.ts";
|
|
|
16
16
|
* knowing whether the scope was ever live. Every non-`"planted"`
|
|
17
17
|
* status is now typed so callers can discriminate + surface
|
|
18
18
|
* agent-actionable recovery hints.
|
|
19
|
+
*
|
|
20
|
+
* Cross-paragraph ranges are first-class (planted) as of the
|
|
21
|
+
* 2026-04-24 multi-paragraph-scopes slice. The `cross-paragraph-range`
|
|
22
|
+
* failure variant no longer exists — markers now span paragraphs.
|
|
19
23
|
*/
|
|
20
24
|
export type InsertScopeMarkersResult =
|
|
21
25
|
| {
|
|
@@ -25,14 +29,6 @@ export type InsertScopeMarkersResult =
|
|
|
25
29
|
/** Absolute span that got planted (after from/to normalization). */
|
|
26
30
|
readonly plantedRange: { readonly from: number; readonly to: number };
|
|
27
31
|
}
|
|
28
|
-
| {
|
|
29
|
-
readonly status: "cross-paragraph-range";
|
|
30
|
-
readonly scopeId: string;
|
|
31
|
-
readonly from: number;
|
|
32
|
-
readonly to: number;
|
|
33
|
-
readonly fromBlockIndex: number;
|
|
34
|
-
readonly toBlockIndex: number;
|
|
35
|
-
}
|
|
36
32
|
| {
|
|
37
33
|
readonly status: "non-paragraph-target";
|
|
38
34
|
readonly scopeId: string;
|
|
@@ -60,13 +56,17 @@ export type InsertScopeMarkersResult =
|
|
|
60
56
|
* pair of `scope_marker_start` / `scope_marker_end` inline nodes got
|
|
61
57
|
* inserted at `[from, to]`.
|
|
62
58
|
*
|
|
59
|
+
* Same-paragraph ranges insert both markers into the single owning
|
|
60
|
+
* paragraph. Cross-paragraph ranges place the start marker inside the
|
|
61
|
+
* paragraph containing `from` and the end marker inside the paragraph
|
|
62
|
+
* containing `to`; intermediate top-level blocks (paragraphs, tables,
|
|
63
|
+
* section breaks) are passed through unmodified and fall inside the
|
|
64
|
+
* scope by document order.
|
|
65
|
+
*
|
|
63
66
|
* Failure modes (all previously silent — pre-2026-04-24):
|
|
64
|
-
* - `
|
|
65
|
-
* top-level paragraph blocks. Multi-block marker scopes are not
|
|
66
|
-
* yet wired; narrow the range to land inside one paragraph.
|
|
67
|
-
* - `non-paragraph-target` — the target range lands inside a
|
|
67
|
+
* - `non-paragraph-target` — either `from` or `to` lands inside a
|
|
68
68
|
* non-paragraph block (table / SDT / section_break). Marker
|
|
69
|
-
* scopes only plant inside paragraphs
|
|
69
|
+
* scopes only plant inside paragraphs.
|
|
70
70
|
* - `range-out-of-bounds` — `to` exceeds the main story length.
|
|
71
71
|
* - `empty-document` — the document has no root / no children.
|
|
72
72
|
*
|
|
@@ -82,10 +82,10 @@ export function insertScopeMarkers(
|
|
|
82
82
|
to: number;
|
|
83
83
|
},
|
|
84
84
|
): InsertScopeMarkersResult {
|
|
85
|
-
const { scopeId
|
|
85
|
+
const { scopeId } = params;
|
|
86
86
|
const root = document.content as DocumentRootNode;
|
|
87
|
-
const normalizedFrom = Math.min(from, to);
|
|
88
|
-
const normalizedTo = Math.max(from, to);
|
|
87
|
+
const normalizedFrom = Math.min(params.from, params.to);
|
|
88
|
+
const normalizedTo = Math.max(params.from, params.to);
|
|
89
89
|
|
|
90
90
|
if (!root || root.type !== "doc" || root.children.length === 0) {
|
|
91
91
|
return {
|
|
@@ -96,9 +96,15 @@ export function insertScopeMarkers(
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
99
|
+
// Pre-pass — locate which block each of `from` and `to` resolves to,
|
|
100
|
+
// compute story length, and build per-paragraph slot envelopes.
|
|
101
|
+
// **Invariant:** the cursor arithmetic here MUST match
|
|
102
|
+
// `src/runtime/scopes/position-map.ts::computeBlockPositions`
|
|
103
|
+
// (same `inlineLength`, same `+1` per-block boundary). Layer 04
|
|
104
|
+
// cannot change one walker without the other or cross-paragraph
|
|
105
|
+
// marker insertion silently drifts vs. enumerated scope ranges.
|
|
106
|
+
type ParaSlot = { index: number; from: number; to: number };
|
|
107
|
+
const paraSlots: ParaSlot[] = [];
|
|
102
108
|
let cursor = 0;
|
|
103
109
|
let fromBlockIndex = -1;
|
|
104
110
|
let fromBlockKind: string | null = null;
|
|
@@ -115,15 +121,24 @@ export function insertScopeMarkers(
|
|
|
115
121
|
(total, child) => total + inlineLength(child as InlineNode),
|
|
116
122
|
0,
|
|
117
123
|
);
|
|
124
|
+
paraSlots.push({ index: i, from: blockFrom, to: blockFrom + blockLength });
|
|
118
125
|
} else {
|
|
119
126
|
blockLength = 1;
|
|
120
127
|
}
|
|
121
128
|
const blockTo = blockFrom + blockLength;
|
|
122
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
fromBlockIndex === -1 &&
|
|
131
|
+
normalizedFrom >= blockFrom &&
|
|
132
|
+
normalizedFrom <= blockTo
|
|
133
|
+
) {
|
|
123
134
|
fromBlockIndex = i;
|
|
124
135
|
fromBlockKind = block.type;
|
|
125
136
|
}
|
|
126
|
-
if (
|
|
137
|
+
if (
|
|
138
|
+
toBlockIndex === -1 &&
|
|
139
|
+
normalizedTo >= blockFrom &&
|
|
140
|
+
normalizedTo <= blockTo
|
|
141
|
+
) {
|
|
127
142
|
toBlockIndex = i;
|
|
128
143
|
toBlockKind = block.type;
|
|
129
144
|
}
|
|
@@ -150,62 +165,81 @@ export function insertScopeMarkers(
|
|
|
150
165
|
storyLength,
|
|
151
166
|
};
|
|
152
167
|
}
|
|
153
|
-
if (
|
|
168
|
+
if (fromBlockKind !== "paragraph" || toBlockKind !== "paragraph") {
|
|
169
|
+
// Either endpoint lands on a non-paragraph top-level block
|
|
170
|
+
// (table / section_break / sdt). Report the first non-paragraph
|
|
171
|
+
// offender for diagnostic clarity.
|
|
172
|
+
const nonParaIndex =
|
|
173
|
+
fromBlockKind !== "paragraph" ? fromBlockIndex : toBlockIndex;
|
|
174
|
+
const nonParaKind =
|
|
175
|
+
fromBlockKind !== "paragraph" ? fromBlockKind : toBlockKind;
|
|
154
176
|
return {
|
|
155
|
-
status: "
|
|
177
|
+
status: "non-paragraph-target",
|
|
156
178
|
scopeId,
|
|
157
179
|
from: normalizedFrom,
|
|
158
180
|
to: normalizedTo,
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
blockIndex: nonParaIndex,
|
|
182
|
+
blockKind: nonParaKind ?? "unknown",
|
|
161
183
|
};
|
|
162
184
|
}
|
|
163
|
-
|
|
185
|
+
|
|
186
|
+
const startSlot = paraSlots.find((s) => s.index === fromBlockIndex)!;
|
|
187
|
+
const endSlot = paraSlots.find((s) => s.index === toBlockIndex)!;
|
|
188
|
+
|
|
189
|
+
// Same-paragraph fast path: inject both markers into one paragraph.
|
|
190
|
+
// Preserves all existing single-paragraph behaviour byte-for-byte
|
|
191
|
+
// (regression pin [C9]).
|
|
192
|
+
if (startSlot.index === endSlot.index) {
|
|
193
|
+
const newChildren = injectMarkersIntoInlineList(
|
|
194
|
+
(root.children[startSlot.index] as ParagraphNode).children as InlineNode[],
|
|
195
|
+
scopeId,
|
|
196
|
+
normalizedFrom - startSlot.from,
|
|
197
|
+
normalizedTo - startSlot.from,
|
|
198
|
+
"both",
|
|
199
|
+
);
|
|
200
|
+
const children = root.children.map((block, i) =>
|
|
201
|
+
i === startSlot.index
|
|
202
|
+
? ({ ...block, children: newChildren } as ParagraphNode)
|
|
203
|
+
: block,
|
|
204
|
+
);
|
|
164
205
|
return {
|
|
165
|
-
status: "
|
|
206
|
+
status: "planted",
|
|
207
|
+
document: { ...document, content: { ...root, children } },
|
|
166
208
|
scopeId,
|
|
167
|
-
from: normalizedFrom,
|
|
168
|
-
to: normalizedTo,
|
|
169
|
-
blockIndex: fromBlockIndex,
|
|
170
|
-
blockKind: fromBlockKind ?? "unknown",
|
|
209
|
+
plantedRange: { from: normalizedFrom, to: normalizedTo },
|
|
171
210
|
};
|
|
172
211
|
}
|
|
173
212
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
if (blockIndex < root.children.length - 1) cursor += 1;
|
|
188
|
-
return block;
|
|
213
|
+
// Cross-paragraph path — start marker goes in the start-bearing
|
|
214
|
+
// paragraph; end marker goes in the end-bearing paragraph;
|
|
215
|
+
// intermediate blocks pass through unchanged.
|
|
216
|
+
const children = root.children.map((block, i) => {
|
|
217
|
+
if (i === startSlot.index) {
|
|
218
|
+
const newChildren = injectMarkersIntoInlineList(
|
|
219
|
+
(block as ParagraphNode).children as InlineNode[],
|
|
220
|
+
scopeId,
|
|
221
|
+
normalizedFrom - startSlot.from,
|
|
222
|
+
Number.POSITIVE_INFINITY,
|
|
223
|
+
"start-only",
|
|
224
|
+
);
|
|
225
|
+
return { ...block, children: newChildren } as ParagraphNode;
|
|
189
226
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return
|
|
227
|
+
if (i === endSlot.index) {
|
|
228
|
+
const newChildren = injectMarkersIntoInlineList(
|
|
229
|
+
(block as ParagraphNode).children as InlineNode[],
|
|
230
|
+
scopeId,
|
|
231
|
+
Number.NEGATIVE_INFINITY,
|
|
232
|
+
normalizedTo - endSlot.from,
|
|
233
|
+
"end-only",
|
|
234
|
+
);
|
|
235
|
+
return { ...block, children: newChildren } as ParagraphNode;
|
|
236
|
+
}
|
|
237
|
+
return block;
|
|
201
238
|
});
|
|
202
239
|
|
|
203
240
|
return {
|
|
204
241
|
status: "planted",
|
|
205
|
-
document: {
|
|
206
|
-
...document,
|
|
207
|
-
content: { ...root, children },
|
|
208
|
-
},
|
|
242
|
+
document: { ...document, content: { ...root, children } },
|
|
209
243
|
scopeId,
|
|
210
244
|
plantedRange: { from: normalizedFrom, to: normalizedTo },
|
|
211
245
|
};
|
|
@@ -273,28 +307,28 @@ function injectMarkersIntoInlineList(
|
|
|
273
307
|
scopeId: string,
|
|
274
308
|
startOffset: number,
|
|
275
309
|
endOffset: number,
|
|
310
|
+
mode: "both" | "start-only" | "end-only",
|
|
276
311
|
): InlineNode[] {
|
|
277
|
-
const start: ScopeMarkerStartNode = {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
type: "scope_marker_end",
|
|
283
|
-
scopeId,
|
|
284
|
-
};
|
|
312
|
+
const start: ScopeMarkerStartNode = { type: "scope_marker_start", scopeId };
|
|
313
|
+
const end: ScopeMarkerEndNode = { type: "scope_marker_end", scopeId };
|
|
314
|
+
|
|
315
|
+
const needStart = mode === "both" || mode === "start-only";
|
|
316
|
+
const needEnd = mode === "both" || mode === "end-only";
|
|
285
317
|
|
|
286
318
|
const output: InlineNode[] = [];
|
|
287
319
|
let cursor = 0;
|
|
288
|
-
let startEmitted =
|
|
289
|
-
let endEmitted =
|
|
320
|
+
let startEmitted = !needStart;
|
|
321
|
+
let endEmitted = !needEnd;
|
|
290
322
|
|
|
291
323
|
for (const node of inlines) {
|
|
292
324
|
const length = inlineLength(node);
|
|
293
325
|
const nodeStart = cursor;
|
|
294
326
|
const nodeEnd = cursor + length;
|
|
295
327
|
|
|
296
|
-
const startInside =
|
|
297
|
-
|
|
328
|
+
const startInside =
|
|
329
|
+
needStart && !startEmitted && startOffset >= nodeStart && startOffset <= nodeEnd;
|
|
330
|
+
const endInside =
|
|
331
|
+
needEnd && !endEmitted && endOffset >= nodeStart && endOffset <= nodeEnd;
|
|
298
332
|
|
|
299
333
|
if (!startInside && !endInside) {
|
|
300
334
|
output.push(node);
|
|
@@ -302,10 +336,7 @@ function injectMarkersIntoInlineList(
|
|
|
302
336
|
continue;
|
|
303
337
|
}
|
|
304
338
|
|
|
305
|
-
// Currently only text nodes support splitting for an internal cut.
|
|
306
339
|
if (node.type !== "text") {
|
|
307
|
-
// For non-text nodes, markers land at the node boundary closest to the
|
|
308
|
-
// target offset — avoids mid-atom splits which would corrupt the node.
|
|
309
340
|
if (startInside && !startEmitted && startOffset <= nodeStart) {
|
|
310
341
|
output.push(start);
|
|
311
342
|
startEmitted = true;
|
|
@@ -330,11 +361,7 @@ function injectMarkersIntoInlineList(
|
|
|
330
361
|
const text = node.text;
|
|
331
362
|
const chars = Array.from(text);
|
|
332
363
|
const marks = node.marks;
|
|
333
|
-
const pieces: {
|
|
334
|
-
cut: number;
|
|
335
|
-
emit: "start" | "end";
|
|
336
|
-
}[] = [];
|
|
337
|
-
|
|
364
|
+
const pieces: { cut: number; emit: "start" | "end" }[] = [];
|
|
338
365
|
if (startInside) pieces.push({ cut: startOffset - nodeStart, emit: "start" });
|
|
339
366
|
if (endInside) pieces.push({ cut: endOffset - nodeStart, emit: "end" });
|
|
340
367
|
pieces.sort((a, b) => a.cut - b.cut || (a.emit === "start" ? -1 : 1));
|
|
@@ -370,12 +397,11 @@ function injectMarkersIntoInlineList(
|
|
|
370
397
|
cursor = nodeEnd;
|
|
371
398
|
}
|
|
372
399
|
|
|
373
|
-
|
|
374
|
-
if (!startEmitted) {
|
|
400
|
+
if (needStart && !startEmitted) {
|
|
375
401
|
output.push(start);
|
|
376
402
|
startEmitted = true;
|
|
377
403
|
}
|
|
378
|
-
if (!endEmitted) {
|
|
404
|
+
if (needEnd && !endEmitted) {
|
|
379
405
|
output.push(end);
|
|
380
406
|
endEmitted = true;
|
|
381
407
|
}
|
|
@@ -110,6 +110,22 @@ export interface EffectiveFieldDisplay {
|
|
|
110
110
|
* visual vocabulary (CSS class, inline style, decoration).
|
|
111
111
|
*/
|
|
112
112
|
export interface RevisionDisplayFlags {
|
|
113
|
+
/**
|
|
114
|
+
* The attached revision's id. Enables render consumers to emit
|
|
115
|
+
* `data-revision-id` + route sidebar scroll-to-revision off the
|
|
116
|
+
* authoritative L03 flags rather than re-deriving from the review-store
|
|
117
|
+
* side channel. Present whenever `input.revision !== undefined` drove
|
|
118
|
+
* flag emission.
|
|
119
|
+
*/
|
|
120
|
+
readonly revisionId: string;
|
|
121
|
+
/**
|
|
122
|
+
* The revision's kind. Mirrors `RevisionRecord.kind` from the canonical
|
|
123
|
+
* document (`src/model/canonical-document.ts`). Render consumers use
|
|
124
|
+
* this to branch on insertion/deletion/move/property-change variants
|
|
125
|
+
* (e.g. widget-bracket placement in suggestions mode) without reading
|
|
126
|
+
* the review store.
|
|
127
|
+
*/
|
|
128
|
+
readonly kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
|
|
113
129
|
readonly markupMode: "clean" | "simple" | "all";
|
|
114
130
|
/** Hide the run entirely (e.g. open deletion in "clean" mode). */
|
|
115
131
|
readonly hidden?: boolean;
|
|
@@ -44,16 +44,16 @@ export function applyRevisionDisplay(
|
|
|
44
44
|
switch (markupMode) {
|
|
45
45
|
case "clean": {
|
|
46
46
|
if (revision.kind === "deletion" && revision.status === "open") {
|
|
47
|
-
return buildFlags(markupMode, authorColor, { hidden: true });
|
|
47
|
+
return buildFlags(revision, markupMode, authorColor, { hidden: true });
|
|
48
48
|
}
|
|
49
|
-
return buildFlags(markupMode, authorColor);
|
|
49
|
+
return buildFlags(revision, markupMode, authorColor);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
case "simple": {
|
|
53
53
|
// Content visible, de-emphasized. No strikethrough / underline
|
|
54
54
|
// markers — the consumer renders a muted style (opacity /
|
|
55
55
|
// secondary color) uniformly.
|
|
56
|
-
return buildFlags(markupMode, authorColor, { deemphasize: true });
|
|
56
|
+
return buildFlags(revision, markupMode, authorColor, { deemphasize: true });
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
case "all": {
|
|
@@ -65,18 +65,18 @@ export function applyRevisionDisplay(
|
|
|
65
65
|
// add the revision strikethrough flag — avoids double-struck
|
|
66
66
|
// glyphs in render.
|
|
67
67
|
if (run.strikethrough === true) {
|
|
68
|
-
return buildFlags(markupMode, authorColor);
|
|
68
|
+
return buildFlags(revision, markupMode, authorColor);
|
|
69
69
|
}
|
|
70
|
-
return buildFlags(markupMode, authorColor, { strikethrough: true });
|
|
70
|
+
return buildFlags(revision, markupMode, authorColor, { strikethrough: true });
|
|
71
71
|
}
|
|
72
72
|
case "insertion": {
|
|
73
73
|
// Mirror short-circuit for insertions. If the run already carries
|
|
74
74
|
// a single-underline via direct formatting, skip the insertion
|
|
75
75
|
// underline so the render doesn't over-paint.
|
|
76
76
|
if (run.underline === "single") {
|
|
77
|
-
return buildFlags(markupMode, authorColor);
|
|
77
|
+
return buildFlags(revision, markupMode, authorColor);
|
|
78
78
|
}
|
|
79
|
-
return buildFlags(markupMode, authorColor, { insertionUnderline: true });
|
|
79
|
+
return buildFlags(revision, markupMode, authorColor, { insertionUnderline: true });
|
|
80
80
|
}
|
|
81
81
|
case "formatting":
|
|
82
82
|
case "property-change":
|
|
@@ -84,20 +84,26 @@ export function applyRevisionDisplay(
|
|
|
84
84
|
// Paragraph-level markers (change-bar, move-from/to arrows) are
|
|
85
85
|
// owned by the paragraph projection. For runs that carry these
|
|
86
86
|
// revision kinds, fall through to the author-color-only posture.
|
|
87
|
-
return buildFlags(markupMode, authorColor);
|
|
87
|
+
return buildFlags(revision, markupMode, authorColor);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
return buildFlags(markupMode, authorColor);
|
|
90
|
+
return buildFlags(revision, markupMode, authorColor);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function buildFlags(
|
|
96
|
+
revision: RevisionRecord,
|
|
96
97
|
markupMode: RevisionMarkupMode,
|
|
97
98
|
authorColor: string | undefined,
|
|
98
|
-
extras: Omit<
|
|
99
|
+
extras: Omit<
|
|
100
|
+
RevisionDisplayFlags,
|
|
101
|
+
"revisionId" | "kind" | "markupMode" | "authorColor"
|
|
102
|
+
> = {},
|
|
99
103
|
): RevisionDisplayFlags {
|
|
100
104
|
return {
|
|
105
|
+
revisionId: revision.changeId,
|
|
106
|
+
kind: revision.kind,
|
|
101
107
|
markupMode,
|
|
102
108
|
...(authorColor !== undefined ? { authorColor } : {}),
|
|
103
109
|
...extras,
|
|
@@ -18,7 +18,11 @@ import type {
|
|
|
18
18
|
WorkflowOverlay,
|
|
19
19
|
} from "./_scope-dependencies.ts";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildParagraphIndexMap,
|
|
23
|
+
buildSectionIndexByBlockIndex,
|
|
24
|
+
compileScope,
|
|
25
|
+
} from "./compile-scope.ts";
|
|
22
26
|
import type { EnumeratedScope } from "./enumerate-scopes.ts";
|
|
23
27
|
import { enumerateScopes } from "./enumerate-scopes.ts";
|
|
24
28
|
import { composeEvidence } from "./evidence.ts";
|
|
@@ -157,6 +161,10 @@ export function compileScopeBundleById(
|
|
|
157
161
|
...(fullDoc ? { document: fullDoc } : {}),
|
|
158
162
|
...(inputs.overlay !== undefined ? { overlay: inputs.overlay } : {}),
|
|
159
163
|
paragraphIndexByBlockIndex: buildParagraphIndexMap(inputs.document),
|
|
164
|
+
// Thread the section-index map up-front so the `kind: "scope"` compile
|
|
165
|
+
// arm does not re-walk the document. Shared between dispatch + the
|
|
166
|
+
// `paragraph`/`heading`/`list-item` + `scope` kinds that read it.
|
|
167
|
+
sectionIndexByBlockIndex: buildSectionIndexByBlockIndex(inputs.document),
|
|
160
168
|
});
|
|
161
169
|
if (!compiled) return null;
|
|
162
170
|
return compileScopeBundle(compiled, { ...inputs, scopes });
|
|
@@ -61,6 +61,7 @@ import { compileHeadingScope } from "./scope-kinds/heading.ts";
|
|
|
61
61
|
import { compileListItemScope } from "./scope-kinds/list-item.ts";
|
|
62
62
|
import { compileParagraphScope } from "./scope-kinds/paragraph.ts";
|
|
63
63
|
import { compileRevisionScope } from "./scope-kinds/revision.ts";
|
|
64
|
+
import { compileScopeKind } from "./scope-kinds/scope.ts";
|
|
64
65
|
import { compileTableScope } from "./scope-kinds/table.ts";
|
|
65
66
|
import { compileTableCellScope } from "./scope-kinds/table-cell.ts";
|
|
66
67
|
import { compileTableRowScope } from "./scope-kinds/table-row.ts";
|
|
@@ -256,6 +257,21 @@ export function compileScope(
|
|
|
256
257
|
return compileCommentThreadScope(entry);
|
|
257
258
|
case "revision":
|
|
258
259
|
return compileRevisionScope(entry);
|
|
260
|
+
case "scope": {
|
|
261
|
+
if (!options.document) return null;
|
|
262
|
+
let scopeSectionMap = options.sectionIndexByBlockIndex;
|
|
263
|
+
if (!scopeSectionMap) {
|
|
264
|
+
scopeSectionMap = buildSectionIndexByBlockIndex(options.document);
|
|
265
|
+
}
|
|
266
|
+
const scopeSectionIndex = scopeSectionMap.get(entry.startBlockIndex);
|
|
267
|
+
return compileScopeKind(entry, {
|
|
268
|
+
document: options.document,
|
|
269
|
+
...(workflow ? { workflow } : {}),
|
|
270
|
+
...(typeof scopeSectionIndex === "number"
|
|
271
|
+
? { sectionIndex: scopeSectionIndex }
|
|
272
|
+
: {}),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
259
275
|
default:
|
|
260
276
|
return null;
|
|
261
277
|
}
|