@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
|
@@ -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) =>
|
package/src/legal/bookmarks.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
BookmarkStartNode,
|
|
5
5
|
CanonicalDocument,
|
|
6
6
|
DocumentNode,
|
|
7
|
+
FieldRegistry,
|
|
8
|
+
FieldRegistryEntry,
|
|
7
9
|
} from "../model/canonical-document.ts";
|
|
8
10
|
|
|
9
11
|
export interface LegalBookmark {
|
|
@@ -210,6 +212,48 @@ function compareBookmarks(left: LegalBookmark, right: LegalBookmark): number {
|
|
|
210
212
|
);
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Build a lookup map from bookmark name to bookmarkId for the document.
|
|
217
|
+
* Used by the field refresh runtime to resolve REF/PAGEREF/NOTEREF targets.
|
|
218
|
+
*/
|
|
219
|
+
export function buildBookmarkNameMap(
|
|
220
|
+
document: Pick<CanonicalDocument, "content"> | DocumentNode,
|
|
221
|
+
): Map<string, { bookmarkId: string; paragraphIndex: number }> {
|
|
222
|
+
const root = "content" in document ? document.content : document;
|
|
223
|
+
const map = new Map<string, { bookmarkId: string; paragraphIndex: number }>();
|
|
224
|
+
let paragraphIndex = -1;
|
|
225
|
+
|
|
226
|
+
walkDocument(root, (node) => {
|
|
227
|
+
if (node.type === "paragraph") {
|
|
228
|
+
paragraphIndex += 1;
|
|
229
|
+
}
|
|
230
|
+
if (node.type === "bookmark_start" && node.name) {
|
|
231
|
+
map.set(node.name, { bookmarkId: node.bookmarkId, paragraphIndex });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return map;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve field-to-bookmark dependencies from a field registry.
|
|
240
|
+
* Returns a map of bookmark names to the field entries that reference them.
|
|
241
|
+
* This enables the runtime to determine which fields need refresh when
|
|
242
|
+
* a bookmark's content changes.
|
|
243
|
+
*/
|
|
244
|
+
export function resolveBookmarkFieldDependencies(
|
|
245
|
+
registry: FieldRegistry,
|
|
246
|
+
): Map<string, FieldRegistryEntry[]> {
|
|
247
|
+
const deps = new Map<string, FieldRegistryEntry[]>();
|
|
248
|
+
for (const entry of registry.supported) {
|
|
249
|
+
if (!entry.fieldTarget) continue;
|
|
250
|
+
const existing = deps.get(entry.fieldTarget) ?? [];
|
|
251
|
+
existing.push(entry);
|
|
252
|
+
deps.set(entry.fieldTarget, existing);
|
|
253
|
+
}
|
|
254
|
+
return deps;
|
|
255
|
+
}
|
|
256
|
+
|
|
213
257
|
function walkDocument(node: DocumentNode, visit: (node: DocumentNode) => void): void {
|
|
214
258
|
visit(node);
|
|
215
259
|
|
|
@@ -7,8 +7,10 @@ import type {
|
|
|
7
7
|
CanonicalDocument,
|
|
8
8
|
DocumentNode,
|
|
9
9
|
FieldNode,
|
|
10
|
+
FieldRegistry,
|
|
10
11
|
HyperlinkNode,
|
|
11
12
|
ParagraphNode,
|
|
13
|
+
TocEntry,
|
|
12
14
|
} from "../model/canonical-document.ts";
|
|
13
15
|
|
|
14
16
|
export interface CrossReferencePattern {
|
|
@@ -33,6 +35,40 @@ interface FieldReference {
|
|
|
33
35
|
instruction: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Collect field references from a canonical document, leveraging the
|
|
40
|
+
* field family classification when available.
|
|
41
|
+
*/
|
|
42
|
+
export function collectFieldReferencesFromCanonicalDocument(
|
|
43
|
+
document: Pick<CanonicalDocument, "content"> | DocumentNode,
|
|
44
|
+
): Array<{ family: string; target: string; instruction: string; paragraphIndex: number; displayText: string }> {
|
|
45
|
+
const root = "content" in document ? document.content : document;
|
|
46
|
+
const results: Array<{ family: string; target: string; instruction: string; paragraphIndex: number; displayText: string }> = [];
|
|
47
|
+
let paragraphIndex = -1;
|
|
48
|
+
|
|
49
|
+
walkDocument(root, (node) => {
|
|
50
|
+
if (node.type === "paragraph") {
|
|
51
|
+
paragraphIndex += 1;
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
if (child.type === "field" && child.fieldFamily && child.fieldTarget) {
|
|
54
|
+
const family = child.fieldFamily;
|
|
55
|
+
if (family === "REF" || family === "PAGEREF" || family === "NOTEREF") {
|
|
56
|
+
results.push({
|
|
57
|
+
family,
|
|
58
|
+
target: child.fieldTarget,
|
|
59
|
+
instruction: child.instruction,
|
|
60
|
+
paragraphIndex,
|
|
61
|
+
displayText: flattenInlineText(child.children).trim(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
36
72
|
const CROSS_REFERENCE_PATTERN =
|
|
37
73
|
/\b(Section|Clause|Article|Schedule|Exhibit|Appendix)\s+(\d+(?:\.\d+)*|[A-Z]-\d+|[A-Z])(?=[^A-Za-z0-9]|$)/g;
|
|
38
74
|
const W_REF_PATTERN = /<w:ref\b([^>]*)\/?>/g;
|
|
@@ -281,7 +317,7 @@ function flattenParagraphText(paragraph: ParagraphNode): string {
|
|
|
281
317
|
case "hyperlink":
|
|
282
318
|
return flattenInlineText(child.children);
|
|
283
319
|
case "field":
|
|
284
|
-
return flattenInlineText(child.children);
|
|
320
|
+
return child.children ? flattenInlineText(child.children) : "";
|
|
285
321
|
case "tab":
|
|
286
322
|
return "\t";
|
|
287
323
|
case "hard_break":
|
|
@@ -335,6 +371,28 @@ function dedupeCrossReferences(references: CrossReference[]): CrossReference[] {
|
|
|
335
371
|
return deduped;
|
|
336
372
|
}
|
|
337
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Collect TOC entries from a field registry's tocStructure.
|
|
376
|
+
* Returns the entries in document order with their heading text and levels.
|
|
377
|
+
* Returns an empty array if no TOC structure is available.
|
|
378
|
+
*/
|
|
379
|
+
export function collectTocEntriesFromRegistry(
|
|
380
|
+
registry: FieldRegistry | undefined,
|
|
381
|
+
): TocEntry[] {
|
|
382
|
+
return registry?.tocStructure?.entries ?? [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Assess whether the TOC structure in a field registry is current
|
|
387
|
+
* with respect to the document's heading structure.
|
|
388
|
+
*/
|
|
389
|
+
export function assessTocFreshness(
|
|
390
|
+
registry: FieldRegistry | undefined,
|
|
391
|
+
): "current" | "stale" | "absent" {
|
|
392
|
+
if (!registry?.tocStructure) return "absent";
|
|
393
|
+
return registry.tocStructure.status;
|
|
394
|
+
}
|
|
395
|
+
|
|
338
396
|
function walkDocument(node: DocumentNode, visit: (node: DocumentNode) => void): void {
|
|
339
397
|
visit(node);
|
|
340
398
|
|