@beyondwork/docx-react-component 1.0.19 → 1.0.21
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 +44 -25
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +130 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -18,25 +18,9 @@ export function splitDocumentAtReviewBoundaries(
|
|
|
18
18
|
return content;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
let cursor = 0;
|
|
22
|
-
const children = content.children.map((block, index) => {
|
|
23
|
-
if (index > 0 && content.children[index - 1]?.type === "paragraph" && block.type === "paragraph") {
|
|
24
|
-
cursor += 1;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (block.type !== "paragraph") {
|
|
28
|
-
cursor += 1;
|
|
29
|
-
return block;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const next = splitParagraph(block, splitPositions, cursor);
|
|
33
|
-
cursor = next.cursor;
|
|
34
|
-
return next.paragraph;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
21
|
return {
|
|
38
22
|
type: "doc",
|
|
39
|
-
children,
|
|
23
|
+
children: splitBlockNodes(content.children, splitPositions, 0, true).children,
|
|
40
24
|
};
|
|
41
25
|
}
|
|
42
26
|
|
|
@@ -98,7 +82,7 @@ function splitParagraph(
|
|
|
98
82
|
}
|
|
99
83
|
|
|
100
84
|
children.push(child);
|
|
101
|
-
nextCursor +=
|
|
85
|
+
nextCursor += measureInlineNodeForReviewBoundaries(child);
|
|
102
86
|
}
|
|
103
87
|
|
|
104
88
|
return {
|
|
@@ -110,6 +94,147 @@ function splitParagraph(
|
|
|
110
94
|
};
|
|
111
95
|
}
|
|
112
96
|
|
|
97
|
+
function splitBlockNodes(
|
|
98
|
+
blocks: readonly DocumentRootNode["children"][number][],
|
|
99
|
+
splitPositions: ReadonlySet<number>,
|
|
100
|
+
cursor: number,
|
|
101
|
+
useSurfaceParagraphSeparators: boolean,
|
|
102
|
+
): {
|
|
103
|
+
children: DocumentRootNode["children"];
|
|
104
|
+
cursor: number;
|
|
105
|
+
} {
|
|
106
|
+
const children: DocumentRootNode["children"] = [];
|
|
107
|
+
let nextCursor = cursor;
|
|
108
|
+
for (const [index, block] of blocks.entries()) {
|
|
109
|
+
if (block.type === "paragraph") {
|
|
110
|
+
if (useSurfaceParagraphSeparators && index > 0) {
|
|
111
|
+
nextCursor += 1;
|
|
112
|
+
}
|
|
113
|
+
const next = splitParagraph(block, splitPositions, nextCursor);
|
|
114
|
+
children.push(next.paragraph);
|
|
115
|
+
nextCursor = next.cursor;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (block.type === "table") {
|
|
120
|
+
const next = splitTableAtReviewBoundaries(block, splitPositions, nextCursor);
|
|
121
|
+
children.push(next.table);
|
|
122
|
+
nextCursor = next.cursor;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (block.type === "sdt") {
|
|
127
|
+
const next = splitBlockNodes(block.children, splitPositions, nextCursor, false);
|
|
128
|
+
children.push({
|
|
129
|
+
...block,
|
|
130
|
+
children: next.children,
|
|
131
|
+
});
|
|
132
|
+
nextCursor = next.cursor;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (block.type === "custom_xml") {
|
|
137
|
+
children.push(block);
|
|
138
|
+
nextCursor += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
children.push(block);
|
|
143
|
+
nextCursor += 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
children,
|
|
148
|
+
cursor: nextCursor,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function splitTableAtReviewBoundaries(
|
|
153
|
+
table: Extract<DocumentRootNode["children"][number], { type: "table" }>,
|
|
154
|
+
splitPositions: ReadonlySet<number>,
|
|
155
|
+
cursor: number,
|
|
156
|
+
): {
|
|
157
|
+
table: Extract<DocumentRootNode["children"][number], { type: "table" }>;
|
|
158
|
+
cursor: number;
|
|
159
|
+
} {
|
|
160
|
+
let nextCursor = cursor;
|
|
161
|
+
const rows = table.rows.map((row) => ({
|
|
162
|
+
...row,
|
|
163
|
+
cells: row.cells.map((cell) => {
|
|
164
|
+
const next = splitBlockNodes(cell.children, splitPositions, nextCursor, false);
|
|
165
|
+
nextCursor = next.cursor;
|
|
166
|
+
return {
|
|
167
|
+
...cell,
|
|
168
|
+
children: next.children,
|
|
169
|
+
};
|
|
170
|
+
}),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
table: {
|
|
175
|
+
...table,
|
|
176
|
+
rows,
|
|
177
|
+
},
|
|
178
|
+
cursor: nextCursor,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function advanceCursorThroughTable(
|
|
183
|
+
table: Extract<DocumentRootNode["children"][number], { type: "table" }>,
|
|
184
|
+
cursor: number,
|
|
185
|
+
): number {
|
|
186
|
+
let nextCursor = cursor;
|
|
187
|
+
for (const row of table.rows) {
|
|
188
|
+
for (const cell of row.cells) {
|
|
189
|
+
nextCursor = measureBlockNodesForReviewBoundaries(cell.children, nextCursor, false);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return nextCursor;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function measureBlockNodesForReviewBoundaries(
|
|
196
|
+
blocks: readonly DocumentRootNode["children"][number][],
|
|
197
|
+
cursor: number,
|
|
198
|
+
useSurfaceParagraphSeparators: boolean,
|
|
199
|
+
): number {
|
|
200
|
+
let nextCursor = cursor;
|
|
201
|
+
for (const [index, block] of blocks.entries()) {
|
|
202
|
+
if (block.type === "paragraph") {
|
|
203
|
+
if (useSurfaceParagraphSeparators && index > 0) {
|
|
204
|
+
nextCursor += 1;
|
|
205
|
+
}
|
|
206
|
+
nextCursor += block.children.reduce(
|
|
207
|
+
(size, child) => size + measureInlineNodeForReviewBoundaries(child),
|
|
208
|
+
0,
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (block.type === "table") {
|
|
214
|
+
nextCursor = advanceCursorThroughTable(block, nextCursor);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (block.type === "sdt") {
|
|
219
|
+
nextCursor = measureBlockNodesForReviewBoundaries(
|
|
220
|
+
block.children,
|
|
221
|
+
nextCursor,
|
|
222
|
+
false,
|
|
223
|
+
);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (block.type === "custom_xml") {
|
|
228
|
+
nextCursor += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
nextCursor += 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return nextCursor;
|
|
236
|
+
}
|
|
237
|
+
|
|
113
238
|
function splitHyperlinkNode(
|
|
114
239
|
node: HyperlinkNode,
|
|
115
240
|
splitPositions: ReadonlySet<number>,
|
|
@@ -132,7 +257,7 @@ function splitHyperlinkNode(
|
|
|
132
257
|
}
|
|
133
258
|
|
|
134
259
|
groups[groups.length - 1]?.push(child);
|
|
135
|
-
nextCursor +=
|
|
260
|
+
nextCursor += measureInlineNodeForReviewBoundaries(child);
|
|
136
261
|
if (splitPositions.has(nextCursor)) {
|
|
137
262
|
groups.push([]);
|
|
138
263
|
}
|
|
@@ -192,3 +317,40 @@ function splitTextNode(
|
|
|
192
317
|
cursor: cursor + codepoints.length,
|
|
193
318
|
};
|
|
194
319
|
}
|
|
320
|
+
|
|
321
|
+
function measureInlineNodeForReviewBoundaries(node: InlineNode): number {
|
|
322
|
+
switch (node.type) {
|
|
323
|
+
case "text":
|
|
324
|
+
return Array.from(node.text).length;
|
|
325
|
+
case "bookmark_start":
|
|
326
|
+
case "bookmark_end":
|
|
327
|
+
return 0;
|
|
328
|
+
case "hyperlink":
|
|
329
|
+
return node.children.reduce(
|
|
330
|
+
(size, child) => size + measureInlineNodeForReviewBoundaries(child),
|
|
331
|
+
0,
|
|
332
|
+
);
|
|
333
|
+
case "field": {
|
|
334
|
+
const childWidth = node.children.reduce(
|
|
335
|
+
(size, child) => size + measureInlineNodeForReviewBoundaries(child),
|
|
336
|
+
0,
|
|
337
|
+
);
|
|
338
|
+
return childWidth > 0 ? childWidth : 1;
|
|
339
|
+
}
|
|
340
|
+
case "tab":
|
|
341
|
+
case "hard_break":
|
|
342
|
+
case "column_break":
|
|
343
|
+
case "footnote_ref":
|
|
344
|
+
case "image":
|
|
345
|
+
case "opaque_inline":
|
|
346
|
+
case "chart_preview":
|
|
347
|
+
case "smartart_preview":
|
|
348
|
+
case "shape":
|
|
349
|
+
case "wordart":
|
|
350
|
+
case "vml_shape":
|
|
351
|
+
case "symbol":
|
|
352
|
+
return 1;
|
|
353
|
+
default:
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CustomXmlNode,
|
|
5
5
|
DiagnosticStore,
|
|
6
6
|
DocumentRootNode,
|
|
7
|
+
FieldRegistry,
|
|
7
8
|
InlineNode,
|
|
8
9
|
MediaCatalog,
|
|
9
10
|
OpaqueBlockNode,
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
ParsedTableCellNode,
|
|
35
36
|
ParsedTableRowNode,
|
|
36
37
|
} from "../ooxml/parse-main-document.ts";
|
|
38
|
+
import { classifyFieldInstruction, buildFieldRegistry } from "../ooxml/parse-fields.ts";
|
|
37
39
|
|
|
38
40
|
export interface NormalizedTextDocument {
|
|
39
41
|
content: DocumentRootNode;
|
|
@@ -41,6 +43,8 @@ export interface NormalizedTextDocument {
|
|
|
41
43
|
preservation: PreservationStore;
|
|
42
44
|
diagnostics: DiagnosticStore;
|
|
43
45
|
finalSectionProperties?: ParsedMainDocument["finalSectionProperties"];
|
|
46
|
+
/** Package-backed field registry built during normalization. */
|
|
47
|
+
fieldRegistry?: FieldRegistry;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface NormalizationState {
|
|
@@ -56,6 +60,7 @@ interface NormalizationState {
|
|
|
56
60
|
export function normalizeParsedTextDocument(
|
|
57
61
|
document: ParsedMainDocument,
|
|
58
62
|
packagePartName = "/word/document.xml",
|
|
63
|
+
options?: { styles?: import("../../model/canonical-document.ts").StylesCatalog },
|
|
59
64
|
): NormalizedTextDocument {
|
|
60
65
|
const state: NormalizationState = {
|
|
61
66
|
nextFragmentIndex: 1,
|
|
@@ -89,17 +94,24 @@ export function normalizeParsedTextDocument(
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
const content: DocumentRootNode = { type: "doc", children };
|
|
98
|
+
|
|
99
|
+
// Build the field registry from normalized content.
|
|
100
|
+
// When styles are available, the registry includes full TOC heading resolution.
|
|
101
|
+
// Without styles, it still catalogs all field instances for the supported/preserve-only partition.
|
|
102
|
+
const styles = options?.styles ?? { paragraphs: {}, characters: {}, tables: {} };
|
|
103
|
+
const fieldRegistry = buildFieldRegistry({ content, styles });
|
|
104
|
+
const hasFields = fieldRegistry.supported.length > 0 || fieldRegistry.preserveOnly.length > 0;
|
|
105
|
+
|
|
92
106
|
return {
|
|
93
|
-
content
|
|
94
|
-
type: "doc",
|
|
95
|
-
children,
|
|
96
|
-
},
|
|
107
|
+
content,
|
|
97
108
|
media: state.media,
|
|
98
109
|
preservation: state.preservation,
|
|
99
110
|
diagnostics: state.diagnostics,
|
|
100
111
|
...(document.finalSectionProperties !== undefined
|
|
101
112
|
? { finalSectionProperties: document.finalSectionProperties }
|
|
102
113
|
: {}),
|
|
114
|
+
...(hasFields ? { fieldRegistry } : {}),
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -158,6 +170,9 @@ function normalizeParagraph(
|
|
|
158
170
|
...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
|
|
159
171
|
...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
|
|
160
172
|
...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
|
|
173
|
+
...(paragraph.contextualSpacing !== undefined
|
|
174
|
+
? { contextualSpacing: paragraph.contextualSpacing }
|
|
175
|
+
: {}),
|
|
161
176
|
...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
|
|
162
177
|
...(paragraph.tabStops && paragraph.tabStops.length > 0 ? { tabStops: paragraph.tabStops } : {}),
|
|
163
178
|
...(paragraph.keepNext ? { keepNext: paragraph.keepNext } : {}),
|
|
@@ -397,15 +412,33 @@ function normalizeInlineChildren(
|
|
|
397
412
|
bookmarkId: node.bookmarkId,
|
|
398
413
|
});
|
|
399
414
|
break;
|
|
400
|
-
case "
|
|
415
|
+
case "footnote_ref":
|
|
416
|
+
normalized.push({
|
|
417
|
+
type: "footnote_ref",
|
|
418
|
+
noteId: node.noteId,
|
|
419
|
+
noteKind: node.noteKind,
|
|
420
|
+
});
|
|
421
|
+
state.cursor += 1;
|
|
422
|
+
break;
|
|
423
|
+
case "field": {
|
|
424
|
+
const classification = classifyFieldInstruction(node.instruction);
|
|
425
|
+
const cursorBeforeField = state.cursor;
|
|
426
|
+
const fieldChildren = node.children
|
|
427
|
+
? normalizeInlineChildren(node.children, state, packagePartName)
|
|
428
|
+
: normalizeFieldContentXml(node.contentXml ?? "");
|
|
429
|
+
state.cursor = cursorBeforeField;
|
|
401
430
|
normalized.push({
|
|
402
431
|
type: "field",
|
|
403
432
|
fieldType: node.fieldType,
|
|
404
433
|
instruction: node.instruction,
|
|
405
|
-
children:
|
|
434
|
+
children: fieldChildren,
|
|
435
|
+
fieldFamily: classification.family,
|
|
436
|
+
...(classification.target ? { fieldTarget: classification.target } : {}),
|
|
437
|
+
refreshStatus: classification.supported ? "stale" : "preserve-only",
|
|
406
438
|
});
|
|
407
|
-
state.cursor += 1;
|
|
439
|
+
state.cursor += fieldChildren.length > 0 ? fieldChildren.length : 1;
|
|
408
440
|
break;
|
|
441
|
+
}
|
|
409
442
|
}
|
|
410
443
|
}
|
|
411
444
|
|
|
@@ -416,7 +449,8 @@ function normalizeImageNode(
|
|
|
416
449
|
node: ParsedImageNode,
|
|
417
450
|
state: NormalizationState,
|
|
418
451
|
): InlineNode {
|
|
419
|
-
|
|
452
|
+
const existingMediaItem = state.media.items[node.mediaId];
|
|
453
|
+
if (!existingMediaItem) {
|
|
420
454
|
const packagePartName =
|
|
421
455
|
typeof node.packagePartName === "string" && node.packagePartName.length > 0
|
|
422
456
|
? node.packagePartName
|
|
@@ -432,6 +466,17 @@ function normalizeImageNode(
|
|
|
432
466
|
packagePartName,
|
|
433
467
|
...(node.relationshipId ? { relationshipId: node.relationshipId } : {}),
|
|
434
468
|
...(node.altText ? { altText: node.altText } : {}),
|
|
469
|
+
...(node.widthEmu !== undefined ? { widthEmu: node.widthEmu } : {}),
|
|
470
|
+
...(node.heightEmu !== undefined ? { heightEmu: node.heightEmu } : {}),
|
|
471
|
+
};
|
|
472
|
+
} else if (
|
|
473
|
+
node.widthEmu !== undefined ||
|
|
474
|
+
node.heightEmu !== undefined
|
|
475
|
+
) {
|
|
476
|
+
state.media.items[node.mediaId] = {
|
|
477
|
+
...existingMediaItem,
|
|
478
|
+
...(node.widthEmu !== undefined ? { widthEmu: node.widthEmu } : {}),
|
|
479
|
+
...(node.heightEmu !== undefined ? { heightEmu: node.heightEmu } : {}),
|
|
435
480
|
};
|
|
436
481
|
}
|
|
437
482
|
|
|
@@ -559,3 +604,32 @@ function recordOpaqueFragment(
|
|
|
559
604
|
warningId,
|
|
560
605
|
};
|
|
561
606
|
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Extract text content from field contentXml to populate the field's children
|
|
610
|
+
* array. This enables cross-reference and TOC content to be visible in the
|
|
611
|
+
* canonical model and surface projection.
|
|
612
|
+
*/
|
|
613
|
+
function normalizeFieldContentXml(contentXml: string | undefined): InlineNode[] {
|
|
614
|
+
if (!contentXml || contentXml.trim().length === 0) {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Extract text from <w:t> elements within the content runs
|
|
619
|
+
const textPattern = /<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g;
|
|
620
|
+
const children: InlineNode[] = [];
|
|
621
|
+
|
|
622
|
+
for (const match of contentXml.matchAll(textPattern)) {
|
|
623
|
+
const text = match[1]
|
|
624
|
+
.replace(/&/g, "&")
|
|
625
|
+
.replace(/</g, "<")
|
|
626
|
+
.replace(/>/g, ">")
|
|
627
|
+
.replace(/"/g, '"')
|
|
628
|
+
.replace(/'/g, "'");
|
|
629
|
+
if (text.length > 0) {
|
|
630
|
+
children.push({ type: "text", text });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return children;
|
|
635
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const HIGHLIGHT_COLOR_MAP = {
|
|
2
|
+
black: "000000",
|
|
3
|
+
blue: "0000FF",
|
|
4
|
+
cyan: "00FFFF",
|
|
5
|
+
darkBlue: "000080",
|
|
6
|
+
darkCyan: "008080",
|
|
7
|
+
darkGray: "808080",
|
|
8
|
+
darkGreen: "008000",
|
|
9
|
+
darkMagenta: "800080",
|
|
10
|
+
darkRed: "8B0000",
|
|
11
|
+
darkYellow: "808000",
|
|
12
|
+
green: "00FF00",
|
|
13
|
+
lightGray: "C0C0C0",
|
|
14
|
+
magenta: "FF00FF",
|
|
15
|
+
red: "FF0000",
|
|
16
|
+
white: "FFFFFF",
|
|
17
|
+
yellow: "FFFF00",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type HighlightColorName = keyof typeof HIGHLIGHT_COLOR_MAP;
|
|
21
|
+
|
|
22
|
+
export function resolveHighlightColor(
|
|
23
|
+
value: string | null | undefined,
|
|
24
|
+
): { color: string; val: HighlightColorName } | undefined {
|
|
25
|
+
if (!value || value === "none") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalizedValue = value as HighlightColorName;
|
|
30
|
+
const color = HIGHLIGHT_COLOR_MAP[normalizedValue];
|
|
31
|
+
if (!color) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
color,
|
|
37
|
+
val: normalizedValue,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -65,6 +65,8 @@ export interface CommentImportDiagnostic {
|
|
|
65
65
|
| "preserve_only_revision_overlap";
|
|
66
66
|
message: string;
|
|
67
67
|
featureClass: "preserve-only";
|
|
68
|
+
detachedReason?: "incomplete-markers" | "multi-paragraph" | "opaque-region" | "revision-overlap";
|
|
69
|
+
actionabilityNote?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface ParsedCommentsResult {
|
|
@@ -143,8 +145,10 @@ export function parseCommentsFromOoxml(
|
|
|
143
145
|
diagnostics.push({
|
|
144
146
|
commentId: rootCommentId,
|
|
145
147
|
code: "missing_anchor_reference",
|
|
146
|
-
message: "Comment anchor markers are incomplete and
|
|
148
|
+
message: "Comment anchor markers are incomplete (missing start, end, or reference). Thread is visible but detached and not actionable.",
|
|
147
149
|
featureClass: "preserve-only",
|
|
150
|
+
detachedReason: "incomplete-markers",
|
|
151
|
+
actionabilityNote: "Re-anchoring requires the host to supply a valid range. The comment body and thread are preserved for display.",
|
|
148
152
|
});
|
|
149
153
|
threads.push(
|
|
150
154
|
createImportedCommentThread({
|
|
@@ -171,8 +175,10 @@ export function parseCommentsFromOoxml(
|
|
|
171
175
|
commentId: rootCommentId,
|
|
172
176
|
code: "multi_paragraph_anchor_preserve_only",
|
|
173
177
|
message:
|
|
174
|
-
"Comment anchor spans multiple paragraphs
|
|
178
|
+
"Comment anchor spans multiple paragraphs. Thread is visible but detached; cross-paragraph anchoring is not yet supported for live editing.",
|
|
175
179
|
featureClass: "preserve-only",
|
|
180
|
+
detachedReason: "multi-paragraph",
|
|
181
|
+
actionabilityNote: "The comment thread and body are preserved. Operators see the thread in the sidebar but cannot navigate to an inline highlight.",
|
|
176
182
|
});
|
|
177
183
|
threads.push(
|
|
178
184
|
createImportedCommentThread({
|
|
@@ -470,32 +476,92 @@ function parseCommentAnchors(documentXml: string): Map<string, CommentAnchorBoun
|
|
|
470
476
|
const documentElement = findChildElement(root, "document");
|
|
471
477
|
const bodyElement = findChildElement(documentElement, "body");
|
|
472
478
|
const anchors = new Map<string, CommentAnchorBounds>();
|
|
473
|
-
|
|
474
|
-
let paragraphIndex = -1;
|
|
475
|
-
let previousWasParagraph = false;
|
|
479
|
+
walkCommentAnchorBlocks(bodyElement.children, anchors, 0, -1, true);
|
|
476
480
|
|
|
477
|
-
|
|
478
|
-
|
|
481
|
+
return anchors;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function walkCommentAnchorBlocks(
|
|
485
|
+
nodes: readonly XmlNode[],
|
|
486
|
+
anchors: Map<string, CommentAnchorBounds>,
|
|
487
|
+
cursor: number,
|
|
488
|
+
paragraphIndex: number,
|
|
489
|
+
useSurfaceParagraphSeparators: boolean,
|
|
490
|
+
): {
|
|
491
|
+
cursor: number;
|
|
492
|
+
paragraphIndex: number;
|
|
493
|
+
} {
|
|
494
|
+
let nextCursor = cursor;
|
|
495
|
+
let nextParagraphIndex = paragraphIndex;
|
|
496
|
+
let elementIndex = -1;
|
|
497
|
+
|
|
498
|
+
for (const node of nodes) {
|
|
499
|
+
if (node.type !== "element") {
|
|
479
500
|
continue;
|
|
480
501
|
}
|
|
502
|
+
elementIndex += 1;
|
|
481
503
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
504
|
+
const name = localName(node.name);
|
|
505
|
+
if (name === "p") {
|
|
506
|
+
if (useSurfaceParagraphSeparators && elementIndex > 0) {
|
|
507
|
+
nextCursor += 1;
|
|
508
|
+
}
|
|
509
|
+
nextParagraphIndex += 1;
|
|
510
|
+
walkParagraph(node, nextParagraphIndex, anchors, () => nextCursor, (next) => {
|
|
511
|
+
nextCursor = next;
|
|
512
|
+
});
|
|
485
513
|
continue;
|
|
486
514
|
}
|
|
487
515
|
|
|
488
|
-
if (
|
|
489
|
-
|
|
516
|
+
if (name === "tbl") {
|
|
517
|
+
for (const child of node.children) {
|
|
518
|
+
if (child.type !== "element" || localName(child.name) !== "tr") {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
for (const rowChild of child.children) {
|
|
522
|
+
if (rowChild.type !== "element" || localName(rowChild.name) !== "tc") {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const result = walkCommentAnchorBlocks(
|
|
526
|
+
rowChild.children,
|
|
527
|
+
anchors,
|
|
528
|
+
nextCursor,
|
|
529
|
+
nextParagraphIndex,
|
|
530
|
+
false,
|
|
531
|
+
);
|
|
532
|
+
nextCursor = result.cursor;
|
|
533
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
continue;
|
|
490
537
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
538
|
+
|
|
539
|
+
if (name === "sdt") {
|
|
540
|
+
const sdtContent = findChildElement(node, "sdtContent");
|
|
541
|
+
const result = walkCommentAnchorBlocks(
|
|
542
|
+
sdtContent.children,
|
|
543
|
+
anchors,
|
|
544
|
+
nextCursor,
|
|
545
|
+
nextParagraphIndex,
|
|
546
|
+
false,
|
|
547
|
+
);
|
|
548
|
+
nextCursor = result.cursor;
|
|
549
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (name === "customXml") {
|
|
554
|
+
nextCursor += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
nextCursor += 0;
|
|
496
559
|
}
|
|
497
560
|
|
|
498
|
-
return
|
|
561
|
+
return {
|
|
562
|
+
cursor: nextCursor,
|
|
563
|
+
paragraphIndex: nextParagraphIndex,
|
|
564
|
+
};
|
|
499
565
|
}
|
|
500
566
|
|
|
501
567
|
function walkParagraph(
|
|
@@ -554,7 +620,7 @@ function walkInlineNode(
|
|
|
554
620
|
.filter((child): child is XmlTextNode => child.type === "text")
|
|
555
621
|
.map((child) => child.text)
|
|
556
622
|
.join("");
|
|
557
|
-
setCursor(getCursor() + text.length);
|
|
623
|
+
setCursor(getCursor() + Array.from(text).length);
|
|
558
624
|
return;
|
|
559
625
|
}
|
|
560
626
|
case "tab":
|