@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.
- package/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- 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: [
|
|
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: [
|
|
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: [
|
|
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(
|
|
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(
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|