@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
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Serialize a `StylesCatalog` to an OOXML `<w:styles>` document fragment.
3
+ *
4
+ * ⚠️ NOT YET WIRED INTO THE EXPORT PIPELINE. As of the Task 7 commit (bullet-list
5
+ * fidelity Phase 1), the real docx export path (`reattach-preserved-parts.ts`)
6
+ * copies `word/styles.xml` verbatim from the source package — this module is
7
+ * reachable only from tests. That is fine for Phases 2–3 (the cascade resolver
8
+ * reads the parsed catalog fields; it doesn't require re-emission).
9
+ *
10
+ * Before wiring this serializer into `docx-session.ts` / `ownedOutputPaths`, the
11
+ * following gaps MUST be closed — otherwise wiring it in will silently destroy
12
+ * user data:
13
+ *
14
+ * 1. `StylesCatalog.latentStyles` is not emitted. Add a `buildLatentStylesXml`
15
+ * counterpart and call it inside `<w:styles>` after `<w:docDefaults>`.
16
+ * 2. `StylesCatalog.tables` is not emitted. Table styles round-trip through
17
+ * the existing `serialize-table-styles` path today only because styles.xml
18
+ * passes through verbatim; wiring this serializer would drop them.
19
+ * 3. The `<w:style>` metadata fields `<w:aliases>`, `<w:link>`, `<w:autoRedefine>`,
20
+ * `<w:hidden>`, `<w:uiPriority>`, `<w:semiHidden>`, `<w:unhideWhenUsed>`,
21
+ * `<w:qFormat>`, `<w:locked>`, `<w:personal>`, `<w:personal{Compose,Reply}>`,
22
+ * `<w:rsid>`, plus the `<w:customStyle>` attribute, are not modeled on
23
+ * `ParagraphStyleDefinition`/`CharacterStyleDefinition` yet. Extend the
24
+ * canonical model first, parse them, and emit them here.
25
+ * 4. Root-level `<w:styles>` metadata (`<w:docId>`, `<w:rsids>`) is not
26
+ * modeled or emitted.
27
+ *
28
+ * Until those are closed, wiring this in would be a regression.
29
+ */
30
+
31
+ import type { StylesCatalog } from "../../model/canonical-document.ts";
32
+ import { buildRunPropertiesXml } from "./serialize-run-formatting.ts";
33
+ import { buildParagraphPropertiesXml } from "./serialize-paragraph-formatting.ts";
34
+
35
+ export const WORD_STYLES_CONTENT_TYPE =
36
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
37
+
38
+ function escXml(value: string): string {
39
+ return value
40
+ .replace(/&/g, "&amp;")
41
+ .replace(/</g, "&lt;")
42
+ .replace(/>/g, "&gt;")
43
+ .replace(/"/g, "&quot;");
44
+ }
45
+
46
+ function buildDocDefaultsXml(catalog: StylesCatalog): string {
47
+ const { docDefaults } = catalog;
48
+ if (!docDefaults) return "";
49
+
50
+ const rPrXml = buildRunPropertiesXml(docDefaults.run);
51
+ const pPrXml = buildParagraphPropertiesXml(docDefaults.paragraph);
52
+
53
+ if (!rPrXml && !pPrXml) return "";
54
+
55
+ const rPrDefault = rPrXml
56
+ ? `<w:rPrDefault>${rPrXml}</w:rPrDefault>`
57
+ : "";
58
+ const pPrDefault = pPrXml
59
+ ? `<w:pPrDefault>${pPrXml}</w:pPrDefault>`
60
+ : "";
61
+
62
+ return `<w:docDefaults>${rPrDefault}${pPrDefault}</w:docDefaults>`;
63
+ }
64
+
65
+ function buildParagraphStyleXml(
66
+ style: StylesCatalog["paragraphs"][string],
67
+ ): string {
68
+ const defaultAttr = style.isDefault ? ` w:default="1"` : "";
69
+ const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
70
+ const basedOnEl = style.basedOn
71
+ ? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
72
+ : "";
73
+ const nextEl = style.nextStyle
74
+ ? `<w:next w:val="${escXml(style.nextStyle)}"/>`
75
+ : "";
76
+
77
+ // Build pPr: may contain numPr (from numbering) and any canonical formatting.
78
+ // We reconstruct the pPr children in canonical order:
79
+ // pStyle handled externally; numPr; then pPr formatting body (which includes
80
+ // outlineLvl at position 14 in the canonical order).
81
+ const numPrXml = style.numbering
82
+ ? buildStyleNumPrXml(style.numbering)
83
+ : "";
84
+
85
+ // Build pPr body — merge numPr into the formatting pPr.
86
+ // The canonical pPr formatter handles everything except numPr.
87
+ // We inject numPr after keepNext/keepLines/pageBreakBefore, before pBdr
88
+ // (position 3 in ECMA-376 pPr schema order).
89
+ const pPrBodyXml = buildParagraphPropertiesXmlWithNumPr(style.paragraphProperties, numPrXml);
90
+
91
+ const rPrXml = buildRunPropertiesXml(style.runProperties);
92
+
93
+ return (
94
+ `<w:style w:type="paragraph" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
95
+ nameEl +
96
+ basedOnEl +
97
+ nextEl +
98
+ pPrBodyXml +
99
+ rPrXml +
100
+ `</w:style>`
101
+ );
102
+ }
103
+
104
+ function buildStyleNumPrXml(
105
+ numbering: NonNullable<StylesCatalog["paragraphs"][string]["numbering"]>,
106
+ ): string {
107
+ // Strip canonical "num:" prefix to get the raw numId value.
108
+ const rawId = numbering.numberingInstanceId.startsWith("num:")
109
+ ? numbering.numberingInstanceId.slice(4)
110
+ : numbering.numberingInstanceId;
111
+ const ilvlEl =
112
+ numbering.level !== undefined
113
+ ? `<w:ilvl w:val="${numbering.level}"/>`
114
+ : "";
115
+ return `<w:numPr>${ilvlEl}<w:numId w:val="${escXml(rawId)}"/></w:numPr>`;
116
+ }
117
+
118
+ /**
119
+ * Build a `<w:pPr>` that folds in an optional `<w:numPr>` at the canonical
120
+ * schema position (after keepNext/keepLines/pageBreakBefore, before pBdr).
121
+ *
122
+ * When there is no canonical paragraph formatting AND no numPr, returns "".
123
+ */
124
+ function buildParagraphPropertiesXmlWithNumPr(
125
+ pPr: StylesCatalog["paragraphs"][string]["paragraphProperties"],
126
+ numPrXml: string,
127
+ ): string {
128
+ if (!numPrXml) {
129
+ // Delegate entirely to the shared formatter.
130
+ return buildParagraphPropertiesXml(pPr);
131
+ }
132
+
133
+ // We need to inject numPr into the correct position.
134
+ // Use the shared formatter's output if pPr exists, then splice numPr in
135
+ // after the toggle booleans (keepNext/keepLines/pageBreakBefore) and
136
+ // before pBdr. The simplest approach: re-build manually with full control.
137
+ if (!pPr) {
138
+ return `<w:pPr>${numPrXml}</w:pPr>`;
139
+ }
140
+
141
+ // Rebuild with the same order as buildParagraphPropertiesXml, inserting numPr
142
+ // after the first three toggles (positions 1-3) per ECMA-376 pPr schema.
143
+ const parts: string[] = [];
144
+ const fn = (tag: string, v: boolean | undefined) =>
145
+ v === undefined ? "" : v ? `<w:${tag}/>` : `<w:${tag} w:val="0"/>`;
146
+
147
+ parts.push(fn("keepNext", pPr.keepNext));
148
+ parts.push(fn("keepLines", pPr.keepLines));
149
+ parts.push(fn("pageBreakBefore", pPr.pageBreakBefore));
150
+ parts.push(numPrXml);
151
+ // Remaining fields delegated through the helper for pBdr, shd, tabs, etc.
152
+ // We do this by generating the full pPr body and stripping the outer <w:pPr> wrapper,
153
+ // then removing any duplicate keepNext/keepLines/pageBreakBefore already emitted.
154
+ const innerXml = buildParagraphPropertiesXml(pPr);
155
+ if (innerXml) {
156
+ // Strip outer <w:pPr>...</w:pPr> wrapper.
157
+ const inner = innerXml.replace(/^<w:pPr>/, "").replace(/<\/w:pPr>$/, "");
158
+ // Remove the three toggles we already emitted above to avoid duplication.
159
+ const deduped = inner
160
+ .replace(/<w:keepNext(?:\s[^>]*)?\/?>/g, "")
161
+ .replace(/<w:keepLines(?:\s[^>]*)?\/?>/g, "")
162
+ .replace(/<w:pageBreakBefore(?:\s[^>]*)?\/?>/g, "");
163
+ parts.push(deduped);
164
+ }
165
+
166
+ const body = parts.filter(Boolean).join("");
167
+ return body.length > 0 ? `<w:pPr>${body}</w:pPr>` : "";
168
+ }
169
+
170
+ function buildCharacterStyleXml(
171
+ style: StylesCatalog["characters"][string],
172
+ ): string {
173
+ const defaultAttr = style.isDefault ? ` w:default="1"` : "";
174
+ const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
175
+ const basedOnEl = style.basedOn
176
+ ? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
177
+ : "";
178
+ const rPrXml = buildRunPropertiesXml(style.runProperties);
179
+
180
+ return (
181
+ `<w:style w:type="character" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
182
+ nameEl +
183
+ basedOnEl +
184
+ rPrXml +
185
+ `</w:style>`
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Serialize a `StylesCatalog` to a `<w:styles>` XML string.
191
+ *
192
+ * Only paragraph and character styles are emitted. Table styles are skipped
193
+ * (they are preserved as opaque package fragments in round-trip workflows).
194
+ */
195
+ export function serializeStylesXml(catalog: StylesCatalog): string {
196
+ const docDefaultsXml = buildDocDefaultsXml(catalog);
197
+
198
+ const paragraphStyles = Object.values(catalog.paragraphs)
199
+ .map((style) => buildParagraphStyleXml(style))
200
+ .join("");
201
+
202
+ const characterStyles = Object.values(catalog.characters)
203
+ .map((style) => buildCharacterStyleXml(style))
204
+ .join("");
205
+
206
+ const body = docDefaultsXml + paragraphStyles + characterStyles;
207
+
208
+ return [
209
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
210
+ `<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">${body}</w:styles>`,
211
+ ].join("\n");
212
+ }
@@ -47,6 +47,28 @@ function buildTablePropertiesXml(table: ParsedTable): string {
47
47
  if (table.alignment) {
48
48
  children.push(`<w:jc w:val="${table.alignment}"/>`);
49
49
  }
50
+ if (table.indent) {
51
+ children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${table.indent.type}"/>`);
52
+ }
53
+ if (table.layoutMode) {
54
+ children.push(`<w:tblLayout w:type="${table.layoutMode}"/>`);
55
+ }
56
+ if (table.cellSpacing) {
57
+ children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${table.cellSpacing.type}"/>`);
58
+ }
59
+ if (table.bidiVisual !== undefined) {
60
+ children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
61
+ }
62
+ if (table.caption !== undefined) {
63
+ children.push(`<w:tblCaption w:val="${escapeAttribute(table.caption)}"/>`);
64
+ }
65
+ if (table.description !== undefined) {
66
+ children.push(`<w:tblDescription w:val="${escapeAttribute(table.description)}"/>`);
67
+ }
68
+ if (table.floating) {
69
+ const floatingXml = serializeTableFloating(table.floating);
70
+ if (floatingXml) children.push(floatingXml);
71
+ }
50
72
  if (table.borders) {
51
73
  const bordersXml = serializeTableBorders(table.borders);
52
74
  if (bordersXml) children.push(`<w:tblBorders>${bordersXml}</w:tblBorders>`);
@@ -62,8 +84,33 @@ function buildTablePropertiesXml(table: ParsedTable): string {
62
84
  return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
63
85
  }
64
86
 
87
+ function serializeTableFloating(floating: NonNullable<ParsedTable["floating"]>): string {
88
+ const attrs: string[] = [];
89
+ if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${floating.horizontalAnchor}"`);
90
+ if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${floating.verticalAnchor}"`);
91
+ if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${floating.horizontalAlign}"`);
92
+ if (floating.horizontalOffset !== undefined) attrs.push(`w:tblpX="${floating.horizontalOffset}"`);
93
+ if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${floating.verticalAlign}"`);
94
+ if (floating.verticalOffset !== undefined) attrs.push(`w:tblpY="${floating.verticalOffset}"`);
95
+ if (floating.leftFromText !== undefined) attrs.push(`w:leftFromText="${floating.leftFromText}"`);
96
+ if (floating.rightFromText !== undefined) attrs.push(`w:rightFromText="${floating.rightFromText}"`);
97
+ if (floating.topFromText !== undefined) attrs.push(`w:topFromText="${floating.topFromText}"`);
98
+ if (floating.bottomFromText !== undefined) attrs.push(`w:bottomFromText="${floating.bottomFromText}"`);
99
+ const tblpPr = attrs.length > 0 ? `<w:tblpPr ${attrs.join(" ")}/>` : "";
100
+ const overlap = floating.overlap !== undefined
101
+ ? `<w:tblOverlap w:val="${floating.overlap ? "overlap" : "never"}"/>`
102
+ : "";
103
+ return `${tblpPr}${overlap}`;
104
+ }
105
+
65
106
  function buildRowPropertiesXml(row: ParsedTableRow): string {
66
107
  const children: string[] = [];
108
+ if (row.cnfStyle) {
109
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
110
+ }
111
+ if (row.cantSplit !== undefined) {
112
+ children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
113
+ }
67
114
  if (row.height !== undefined) {
68
115
  const hRuleAttr = row.heightRule ? ` w:hRule="${row.heightRule}"` : "";
69
116
  children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
@@ -71,6 +118,9 @@ function buildRowPropertiesXml(row: ParsedTableRow): string {
71
118
  if (row.isHeader) {
72
119
  children.push(`<w:tblHeader/>`);
73
120
  }
121
+ if (row.horizontalAlignment) {
122
+ children.push(`<w:jc w:val="${row.horizontalAlignment}"/>`);
123
+ }
74
124
  return children.length > 0 ? `<w:trPr>${children.join("")}</w:trPr>` : "";
75
125
  }
76
126
 
@@ -80,6 +130,9 @@ function ensureCellProperties(cell: ParsedTableCell): string {
80
130
  }
81
131
 
82
132
  const children: string[] = [];
133
+ if (cell.cnfStyle) {
134
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
135
+ }
83
136
  if (cell.width) {
84
137
  children.push(serializeWidth("tcW", cell.width));
85
138
  }
@@ -100,6 +153,19 @@ function ensureCellProperties(cell: ParsedTableCell): string {
100
153
  if (cell.shading) {
101
154
  children.push(serializeCellShading(cell.shading));
102
155
  }
156
+ if (cell.margins) {
157
+ const marginsXml = serializeTableCellMargins(cell.margins);
158
+ if (marginsXml) children.push(`<w:tcMar>${marginsXml}</w:tcMar>`);
159
+ }
160
+ if (cell.noWrap !== undefined) {
161
+ children.push(cell.noWrap ? `<w:noWrap/>` : `<w:noWrap w:val="0"/>`);
162
+ }
163
+ if (cell.fitText !== undefined) {
164
+ children.push(cell.fitText ? `<w:tcFitText/>` : `<w:tcFitText w:val="0"/>`);
165
+ }
166
+ if (cell.textDirection) {
167
+ children.push(`<w:textDirection w:val="${cell.textDirection}"/>`);
168
+ }
103
169
  if (cell.verticalAlign) {
104
170
  children.push(`<w:vAlign w:val="${cell.verticalAlign}"/>`);
105
171
  }
@@ -174,3 +240,11 @@ function serializeTableCellMargins(margins: ParsedCellMargins): string {
174
240
  if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
175
241
  return parts.join("");
176
242
  }
243
+
244
+ function escapeAttribute(value: string): string {
245
+ return value
246
+ .replace(/&/gu, "&amp;")
247
+ .replace(/"/gu, "&quot;")
248
+ .replace(/</gu, "&lt;")
249
+ .replace(/>/gu, "&gt;");
250
+ }
@@ -44,6 +44,25 @@ interface CellShadingLike {
44
44
  fill?: string;
45
45
  }
46
46
 
47
+ interface TableIndentLike {
48
+ value: number;
49
+ type: string;
50
+ }
51
+
52
+ interface TableFloatingPropertiesLike {
53
+ horizontalAnchor?: string;
54
+ verticalAnchor?: string;
55
+ horizontalAlign?: string;
56
+ horizontalOffset?: number;
57
+ verticalAlign?: string;
58
+ verticalOffset?: number;
59
+ leftFromText?: number;
60
+ rightFromText?: number;
61
+ topFromText?: number;
62
+ bottomFromText?: number;
63
+ overlap?: boolean;
64
+ }
65
+
47
66
  interface TablePropertiesLike {
48
67
  propertiesXml?: string;
49
68
  styleId?: string;
@@ -52,6 +71,13 @@ interface TablePropertiesLike {
52
71
  borders?: TableBordersLike;
53
72
  cellMargins?: TableCellMarginsLike;
54
73
  tblLook?: TableLookLike;
74
+ indent?: TableIndentLike;
75
+ layoutMode?: string;
76
+ cellSpacing?: TableWidthLike;
77
+ caption?: string;
78
+ description?: string;
79
+ bidiVisual?: boolean;
80
+ floating?: TableFloatingPropertiesLike;
55
81
  }
56
82
 
57
83
  interface TableRowPropertiesLike {
@@ -63,6 +89,9 @@ interface TableRowPropertiesLike {
63
89
  height?: number;
64
90
  heightRule?: string;
65
91
  isHeader?: boolean;
92
+ cantSplit?: boolean;
93
+ horizontalAlignment?: string;
94
+ cnfStyle?: string;
66
95
  }
67
96
 
68
97
  interface TableCellPropertiesLike {
@@ -73,6 +102,11 @@ interface TableCellPropertiesLike {
73
102
  borders?: TableBordersLike;
74
103
  shading?: CellShadingLike;
75
104
  verticalAlign?: string;
105
+ textDirection?: string;
106
+ noWrap?: boolean;
107
+ fitText?: boolean;
108
+ margins?: TableCellMarginsLike;
109
+ cnfStyle?: string;
76
110
  }
77
111
 
78
112
  interface PropertyStripSpec {
@@ -82,16 +116,49 @@ interface PropertyStripSpec {
82
116
 
83
117
  const TABLE_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
84
118
  pairedTags: ["w:tblBorders", "w:tblCellMar"],
85
- selfClosingTags: ["w:tblStyle", "w:tblW", "w:jc", "w:tblLook"],
119
+ selfClosingTags: [
120
+ "w:tblStyle",
121
+ "w:tblW",
122
+ "w:jc",
123
+ "w:tblLook",
124
+ "w:tblInd",
125
+ "w:tblLayout",
126
+ "w:tblCellSpacing",
127
+ "w:tblCaption",
128
+ "w:tblDescription",
129
+ "w:bidiVisual",
130
+ "w:tblpPr",
131
+ "w:tblOverlap",
132
+ ],
86
133
  };
87
134
 
88
135
  const ROW_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
89
- selfClosingTags: ["w:gridBefore", "w:wBefore", "w:gridAfter", "w:wAfter", "w:trHeight", "w:tblHeader"],
136
+ selfClosingTags: [
137
+ "w:gridBefore",
138
+ "w:wBefore",
139
+ "w:gridAfter",
140
+ "w:wAfter",
141
+ "w:trHeight",
142
+ "w:tblHeader",
143
+ "w:cantSplit",
144
+ "w:jc",
145
+ "w:cnfStyle",
146
+ ],
90
147
  };
91
148
 
92
149
  const CELL_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
93
- pairedTags: ["w:tcBorders"],
94
- selfClosingTags: ["w:tcW", "w:gridSpan", "w:vMerge", "w:shd", "w:vAlign"],
150
+ pairedTags: ["w:tcBorders", "w:tcMar"],
151
+ selfClosingTags: [
152
+ "w:tcW",
153
+ "w:gridSpan",
154
+ "w:vMerge",
155
+ "w:shd",
156
+ "w:vAlign",
157
+ "w:textDirection",
158
+ "w:noWrap",
159
+ "w:tcFitText",
160
+ "w:cnfStyle",
161
+ ],
95
162
  };
96
163
 
97
164
  export function serializeTablePropertiesXml(table: TablePropertiesLike): string {
@@ -174,6 +241,28 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
174
241
  if (table.alignment) {
175
242
  children.push(`<w:jc w:val="${escapeAttribute(table.alignment)}"/>`);
176
243
  }
244
+ if (table.indent) {
245
+ children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${escapeAttribute(table.indent.type)}"/>`);
246
+ }
247
+ if (table.layoutMode) {
248
+ children.push(`<w:tblLayout w:type="${escapeAttribute(table.layoutMode)}"/>`);
249
+ }
250
+ if (table.cellSpacing) {
251
+ children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${escapeAttribute(table.cellSpacing.type)}"/>`);
252
+ }
253
+ if (table.bidiVisual !== undefined) {
254
+ children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
255
+ }
256
+ if (table.caption !== undefined) {
257
+ children.push(`<w:tblCaption w:val="${escapeAttribute(table.caption)}"/>`);
258
+ }
259
+ if (table.description !== undefined) {
260
+ children.push(`<w:tblDescription w:val="${escapeAttribute(table.description)}"/>`);
261
+ }
262
+ if (table.floating) {
263
+ const floatingXml = serializeTableFloating(table.floating);
264
+ if (floatingXml) children.push(floatingXml);
265
+ }
177
266
  if (table.borders) {
178
267
  const bordersXml = serializeBorders(table.borders);
179
268
  if (bordersXml) {
@@ -195,8 +284,30 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
195
284
  return children.join("");
196
285
  }
197
286
 
287
+ function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
288
+ const attrs: string[] = [];
289
+ if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${escapeAttribute(floating.horizontalAnchor)}"`);
290
+ if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${escapeAttribute(floating.verticalAnchor)}"`);
291
+ if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${escapeAttribute(floating.horizontalAlign)}"`);
292
+ if (floating.horizontalOffset !== undefined) attrs.push(`w:tblpX="${floating.horizontalOffset}"`);
293
+ if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${escapeAttribute(floating.verticalAlign)}"`);
294
+ if (floating.verticalOffset !== undefined) attrs.push(`w:tblpY="${floating.verticalOffset}"`);
295
+ if (floating.leftFromText !== undefined) attrs.push(`w:leftFromText="${floating.leftFromText}"`);
296
+ if (floating.rightFromText !== undefined) attrs.push(`w:rightFromText="${floating.rightFromText}"`);
297
+ if (floating.topFromText !== undefined) attrs.push(`w:topFromText="${floating.topFromText}"`);
298
+ if (floating.bottomFromText !== undefined) attrs.push(`w:bottomFromText="${floating.bottomFromText}"`);
299
+ const tblpPr = attrs.length > 0 ? `<w:tblpPr ${attrs.join(" ")}/>` : "";
300
+ const overlap = floating.overlap !== undefined
301
+ ? `<w:tblOverlap w:val="${floating.overlap ? "overlap" : "never"}"/>`
302
+ : "";
303
+ return `${tblpPr}${overlap}`;
304
+ }
305
+
198
306
  function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
199
307
  const children: string[] = [];
308
+ if (row.cnfStyle) {
309
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
310
+ }
200
311
  if (row.gridBefore !== undefined) {
201
312
  children.push(`<w:gridBefore w:val="${twip(row.gridBefore)}"/>`);
202
313
  }
@@ -213,6 +324,9 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
213
324
  `<w:wAfter w:w="${twip(row.widthAfter.value)}" w:type="${row.widthAfter.type}"/>`,
214
325
  );
215
326
  }
327
+ if (row.cantSplit !== undefined) {
328
+ children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
329
+ }
216
330
  if (row.height !== undefined) {
217
331
  const hRuleAttr = row.heightRule ? ` w:hRule="${escapeAttribute(row.heightRule)}"` : "";
218
332
  children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
@@ -228,11 +342,17 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
228
342
  } else if (row.isHeader === false) {
229
343
  children.push(`<w:tblHeader w:val="false"/>`);
230
344
  }
345
+ if (row.horizontalAlignment) {
346
+ children.push(`<w:jc w:val="${escapeAttribute(row.horizontalAlignment)}"/>`);
347
+ }
231
348
  return children.join("");
232
349
  }
233
350
 
234
351
  function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
235
352
  const children: string[] = [];
353
+ if (cell.cnfStyle) {
354
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
355
+ }
236
356
  if (cell.width) {
237
357
  children.push(serializeWidth("tcW", cell.width));
238
358
  }
@@ -258,6 +378,21 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
258
378
  children.push(shadingXml);
259
379
  }
260
380
  }
381
+ if (cell.margins) {
382
+ const marginsXml = serializeTableCellMargins(cell.margins);
383
+ if (marginsXml) {
384
+ children.push(`<w:tcMar>${marginsXml}</w:tcMar>`);
385
+ }
386
+ }
387
+ if (cell.noWrap !== undefined) {
388
+ children.push(cell.noWrap ? `<w:noWrap/>` : `<w:noWrap w:val="0"/>`);
389
+ }
390
+ if (cell.fitText !== undefined) {
391
+ children.push(cell.fitText ? `<w:tcFitText/>` : `<w:tcFitText w:val="0"/>`);
392
+ }
393
+ if (cell.textDirection) {
394
+ children.push(`<w:textDirection w:val="${escapeAttribute(cell.textDirection)}"/>`);
395
+ }
261
396
  if (cell.verticalAlign) {
262
397
  children.push(`<w:vAlign w:val="${escapeAttribute(cell.verticalAlign)}"/>`);
263
398
  }
@@ -209,6 +209,13 @@ function normalizeTable(
209
209
  ...(table.borders ? { borders: table.borders } : {}),
210
210
  ...(table.cellMargins ? { cellMargins: table.cellMargins } : {}),
211
211
  ...(table.tblLook ? { tblLook: table.tblLook } : {}),
212
+ ...(table.indent ? { indent: table.indent } : {}),
213
+ ...(table.layoutMode ? { layoutMode: table.layoutMode } : {}),
214
+ ...(table.cellSpacing ? { cellSpacing: table.cellSpacing } : {}),
215
+ ...(table.caption !== undefined ? { caption: table.caption } : {}),
216
+ ...(table.description !== undefined ? { description: table.description } : {}),
217
+ ...(table.bidiVisual !== undefined ? { bidiVisual: table.bidiVisual } : {}),
218
+ ...(table.floating ? { floating: table.floating } : {}),
212
219
  };
213
220
  }
214
221
 
@@ -228,6 +235,9 @@ function normalizeTableRow(
228
235
  ...(row.height !== undefined ? { height: row.height } : {}),
229
236
  ...(row.heightRule ? { heightRule: row.heightRule } : {}),
230
237
  ...(row.isHeader !== undefined ? { isHeader: row.isHeader } : {}),
238
+ ...(row.cantSplit !== undefined ? { cantSplit: row.cantSplit } : {}),
239
+ ...(row.horizontalAlignment ? { horizontalAlignment: row.horizontalAlignment } : {}),
240
+ ...(row.cnfStyle ? { cnfStyle: row.cnfStyle } : {}),
231
241
  cells,
232
242
  };
233
243
  }
@@ -254,6 +264,11 @@ function normalizeTableCell(
254
264
  ...(cell.borders ? { borders: cell.borders } : {}),
255
265
  ...(cell.shading ? { shading: cell.shading } : {}),
256
266
  ...(cell.verticalAlign ? { verticalAlign: cell.verticalAlign } : {}),
267
+ ...(cell.textDirection ? { textDirection: cell.textDirection } : {}),
268
+ ...(cell.noWrap !== undefined ? { noWrap: cell.noWrap } : {}),
269
+ ...(cell.fitText !== undefined ? { fitText: cell.fitText } : {}),
270
+ ...(cell.margins ? { margins: cell.margins } : {}),
271
+ ...(cell.cnfStyle ? { cnfStyle: cell.cnfStyle } : {}),
257
272
  children,
258
273
  };
259
274
  }
@@ -215,7 +215,14 @@ import type {
215
215
  const FIELD_FAMILY_PATTERN =
216
216
  /^\s*(REF|PAGEREF|NOTEREF|TOC|PAGE|NUMPAGES|DATE|TIME|AUTHOR|FILENAME|MERGEFIELD|IF|SEQ|INDEX|TC|STYLEREF)\b/i;
217
217
 
218
- const SUPPORTED_FAMILIES = new Set<string>(["REF", "PAGEREF", "NOTEREF", "TOC"]);
218
+ const SUPPORTED_FAMILIES = new Set<string>([
219
+ "REF",
220
+ "PAGEREF",
221
+ "NOTEREF",
222
+ "TOC",
223
+ "PAGE",
224
+ "NUMPAGES",
225
+ ]);
219
226
 
220
227
  /**
221
228
  * Classify a field instruction into its field family.
@@ -257,8 +264,8 @@ export function isSupportedFieldFamily(family: FieldFamily): family is Supported
257
264
  * Build a field registry from a canonical document, cataloging every field
258
265
  * instance with its classification, dependency metadata, and refresh status.
259
266
  *
260
- * The registry partitions fields into `supported` (REF, PAGEREF, NOTEREF, TOC)
261
- * and `preserveOnly` (all others) slices.
267
+ * The registry partitions fields into `supported` (REF, PAGEREF, NOTEREF,
268
+ * TOC, PAGE, NUMPAGES) and `preserveOnly` (all others) slices.
262
269
  */
263
270
  export function buildFieldRegistry(
264
271
  document: Pick<CanonicalDocument, "content" | "styles"> & {