@beyondwork/docx-react-component 1.0.37 → 1.0.39

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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  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/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -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;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared XML helpers for OOXML parsers.
3
+ *
4
+ * These functions factor out the common pattern of reading namespaced-prefixed
5
+ * attributes (e.g., `w:val`) with fallback to the unprefixed form, handling
6
+ * the OOXML `ST_OnOff` toggle semantics (missing child = undefined, present
7
+ * with no val = true, `val="0"|"false"|"off"` = false), and reading integers.
8
+ *
9
+ * Used by parse-run-formatting.ts, parse-paragraph-formatting.ts, and (later)
10
+ * parse-styles.ts, parse-numbering.ts.
11
+ */
12
+
13
+ import type { XmlElementNode } from "./xml-element.ts";
14
+
15
+ export function localName(name: string): string {
16
+ const sep = name.indexOf(":");
17
+ return sep >= 0 ? name.slice(sep + 1) : name;
18
+ }
19
+
20
+ export function findChildOptional(
21
+ node: XmlElementNode,
22
+ local: string,
23
+ ): XmlElementNode | undefined {
24
+ return node.children.find(
25
+ (c): c is XmlElementNode => c.type === "element" && localName(c.name) === local,
26
+ );
27
+ }
28
+
29
+ /** ST_OnOff: missing child → undefined; present bare or w:val="1|true|on" → true; w:val="0|false|off" → false. */
30
+ export function readOnOff(node: XmlElementNode | undefined): boolean | undefined {
31
+ if (!node) return undefined;
32
+ const raw = node.attributes["w:val"] ?? node.attributes.val;
33
+ if (raw === undefined) return true;
34
+ const n = raw.toLowerCase();
35
+ if (n === "0" || n === "false" || n === "off") return false;
36
+ if (n === "1" || n === "true" || n === "on") return true;
37
+ return true;
38
+ }
39
+
40
+ /** Read the child's `w:val` attribute as an int. Returns undefined if missing or not a finite integer. */
41
+ export function readIntVal(node: XmlElementNode | undefined): number | undefined {
42
+ if (!node) return undefined;
43
+ const raw = node.attributes["w:val"] ?? node.attributes.val;
44
+ if (raw === undefined) return undefined;
45
+ const v = Number.parseInt(raw, 10);
46
+ return Number.isFinite(v) ? v : undefined;
47
+ }
48
+
49
+ /** Read an arbitrary attribute from a node as an int, with namespace fallback. */
50
+ export function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
51
+ const raw = node.attributes[attr] ?? node.attributes[attr.replace(/^w:/, "")];
52
+ if (raw === undefined) return undefined;
53
+ const v = Number.parseInt(raw, 10);
54
+ return Number.isFinite(v) ? v : undefined;
55
+ }
56
+
57
+ /** Read an arbitrary attribute from a node as a string, with namespace fallback. */
58
+ export function readStringAttr(node: XmlElementNode, attr: string): string | undefined {
59
+ return node.attributes[attr] ?? node.attributes[attr.replace(/^w:/, "")];
60
+ }
@@ -0,0 +1,19 @@
1
+ export interface XmlElementNode {
2
+ type: "element";
3
+ name: string;
4
+ attributes: Record<string, string>;
5
+ children: Array<XmlElementNode | XmlTextNode>;
6
+ /** Optional source offset (start) — parsers that track offsets may populate. */
7
+ start?: number;
8
+ /** Optional source offset (end) — parsers that track offsets may populate. */
9
+ end?: number;
10
+ }
11
+
12
+ export interface XmlTextNode {
13
+ type: "text";
14
+ text: string;
15
+ start?: number;
16
+ end?: number;
17
+ }
18
+
19
+ export type XmlNode = XmlElementNode | XmlTextNode;
@@ -83,6 +83,7 @@ export interface StylesCatalog {
83
83
  tables: Record<string, TableStyleDefinition>;
84
84
  latentStyles?: Record<string, LatentStyleDefinition>;
85
85
  fromPackage?: boolean;
86
+ docDefaults?: DocumentDefaults;
86
87
  }
87
88
 
88
89
  export interface ParagraphStyleDefinition {
@@ -94,6 +95,8 @@ export interface ParagraphStyleDefinition {
94
95
  displayName: string;
95
96
  kind: "paragraph";
96
97
  isDefault: boolean;
98
+ paragraphProperties?: CanonicalParagraphFormatting;
99
+ runProperties?: CanonicalRunFormatting;
97
100
  }
98
101
 
99
102
  export interface ParagraphStyleNumberingReference {
@@ -107,6 +110,7 @@ export interface CharacterStyleDefinition {
107
110
  displayName: string;
108
111
  kind: "character";
109
112
  isDefault: boolean;
113
+ runProperties?: CanonicalRunFormatting;
110
114
  }
111
115
 
112
116
  export interface TableStyleDefinition {
@@ -136,6 +140,12 @@ export interface NumberingCatalog {
136
140
  export interface AbstractNumberingDefinition {
137
141
  abstractNumberingId: string;
138
142
  levels: NumberingLevelDefinition[];
143
+ nsid?: string;
144
+ multiLevelType?: "singleLevel" | "multilevel" | "hybridMultilevel";
145
+ /** ECMA-376 17.9.26 `<w:tmpl>` — template code identifying the abstractNum's origin template (ST_LongHexNumber). Preserved for round-trip. */
146
+ tplc?: string;
147
+ styleLink?: string;
148
+ numStyleLink?: string;
139
149
  }
140
150
 
141
151
  export interface NumberingLevelParagraphGeometry {
@@ -154,6 +164,8 @@ export interface NumberingLevelDefinition {
154
164
  isLegalNumbering?: boolean;
155
165
  suffix?: "tab" | "space" | "nothing";
156
166
  paragraphGeometry?: NumberingLevelParagraphGeometry;
167
+ runProperties?: CanonicalRunFormatting;
168
+ restartAfterLevel?: number;
157
169
  }
158
170
 
159
171
  export interface NumberingLevelOverrideDefinition {
@@ -165,6 +177,8 @@ export interface NumberingLevelOverrideDefinition {
165
177
  isLegalNumbering?: boolean;
166
178
  suffix?: "tab" | "space" | "nothing";
167
179
  paragraphGeometry?: NumberingLevelParagraphGeometry;
180
+ runProperties?: CanonicalRunFormatting;
181
+ restartAfterLevel?: number;
168
182
  }
169
183
 
170
184
  export interface NumberingInstance {
@@ -350,6 +364,68 @@ export interface ParagraphShading {
350
364
  val?: string;
351
365
  }
352
366
 
367
+ /** Body of an OOXML `<w:rPr>` (run properties). All fields optional; absence = "not specified at this level". */
368
+ export interface CanonicalRunFormatting {
369
+ bold?: boolean;
370
+ italic?: boolean;
371
+ underline?: "single" | "double" | "thick" | "dotted" | "dash" | "wave" | "none";
372
+ strikethrough?: boolean;
373
+ doubleStrikethrough?: boolean;
374
+ vanish?: boolean;
375
+ allCaps?: boolean;
376
+ smallCaps?: boolean;
377
+ verticalAlign?: "baseline" | "superscript" | "subscript";
378
+ /**
379
+ * Convenience alias for the primary font family — the first non-empty of
380
+ * `fontFamilyAscii` → `fontFamilyHAnsi` → `fontFamilyEastAsia` → `fontFamilyCs`.
381
+ * Script-aware consumers should read the specific `fontFamily{Ascii,HAnsi,EastAsia,Cs}`
382
+ * fields directly.
383
+ */
384
+ fontFamily?: string;
385
+ fontFamilyAscii?: string;
386
+ fontFamilyHAnsi?: string;
387
+ fontFamilyEastAsia?: string;
388
+ fontFamilyCs?: string;
389
+ fontSizeHalfPoints?: number;
390
+ fontSizeCsHalfPoints?: number;
391
+ /**
392
+ * Color value from `<w:color w:val>`. Either an OOXML hex (e.g., `"2E74B5"`)
393
+ * or the sentinel `"auto"` (which serializers must round-trip verbatim).
394
+ */
395
+ colorHex?: string;
396
+ colorThemeSlot?: string;
397
+ highlight?: string;
398
+ characterSpacingTwips?: number;
399
+ characterStyleId?: string;
400
+ languageCode?: string;
401
+ }
402
+
403
+ /** Body of an OOXML `<w:pPr>` (paragraph properties). All fields optional; absence = "not specified at this level". */
404
+ export interface CanonicalParagraphFormatting {
405
+ spacing?: ParagraphSpacing;
406
+ indentation?: ParagraphIndentation;
407
+ alignment?: "left" | "center" | "right" | "both" | "distribute" | "start" | "end";
408
+ borders?: ParagraphBorders;
409
+ shading?: ParagraphShading;
410
+ tabStops?: TabStop[];
411
+ contextualSpacing?: boolean;
412
+ keepNext?: boolean;
413
+ keepLines?: boolean;
414
+ widowControl?: boolean;
415
+ pageBreakBefore?: boolean;
416
+ outlineLevel?: number;
417
+ bidi?: boolean;
418
+ suppressLineNumbers?: boolean;
419
+ suppressAutoHyphens?: boolean;
420
+ paragraphMarkRunProperties?: CanonicalRunFormatting;
421
+ }
422
+
423
+ /** Body of an OOXML `<w:docDefaults>` — baseline formatting applied before style chain. */
424
+ export interface DocumentDefaults {
425
+ paragraph?: CanonicalParagraphFormatting;
426
+ run?: CanonicalRunFormatting;
427
+ }
428
+
353
429
  export interface ParagraphNode {
354
430
  type: "paragraph";
355
431
  styleId?: string;
@@ -416,6 +492,25 @@ export interface TableWidth {
416
492
  type: "dxa" | "auto" | "pct" | "nil";
417
493
  }
418
494
 
495
+ export interface TableIndent {
496
+ value: number;
497
+ type: "dxa" | "auto" | "pct" | "nil";
498
+ }
499
+
500
+ export interface TableFloatingProperties {
501
+ horizontalAnchor?: "margin" | "page" | "text";
502
+ verticalAnchor?: "margin" | "page" | "text";
503
+ horizontalAlign?: "left" | "center" | "right" | "inside" | "outside";
504
+ horizontalOffset?: number;
505
+ verticalAlign?: "top" | "center" | "bottom" | "inside" | "outside";
506
+ verticalOffset?: number;
507
+ leftFromText?: number;
508
+ rightFromText?: number;
509
+ topFromText?: number;
510
+ bottomFromText?: number;
511
+ overlap?: boolean;
512
+ }
513
+
419
514
  export interface CellShading {
420
515
  fill?: string;
421
516
  color?: string;
@@ -481,6 +576,13 @@ export interface TableNode {
481
576
  borders?: TableBorders;
482
577
  cellMargins?: TableCellMargins;
483
578
  tblLook?: TableLook;
579
+ indent?: TableIndent;
580
+ layoutMode?: "fixed" | "autofit";
581
+ cellSpacing?: TableWidth;
582
+ caption?: string;
583
+ description?: string;
584
+ bidiVisual?: boolean;
585
+ floating?: TableFloatingProperties;
484
586
  }
485
587
 
486
588
  export interface TableRowNode {
@@ -494,6 +596,9 @@ export interface TableRowNode {
494
596
  height?: number;
495
597
  heightRule?: "auto" | "atLeast" | "exact";
496
598
  isHeader?: boolean;
599
+ cantSplit?: boolean;
600
+ horizontalAlignment?: "left" | "center" | "right";
601
+ cnfStyle?: string;
497
602
  }
498
603
 
499
604
  export interface TableCellNode {
@@ -506,6 +611,11 @@ export interface TableCellNode {
506
611
  borders?: TableCellBorders;
507
612
  shading?: CellShading;
508
613
  verticalAlign?: "top" | "center" | "bottom";
614
+ textDirection?: "lrTb" | "tbRl" | "btLr";
615
+ noWrap?: boolean;
616
+ fitText?: boolean;
617
+ margins?: TableCellMargins;
618
+ cnfStyle?: string;
509
619
  }
510
620
 
511
621
  export interface SdtCheckboxState {
@@ -560,15 +670,19 @@ export interface AltChunkNode {
560
670
  * These families have stable registry IDs, dependency metadata, and
561
671
  * runtime-owned refresh behavior.
562
672
  */
563
- export type SupportedFieldFamily = "REF" | "PAGEREF" | "NOTEREF" | "TOC";
673
+ export type SupportedFieldFamily =
674
+ | "REF"
675
+ | "PAGEREF"
676
+ | "NOTEREF"
677
+ | "TOC"
678
+ | "PAGE"
679
+ | "NUMPAGES";
564
680
 
565
681
  /**
566
682
  * Unsupported field families that remain preserve-only.
567
683
  * They survive round-trip but do not participate in runtime refresh.
568
684
  */
569
685
  export type PreserveOnlyFieldFamily =
570
- | "PAGE"
571
- | "NUMPAGES"
572
686
  | "DATE"
573
687
  | "TIME"
574
688
  | "AUTHOR"