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.
@@ -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 };