@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
@@ -1,3 +1,5 @@
1
+ import { twip } from "./twip.ts";
2
+
1
3
  interface TableWidthLike {
2
4
  value: number;
3
5
  type: string;
@@ -42,6 +44,25 @@ interface CellShadingLike {
42
44
  fill?: string;
43
45
  }
44
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
+
45
66
  interface TablePropertiesLike {
46
67
  propertiesXml?: string;
47
68
  styleId?: string;
@@ -50,6 +71,13 @@ interface TablePropertiesLike {
50
71
  borders?: TableBordersLike;
51
72
  cellMargins?: TableCellMarginsLike;
52
73
  tblLook?: TableLookLike;
74
+ indent?: TableIndentLike;
75
+ layoutMode?: string;
76
+ cellSpacing?: TableWidthLike;
77
+ caption?: string;
78
+ description?: string;
79
+ bidiVisual?: boolean;
80
+ floating?: TableFloatingPropertiesLike;
53
81
  }
54
82
 
55
83
  interface TableRowPropertiesLike {
@@ -61,6 +89,9 @@ interface TableRowPropertiesLike {
61
89
  height?: number;
62
90
  heightRule?: string;
63
91
  isHeader?: boolean;
92
+ cantSplit?: boolean;
93
+ horizontalAlignment?: string;
94
+ cnfStyle?: string;
64
95
  }
65
96
 
66
97
  interface TableCellPropertiesLike {
@@ -71,6 +102,11 @@ interface TableCellPropertiesLike {
71
102
  borders?: TableBordersLike;
72
103
  shading?: CellShadingLike;
73
104
  verticalAlign?: string;
105
+ textDirection?: string;
106
+ noWrap?: boolean;
107
+ fitText?: boolean;
108
+ margins?: TableCellMarginsLike;
109
+ cnfStyle?: string;
74
110
  }
75
111
 
76
112
  interface PropertyStripSpec {
@@ -80,16 +116,49 @@ interface PropertyStripSpec {
80
116
 
81
117
  const TABLE_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
82
118
  pairedTags: ["w:tblBorders", "w:tblCellMar"],
83
- 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
+ ],
84
133
  };
85
134
 
86
135
  const ROW_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
87
- 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
+ ],
88
147
  };
89
148
 
90
149
  const CELL_PROPERTY_STRIP_SPEC: PropertyStripSpec = {
91
- pairedTags: ["w:tcBorders"],
92
- 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
+ ],
93
162
  };
94
163
 
95
164
  export function serializeTablePropertiesXml(table: TablePropertiesLike): string {
@@ -172,6 +241,28 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
172
241
  if (table.alignment) {
173
242
  children.push(`<w:jc w:val="${escapeAttribute(table.alignment)}"/>`);
174
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
+ }
175
266
  if (table.borders) {
176
267
  const bordersXml = serializeBorders(table.borders);
177
268
  if (bordersXml) {
@@ -193,37 +284,80 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
193
284
  return children.join("");
194
285
  }
195
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
+
196
306
  function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
197
307
  const children: string[] = [];
308
+ if (row.cnfStyle) {
309
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(row.cnfStyle)}"/>`);
310
+ }
198
311
  if (row.gridBefore !== undefined) {
199
- children.push(`<w:gridBefore w:val="${row.gridBefore}"/>`);
312
+ children.push(`<w:gridBefore w:val="${twip(row.gridBefore)}"/>`);
200
313
  }
201
314
  if (row.widthBefore) {
202
- children.push(`<w:wBefore w:w="${row.widthBefore.value}" w:type="${row.widthBefore.type}"/>`);
315
+ children.push(
316
+ `<w:wBefore w:w="${twip(row.widthBefore.value)}" w:type="${row.widthBefore.type}"/>`,
317
+ );
203
318
  }
204
319
  if (row.gridAfter !== undefined) {
205
- children.push(`<w:gridAfter w:val="${row.gridAfter}"/>`);
320
+ children.push(`<w:gridAfter w:val="${twip(row.gridAfter)}"/>`);
206
321
  }
207
322
  if (row.widthAfter) {
208
- children.push(`<w:wAfter w:w="${row.widthAfter.value}" w:type="${row.widthAfter.type}"/>`);
323
+ children.push(
324
+ `<w:wAfter w:w="${twip(row.widthAfter.value)}" w:type="${row.widthAfter.type}"/>`,
325
+ );
326
+ }
327
+ if (row.cantSplit !== undefined) {
328
+ children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
209
329
  }
210
330
  if (row.height !== undefined) {
211
331
  const hRuleAttr = row.heightRule ? ` w:hRule="${escapeAttribute(row.heightRule)}"` : "";
212
- children.push(`<w:trHeight w:val="${row.height}"${hRuleAttr}/>`);
332
+ children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
213
333
  }
214
- if (row.isHeader !== undefined) {
215
- children.push(row.isHeader ? `<w:tblHeader/>` : `<w:tblHeader w:val="0"/>`);
334
+ // ST_OnOff element (A.3):
335
+ // - true → <w:tblHeader/> (element-only form = implicit true).
336
+ // - false → <w:tblHeader w:val="false"/> (explicit override — canonical
337
+ // carries an explicit false because the source distinguished it
338
+ // from "missing" at import time; never emit "0"/"1" per A.3).
339
+ // - undefined → omit (no element).
340
+ if (row.isHeader === true) {
341
+ children.push(`<w:tblHeader/>`);
342
+ } else if (row.isHeader === false) {
343
+ children.push(`<w:tblHeader w:val="false"/>`);
344
+ }
345
+ if (row.horizontalAlignment) {
346
+ children.push(`<w:jc w:val="${escapeAttribute(row.horizontalAlignment)}"/>`);
216
347
  }
217
348
  return children.join("");
218
349
  }
219
350
 
220
351
  function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
221
352
  const children: string[] = [];
353
+ if (cell.cnfStyle) {
354
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(cell.cnfStyle)}"/>`);
355
+ }
222
356
  if (cell.width) {
223
357
  children.push(serializeWidth("tcW", cell.width));
224
358
  }
225
359
  if (cell.gridSpan && cell.gridSpan > 1) {
226
- children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
360
+ children.push(`<w:gridSpan w:val="${twip(cell.gridSpan)}"/>`);
227
361
  }
228
362
  if (cell.verticalMerge) {
229
363
  children.push(
@@ -244,6 +378,21 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
244
378
  children.push(shadingXml);
245
379
  }
246
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
+ }
247
396
  if (cell.verticalAlign) {
248
397
  children.push(`<w:vAlign w:val="${escapeAttribute(cell.verticalAlign)}"/>`);
249
398
  }
@@ -251,7 +400,9 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
251
400
  }
252
401
 
253
402
  function serializeWidth(elementName: "tblW" | "tcW", width: TableWidthLike): string {
254
- return `<w:${elementName} w:w="${width.value}" w:type="${escapeAttribute(width.type)}"/>`;
403
+ // OOXML allows w:w to be percentage (pct) or twentieths-of-a-percent too, but
404
+ // both are integer-typed in the schema. Always round at the authoring edge.
405
+ return `<w:${elementName} w:w="${twip(width.value)}" w:type="${escapeAttribute(width.type)}"/>`;
255
406
  }
256
407
 
257
408
  function serializeBorders(borders: TableBordersLike): string {
@@ -265,18 +416,18 @@ function serializeBorders(borders: TableBordersLike): string {
265
416
  function serializeBorderSpec(elementName: string, border: BorderSpecLike): string {
266
417
  const attrs: string[] = [];
267
418
  if (border.value) attrs.push(`w:val="${escapeAttribute(border.value)}"`);
268
- if (border.size !== undefined) attrs.push(`w:sz="${border.size}"`);
269
- if (border.space !== undefined) attrs.push(`w:space="${border.space}"`);
419
+ if (border.size !== undefined) attrs.push(`w:sz="${twip(border.size)}"`);
420
+ if (border.space !== undefined) attrs.push(`w:space="${twip(border.space)}"`);
270
421
  if (border.color) attrs.push(`w:color="${escapeAttribute(border.color)}"`);
271
422
  return attrs.length > 0 ? `<w:${elementName} ${attrs.join(" ")}/>` : "";
272
423
  }
273
424
 
274
425
  function serializeTableCellMargins(margins: TableCellMarginsLike): string {
275
426
  const parts: string[] = [];
276
- if (margins.top !== undefined) parts.push(`<w:top w:w="${margins.top}" w:type="dxa"/>`);
277
- if (margins.left !== undefined) parts.push(`<w:left w:w="${margins.left}" w:type="dxa"/>`);
278
- if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${margins.bottom}" w:type="dxa"/>`);
279
- if (margins.right !== undefined) parts.push(`<w:right w:w="${margins.right}" w:type="dxa"/>`);
427
+ if (margins.top !== undefined) parts.push(`<w:top w:w="${twip(margins.top)}" w:type="dxa"/>`);
428
+ if (margins.left !== undefined) parts.push(`<w:left w:w="${twip(margins.left)}" w:type="dxa"/>`);
429
+ if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${twip(margins.bottom)}" w:type="dxa"/>`);
430
+ if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
280
431
  return parts.join("");
281
432
  }
282
433
 
@@ -295,7 +446,10 @@ function serializeTableLook(tblLook: TableLookLike): string {
295
446
  ] as const) {
296
447
  const value = tblLook[key];
297
448
  if (value !== undefined) {
298
- attrs.push(`${attr}="${value ? "1" : "0"}"`);
449
+ // tblLook individual attributes are ST_OnOff per ECMA-376 2nd Ed.
450
+ // Emit the string form ("true"/"false"); the "1"/"0" form is rejected
451
+ // by DocumentFormat.OpenXml's enumeration validator.
452
+ attrs.push(`${attr}="${value ? "true" : "false"}"`);
299
453
  }
300
454
  }
301
455
  return attrs.length > 0 ? `<w:tblLook ${attrs.join(" ")}/>` : "";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Integer-twip guard for OOXML numeric attributes.
3
+ *
4
+ * OOXML twip-based attributes (w:ind.left, w:pgSz.w, w:tblW.w, w:tcW.w,
5
+ * w:gridCol.w, w:trHeight.val, border w:sz, tab-stop w:pos, etc.) are
6
+ * schema-typed as ST_TwipsMeasure / ST_SignedTwipsMeasure / ST_MeasurementOrPercent
7
+ * or plain xsd:int variants. The OpenXML SDK validates these as Int32. Emitting
8
+ * a fractional value like "566.9999999999999" triggers the SDK
9
+ * "invalid 'Int32' value" finding, even when the numeric result is only off
10
+ * by float-rounding noise.
11
+ *
12
+ * These helpers normalise every numeric attribute write through `Math.round`
13
+ * and reject non-finite inputs at the authoring boundary so the serializer
14
+ * cannot accidentally emit NaN, Infinity, or fractional values.
15
+ *
16
+ * Callers should route through:
17
+ * twip(value) — optional numeric: undefined passes through; returns integer.
18
+ * requireTwip(value) — mandatory numeric: throws on non-finite input.
19
+ *
20
+ * Source:
21
+ * docs/plans/close-render-fidelity.md §2 A.2
22
+ */
23
+
24
+ /**
25
+ * Normalise a numeric twip attribute to an integer. Returns undefined when
26
+ * the input is undefined, preserving existing "omit attribute" semantics.
27
+ * Throws when the numeric input is NaN or non-finite — this indicates a
28
+ * bug in whichever authoring path produced the value and must never reach
29
+ * the serialized XML buffer.
30
+ */
31
+ export function twip(value: number | undefined | null): number | undefined {
32
+ if (value === undefined || value === null) return undefined;
33
+ if (!Number.isFinite(value)) {
34
+ throw new RangeError(
35
+ `twip(): expected finite number, received ${String(value)}`,
36
+ );
37
+ }
38
+ return Math.round(value);
39
+ }
40
+
41
+ /**
42
+ * Normalise a mandatory numeric twip attribute to an integer. Throws when
43
+ * the input is not a finite number.
44
+ */
45
+ export function requireTwip(value: number): number {
46
+ if (!Number.isFinite(value)) {
47
+ throw new RangeError(
48
+ `requireTwip(): expected finite number, received ${String(value)}`,
49
+ );
50
+ }
51
+ return Math.round(value);
52
+ }
53
+
54
+ /**
55
+ * Parse a numeric attribute from an OOXML document. Accepts whatever the
56
+ * raw string carries — Word sometimes emits floats from scaling math — and
57
+ * returns either an integer or `undefined` when the input is missing /
58
+ * unparseable. This symmetric helper is used on the parser side so the
59
+ * canonical model never holds a fractional twip.
60
+ */
61
+ export function parseTwipAttribute(raw: string | undefined): number | undefined {
62
+ if (raw === undefined || raw === null || raw === "") return undefined;
63
+ const parsed = Number.parseFloat(raw);
64
+ if (!Number.isFinite(parsed)) return undefined;
65
+ return Math.round(parsed);
66
+ }
@@ -182,6 +182,11 @@ function normalizeParagraph(
182
182
  ...(paragraph.bidi ? { bidi: paragraph.bidi } : {}),
183
183
  ...(paragraph.borders ? { borders: paragraph.borders } : {}),
184
184
  ...(paragraph.shading ? { shading: paragraph.shading } : {}),
185
+ // A.7: preserve w14:paraId / w14:textId across import → export so
186
+ // downstream tools that diff documents by paragraph id stay stable.
187
+ ...(paragraph.wordExtensionIds
188
+ ? { wordExtensionIds: paragraph.wordExtensionIds }
189
+ : {}),
185
190
  children,
186
191
  };
187
192
  }
@@ -204,6 +209,13 @@ function normalizeTable(
204
209
  ...(table.borders ? { borders: table.borders } : {}),
205
210
  ...(table.cellMargins ? { cellMargins: table.cellMargins } : {}),
206
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 } : {}),
207
219
  };
208
220
  }
209
221
 
@@ -223,6 +235,9 @@ function normalizeTableRow(
223
235
  ...(row.height !== undefined ? { height: row.height } : {}),
224
236
  ...(row.heightRule ? { heightRule: row.heightRule } : {}),
225
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 } : {}),
226
241
  cells,
227
242
  };
228
243
  }
@@ -249,6 +264,11 @@ function normalizeTableCell(
249
264
  ...(cell.borders ? { borders: cell.borders } : {}),
250
265
  ...(cell.shading ? { shading: cell.shading } : {}),
251
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 } : {}),
252
272
  children,
253
273
  };
254
274
  }
@@ -14,16 +14,31 @@ import type {
14
14
  import { classifyFieldInstruction } from "./parse-fields.ts";
15
15
  import {
16
16
  readCellBorders,
17
+ readCellCnfStyle,
18
+ readCellFitText,
19
+ readCellMargins,
20
+ readCellNoWrap,
17
21
  readCellShading,
22
+ readCellTextDirection,
18
23
  readCellVerticalAlign,
19
24
  readCellWidth,
20
25
  readGridColumns as readSharedGridColumns,
26
+ readRowCantSplit,
27
+ readRowCnfStyle,
21
28
  readRowHeight,
22
29
  readRowHeightRule,
30
+ readRowHorizontalAlignment,
23
31
  readRowIsHeader,
24
32
  readTableAlignment,
33
+ readTableBidiVisual,
25
34
  readTableBorders,
35
+ readTableCaption,
26
36
  readTableCellMargins,
37
+ readTableCellSpacing,
38
+ readTableDescription,
39
+ readTableFloating,
40
+ readTableIndent,
41
+ readTableLayoutMode,
27
42
  readTableLook,
28
43
  readTableStyleId,
29
44
  readTableWidth,
@@ -546,7 +561,8 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
546
561
  }
547
562
  case "color": {
548
563
  const colorVal = child.attributes["w:val"] ?? child.attributes.val;
549
- if (colorVal && colorVal !== "auto") marks.push({ type: "textColor", color: colorVal });
564
+ // A.9: preserve "auto" verbatim for round-trip.
565
+ if (colorVal) marks.push({ type: "textColor", color: colorVal });
550
566
  break;
551
567
  }
552
568
  case "smallCaps":
@@ -671,6 +687,13 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
671
687
  let borders: TableNode["borders"];
672
688
  let cellMargins: TableNode["cellMargins"];
673
689
  let tblLook: TableNode["tblLook"];
690
+ let indent: TableNode["indent"];
691
+ let layoutMode: TableNode["layoutMode"];
692
+ let cellSpacing: TableNode["cellSpacing"];
693
+ let caption: TableNode["caption"];
694
+ let description: TableNode["description"];
695
+ let bidiVisual: TableNode["bidiVisual"];
696
+ let floating: TableNode["floating"];
674
697
 
675
698
  for (const child of tblElement.children) {
676
699
  if (child.type !== "element") continue;
@@ -684,6 +707,13 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
684
707
  borders = readTableBorders(child);
685
708
  cellMargins = readTableCellMargins(child);
686
709
  tblLook = readTableLook(child);
710
+ indent = readTableIndent(child);
711
+ layoutMode = readTableLayoutMode(child);
712
+ cellSpacing = readTableCellSpacing(child);
713
+ caption = readTableCaption(child);
714
+ description = readTableDescription(child);
715
+ bidiVisual = readTableBidiVisual(child);
716
+ floating = readTableFloating(child);
687
717
  } else if (name === "tblGrid") {
688
718
  gridColumns = readGridColumns(child);
689
719
  } else if (name === "tr") {
@@ -702,6 +732,13 @@ function parseSimpleTableElement(tblElement: XmlElementNode): TableNode {
702
732
  ...(borders ? { borders } : {}),
703
733
  ...(cellMargins ? { cellMargins } : {}),
704
734
  ...(tblLook ? { tblLook } : {}),
735
+ ...(indent ? { indent } : {}),
736
+ ...(layoutMode ? { layoutMode } : {}),
737
+ ...(cellSpacing ? { cellSpacing } : {}),
738
+ ...(caption !== undefined ? { caption } : {}),
739
+ ...(description !== undefined ? { description } : {}),
740
+ ...(bidiVisual !== undefined ? { bidiVisual } : {}),
741
+ ...(floating ? { floating } : {}),
705
742
  };
706
743
  }
707
744
 
@@ -715,6 +752,9 @@ function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
715
752
  let height: TableRowNode["height"];
716
753
  let heightRule: TableRowNode["heightRule"];
717
754
  let isHeader: TableRowNode["isHeader"];
755
+ let cantSplit: TableRowNode["cantSplit"];
756
+ let horizontalAlignment: TableRowNode["horizontalAlignment"];
757
+ let cnfStyle: TableRowNode["cnfStyle"];
718
758
 
719
759
  for (const child of trElement.children) {
720
760
  if (child.type !== "element") continue;
@@ -725,6 +765,9 @@ function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
725
765
  height = readRowHeight(child);
726
766
  heightRule = readRowHeightRule(child);
727
767
  isHeader = readRowIsHeader(child);
768
+ cantSplit = readRowCantSplit(child);
769
+ horizontalAlignment = readRowHorizontalAlignment(child);
770
+ cnfStyle = readRowCnfStyle(child);
728
771
  } else if (name === "tc") {
729
772
  cells.push(parseSimpleTableCell(child));
730
773
  }
@@ -736,6 +779,9 @@ function parseSimpleTableRow(trElement: XmlElementNode): TableRowNode {
736
779
  ...(height !== undefined ? { height } : {}),
737
780
  ...(heightRule ? { heightRule } : {}),
738
781
  ...(isHeader !== undefined ? { isHeader } : {}),
782
+ ...(cantSplit !== undefined ? { cantSplit } : {}),
783
+ ...(horizontalAlignment ? { horizontalAlignment } : {}),
784
+ ...(cnfStyle ? { cnfStyle } : {}),
739
785
  cells,
740
786
  };
741
787
  }
@@ -749,6 +795,11 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
749
795
  let borders: TableCellNode["borders"];
750
796
  let shading: TableCellNode["shading"];
751
797
  let verticalAlign: TableCellNode["verticalAlign"];
798
+ let textDirection: TableCellNode["textDirection"];
799
+ let noWrap: TableCellNode["noWrap"];
800
+ let fitText: TableCellNode["fitText"];
801
+ let margins: TableCellNode["margins"];
802
+ let cnfStyle: TableCellNode["cnfStyle"];
752
803
 
753
804
  for (const child of tcElement.children) {
754
805
  if (child.type !== "element") continue;
@@ -769,6 +820,11 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
769
820
  borders = readCellBorders(child);
770
821
  shading = readCellShading(child);
771
822
  verticalAlign = readCellVerticalAlign(child);
823
+ textDirection = readCellTextDirection(child);
824
+ noWrap = readCellNoWrap(child);
825
+ fitText = readCellFitText(child);
826
+ margins = readCellMargins(child);
827
+ cnfStyle = readCellCnfStyle(child);
772
828
  } else if (name === "p") {
773
829
  children.push(parseParagraphElement(child));
774
830
  }
@@ -783,6 +839,11 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
783
839
  ...(borders ? { borders } : {}),
784
840
  ...(shading ? { shading } : {}),
785
841
  ...(verticalAlign ? { verticalAlign } : {}),
842
+ ...(textDirection ? { textDirection } : {}),
843
+ ...(noWrap !== undefined ? { noWrap } : {}),
844
+ ...(fitText !== undefined ? { fitText } : {}),
845
+ ...(margins ? { margins } : {}),
846
+ ...(cnfStyle ? { cnfStyle } : {}),
786
847
  children: children.length > 0 ? children : [{ type: "paragraph", children: [] }],
787
848
  };
788
849
  }