@beyondwork/docx-react-component 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- 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 +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -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 +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -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-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
|
@@ -147,6 +147,11 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
|
|
|
147
147
|
const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val ?? `%${level + 1}.`;
|
|
148
148
|
const paragraphStyleId =
|
|
149
149
|
paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
|
|
150
|
+
const isLegalNode = findChildElementOptional(child, "isLgl");
|
|
151
|
+
const isLegalNumbering = isLegalNode !== undefined;
|
|
152
|
+
const suffixNode = findChildElementOptional(child, "suff");
|
|
153
|
+
const suffixVal = suffixNode?.attributes["w:val"] ?? suffixNode?.attributes.val;
|
|
154
|
+
const suffix = suffixVal === "space" || suffixVal === "nothing" ? suffixVal : suffixVal === "tab" ? "tab" : undefined;
|
|
150
155
|
|
|
151
156
|
levels.push({
|
|
152
157
|
level,
|
|
@@ -154,6 +159,8 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
|
|
|
154
159
|
text,
|
|
155
160
|
...(startAt !== undefined ? { startAt } : {}),
|
|
156
161
|
...(paragraphStyleId ? { paragraphStyleId } : {}),
|
|
162
|
+
...(isLegalNumbering ? { isLegalNumbering } : {}),
|
|
163
|
+
...(suffix ? { suffix } : {}),
|
|
157
164
|
});
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -38,9 +38,14 @@ export interface RevisionImportDiagnostic {
|
|
|
38
38
|
| "preserve_only_move_revision"
|
|
39
39
|
| "preserve_only_formatting_revision"
|
|
40
40
|
| "nested_revision_preserve_only"
|
|
41
|
-
| "
|
|
41
|
+
| "shallow_nested_revision_promoted"
|
|
42
|
+
| "linked_move_pair"
|
|
43
|
+
| "unlinked_move_revision"
|
|
44
|
+
| "ambiguous_revision_anchor"
|
|
45
|
+
| "table_structural_revision_preserve_only"
|
|
46
|
+
| "table_property_revision_parsed";
|
|
42
47
|
message: string;
|
|
43
|
-
featureClass: "preserve-only";
|
|
48
|
+
featureClass: "preserve-only" | "supported" | "informational";
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export interface ParsedRevisionsResult {
|
|
@@ -69,6 +74,7 @@ interface RevisionMetadata {
|
|
|
69
74
|
const SUPPORTED_CONTAINER_TYPES = new Set(["ins", "del"]);
|
|
70
75
|
const PRESERVE_ONLY_CONTAINER_TYPES = new Set(["moveFrom", "moveTo"]);
|
|
71
76
|
const PROPERTY_CHANGE_REVISION_TYPES = new Set(["rPrChange", "pPrChange"]);
|
|
77
|
+
const STRUCTURAL_TABLE_REVISION_TYPES = new Set(["cellIns", "cellDel", "cellMerge"]);
|
|
72
78
|
|
|
73
79
|
export function parseRevisionsFromDocumentXml(
|
|
74
80
|
documentXml: string,
|
|
@@ -124,6 +130,8 @@ export function parseRevisionsFromDocumentXml(
|
|
|
124
130
|
previousWasParagraph = true;
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
linkMoveRevisionPairs(state);
|
|
134
|
+
|
|
127
135
|
return {
|
|
128
136
|
revisions: state.revisions,
|
|
129
137
|
preservedMarkup: state.preservedMarkup.sort((left, right) => left.xmlStart - right.xmlStart),
|
|
@@ -284,6 +292,46 @@ function walkContentNode(
|
|
|
284
292
|
|
|
285
293
|
if (hasNestedRevision) {
|
|
286
294
|
const nestedKind = type === "ins" ? "insertion" : type === "del" ? "deletion" : "move";
|
|
295
|
+
const isShallowNesting = isShallowPropertyChangeNesting(node);
|
|
296
|
+
|
|
297
|
+
if (isShallowNesting && SUPPORTED_CONTAINER_TYPES.has(type)) {
|
|
298
|
+
state.revisions.push(
|
|
299
|
+
createRevisionRecord({
|
|
300
|
+
revisionId: metadata.revisionId,
|
|
301
|
+
kind: nestedKind as "insertion" | "deletion",
|
|
302
|
+
anchor:
|
|
303
|
+
length > 0
|
|
304
|
+
? createRevisionRangeAnchor(start, end)
|
|
305
|
+
: createDetachedAnchor({ from: start, to: end }, "importAmbiguity"),
|
|
306
|
+
authorId: metadata.authorId,
|
|
307
|
+
createdAt: metadata.createdAt,
|
|
308
|
+
metadata: {
|
|
309
|
+
source: "import",
|
|
310
|
+
importedRevisionForm:
|
|
311
|
+
type === "ins" ? "run-insertion" : "run-deletion",
|
|
312
|
+
originalRevisionType: type,
|
|
313
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
state.preservedMarkup.push({
|
|
318
|
+
revisionId: metadata.revisionId,
|
|
319
|
+
rawXml: state.documentXml.slice(node.start, node.end),
|
|
320
|
+
xmlStart: node.start,
|
|
321
|
+
xmlEnd: node.end,
|
|
322
|
+
originalRevisionType: type,
|
|
323
|
+
});
|
|
324
|
+
state.diagnostics.push({
|
|
325
|
+
revisionId: metadata.revisionId,
|
|
326
|
+
code: "shallow_nested_revision_promoted",
|
|
327
|
+
message: "Shallow nested revision (property changes inside ins/del) promoted to supported rendering.",
|
|
328
|
+
featureClass: "supported",
|
|
329
|
+
});
|
|
330
|
+
parseNestedPropertyChangeRevisions(node, paragraphIndex, state, start);
|
|
331
|
+
advanceCursor(node, setCursor, getCursor);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
287
335
|
state.revisions.push(
|
|
288
336
|
createRevisionRecord({
|
|
289
337
|
revisionId: metadata.revisionId,
|
|
@@ -353,7 +401,7 @@ function walkContentNode(
|
|
|
353
401
|
state.diagnostics.push({
|
|
354
402
|
revisionId: metadata.revisionId,
|
|
355
403
|
code: "preserve_only_move_revision",
|
|
356
|
-
message: "
|
|
404
|
+
message: "Move revision parsed as preserve-only. Content is preserved for round-trip; linking resolved in post-parse step.",
|
|
357
405
|
featureClass: "preserve-only",
|
|
358
406
|
});
|
|
359
407
|
advanceCursor(node, setCursor, getCursor);
|
|
@@ -475,45 +523,169 @@ function parseTblPropertyRevisions(
|
|
|
475
523
|
state: ParseState,
|
|
476
524
|
): void {
|
|
477
525
|
const tblPr = findChildElement(table, "tblPr");
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
526
|
+
if (tblPr) {
|
|
527
|
+
const tblPrChange = findChildElement(tblPr, "tblPrChange");
|
|
528
|
+
if (tblPrChange) {
|
|
529
|
+
const metadata = readRevisionMetadata(tblPrChange, state, "property-change");
|
|
530
|
+
const innerTblPr = findChildElement(tblPrChange, "tblPr");
|
|
531
|
+
const beforeXml = innerTblPr ? state.documentXml.slice(innerTblPr.start, innerTblPr.end) : "";
|
|
532
|
+
const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
|
|
481
533
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
534
|
+
state.revisions.push(
|
|
535
|
+
createRevisionRecord({
|
|
536
|
+
revisionId: metadata.revisionId,
|
|
537
|
+
kind: "property-change",
|
|
538
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
539
|
+
authorId: metadata.authorId,
|
|
540
|
+
createdAt: metadata.createdAt,
|
|
541
|
+
metadata: {
|
|
542
|
+
source: "import",
|
|
543
|
+
originalRevisionType: "tblPrChange",
|
|
544
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
545
|
+
propertyChangeData,
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
state.preservedMarkup.push({
|
|
550
|
+
revisionId: metadata.revisionId,
|
|
551
|
+
rawXml: state.documentXml.slice(tblPrChange.start, tblPrChange.end),
|
|
552
|
+
xmlStart: tblPrChange.start,
|
|
553
|
+
xmlEnd: tblPrChange.end,
|
|
554
|
+
originalRevisionType: "tblPrChange",
|
|
555
|
+
containerXmlStart: tblPr.start,
|
|
556
|
+
containerXmlEnd: tblPr.end,
|
|
557
|
+
beforeContainerXml: beforeXml,
|
|
558
|
+
});
|
|
559
|
+
state.diagnostics.push({
|
|
560
|
+
revisionId: metadata.revisionId,
|
|
561
|
+
code: "table_property_revision_parsed",
|
|
562
|
+
message: "Table property change (tblPrChange) parsed as actionable property-change revision.",
|
|
563
|
+
featureClass: "supported",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
485
566
|
}
|
|
486
567
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const beforeXml = innerTblPr ? state.documentXml.slice(innerTblPr.start, innerTblPr.end) : "";
|
|
490
|
-
const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
|
|
568
|
+
parseTableRowAndCellRevisions(table, position, state);
|
|
569
|
+
}
|
|
491
570
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
571
|
+
function parseTableRowAndCellRevisions(
|
|
572
|
+
table: XmlElementNode,
|
|
573
|
+
position: number,
|
|
574
|
+
state: ParseState,
|
|
575
|
+
): void {
|
|
576
|
+
for (const child of table.children) {
|
|
577
|
+
if (child.type !== "element" || localName(child.name) !== "tr") {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const trPr = findChildElement(child, "trPr");
|
|
582
|
+
if (trPr) {
|
|
583
|
+
const trPrChange = findChildElement(trPr, "trPrChange");
|
|
584
|
+
if (trPrChange) {
|
|
585
|
+
const metadata = readRevisionMetadata(trPrChange, state, "property-change");
|
|
586
|
+
const innerTrPr = findChildElement(trPrChange, "trPr");
|
|
587
|
+
const beforeXml = innerTrPr ? state.documentXml.slice(innerTrPr.start, innerTrPr.end) : "";
|
|
588
|
+
const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
|
|
589
|
+
|
|
590
|
+
state.revisions.push(
|
|
591
|
+
createRevisionRecord({
|
|
592
|
+
revisionId: metadata.revisionId,
|
|
593
|
+
kind: "property-change",
|
|
594
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
595
|
+
authorId: metadata.authorId,
|
|
596
|
+
createdAt: metadata.createdAt,
|
|
597
|
+
metadata: {
|
|
598
|
+
source: "import",
|
|
599
|
+
originalRevisionType: "trPrChange",
|
|
600
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
601
|
+
propertyChangeData,
|
|
602
|
+
preserveOnlyReason: "Row property changes remain preserve-only for export safety.",
|
|
603
|
+
},
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
state.preservedMarkup.push({
|
|
607
|
+
revisionId: metadata.revisionId,
|
|
608
|
+
rawXml: state.documentXml.slice(trPrChange.start, trPrChange.end),
|
|
609
|
+
xmlStart: trPrChange.start,
|
|
610
|
+
xmlEnd: trPrChange.end,
|
|
611
|
+
originalRevisionType: "trPrChange",
|
|
612
|
+
containerXmlStart: trPr.start,
|
|
613
|
+
containerXmlEnd: trPr.end,
|
|
614
|
+
beforeContainerXml: beforeXml,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const tcNode of child.children) {
|
|
620
|
+
if (tcNode.type !== "element" || localName(tcNode.name) !== "tc") {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const tcPr = findChildElement(tcNode, "tcPr");
|
|
625
|
+
if (!tcPr) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const tcPrChange = findChildElement(tcPr, "tcPrChange");
|
|
630
|
+
if (tcPrChange) {
|
|
631
|
+
const metadata = readRevisionMetadata(tcPrChange, state, "property-change");
|
|
632
|
+
const innerTcPr = findChildElement(tcPrChange, "tcPr");
|
|
633
|
+
const beforeXml = innerTcPr ? state.documentXml.slice(innerTcPr.start, innerTcPr.end) : "";
|
|
634
|
+
const propertyChangeData: PropertyChangeData = { xmlTag: "tblPrChange", beforeXml };
|
|
635
|
+
|
|
636
|
+
state.revisions.push(
|
|
637
|
+
createRevisionRecord({
|
|
638
|
+
revisionId: metadata.revisionId,
|
|
639
|
+
kind: "property-change",
|
|
640
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
641
|
+
authorId: metadata.authorId,
|
|
642
|
+
createdAt: metadata.createdAt,
|
|
643
|
+
metadata: {
|
|
644
|
+
source: "import",
|
|
645
|
+
originalRevisionType: "tcPrChange",
|
|
646
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
647
|
+
propertyChangeData,
|
|
648
|
+
preserveOnlyReason: "Cell property changes remain preserve-only for export safety.",
|
|
649
|
+
},
|
|
650
|
+
}),
|
|
651
|
+
);
|
|
652
|
+
state.preservedMarkup.push({
|
|
653
|
+
revisionId: metadata.revisionId,
|
|
654
|
+
rawXml: state.documentXml.slice(tcPrChange.start, tcPrChange.end),
|
|
655
|
+
xmlStart: tcPrChange.start,
|
|
656
|
+
xmlEnd: tcPrChange.end,
|
|
657
|
+
originalRevisionType: "tcPrChange",
|
|
658
|
+
containerXmlStart: tcPr.start,
|
|
659
|
+
containerXmlEnd: tcPr.end,
|
|
660
|
+
beforeContainerXml: beforeXml,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const tcChild of tcPr.children) {
|
|
665
|
+
if (tcChild.type !== "element") {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const tcChildName = localName(tcChild.name);
|
|
670
|
+
if (STRUCTURAL_TABLE_REVISION_TYPES.has(tcChildName)) {
|
|
671
|
+
const metadata = readRevisionMetadata(tcChild, state, tcChildName);
|
|
672
|
+
state.preservedMarkup.push({
|
|
673
|
+
revisionId: metadata.revisionId,
|
|
674
|
+
rawXml: state.documentXml.slice(tcChild.start, tcChild.end),
|
|
675
|
+
xmlStart: tcChild.start,
|
|
676
|
+
xmlEnd: tcChild.end,
|
|
677
|
+
originalRevisionType: tcChildName,
|
|
678
|
+
});
|
|
679
|
+
state.diagnostics.push({
|
|
680
|
+
revisionId: metadata.revisionId,
|
|
681
|
+
code: "table_structural_revision_preserve_only",
|
|
682
|
+
message: `Structural table revision (${tcChildName}) remains preserve-only; topology-altering changes cannot be safely applied.`,
|
|
683
|
+
featureClass: "preserve-only",
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
517
689
|
}
|
|
518
690
|
|
|
519
691
|
function parseSectPrRevisions(
|
|
@@ -604,6 +776,113 @@ function sanitizeRevisionToken(value: string): string {
|
|
|
604
776
|
return sanitized.length > 0 ? sanitized : "unknown";
|
|
605
777
|
}
|
|
606
778
|
|
|
779
|
+
function linkMoveRevisionPairs(state: ParseState): void {
|
|
780
|
+
const moveRevisions = state.revisions.filter(
|
|
781
|
+
(r) => r.kind === "move" && r.metadata.moveData,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const byMoveId = new Map<string, typeof moveRevisions>();
|
|
785
|
+
for (const revision of moveRevisions) {
|
|
786
|
+
const moveId = revision.metadata.moveData!.moveId;
|
|
787
|
+
const group = byMoveId.get(moveId) ?? [];
|
|
788
|
+
group.push(revision);
|
|
789
|
+
byMoveId.set(moveId, group);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const [moveId, group] of byMoveId) {
|
|
793
|
+
const fromRevision = group.find(
|
|
794
|
+
(r) => r.metadata.moveData!.direction === "from",
|
|
795
|
+
);
|
|
796
|
+
const toRevision = group.find(
|
|
797
|
+
(r) => r.metadata.moveData!.direction === "to",
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
if (fromRevision && toRevision) {
|
|
801
|
+
fromRevision.metadata.moveData!.linkedRevisionId = toRevision.revisionId;
|
|
802
|
+
toRevision.metadata.moveData!.linkedRevisionId = fromRevision.revisionId;
|
|
803
|
+
|
|
804
|
+
state.diagnostics.push({
|
|
805
|
+
revisionId: fromRevision.revisionId,
|
|
806
|
+
code: "linked_move_pair",
|
|
807
|
+
message: `Move-from revision linked to move-to revision ${toRevision.revisionId} via moveId ${moveId}.`,
|
|
808
|
+
featureClass: "informational",
|
|
809
|
+
});
|
|
810
|
+
state.diagnostics.push({
|
|
811
|
+
revisionId: toRevision.revisionId,
|
|
812
|
+
code: "linked_move_pair",
|
|
813
|
+
message: `Move-to revision linked to move-from revision ${fromRevision.revisionId} via moveId ${moveId}.`,
|
|
814
|
+
featureClass: "informational",
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
for (const revision of group) {
|
|
818
|
+
state.diagnostics.push({
|
|
819
|
+
revisionId: revision.revisionId,
|
|
820
|
+
code: "unlinked_move_revision",
|
|
821
|
+
message: `Move revision has no matching counterpart for moveId ${moveId}. Remains preserve-only.`,
|
|
822
|
+
featureClass: "preserve-only",
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function isShallowPropertyChangeNesting(node: XmlElementNode): boolean {
|
|
830
|
+
for (const child of node.children) {
|
|
831
|
+
if (child.type !== "element") {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const name = localName(child.name);
|
|
836
|
+
|
|
837
|
+
if (SUPPORTED_CONTAINER_TYPES.has(name) || PRESERVE_ONLY_CONTAINER_TYPES.has(name)) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (name === "r") {
|
|
842
|
+
const runProperties = findChildElement(child, "rPr");
|
|
843
|
+
if (runProperties) {
|
|
844
|
+
for (const rpChild of runProperties.children) {
|
|
845
|
+
if (rpChild.type !== "element") {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const rpChildName = localName(rpChild.name);
|
|
850
|
+
if (
|
|
851
|
+
SUPPORTED_CONTAINER_TYPES.has(rpChildName) ||
|
|
852
|
+
PRESERVE_ONLY_CONTAINER_TYPES.has(rpChildName)
|
|
853
|
+
) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!isShallowPropertyChangeNesting(child)) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function parseNestedPropertyChangeRevisions(
|
|
870
|
+
container: XmlElementNode,
|
|
871
|
+
paragraphIndex: number,
|
|
872
|
+
state: ParseState,
|
|
873
|
+
containerStart: number,
|
|
874
|
+
): void {
|
|
875
|
+
for (const child of container.children) {
|
|
876
|
+
if (child.type !== "element") {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (localName(child.name) === "r") {
|
|
881
|
+
parseRunFormattingRevisions(child, paragraphIndex, state, containerStart);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
607
886
|
function containsNestedRevision(node: XmlElementNode): boolean {
|
|
608
887
|
return node.children.some(
|
|
609
888
|
(child) =>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { DocumentSettings } from "../../model/canonical-document.ts";
|
|
2
|
+
|
|
3
|
+
interface XmlElementNode {
|
|
4
|
+
type: "element";
|
|
5
|
+
name: string;
|
|
6
|
+
attributes: Record<string, string>;
|
|
7
|
+
children: XmlNode[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface XmlTextNode {
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type XmlNode = XmlElementNode | XmlTextNode;
|
|
16
|
+
|
|
17
|
+
export function parseSettingsXml(xml: string): DocumentSettings {
|
|
18
|
+
if (!xml.trim()) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const root = parseXml(xml);
|
|
23
|
+
const settingsElement = findChildElementOptional(root, "settings");
|
|
24
|
+
if (!settingsElement) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const evenAndOddHeaders = findChildElementOptional(settingsElement, "evenAndOddHeaders");
|
|
29
|
+
const zoom = findChildElementOptional(settingsElement, "zoom");
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...(evenAndOddHeaders
|
|
33
|
+
? {
|
|
34
|
+
evenAndOddHeaders: readOnOffValue(evenAndOddHeaders, true),
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
...(zoom ? readZoomLevel(zoom) : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findChildElementOptional(
|
|
42
|
+
node: XmlElementNode,
|
|
43
|
+
childLocalName: string,
|
|
44
|
+
): XmlElementNode | undefined {
|
|
45
|
+
return node.children.find(
|
|
46
|
+
(entry): entry is XmlElementNode =>
|
|
47
|
+
entry.type === "element" && localName(entry.name) === childLocalName,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function localName(name: string): string {
|
|
52
|
+
const idx = name.indexOf(":");
|
|
53
|
+
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readOnOffValue(
|
|
57
|
+
element: XmlElementNode,
|
|
58
|
+
defaultValue: boolean,
|
|
59
|
+
): boolean {
|
|
60
|
+
const value =
|
|
61
|
+
element.attributes["w:val"] ??
|
|
62
|
+
element.attributes.val ??
|
|
63
|
+
(defaultValue ? "true" : "false");
|
|
64
|
+
return value !== "0" && value !== "false";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readZoomLevel(
|
|
68
|
+
element: XmlElementNode,
|
|
69
|
+
): Pick<DocumentSettings, "zoomLevel"> {
|
|
70
|
+
const rawValue = element.attributes["w:val"] ?? element.attributes.val;
|
|
71
|
+
if (rawValue === "bestFit") {
|
|
72
|
+
return { zoomLevel: "pageWidth" };
|
|
73
|
+
}
|
|
74
|
+
if (rawValue === "fullPage") {
|
|
75
|
+
return { zoomLevel: "onePage" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rawPercent =
|
|
79
|
+
element.attributes["w:percent"] ??
|
|
80
|
+
element.attributes.percent;
|
|
81
|
+
if (!rawPercent) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const normalizedPercent = rawPercent.trim().replace(/%$/u, "");
|
|
86
|
+
const parsed = Number.parseInt(normalizedPercent, 10);
|
|
87
|
+
if (!Number.isFinite(parsed)) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { zoomLevel: parsed };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseXml(xml: string): XmlElementNode {
|
|
95
|
+
const root: XmlElementNode = {
|
|
96
|
+
type: "element",
|
|
97
|
+
name: "__root__",
|
|
98
|
+
attributes: {},
|
|
99
|
+
children: [],
|
|
100
|
+
};
|
|
101
|
+
const stack: XmlElementNode[] = [root];
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
|
|
104
|
+
while (cursor < xml.length) {
|
|
105
|
+
if (xml.startsWith("<!--", cursor)) {
|
|
106
|
+
const end = xml.indexOf("-->", cursor);
|
|
107
|
+
cursor = end >= 0 ? end + 3 : xml.length;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (xml.startsWith("<?", cursor)) {
|
|
112
|
+
const end = xml.indexOf("?>", cursor);
|
|
113
|
+
cursor = end >= 0 ? end + 2 : xml.length;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nextLt = xml.indexOf("<", cursor);
|
|
118
|
+
if (nextLt < 0) {
|
|
119
|
+
pushText(xml.slice(cursor));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (nextLt > cursor) {
|
|
124
|
+
pushText(xml.slice(cursor, nextLt));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (xml.startsWith("</", nextLt)) {
|
|
128
|
+
const end = xml.indexOf(">", nextLt);
|
|
129
|
+
if (end < 0) break;
|
|
130
|
+
if (stack.length > 1) {
|
|
131
|
+
stack.pop();
|
|
132
|
+
}
|
|
133
|
+
cursor = end + 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const end = xml.indexOf(">", nextLt);
|
|
138
|
+
if (end < 0) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rawTag = xml.slice(nextLt + 1, end).trim();
|
|
143
|
+
const selfClosing = rawTag.endsWith("/");
|
|
144
|
+
const tagBody = selfClosing ? rawTag.slice(0, -1).trim() : rawTag;
|
|
145
|
+
const spaceIndex = tagBody.search(/\s/u);
|
|
146
|
+
const name =
|
|
147
|
+
spaceIndex >= 0 ? tagBody.slice(0, spaceIndex) : tagBody;
|
|
148
|
+
const attrs = spaceIndex >= 0 ? tagBody.slice(spaceIndex + 1) : "";
|
|
149
|
+
const element: XmlElementNode = {
|
|
150
|
+
type: "element",
|
|
151
|
+
name,
|
|
152
|
+
attributes: parseAttributes(attrs),
|
|
153
|
+
children: [],
|
|
154
|
+
};
|
|
155
|
+
stack[stack.length - 1]?.children.push(element);
|
|
156
|
+
if (!selfClosing) {
|
|
157
|
+
stack.push(element);
|
|
158
|
+
}
|
|
159
|
+
cursor = end + 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return root;
|
|
163
|
+
|
|
164
|
+
function pushText(raw: string): void {
|
|
165
|
+
const normalized = raw.replace(/\r\n?/gu, "\n");
|
|
166
|
+
if (!normalized.trim()) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
stack[stack.length - 1]?.children.push({
|
|
170
|
+
type: "text",
|
|
171
|
+
text: normalized,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseAttributes(raw: string): Record<string, string> {
|
|
177
|
+
const attributes: Record<string, string> = {};
|
|
178
|
+
const pattern = /([^\s=]+)\s*=\s*("([^"]*)"|'([^']*)')/gu;
|
|
179
|
+
for (const match of raw.matchAll(pattern)) {
|
|
180
|
+
const [, name, , dq, sq] = match;
|
|
181
|
+
attributes[name] = dq ?? sq ?? "";
|
|
182
|
+
}
|
|
183
|
+
return attributes;
|
|
184
|
+
}
|
|
@@ -15,8 +15,12 @@ const WPS_SHAPE_GRAPHIC_URI =
|
|
|
15
15
|
|
|
16
16
|
export interface ParsedWpsShape {
|
|
17
17
|
type: "shape";
|
|
18
|
+
/** True if this shape is a text box (geometry=rect with txbx content). */
|
|
19
|
+
isTextBox?: boolean;
|
|
18
20
|
/** Extracted text content from wps:txbx for display. */
|
|
19
21
|
text?: string;
|
|
22
|
+
/** Raw txbxContent XML for structured re-rendering. */
|
|
23
|
+
txbxContentXml?: string;
|
|
20
24
|
/** DrawML geometry preset, e.g. "rect", "roundRect". */
|
|
21
25
|
geometry?: string;
|
|
22
26
|
/** Original drawing XML for lossless round-trip export. */
|
|
@@ -81,9 +85,17 @@ export function parseShapeXml(drawingXml: string): ParsedWpsShape | ParsedWordAr
|
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
// Text box detection: rect or no geometry with text content
|
|
89
|
+
const isTextBox = Boolean(txbxContent && (!prst || prst === "rect"));
|
|
90
|
+
|
|
91
|
+
// Extract raw txbxContent XML for structured re-rendering of text boxes
|
|
92
|
+
const txbxContentXml = txbxContent ? extractRawXml(txbxContent, drawingXml) : undefined;
|
|
93
|
+
|
|
84
94
|
return {
|
|
85
95
|
type: "shape",
|
|
96
|
+
...(isTextBox ? { isTextBox: true } : {}),
|
|
86
97
|
...(text ? { text } : {}),
|
|
98
|
+
...(txbxContentXml ? { txbxContentXml } : {}),
|
|
87
99
|
...(prst ? { geometry: prst } : {}),
|
|
88
100
|
rawXml: drawingXml,
|
|
89
101
|
};
|
|
@@ -131,6 +143,19 @@ export function parseVmlXml(pictXml: string): ParsedVmlShape | null {
|
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
// ---- Raw XML extraction helpers ----
|
|
147
|
+
|
|
148
|
+
function extractRawXml(node: XmlElementNode, sourceXml: string): string | undefined {
|
|
149
|
+
// Find the txbxContent element boundaries in the source XML by tag name
|
|
150
|
+
const tagName = node.name;
|
|
151
|
+
const openIdx = sourceXml.indexOf(`<${tagName}`);
|
|
152
|
+
if (openIdx < 0) return undefined;
|
|
153
|
+
const closeTag = `</${tagName}>`;
|
|
154
|
+
const closeIdx = sourceXml.indexOf(closeTag, openIdx);
|
|
155
|
+
if (closeIdx < 0) return undefined;
|
|
156
|
+
return sourceXml.slice(openIdx, closeIdx + closeTag.length);
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
// ---- Text extraction helpers ----
|
|
135
160
|
|
|
136
161
|
function extractAllText(node: XmlElementNode): string {
|