@beyondwork/docx-react-component 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -69,6 +69,25 @@ export interface ParsedTableLook {
69
69
  noVBand?: boolean;
70
70
  }
71
71
 
72
+ export interface ParsedTableIndent {
73
+ value: number;
74
+ type: "dxa" | "auto" | "pct" | "nil";
75
+ }
76
+
77
+ export interface ParsedTableFloating {
78
+ horizontalAnchor?: "margin" | "page" | "text";
79
+ verticalAnchor?: "margin" | "page" | "text";
80
+ horizontalAlign?: "left" | "center" | "right" | "inside" | "outside";
81
+ horizontalOffset?: number;
82
+ verticalAlign?: "top" | "center" | "bottom" | "inside" | "outside";
83
+ verticalOffset?: number;
84
+ leftFromText?: number;
85
+ rightFromText?: number;
86
+ topFromText?: number;
87
+ bottomFromText?: number;
88
+ overlap?: boolean;
89
+ }
90
+
72
91
  export interface ParsedTableDocument {
73
92
  tables: ParsedTable[];
74
93
  }
@@ -85,6 +104,13 @@ export interface ParsedTable {
85
104
  borders?: ParsedTableBorders;
86
105
  cellMargins?: ParsedCellMargins;
87
106
  tblLook?: ParsedTableLook;
107
+ indent?: ParsedTableIndent;
108
+ layoutMode?: "fixed" | "autofit";
109
+ cellSpacing?: ParsedTableWidth;
110
+ caption?: string;
111
+ description?: string;
112
+ bidiVisual?: boolean;
113
+ floating?: ParsedTableFloating;
88
114
  }
89
115
 
90
116
  export interface ParsedTableRow {
@@ -94,6 +120,9 @@ export interface ParsedTableRow {
94
120
  height?: number;
95
121
  heightRule?: "auto" | "atLeast" | "exact";
96
122
  isHeader?: boolean;
123
+ cantSplit?: boolean;
124
+ horizontalAlignment?: "left" | "center" | "right";
125
+ cnfStyle?: string;
97
126
  }
98
127
 
99
128
  export interface ParsedTableCell {
@@ -106,6 +135,11 @@ export interface ParsedTableCell {
106
135
  borders?: ParsedTableCellBorders;
107
136
  shading?: ParsedCellShading;
108
137
  verticalAlign?: "top" | "center" | "bottom";
138
+ textDirection?: "lrTb" | "tbRl" | "btLr";
139
+ noWrap?: boolean;
140
+ fitText?: boolean;
141
+ margins?: ParsedCellMargins;
142
+ cnfStyle?: string;
109
143
  }
110
144
 
111
145
  export function parseTablesFromDocumentXml(xml: string): ParsedTableDocument {
@@ -133,6 +167,13 @@ function parseTable(node: XmlElementNode, sourceXml: string): ParsedTable {
133
167
  const borders = propertiesNode ? readTableBorders(propertiesNode) : undefined;
134
168
  const cellMargins = propertiesNode ? readTableCellMargins(propertiesNode) : undefined;
135
169
  const tblLook = propertiesNode ? readTableLook(propertiesNode) : undefined;
170
+ const indent = propertiesNode ? readTableIndent(propertiesNode) : undefined;
171
+ const layoutMode = propertiesNode ? readTableLayoutMode(propertiesNode) : undefined;
172
+ const cellSpacing = propertiesNode ? readTableCellSpacing(propertiesNode) : undefined;
173
+ const caption = propertiesNode ? readTableCaption(propertiesNode) : undefined;
174
+ const description = propertiesNode ? readTableDescription(propertiesNode) : undefined;
175
+ const bidiVisual = propertiesNode ? readTableBidiVisual(propertiesNode) : undefined;
176
+ const floating = propertiesNode ? readTableFloating(propertiesNode) : undefined;
136
177
 
137
178
  return {
138
179
  type: "table",
@@ -146,6 +187,13 @@ function parseTable(node: XmlElementNode, sourceXml: string): ParsedTable {
146
187
  ...(borders ? { borders } : {}),
147
188
  ...(cellMargins ? { cellMargins } : {}),
148
189
  ...(tblLook ? { tblLook } : {}),
190
+ ...(indent ? { indent } : {}),
191
+ ...(layoutMode ? { layoutMode } : {}),
192
+ ...(cellSpacing ? { cellSpacing } : {}),
193
+ ...(caption !== undefined ? { caption } : {}),
194
+ ...(description !== undefined ? { description } : {}),
195
+ ...(bidiVisual !== undefined ? { bidiVisual } : {}),
196
+ ...(floating ? { floating } : {}),
149
197
  };
150
198
  }
151
199
 
@@ -154,6 +202,9 @@ function parseRow(node: XmlElementNode, sourceXml: string): ParsedTableRow {
154
202
  const height = propertiesNode ? readRowHeight(propertiesNode) : undefined;
155
203
  const heightRule = propertiesNode ? readRowHeightRule(propertiesNode) : undefined;
156
204
  const isHeader = propertiesNode ? readRowIsHeader(propertiesNode) : undefined;
205
+ const cantSplit = propertiesNode ? readRowCantSplit(propertiesNode) : undefined;
206
+ const horizontalAlignment = propertiesNode ? readRowHorizontalAlignment(propertiesNode) : undefined;
207
+ const cnfStyle = propertiesNode ? readRowCnfStyle(propertiesNode) : undefined;
157
208
 
158
209
  return {
159
210
  ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
@@ -164,6 +215,9 @@ function parseRow(node: XmlElementNode, sourceXml: string): ParsedTableRow {
164
215
  ...(height !== undefined ? { height } : {}),
165
216
  ...(heightRule ? { heightRule } : {}),
166
217
  ...(isHeader !== undefined ? { isHeader } : {}),
218
+ ...(cantSplit !== undefined ? { cantSplit } : {}),
219
+ ...(horizontalAlignment ? { horizontalAlignment } : {}),
220
+ ...(cnfStyle ? { cnfStyle } : {}),
167
221
  };
168
222
  }
169
223
 
@@ -179,6 +233,11 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
179
233
  const borders = propertiesNode ? readCellBorders(propertiesNode) : undefined;
180
234
  const shading = propertiesNode ? readCellShading(propertiesNode) : undefined;
181
235
  const verticalAlign = propertiesNode ? readCellVerticalAlign(propertiesNode) : undefined;
236
+ const textDirection = propertiesNode ? readCellTextDirection(propertiesNode) : undefined;
237
+ const noWrap = propertiesNode ? readCellNoWrap(propertiesNode) : undefined;
238
+ const fitText = propertiesNode ? readCellFitText(propertiesNode) : undefined;
239
+ const margins = propertiesNode ? readCellMargins(propertiesNode) : undefined;
240
+ const cnfStyle = propertiesNode ? readCellCnfStyle(propertiesNode) : undefined;
182
241
 
183
242
  return {
184
243
  ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
@@ -190,6 +249,11 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
190
249
  ...(borders ? { borders } : {}),
191
250
  ...(shading ? { shading } : {}),
192
251
  ...(verticalAlign ? { verticalAlign } : {}),
252
+ ...(textDirection ? { textDirection } : {}),
253
+ ...(noWrap !== undefined ? { noWrap } : {}),
254
+ ...(fitText !== undefined ? { fitText } : {}),
255
+ ...(margins ? { margins } : {}),
256
+ ...(cnfStyle ? { cnfStyle } : {}),
193
257
  };
194
258
  }
195
259
 
@@ -606,6 +670,191 @@ export function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undef
606
670
  return val !== "false" && val !== "0";
607
671
  }
608
672
 
673
+ export function readTableIndent(propertiesNode: XmlElementNode): ParsedTableIndent | undefined {
674
+ const indentNode = findFirstChild(propertiesNode, "tblInd");
675
+ if (!indentNode) return undefined;
676
+ const valueRaw = indentNode.attributes["w:w"] ?? indentNode.attributes.w;
677
+ const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
678
+ if (!Number.isFinite(value)) return undefined;
679
+ const rawType = (indentNode.attributes["w:type"] ?? indentNode.attributes.type ?? "dxa").toLowerCase();
680
+ const type: ParsedTableIndent["type"] =
681
+ rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
682
+ return { value, type };
683
+ }
684
+
685
+ export function readTableLayoutMode(propertiesNode: XmlElementNode): "fixed" | "autofit" | undefined {
686
+ const layoutNode = findFirstChild(propertiesNode, "tblLayout");
687
+ if (!layoutNode) return undefined;
688
+ const raw = (layoutNode.attributes["w:type"] ?? layoutNode.attributes.type ?? "").toLowerCase();
689
+ if (raw === "fixed") return "fixed";
690
+ if (raw === "autofit") return "autofit";
691
+ return undefined;
692
+ }
693
+
694
+ export function readTableCellSpacing(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
695
+ const spacingNode = findFirstChild(propertiesNode, "tblCellSpacing");
696
+ if (!spacingNode) return undefined;
697
+ const valueRaw = spacingNode.attributes["w:w"] ?? spacingNode.attributes.w;
698
+ const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
699
+ if (!Number.isFinite(value)) return undefined;
700
+ const rawType = (spacingNode.attributes["w:type"] ?? spacingNode.attributes.type ?? "dxa").toLowerCase();
701
+ const type: ParsedTableWidth["type"] =
702
+ rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
703
+ return { value, type };
704
+ }
705
+
706
+ export function readTableCaption(propertiesNode: XmlElementNode): string | undefined {
707
+ const captionNode = findFirstChild(propertiesNode, "tblCaption");
708
+ if (!captionNode) return undefined;
709
+ return captionNode.attributes["w:val"] ?? captionNode.attributes.val;
710
+ }
711
+
712
+ export function readTableDescription(propertiesNode: XmlElementNode): string | undefined {
713
+ const descriptionNode = findFirstChild(propertiesNode, "tblDescription");
714
+ if (!descriptionNode) return undefined;
715
+ return descriptionNode.attributes["w:val"] ?? descriptionNode.attributes.val;
716
+ }
717
+
718
+ export function readTableBidiVisual(propertiesNode: XmlElementNode): boolean | undefined {
719
+ const bidiNode = findFirstChild(propertiesNode, "bidiVisual");
720
+ if (!bidiNode) return undefined;
721
+ const val = bidiNode.attributes["w:val"] ?? bidiNode.attributes.val;
722
+ return val !== "false" && val !== "0" && val !== "off";
723
+ }
724
+
725
+ export function readTableFloating(propertiesNode: XmlElementNode): ParsedTableFloating | undefined {
726
+ const tblpPrNode = findFirstChild(propertiesNode, "tblpPr");
727
+ const overlapNode = findFirstChild(propertiesNode, "tblOverlap");
728
+ if (!tblpPrNode && !overlapNode) return undefined;
729
+
730
+ const floating: ParsedTableFloating = {};
731
+
732
+ if (tblpPrNode) {
733
+ const attr = (name: string): string | undefined =>
734
+ tblpPrNode.attributes[`w:${name}`] ?? tblpPrNode.attributes[name];
735
+ const num = (raw: string | undefined): number | undefined => {
736
+ if (raw === undefined) return undefined;
737
+ const parsed = Number.parseInt(raw, 10);
738
+ return Number.isFinite(parsed) ? parsed : undefined;
739
+ };
740
+ const anchor = (raw: string | undefined): ParsedTableFloating["horizontalAnchor"] | undefined => {
741
+ if (raw === "margin" || raw === "page" || raw === "text") return raw;
742
+ return undefined;
743
+ };
744
+ const hAlign = (raw: string | undefined): ParsedTableFloating["horizontalAlign"] | undefined => {
745
+ if (raw === "left" || raw === "center" || raw === "right" || raw === "inside" || raw === "outside") return raw;
746
+ return undefined;
747
+ };
748
+ const vAlign = (raw: string | undefined): ParsedTableFloating["verticalAlign"] | undefined => {
749
+ if (raw === "top" || raw === "center" || raw === "bottom" || raw === "inside" || raw === "outside") return raw;
750
+ return undefined;
751
+ };
752
+
753
+ const hAnc = anchor(attr("horzAnchor"));
754
+ if (hAnc) floating.horizontalAnchor = hAnc;
755
+ const vAnc = anchor(attr("vertAnchor"));
756
+ if (vAnc) floating.verticalAnchor = vAnc;
757
+ const hA = hAlign(attr("tblpXSpec"));
758
+ if (hA) floating.horizontalAlign = hA;
759
+ const hOff = num(attr("tblpX"));
760
+ if (hOff !== undefined) floating.horizontalOffset = hOff;
761
+ const vA = vAlign(attr("tblpYSpec"));
762
+ if (vA) floating.verticalAlign = vA;
763
+ const vOff = num(attr("tblpY"));
764
+ if (vOff !== undefined) floating.verticalOffset = vOff;
765
+ const left = num(attr("leftFromText"));
766
+ if (left !== undefined) floating.leftFromText = left;
767
+ const right = num(attr("rightFromText"));
768
+ if (right !== undefined) floating.rightFromText = right;
769
+ const top = num(attr("topFromText"));
770
+ if (top !== undefined) floating.topFromText = top;
771
+ const bottom = num(attr("bottomFromText"));
772
+ if (bottom !== undefined) floating.bottomFromText = bottom;
773
+ }
774
+
775
+ if (overlapNode) {
776
+ const val = (overlapNode.attributes["w:val"] ?? overlapNode.attributes.val ?? "overlap").toLowerCase();
777
+ floating.overlap = val === "overlap";
778
+ }
779
+
780
+ return Object.keys(floating).length > 0 ? floating : undefined;
781
+ }
782
+
783
+ export function readRowCantSplit(propertiesNode: XmlElementNode): boolean | undefined {
784
+ const cantSplitNode = findFirstChild(propertiesNode, "cantSplit");
785
+ if (!cantSplitNode) return undefined;
786
+ const val = cantSplitNode.attributes["w:val"] ?? cantSplitNode.attributes.val;
787
+ return val !== "false" && val !== "0" && val !== "off";
788
+ }
789
+
790
+ export function readRowHorizontalAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
791
+ const jcNode = findFirstChild(propertiesNode, "jc");
792
+ if (!jcNode) return undefined;
793
+ const val = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
794
+ if (val === "left" || val === "center" || val === "right") return val;
795
+ return undefined;
796
+ }
797
+
798
+ export function readRowCnfStyle(propertiesNode: XmlElementNode): string | undefined {
799
+ const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
800
+ if (!cnfNode) return undefined;
801
+ return cnfNode.attributes["w:val"] ?? cnfNode.attributes.val;
802
+ }
803
+
804
+ export function readCellTextDirection(propertiesNode: XmlElementNode): "lrTb" | "tbRl" | "btLr" | undefined {
805
+ const dirNode = findFirstChild(propertiesNode, "textDirection");
806
+ if (!dirNode) return undefined;
807
+ const val = dirNode.attributes["w:val"] ?? dirNode.attributes.val;
808
+ if (val === "lrTb" || val === "tbRl" || val === "btLr") return val;
809
+ return undefined;
810
+ }
811
+
812
+ export function readCellNoWrap(propertiesNode: XmlElementNode): boolean | undefined {
813
+ const noWrapNode = findFirstChild(propertiesNode, "noWrap");
814
+ if (!noWrapNode) return undefined;
815
+ const val = noWrapNode.attributes["w:val"] ?? noWrapNode.attributes.val;
816
+ return val !== "false" && val !== "0" && val !== "off";
817
+ }
818
+
819
+ export function readCellFitText(propertiesNode: XmlElementNode): boolean | undefined {
820
+ const fitNode = findFirstChild(propertiesNode, "tcFitText");
821
+ if (!fitNode) return undefined;
822
+ const val = fitNode.attributes["w:val"] ?? fitNode.attributes.val;
823
+ return val !== "false" && val !== "0" && val !== "off";
824
+ }
825
+
826
+ export function readCellMargins(propertiesNode: XmlElementNode): ParsedCellMargins | undefined {
827
+ const marginsNode = findFirstChild(propertiesNode, "tcMar");
828
+ if (!marginsNode) return undefined;
829
+ const readSide = (name: string): number | undefined => {
830
+ const sideNode = findFirstChild(marginsNode, name);
831
+ if (!sideNode) return undefined;
832
+ const raw = sideNode.attributes["w:w"] ?? sideNode.attributes.w;
833
+ if (raw === undefined) return undefined;
834
+ const parsed = Number.parseInt(raw, 10);
835
+ return Number.isFinite(parsed) ? parsed : undefined;
836
+ };
837
+ const top = readSide("top");
838
+ const bottom = readSide("bottom");
839
+ const left = readSide("start") ?? readSide("left");
840
+ const right = readSide("end") ?? readSide("right");
841
+ if (top === undefined && bottom === undefined && left === undefined && right === undefined) {
842
+ return undefined;
843
+ }
844
+ return {
845
+ ...(top !== undefined ? { top } : {}),
846
+ ...(bottom !== undefined ? { bottom } : {}),
847
+ ...(left !== undefined ? { left } : {}),
848
+ ...(right !== undefined ? { right } : {}),
849
+ };
850
+ }
851
+
852
+ export function readCellCnfStyle(propertiesNode: XmlElementNode): string | undefined {
853
+ const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
854
+ if (!cnfNode) return undefined;
855
+ return cnfNode.attributes["w:val"] ?? cnfNode.attributes.val;
856
+ }
857
+
609
858
  function parseBorderSpec(child: XmlElementNode): ParsedBorderSpec | undefined {
610
859
  const value = child.attributes["w:val"] ?? child.attributes.val;
611
860
  const sizeRaw = child.attributes["w:sz"] ?? child.attributes.sz;
@@ -254,6 +254,84 @@ export function resolveBookmarkFieldDependencies(
254
254
  return deps;
255
255
  }
256
256
 
257
+ /**
258
+ * A.4 — detect duplicate bookmark ids in the canonical document.
259
+ *
260
+ * Walks the document in order and records every bookmarkStart. The first
261
+ * occurrence of each id keeps its original value; later occurrences are
262
+ * scheduled for re-keying to a fresh monotonically-increasing id above
263
+ * `max(existing numeric ids)` so newly-allocated ids never collide with
264
+ * any surviving original.
265
+ *
266
+ * Returns a plan the caller can use to mutate both bookmark nodes and
267
+ * every reference to them (PAGEREF / REF field instructions, hyperlink
268
+ * w:anchor, TOC entries). Each assignment is keyed by the start node's
269
+ * ordinal position so callers can walk the document a second time and
270
+ * apply the new id deterministically.
271
+ */
272
+ export interface BookmarkRekeyAssignment {
273
+ /** Ordinal position of the bookmark_start within the pre-order walk. */
274
+ ordinal: number;
275
+ /** Original bookmark id as emitted by the source. */
276
+ oldId: string;
277
+ /** New id when a re-key was scheduled; undefined when the id is kept. */
278
+ newId?: string;
279
+ }
280
+
281
+ export interface BookmarkRekeyPlan {
282
+ assignments: readonly BookmarkRekeyAssignment[];
283
+ /** All ids that had ≥2 occurrences in the source. */
284
+ duplicatedIds: readonly string[];
285
+ /** New max id allocated by the plan (advisory for allocators). */
286
+ nextId: number;
287
+ }
288
+
289
+ export function detectDuplicateBookmarkIds(
290
+ document: Pick<CanonicalDocument, "content"> | DocumentNode,
291
+ ): BookmarkRekeyPlan {
292
+ const root = "content" in document ? document.content : document;
293
+ const seenStarts = new Set<string>();
294
+ const duplicatedIds = new Set<string>();
295
+ let maxNumericId = 0;
296
+
297
+ const bookmarkStarts: Array<BookmarkStartNode> = [];
298
+
299
+ walkDocument(root, (node) => {
300
+ if (node.type === "bookmark_start") {
301
+ bookmarkStarts.push(node);
302
+ const parsed = Number.parseInt(node.bookmarkId, 10);
303
+ if (Number.isFinite(parsed)) {
304
+ maxNumericId = Math.max(maxNumericId, parsed);
305
+ }
306
+ }
307
+ });
308
+
309
+ const assignments: BookmarkRekeyAssignment[] = [];
310
+ let nextId = maxNumericId + 1;
311
+ for (let ordinal = 0; ordinal < bookmarkStarts.length; ordinal += 1) {
312
+ const start = bookmarkStarts[ordinal]!;
313
+ if (!seenStarts.has(start.bookmarkId)) {
314
+ seenStarts.add(start.bookmarkId);
315
+ assignments.push({ ordinal, oldId: start.bookmarkId });
316
+ continue;
317
+ }
318
+ duplicatedIds.add(start.bookmarkId);
319
+ const fresh = String(nextId);
320
+ nextId += 1;
321
+ assignments.push({
322
+ ordinal,
323
+ oldId: start.bookmarkId,
324
+ newId: fresh,
325
+ });
326
+ }
327
+
328
+ return {
329
+ assignments,
330
+ duplicatedIds: [...duplicatedIds].sort(),
331
+ nextId,
332
+ };
333
+ }
334
+
257
335
  function walkDocument(node: DocumentNode, visit: (node: DocumentNode) => void): void {
258
336
  visit(node);
259
337
 
@@ -372,6 +372,17 @@ export interface ParagraphNode {
372
372
  bidi?: boolean;
373
373
  suppressLineNumbers?: boolean;
374
374
  cnfStyle?: string;
375
+ /**
376
+ * Preserved w14 extension identifiers for this paragraph.
377
+ * Round-trip (§2 A.7) requires these to survive import → export so the
378
+ * `w14:paraId` / `w14:textId` attributes that Word places on paragraph
379
+ * and run boundaries are re-emitted with the same values. Both ids are
380
+ * 8-hex uppercase strings per ECMA-376 Part 1 Appendix A.
381
+ */
382
+ wordExtensionIds?: {
383
+ paraId?: string;
384
+ textId?: string;
385
+ };
375
386
  children: InlineNode[];
376
387
  }
377
388
 
@@ -405,6 +416,25 @@ export interface TableWidth {
405
416
  type: "dxa" | "auto" | "pct" | "nil";
406
417
  }
407
418
 
419
+ export interface TableIndent {
420
+ value: number;
421
+ type: "dxa" | "auto" | "pct" | "nil";
422
+ }
423
+
424
+ export interface TableFloatingProperties {
425
+ horizontalAnchor?: "margin" | "page" | "text";
426
+ verticalAnchor?: "margin" | "page" | "text";
427
+ horizontalAlign?: "left" | "center" | "right" | "inside" | "outside";
428
+ horizontalOffset?: number;
429
+ verticalAlign?: "top" | "center" | "bottom" | "inside" | "outside";
430
+ verticalOffset?: number;
431
+ leftFromText?: number;
432
+ rightFromText?: number;
433
+ topFromText?: number;
434
+ bottomFromText?: number;
435
+ overlap?: boolean;
436
+ }
437
+
408
438
  export interface CellShading {
409
439
  fill?: string;
410
440
  color?: string;
@@ -470,6 +500,13 @@ export interface TableNode {
470
500
  borders?: TableBorders;
471
501
  cellMargins?: TableCellMargins;
472
502
  tblLook?: TableLook;
503
+ indent?: TableIndent;
504
+ layoutMode?: "fixed" | "autofit";
505
+ cellSpacing?: TableWidth;
506
+ caption?: string;
507
+ description?: string;
508
+ bidiVisual?: boolean;
509
+ floating?: TableFloatingProperties;
473
510
  }
474
511
 
475
512
  export interface TableRowNode {
@@ -483,6 +520,9 @@ export interface TableRowNode {
483
520
  height?: number;
484
521
  heightRule?: "auto" | "atLeast" | "exact";
485
522
  isHeader?: boolean;
523
+ cantSplit?: boolean;
524
+ horizontalAlignment?: "left" | "center" | "right";
525
+ cnfStyle?: string;
486
526
  }
487
527
 
488
528
  export interface TableCellNode {
@@ -495,6 +535,11 @@ export interface TableCellNode {
495
535
  borders?: TableCellBorders;
496
536
  shading?: CellShading;
497
537
  verticalAlign?: "top" | "center" | "bottom";
538
+ textDirection?: "lrTb" | "tbRl" | "btLr";
539
+ noWrap?: boolean;
540
+ fitText?: boolean;
541
+ margins?: TableCellMargins;
542
+ cnfStyle?: string;
498
543
  }
499
544
 
500
545
  export interface SdtCheckboxState {
@@ -0,0 +1,130 @@
1
+ import type { ScopeTagTouch } from "../../api/public-types.ts";
2
+ import type {
3
+ CanonicalAnchor,
4
+ CommentThread,
5
+ RevisionRecord,
6
+ } from "../../model/canonical-document.ts";
7
+
8
+ /**
9
+ * Diff prior vs next review state and emit one `ScopeTagTouch` per changed
10
+ * comment or revision anchor. The predicted-text lane consumes these touches
11
+ * to classify a text command's ack as `equivalent` (no touches), `adjusted`
12
+ * (anchors shifted), or to feed decoration redraws without a PM rebuild.
13
+ *
14
+ * Notes
15
+ * - Newly created annotations show up as "extended" (revisions) or "split"
16
+ * (comments) so the lane knows to redraw them.
17
+ * - Detached annotations show up as "detached".
18
+ * - Anchors whose range is byte-identical across the diff are omitted.
19
+ */
20
+ export function collectScopeTagTouches(
21
+ priorComments: Readonly<Record<string, CommentThread>>,
22
+ nextComments: Readonly<Record<string, CommentThread>>,
23
+ priorRevisions: Readonly<Record<string, RevisionRecord>>,
24
+ nextRevisions: Readonly<Record<string, RevisionRecord>>,
25
+ ): ScopeTagTouch[] {
26
+ const touches: ScopeTagTouch[] = [];
27
+
28
+ for (const [id, prior] of Object.entries(priorComments)) {
29
+ const next = nextComments[id];
30
+ if (!next) {
31
+ touches.push({
32
+ tagType: "comment",
33
+ tagId: id,
34
+ behavior: "detached",
35
+ range: anchorRange(prior.anchor),
36
+ });
37
+ continue;
38
+ }
39
+ const behavior = classifyAnchorChange(prior.anchor, next.anchor);
40
+ if (behavior !== "unchanged") {
41
+ touches.push({
42
+ tagType: "comment",
43
+ tagId: id,
44
+ behavior,
45
+ range: anchorRange(next.anchor),
46
+ });
47
+ }
48
+ }
49
+
50
+ for (const [id, next] of Object.entries(nextComments)) {
51
+ if (!priorComments[id]) {
52
+ touches.push({
53
+ tagType: "comment",
54
+ tagId: id,
55
+ behavior: "split",
56
+ range: anchorRange(next.anchor),
57
+ });
58
+ }
59
+ }
60
+
61
+ for (const [id, prior] of Object.entries(priorRevisions)) {
62
+ const next = nextRevisions[id];
63
+ if (!next) {
64
+ touches.push({
65
+ tagType: "revision",
66
+ tagId: id,
67
+ behavior: "detached",
68
+ range: anchorRange(prior.anchor),
69
+ });
70
+ continue;
71
+ }
72
+ const behavior = classifyAnchorChange(prior.anchor, next.anchor);
73
+ if (behavior !== "unchanged") {
74
+ touches.push({
75
+ tagType: "revision",
76
+ tagId: id,
77
+ behavior,
78
+ range: anchorRange(next.anchor),
79
+ });
80
+ }
81
+ }
82
+
83
+ for (const [id, next] of Object.entries(nextRevisions)) {
84
+ if (!priorRevisions[id]) {
85
+ touches.push({
86
+ tagType: "revision",
87
+ tagId: id,
88
+ behavior: "extended",
89
+ range: anchorRange(next.anchor),
90
+ });
91
+ }
92
+ }
93
+
94
+ return touches;
95
+ }
96
+
97
+ function anchorRange(anchor: CanonicalAnchor): { from: number; to: number } {
98
+ if (anchor.kind === "range") {
99
+ return { from: anchor.range.from, to: anchor.range.to };
100
+ }
101
+ if (anchor.kind === "node") {
102
+ return { from: anchor.at, to: anchor.at };
103
+ }
104
+ return { from: anchor.lastKnownRange.from, to: anchor.lastKnownRange.to };
105
+ }
106
+
107
+ function classifyAnchorChange(
108
+ prior: CanonicalAnchor,
109
+ next: CanonicalAnchor,
110
+ ): "unchanged" | "extended" | "trimmed" | "split" | "detached" {
111
+ if (prior.kind === "detached" && next.kind === "detached") {
112
+ const pr = prior.lastKnownRange;
113
+ const nr = next.lastKnownRange;
114
+ return pr.from === nr.from && pr.to === nr.to ? "unchanged" : "detached";
115
+ }
116
+ if (prior.kind !== "detached" && next.kind === "detached") {
117
+ return "detached";
118
+ }
119
+ if (prior.kind === "detached" && next.kind !== "detached") {
120
+ return "extended";
121
+ }
122
+ const priorR = anchorRange(prior);
123
+ const nextR = anchorRange(next);
124
+ if (priorR.from === nextR.from && priorR.to === nextR.to) return "unchanged";
125
+ const priorLen = priorR.to - priorR.from;
126
+ const nextLen = nextR.to - nextR.from;
127
+ if (nextLen > priorLen) return "extended";
128
+ if (nextLen < priorLen) return "trimmed";
129
+ return "split";
130
+ }
@@ -15,6 +15,7 @@ import type {
15
15
  SubPartsCatalog,
16
16
  } from "../model/canonical-document.ts";
17
17
  import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
18
+ import { resolveDefaultPageSizeTwips } from "./layout/default-page-format.ts";
18
19
  import {
19
20
  resolveSectionVariants,
20
21
  sectionSupportsStoryTarget,
@@ -88,9 +89,10 @@ export function buildPageLayoutSnapshot(
88
89
  properties: SectionProperties | undefined,
89
90
  subParts: SubPartsCatalog | undefined,
90
91
  ): PageLayoutSnapshot {
92
+ const defaultSize = resolveDefaultPageSizeTwips();
91
93
  const pageSize = properties?.pageSize ?? {
92
- width: 12240,
93
- height: 15840,
94
+ width: defaultSize.widthTwips,
95
+ height: defaultSize.heightTwips,
94
96
  orientation: "portrait" as const,
95
97
  };
96
98
  const margins = properties?.pageMargins ?? {