@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
@@ -3,6 +3,7 @@ import {
3
3
  isSyntheticDocxNullAbstractDefinition,
4
4
  isSyntheticDocxNullNumberingInstance,
5
5
  } from "../ooxml/numbering-sentinels.ts";
6
+ import { twip } from "./twip.ts";
6
7
 
7
8
  export const WORD_NUMBERING_CONTENT_TYPE =
8
9
  "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
@@ -48,14 +49,18 @@ export function serializeParagraphNumberingProperties(
48
49
  return "";
49
50
  }
50
51
 
51
- return `<w:numPr><w:ilvl w:val="${numbering.level}"/><w:numId w:val="${escapeAttribute(
52
- stripCanonicalPrefix(numbering.numberingInstanceId, "num:"),
52
+ return `<w:numPr><w:ilvl w:val="${clampIlvl(numbering.level)}"/><w:numId w:val="${escapeAttribute(
53
+ clampNonNegativeIdString(
54
+ stripCanonicalPrefix(numbering.numberingInstanceId, "num:"),
55
+ ),
53
56
  )}"/></w:numPr>`;
54
57
  }
55
58
 
56
59
  function serializeAbstractDefinition(definition: NumberingCatalog["abstractDefinitions"][string]): string {
57
60
  const abstractNumId = escapeAttribute(
58
- stripCanonicalPrefix(definition.abstractNumberingId, "abstract-num:"),
61
+ clampNonNegativeIdString(
62
+ stripCanonicalPrefix(definition.abstractNumberingId, "abstract-num:"),
63
+ ),
59
64
  );
60
65
  const levels = [...definition.levels]
61
66
  .sort((left, right) => left.level - right.level)
@@ -69,7 +74,10 @@ function serializeLevel(
69
74
  level: NumberingCatalog["abstractDefinitions"][string]["levels"][number],
70
75
  serializedLevel = level.level,
71
76
  ): string {
72
- const start = level.startAt !== undefined ? `<w:start w:val="${level.startAt}"/>` : "";
77
+ const start =
78
+ level.startAt !== undefined
79
+ ? `<w:start w:val="${clampStart(level.startAt)}"/>`
80
+ : "";
73
81
  const paragraphStyle = level.paragraphStyleId
74
82
  ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
75
83
  : "";
@@ -80,7 +88,7 @@ function serializeLevel(
80
88
  : "";
81
89
  const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
82
90
 
83
- return `<w:lvl w:ilvl="${serializedLevel}">${start}<w:numFmt w:val="${escapeAttribute(
91
+ return `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${start}<w:numFmt w:val="${escapeAttribute(
84
92
  level.format,
85
93
  )}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}${isLegal}${suffix}${justification}${paragraphProperties}</w:lvl>`;
86
94
  }
@@ -93,7 +101,10 @@ function serializeLevelOverride(
93
101
  return "";
94
102
  }
95
103
 
96
- const start = level.startAt !== undefined ? `<w:start w:val="${level.startAt}"/>` : "";
104
+ const start =
105
+ level.startAt !== undefined
106
+ ? `<w:start w:val="${clampStart(level.startAt)}"/>`
107
+ : "";
97
108
  const format = level.format ? `<w:numFmt w:val="${escapeAttribute(level.format)}"/>` : "";
98
109
  const text = level.text !== undefined
99
110
  ? `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`
@@ -101,12 +112,17 @@ function serializeLevelOverride(
101
112
  const paragraphStyle = level.paragraphStyleId
102
113
  ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
103
114
  : "";
115
+ // ST_OnOff element (A.3):
116
+ // - true → <w:isLgl/>.
117
+ // - false → <w:isLgl w:val="false"/> (explicit override of a parent
118
+ // abstract level that declared legal numbering on).
119
+ // - undefined → omit.
104
120
  const isLegal =
105
- level.isLegalNumbering === undefined
106
- ? ""
107
- : level.isLegalNumbering
108
- ? "<w:isLgl/>"
109
- : `<w:isLgl w:val="0"/>`;
121
+ level.isLegalNumbering === true
122
+ ? "<w:isLgl/>"
123
+ : level.isLegalNumbering === false
124
+ ? `<w:isLgl w:val="false"/>`
125
+ : "";
110
126
  const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
111
127
  const justification = level.paragraphGeometry?.justification
112
128
  ? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
@@ -114,13 +130,19 @@ function serializeLevelOverride(
114
130
  const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
115
131
  const body = `${start}${format}${text}${paragraphStyle}${isLegal}${suffix}${justification}${paragraphProperties}`;
116
132
 
117
- return body.length > 0 ? `<w:lvl w:ilvl="${serializedLevel}">${body}</w:lvl>` : "";
133
+ return body.length > 0
134
+ ? `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${body}</w:lvl>`
135
+ : "";
118
136
  }
119
137
 
120
138
  function serializeInstance(instance: NumberingCatalog["instances"][string]): string {
121
- const numId = escapeAttribute(stripCanonicalPrefix(instance.numberingInstanceId, "num:"));
139
+ const numId = escapeAttribute(
140
+ clampNonNegativeIdString(stripCanonicalPrefix(instance.numberingInstanceId, "num:")),
141
+ );
122
142
  const abstractNumId = escapeAttribute(
123
- stripCanonicalPrefix(instance.abstractNumberingId, "abstract-num:"),
143
+ clampNonNegativeIdString(
144
+ stripCanonicalPrefix(instance.abstractNumberingId, "abstract-num:"),
145
+ ),
124
146
  );
125
147
  const overrides = [...instance.overrides]
126
148
  .sort((left, right) => left.level - right.level)
@@ -132,11 +154,13 @@ function serializeInstance(instance: NumberingCatalog["instances"][string]): str
132
154
 
133
155
  function serializeOverride(override: NumberingCatalog["instances"][string]["overrides"][number]): string {
134
156
  const startOverride =
135
- override.startAt !== undefined ? `<w:startOverride w:val="${override.startAt}"/>` : "";
157
+ override.startAt !== undefined
158
+ ? `<w:startOverride w:val="${clampStart(override.startAt)}"/>`
159
+ : "";
136
160
  const levelDefinition = override.levelDefinition
137
161
  ? serializeLevelOverride(override.levelDefinition, override.level)
138
162
  : "";
139
- return `<w:lvlOverride w:ilvl="${override.level}">${startOverride}${levelDefinition}</w:lvlOverride>`;
163
+ return `<w:lvlOverride w:ilvl="${clampIlvl(override.level)}">${startOverride}${levelDefinition}</w:lvlOverride>`;
140
164
  }
141
165
 
142
166
  function serializeLevelParagraphGeometry(
@@ -150,13 +174,13 @@ function serializeLevelParagraphGeometry(
150
174
  if (paragraphGeometry.spacing) {
151
175
  const attrs: string[] = [];
152
176
  if (paragraphGeometry.spacing.before !== undefined) {
153
- attrs.push(`w:before="${paragraphGeometry.spacing.before}"`);
177
+ attrs.push(`w:before="${twip(paragraphGeometry.spacing.before)}"`);
154
178
  }
155
179
  if (paragraphGeometry.spacing.after !== undefined) {
156
- attrs.push(`w:after="${paragraphGeometry.spacing.after}"`);
180
+ attrs.push(`w:after="${twip(paragraphGeometry.spacing.after)}"`);
157
181
  }
158
182
  if (paragraphGeometry.spacing.line !== undefined) {
159
- attrs.push(`w:line="${paragraphGeometry.spacing.line}"`);
183
+ attrs.push(`w:line="${twip(paragraphGeometry.spacing.line)}"`);
160
184
  }
161
185
  if (paragraphGeometry.spacing.lineRule !== undefined) {
162
186
  attrs.push(`w:lineRule="${paragraphGeometry.spacing.lineRule}"`);
@@ -169,16 +193,16 @@ function serializeLevelParagraphGeometry(
169
193
  if (paragraphGeometry.indentation) {
170
194
  const attrs: string[] = [];
171
195
  if (paragraphGeometry.indentation.left !== undefined) {
172
- attrs.push(`w:left="${paragraphGeometry.indentation.left}"`);
196
+ attrs.push(`w:left="${twip(paragraphGeometry.indentation.left)}"`);
173
197
  }
174
198
  if (paragraphGeometry.indentation.right !== undefined) {
175
- attrs.push(`w:right="${paragraphGeometry.indentation.right}"`);
199
+ attrs.push(`w:right="${twip(paragraphGeometry.indentation.right)}"`);
176
200
  }
177
201
  if (paragraphGeometry.indentation.firstLine !== undefined) {
178
- attrs.push(`w:firstLine="${paragraphGeometry.indentation.firstLine}"`);
202
+ attrs.push(`w:firstLine="${twip(paragraphGeometry.indentation.firstLine)}"`);
179
203
  }
180
204
  if (paragraphGeometry.indentation.hanging !== undefined) {
181
- attrs.push(`w:hanging="${paragraphGeometry.indentation.hanging}"`);
205
+ attrs.push(`w:hanging="${twip(paragraphGeometry.indentation.hanging)}"`);
182
206
  }
183
207
  if (attrs.length > 0) {
184
208
  children.push(`<w:ind ${attrs.join(" ")}/>`);
@@ -189,7 +213,7 @@ function serializeLevelParagraphGeometry(
189
213
  const tabsXml = paragraphGeometry.tabStops.map((tab) => {
190
214
  const leader = tab.leader === "middleDot" ? "middledot" : tab.leader;
191
215
  const leaderAttr = leader ? ` w:leader="${leader}"` : "";
192
- return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
216
+ return `<w:tab w:val="${tab.align}" w:pos="${twip(tab.position)}"${leaderAttr}/>`;
193
217
  }).join("");
194
218
  children.push(`<w:tabs>${tabsXml}</w:tabs>`);
195
219
  }
@@ -197,6 +221,72 @@ function serializeLevelParagraphGeometry(
197
221
  return children.length > 0 ? `<w:pPr>${children.join("")}</w:pPr>` : "";
198
222
  }
199
223
 
224
+ /**
225
+ * ST_DecimalNumberOrPercent / ST_DecimalNumber based ilvl/numId/abstractNumId
226
+ * attributes are integer-typed. Ilvl is bounded 0-8 by the schema.
227
+ * Negative ids violate MinInclusive ≥ 0 and make the validator fail.
228
+ * These clampers normalise at the authoring edge (A.6 MinInclusive clamp).
229
+ */
230
+ function clampIlvl(value: number | undefined): number {
231
+ if (value === undefined || !Number.isFinite(value)) return 0;
232
+ const rounded = Math.round(value);
233
+ if (rounded < 0) {
234
+ warnClamp("w:ilvl", rounded, 0);
235
+ return 0;
236
+ }
237
+ if (rounded > 8) {
238
+ warnClamp("w:ilvl", rounded, 8);
239
+ return 8;
240
+ }
241
+ return rounded;
242
+ }
243
+
244
+ function clampStart(value: number | undefined): number {
245
+ if (value === undefined || !Number.isFinite(value)) return 1;
246
+ const rounded = Math.round(value);
247
+ // w:start MinInclusive per the schema is 0 (0 is legal for Word-created
248
+ // lists that begin with a 0-based custom format). Negative values fail.
249
+ if (rounded < 0) {
250
+ warnClamp("w:start", rounded, 0);
251
+ return 0;
252
+ }
253
+ return rounded;
254
+ }
255
+
256
+ /**
257
+ * Clamp a stringified id attribute (w:numId / w:abstractNumId) to the
258
+ * schema's MinInclusive=0. Accepts the canonical id string (produced by
259
+ * `stripCanonicalPrefix`) and returns a string safe to embed as a value.
260
+ * If the string is non-numeric or positive it passes through unchanged.
261
+ */
262
+ function clampNonNegativeIdString(value: string): string {
263
+ const parsed = Number.parseInt(value, 10);
264
+ if (!Number.isFinite(parsed)) return value;
265
+ if (parsed < 0) {
266
+ warnClamp("w:numId/w:abstractNumId", parsed, 0);
267
+ return "0";
268
+ }
269
+ return value;
270
+ }
271
+
272
+ let warnedClamps = 0;
273
+ const WARNED_CLAMPS_MAX = 20;
274
+ function warnClamp(attr: string, from: number, to: number): void {
275
+ if (warnedClamps >= WARNED_CLAMPS_MAX) return;
276
+ warnedClamps += 1;
277
+ // Only warn outside of production builds. The test runner shows these
278
+ // via console.warn automatically; in production this is a silent no-op
279
+ // so the Buffer cost is zero.
280
+ const proc = (globalThis as unknown as {
281
+ process?: { env?: Record<string, string | undefined> };
282
+ }).process;
283
+ if (proc?.env?.NODE_ENV !== "production") {
284
+ console.warn(
285
+ `[numbering] clamped ${attr} ${from} -> ${to} at MinInclusive (A.6 guard)`,
286
+ );
287
+ }
288
+ }
289
+
200
290
  function compareSerializedIds(left: string, right: string): number {
201
291
  return stripKnownPrefix(left).localeCompare(stripKnownPrefix(right), "en", { numeric: true });
202
292
  }
@@ -10,13 +10,14 @@ import type {
10
10
  ParsedTableRow,
11
11
  ParsedTableWidth,
12
12
  } from "../ooxml/parse-tables.ts";
13
+ import { twip } from "./twip.ts";
13
14
 
14
15
  export function serializeTable(table: ParsedTable): string {
15
16
  const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
16
17
  const gridXml =
17
18
  table.gridColumns.length > 0
18
19
  ? `<w:tblGrid>${table.gridColumns
19
- .map((width) => `<w:gridCol w:w="${width}"/>`)
20
+ .map((width) => `<w:gridCol w:w="${twip(width)}"/>`)
20
21
  .join("")}</w:tblGrid>`
21
22
  : "";
22
23
  const rowsXml = table.rows.map(serializeRow).join("");
@@ -46,6 +47,28 @@ function buildTablePropertiesXml(table: ParsedTable): string {
46
47
  if (table.alignment) {
47
48
  children.push(`<w:jc w:val="${table.alignment}"/>`);
48
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
+ }
49
72
  if (table.borders) {
50
73
  const bordersXml = serializeTableBorders(table.borders);
51
74
  if (bordersXml) children.push(`<w:tblBorders>${bordersXml}</w:tblBorders>`);
@@ -61,15 +84,43 @@ function buildTablePropertiesXml(table: ParsedTable): string {
61
84
  return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
62
85
  }
63
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
+
64
106
  function buildRowPropertiesXml(row: ParsedTableRow): string {
65
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
+ }
66
114
  if (row.height !== undefined) {
67
115
  const hRuleAttr = row.heightRule ? ` w:hRule="${row.heightRule}"` : "";
68
- children.push(`<w:trHeight w:val="${row.height}"${hRuleAttr}/>`);
116
+ children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
69
117
  }
70
118
  if (row.isHeader) {
71
119
  children.push(`<w:tblHeader/>`);
72
120
  }
121
+ if (row.horizontalAlignment) {
122
+ children.push(`<w:jc w:val="${row.horizontalAlignment}"/>`);
123
+ }
73
124
  return children.length > 0 ? `<w:trPr>${children.join("")}</w:trPr>` : "";
74
125
  }
75
126
 
@@ -79,11 +130,14 @@ function ensureCellProperties(cell: ParsedTableCell): string {
79
130
  }
80
131
 
81
132
  const children: string[] = [];
133
+ if (cell.cnfStyle) {
134
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
135
+ }
82
136
  if (cell.width) {
83
137
  children.push(serializeWidth("tcW", cell.width));
84
138
  }
85
139
  if (cell.gridSpan && cell.gridSpan > 1) {
86
- children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
140
+ children.push(`<w:gridSpan w:val="${twip(cell.gridSpan)}"/>`);
87
141
  }
88
142
  if (cell.verticalMerge) {
89
143
  children.push(
@@ -99,6 +153,19 @@ function ensureCellProperties(cell: ParsedTableCell): string {
99
153
  if (cell.shading) {
100
154
  children.push(serializeCellShading(cell.shading));
101
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
+ }
102
169
  if (cell.verticalAlign) {
103
170
  children.push(`<w:vAlign w:val="${cell.verticalAlign}"/>`);
104
171
  }
@@ -107,14 +174,14 @@ function ensureCellProperties(cell: ParsedTableCell): string {
107
174
  }
108
175
 
109
176
  function serializeWidth(element: string, width: ParsedTableWidth): string {
110
- return `<w:${element} w:w="${width.value}" w:type="${width.type}"/>`;
177
+ return `<w:${element} w:w="${twip(width.value)}" w:type="${width.type}"/>`;
111
178
  }
112
179
 
113
180
  function serializeBorderSpec(element: string, spec: ParsedBorderSpec): string {
114
181
  const attrs: string[] = [];
115
182
  if (spec.value) attrs.push(`w:val="${spec.value}"`);
116
- if (spec.size !== undefined) attrs.push(`w:sz="${spec.size}"`);
117
- if (spec.space !== undefined) attrs.push(`w:space="${spec.space}"`);
183
+ if (spec.size !== undefined) attrs.push(`w:sz="${twip(spec.size)}"`);
184
+ if (spec.space !== undefined) attrs.push(`w:space="${twip(spec.space)}"`);
118
185
  if (spec.color) attrs.push(`w:color="${spec.color}"`);
119
186
  const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
120
187
  return `<w:${element}${attrsStr}/>`;
@@ -149,7 +216,8 @@ function serializeTableLook(tblLook: ParsedTableLook): string {
149
216
  ] as const) {
150
217
  const value = tblLook[key];
151
218
  if (value !== undefined) {
152
- attrs.push(`${attr}="${value ? "1" : "0"}"`);
219
+ // ST_OnOff (A.3): emit "true"/"false", never "1"/"0".
220
+ attrs.push(`${attr}="${value ? "true" : "false"}"`);
153
221
  }
154
222
  }
155
223
  return attrs.length > 0 ? `<w:tblLook ${attrs.join(" ")}/>` : "";
@@ -166,9 +234,17 @@ function serializeCellShading(shading: ParsedCellShading): string {
166
234
 
167
235
  function serializeTableCellMargins(margins: ParsedCellMargins): string {
168
236
  const parts: string[] = [];
169
- if (margins.top !== undefined) parts.push(`<w:top w:w="${margins.top}" w:type="dxa"/>`);
170
- if (margins.left !== undefined) parts.push(`<w:left w:w="${margins.left}" w:type="dxa"/>`);
171
- if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${margins.bottom}" w:type="dxa"/>`);
172
- if (margins.right !== undefined) parts.push(`<w:right w:w="${margins.right}" w:type="dxa"/>`);
237
+ if (margins.top !== undefined) parts.push(`<w:top w:w="${twip(margins.top)}" w:type="dxa"/>`);
238
+ if (margins.left !== undefined) parts.push(`<w:left w:w="${twip(margins.left)}" w:type="dxa"/>`);
239
+ if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${twip(margins.bottom)}" w:type="dxa"/>`);
240
+ if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
173
241
  return parts.join("");
174
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
+ }