@beyondwork/docx-react-component 1.0.14 → 1.0.16
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 +43 -24
- package/src/api/public-types.ts +15 -0
- package/src/compare/diff-engine.ts +84 -7
- package/src/compare/index.ts +25 -0
- package/src/compare/snapshot.ts +31 -0
- package/src/core/commands/formatting-commands.ts +225 -0
- package/src/formats/xlsx/io/serialize-shared-strings.ts +72 -0
- package/src/formats/xlsx/io/serialize-sheet.ts +333 -0
- package/src/formats/xlsx/io/serialize-styles.ts +98 -0
- package/src/formats/xlsx/io/serialize-workbook.ts +429 -0
- package/src/formats/xlsx/runtime/cell-commands.ts +567 -0
- package/src/formats/xlsx/runtime/sheet-commands.ts +206 -0
- package/src/formats/xlsx/runtime/workbook-runtime.ts +177 -0
- package/src/formats/xlsx/runtime/workbook-transaction.ts +822 -0
- package/src/io/ooxml/parse-main-document.ts +6 -6
- package/src/io/ooxml/parse-revisions.ts +18 -24
- package/src/legal/bookmarks.ts +35 -0
- package/src/legal/index.ts +32 -0
- package/src/legal/signature-blocks.ts +259 -0
- package/src/runtime/document-runtime.ts +43 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/surface-projection.ts +292 -9
- package/src/ui/WordReviewEditor.tsx +107 -4
- package/src/ui-tailwind/editor-surface/pm-schema.ts +148 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +15 -29
|
@@ -754,17 +754,17 @@ function readTableGridColumns(node: XmlElementNode): number[] {
|
|
|
754
754
|
* Check if a table's raw XML contains content that cannot safely round-trip
|
|
755
755
|
* through the parsed table path yet. This includes:
|
|
756
756
|
* - Revision markup (tracked changes inside cells)
|
|
757
|
-
* -
|
|
758
|
-
* -
|
|
757
|
+
* - Field syntax and smart tags that still lack a safe table-local live model
|
|
758
|
+
* - Grid geometry tags that the table serializer does not reconstruct yet
|
|
759
759
|
*
|
|
760
760
|
* Tables matching this check stay opaque until the respective features
|
|
761
761
|
* are implemented in the table editing path.
|
|
762
762
|
*/
|
|
763
763
|
function tableRequiresOpaquePreservation(rawXml: string): boolean {
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
return /<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|
|
|
764
|
+
// Safe table-local content now includes hyperlinks, bookmarks, comments,
|
|
765
|
+
// nested tables, floating images, and VML preview atoms because the parser
|
|
766
|
+
// and serializer can preserve them without degrading the whole table.
|
|
767
|
+
return /<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|fldChar|fldSimple|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml);
|
|
768
768
|
}
|
|
769
769
|
|
|
770
770
|
function readCellGridSpan(node: XmlElementNode): number | undefined {
|
|
@@ -68,7 +68,7 @@ interface RevisionMetadata {
|
|
|
68
68
|
|
|
69
69
|
const SUPPORTED_CONTAINER_TYPES = new Set(["ins", "del"]);
|
|
70
70
|
const PRESERVE_ONLY_CONTAINER_TYPES = new Set(["moveFrom", "moveTo"]);
|
|
71
|
-
const
|
|
71
|
+
const PROPERTY_CHANGE_REVISION_TYPES = new Set(["rPrChange", "pPrChange"]);
|
|
72
72
|
|
|
73
73
|
export function parseRevisionsFromDocumentXml(
|
|
74
74
|
documentXml: string,
|
|
@@ -158,16 +158,17 @@ function parseParagraphMarkRevisions(
|
|
|
158
158
|
|
|
159
159
|
const type = localName(child.name);
|
|
160
160
|
if (type === "pPrChange") {
|
|
161
|
-
const metadata = readRevisionMetadata(child, state, "
|
|
161
|
+
const metadata = readRevisionMetadata(child, state, "property-change");
|
|
162
162
|
const innerPPr = findChildElement(child, "pPr");
|
|
163
|
+
const beforeXml = innerPPr ? state.documentXml.slice(innerPPr.start, innerPPr.end) : "";
|
|
163
164
|
const propertyChangeData: PropertyChangeData = {
|
|
164
165
|
xmlTag: "pPrChange",
|
|
165
|
-
beforeXml
|
|
166
|
+
beforeXml,
|
|
166
167
|
};
|
|
167
168
|
state.revisions.push(
|
|
168
169
|
createRevisionRecord({
|
|
169
170
|
revisionId: metadata.revisionId,
|
|
170
|
-
kind: "
|
|
171
|
+
kind: "property-change",
|
|
171
172
|
anchor: paragraphRange,
|
|
172
173
|
authorId: metadata.authorId,
|
|
173
174
|
createdAt: metadata.createdAt,
|
|
@@ -175,7 +176,6 @@ function parseParagraphMarkRevisions(
|
|
|
175
176
|
source: "import",
|
|
176
177
|
originalRevisionType: "pPrChange",
|
|
177
178
|
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
178
|
-
preserveOnlyReason: "Imported preserve-only revision.",
|
|
179
179
|
propertyChangeData,
|
|
180
180
|
},
|
|
181
181
|
}),
|
|
@@ -186,12 +186,9 @@ function parseParagraphMarkRevisions(
|
|
|
186
186
|
xmlStart: child.start,
|
|
187
187
|
xmlEnd: child.end,
|
|
188
188
|
originalRevisionType: "pPrChange",
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
code: "preserve_only_formatting_revision",
|
|
193
|
-
message: "Paragraph property revisions remain preserve-only for Wave 6.",
|
|
194
|
-
featureClass: "preserve-only",
|
|
189
|
+
containerXmlStart: paragraphProperties.start,
|
|
190
|
+
containerXmlEnd: paragraphProperties.end,
|
|
191
|
+
beforeContainerXml: beforeXml,
|
|
195
192
|
});
|
|
196
193
|
}
|
|
197
194
|
}
|
|
@@ -356,7 +353,7 @@ function walkContentNode(
|
|
|
356
353
|
state.diagnostics.push({
|
|
357
354
|
revisionId: metadata.revisionId,
|
|
358
355
|
code: "preserve_only_move_revision",
|
|
359
|
-
message: "Tracked move revisions
|
|
356
|
+
message: "Tracked move revisions are preserve-only. Content is preserved for round-trip but cannot be accepted or rejected.",
|
|
360
357
|
featureClass: "preserve-only",
|
|
361
358
|
});
|
|
362
359
|
advanceCursor(node, setCursor, getCursor);
|
|
@@ -432,21 +429,22 @@ function parseRunFormattingRevisions(
|
|
|
432
429
|
: createDetachedAnchor({ from: runStart, to: runStart }, "importAmbiguity");
|
|
433
430
|
|
|
434
431
|
for (const child of runProperties.children) {
|
|
435
|
-
if (child.type !== "element" || !
|
|
432
|
+
if (child.type !== "element" || !PROPERTY_CHANGE_REVISION_TYPES.has(localName(child.name))) {
|
|
436
433
|
continue;
|
|
437
434
|
}
|
|
438
435
|
|
|
439
436
|
const childLocalName = localName(child.name) as "rPrChange" | "pPrChange";
|
|
440
|
-
const metadata = readRevisionMetadata(child, state, "
|
|
437
|
+
const metadata = readRevisionMetadata(child, state, "property-change");
|
|
441
438
|
const innerRPr = findChildElement(child, "rPr");
|
|
439
|
+
const beforeXml = innerRPr ? state.documentXml.slice(innerRPr.start, innerRPr.end) : "";
|
|
442
440
|
const propertyChangeData: PropertyChangeData = {
|
|
443
441
|
xmlTag: childLocalName === "rPrChange" ? "rPrChange" : "pPrChange",
|
|
444
|
-
beforeXml
|
|
442
|
+
beforeXml,
|
|
445
443
|
};
|
|
446
444
|
state.revisions.push(
|
|
447
445
|
createRevisionRecord({
|
|
448
446
|
revisionId: metadata.revisionId,
|
|
449
|
-
kind: "
|
|
447
|
+
kind: "property-change",
|
|
450
448
|
anchor,
|
|
451
449
|
authorId: metadata.authorId,
|
|
452
450
|
createdAt: metadata.createdAt,
|
|
@@ -454,7 +452,6 @@ function parseRunFormattingRevisions(
|
|
|
454
452
|
source: "import",
|
|
455
453
|
originalRevisionType: childLocalName,
|
|
456
454
|
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
457
|
-
preserveOnlyReason: "Imported preserve-only revision.",
|
|
458
455
|
propertyChangeData,
|
|
459
456
|
},
|
|
460
457
|
}),
|
|
@@ -465,12 +462,9 @@ function parseRunFormattingRevisions(
|
|
|
465
462
|
xmlStart: child.start,
|
|
466
463
|
xmlEnd: child.end,
|
|
467
464
|
originalRevisionType: childLocalName,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
code: "preserve_only_formatting_revision",
|
|
472
|
-
message: "Formatting revisions remain preserve-only for Wave 6.",
|
|
473
|
-
featureClass: "preserve-only",
|
|
465
|
+
containerXmlStart: runProperties.start,
|
|
466
|
+
containerXmlEnd: runProperties.end,
|
|
467
|
+
beforeContainerXml: beforeXml,
|
|
474
468
|
});
|
|
475
469
|
}
|
|
476
470
|
}
|
|
@@ -616,7 +610,7 @@ function containsNestedRevision(node: XmlElementNode): boolean {
|
|
|
616
610
|
child.type === "element" &&
|
|
617
611
|
(SUPPORTED_CONTAINER_TYPES.has(localName(child.name)) ||
|
|
618
612
|
PRESERVE_ONLY_CONTAINER_TYPES.has(localName(child.name)) ||
|
|
619
|
-
|
|
613
|
+
PROPERTY_CHANGE_REVISION_TYPES.has(localName(child.name)) ||
|
|
620
614
|
containsNestedRevision(child)),
|
|
621
615
|
);
|
|
622
616
|
}
|
package/src/legal/bookmarks.ts
CHANGED
|
@@ -167,6 +167,41 @@ export function isHiddenBookmarkName(name: string | undefined): boolean {
|
|
|
167
167
|
return Boolean(name && name.startsWith("_"));
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
export interface BookmarkIntegrityResult {
|
|
171
|
+
totalCount: number;
|
|
172
|
+
pairedCount: number;
|
|
173
|
+
startOnlyCount: number;
|
|
174
|
+
endOnlyCount: number;
|
|
175
|
+
hiddenCount: number;
|
|
176
|
+
namedCount: number;
|
|
177
|
+
integrityScore: "complete" | "partial" | "degraded";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function assessBookmarkIntegrity(bookmarks: LegalBookmark[]): BookmarkIntegrityResult {
|
|
181
|
+
const pairedCount = bookmarks.filter((b) => b.status === "paired").length;
|
|
182
|
+
const startOnlyCount = bookmarks.filter((b) => b.status === "start-only").length;
|
|
183
|
+
const endOnlyCount = bookmarks.filter((b) => b.status === "end-only").length;
|
|
184
|
+
const hiddenCount = bookmarks.filter((b) => b.hidden).length;
|
|
185
|
+
const namedCount = bookmarks.filter((b) => b.name && !b.hidden).length;
|
|
186
|
+
const totalCount = bookmarks.length;
|
|
187
|
+
const unpairedCount = startOnlyCount + endOnlyCount;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
totalCount,
|
|
191
|
+
pairedCount,
|
|
192
|
+
startOnlyCount,
|
|
193
|
+
endOnlyCount,
|
|
194
|
+
hiddenCount,
|
|
195
|
+
namedCount,
|
|
196
|
+
integrityScore:
|
|
197
|
+
unpairedCount === 0
|
|
198
|
+
? "complete"
|
|
199
|
+
: unpairedCount <= pairedCount
|
|
200
|
+
? "partial"
|
|
201
|
+
: "degraded",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
170
205
|
function compareBookmarks(left: LegalBookmark, right: LegalBookmark): number {
|
|
171
206
|
return (
|
|
172
207
|
(left.startIndex ?? left.endIndex ?? Number.MAX_SAFE_INTEGER) -
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
parseBookmarksFromDocumentXml,
|
|
3
|
+
collectBookmarksFromCanonicalDocument,
|
|
4
|
+
assessBookmarkIntegrity,
|
|
5
|
+
isHiddenBookmarkName,
|
|
6
|
+
type BookmarkIntegrityResult,
|
|
7
|
+
type LegalBookmark,
|
|
8
|
+
} from "./bookmarks.ts";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
parseCrossReferencesFromDocumentXml,
|
|
12
|
+
collectCrossReferencesFromCanonicalDocument,
|
|
13
|
+
detectCrossReferencePatterns,
|
|
14
|
+
parseFieldReferenceInstruction,
|
|
15
|
+
type CrossReference,
|
|
16
|
+
type CrossReferencePattern,
|
|
17
|
+
} from "./cross-references.ts";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
collectDefinedTermsFromDocumentXml,
|
|
21
|
+
collectDefinedTermsFromCanonicalDocument,
|
|
22
|
+
buildDefinedTermCatalog,
|
|
23
|
+
type DefinedTerm,
|
|
24
|
+
type DefinedTermOccurrence,
|
|
25
|
+
} from "./defined-terms.ts";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
detectSignatureBlocksFromCanonicalDocument,
|
|
29
|
+
type SignatureBlockCandidate,
|
|
30
|
+
type SignatureBlockReport,
|
|
31
|
+
type SignatureParty,
|
|
32
|
+
} from "./signature-blocks.ts";
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockNode,
|
|
3
|
+
CanonicalDocument,
|
|
4
|
+
DocumentNode,
|
|
5
|
+
ParagraphNode,
|
|
6
|
+
} from "../model/canonical-document.ts";
|
|
7
|
+
|
|
8
|
+
export interface SignatureBlockCandidate {
|
|
9
|
+
startIndex: number;
|
|
10
|
+
endIndex: number;
|
|
11
|
+
kind: "execution-block" | "witness-block" | "notary-block";
|
|
12
|
+
parties: SignatureParty[];
|
|
13
|
+
confidence: "high" | "medium";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SignatureParty {
|
|
17
|
+
roleLabel?: string;
|
|
18
|
+
nameLabel?: string;
|
|
19
|
+
paragraphIndex: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SignatureBlockReport {
|
|
23
|
+
reportVersion: "signature-block-report/1";
|
|
24
|
+
candidates: SignatureBlockCandidate[];
|
|
25
|
+
preservationSafe: boolean;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EXECUTION_TRIGGER_PATTERN =
|
|
30
|
+
/\b(?:IN WITNESS WHEREOF|EXECUTED|AGREED AND ACCEPTED|SIGNATURES FOLLOW)\b/i;
|
|
31
|
+
const ROLE_LINE_PATTERN =
|
|
32
|
+
/^\s*(?:By|Name|Title|Date|Witness|Authorized Signatory|Signature)\s*[:\-]?\s*/i;
|
|
33
|
+
const PARTY_LABEL_PATTERN =
|
|
34
|
+
/^(?:(?:The |)[A-Z][A-Za-z0-9 ,.'&()-]{2,60})\s*$/;
|
|
35
|
+
const WITNESS_PATTERN = /\bWITNESS(?:ED|ES)?\b/i;
|
|
36
|
+
const NOTARY_PATTERN = /\bNOTARY\s+PUBLIC\b/i;
|
|
37
|
+
const UNDERLINE_PLACEHOLDER_PATTERN = /_{4,}|\.{4,}/;
|
|
38
|
+
|
|
39
|
+
export function detectSignatureBlocksFromCanonicalDocument(
|
|
40
|
+
document: Pick<CanonicalDocument, "content" | "preservation"> | DocumentNode,
|
|
41
|
+
): SignatureBlockReport {
|
|
42
|
+
const root = "content" in document ? document.content : document;
|
|
43
|
+
const paragraphs: Array<{ text: string; node: ParagraphNode }> = [];
|
|
44
|
+
const warnings: string[] = [];
|
|
45
|
+
|
|
46
|
+
walkBlocks(root, (node) => {
|
|
47
|
+
if (node.type === "paragraph") {
|
|
48
|
+
paragraphs.push({ text: flattenParagraphText(node), node });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const candidates = findSignatureBlockCandidates(paragraphs);
|
|
53
|
+
|
|
54
|
+
const hasPreservation =
|
|
55
|
+
"preservation" in document &&
|
|
56
|
+
document.preservation !== undefined;
|
|
57
|
+
const preservationSafe = hasPreservation
|
|
58
|
+
? !hasOpaqueFragmentsInSignatureRanges(
|
|
59
|
+
document as Pick<CanonicalDocument, "content" | "preservation">,
|
|
60
|
+
candidates,
|
|
61
|
+
)
|
|
62
|
+
: true;
|
|
63
|
+
|
|
64
|
+
if (!preservationSafe) {
|
|
65
|
+
warnings.push(
|
|
66
|
+
"One or more signature block regions overlap with preserve-only opaque fragments. Export fidelity may be degraded.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
reportVersion: "signature-block-report/1",
|
|
72
|
+
candidates,
|
|
73
|
+
preservationSafe,
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findSignatureBlockCandidates(
|
|
79
|
+
paragraphs: ReadonlyArray<{ text: string; node: ParagraphNode }>,
|
|
80
|
+
): SignatureBlockCandidate[] {
|
|
81
|
+
const candidates: SignatureBlockCandidate[] = [];
|
|
82
|
+
let index = 0;
|
|
83
|
+
|
|
84
|
+
while (index < paragraphs.length) {
|
|
85
|
+
const text = paragraphs[index].text;
|
|
86
|
+
|
|
87
|
+
if (EXECUTION_TRIGGER_PATTERN.test(text)) {
|
|
88
|
+
const block = scanSignatureBlock(paragraphs, index);
|
|
89
|
+
if (block) {
|
|
90
|
+
candidates.push(block);
|
|
91
|
+
index = block.endIndex + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return candidates;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scanSignatureBlock(
|
|
103
|
+
paragraphs: ReadonlyArray<{ text: string; node: ParagraphNode }>,
|
|
104
|
+
triggerIndex: number,
|
|
105
|
+
): SignatureBlockCandidate | undefined {
|
|
106
|
+
const parties: SignatureParty[] = [];
|
|
107
|
+
let endIndex = triggerIndex;
|
|
108
|
+
let kind: SignatureBlockCandidate["kind"] = "execution-block";
|
|
109
|
+
let hasSignatureContent = false;
|
|
110
|
+
|
|
111
|
+
for (
|
|
112
|
+
let index = triggerIndex + 1;
|
|
113
|
+
index < paragraphs.length && index <= triggerIndex + 40;
|
|
114
|
+
index += 1
|
|
115
|
+
) {
|
|
116
|
+
const text = paragraphs[index].text.trim();
|
|
117
|
+
|
|
118
|
+
if (text.length === 0) {
|
|
119
|
+
endIndex = index;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (WITNESS_PATTERN.test(text)) {
|
|
124
|
+
kind = "witness-block";
|
|
125
|
+
}
|
|
126
|
+
if (NOTARY_PATTERN.test(text)) {
|
|
127
|
+
kind = "notary-block";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (ROLE_LINE_PATTERN.test(text)) {
|
|
131
|
+
hasSignatureContent = true;
|
|
132
|
+
endIndex = index;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (UNDERLINE_PLACEHOLDER_PATTERN.test(text)) {
|
|
137
|
+
hasSignatureContent = true;
|
|
138
|
+
endIndex = index;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (PARTY_LABEL_PATTERN.test(text) && !isBodyParagraph(text)) {
|
|
143
|
+
parties.push({
|
|
144
|
+
roleLabel: text.trim(),
|
|
145
|
+
paragraphIndex: index,
|
|
146
|
+
});
|
|
147
|
+
hasSignatureContent = true;
|
|
148
|
+
endIndex = index;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (hasSignatureContent && isBodyParagraph(text)) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
endIndex = index;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!hasSignatureContent) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
startIndex: triggerIndex,
|
|
165
|
+
endIndex,
|
|
166
|
+
kind,
|
|
167
|
+
parties,
|
|
168
|
+
confidence: parties.length > 0 ? "high" : "medium",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isBodyParagraph(text: string): boolean {
|
|
173
|
+
return text.length > 120 && /[.;]/.test(text);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasOpaqueFragmentsInSignatureRanges(
|
|
177
|
+
document: Pick<CanonicalDocument, "content" | "preservation">,
|
|
178
|
+
candidates: readonly SignatureBlockCandidate[],
|
|
179
|
+
): boolean {
|
|
180
|
+
if (candidates.length === 0) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const opaqueCount = Object.keys(document.preservation.opaqueFragments).length;
|
|
185
|
+
if (opaqueCount === 0) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let paragraphIndex = 0;
|
|
190
|
+
const opaqueIndices = new Set<number>();
|
|
191
|
+
|
|
192
|
+
for (const block of document.content.children) {
|
|
193
|
+
if (block.type === "paragraph") {
|
|
194
|
+
paragraphIndex += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (block.type === "opaque_block") {
|
|
198
|
+
opaqueIndices.add(paragraphIndex);
|
|
199
|
+
}
|
|
200
|
+
paragraphIndex += 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return candidates.some((candidate) => {
|
|
204
|
+
for (let index = candidate.startIndex; index <= candidate.endIndex; index += 1) {
|
|
205
|
+
if (opaqueIndices.has(index)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function flattenParagraphText(paragraph: ParagraphNode): string {
|
|
214
|
+
return paragraph.children
|
|
215
|
+
.map((child) => {
|
|
216
|
+
switch (child.type) {
|
|
217
|
+
case "text":
|
|
218
|
+
return child.text;
|
|
219
|
+
case "hyperlink":
|
|
220
|
+
case "field":
|
|
221
|
+
return child.children
|
|
222
|
+
.map((nested) =>
|
|
223
|
+
nested.type === "text" ? nested.text : nested.type === "tab" ? "\t" : "",
|
|
224
|
+
)
|
|
225
|
+
.join("");
|
|
226
|
+
case "tab":
|
|
227
|
+
return "\t";
|
|
228
|
+
case "hard_break":
|
|
229
|
+
case "column_break":
|
|
230
|
+
return "\n";
|
|
231
|
+
default:
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.join("");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function walkBlocks(
|
|
239
|
+
node: DocumentNode,
|
|
240
|
+
visit: (node: DocumentNode) => void,
|
|
241
|
+
): void {
|
|
242
|
+
visit(node);
|
|
243
|
+
|
|
244
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
245
|
+
for (const child of node.children) {
|
|
246
|
+
walkBlocks(child, visit);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (node.type === "table") {
|
|
251
|
+
for (const row of node.rows) {
|
|
252
|
+
walkBlocks(row, visit);
|
|
253
|
+
}
|
|
254
|
+
} else if (node.type === "table_row") {
|
|
255
|
+
for (const cell of node.cells) {
|
|
256
|
+
walkBlocks(cell, visit);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createEditorState,
|
|
3
|
+
createSelectionSnapshot,
|
|
3
4
|
createPersistedEditorSnapshot,
|
|
4
5
|
deriveDocumentStats,
|
|
5
6
|
type CanonicalDocumentEnvelope,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
type EditorCommand,
|
|
36
37
|
type EditorTransaction,
|
|
37
38
|
} from "../core/commands/index.ts";
|
|
39
|
+
import { insertText } from "../core/commands/text-commands.ts";
|
|
38
40
|
import {
|
|
39
41
|
createDetachedAnchor,
|
|
40
42
|
createNodeAnchor,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
52
54
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
53
55
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
56
|
+
import { getFormattingStateFromRenderSnapshot } from "../core/commands/formatting-commands.ts";
|
|
54
57
|
|
|
55
58
|
export type Unsubscribe = () => void;
|
|
56
59
|
|
|
@@ -58,11 +61,13 @@ export interface DocumentRuntime {
|
|
|
58
61
|
subscribe(listener: () => void): Unsubscribe;
|
|
59
62
|
subscribeToEvents(listener: (event: WordReviewEditorEvent) => void): Unsubscribe;
|
|
60
63
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
64
|
+
getFormattingState(): import("../api/public-types").FormattingStateSnapshot;
|
|
61
65
|
dispatch(command: EditorCommand): void;
|
|
62
66
|
undo(): void;
|
|
63
67
|
redo(): void;
|
|
64
68
|
focus(): void;
|
|
65
69
|
blur(): void;
|
|
70
|
+
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
66
71
|
addComment(params: AddCommentParams): string;
|
|
67
72
|
openComment(commentId: string): void;
|
|
68
73
|
resolveComment(commentId: string): void;
|
|
@@ -162,6 +167,9 @@ export function createDocumentRuntime(
|
|
|
162
167
|
getRenderSnapshot() {
|
|
163
168
|
return cachedRenderSnapshot;
|
|
164
169
|
},
|
|
170
|
+
getFormattingState() {
|
|
171
|
+
return getFormattingStateFromRenderSnapshot(cachedRenderSnapshot);
|
|
172
|
+
},
|
|
165
173
|
dispatch(command) {
|
|
166
174
|
if (command.type === "history.undo") {
|
|
167
175
|
applyHistory("undo");
|
|
@@ -208,6 +216,25 @@ export function createDocumentRuntime(
|
|
|
208
216
|
origin: createOrigin("api", clock()),
|
|
209
217
|
});
|
|
210
218
|
},
|
|
219
|
+
replaceText(text, target) {
|
|
220
|
+
try {
|
|
221
|
+
const timestamp = clock();
|
|
222
|
+
const selection = target
|
|
223
|
+
? createSelectionFromPublicAnchor(target)
|
|
224
|
+
: state.selection;
|
|
225
|
+
const result = insertText(state.document, selection, text, { timestamp });
|
|
226
|
+
|
|
227
|
+
this.dispatch({
|
|
228
|
+
type: "document.replace",
|
|
229
|
+
document: result.document,
|
|
230
|
+
mapping: result.mapping,
|
|
231
|
+
selection: result.selection,
|
|
232
|
+
origin: createOrigin("api", timestamp),
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
emitError(toRuntimeError(error));
|
|
236
|
+
}
|
|
237
|
+
},
|
|
211
238
|
addComment(params) {
|
|
212
239
|
const commentId = createEntityId("comment", state.document.review.comments, clock());
|
|
213
240
|
const anchor = params.anchor
|
|
@@ -694,6 +721,22 @@ function toInternalAnchorProjection(
|
|
|
694
721
|
}
|
|
695
722
|
}
|
|
696
723
|
|
|
724
|
+
function createSelectionFromPublicAnchor(
|
|
725
|
+
anchor: EditorAnchorProjection,
|
|
726
|
+
): import("../core/state/editor-state.ts").SelectionSnapshot {
|
|
727
|
+
switch (anchor.kind) {
|
|
728
|
+
case "range":
|
|
729
|
+
return createSelectionSnapshot(anchor.from, anchor.to);
|
|
730
|
+
case "node":
|
|
731
|
+
return createSelectionSnapshot(anchor.at, anchor.at);
|
|
732
|
+
case "detached":
|
|
733
|
+
return createSelectionSnapshot(
|
|
734
|
+
anchor.lastKnownRange.from,
|
|
735
|
+
anchor.lastKnownRange.to,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
697
740
|
function toPublicCompatibilityReport(
|
|
698
741
|
report: InternalCompatibilityReport,
|
|
699
742
|
): CompatibilityReport {
|