@beyondwork/docx-react-component 1.0.58 → 1.0.60

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 (135) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +980 -10
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +4 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +72 -42
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/workflow-payload.ts +6 -1
  69. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  70. package/src/io/ooxml/xml-parser.ts +183 -0
  71. package/src/legal/bookmarks.ts +1 -1
  72. package/src/legal/cross-references.ts +1 -1
  73. package/src/legal/defined-terms.ts +1 -1
  74. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  75. package/src/legal/signature-blocks.ts +1 -1
  76. package/src/model/canonical-document.ts +159 -6
  77. package/src/model/chart-types.ts +439 -0
  78. package/src/model/snapshot.ts +5 -1
  79. package/src/review/store/comment-remapping.ts +24 -11
  80. package/src/review/store/revision-actions.ts +482 -2
  81. package/src/review/store/revision-store.ts +15 -0
  82. package/src/review/store/revision-types.ts +76 -0
  83. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  84. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  85. package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
  86. package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
  87. package/src/runtime/document-runtime.ts +821 -54
  88. package/src/runtime/document-search.ts +115 -0
  89. package/src/runtime/edit-ops/index.ts +18 -2
  90. package/src/runtime/footnote-resolver.ts +130 -0
  91. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  92. package/src/runtime/layout/layout-engine-version.ts +37 -1
  93. package/src/runtime/layout/page-graph.ts +14 -1
  94. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  95. package/src/runtime/numbering-prefix.ts +17 -0
  96. package/src/runtime/query-scopes.ts +108 -10
  97. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  98. package/src/runtime/revision-runtime.ts +27 -1
  99. package/src/runtime/selection/post-edit-validator.ts +60 -6
  100. package/src/runtime/structure-ops/index.ts +20 -4
  101. package/src/runtime/surface-projection.ts +290 -21
  102. package/src/runtime/table-schema.ts +6 -0
  103. package/src/runtime/theme-color-resolver.ts +2 -2
  104. package/src/runtime/units.ts +9 -0
  105. package/src/runtime/workflow-rail-segments.ts +4 -0
  106. package/src/ui/WordReviewEditor.tsx +187 -43
  107. package/src/ui/editor-runtime-boundary.ts +10 -0
  108. package/src/ui/editor-shell-view.tsx +4 -1
  109. package/src/ui/headless/chrome-registry.ts +53 -0
  110. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  111. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  112. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  113. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  114. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  115. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  116. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  117. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
  118. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
  119. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  120. package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
  121. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
  122. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  124. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  125. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  126. package/src/ui-tailwind/index.ts +9 -0
  127. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  128. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  129. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  130. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  131. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  132. package/src/ui-tailwind/theme/tokens.ts +14 -0
  133. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  134. package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
  135. package/src/validation/diagnostics.ts +1 -0
@@ -84,5 +84,11 @@ export interface ScopeMetadataResolver {
84
84
  version: number;
85
85
  }>;
86
86
 
87
+ /**
88
+ * Release a ref that is no longer needed (called when toggling a
89
+ * scope from `"external"` back to `"internal"`). Optional — hosts
90
+ * that don't need cleanup may omit it. Rejections are surfaced as
91
+ * non-fatal warnings; the scope continues to function internally.
92
+ */
87
93
  delete?(ref: ScopeMetadataStorageRef): Promise<void>;
88
94
  }
@@ -523,6 +523,7 @@ function getInlineLength(node: InlineNode): number {
523
523
  case "wordart":
524
524
  case "vml_shape":
525
525
  case "drawing_frame":
526
+ case "ole_embed":
526
527
  return 1;
527
528
  }
528
529
  }
@@ -584,6 +585,8 @@ function getInlineDisplayText(node: InlineNode): string {
584
585
  return node.text ?? "[VML Shape]";
585
586
  case "drawing_frame":
586
587
  return node.content.type === "picture" ? "[Image]" : "[Drawing]";
588
+ case "ole_embed":
589
+ return "[Embedded object]";
587
590
  }
588
591
  }
589
592
 
@@ -1041,6 +1041,7 @@ function inlineNodeLength(node: InlineNode): number {
1041
1041
  case "wordart":
1042
1042
  case "vml_shape":
1043
1043
  case "drawing_frame":
1044
+ case "ole_embed":
1044
1045
  return 1;
1045
1046
  case "field":
1046
1047
  return node.children.reduce<number>(
@@ -8,6 +8,7 @@ import {
8
8
  type SelectionSnapshot,
9
9
  } from "../state/editor-state.ts";
10
10
  import {
11
+ anchorUnaffectedByMapping,
11
12
  areAnchorsEqual,
12
13
  createDetachedAnchor,
13
14
  createEmptyMapping,
@@ -89,6 +90,14 @@ import {
89
90
  setHeaderFooterLinkAtSectionIndex,
90
91
  } from "./section-layout-commands.ts";
91
92
  import { insertPageBreak, insertTable } from "./text-commands.ts";
93
+ // NOTE: This file imports `editLayer` from `runtime/edit-ops/index.ts`,
94
+ // which in turn imports command primitives from `./text-commands.ts`. That
95
+ // is a **diamond** dependency (A → B, A → C, B → C), NOT a module-level
96
+ // cycle — `text-commands.ts` does not re-enter `core/commands/index.ts`
97
+ // or `runtime/edit-ops/`. Verified 2026-04-20 by static DFS; see
98
+ // docs/plans/architecture-lane.md §F3. Keep it this way — don't add
99
+ // imports from `runtime/` that route through this file, as that could
100
+ // introduce a real cycle.
92
101
  import { editLayer } from "../../runtime/edit-ops/index.ts";
93
102
  import { structureLayer } from "../../runtime/structure-ops/index.ts";
94
103
  import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
@@ -411,6 +420,14 @@ export type EditorCommand =
411
420
  export interface TransactionEffects {
412
421
  warningsAdded: EditorWarning[];
413
422
  warningsCleared: Array<{ warningId: string; code: EditorWarning["code"] }>;
423
+ /**
424
+ * Fire-and-forget warnings emitted by the `notify` loop as `warning_added`
425
+ * events + `onWarning` callbacks, but NOT written to `state.warnings` or
426
+ * `CompatibilityReport.warnings`. Used for no-op diagnostic signaling such
427
+ * as `review_target_not_found` — the host should be told an op was
428
+ * skipped without accumulating a persistent entry every time.
429
+ */
430
+ transientWarnings?: EditorWarning[];
414
431
  commentAdded?: { commentId: string; anchor: EditorAnchorProjection };
415
432
  commentResolved?: { commentId: string };
416
433
  commentReopened?: { commentId: string };
@@ -800,6 +817,16 @@ export function executeEditorCommand(
800
817
  return createTransaction(state, {
801
818
  historyBoundary: "skip",
802
819
  markDirty: false,
820
+ effects: {
821
+ transientWarnings: [
822
+ reviewTargetNotFoundWarning(
823
+ "resolveComment",
824
+ command.commentId,
825
+ "comment_unknown",
826
+ "unknown commentId",
827
+ ),
828
+ ],
829
+ },
803
830
  });
804
831
  }
805
832
 
@@ -847,8 +874,38 @@ export function executeEditorCommand(
847
874
  }
848
875
  case "comment.reopen": {
849
876
  const existingThread = state.document.review.comments[command.commentId];
850
- if (!existingThread || existingThread.status === "detached") {
851
- return createTransaction(state, { historyBoundary: "skip", markDirty: false });
877
+ if (!existingThread) {
878
+ return createTransaction(state, {
879
+ historyBoundary: "skip",
880
+ markDirty: false,
881
+ effects: {
882
+ transientWarnings: [
883
+ reviewTargetNotFoundWarning(
884
+ "reopenComment",
885
+ command.commentId,
886
+ "comment_unknown",
887
+ "unknown commentId",
888
+ ),
889
+ ],
890
+ },
891
+ });
892
+ }
893
+ if (existingThread.status === "detached") {
894
+ return createTransaction(state, {
895
+ historyBoundary: "skip",
896
+ markDirty: false,
897
+ effects: {
898
+ transientWarnings: [
899
+ reviewTargetNotFoundWarning(
900
+ "reopenComment",
901
+ command.commentId,
902
+ "comment_detached",
903
+ "thread is detached",
904
+ { status: "detached" },
905
+ ),
906
+ ],
907
+ },
908
+ });
852
909
  }
853
910
  const reopenedComments = {
854
911
  ...state.document.review.comments,
@@ -880,11 +937,38 @@ export function executeEditorCommand(
880
937
  case "comment.add-reply": {
881
938
  const threadForReply = state.document.review.comments[command.commentId];
882
939
  if (!threadForReply) {
883
- return createTransaction(state, { historyBoundary: "skip", markDirty: false });
940
+ return createTransaction(state, {
941
+ historyBoundary: "skip",
942
+ markDirty: false,
943
+ effects: {
944
+ transientWarnings: [
945
+ reviewTargetNotFoundWarning(
946
+ "addCommentReply",
947
+ command.commentId,
948
+ "comment_unknown",
949
+ "unknown commentId",
950
+ ),
951
+ ],
952
+ },
953
+ });
884
954
  }
885
955
  // Block reply on resolved or detached threads (must reopen first)
886
956
  if (threadForReply.status === "resolved" || threadForReply.status === "detached") {
887
- return createTransaction(state, { historyBoundary: "skip", markDirty: false });
957
+ return createTransaction(state, {
958
+ historyBoundary: "skip",
959
+ markDirty: false,
960
+ effects: {
961
+ transientWarnings: [
962
+ reviewTargetNotFoundWarning(
963
+ "addCommentReply",
964
+ command.commentId,
965
+ "comment_status",
966
+ `thread is ${threadForReply.status}`,
967
+ { status: threadForReply.status },
968
+ ),
969
+ ],
970
+ },
971
+ });
888
972
  }
889
973
  const entryId = `${command.commentId}-entry-${(threadForReply.entries?.length ?? 0) + 1}`;
890
974
  const newEntry = {
@@ -919,8 +1003,37 @@ export function executeEditorCommand(
919
1003
  }
920
1004
  case "comment.edit-body": {
921
1005
  const threadToEdit = state.document.review.comments[command.commentId];
922
- if (!threadToEdit || !threadToEdit.entries?.length) {
923
- return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1006
+ if (!threadToEdit) {
1007
+ return createTransaction(state, {
1008
+ historyBoundary: "skip",
1009
+ markDirty: false,
1010
+ effects: {
1011
+ transientWarnings: [
1012
+ reviewTargetNotFoundWarning(
1013
+ "editCommentBody",
1014
+ command.commentId,
1015
+ "comment_unknown",
1016
+ "unknown commentId",
1017
+ ),
1018
+ ],
1019
+ },
1020
+ });
1021
+ }
1022
+ if (!threadToEdit.entries?.length) {
1023
+ return createTransaction(state, {
1024
+ historyBoundary: "skip",
1025
+ markDirty: false,
1026
+ effects: {
1027
+ transientWarnings: [
1028
+ reviewTargetNotFoundWarning(
1029
+ "editCommentBody",
1030
+ command.commentId,
1031
+ "comment_no_entries",
1032
+ "thread has no entries to edit",
1033
+ ),
1034
+ ],
1035
+ },
1036
+ });
924
1037
  }
925
1038
  const editedEntries = [...threadToEdit.entries];
926
1039
  editedEntries[0] = { ...editedEntries[0], body: command.body };
@@ -1443,6 +1556,9 @@ function createTransaction(
1443
1556
  effects: {
1444
1557
  warningsAdded: options.effects?.warningsAdded ?? [],
1445
1558
  warningsCleared: options.effects?.warningsCleared ?? [],
1559
+ ...(options.effects?.transientWarnings
1560
+ ? { transientWarnings: options.effects.transientWarnings }
1561
+ : {}),
1446
1562
  commentAdded: options.effects?.commentAdded,
1447
1563
  commentResolved: options.effects?.commentResolved,
1448
1564
  changeAccepted: options.effects?.changeAccepted,
@@ -1453,6 +1569,71 @@ function createTransaction(
1453
1569
  };
1454
1570
  }
1455
1571
 
1572
+ /**
1573
+ * Classify why `applyReviewCommand` produced no applied outcome so hosts can
1574
+ * branch on `details.reason`. Inspects the pre-command revision store.
1575
+ */
1576
+ function classifyReviewSkipReason(
1577
+ store: RevisionStore,
1578
+ command: ReviewCommand,
1579
+ ): { reason: string; message: string; extraDetails?: Record<string, unknown> } {
1580
+ if (command.type === "review.accept-all-revisions" || command.type === "review.reject-all-revisions") {
1581
+ return {
1582
+ reason: "no_actionable_revisions",
1583
+ message: "no actionable revisions to apply",
1584
+ };
1585
+ }
1586
+ const existing = store.revisions[command.revisionId];
1587
+ if (!existing) {
1588
+ return { reason: "revision_unknown", message: "unknown revisionId" };
1589
+ }
1590
+ if (existing.status === "detached") {
1591
+ return {
1592
+ reason: "revision_detached",
1593
+ message: "revision anchor is detached",
1594
+ extraDetails: { status: "detached" },
1595
+ };
1596
+ }
1597
+ if (existing.status === "accepted" || existing.status === "rejected") {
1598
+ return {
1599
+ reason: "revision_status",
1600
+ message: `revision is already ${existing.status}`,
1601
+ extraDetails: { status: existing.status },
1602
+ };
1603
+ }
1604
+ return {
1605
+ reason: "revision_preserve_only",
1606
+ message: "revision is preserve-only and cannot be accepted or rejected",
1607
+ extraDetails: { kind: existing.kind },
1608
+ };
1609
+ }
1610
+
1611
+ /**
1612
+ * Build a fire-and-forget `review_target_not_found` warning for a review op
1613
+ * whose target id was unknown, resolved, detached, or preserve-only. Emitted
1614
+ * via `effects.transientWarnings` — surfaces on `onWarning` + `warning_added`
1615
+ * event without accumulating in `state.warnings` or `CompatibilityReport`.
1616
+ *
1617
+ * `reason` is a machine-readable code consumers can branch on; `message`
1618
+ * is the human-readable string that lands on `EditorWarning.message`.
1619
+ */
1620
+ function reviewTargetNotFoundWarning(
1621
+ op: string,
1622
+ targetId: string,
1623
+ reason: string,
1624
+ message: string,
1625
+ extraDetails?: Record<string, unknown>,
1626
+ ): EditorWarning {
1627
+ return {
1628
+ warningId: `review-target-not-found-${op}-${targetId}-${Date.now()}`,
1629
+ code: "review_target_not_found",
1630
+ severity: "info",
1631
+ message: `${op}("${targetId}") skipped: ${message}.`,
1632
+ source: "review",
1633
+ details: { op, targetId, reason, ...(extraDetails ?? {}) },
1634
+ };
1635
+ }
1636
+
1456
1637
  function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
1457
1638
  const activeRange = normalizeSelectionAnchor(
1458
1639
  selection.activeRange,
@@ -1673,10 +1854,11 @@ function applyReviewCommand(
1673
1854
  });
1674
1855
  }
1675
1856
 
1857
+ const priorStore = createRevisionStoreFromState(state);
1676
1858
  const result = applyRevisionRuntimeCommand({
1677
1859
  state: {
1678
1860
  document: state.document,
1679
- store: createRevisionStoreFromState(state),
1861
+ store: priorStore,
1680
1862
  },
1681
1863
  command,
1682
1864
  timestamp,
@@ -1684,9 +1866,27 @@ function applyReviewCommand(
1684
1866
  const hasAppliedOutcome = result.outcomes.some((outcome) => outcome.kind === "applied");
1685
1867
 
1686
1868
  if (!hasAppliedOutcome) {
1869
+ const op =
1870
+ command.type === "review.accept-revision"
1871
+ ? "acceptChange"
1872
+ : command.type === "review.reject-revision"
1873
+ ? "rejectChange"
1874
+ : command.type === "review.accept-all-revisions"
1875
+ ? "acceptAllChanges"
1876
+ : "rejectAllChanges";
1877
+ const targetId =
1878
+ command.type === "review.accept-revision" || command.type === "review.reject-revision"
1879
+ ? command.revisionId
1880
+ : "*";
1881
+ const { reason, message, extraDetails } = classifyReviewSkipReason(priorStore, command);
1687
1882
  return createTransaction(state, {
1688
1883
  historyBoundary: "skip",
1689
1884
  markDirty: false,
1885
+ effects: {
1886
+ transientWarnings: [
1887
+ reviewTargetNotFoundWarning(op, targetId, reason, message, extraDetails),
1888
+ ],
1889
+ },
1690
1890
  });
1691
1891
  }
1692
1892
 
@@ -1781,15 +1981,24 @@ function remapReviewStateAfterContentChange(
1781
1981
  nextContent: nextDocument.content,
1782
1982
  existingWarnings: mappedWarnings,
1783
1983
  });
1784
- const revisions = Object.fromEntries(
1785
- Object.entries(nextDocument.review.revisions).map(([changeId, revision]) => [
1786
- changeId,
1787
- {
1788
- ...revision,
1789
- anchor: mapAnchor(revision.anchor, mapping),
1790
- },
1791
- ]),
1792
- );
1984
+ // Remap revision anchors through the mapping. Most revisions are outside the
1985
+ // edited range and don't need new objects — skip allocation for those and
1986
+ // only rebuild the record when at least one anchor actually moved.
1987
+ const sourceRevisions = nextDocument.review.revisions;
1988
+ let revisions = sourceRevisions;
1989
+ if (mapping.steps.length > 0) {
1990
+ let changed = false;
1991
+ const remappedEntries = Object.entries(sourceRevisions).map(([changeId, revision]) => {
1992
+ if (anchorUnaffectedByMapping(revision.anchor, mapping)) {
1993
+ return [changeId, revision] as const;
1994
+ }
1995
+ changed = true;
1996
+ return [changeId, { ...revision, anchor: mapAnchor(revision.anchor, mapping) }] as const;
1997
+ });
1998
+ if (changed) {
1999
+ revisions = Object.fromEntries(remappedEntries);
2000
+ }
2001
+ }
1793
2002
  const activeCommentId =
1794
2003
  state.runtime.activeCommentId &&
1795
2004
  remappedComments.comments[state.runtime.activeCommentId]
@@ -0,0 +1,181 @@
1
+ import type {
2
+ BlockNode,
3
+ DocumentRootNode,
4
+ FieldNode,
5
+ InlineNode,
6
+ LegacyFormFieldNode,
7
+ ParagraphNode,
8
+ TableCellNode,
9
+ TableNode,
10
+ TableRowNode,
11
+ } from "../../model/canonical-document.ts";
12
+
13
+ /**
14
+ * Any document shape that carries a `DocumentRootNode` on a `content` field.
15
+ * `CanonicalDocumentEnvelope`, `NormalizedTextDocument`, and the editor
16
+ * state snapshot all match this shape structurally.
17
+ */
18
+ export interface DocWithContent {
19
+ content: DocumentRootNode;
20
+ }
21
+
22
+ /**
23
+ * Phase V.a — update a legacy form field value by name.
24
+ *
25
+ * For each `FieldNode` in the document whose `legacyFormField.name` matches
26
+ * the supplied name, the relevant typed field is updated:
27
+ *
28
+ * kind="textInput" → `textInput.default = value` (value must be string)
29
+ * kind="checkBox" → `checkBox.checked = value` (value must be boolean)
30
+ * kind="ddList" → `ddList.default = value` (value must be numeric
31
+ * index into `ddList.listEntry`)
32
+ *
33
+ * The node is marked `mutated: true` so the serializer regenerates the
34
+ * `<w:ffData>` XML from the typed fields (via `regenerateFFDataRawXml`)
35
+ * instead of emitting the preserved `rawXml` verbatim.
36
+ *
37
+ * Walk covers body, tables, SDT/customXml wrappers. Headers and footers are
38
+ * owned by their own document parts; call `setLegacyFormFieldValue` on
39
+ * those parts separately if the host needs to mutate them.
40
+ *
41
+ * If no field matches the name, the document is returned unchanged.
42
+ */
43
+ export function setLegacyFormFieldValue<D extends DocWithContent>(
44
+ document: D,
45
+ fieldName: string,
46
+ value: string | boolean | number,
47
+ ): D {
48
+ const nextChildren = document.content.children.map((child) =>
49
+ rewriteBlock(child, fieldName, value),
50
+ );
51
+
52
+ if (arraysShallowEqual(nextChildren, document.content.children)) {
53
+ return document;
54
+ }
55
+
56
+ return {
57
+ ...document,
58
+ content: { ...document.content, children: nextChildren },
59
+ };
60
+ }
61
+
62
+ function rewriteBlock(
63
+ block: BlockNode,
64
+ fieldName: string,
65
+ value: string | boolean | number,
66
+ ): BlockNode {
67
+ if (block.type === "paragraph") {
68
+ return rewriteParagraph(block, fieldName, value);
69
+ }
70
+ if (block.type === "table") {
71
+ return rewriteTable(block, fieldName, value);
72
+ }
73
+ if (block.type === "sdt" || block.type === "custom_xml") {
74
+ const nextChildren = block.children.map((c) => rewriteBlock(c, fieldName, value));
75
+ if (arraysShallowEqual(nextChildren, block.children)) return block;
76
+ return { ...block, children: nextChildren };
77
+ }
78
+ return block;
79
+ }
80
+
81
+ function rewriteParagraph(
82
+ paragraph: ParagraphNode,
83
+ fieldName: string,
84
+ value: string | boolean | number,
85
+ ): ParagraphNode {
86
+ const nextChildren = paragraph.children.map((inline) =>
87
+ rewriteInline(inline, fieldName, value),
88
+ );
89
+ if (arraysShallowEqual(nextChildren, paragraph.children)) return paragraph;
90
+ return { ...paragraph, children: nextChildren };
91
+ }
92
+
93
+ function rewriteInline(
94
+ inline: InlineNode,
95
+ fieldName: string,
96
+ value: string | boolean | number,
97
+ ): InlineNode {
98
+ if (inline.type !== "field") return inline;
99
+ const field = inline as FieldNode;
100
+ const lff = field.legacyFormField;
101
+ if (!lff || lff.name !== fieldName) return inline;
102
+
103
+ const nextLff = applyValueToLegacyFormField(lff, value);
104
+ if (nextLff === lff) return inline;
105
+ return { ...field, legacyFormField: nextLff };
106
+ }
107
+
108
+ function applyValueToLegacyFormField(
109
+ lff: LegacyFormFieldNode,
110
+ value: string | boolean | number,
111
+ ): LegacyFormFieldNode {
112
+ switch (lff.kind) {
113
+ case "textInput": {
114
+ if (typeof value !== "string") return lff;
115
+ const currentDefault = lff.textInput?.default;
116
+ if (currentDefault === value) return lff;
117
+ return {
118
+ ...lff,
119
+ textInput: { ...(lff.textInput ?? {}), default: value },
120
+ mutated: true,
121
+ };
122
+ }
123
+ case "checkBox": {
124
+ if (typeof value !== "boolean") return lff;
125
+ if (lff.checkBox?.checked === value) return lff;
126
+ return {
127
+ ...lff,
128
+ checkBox: { ...(lff.checkBox ?? {}), checked: value },
129
+ mutated: true,
130
+ };
131
+ }
132
+ case "ddList": {
133
+ if (typeof value !== "number") return lff;
134
+ if (lff.ddList?.default === value) return lff;
135
+ return {
136
+ ...lff,
137
+ ddList: { ...(lff.ddList ?? {}), default: value },
138
+ mutated: true,
139
+ };
140
+ }
141
+ }
142
+ }
143
+
144
+ function rewriteTable(
145
+ table: TableNode,
146
+ fieldName: string,
147
+ value: string | boolean | number,
148
+ ): TableNode {
149
+ const nextRows = table.rows.map((row) => rewriteRow(row, fieldName, value));
150
+ if (arraysShallowEqual(nextRows, table.rows)) return table;
151
+ return { ...table, rows: nextRows };
152
+ }
153
+
154
+ function rewriteRow(
155
+ row: TableRowNode,
156
+ fieldName: string,
157
+ value: string | boolean | number,
158
+ ): TableRowNode {
159
+ const nextCells = row.cells.map((cell) => rewriteCell(cell, fieldName, value));
160
+ if (arraysShallowEqual(nextCells, row.cells)) return row;
161
+ return { ...row, cells: nextCells };
162
+ }
163
+
164
+ function rewriteCell(
165
+ cell: TableCellNode,
166
+ fieldName: string,
167
+ value: string | boolean | number,
168
+ ): TableCellNode {
169
+ const nextChildren = cell.children.map((child) => rewriteBlock(child, fieldName, value));
170
+ if (arraysShallowEqual(nextChildren, cell.children)) return cell;
171
+ return { ...cell, children: nextChildren };
172
+ }
173
+
174
+ function arraysShallowEqual<T>(a: readonly T[], b: readonly T[]): boolean {
175
+ if (a === b) return true;
176
+ if (a.length !== b.length) return false;
177
+ for (let i = 0; i < a.length; i += 1) {
178
+ if (a[i] !== b[i]) return false;
179
+ }
180
+ return true;
181
+ }