Generating DOCX
Build LitAST with hand-written nodes or the docx authoring helpers, then produce complete DOCX bytes with generateDocx; includes unit conventions.
generateDocx(ast: LitAstNode): Promise<Uint8Array>
generateDocx takes no other arguments. It builds a whole DOCX package from the LitAST and returns the bytes of a complete file; it never patches an existing file in place.
Writing the bytes
// Node
await writeFile("out.docx", await generateDocx(ast));
// Browser: create a download link
const bytes = await generateDocx(ast);
const url = URL.createObjectURL(new Blob([bytes], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}));
Hand-written nodes
LitAST nodes are plain objects, and the root is a document:
const ast = {
type: "document",
children: [
{
type: "paragraph",
children: [
{ type: "text", text: "Hello " },
{ type: "text", text: "world", marks: [{ type: "bold" }] },
],
},
],
};
Generating this document and reading it back with parseDocx yields exactly the same children.
Using the docx helpers
docx is a grouped helper object. It returns nodes like the ones above, only shorter to write and type-checked:
| Group | Contents |
|---|---|
docx.blocks | paragraph, heading, blockquote, codeBlock, pageBreak, callout |
docx.inline | text, link, ruby |
docx.marks | bold, highlight, link, textStyle |
docx.tables | table, row, cell, shadedCell |
docx.styles | paragraphProperties, spacing, indentation, shading, border, runProperties |
The helpers do not cover every node. The document root, image, section and list are written by hand, and the two styles mix freely. Marks without a helper, such as italic, are written as { type: "italic" }.
docx.blocks.paragraph({
children: [
docx.inline.text("Important", { marks: [docx.marks.bold(), docx.marks.textStyle({ color: "C00000", fontSize: "14pt" })] }),
docx.inline.text(": see "),
docx.inline.link("the notes", { href: "https://example.com" }),
],
properties: docx.styles.paragraphProperties({
textAlign: "center",
spacing: docx.styles.spacing({ after: 120 }),
}),
});
Phonetic guides
docx.inline.ruby produces native w:ruby. base is the body text and annotation is the
phonetic text; each layer can carry its own formatting. The caller supplies the pronunciation;
lit-docx does not include an automatic pinyin dictionary.
const guide = docx.inline.ruby({
marks: [docx.marks.textStyle({ fontSize: "12pt" })],
base: [docx.inline.text("汉", {
marks: [docx.marks.textStyle({ fontSize: "12pt" })],
})],
annotation: [docx.inline.text("hàn", {
marks: [docx.marks.textStyle({ fontFamily: "Arial", fontSize: "6pt" })],
})],
rubyAlign: "center",
hps: 12,
hpsRaise: 24,
hpsBaseText: 24,
lid: "zh-CN",
});
const bytes = await generateDocx({
type: "document",
children: [docx.blocks.paragraph({ children: [guide] })],
});
All five properties are required when authoring. Numeric hps, hpsRaise and hpsBaseText
values are half-points. hpsRaise is the stored raise, not the distance shown in Word's
Phonetic Guide dialog. This example follows a Word 16.109.2 sample with 12pt base text and
6pt annotation; it is not an automatic spacing algorithm for arbitrary fonts.
Keep the outer marks size, base-text size and hpsBaseText consistent; the reference size does not replace run formatting.
Alignment values are center, left, right, distributeLetter, distributeSpace and rightVertical.
Parsed nodes store base text in children and annotation in attrs.rubyText. Edit those
nodes or properties before generating again. Unchanged or unmodeled source properties
follow the preservation rules. Place a hyperlink in the
complete ruby node's marks; do not put docx.inline.link directly in either text layer.
Tables
Tables nest as table > tableRow > tableCell > paragraph. A cell needs at least one block; cells created with the helper get an empty paragraph automatically.
docx.tables.table({
columns: [3000, 3000],
rows: [
docx.tables.row({ isHeader: true, cells: [docx.tables.cell({ children: [docx.blocks.paragraph({ children: [docx.inline.text("Region")] })] })] }),
],
});
Images, sections, headers and footers
- Images:
{ type: "image", attrs: { src, width, height } }.srcis the image data URL; width and height are in pixels at 96 DPI. - Sections: a
sectionis a sibling insidedocument.children. It ends the section formed by the content before it and carries that section's page setup. - Headers and footers: children of a
section. Aheaderusesattrs.headerTypeand afooterusesattrs.footerType, each"default","first"or"even".
The report in Quick start uses all three.
Units
| Value | Unit |
|---|---|
| Page size, margins, paragraph indents, spacing before and after, table column widths | twips, 1/20 of a point; 1 inch = 1440, 1 cm ≈ 567 |
| Image width and height | pixels at 96 DPI |
fontSize in textStyle | a string such as "12pt", or a number of points |
Ruby hps, hpsRaise, hpsBaseText | Numeric half-points, or preserved explicit OOXML measurement strings |
| Colors | 6-digit hex RGB; "C00000" and "#C00000" both work; parsed results include # |
A4 is pageWidth: 11906, pageHeight: 16838; US Letter is 12240 × 15840.
Parts added for new documents
A document built from scratch has no original styles.xml, numbering.xml and similar parts, so the generator adds the numbering, comment and note parts the content actually uses. LitAST read from an existing file writes back the original parts kept in ast.meta.rawXmlFiles; see Round trips and preservation.