docx-kit 0.1.0 → 0.3.0
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 +13 -15
- package/dist/browser.d.ts +65 -44
- package/dist/browser.js +73 -30
- package/dist/node.d.ts +58 -53
- package/dist/node.js +78 -16
- package/package.json +68 -48
- package/dist/compileDocument-BvuU28Yj.js +0 -750
- package/dist/errors-c9CC63Ti.js +0 -63
- package/dist/fs-DF8ug9Wi.js +0 -43
- package/dist/image-6yS25Ggr.js +0 -76
- package/dist/index.d.ts +0 -1032
- package/dist/index.js +0 -683
- package/dist/pack-0Jsv03Sk.js +0 -79
|
@@ -1,750 +0,0 @@
|
|
|
1
|
-
import { t as DocxKitError } from "./errors-c9CC63Ti.js";
|
|
2
|
-
import { n as dataUrlToUint8Array, t as createImageRun } from "./image-6yS25Ggr.js";
|
|
3
|
-
import { AlignmentType, Document, HeadingLevel, PageBreak, Paragraph, ShadingType, Table, TableCell, TableRow, TextRun, VerticalAlign, WidthType } from "docx";
|
|
4
|
-
//#region src/style/normalizeStyle.ts
|
|
5
|
-
/**
|
|
6
|
-
* Style normalization — merge multiple style sources into one resolved rule.
|
|
7
|
-
*
|
|
8
|
-
* Implements the CSS-like cascade:
|
|
9
|
-
* `defaults → className(s) → inline style`
|
|
10
|
-
*
|
|
11
|
-
* @module style/normalizeStyle
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Merge styles in priority order (lowest → highest):
|
|
15
|
-
* `base` → `className`(s) → `inline style`.
|
|
16
|
-
*
|
|
17
|
-
* - `base` — Typically a default style for the element type
|
|
18
|
-
* - `className` — Single string, space-separated string, or array of class names
|
|
19
|
-
* - `inline` — Inline style override on the node itself
|
|
20
|
-
* - `styles` — The stylesheet map to resolve class names against
|
|
21
|
-
*
|
|
22
|
-
* @returns A new merged `DocxStyleRule` (does not mutate inputs).
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* const resolved = resolveStyle({
|
|
27
|
-
* base: { fontSize: 12 },
|
|
28
|
-
* className: ['body', 'blue'],
|
|
29
|
-
* inline: { fontWeight: 'bold' },
|
|
30
|
-
* styles: { body: { lineHeight: 1.5 }, blue: { color: '#00f' } },
|
|
31
|
-
* })
|
|
32
|
-
* // => { fontSize: 12, lineHeight: 1.5, color: '#00f', fontWeight: 'bold' }
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
function resolveStyle(options) {
|
|
36
|
-
const classStyles = (Array.isArray(options.className) ? options.className : options.className ? options.className.split(/\s+/).filter(Boolean) : []).map((name) => {
|
|
37
|
-
const rule = options.styles?.[name];
|
|
38
|
-
if (options.styles != null && rule == null) throw new DocxKitError("STYLE_UNKNOWN_CLASS", `Style class not found: "${name}"`);
|
|
39
|
-
return rule;
|
|
40
|
-
}).filter((s) => s != null);
|
|
41
|
-
return Object.assign({}, options.base, ...classStyles, options.inline);
|
|
42
|
-
}
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region src/compiler/units.ts
|
|
45
|
-
/**
|
|
46
|
-
* Word OpenXML unit conversion utilities.
|
|
47
|
-
*
|
|
48
|
-
* All bare numbers are interpreted according to the property they represent:
|
|
49
|
-
* - `fontSize` (toPtHalf) → **pt**
|
|
50
|
-
* - spacing / indent / margin (toTwip) → **pt**
|
|
51
|
-
* - image size (toPx) → **px**
|
|
52
|
-
*
|
|
53
|
-
* @module compiler/units
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* Points per inch — standard typographic convention.
|
|
57
|
-
*
|
|
58
|
-
* All other conversions derive from this and the DPI assumption.
|
|
59
|
-
*/
|
|
60
|
-
const POINTS_PER_INCH = 72;
|
|
61
|
-
/**
|
|
62
|
-
* Pixels per inch at 96 DPI (standard screen resolution).
|
|
63
|
-
*/
|
|
64
|
-
const PIXELS_PER_INCH = 96;
|
|
65
|
-
/**
|
|
66
|
-
* Pixels per point at 96 DPI.
|
|
67
|
-
*
|
|
68
|
-
* 96 px/in ÷ 72 pt/in = 4/3 ≈ 1.333 px/pt.
|
|
69
|
-
*/
|
|
70
|
-
const PX_PER_POINT = PIXELS_PER_INCH / POINTS_PER_INCH;
|
|
71
|
-
/**
|
|
72
|
-
* Points per pixel at 96 DPI.
|
|
73
|
-
*
|
|
74
|
-
* 72 pt/in ÷ 96 px/in = 0.75 pt/px.
|
|
75
|
-
*/
|
|
76
|
-
const POINTS_PER_PX = POINTS_PER_INCH / PIXELS_PER_INCH;
|
|
77
|
-
/**
|
|
78
|
-
* Points per millimeter.
|
|
79
|
-
*
|
|
80
|
-
* 72 pt/in ÷ 25.4 mm/in ≈ 2.835 pt/mm.
|
|
81
|
-
*/
|
|
82
|
-
const POINTS_PER_MM = POINTS_PER_INCH / 25.4;
|
|
83
|
-
/**
|
|
84
|
-
* Points per centimeter.
|
|
85
|
-
*
|
|
86
|
-
* POINTS_PER_MM × 10.
|
|
87
|
-
*/
|
|
88
|
-
const POINTS_PER_CM = POINTS_PER_MM * 10;
|
|
89
|
-
/**
|
|
90
|
-
* Twips per point — the fundamental Word OpenXML spacing unit.
|
|
91
|
-
*
|
|
92
|
-
* 1 twip = 1/20 of a point. All margins, indents, and spacing
|
|
93
|
-
* values in Word are expressed in twips.
|
|
94
|
-
*/
|
|
95
|
-
const TWIPS_PER_POINT = 20;
|
|
96
|
-
/**
|
|
97
|
-
* Twips per pixel at 96 DPI.
|
|
98
|
-
*
|
|
99
|
-
* POINTS_PER_PX × TWIPS_PER_POINT = 0.75 × 20 = 15.
|
|
100
|
-
*/
|
|
101
|
-
const TWIPS_PER_PX = POINTS_PER_PX * TWIPS_PER_POINT;
|
|
102
|
-
/**
|
|
103
|
-
* Half-points per point (used for font sizes in Word OpenXML).
|
|
104
|
-
*
|
|
105
|
-
* Word stores font sizes in half-points: 12pt → 24 half-points.
|
|
106
|
-
*/
|
|
107
|
-
const HALF_POINTS_PER_POINT = 2;
|
|
108
|
-
/**
|
|
109
|
-
* Half-points per pixel at 96 DPI.
|
|
110
|
-
*
|
|
111
|
-
* POINTS_PER_PX × HALF_POINTS_PER_POINT = 0.75 × 2 = 1.5.
|
|
112
|
-
*/
|
|
113
|
-
const HALF_POINTS_PER_PX = POINTS_PER_PX * HALF_POINTS_PER_POINT;
|
|
114
|
-
/**
|
|
115
|
-
* Parse a CSS margin/padding shorthand string into top/right/bottom/left
|
|
116
|
-
* values expressed in **twips** (1/20 pt).
|
|
117
|
-
*
|
|
118
|
-
* Supports 1-value, 2-value, and 4-value shorthand.
|
|
119
|
-
*
|
|
120
|
-
* @param value - — Single number, unit string, or CSS shorthand
|
|
121
|
-
* @returns `{ top, right, bottom, left }` values in twips, or `undefined`
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* ```ts
|
|
125
|
-
* parseShorthandTwip('10pt 20pt')
|
|
126
|
-
* // => { top: 200, right: 400, bottom: 200, left: 400 }
|
|
127
|
-
*
|
|
128
|
-
* parseShorthandTwip('5pt 10pt 15pt 20pt')
|
|
129
|
-
* // => { top: 100, right: 200, bottom: 300, left: 400 }
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
function parseShorthandTwip(value) {
|
|
133
|
-
if (value == null) return;
|
|
134
|
-
const parts = String(value).trim().split(/\s+/);
|
|
135
|
-
if (parts.length === 1) {
|
|
136
|
-
const v = toTwip(parts[0]) ?? 0;
|
|
137
|
-
return {
|
|
138
|
-
bottom: v,
|
|
139
|
-
left: v,
|
|
140
|
-
right: v,
|
|
141
|
-
top: v
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (parts.length === 2) {
|
|
145
|
-
const [tb, lr] = parts.map((p) => toTwip(p) ?? 0);
|
|
146
|
-
return {
|
|
147
|
-
bottom: tb,
|
|
148
|
-
left: lr,
|
|
149
|
-
right: lr,
|
|
150
|
-
top: tb
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (parts.length === 4) {
|
|
154
|
-
const [top, right, bottom, left] = parts.map((p) => toTwip(p) ?? 0);
|
|
155
|
-
return {
|
|
156
|
-
bottom,
|
|
157
|
-
left,
|
|
158
|
-
right,
|
|
159
|
-
top
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Convert a font-size value to Word **half-points** (used by the `docx`
|
|
165
|
-
* library's `size` field on text runs).
|
|
166
|
-
*
|
|
167
|
-
* Bare number is treated as **pt** (e.g. `12` → 24 half-points).
|
|
168
|
-
*
|
|
169
|
-
* @param value - — Number (pt) or unit string (`"12pt"`, `"16px"`, `"4mm"`, etc.)
|
|
170
|
-
* @returns Half-point integer, or `undefined` if input is null/undefined
|
|
171
|
-
*
|
|
172
|
-
* @example
|
|
173
|
-
* ```ts
|
|
174
|
-
* toPtHalf(12) // 24 (12pt × 2)
|
|
175
|
-
* toPtHalf('14pt') // 28
|
|
176
|
-
* toPtHalf('16px') // 24 (16px ≈ 12pt)
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
|
-
function toPtHalf(value) {
|
|
180
|
-
if (value == null) return;
|
|
181
|
-
if (typeof value === "number") return Math.round(value * HALF_POINTS_PER_POINT);
|
|
182
|
-
const n = Number.parseFloat(value);
|
|
183
|
-
if (value.endsWith("pt")) return Math.round(n * HALF_POINTS_PER_POINT);
|
|
184
|
-
if (value.endsWith("px")) return Math.round(n * HALF_POINTS_PER_PX);
|
|
185
|
-
if (value.endsWith("mm")) return Math.round(n * POINTS_PER_MM * HALF_POINTS_PER_POINT);
|
|
186
|
-
if (value.endsWith("cm")) return Math.round(n * POINTS_PER_CM * HALF_POINTS_PER_POINT);
|
|
187
|
-
if (value.endsWith("in")) return Math.round(n * POINTS_PER_INCH * HALF_POINTS_PER_POINT);
|
|
188
|
-
return Math.round(n * HALF_POINTS_PER_POINT);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Convert a length value to **pixels** (used for image dimensions).
|
|
192
|
-
*
|
|
193
|
-
* Bare number is treated as **px**.
|
|
194
|
-
*
|
|
195
|
-
* @param value - — Number (px) or unit string (`"100px"`, `"72pt"`, `"5cm"`, etc.)
|
|
196
|
-
* @returns Pixel integer, or `undefined` if input is null/undefined
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* ```ts
|
|
200
|
-
* toPx(200) // 200
|
|
201
|
-
* toPx('150px') // 150
|
|
202
|
-
* toPx('72pt') // 96 (72pt × 1.333)
|
|
203
|
-
* ```
|
|
204
|
-
*/
|
|
205
|
-
function toPx(value) {
|
|
206
|
-
if (value == null) return;
|
|
207
|
-
if (typeof value === "number") return value;
|
|
208
|
-
const n = Number.parseFloat(value);
|
|
209
|
-
if (value.endsWith("px")) return n;
|
|
210
|
-
if (value.endsWith("pt")) return Math.round(n * PX_PER_POINT);
|
|
211
|
-
if (value.endsWith("mm")) return Math.round(n * POINTS_PER_MM * PX_PER_POINT);
|
|
212
|
-
if (value.endsWith("cm")) return Math.round(n * POINTS_PER_CM * PX_PER_POINT);
|
|
213
|
-
if (value.endsWith("in")) return Math.round(n * PIXELS_PER_INCH);
|
|
214
|
-
return n;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Convert a length value to **twips** (1/20 of a point).
|
|
218
|
-
*
|
|
219
|
-
* Used for spacing, indent, and margin values in Word OpenXML.
|
|
220
|
-
* Bare number is treated as **pt**.
|
|
221
|
-
*
|
|
222
|
-
* @param value - — Number (pt) or unit string (`"10pt"`, `"12px"`, `"5mm"`, etc.)
|
|
223
|
-
* @returns Twip integer, or `undefined` if input is null/undefined
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* ```ts
|
|
227
|
-
* toTwip(12) // 240 (12pt × 20)
|
|
228
|
-
* toTwip('1in') // 1440 (1 inch × 1440)
|
|
229
|
-
* toTwip('10mm') // 567 (10mm × 56.7)
|
|
230
|
-
* ```
|
|
231
|
-
*/
|
|
232
|
-
function toTwip(value) {
|
|
233
|
-
if (value == null) return;
|
|
234
|
-
if (typeof value === "number") return Math.round(value * TWIPS_PER_POINT);
|
|
235
|
-
const n = Number.parseFloat(value);
|
|
236
|
-
if (value.endsWith("pt")) return Math.round(n * TWIPS_PER_POINT);
|
|
237
|
-
if (value.endsWith("px")) return Math.round(n * TWIPS_PER_PX);
|
|
238
|
-
if (value.endsWith("mm")) return Math.round(n * POINTS_PER_MM * TWIPS_PER_POINT);
|
|
239
|
-
if (value.endsWith("cm")) return Math.round(n * POINTS_PER_CM * TWIPS_PER_POINT);
|
|
240
|
-
if (value.endsWith("in")) return Math.round(n * POINTS_PER_INCH * TWIPS_PER_POINT);
|
|
241
|
-
return Math.round(n * TWIPS_PER_POINT);
|
|
242
|
-
}
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region src/compiler/compileStyle.ts
|
|
245
|
-
/**
|
|
246
|
-
* Style compiler — maps `DocxStyleRule` (CSS-like) → `docx` constructor options.
|
|
247
|
-
*
|
|
248
|
-
* Each exported function converts one aspect of a style rule into the shape
|
|
249
|
-
* expected by the underlying `docx` library.
|
|
250
|
-
*
|
|
251
|
-
* @module compiler/compileStyle
|
|
252
|
-
*/
|
|
253
|
-
/**
|
|
254
|
-
* Compile a `DocxStyleRule` into `docx` table cell options
|
|
255
|
-
* (vertical alignment, margins, shading).
|
|
256
|
-
*
|
|
257
|
-
* @param style - — The resolved style rule
|
|
258
|
-
* @returns Options object suitable for `new TableCell(...)`
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```ts
|
|
262
|
-
* compileCellStyle({ verticalAlign: 'middle', backgroundColor: '#f0f0f0' })
|
|
263
|
-
* // => { verticalAlign: VerticalAlign.CENTER, shading: { fill: 'f0f0f0', ... } }
|
|
264
|
-
* ```
|
|
265
|
-
*/
|
|
266
|
-
function compileCellStyle(style) {
|
|
267
|
-
const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
|
|
268
|
-
return {
|
|
269
|
-
verticalAlign: compileVerticalAlign(style.verticalAlign),
|
|
270
|
-
margins: {
|
|
271
|
-
bottom: toTwip(style.marginBottom) ?? shorthand?.bottom,
|
|
272
|
-
left: toTwip(style.marginLeft) ?? shorthand?.left,
|
|
273
|
-
right: toTwip(style.marginRight) ?? shorthand?.right,
|
|
274
|
-
top: toTwip(style.marginTop) ?? shorthand?.top
|
|
275
|
-
},
|
|
276
|
-
shading: style.backgroundColor == null ? void 0 : {
|
|
277
|
-
fill: normalizeColor(style.backgroundColor),
|
|
278
|
-
type: ShadingType.CLEAR
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Compile a column width value.
|
|
284
|
-
*
|
|
285
|
-
* If the value is a percentage string (e.g. `"30%"`), returns
|
|
286
|
-
* a percentage width config. Otherwise returns `undefined`.
|
|
287
|
-
*
|
|
288
|
-
* @param width - — Column width value
|
|
289
|
-
* @returns Width config or `undefined`
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* ```ts
|
|
293
|
-
* compileColumnWidth('25%')
|
|
294
|
-
* // => { size: 25, type: WidthType.PERCENTAGE }
|
|
295
|
-
* ```
|
|
296
|
-
*/
|
|
297
|
-
function compileColumnWidth(width) {
|
|
298
|
-
if (typeof width === "string" && width.endsWith("%")) return {
|
|
299
|
-
size: Number.parseFloat(width),
|
|
300
|
-
type: WidthType.PERCENTAGE
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Compile a `DocxStyleRule` into `docx` paragraph-level options
|
|
305
|
-
* (alignment, indent, spacing).
|
|
306
|
-
*
|
|
307
|
-
* @param style - — The resolved style rule
|
|
308
|
-
* @returns Options object suitable for `new Paragraph(...)`
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* ```ts
|
|
312
|
-
* compileParagraphStyle({ textAlign: 'center', marginTop: '10pt' })
|
|
313
|
-
* // => { alignment: AlignmentType.CENTER, spacing: { before: 200, ... } }
|
|
314
|
-
* ```
|
|
315
|
-
*/
|
|
316
|
-
function compileParagraphStyle(style) {
|
|
317
|
-
const spacing = resolveSpacing(style);
|
|
318
|
-
const indent = resolveIndent(style);
|
|
319
|
-
return {
|
|
320
|
-
alignment: compileAlignment(style.textAlign),
|
|
321
|
-
indent,
|
|
322
|
-
spacing
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Compile a `DocxStyleRule` into `docx` run-level options
|
|
327
|
-
* (font, color, size, bold, italic, underline, etc.).
|
|
328
|
-
*
|
|
329
|
-
* Also merges any passthrough properties from `style.docx`.
|
|
330
|
-
*
|
|
331
|
-
* @param style - — The resolved style rule
|
|
332
|
-
* @returns Options object suitable for `new TextRun(...)`
|
|
333
|
-
*
|
|
334
|
-
* @example
|
|
335
|
-
* ```ts
|
|
336
|
-
* compileTextStyle({ fontSize: 14, fontWeight: 'bold', color: '#ff0000' })
|
|
337
|
-
* // => { size: 28, bold: true, color: 'ff0000' }
|
|
338
|
-
* ```
|
|
339
|
-
*/
|
|
340
|
-
function compileTextStyle(style) {
|
|
341
|
-
const result = {
|
|
342
|
-
allCaps: style.allCaps,
|
|
343
|
-
bold: style.fontWeight === "bold" || Number(style.fontWeight) >= 600,
|
|
344
|
-
color: normalizeColor(style.color),
|
|
345
|
-
font: style.fontFamily,
|
|
346
|
-
italics: style.fontStyle === "italic",
|
|
347
|
-
size: toPtHalf(style.fontSize),
|
|
348
|
-
strike: style.strike,
|
|
349
|
-
underline: style.underline ? {} : void 0
|
|
350
|
-
};
|
|
351
|
-
if (style.docx) Object.assign(result, style.docx);
|
|
352
|
-
return result;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Map the CSS-like text-align value to a `docx` `AlignmentType`.
|
|
356
|
-
*/
|
|
357
|
-
function compileAlignment(value) {
|
|
358
|
-
if (value === "center") return AlignmentType.CENTER;
|
|
359
|
-
if (value === "right") return AlignmentType.RIGHT;
|
|
360
|
-
if (value === "justify") return AlignmentType.JUSTIFIED;
|
|
361
|
-
return AlignmentType.LEFT;
|
|
362
|
-
}
|
|
363
|
-
/** Convert line-height multiplier to twips. 1 → 240 (single-spacing). */
|
|
364
|
-
function compileLineHeight(value) {
|
|
365
|
-
if (typeof value === "number") return Math.round(value * 240);
|
|
366
|
-
return toTwip(value);
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Map the CSS-like vertical-align to a `docx` `VerticalAlign` enum value.
|
|
370
|
-
*/
|
|
371
|
-
function compileVerticalAlign(value) {
|
|
372
|
-
if (value === "middle") return VerticalAlign.CENTER;
|
|
373
|
-
if (value === "bottom") return VerticalAlign.BOTTOM;
|
|
374
|
-
return VerticalAlign.TOP;
|
|
375
|
-
}
|
|
376
|
-
/** Strip leading `#` from a color string (docx expects raw hex). */
|
|
377
|
-
function normalizeColor(color) {
|
|
378
|
-
return color?.replace(/^#/, "");
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Resolve paragraph indent from a `DocxStyleRule`.
|
|
382
|
-
*
|
|
383
|
-
* Handles margin shorthand, explicit left/right margin, and text-indent.
|
|
384
|
-
*/
|
|
385
|
-
function resolveIndent(style) {
|
|
386
|
-
const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
|
|
387
|
-
const left = toTwip(style.marginLeft) ?? shorthand?.left;
|
|
388
|
-
const right = toTwip(style.marginRight) ?? shorthand?.right;
|
|
389
|
-
const firstLine = toTwip(style.textIndent);
|
|
390
|
-
if (left == null && right == null && firstLine == null) return;
|
|
391
|
-
return {
|
|
392
|
-
firstLine,
|
|
393
|
-
left,
|
|
394
|
-
right
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Resolve paragraph spacing from a `DocxStyleRule`.
|
|
399
|
-
*
|
|
400
|
-
* Handles margin shorthand for top/bottom spacing and line height.
|
|
401
|
-
*/
|
|
402
|
-
function resolveSpacing(style) {
|
|
403
|
-
const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
|
|
404
|
-
const before = toTwip(style.marginTop) ?? shorthand?.top;
|
|
405
|
-
const after = toTwip(style.marginBottom) ?? shorthand?.bottom;
|
|
406
|
-
const line = compileLineHeight(style.lineHeight);
|
|
407
|
-
if (before == null && after == null && line == null) return;
|
|
408
|
-
return {
|
|
409
|
-
after,
|
|
410
|
-
before,
|
|
411
|
-
line
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
//#endregion
|
|
415
|
-
//#region src/compiler/compileNode.ts
|
|
416
|
-
/**
|
|
417
|
-
* Node compiler — dispatches DSL nodes to `docx` objects.
|
|
418
|
-
*
|
|
419
|
-
* Each node type (heading, paragraph, image, table, pageBreak, plugin)
|
|
420
|
-
* is compiled into the corresponding `docx` constructor.
|
|
421
|
-
*
|
|
422
|
-
* @module compiler/compileNode
|
|
423
|
-
*/
|
|
424
|
-
/** Map heading level numbers to `docx` `HeadingLevel` enum values. */
|
|
425
|
-
const HEADING_MAP = {
|
|
426
|
-
1: HeadingLevel.HEADING_1,
|
|
427
|
-
2: HeadingLevel.HEADING_2,
|
|
428
|
-
3: HeadingLevel.HEADING_3,
|
|
429
|
-
4: HeadingLevel.HEADING_4,
|
|
430
|
-
5: HeadingLevel.HEADING_5,
|
|
431
|
-
6: HeadingLevel.HEADING_6
|
|
432
|
-
};
|
|
433
|
-
/**
|
|
434
|
-
* Compile a single DSL node into its `docx` representation.
|
|
435
|
-
*
|
|
436
|
-
* Dispatches to the appropriate sub-compiler based on `node.type`.
|
|
437
|
-
*
|
|
438
|
-
* @param ctx - — Compilation context with config, node, and plugins
|
|
439
|
-
* @returns A `docx` object (Paragraph, Table, etc.) or array of objects
|
|
440
|
-
*
|
|
441
|
-
* @example
|
|
442
|
-
* ```ts
|
|
443
|
-
* const para = await compileNode({
|
|
444
|
-
* config: { styles: { p: { fontSize: 12 } } },
|
|
445
|
-
* node: { type: 'paragraph', text: 'Hello', className: 'p' },
|
|
446
|
-
* plugins: new Map(),
|
|
447
|
-
* })
|
|
448
|
-
* ```
|
|
449
|
-
*/
|
|
450
|
-
async function compileNode(ctx) {
|
|
451
|
-
switch (ctx.node.type) {
|
|
452
|
-
case "heading": return compileHeading(ctx.node, ctx.config);
|
|
453
|
-
case "image": return compileImage(ctx.node, ctx.config);
|
|
454
|
-
case "pageBreak": return new Paragraph({ children: [new PageBreak()] });
|
|
455
|
-
case "paragraph": return compileParagraph(ctx.node, ctx.config);
|
|
456
|
-
case "table": return compileTable(ctx.node, ctx.config);
|
|
457
|
-
case "plugin": {
|
|
458
|
-
const plugin = ctx.plugins.get(ctx.node.name);
|
|
459
|
-
if (!plugin) throw new DocxKitError("PLUGIN_NOT_REGISTERED", `Plugin not registered: ${ctx.node.name}`);
|
|
460
|
-
try {
|
|
461
|
-
return await plugin.render(ctx.node.options, createPluginContext(ctx));
|
|
462
|
-
} catch (err) {
|
|
463
|
-
throw new DocxKitError("PLUGIN_RENDER_FAILED", `Plugin render failed: ${plugin.name}`, err);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
default: throw new DocxKitError("UNKNOWN_NODE_TYPE", `Unknown node type: ${ctx.node.type}`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Build floating layout options for `ImageRun`.
|
|
471
|
-
*/
|
|
472
|
-
function compileFloating(floating) {
|
|
473
|
-
if (!floating) return;
|
|
474
|
-
if (floating === true) return {};
|
|
475
|
-
return {
|
|
476
|
-
horizontalPosition: floating.x === void 0 ? void 0 : { offset: floating.x },
|
|
477
|
-
verticalPosition: floating.y === void 0 ? void 0 : { offset: floating.y }
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Compile a heading node into a `docx` Paragraph with heading level.
|
|
482
|
-
*
|
|
483
|
-
* Default class name is `h{level}` (e.g. `h1`, `h2`).
|
|
484
|
-
*/
|
|
485
|
-
function compileHeading(node, config) {
|
|
486
|
-
const style = resolveStyle({
|
|
487
|
-
className: node.className ?? `h${node.level}`,
|
|
488
|
-
inline: node.style,
|
|
489
|
-
styles: config.styles
|
|
490
|
-
});
|
|
491
|
-
return new Paragraph({
|
|
492
|
-
...compileParagraphStyle(style),
|
|
493
|
-
heading: HEADING_MAP[node.level],
|
|
494
|
-
children: [new TextRun({
|
|
495
|
-
text: node.text,
|
|
496
|
-
...compileTextStyle(style)
|
|
497
|
-
})]
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Compile an image node into a `docx` Paragraph containing an `ImageRun`.
|
|
502
|
-
*
|
|
503
|
-
* Handles image data normalization (Blob → Uint8Array), auto format detection,
|
|
504
|
-
* floating layout, and size transformations.
|
|
505
|
-
*/
|
|
506
|
-
async function compileImage(node, _config) {
|
|
507
|
-
if (node.data == null || typeof node.data === "string" && node.data.length === 0) throw new DocxKitError("IMAGE_INVALID_DATA", "Image data is empty or null");
|
|
508
|
-
const data = await normalizeImageData(node.data);
|
|
509
|
-
const imageType = node.imageType ?? "png";
|
|
510
|
-
return new Paragraph({ children: [createImageRun({
|
|
511
|
-
data,
|
|
512
|
-
floating: compileFloating(node.floating),
|
|
513
|
-
height: toPx(node.height),
|
|
514
|
-
type: imageType,
|
|
515
|
-
width: toPx(node.width)
|
|
516
|
-
})] });
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Compile a paragraph node into a `docx` Paragraph.
|
|
520
|
-
*
|
|
521
|
-
* Supports plain text (`node.text`) or inline children (`node.children`).
|
|
522
|
-
*/
|
|
523
|
-
function compileParagraph(node, config) {
|
|
524
|
-
const style = resolveStyle({
|
|
525
|
-
base: config.defaults?.paragraph,
|
|
526
|
-
className: node.className ?? "p",
|
|
527
|
-
inline: node.style,
|
|
528
|
-
styles: config.styles
|
|
529
|
-
});
|
|
530
|
-
const children = node.children && node.children.length > 0 ? node.children.map((child) => child.type === "text" ? new TextRun({
|
|
531
|
-
text: child.text,
|
|
532
|
-
...compileTextStyle(resolveStyle({
|
|
533
|
-
base: style,
|
|
534
|
-
className: child.className,
|
|
535
|
-
inline: child.style,
|
|
536
|
-
styles: config.styles
|
|
537
|
-
}))
|
|
538
|
-
}) : (() => {
|
|
539
|
-
throw new DocxKitError("UNKNOWN_NODE_TYPE", `Inline image is not supported yet. Use top-level \`.image()\` instead.`);
|
|
540
|
-
})()) : [new TextRun({
|
|
541
|
-
text: node.text ?? "",
|
|
542
|
-
...compileTextStyle(style)
|
|
543
|
-
})];
|
|
544
|
-
return new Paragraph({
|
|
545
|
-
...compileParagraphStyle(style),
|
|
546
|
-
children
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Compile a table node into a `docx` Table.
|
|
551
|
-
*
|
|
552
|
-
* Generates header row (unless `header: false`) and data rows.
|
|
553
|
-
* Supports custom cell renderers via `column.render`.
|
|
554
|
-
*/
|
|
555
|
-
function compileTable(node, _config) {
|
|
556
|
-
if (node.columns.length === 0) throw new DocxKitError("TABLE_INVALID_COLUMNS", "Table must have at least one column");
|
|
557
|
-
const rows = [];
|
|
558
|
-
if (node.header !== false) rows.push(new TableRow({
|
|
559
|
-
tableHeader: true,
|
|
560
|
-
children: node.columns.map((col) => new TableCell({
|
|
561
|
-
...compileCellStyle(node.headerCellStyle ?? {}),
|
|
562
|
-
children: [new Paragraph(String(col.title))],
|
|
563
|
-
width: compileColumnWidth(col.width)
|
|
564
|
-
}))
|
|
565
|
-
}));
|
|
566
|
-
rows.push(...node.data.map((row, rowIndex) => new TableRow({ children: node.columns.map((col) => {
|
|
567
|
-
const raw = row[col.key];
|
|
568
|
-
const rendered = col.render ? col.render(raw, row, rowIndex) : String(raw ?? "");
|
|
569
|
-
return new TableCell({
|
|
570
|
-
...compileCellStyle(node.cellStyle ?? {}),
|
|
571
|
-
children: [new Paragraph(String(rendered))],
|
|
572
|
-
width: compileColumnWidth(col.width)
|
|
573
|
-
});
|
|
574
|
-
}) })));
|
|
575
|
-
return new Table({
|
|
576
|
-
rows,
|
|
577
|
-
width: {
|
|
578
|
-
size: 100,
|
|
579
|
-
type: WidthType.PERCENTAGE
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Build a `PluginRenderContext` for plugin rendering.
|
|
585
|
-
*
|
|
586
|
-
* Provides access to config, image utilities, and the ability to
|
|
587
|
-
* recursively compile child nodes.
|
|
588
|
-
*/
|
|
589
|
-
function createPluginContext(ctx) {
|
|
590
|
-
return {
|
|
591
|
-
config: ctx.config,
|
|
592
|
-
utils: { image: {
|
|
593
|
-
fromDataUrl: (dataUrl) => dataUrlToUint8Array(dataUrl),
|
|
594
|
-
fromBlob: async (blob) => new Uint8Array(await blob.arrayBuffer())
|
|
595
|
-
} },
|
|
596
|
-
compileNode: (node) => compileNode({
|
|
597
|
-
config: ctx.config,
|
|
598
|
-
node,
|
|
599
|
-
plugins: ctx.plugins
|
|
600
|
-
})
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Normalize image data to a form `ImageRun` accepts.
|
|
605
|
-
*
|
|
606
|
-
* Converts `Blob` → `Uint8Array` in browser environments.
|
|
607
|
-
*/
|
|
608
|
-
async function normalizeImageData(data) {
|
|
609
|
-
if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
610
|
-
return data;
|
|
611
|
-
}
|
|
612
|
-
//#endregion
|
|
613
|
-
//#region src/compiler/compileDocument.ts
|
|
614
|
-
/**
|
|
615
|
-
* Document compiler — assembles a `docx` `Document` from an array of DSL nodes.
|
|
616
|
-
*
|
|
617
|
-
* This is the top-level compilation entry point. It iterates over all block
|
|
618
|
-
* nodes, compiles each one via {@link compileNode}, and wraps them in a
|
|
619
|
-
* single-section `Document` with page properties and metadata.
|
|
620
|
-
*
|
|
621
|
-
* @module compiler/compileDocument
|
|
622
|
-
*/
|
|
623
|
-
/**
|
|
624
|
-
* Compile an array of block nodes into a `docx` {@link Document} instance.
|
|
625
|
-
*
|
|
626
|
-
* This is the primary compilation pipeline — nodes flow through
|
|
627
|
-
* `compileNode` → individual sub-compilers → `docx` objects → `Document`.
|
|
628
|
-
*
|
|
629
|
-
* @param options - — Compilation options with config, nodes, and plugins
|
|
630
|
-
* @returns A `docx` `Document` ready for packaging
|
|
631
|
-
*
|
|
632
|
-
* @example
|
|
633
|
-
* ```ts
|
|
634
|
-
* const doc = await compileDocument({
|
|
635
|
-
* config: { page: { size: 'A4' } },
|
|
636
|
-
* nodes: [
|
|
637
|
-
* { type: 'heading', level: 1, text: 'Title' },
|
|
638
|
-
* { type: 'paragraph', text: 'Hello world' },
|
|
639
|
-
* ],
|
|
640
|
-
* plugins: new Map(),
|
|
641
|
-
* })
|
|
642
|
-
* ```
|
|
643
|
-
*/
|
|
644
|
-
async function compileDocument(options) {
|
|
645
|
-
const children = [];
|
|
646
|
-
for (const node of options.nodes) {
|
|
647
|
-
const compiled = await compileNode({
|
|
648
|
-
config: options.config,
|
|
649
|
-
node,
|
|
650
|
-
plugins: options.plugins
|
|
651
|
-
});
|
|
652
|
-
if (Array.isArray(compiled)) children.push(...compiled);
|
|
653
|
-
else children.push(compiled);
|
|
654
|
-
}
|
|
655
|
-
return new Document({
|
|
656
|
-
creator: options.config.metadata?.creator,
|
|
657
|
-
description: options.config.metadata?.description,
|
|
658
|
-
keywords: options.config.metadata?.keywords?.join(", "),
|
|
659
|
-
lastModifiedBy: options.config.metadata?.lastModifiedBy,
|
|
660
|
-
subject: options.config.metadata?.subject,
|
|
661
|
-
title: options.config.metadata?.title,
|
|
662
|
-
sections: [{
|
|
663
|
-
...compileSectionProperties(options.config),
|
|
664
|
-
children
|
|
665
|
-
}]
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Compile a single page margin value (shorthand or explicit) into twips.
|
|
670
|
-
*/
|
|
671
|
-
function compilePageMargin(margin) {
|
|
672
|
-
if (!margin) return;
|
|
673
|
-
const parsed = parseShorthandTwip(margin);
|
|
674
|
-
if (!parsed) return;
|
|
675
|
-
return {
|
|
676
|
-
bottom: parsed.bottom,
|
|
677
|
-
left: parsed.left,
|
|
678
|
-
right: parsed.right,
|
|
679
|
-
top: parsed.top
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Build the section properties (page size, margin) for the `Document`.
|
|
684
|
-
*/
|
|
685
|
-
function compileSectionProperties(config) {
|
|
686
|
-
return { properties: { page: {
|
|
687
|
-
margin: compilePageMargin(config.page?.margin),
|
|
688
|
-
size: compilePageSize(config.page?.size, config.page?.orientation)
|
|
689
|
-
} } };
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Page size presets in twips (width × height).
|
|
693
|
-
*
|
|
694
|
-
* Values sourced from the OOXML specification.
|
|
695
|
-
*/
|
|
696
|
-
const PAGE_SIZE_PRESETS = {
|
|
697
|
-
/** A3: 297 × 420 mm */
|
|
698
|
-
A3: {
|
|
699
|
-
height: 23811,
|
|
700
|
-
width: 16838
|
|
701
|
-
},
|
|
702
|
-
/** A4: 210 × 297 mm */
|
|
703
|
-
A4: {
|
|
704
|
-
height: 16838,
|
|
705
|
-
width: 11906
|
|
706
|
-
},
|
|
707
|
-
/** US Legal: 8.5 × 14 in */
|
|
708
|
-
Legal: {
|
|
709
|
-
height: 20160,
|
|
710
|
-
width: 12240
|
|
711
|
-
},
|
|
712
|
-
/** US Letter: 8.5 × 11 in */
|
|
713
|
-
Letter: {
|
|
714
|
-
height: 15840,
|
|
715
|
-
width: 12240
|
|
716
|
-
}
|
|
717
|
-
};
|
|
718
|
-
/**
|
|
719
|
-
* Compile page size into Word twips.
|
|
720
|
-
*
|
|
721
|
-
* Resolves presets (`"A4"`, `"Letter"`, etc.) or custom dimensions.
|
|
722
|
-
* Swaps width/height when orientation is `"landscape"`.
|
|
723
|
-
*/
|
|
724
|
-
function compilePageSize(size, orientation) {
|
|
725
|
-
if (!size) return;
|
|
726
|
-
let width;
|
|
727
|
-
let height;
|
|
728
|
-
if (typeof size === "string") {
|
|
729
|
-
const preset = PAGE_SIZE_PRESETS[size];
|
|
730
|
-
if (preset) {
|
|
731
|
-
width = preset.width;
|
|
732
|
-
height = preset.height;
|
|
733
|
-
}
|
|
734
|
-
} else if (typeof size === "object" && size !== null) {
|
|
735
|
-
const s = size;
|
|
736
|
-
width = toTwip(s.width);
|
|
737
|
-
height = toTwip(s.height);
|
|
738
|
-
}
|
|
739
|
-
if (width == null || height == null) return;
|
|
740
|
-
if (orientation === "landscape") return {
|
|
741
|
-
height: width,
|
|
742
|
-
width: height
|
|
743
|
-
};
|
|
744
|
-
return {
|
|
745
|
-
height,
|
|
746
|
-
width
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
//#endregion
|
|
750
|
-
export { compileDocument };
|