docx-kit 0.2.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.
@@ -1,1051 +0,0 @@
1
- import { n as dataUrlToUint8Array, t as createImageRun } from "./image-CdfLh9GO.mjs";
2
- import { t as DocxKitError } from "./errors-c9CC63Ti.mjs";
3
- import { AlignmentType, BorderStyle, Document, ExternalHyperlink, Footer, Header, HeadingLevel, HighlightColor, LevelFormat, 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 all border properties from a `DocxStyleRule`.
255
- *
256
- * Returns `undefined` if no borders are specified.
257
- *
258
- * @param style - — The resolved style rule
259
- * @returns Border options for `docx` or `undefined`
260
- *
261
- * @example
262
- * ```ts
263
- * compileBorder({ border: { style: 'single', color: '#333', width: '1pt' } })
264
- * // => { top: {...}, right: {...}, bottom: {...}, left: {...} }
265
- * ```
266
- */
267
- function compileBorder(style) {
268
- const b = style.border;
269
- const bt = style.borderTop ?? b;
270
- const br = style.borderRight ?? b;
271
- const bb = style.borderBottom ?? b;
272
- const bl = style.borderLeft ?? b;
273
- if (!bt && !br && !bb && !bl) return;
274
- return {
275
- bottom: bb ? compileSingleBorder(bb) : void 0,
276
- left: bl ? compileSingleBorder(bl) : void 0,
277
- right: br ? compileSingleBorder(br) : void 0,
278
- top: bt ? compileSingleBorder(bt) : void 0
279
- };
280
- }
281
- /**
282
- * Compile a `DocxStyleRule` into `docx` table cell options
283
- * (vertical alignment, margins, shading).
284
- *
285
- * Applies sensible default cell padding (5 pt left/right, 2 pt top/bottom)
286
- * when no margins are explicitly set.
287
- *
288
- * @param style - — The resolved style rule
289
- * @returns Options object suitable for `new TableCell(...)`
290
- *
291
- * @example
292
- * ```ts
293
- * compileCellStyle({ verticalAlign: 'middle', backgroundColor: '#f0f0f0' })
294
- * // => { verticalAlign: VerticalAlign.CENTER, shading: { fill: 'f0f0f0', ... } }
295
- * ```
296
- */
297
- function compileCellStyle(style) {
298
- const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
299
- const bottom = toTwip(style.marginBottom) ?? shorthand?.bottom;
300
- const left = toTwip(style.marginLeft) ?? shorthand?.left;
301
- const right = toTwip(style.marginRight) ?? shorthand?.right;
302
- const top = toTwip(style.marginTop) ?? shorthand?.top;
303
- const hasAnyMargin = bottom != null || left != null || right != null || top != null;
304
- return {
305
- verticalAlign: compileVerticalAlign(style.verticalAlign),
306
- margins: hasAnyMargin ? {
307
- bottom: bottom ?? 0,
308
- left: left ?? 0,
309
- right: right ?? 0,
310
- top: top ?? 0
311
- } : {
312
- bottom: 0,
313
- left: 100,
314
- right: 100,
315
- top: 0
316
- },
317
- shading: style.backgroundColor == null ? void 0 : {
318
- fill: normalizeColor(style.backgroundColor),
319
- type: ShadingType.CLEAR
320
- }
321
- };
322
- }
323
- /**
324
- * Compile a column width value.
325
- *
326
- * If the value is a percentage string (e.g. `"30%"`), returns
327
- * a percentage width config. Otherwise returns `undefined`.
328
- *
329
- * @param width - — Column width value
330
- * @returns Width config or `undefined`
331
- *
332
- * @example
333
- * ```ts
334
- * compileColumnWidth('25%')
335
- * // => { size: 25, type: WidthType.PERCENTAGE }
336
- * ```
337
- */
338
- function compileColumnWidth(width) {
339
- if (typeof width === "string" && width.endsWith("%")) return {
340
- size: Number.parseFloat(width),
341
- type: WidthType.PERCENTAGE
342
- };
343
- }
344
- /** Compile paragraph-level borders (separate from cell borders). */
345
- function compileParagraphBorder(style) {
346
- const borders = compileBorder(style);
347
- if (!borders) return {};
348
- return { border: borders };
349
- }
350
- /**
351
- * Compile a `DocxStyleRule` into `docx` paragraph-level options
352
- * (alignment, indent, spacing).
353
- *
354
- * @param style - — The resolved style rule
355
- * @returns Options object suitable for `new Paragraph(...)`
356
- *
357
- * @example
358
- * ```ts
359
- * compileParagraphStyle({ textAlign: 'center', marginTop: '10pt' })
360
- * // => { alignment: AlignmentType.CENTER, spacing: { before: 200, ... } }
361
- * ```
362
- */
363
- function compileParagraphStyle(style) {
364
- const spacing = resolveSpacing(style);
365
- const indent = resolveIndent(style);
366
- return {
367
- alignment: compileAlignment(style.textAlign),
368
- ...compileParagraphBorder(style),
369
- indent,
370
- keepLines: style.keepLines,
371
- keepNext: style.keepNext,
372
- pageBreakBefore: style.pageBreakBefore,
373
- spacing
374
- };
375
- }
376
- /**
377
- * Compile a `DocxStyleRule` into `docx` run-level options
378
- * (font, color, size, bold, italic, underline, etc.).
379
- *
380
- * Also merges any passthrough properties from `style.docx`.
381
- *
382
- * @param style - — The resolved style rule
383
- * @returns Options object suitable for `new TextRun(...)`
384
- *
385
- * @example
386
- * ```ts
387
- * compileTextStyle({ fontSize: 14, fontWeight: 'bold', color: '#ff0000' })
388
- * // => { size: 28, bold: true, color: 'ff0000' }
389
- * ```
390
- */
391
- function compileTextStyle(style) {
392
- const result = {
393
- allCaps: style.allCaps,
394
- bold: style.fontWeight === "bold" || Number(style.fontWeight) >= 600,
395
- characterSpacing: style.characterSpacing,
396
- color: normalizeColor(style.color),
397
- font: style.fontFamily,
398
- highlight: compileHighlight(style.highlight),
399
- italics: style.fontStyle === "italic",
400
- size: toPtHalf(style.fontSize),
401
- smallCaps: style.smallCaps,
402
- strike: style.strike,
403
- subScript: style.subScript,
404
- superScript: style.superScript,
405
- underline: style.underline ? {} : void 0
406
- };
407
- if (style.docx) Object.assign(result, style.docx);
408
- return result;
409
- }
410
- /**
411
- * Map the CSS-like text-align value to a `docx` `AlignmentType`.
412
- */
413
- function compileAlignment(value) {
414
- if (value === "center") return AlignmentType.CENTER;
415
- if (value === "right") return AlignmentType.RIGHT;
416
- if (value === "justify") return AlignmentType.JUSTIFIED;
417
- return AlignmentType.LEFT;
418
- }
419
- /**
420
- * Map a CSS-like border-style string to a `docx` `BorderStyle` enum value.
421
- */
422
- function compileBorderStyle(style) {
423
- if (style === "dashed") return BorderStyle.DASHED;
424
- if (style === "dotted") return BorderStyle.DOTTED;
425
- if (style === "double") return BorderStyle.DOUBLE;
426
- if (style === "none") return BorderStyle.NONE;
427
- return BorderStyle.SINGLE;
428
- }
429
- /**
430
- * Map a highlight color name to `docx` `HighlightColor`.
431
- */
432
- function compileHighlight(color) {
433
- if (!color || color === "none") return;
434
- return HighlightColor[color.toUpperCase()];
435
- }
436
- /** Convert line-height multiplier to twips. 1 → 240 (single-spacing). */
437
- function compileLineHeight(value) {
438
- if (typeof value === "number") return Math.round(value * 240);
439
- return toTwip(value);
440
- }
441
- /**
442
- * Compile a single border side into `docx` border options.
443
- */
444
- function compileSingleBorder(rule) {
445
- return {
446
- color: normalizeColor(rule.color),
447
- size: rule.width == null ? 1 : toTwip(rule.width),
448
- style: compileBorderStyle(rule.style)
449
- };
450
- }
451
- /**
452
- * Map the CSS-like vertical-align to a `docx` `VerticalAlign` enum value.
453
- */
454
- function compileVerticalAlign(value) {
455
- if (value === "middle") return VerticalAlign.CENTER;
456
- if (value === "bottom") return VerticalAlign.BOTTOM;
457
- return VerticalAlign.TOP;
458
- }
459
- /** Strip leading `#` from a color string (docx expects raw hex). */
460
- function normalizeColor(color) {
461
- return color?.replace(/^#/, "");
462
- }
463
- /**
464
- * Resolve paragraph indent from a `DocxStyleRule`.
465
- *
466
- * Handles margin shorthand, explicit left/right margin, and text-indent.
467
- */
468
- function resolveIndent(style) {
469
- const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
470
- const left = toTwip(style.marginLeft) ?? shorthand?.left;
471
- const right = toTwip(style.marginRight) ?? shorthand?.right;
472
- const firstLine = toTwip(style.textIndent);
473
- if (left == null && right == null && firstLine == null) return;
474
- return {
475
- firstLine,
476
- left,
477
- right
478
- };
479
- }
480
- /**
481
- * Resolve paragraph spacing from a `DocxStyleRule`.
482
- *
483
- * Handles margin shorthand for top/bottom spacing and line height.
484
- */
485
- function resolveSpacing(style) {
486
- const shorthand = style.margin == null ? void 0 : parseShorthandTwip(style.margin);
487
- const before = toTwip(style.marginTop) ?? shorthand?.top;
488
- const after = toTwip(style.marginBottom) ?? shorthand?.bottom;
489
- const line = compileLineHeight(style.lineHeight);
490
- if (before == null && after == null && line == null) return;
491
- return {
492
- after,
493
- before,
494
- line
495
- };
496
- }
497
- //#endregion
498
- //#region src/compiler/compileNode.ts
499
- /**
500
- * Node compiler — dispatches DSL nodes to `docx` objects.
501
- *
502
- * Each node type (heading, paragraph, image, table, pageBreak, plugin)
503
- * is compiled into the corresponding `docx` constructor.
504
- *
505
- * @module compiler/compileNode
506
- */
507
- /** Map heading level numbers to `docx` `HeadingLevel` enum values. */
508
- const HEADING_MAP = {
509
- 1: HeadingLevel.HEADING_1,
510
- 2: HeadingLevel.HEADING_2,
511
- 3: HeadingLevel.HEADING_3,
512
- 4: HeadingLevel.HEADING_4,
513
- 5: HeadingLevel.HEADING_5,
514
- 6: HeadingLevel.HEADING_6
515
- };
516
- /**
517
- * Compile a single DSL node into its `docx` representation.
518
- *
519
- * Dispatches to the appropriate sub-compiler based on `node.type`.
520
- *
521
- * @param ctx - — Compilation context with config, node, and plugins
522
- * @returns A `docx` object (Paragraph, Table, etc.) or array of objects
523
- *
524
- * @example
525
- * ```ts
526
- * const para = await compileNode({
527
- * config: { styles: { p: { fontSize: 12 } } },
528
- * node: { type: 'paragraph', text: 'Hello', className: 'p' },
529
- * plugins: new Map(),
530
- * })
531
- * ```
532
- */
533
- async function compileNode(ctx) {
534
- switch (ctx.node.type) {
535
- case "bulletList": return compileBulletList(ctx.node, ctx.config);
536
- case "heading": return compileHeading(ctx.node, ctx.config);
537
- case "hyperlink": return compileHyperlink(ctx.node, ctx.config);
538
- case "image": return compileImage(ctx.node, ctx.config);
539
- case "numberedList": return compileNumberedList(ctx.node, ctx.config);
540
- case "pageBreak": return new Paragraph({ children: [new PageBreak()] });
541
- case "sectionBreak": throw new DocxKitError("UNKNOWN_NODE_TYPE", "Section break nodes must be handled at the document compilation level");
542
- case "paragraph": return compileParagraph(ctx.node, ctx.config);
543
- case "table": return compileTable(ctx.node, ctx.config);
544
- case "plugin": {
545
- const plugin = ctx.plugins.get(ctx.node.name);
546
- if (!plugin) throw new DocxKitError("PLUGIN_NOT_REGISTERED", `Plugin not registered: ${ctx.node.name}`);
547
- try {
548
- return await plugin.render(ctx.node.options, createPluginContext(ctx));
549
- } catch (err) {
550
- throw new DocxKitError("PLUGIN_RENDER_FAILED", `Plugin render failed: ${plugin.name}`, err);
551
- }
552
- }
553
- default: throw new DocxKitError("UNKNOWN_NODE_TYPE", `Unknown node type: ${ctx.node.type}`);
554
- }
555
- }
556
- /**
557
- * Build floating layout options for `ImageRun`.
558
- */
559
- function compileFloating(floating) {
560
- if (!floating) return;
561
- if (floating === true) return {};
562
- return {
563
- horizontalPosition: floating.x === void 0 ? void 0 : { offset: floating.x },
564
- verticalPosition: floating.y === void 0 ? void 0 : { offset: floating.y }
565
- };
566
- }
567
- /**
568
- * Compile a heading node into a `docx` Paragraph with heading level.
569
- *
570
- * Default class name is `h{level}` (e.g. `h1`, `h2`).
571
- */
572
- function compileHeading(node, config) {
573
- const style = resolveStyle({
574
- className: node.className ?? `h${node.level}`,
575
- inline: node.style,
576
- styles: config.styles
577
- });
578
- return new Paragraph({
579
- ...compileParagraphStyle(style),
580
- heading: HEADING_MAP[node.level],
581
- children: [new TextRun({
582
- text: node.text,
583
- ...compileTextStyle(style)
584
- })]
585
- });
586
- }
587
- /**
588
- * Compile a hyperlink node into an `ExternalHyperlink` containing `TextRun`s.
589
- */
590
- function compileHyperlink(node, config) {
591
- const style = resolveStyle({
592
- className: node.className,
593
- inline: node.style,
594
- styles: config.styles
595
- });
596
- return new Paragraph({ children: [new ExternalHyperlink({
597
- children: node.children.map((child) => {
598
- if (typeof child === "string") return new TextRun({
599
- text: child,
600
- ...compileTextStyle(style)
601
- });
602
- return new TextRun({
603
- text: child.text,
604
- ...compileTextStyle(resolveStyle({
605
- base: style,
606
- className: child.className,
607
- inline: child.style,
608
- styles: config.styles
609
- }))
610
- });
611
- }),
612
- link: node.url
613
- })] });
614
- }
615
- /**
616
- * Compile an image node into a `docx` Paragraph containing an `ImageRun`.
617
- *
618
- * Handles image data normalization (Blob → Uint8Array), auto format detection,
619
- * floating layout, and size transformations.
620
- */
621
- async function compileImage(node, config) {
622
- if (node.data == null || typeof node.data === "string" && node.data.length === 0) throw new DocxKitError("IMAGE_INVALID_DATA", "Image data is empty or null");
623
- const data = await normalizeImageData(node.data);
624
- const imageType = node.imageType ?? "png";
625
- return new Paragraph({
626
- ...compileParagraphStyle(resolveStyle({
627
- base: config.defaults?.image,
628
- className: node.className,
629
- inline: node.style,
630
- styles: config.styles
631
- })),
632
- children: [createImageRun({
633
- data,
634
- floating: compileFloating(node.floating),
635
- height: toPx(node.height),
636
- type: imageType,
637
- width: toPx(node.width)
638
- })]
639
- });
640
- }
641
- /**
642
- * Compile a paragraph node into a `docx` Paragraph.
643
- *
644
- * Supports plain text (`node.text`) or inline children (`node.children`).
645
- */
646
- function compileParagraph(node, config) {
647
- const style = resolveStyle({
648
- base: config.defaults?.paragraph,
649
- className: node.className ?? "p",
650
- inline: node.style,
651
- styles: config.styles
652
- });
653
- const children = node.children && node.children.length > 0 ? node.children.map((child) => child.type === "text" ? new TextRun({
654
- text: child.text,
655
- ...compileTextStyle(resolveStyle({
656
- base: style,
657
- className: child.className,
658
- inline: child.style,
659
- styles: config.styles
660
- }))
661
- }) : (() => {
662
- throw new DocxKitError("UNKNOWN_NODE_TYPE", `Inline image is not supported yet. Use top-level \`.image()\` instead.`);
663
- })()) : [new TextRun({
664
- text: node.text ?? "",
665
- ...compileTextStyle(style)
666
- })];
667
- return new Paragraph({
668
- ...compileParagraphStyle(style),
669
- children
670
- });
671
- }
672
- const numberingConfigMap = /* @__PURE__ */ new Map();
673
- let numberingCounter = 0;
674
- /** Reset numbering state between compilations. */
675
- function resetNumberingState() {
676
- numberingConfigMap.clear();
677
- numberingCounter = 0;
678
- }
679
- /**
680
- * Generate a numbering config entry for a bullet list
681
- * and return Paragraphs with numbering references.
682
- */
683
- function compileBulletList(node, config) {
684
- const ref = `bullet-${++numberingCounter}`;
685
- const bullet = node.bullet ?? "•";
686
- numberingConfigMap.set(ref, {
687
- reference: ref,
688
- levels: [{
689
- alignment: AlignmentType.LEFT,
690
- format: LevelFormat.BULLET,
691
- level: node.level ?? 0,
692
- text: bullet
693
- }]
694
- });
695
- const style = resolveStyle({
696
- className: node.className,
697
- inline: node.style,
698
- styles: config.styles
699
- });
700
- return node.items.map((item) => {
701
- const text = typeof item === "string" ? item : item.text;
702
- const itemStyle = typeof item === "object" ? resolveStyle({
703
- base: style,
704
- className: item.className,
705
- inline: item.style,
706
- styles: config.styles
707
- }) : style;
708
- return new Paragraph({
709
- ...compileParagraphStyle(itemStyle),
710
- numbering: {
711
- level: node.level ?? 0,
712
- reference: ref
713
- },
714
- children: [new TextRun({
715
- text,
716
- ...compileTextStyle(itemStyle)
717
- })]
718
- });
719
- });
720
- }
721
- /**
722
- * Generate a numbering config entry for an ordered list
723
- * and return Paragraphs with numbering references.
724
- */
725
- function compileNumberedList(node, config) {
726
- const ref = `numbered-${++numberingCounter}`;
727
- const formatMap = {
728
- decimal: LevelFormat.DECIMAL,
729
- lowerLetter: LevelFormat.LOWER_LETTER,
730
- lowerRoman: LevelFormat.LOWER_ROMAN,
731
- upperLetter: LevelFormat.UPPER_LETTER,
732
- upperRoman: LevelFormat.UPPER_ROMAN
733
- };
734
- numberingConfigMap.set(ref, {
735
- reference: ref,
736
- levels: [{
737
- alignment: AlignmentType.LEFT,
738
- level: node.level ?? 0,
739
- start: node.start ?? 1,
740
- text: "%1.",
741
- format: formatMap[node.numberingFormat ?? "decimal"] ?? LevelFormat.DECIMAL
742
- }]
743
- });
744
- const style = resolveStyle({
745
- className: node.className,
746
- inline: node.style,
747
- styles: config.styles
748
- });
749
- return node.items.map((item) => {
750
- const text = typeof item === "string" ? item : item.text;
751
- const itemStyle = typeof item === "object" ? resolveStyle({
752
- base: style,
753
- className: item.className,
754
- inline: item.style,
755
- styles: config.styles
756
- }) : style;
757
- return new Paragraph({
758
- ...compileParagraphStyle(itemStyle),
759
- numbering: {
760
- level: node.level ?? 0,
761
- reference: ref
762
- },
763
- children: [new TextRun({
764
- text,
765
- ...compileTextStyle(itemStyle)
766
- })]
767
- });
768
- });
769
- }
770
- /**
771
- * Compile a table node into a `docx` Table.
772
- *
773
- * Generates header row (unless `header: false`) and data rows.
774
- * Supports custom cell renderers via `column.render`.
775
- */
776
- function compileTable(node, _config) {
777
- if (node.columns.length === 0) throw new DocxKitError("TABLE_INVALID_COLUMNS", "Table must have at least one column");
778
- const rows = [];
779
- if (node.header !== false) rows.push(new TableRow({
780
- tableHeader: true,
781
- children: node.columns.map((col) => new TableCell({
782
- ...compileCellStyle(node.headerCellStyle ?? {}),
783
- children: [new Paragraph(String(col.title))],
784
- ...col.colSpan && col.colSpan > 1 ? { columnSpan: col.colSpan } : {},
785
- width: compileColumnWidth(col.width)
786
- }))
787
- }));
788
- rows.push(...node.data.map((row, rowIndex) => new TableRow({ children: node.columns.map((col) => {
789
- const raw = row[col.key];
790
- const rendered = col.render ? col.render(raw, row, rowIndex) : String(raw ?? "");
791
- const cellColSpan = row[`_${col.key}_colSpan`] ?? col.colSpan;
792
- const isEvenRow = rowIndex % 2 === 1;
793
- const baseCellStyle = node.cellStyle ? { ...compileCellStyle(node.cellStyle) } : {};
794
- return new TableCell({
795
- ...baseCellStyle,
796
- ...node.striped && isEvenRow && baseCellStyle.shading ? { shading: {
797
- ...baseCellStyle.shading,
798
- fill: "F2F2F2"
799
- } } : {},
800
- children: [new Paragraph(String(rendered))],
801
- ...cellColSpan && cellColSpan > 1 ? { columnSpan: cellColSpan } : {},
802
- width: compileColumnWidth(col.width)
803
- });
804
- }) })));
805
- return new Table({
806
- rows,
807
- width: {
808
- size: 100,
809
- type: WidthType.PERCENTAGE
810
- }
811
- });
812
- }
813
- /**
814
- * Build a `PluginRenderContext` for plugin rendering.
815
- *
816
- * Provides access to config, image utilities, and the ability to
817
- * recursively compile child nodes.
818
- */
819
- function createPluginContext(ctx) {
820
- return {
821
- config: ctx.config,
822
- utils: { image: {
823
- fromDataUrl: (dataUrl) => dataUrlToUint8Array(dataUrl),
824
- fromBlob: async (blob) => new Uint8Array(await blob.arrayBuffer())
825
- } },
826
- compileNode: (node) => compileNode({
827
- config: ctx.config,
828
- node,
829
- plugins: ctx.plugins
830
- })
831
- };
832
- }
833
- /**
834
- * Normalize image data to a form `ImageRun` accepts.
835
- *
836
- * Converts `Blob` → `Uint8Array` in browser environments.
837
- */
838
- async function normalizeImageData(data) {
839
- if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
840
- return data;
841
- }
842
- //#endregion
843
- //#region src/compiler/compileDocument.ts
844
- /**
845
- * Document compiler — assembles a `docx` `Document` from an array of DSL nodes.
846
- *
847
- * This is the top-level compilation entry point. It iterates over all block
848
- * nodes, compiles each one via {@link compileNode}, and wraps them in a
849
- * `Document` with page properties and metadata.
850
- *
851
- * Multi-section support: {@link SectionBreakNode} markers split content
852
- * into separate sections, each with its own page setup, headers, and footers.
853
- *
854
- * @module compiler/compileDocument
855
- */
856
- /**
857
- * Compile an array of block nodes into a `docx` {@link Document} instance.
858
- *
859
- * This is the primary compilation pipeline — nodes flow through
860
- * `compileNode` → individual sub-compilers → `docx` objects → `Document`.
861
- *
862
- * If `SectionBreakNode` markers are present, content is split into
863
- * multiple sections, each with its own page setup, headers, and footers.
864
- *
865
- * @param options - — Compilation options with config, nodes, and plugins
866
- * @returns A `docx` `Document` ready for packaging
867
- *
868
- * @example
869
- * ```ts
870
- * const doc = await compileDocument({
871
- * config: { page: { size: 'A4' } },
872
- * nodes: [
873
- * { type: 'heading', level: 1, text: 'Title' },
874
- * { type: 'paragraph', text: 'Hello world' },
875
- * ],
876
- * plugins: new Map(),
877
- * })
878
- * ```
879
- */
880
- async function compileDocument(options) {
881
- resetNumberingState();
882
- const sectionGroups = [{ nodes: [] }];
883
- for (const node of options.nodes) if (node.type === "sectionBreak") sectionGroups.push({
884
- config: node.config,
885
- nodes: []
886
- });
887
- else sectionGroups[sectionGroups.length - 1].nodes.push(node);
888
- const sections = [];
889
- for (const [i, group] of sectionGroups.entries()) {
890
- const children = [];
891
- for (const node of group.nodes) {
892
- const compiled = await compileNode({
893
- config: options.config,
894
- node,
895
- plugins: options.plugins
896
- });
897
- if (Array.isArray(compiled)) children.push(...compiled);
898
- else children.push(compiled);
899
- }
900
- const sectionConfig = group.config;
901
- const pageConfig = i === 0 ? options.config : mergePageConfig(options.config, sectionConfig);
902
- sections.push({
903
- ...compileSectionProperties(pageConfig),
904
- children,
905
- footers: compileFooters(i === 0 ? void 0 : sectionConfig?.footer, options.config),
906
- headers: compileHeaders(i === 0 ? void 0 : sectionConfig?.header, options.config)
907
- });
908
- }
909
- const numberingConfig = numberingConfigMap.size > 0 ? Array.from(numberingConfigMap.values()) : void 0;
910
- return new Document({
911
- creator: options.config.metadata?.creator,
912
- description: options.config.metadata?.description,
913
- keywords: options.config.metadata?.keywords?.join(", "),
914
- lastModifiedBy: options.config.metadata?.lastModifiedBy,
915
- subject: options.config.metadata?.subject,
916
- title: options.config.metadata?.title,
917
- ...numberingConfig ? { numbering: { config: numberingConfig } } : {},
918
- sections
919
- });
920
- }
921
- /**
922
- * Compile section footers into `docx` `Footer` objects.
923
- *
924
- * @returns `{ default?, first?, even? }` or `undefined` if no footers defined
925
- */
926
- function compileFooters(config, _docConfig) {
927
- if (!config) return;
928
- const result = {};
929
- if (config.default) result.default = new Footer({ children: compileHeaderFooterChildren(config.default) });
930
- if (config.first) result.first = new Footer({ children: compileHeaderFooterChildren(config.first) });
931
- if (config.even) result.even = new Footer({ children: compileHeaderFooterChildren(config.even) });
932
- return Object.keys(result).length > 0 ? result : void 0;
933
- }
934
- /**
935
- * Compile a `HeaderFooterContent` into a list of `docx` `Paragraph`s.
936
- */
937
- function compileHeaderFooterChildren(content) {
938
- return content.children.map((text) => new Paragraph(text));
939
- }
940
- /**
941
- * Compile section headers into `docx` `Header` objects.
942
- *
943
- * @returns `{ default?, first?, even? }` or `undefined` if no headers defined
944
- */
945
- function compileHeaders(config, _docConfig) {
946
- if (!config) return;
947
- const result = {};
948
- if (config.default) result.default = new Header({ children: compileHeaderFooterChildren(config.default) });
949
- if (config.first) result.first = new Header({ children: compileHeaderFooterChildren(config.first) });
950
- if (config.even) result.even = new Header({ children: compileHeaderFooterChildren(config.even) });
951
- return Object.keys(result).length > 0 ? result : void 0;
952
- }
953
- /**
954
- * Compile a single page margin value (shorthand or explicit) into twips.
955
- */
956
- function compilePageMargin(margin) {
957
- if (!margin) return;
958
- const parsed = parseShorthandTwip(margin);
959
- if (!parsed) return;
960
- return {
961
- bottom: parsed.bottom,
962
- left: parsed.left,
963
- right: parsed.right,
964
- top: parsed.top
965
- };
966
- }
967
- /**
968
- * Build the section properties (page size, margin) for the `Document`.
969
- */
970
- function compileSectionProperties(config) {
971
- return { properties: { page: {
972
- margin: compilePageMargin(config.page?.margin),
973
- size: compilePageSize(config.page?.size, config.page?.orientation)
974
- } } };
975
- }
976
- /**
977
- * Merge document-level page config with per-section overrides.
978
- *
979
- * Section config wins when both are specified; falls back to document
980
- * defaults for any property not set in the section.
981
- */
982
- function mergePageConfig(docConfig, sectionConfig) {
983
- if (!sectionConfig?.page) return docConfig;
984
- return {
985
- ...docConfig,
986
- page: {
987
- ...docConfig.page,
988
- ...sectionConfig.page
989
- }
990
- };
991
- }
992
- /**
993
- * Page size presets in twips (width × height).
994
- *
995
- * Values sourced from the OOXML specification.
996
- */
997
- const PAGE_SIZE_PRESETS = {
998
- /** A3: 297 × 420 mm */
999
- A3: {
1000
- height: 23811,
1001
- width: 16838
1002
- },
1003
- /** A4: 210 × 297 mm */
1004
- A4: {
1005
- height: 16838,
1006
- width: 11906
1007
- },
1008
- /** US Legal: 8.5 × 14 in */
1009
- Legal: {
1010
- height: 20160,
1011
- width: 12240
1012
- },
1013
- /** US Letter: 8.5 × 11 in */
1014
- Letter: {
1015
- height: 15840,
1016
- width: 12240
1017
- }
1018
- };
1019
- /**
1020
- * Compile page size into Word twips.
1021
- *
1022
- * Resolves presets (`"A4"`, `"Letter"`, etc.) or custom dimensions.
1023
- * Swaps width/height when orientation is `"landscape"`.
1024
- */
1025
- function compilePageSize(size, orientation) {
1026
- if (!size) return;
1027
- let width;
1028
- let height;
1029
- if (typeof size === "string") {
1030
- const preset = PAGE_SIZE_PRESETS[size];
1031
- if (preset) {
1032
- width = preset.width;
1033
- height = preset.height;
1034
- }
1035
- } else if (typeof size === "object" && size !== null) {
1036
- const s = size;
1037
- width = toTwip(s.width);
1038
- height = toTwip(s.height);
1039
- }
1040
- if (width == null || height == null) return;
1041
- if (orientation === "landscape") return {
1042
- height: width,
1043
- width: height
1044
- };
1045
- return {
1046
- height,
1047
- width
1048
- };
1049
- }
1050
- //#endregion
1051
- export { compileDocument };