@beyondwork/docx-react-component 1.0.73 → 1.0.75
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/anchor-conversion.ts +2 -2
- package/src/api/public-types.ts +40 -6
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/workflow.ts +130 -1
- package/src/api/v3/ui/_types.ts +21 -0
- package/src/api/v3/ui/overlays.ts +276 -2
- package/src/api/v3/ui/scope.ts +113 -1
- package/src/compare/diff-engine.ts +1 -2
- package/src/core/commands/index.ts +14 -15
- package/src/core/selection/anchor-conversion.ts +2 -2
- package/src/core/selection/mapping.ts +10 -8
- package/src/core/selection/review-anchors.ts +3 -3
- package/src/io/export/export-session.ts +53 -0
- package/src/io/export/serialize-comments.ts +4 -4
- package/src/io/export/serialize-runtime-revisions.ts +10 -10
- package/src/io/export/split-review-boundaries.ts +4 -4
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +2 -2
- package/src/io/ooxml/parse-comments.ts +2 -2
- package/src/io/ooxml/parse-headers-footers.ts +7 -13
- package/src/io/ooxml/parse-main-document.ts +7 -31
- package/src/io/ooxml/table-opaque-preservation.ts +171 -0
- package/src/model/anchor.ts +9 -1
- package/src/model/canonical-document.ts +76 -3
- package/src/preservation/store.ts +24 -0
- package/src/review/store/comment-anchors.ts +1 -1
- package/src/review/store/comment-remapping.ts +1 -1
- package/src/review/store/revision-actions.ts +4 -4
- package/src/review/store/revision-types.ts +1 -1
- package/src/review/store/scope-tag-diff.ts +1 -1
- package/src/runtime/collab/map-local-selection-on-remote-replay.ts +7 -7
- package/src/runtime/document-runtime.ts +233 -38
- package/src/runtime/formatting/formatting-context.ts +1 -1
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +9 -1
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/scopes/evidence.ts +1 -1
- package/src/runtime/scopes/review-bundle.ts +1 -1
- package/src/runtime/scopes/scope-range.ts +1 -1
- package/src/runtime/selection/post-edit-validator.ts +4 -4
- package/src/runtime/surface-projection.ts +48 -4
- package/src/runtime/workflow/scope-writer.ts +212 -10
- package/src/session/import/review-import.ts +12 -12
- package/src/session/import/workflow-scope-import.ts +9 -8
- package/src/shell/session-bootstrap.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +22 -2
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +5 -2
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +22 -3
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +7 -3
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +5 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +5 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +7 -5
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +99 -43
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +48 -7
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +0 -13
- package/src/ui-tailwind/tw-review-workspace.tsx +13 -41
- package/src/validation/compatibility-engine.ts +1 -1
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +0 -114
- package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +0 -240
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified table-opaque-preservation predicate — shared across
|
|
3
|
+
* `parse-main-document.ts`, `parse-headers-footers.ts`, and
|
|
4
|
+
* `parse-footnotes.ts` so the three parsers agree on which tables can
|
|
5
|
+
* be promoted to the typed `TableNode` shape and which must fall back
|
|
6
|
+
* to `opaque_block` for lossless rawXml round-trip.
|
|
7
|
+
*
|
|
8
|
+
* ## History
|
|
9
|
+
*
|
|
10
|
+
* Before this module existed, each of the three parsers maintained its
|
|
11
|
+
* own divergent allowlist. That divergence drove the coord-01 §11
|
|
12
|
+
* finding (2026-04-24): legacy form fields (`FORMTEXT`,
|
|
13
|
+
* `FORMCHECKBOX`, `FORMDROPDOWN`) classify as `UNKNOWN` under
|
|
14
|
+
* `FIELD_FAMILY_PATTERN` (which targets data-field families), so both
|
|
15
|
+
* the main-story and secondary-story table predicates rejected them
|
|
16
|
+
* even though the body-direct paragraph parser path (via
|
|
17
|
+
* `parseFFDataFromFldChar`) handles them correctly. The result was
|
|
18
|
+
* 58 cell paragraphs lost on APS Short Form because 2 body-direct
|
|
19
|
+
* tables flattened to opaque_block; 5 additional footer tables on
|
|
20
|
+
* the same doc also flatten (those carry `DOCPROPERTY` — a real data
|
|
21
|
+
* field, distinct from the legacy form-field case).
|
|
22
|
+
*
|
|
23
|
+
* ## Contract
|
|
24
|
+
*
|
|
25
|
+
* A `<w:tbl>` is "safe to parse as structured" iff NONE of the
|
|
26
|
+
* following are true:
|
|
27
|
+
*
|
|
28
|
+
* 1. **Revision markup.** `<w:ins>`, `<w:del>`, `<w:moveFrom>`,
|
|
29
|
+
* `<w:moveTo>`, `<w:rPrChange>`, `<w:pPrChange>`,
|
|
30
|
+
* `<w:tblPrChange>`, `<w:trPrChange>`, `<w:tcPrChange>`,
|
|
31
|
+
* `<w:sectPrChange>`, `<w:cellIns>`, `<w:cellDel>`,
|
|
32
|
+
* `<w:cellMerge>`, `<w:smartTag>`.
|
|
33
|
+
* Rationale: tracked-change-aware table editing isn't implemented;
|
|
34
|
+
* flattening preserves fidelity until it is.
|
|
35
|
+
*
|
|
36
|
+
* 2. **Field instruction outside the safe set.** Scan every
|
|
37
|
+
* `w:instr` simple-field attribute + every `<w:instrText>` inside
|
|
38
|
+
* a complex field (`<w:fldChar>begin</w:fldChar>...end`) and
|
|
39
|
+
* classify via `classifyFieldInstruction`. If the classified
|
|
40
|
+
* family isn't in `SAFE_TABLE_FIELD_FAMILIES` AND the instruction
|
|
41
|
+
* isn't a legacy form field (`isLegacyFormFieldInstruction`), the
|
|
42
|
+
* table flattens. The legacy-form-field short-circuit is the
|
|
43
|
+
* coord-01 §11 fix — those tokens are fully supported by the cell
|
|
44
|
+
* paragraph parser but classify as `UNKNOWN`.
|
|
45
|
+
*
|
|
46
|
+
* Extending this contract is a cross-layer architectural decision:
|
|
47
|
+
* widening `SAFE_TABLE_FIELD_FAMILIES` lets more tables parse as
|
|
48
|
+
* structured but commits the edit-command layer (L06/L08) to handle
|
|
49
|
+
* each field family's cell-level edit semantics. Keep this list
|
|
50
|
+
* aligned with the field families supported by the runtime
|
|
51
|
+
* formatting + scope-command pipeline.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Field families safe enough to leave a `<w:tbl>` in structured
|
|
58
|
+
* canonical form. Widening this set commits L06 / L08 to cell-level
|
|
59
|
+
* edit semantics for that family — don't expand opportunistically.
|
|
60
|
+
*/
|
|
61
|
+
export const SAFE_TABLE_FIELD_FAMILIES: ReadonlySet<string> = new Set([
|
|
62
|
+
"REF",
|
|
63
|
+
"PAGEREF",
|
|
64
|
+
"NOTEREF",
|
|
65
|
+
"TOC",
|
|
66
|
+
"PAGE",
|
|
67
|
+
"NUMPAGES",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Revision + structural markup that forces table flattening.
|
|
72
|
+
*/
|
|
73
|
+
const RISKY_TABLE_MARKUP_RE =
|
|
74
|
+
/<w:(ins|del|moveFrom|moveTo|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag)\b/;
|
|
75
|
+
|
|
76
|
+
const SIMPLE_FIELD_INSTR_RE = /\bw:instr="([^"]*)"/g;
|
|
77
|
+
|
|
78
|
+
const COMPLEX_FIELD_TOKEN_RE =
|
|
79
|
+
/<(?:\w+:)?fldChar\b[^>]*?(?:\w+:)?fldCharType="(begin|separate|end)"[^>]*?(?:\/>|>[\s\S]*?<\/(?:\w+:)?fldChar>)|<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Decode the minimal set of XML entities that can appear inside an
|
|
83
|
+
* `instrText` payload. `classifyFieldInstruction` consumes the raw
|
|
84
|
+
* instruction text, so we normalize `&` / `"` / `<` /
|
|
85
|
+
* `>` / `'` before handing it off. Matches the pre-existing
|
|
86
|
+
* behavior in `parse-main-document.ts::extractComplexFieldInstructions`.
|
|
87
|
+
*/
|
|
88
|
+
function decodeXmlEntities(text: string): string {
|
|
89
|
+
return text
|
|
90
|
+
.replace(/</g, "<")
|
|
91
|
+
.replace(/>/g, ">")
|
|
92
|
+
.replace(/"/g, '"')
|
|
93
|
+
.replace(/'/g, "'")
|
|
94
|
+
.replace(/&/g, "&");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract every complex-field instruction (`<w:fldChar
|
|
99
|
+
* fldCharType="begin">...<w:instrText>...</w:instrText>...</w:fldChar
|
|
100
|
+
* fldCharType="end">`) as a single concatenated instruction string.
|
|
101
|
+
* Mirrors `parse-main-document.ts::extractComplexFieldInstructions`.
|
|
102
|
+
*/
|
|
103
|
+
export function extractComplexFieldInstructionsFromRaw(rawXml: string): string[] {
|
|
104
|
+
const instructions: string[] = [];
|
|
105
|
+
let active = "";
|
|
106
|
+
let capturing = false;
|
|
107
|
+
for (const match of rawXml.matchAll(COMPLEX_FIELD_TOKEN_RE)) {
|
|
108
|
+
const [, fldCharType, instrText] = match;
|
|
109
|
+
if (fldCharType === "begin") {
|
|
110
|
+
active = "";
|
|
111
|
+
capturing = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (fldCharType === "separate" || fldCharType === "end") {
|
|
115
|
+
if (capturing && active.trim().length > 0) instructions.push(active);
|
|
116
|
+
active = "";
|
|
117
|
+
capturing = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (capturing && instrText !== undefined) active += decodeXmlEntities(instrText);
|
|
121
|
+
}
|
|
122
|
+
return instructions;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Legacy form fields (ECMA-376 §17.16.21) — `FORMTEXT`, `FORMCHECKBOX`,
|
|
127
|
+
* `FORMDROPDOWN`. These are fully supported by the body-direct
|
|
128
|
+
* paragraph parser via `parseFFDataFromFldChar` but classify as
|
|
129
|
+
* `UNKNOWN` under `FIELD_FAMILY_PATTERN` (which targets data-field
|
|
130
|
+
* families like REF / TOC / MERGEFIELD). Short-circuiting them lets
|
|
131
|
+
* form-field cells stay in structured canonical tables instead of
|
|
132
|
+
* flattening the entire table to `opaque_block`. Coord-01 §11, 2026-04-24.
|
|
133
|
+
*/
|
|
134
|
+
export function isLegacyFormFieldInstruction(instruction: string): boolean {
|
|
135
|
+
return /^\s*(FORMTEXT|FORMCHECKBOX|FORMDROPDOWN)\b/i.test(instruction);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Decides whether a single field instruction (either `w:instr`
|
|
140
|
+
* attribute value or concatenated `instrText` run) is safe for
|
|
141
|
+
* structured-table parsing. Used by the shared predicate below;
|
|
142
|
+
* exposed for direct callers (the debug diagnostics script runs
|
|
143
|
+
* this to classify source instructions alongside the canonical).
|
|
144
|
+
*/
|
|
145
|
+
export function isSafeTableFieldInstruction(instruction: string): boolean {
|
|
146
|
+
if (isLegacyFormFieldInstruction(instruction)) return true;
|
|
147
|
+
const family = classifyFieldInstruction(instruction).family;
|
|
148
|
+
return SAFE_TABLE_FIELD_FAMILIES.has(family);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Shared `<w:tbl>` opaque-preservation predicate. Returns `true`
|
|
153
|
+
* when the table must fall back to `opaque_block` for lossless
|
|
154
|
+
* round-trip, `false` when `parseTable` / `parseSimpleTableElement`
|
|
155
|
+
* can promote it to a structured `TableNode`.
|
|
156
|
+
*
|
|
157
|
+
* Callers (`parse-main-document.ts`, `parse-headers-footers.ts`,
|
|
158
|
+
* `parse-footnotes.ts`) pass the raw XML of the `<w:tbl>` element.
|
|
159
|
+
*/
|
|
160
|
+
export function tableRequiresOpaquePreservation(rawXml: string): boolean {
|
|
161
|
+
if (RISKY_TABLE_MARKUP_RE.test(rawXml)) return true;
|
|
162
|
+
|
|
163
|
+
const simpleInstructions = [...rawXml.matchAll(SIMPLE_FIELD_INSTR_RE)].map(
|
|
164
|
+
(match) => match[1] ?? "",
|
|
165
|
+
);
|
|
166
|
+
const complexInstructions = extractComplexFieldInstructionsFromRaw(rawXml);
|
|
167
|
+
for (const instruction of [...simpleInstructions, ...complexInstructions]) {
|
|
168
|
+
if (!isSafeTableFieldInstruction(instruction)) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
package/src/model/anchor.ts
CHANGED
|
@@ -25,9 +25,17 @@ export interface BoundaryAssoc {
|
|
|
25
25
|
end: Assoc;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Range anchor — flat shape `{ kind: "range", from, to, assoc }` after the
|
|
30
|
+
* 2026-04-23 flat-wins collapse. Prior to the collapse this carried a nested
|
|
31
|
+
* `{ range: DocRange, assoc }` shape. See `canonical-document.ts` CanonicalAnchor
|
|
32
|
+
* for the matching canonical shape + `repairCanonicalDocumentEnvelope` for the
|
|
33
|
+
* persisted-snapshot legacy-to-flat migration.
|
|
34
|
+
*/
|
|
28
35
|
export interface RangeAnchor {
|
|
29
36
|
kind: "range";
|
|
30
|
-
|
|
37
|
+
from: Position;
|
|
38
|
+
to: Position;
|
|
31
39
|
assoc: BoundaryAssoc;
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -2013,7 +2013,8 @@ export interface BoundaryAssoc {
|
|
|
2013
2013
|
export type CanonicalAnchor =
|
|
2014
2014
|
| {
|
|
2015
2015
|
kind: "range";
|
|
2016
|
-
|
|
2016
|
+
from: number;
|
|
2017
|
+
to: number;
|
|
2017
2018
|
assoc: BoundaryAssoc;
|
|
2018
2019
|
}
|
|
2019
2020
|
| {
|
|
@@ -2329,7 +2330,7 @@ export function repairCanonicalDocumentEnvelope(
|
|
|
2329
2330
|
review:
|
|
2330
2331
|
record.review === undefined
|
|
2331
2332
|
? base.review
|
|
2332
|
-
: (record.review as CanonicalDocument["review"]),
|
|
2333
|
+
: (migrateLegacyReviewAnchors(record.review) as CanonicalDocument["review"]),
|
|
2333
2334
|
preservation:
|
|
2334
2335
|
record.preservation === undefined
|
|
2335
2336
|
? base.preservation
|
|
@@ -2355,6 +2356,67 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
2355
2356
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2356
2357
|
}
|
|
2357
2358
|
|
|
2359
|
+
/**
|
|
2360
|
+
* Schema-compat shim for the 2026-04-23 flat-wins anchor collapse. Legacy
|
|
2361
|
+
* persisted snapshots carry range anchors as `{kind: "range", range: {from, to},
|
|
2362
|
+
* assoc}`. Rewrite those to the flat shape `{kind: "range", from, to, assoc}`
|
|
2363
|
+
* on rehydration so old snapshots continue to load.
|
|
2364
|
+
*/
|
|
2365
|
+
export function migrateLegacyReviewAnchors(review: unknown): unknown {
|
|
2366
|
+
if (!isPlainRecord(review)) {
|
|
2367
|
+
return review;
|
|
2368
|
+
}
|
|
2369
|
+
const migrated: Record<string, unknown> = { ...review };
|
|
2370
|
+
if (isPlainRecord(review.comments)) {
|
|
2371
|
+
migrated.comments = Object.fromEntries(
|
|
2372
|
+
Object.entries(review.comments).map(([id, record]) => [
|
|
2373
|
+
id,
|
|
2374
|
+
migrateReviewRecordAnchor(record),
|
|
2375
|
+
]),
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
if (isPlainRecord(review.revisions)) {
|
|
2379
|
+
migrated.revisions = Object.fromEntries(
|
|
2380
|
+
Object.entries(review.revisions).map(([id, record]) => [
|
|
2381
|
+
id,
|
|
2382
|
+
migrateReviewRecordAnchor(record),
|
|
2383
|
+
]),
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
return migrated;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function migrateReviewRecordAnchor(record: unknown): unknown {
|
|
2390
|
+
if (!isPlainRecord(record)) {
|
|
2391
|
+
return record;
|
|
2392
|
+
}
|
|
2393
|
+
const anchor = record.anchor;
|
|
2394
|
+
if (!isPlainRecord(anchor) || anchor.kind !== "range") {
|
|
2395
|
+
return record;
|
|
2396
|
+
}
|
|
2397
|
+
if (typeof anchor.from === "number" && typeof anchor.to === "number") {
|
|
2398
|
+
return record;
|
|
2399
|
+
}
|
|
2400
|
+
const nested = anchor.range;
|
|
2401
|
+
if (
|
|
2402
|
+
isPlainRecord(nested) &&
|
|
2403
|
+
typeof nested.from === "number" &&
|
|
2404
|
+
typeof nested.to === "number"
|
|
2405
|
+
) {
|
|
2406
|
+
const { range: _range, ...rest } = anchor;
|
|
2407
|
+
void _range;
|
|
2408
|
+
return {
|
|
2409
|
+
...record,
|
|
2410
|
+
anchor: {
|
|
2411
|
+
...rest,
|
|
2412
|
+
from: nested.from,
|
|
2413
|
+
to: nested.to,
|
|
2414
|
+
},
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
return record;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2358
2420
|
export function serializeCanonicalDocument(document: CanonicalDocument): string {
|
|
2359
2421
|
assertCanonicalDocument(document);
|
|
2360
2422
|
return stableStringify(document);
|
|
@@ -3507,7 +3569,18 @@ function validateAnchor(
|
|
|
3507
3569
|
}
|
|
3508
3570
|
|
|
3509
3571
|
if (kind === "range") {
|
|
3510
|
-
|
|
3572
|
+
if (typeof record.from !== "number") {
|
|
3573
|
+
issues.push({
|
|
3574
|
+
path: `${path}.from`,
|
|
3575
|
+
message: "range anchor from must be a number.",
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
if (typeof record.to !== "number") {
|
|
3579
|
+
issues.push({
|
|
3580
|
+
path: `${path}.to`,
|
|
3581
|
+
message: "range anchor to must be a number.",
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3511
3584
|
validateBoundaryAssoc(record.assoc, `${path}.assoc`, issues);
|
|
3512
3585
|
} else if (kind === "node") {
|
|
3513
3586
|
if (typeof record.at !== "number") {
|
|
@@ -170,6 +170,15 @@ export function describeStructuredWrapperBlock(
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
if (block.properties.sdtType === "docPartObj") {
|
|
173
|
+
// coord-02 §11 P0 — a Template content control that wraps a
|
|
174
|
+
// drawing_frame (CCEP header logos, cover hero photos) must stay
|
|
175
|
+
// recursable so surface-projection emits the inner image segment.
|
|
176
|
+
// Collapsing to opaque_block here silently drops every image
|
|
177
|
+
// inside a docPartObj SDT. Only collapse when the content is
|
|
178
|
+
// genuinely wrapper-heavy with no user-visible media.
|
|
179
|
+
if (sdtContainsDrawingFrame(block)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
173
182
|
return {
|
|
174
183
|
featureKey: "content-controls",
|
|
175
184
|
label: "Template content control",
|
|
@@ -220,6 +229,21 @@ function createDetail(fragment: OpaqueFragmentRecord): string {
|
|
|
220
229
|
: "Preserved whole-unit to keep unsupported OOXML intact.";
|
|
221
230
|
}
|
|
222
231
|
|
|
232
|
+
function sdtContainsDrawingFrame(block: Extract<BlockNode, { type: "sdt" }>): boolean {
|
|
233
|
+
const visit = (node: unknown): boolean => {
|
|
234
|
+
if (!node || typeof node !== "object") return false;
|
|
235
|
+
const n = node as { type?: string; children?: readonly unknown[] };
|
|
236
|
+
if (n.type === "drawing_frame") return true;
|
|
237
|
+
if (Array.isArray(n.children)) {
|
|
238
|
+
for (const child of n.children) {
|
|
239
|
+
if (visit(child)) return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
return block.children.some(visit);
|
|
245
|
+
}
|
|
246
|
+
|
|
223
247
|
function isTocContentControl(block: Extract<BlockNode, { type: "sdt" }>): boolean {
|
|
224
248
|
const searchText = [
|
|
225
249
|
block.properties.alias,
|
|
@@ -98,7 +98,7 @@ function normalizeCommentAnchor(
|
|
|
98
98
|
|
|
99
99
|
if (
|
|
100
100
|
mappedAnchor.kind === "range" &&
|
|
101
|
-
!rangeStaysWithinCommentableStory(nextContent, mappedAnchor.
|
|
101
|
+
!rangeStaysWithinCommentableStory(nextContent, { from: mappedAnchor.from, to: mappedAnchor.to })
|
|
102
102
|
) {
|
|
103
103
|
return detachReviewAnchor(previousRange, "invalidatedByStructureChange");
|
|
104
104
|
}
|
|
@@ -153,8 +153,8 @@ export function applyRevisionAction(
|
|
|
153
153
|
|
|
154
154
|
const story = parseTextStory(options.document.content);
|
|
155
155
|
const range = normalizeRange(
|
|
156
|
-
revision.anchor.
|
|
157
|
-
revision.anchor.
|
|
156
|
+
revision.anchor.from,
|
|
157
|
+
revision.anchor.to,
|
|
158
158
|
);
|
|
159
159
|
|
|
160
160
|
if (range.to > story.size) {
|
|
@@ -811,8 +811,8 @@ function resolveParagraphMarkDeletionRange(
|
|
|
811
811
|
|
|
812
812
|
const paragraphs = mapParagraphRanges(story);
|
|
813
813
|
const anchorPosition = normalizeRange(
|
|
814
|
-
revision.anchor.
|
|
815
|
-
revision.anchor.
|
|
814
|
+
revision.anchor.from,
|
|
815
|
+
revision.anchor.to,
|
|
816
816
|
).from;
|
|
817
817
|
const paragraph = paragraphs.find(
|
|
818
818
|
(candidate) =>
|
|
@@ -96,7 +96,7 @@ export function collectScopeTagTouches(
|
|
|
96
96
|
|
|
97
97
|
function anchorRange(anchor: CanonicalAnchor): { from: number; to: number } {
|
|
98
98
|
if (anchor.kind === "range") {
|
|
99
|
-
return { from: anchor.
|
|
99
|
+
return { from: anchor.from, to: anchor.to };
|
|
100
100
|
}
|
|
101
101
|
if (anchor.kind === "node") {
|
|
102
102
|
return { from: anchor.at, to: anchor.at };
|
|
@@ -88,10 +88,10 @@ export function mapLocalSelectionOnRemoteReplay(
|
|
|
88
88
|
|
|
89
89
|
if (active.kind === "range") {
|
|
90
90
|
const directionForward = selection.anchor <= selection.head;
|
|
91
|
-
const isCollapsed = active.
|
|
91
|
+
const isCollapsed = active.from === active.to;
|
|
92
92
|
|
|
93
93
|
if (isCollapsed) {
|
|
94
|
-
const collapsedAt = mapPosition(active.
|
|
94
|
+
const collapsedAt = mapPosition(active.from, 1, mapping).position;
|
|
95
95
|
return {
|
|
96
96
|
anchor: collapsedAt,
|
|
97
97
|
head: collapsedAt,
|
|
@@ -107,7 +107,7 @@ export function mapLocalSelectionOnRemoteReplay(
|
|
|
107
107
|
const mapped = mapAnchor(active, mapping);
|
|
108
108
|
|
|
109
109
|
if (mapped.kind === "range") {
|
|
110
|
-
const { from, to } = mapped
|
|
110
|
+
const { from, to } = mapped;
|
|
111
111
|
return {
|
|
112
112
|
anchor: directionForward ? from : to,
|
|
113
113
|
head: directionForward ? to : from,
|
|
@@ -122,7 +122,7 @@ export function mapLocalSelectionOnRemoteReplay(
|
|
|
122
122
|
// positions that land inside a deleted span to `step.from +
|
|
123
123
|
// insertSize`, which is the correct logical "where the selection
|
|
124
124
|
// used to start" position.
|
|
125
|
-
const collapsedAt = mapPosition(active.
|
|
125
|
+
const collapsedAt = mapPosition(active.from, 1, mapping).position;
|
|
126
126
|
return {
|
|
127
127
|
anchor: collapsedAt,
|
|
128
128
|
head: collapsedAt,
|
|
@@ -147,9 +147,9 @@ export function mapLocalSelectionOnRemoteReplay(
|
|
|
147
147
|
}
|
|
148
148
|
if (mapped.kind === "range") {
|
|
149
149
|
return {
|
|
150
|
-
anchor: mapped.
|
|
151
|
-
head: mapped.
|
|
152
|
-
isCollapsed: mapped.
|
|
150
|
+
anchor: mapped.from,
|
|
151
|
+
head: mapped.to,
|
|
152
|
+
isCollapsed: mapped.from === mapped.to,
|
|
153
153
|
activeRange: mapped,
|
|
154
154
|
};
|
|
155
155
|
}
|