@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.
Files changed (71) hide show
  1. package/package.json +44 -25
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. package/src/validation/docx-comment-proof.ts +220 -0
@@ -26,6 +26,8 @@ import {
26
26
  import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
27
27
  import { parseComplexContentXml } from "./parse-complex-content.ts";
28
28
  import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
29
+ import { classifyFieldInstruction } from "./parse-fields.ts";
30
+ import { resolveHighlightColor } from "./highlight-colors.ts";
29
31
 
30
32
  export interface ParsedMainDocument {
31
33
  blocks: ParsedBlockNode[];
@@ -56,6 +58,7 @@ export interface ParsedParagraphNode {
56
58
  };
57
59
  alignment?: "left" | "center" | "right" | "both" | "distribute";
58
60
  spacing?: ParagraphSpacing;
61
+ contextualSpacing?: boolean;
59
62
  indentation?: ParagraphIndentation;
60
63
  tabStops?: TabStop[];
61
64
  keepNext?: boolean;
@@ -90,7 +93,10 @@ export type ParsedInlineNode =
90
93
  | ParsedVmlShapeInlineNode
91
94
  | ParsedBookmarkStartInlineNode
92
95
  | ParsedBookmarkEndInlineNode
93
- | ParsedFieldInlineNode;
96
+ | ParsedFootnoteRefInlineNode
97
+ | ParsedFieldInlineNode
98
+ | ParsedPermStartInlineNode
99
+ | ParsedPermEndInlineNode;
94
100
 
95
101
  export interface ParsedTextNode {
96
102
  type: "text";
@@ -125,6 +131,8 @@ export interface ParsedImageNode {
125
131
  contentType?: string;
126
132
  filename?: string;
127
133
  altText?: string;
134
+ widthEmu?: number;
135
+ heightEmu?: number;
128
136
  placementXml?: string;
129
137
  display?: "inline" | "floating";
130
138
  floating?: {
@@ -204,11 +212,32 @@ export interface ParsedBookmarkEndInlineNode {
204
212
  rawXml: string;
205
213
  }
206
214
 
215
+ export interface ParsedFootnoteRefInlineNode {
216
+ type: "footnote_ref";
217
+ noteId: string;
218
+ noteKind: "footnote" | "endnote";
219
+ }
220
+
207
221
  export interface ParsedFieldInlineNode {
208
222
  type: "field";
209
- fieldType: "simple";
223
+ fieldType: "simple" | "complex";
210
224
  instruction: string;
211
- contentXml: string;
225
+ contentXml?: string;
226
+ children?: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
227
+ rawXml: string;
228
+ }
229
+
230
+ export interface ParsedPermStartInlineNode {
231
+ type: "perm_start";
232
+ rangeId: string;
233
+ editorGroup?: string;
234
+ editor?: string;
235
+ rawXml: string;
236
+ }
237
+
238
+ export interface ParsedPermEndInlineNode {
239
+ type: "perm_end";
240
+ rangeId: string;
212
241
  rawXml: string;
213
242
  }
214
243
 
@@ -412,6 +441,7 @@ function parseBodyChild(
412
441
  let numbering: ParsedParagraphNode["numbering"];
413
442
  let alignment: ParsedParagraphNode["alignment"];
414
443
  let spacing: ParsedParagraphNode["spacing"];
444
+ let contextualSpacing: ParsedParagraphNode["contextualSpacing"];
415
445
  let indentation: ParsedParagraphNode["indentation"];
416
446
  let tabStops: ParsedParagraphNode["tabStops"];
417
447
  let keepNext: ParsedParagraphNode["keepNext"];
@@ -428,6 +458,11 @@ function parseBodyChild(
428
458
  let sectionPropertiesXml: string | undefined;
429
459
  let paragraphSupported = true;
430
460
  const children: ParsedInlineNode[] = [];
461
+ let activeComplexField: {
462
+ instruction: string;
463
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
464
+ mode: "instruction" | "result";
465
+ } | null = null;
431
466
 
432
467
  for (const child of node.children) {
433
468
  if (child.type !== "element") {
@@ -440,6 +475,7 @@ function parseBodyChild(
440
475
  numbering = readParagraphNumbering(child);
441
476
  alignment = readParagraphAlignment(child);
442
477
  spacing = readParagraphSpacing(child);
478
+ contextualSpacing = readOptionalOnOffParagraphProperty(child, "contextualSpacing");
443
479
  indentation = readParagraphIndentation(child);
444
480
  tabStops = readParagraphTabStops(child);
445
481
  keepNext = readOnOffParagraphProperty(child, "keepNext");
@@ -457,33 +493,148 @@ function parseBodyChild(
457
493
  paragraphSupported = paragraphSupported && supportsParagraphProperties(child);
458
494
  break;
459
495
  case "r":
460
- children.push(...parseRun(child, sourceXml, relationships, mediaParts, sourcePartPath));
496
+ activeComplexField = appendParagraphRunNodes(
497
+ child,
498
+ children,
499
+ activeComplexField,
500
+ sourceXml,
501
+ relationships,
502
+ mediaParts,
503
+ sourcePartPath,
504
+ );
461
505
  break;
462
506
  case "hyperlink": {
507
+ flushActiveComplexField(children, () => {
508
+ activeComplexField = null;
509
+ }, activeComplexField);
463
510
  const hyperlink = parseHyperlink(child, sourceXml, relationshipMap);
464
511
  children.push(hyperlink);
465
512
  break;
466
513
  }
467
514
  case "ins":
468
515
  case "del": {
516
+ flushActiveComplexField(children, () => {
517
+ activeComplexField = null;
518
+ }, activeComplexField);
469
519
  children.push(...parseRevisionContainer(child, sourceXml, relationshipMap));
470
520
  break;
471
521
  }
472
522
  case "commentRangeStart":
473
523
  case "commentRangeEnd":
474
524
  break;
475
- case "bookmarkStart":
476
- case "bookmarkEnd":
477
- case "fldSimple":
478
- case "permStart":
479
- case "permEnd":
525
+ case "bookmarkStart": {
526
+ const bkId = child.attributes["w:id"] ?? child.attributes.id ?? "";
527
+ const bkName = child.attributes["w:name"] ?? child.attributes.name ?? "";
528
+ if (bkId) {
529
+ const bookmarkNode = {
530
+ type: "bookmark_start",
531
+ bookmarkId: bkId,
532
+ name: bkName,
533
+ rawXml: sourceXml.slice(child.start, child.end),
534
+ } as ParsedBookmarkStartInlineNode;
535
+ flushActiveComplexField(children, () => {
536
+ activeComplexField = null;
537
+ }, activeComplexField);
538
+ children.push(bookmarkNode);
539
+ } else {
540
+ flushActiveComplexField(children, () => {
541
+ activeComplexField = null;
542
+ }, activeComplexField);
543
+ children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
544
+ }
545
+ break;
546
+ }
547
+ case "bookmarkEnd": {
548
+ const bkEndId = child.attributes["w:id"] ?? child.attributes.id ?? "";
549
+ if (bkEndId) {
550
+ const bookmarkNode = {
551
+ type: "bookmark_end",
552
+ bookmarkId: bkEndId,
553
+ rawXml: sourceXml.slice(child.start, child.end),
554
+ } as ParsedBookmarkEndInlineNode;
555
+ flushActiveComplexField(children, () => {
556
+ activeComplexField = null;
557
+ }, activeComplexField);
558
+ children.push(bookmarkNode);
559
+ } else {
560
+ flushActiveComplexField(children, () => {
561
+ activeComplexField = null;
562
+ }, activeComplexField);
563
+ children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
564
+ }
565
+ break;
566
+ }
567
+ case "fldSimple": {
568
+ flushActiveComplexField(children, () => {
569
+ activeComplexField = null;
570
+ }, activeComplexField);
571
+ const fieldInstr = child.attributes["w:instr"] ?? child.attributes.instr ?? "";
572
+ const fieldContentXml = child.children
573
+ .filter((c): c is XmlElementNode => c.type === "element")
574
+ .map((c) => sourceXml.slice(c.start, c.end))
575
+ .join("");
576
+ children.push({
577
+ type: "field",
578
+ fieldType: "simple",
579
+ instruction: fieldInstr,
580
+ contentXml: fieldContentXml,
581
+ rawXml: sourceXml.slice(child.start, child.end),
582
+ } as ParsedFieldInlineNode);
583
+ break;
584
+ }
480
585
  case "proofErr":
586
+ flushActiveComplexField(children, () => {
587
+ activeComplexField = null;
588
+ }, activeComplexField);
481
589
  children.push({
482
590
  type: "opaque_inline",
483
591
  rawXml: sourceXml.slice(child.start, child.end),
484
592
  });
485
593
  break;
594
+ case "permStart":
595
+ flushActiveComplexField(children, () => {
596
+ activeComplexField = null;
597
+ }, activeComplexField);
598
+ children.push(parsePermStartNode(child, sourceXml));
599
+ break;
600
+ case "permEnd":
601
+ flushActiveComplexField(children, () => {
602
+ activeComplexField = null;
603
+ }, activeComplexField);
604
+ children.push(parsePermEndNode(child, sourceXml));
605
+ break;
606
+ case "footnoteReference": {
607
+ flushActiveComplexField(children, () => {
608
+ activeComplexField = null;
609
+ }, activeComplexField);
610
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
611
+ if (noteId) {
612
+ children.push({
613
+ type: "footnote_ref",
614
+ noteId,
615
+ noteKind: "footnote",
616
+ });
617
+ }
618
+ break;
619
+ }
620
+ case "endnoteReference": {
621
+ flushActiveComplexField(children, () => {
622
+ activeComplexField = null;
623
+ }, activeComplexField);
624
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
625
+ if (noteId) {
626
+ children.push({
627
+ type: "footnote_ref",
628
+ noteId,
629
+ noteKind: "endnote",
630
+ });
631
+ }
632
+ break;
633
+ }
486
634
  default:
635
+ flushActiveComplexField(children, () => {
636
+ activeComplexField = null;
637
+ }, activeComplexField);
487
638
  children.push({
488
639
  type: "opaque_inline",
489
640
  rawXml: sourceXml.slice(child.start, child.end),
@@ -492,6 +643,10 @@ function parseBodyChild(
492
643
  }
493
644
  }
494
645
 
646
+ flushActiveComplexField(children, () => {
647
+ activeComplexField = null;
648
+ }, activeComplexField);
649
+
495
650
  if (!paragraphSupported) {
496
651
  return {
497
652
  type: "opaque_block",
@@ -505,6 +660,7 @@ function parseBodyChild(
505
660
  ...(numbering ? { numbering } : {}),
506
661
  ...(alignment ? { alignment } : {}),
507
662
  ...(spacing ? { spacing } : {}),
663
+ ...(contextualSpacing !== undefined ? { contextualSpacing } : {}),
508
664
  ...(indentation ? { indentation } : {}),
509
665
  ...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
510
666
  ...(keepNext ? { keepNext } : {}),
@@ -524,6 +680,114 @@ function parseBodyChild(
524
680
  };
525
681
  }
526
682
 
683
+ function appendParagraphRunNodes(
684
+ node: XmlElementNode,
685
+ children: ParsedInlineNode[],
686
+ activeComplexField: {
687
+ instruction: string;
688
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
689
+ mode: "instruction" | "result";
690
+ } | null,
691
+ sourceXml: string,
692
+ relationships: readonly OpcRelationship[],
693
+ mediaParts: ReadonlyMap<string, InlineMediaPart>,
694
+ sourcePartPath: string,
695
+ ): {
696
+ instruction: string;
697
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
698
+ mode: "instruction" | "result";
699
+ } | null {
700
+ const hasFieldMarkers = node.children.some(
701
+ (child) =>
702
+ child.type === "element" &&
703
+ (localName(child.name) === "fldChar" || localName(child.name) === "instrText"),
704
+ );
705
+
706
+ if (activeComplexField?.mode === "result" && !hasFieldMarkers) {
707
+ const run = parseRunContentOnly(node, sourceXml);
708
+ if (
709
+ run.supported &&
710
+ run.nodes.every(
711
+ (child) =>
712
+ child.type === "text" || child.type === "hard_break" || child.type === "tab",
713
+ )
714
+ ) {
715
+ activeComplexField.children.push(...run.nodes);
716
+ return activeComplexField;
717
+ }
718
+ }
719
+
720
+ if (!hasFieldMarkers) {
721
+ children.push(...parseRun(node, sourceXml, relationships, mediaParts, sourcePartPath));
722
+ return activeComplexField;
723
+ }
724
+
725
+ for (const child of node.children) {
726
+ if (child.type !== "element") {
727
+ continue;
728
+ }
729
+
730
+ const name = localName(child.name);
731
+ if (name === "fldChar") {
732
+ const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
733
+ if (fldType === "begin") {
734
+ activeComplexField = { instruction: "", children: [], mode: "instruction" };
735
+ } else if (fldType === "separate" && activeComplexField) {
736
+ activeComplexField.mode = "result";
737
+ } else if (fldType === "end" && activeComplexField) {
738
+ flushActiveComplexField(children, () => {
739
+ activeComplexField = null;
740
+ }, activeComplexField);
741
+ }
742
+ continue;
743
+ }
744
+
745
+ if (name === "instrText") {
746
+ const instruction = child.children
747
+ .filter((entry): entry is XmlTextNode => entry.type === "text")
748
+ .map((entry) => entry.text)
749
+ .join("");
750
+ if (activeComplexField) {
751
+ activeComplexField.instruction += instruction;
752
+ } else if (instruction.trim().length > 0) {
753
+ children.push({
754
+ type: "field",
755
+ fieldType: "complex",
756
+ instruction,
757
+ children: [],
758
+ rawXml: sourceXml.slice(node.start, node.end),
759
+ });
760
+ }
761
+ continue;
762
+ }
763
+ }
764
+
765
+ return activeComplexField;
766
+ }
767
+
768
+ function flushActiveComplexField(
769
+ children: ParsedInlineNode[],
770
+ reset: () => void,
771
+ activeComplexField: {
772
+ instruction: string;
773
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
774
+ mode: "instruction" | "result";
775
+ } | null,
776
+ ): void {
777
+ if (!activeComplexField || activeComplexField.instruction.trim().length === 0) {
778
+ return;
779
+ }
780
+
781
+ children.push({
782
+ type: "field",
783
+ fieldType: "complex",
784
+ instruction: activeComplexField.instruction,
785
+ children: activeComplexField.children,
786
+ rawXml: "",
787
+ });
788
+ reset();
789
+ }
790
+
527
791
  function parseTableElement(
528
792
  node: XmlElementNode,
529
793
  sourceXml: string,
@@ -918,9 +1182,35 @@ function readTableGridColumns(node: XmlElementNode): number[] {
918
1182
  */
919
1183
  function tableRequiresOpaquePreservation(rawXml: string): boolean {
920
1184
  // Safe table-local content now includes hyperlinks, bookmarks, comments,
921
- // nested tables, floating images, and VML preview atoms because the parser
922
- // and serializer can preserve them without degrading the whole table.
923
- return /<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|fldChar|fldSimple|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml);
1185
+ // nested tables, floating images, VML preview atoms, and bounded field
1186
+ // families already owned by the current field slice. Risky table-local
1187
+ // semantics still fail closed to preserve-only.
1188
+ if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml)) {
1189
+ return true;
1190
+ }
1191
+
1192
+ const fldSimpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
1193
+ const complexInstructions = [...rawXml.matchAll(/<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu)]
1194
+ .map((match) => decodeXmlEntities(match[1] ?? ""));
1195
+ for (const instruction of [...fldSimpleInstructions, ...complexInstructions]) {
1196
+ const classification = classifyFieldInstruction(instruction);
1197
+ if (!isSafeMainStoryTableFieldFamily(classification.family)) {
1198
+ return true;
1199
+ }
1200
+ }
1201
+
1202
+ return false;
1203
+ }
1204
+
1205
+ function isSafeMainStoryTableFieldFamily(family: string): boolean {
1206
+ return (
1207
+ family === "REF" ||
1208
+ family === "PAGEREF" ||
1209
+ family === "NOTEREF" ||
1210
+ family === "TOC" ||
1211
+ family === "PAGE" ||
1212
+ family === "NUMPAGES"
1213
+ );
924
1214
  }
925
1215
 
926
1216
  function readCellGridSpan(node: XmlElementNode): number | undefined {
@@ -1091,6 +1381,18 @@ function readOnOffParagraphProperty(node: XmlElementNode, name: string): boolean
1091
1381
  return val !== "false" && val !== "0" && val !== "off" ? true : undefined;
1092
1382
  }
1093
1383
 
1384
+ function readOptionalOnOffParagraphProperty(
1385
+ node: XmlElementNode,
1386
+ name: string,
1387
+ ): boolean | undefined {
1388
+ const propNode = node.children.find(
1389
+ (child): child is XmlElementNode => child.type === "element" && localName(child.name) === name,
1390
+ );
1391
+ if (!propNode) return undefined;
1392
+ const val = (propNode.attributes["w:val"] ?? propNode.attributes.val ?? "true").toLowerCase();
1393
+ return val !== "false" && val !== "0" && val !== "off";
1394
+ }
1395
+
1094
1396
  function readParagraphOutlineLevel(node: XmlElementNode): number | undefined {
1095
1397
  const propNode = node.children.find(
1096
1398
  (child): child is XmlElementNode => child.type === "element" && localName(child.name) === "outlineLvl",
@@ -1355,6 +1657,8 @@ function parseRun(
1355
1657
  contentType: media.contentType,
1356
1658
  filename: media.filename,
1357
1659
  ...(media.altText ? { altText: media.altText } : {}),
1660
+ ...(media.widthEmu !== undefined ? { widthEmu: media.widthEmu } : {}),
1661
+ ...(media.heightEmu !== undefined ? { heightEmu: media.heightEmu } : {}),
1358
1662
  placementXml,
1359
1663
  ...(media.display ? { display: media.display } : {}),
1360
1664
  ...(media.floating ? { floating: media.floating } : {}),
@@ -1387,6 +1691,28 @@ function parseRun(
1387
1691
  }
1388
1692
  case "commentReference":
1389
1693
  break;
1694
+ case "footnoteReference": {
1695
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
1696
+ if (noteId) {
1697
+ result.push({
1698
+ type: "footnote_ref",
1699
+ noteId,
1700
+ noteKind: "footnote",
1701
+ });
1702
+ }
1703
+ break;
1704
+ }
1705
+ case "endnoteReference": {
1706
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
1707
+ if (noteId) {
1708
+ result.push({
1709
+ type: "footnote_ref",
1710
+ noteId,
1711
+ noteKind: "endnote",
1712
+ });
1713
+ }
1714
+ break;
1715
+ }
1390
1716
  case "lastRenderedPageBreak":
1391
1717
  case "proofErr":
1392
1718
  result.push({
@@ -1466,8 +1792,6 @@ function parseRevisionContainer(
1466
1792
  case "commentRangeEnd":
1467
1793
  case "bookmarkStart":
1468
1794
  case "bookmarkEnd":
1469
- case "permStart":
1470
- case "permEnd":
1471
1795
  case "proofErr":
1472
1796
  case "lastRenderedPageBreak":
1473
1797
  return [
@@ -1476,6 +1800,10 @@ function parseRevisionContainer(
1476
1800
  rawXml: sourceXml.slice(node.start, node.end),
1477
1801
  },
1478
1802
  ];
1803
+ case "permStart":
1804
+ return [parsePermStartNode(node, sourceXml)];
1805
+ case "permEnd":
1806
+ return [parsePermEndNode(node, sourceXml)];
1479
1807
  default:
1480
1808
  return [
1481
1809
  {
@@ -1697,6 +2025,11 @@ function readRunMarks(node: XmlElementNode, sourceXml: string): MarksParseResult
1697
2025
  marks.push(backgroundColorMark);
1698
2026
  }
1699
2027
 
2028
+ const highlightMark = readRunHighlight(properties);
2029
+ if (highlightMark) {
2030
+ marks.push(highlightMark);
2031
+ }
2032
+
1700
2033
  const charSpacingMark = readNumericRunMark(properties, "spacing", "charSpacing");
1701
2034
  if (charSpacingMark) {
1702
2035
  marks.push(charSpacingMark);
@@ -1809,6 +2142,28 @@ function readRunBackgroundColor(properties: XmlElementNode): TextMark | undefine
1809
2142
  return { type: "backgroundColor", color: fill };
1810
2143
  }
1811
2144
 
2145
+ function readRunHighlight(properties: XmlElementNode): TextMark | undefined {
2146
+ const highlightNode = properties.children.find(
2147
+ (child): child is XmlElementNode =>
2148
+ child.type === "element" && localName(child.name) === "highlight",
2149
+ );
2150
+ if (!highlightNode) {
2151
+ return undefined;
2152
+ }
2153
+
2154
+ const highlightValue = highlightNode.attributes["w:val"] ?? highlightNode.attributes.val;
2155
+ const resolvedHighlight = resolveHighlightColor(highlightValue);
2156
+ if (!resolvedHighlight) {
2157
+ return undefined;
2158
+ }
2159
+
2160
+ return {
2161
+ type: "highlight",
2162
+ color: resolvedHighlight.color,
2163
+ val: resolvedHighlight.val,
2164
+ };
2165
+ }
2166
+
1812
2167
  function readNumericRunMark(
1813
2168
  properties: XmlElementNode,
1814
2169
  elementName: "spacing" | "kern" | "position",
@@ -2412,3 +2767,30 @@ function parseBorderSpec(node: XmlElementNode): BorderSpec | undefined {
2412
2767
 
2413
2768
  return Object.keys(border).length > 0 ? border : undefined;
2414
2769
  }
2770
+
2771
+ function parsePermStartNode(
2772
+ node: XmlElementNode,
2773
+ sourceXml: string,
2774
+ ): ParsedPermStartInlineNode {
2775
+ const rangeId = node.attributes["w:id"] ?? node.attributes.id ?? "";
2776
+ const edGrp = node.attributes["w:edGrp"] ?? node.attributes.edGrp;
2777
+ const ed = node.attributes["w:ed"] ?? node.attributes.ed;
2778
+ return {
2779
+ type: "perm_start",
2780
+ rangeId,
2781
+ ...(edGrp ? { editorGroup: edGrp } : {}),
2782
+ ...(ed ? { editor: ed } : {}),
2783
+ rawXml: sourceXml.slice(node.start, node.end),
2784
+ };
2785
+ }
2786
+
2787
+ function parsePermEndNode(
2788
+ node: XmlElementNode,
2789
+ sourceXml: string,
2790
+ ): ParsedPermEndInlineNode {
2791
+ return {
2792
+ type: "perm_end",
2793
+ rangeId: node.attributes["w:id"] ?? node.attributes.id ?? "",
2794
+ rawXml: sourceXml.slice(node.start, node.end),
2795
+ };
2796
+ }