# Quick start

Read DOCX into a traversable LitAST, then generate DOCX from LitAST, in browsers and Node alike.

`@quirekit/lit-docx` does two things: `parseDocx` reads a DOCX file into **LitAST**, a document tree made of plain JSON objects, and `generateDocx` writes LitAST back to DOCX. It does not depend on any editor and never touches the file system; it only works with bytes, so the same code runs in browsers and Node. It is a QuireKit commercial component distributed as a compiled build: development and evaluation are free, the free edition runs in production with an evaluation notice, and a license key removes the notice. Individuals, education, open-source projects and small teams can request a free community key.

## Installation

lit-docx is a preview and has not been published to npm yet. Once published, install it with:

```bash
pnpm add @quirekit/lit-docx
```

It requires Node 18 or later, or a modern browser. The package ships both ESM and CommonJS builds.

## Try it in the browser

The example below runs entirely in this page. It loads a sample report generated by lit-docx; switch to “Your file” to open your own `.docx`. Inspect the result as a rendered preview or as LitAST, then generate a new `.docx` from the LitAST.

### demos/round-trip.vue

```vue
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { DocxPackageError, generateDocx, parseDocx, type LitNode, type ParseWarning } from "@quirekit/lit-docx";
import { QuireAlert, QuireButton, QuireFileUpload, QuireIcon, QuireJsonViewer, QuireProse, QuireSegmented,
  QuireSpace, QuireTabs, QuireTypography } from "@quirekit/ui/vue";
import { createReport } from "../shared/report";
import { LitDocxView } from "@quirekit/lit-docx-html/vue";

const props = defineProps<{ /** Page language supplied by the documentation site. */ locale?: string }>();
const english = computed(() => props.locale === "en");
const t = computed(() => english.value ? {
  source: "Document source", sample: "Sample report", upload: "Your file", choose: "Choose a .docx file",
  hint: "Or drop it here. The file is read in this page and never uploaded", replace: "Choose another file", download: "Generate .docx",
  downloadHint: "Rebuilds the file from the LitAST below", preview: "Preview", tree: "LitAST", result: "Parse result",
  failed: "This file cannot be read as DOCX", paragraphs: "Paragraphs", tables: "Tables", images: "Images", warnings: "Warnings",
  treeNote: "Internal attributes starting with _ and raw XML are omitted; data URLs are shortened", treeLabel: "LitAST",
  previewNote: "Reflowing HTML rendered by lit-docx-html; pagination, fonts and spacing are not reproduced.",
} : {
  source: "文档来源", sample: "示例报告", upload: "我的文件", choose: "选择 .docx 文件",
  hint: "也可以拖到这里。文件只在本页面中读取，不会上传", replace: "更换文件", download: "生成 .docx",
  downloadHint: "从下方的 LitAST 重新生成文件", preview: "预览", tree: "LitAST", result: "解析结果",
  failed: "无法读取为 DOCX", paragraphs: "段落", tables: "表格", images: "图片", warnings: "警告",
  treeNote: "已省略以 _ 开头的内部属性与原始 XML，Data URL 已截短", treeLabel: "LitAST",
  previewNote: "由 lit-docx-html 渲染的随屏重排 HTML；不还原分页、字体和间距。",
});

const mode = ref<"sample" | "upload">("sample");
const fileName = ref("");
const document = shallowRef<LitNode>();
const warnings = ref<readonly ParseWarning[]>([]);
const failure = ref("");
const busy = ref(false);
const source = shallowRef<Uint8Array>();

/** @param bytes - DOCX bytes. @param name - File name shown in the result. @returns Resolves after parsing. */
async function open(bytes: Uint8Array, name: string): Promise<void> {
  failure.value = "";
  fileName.value = name;
  source.value = bytes;
  try {
    const result = await parseDocx(bytes);
    document.value = result.ast;
    warnings.value = result.warnings;
  } catch (error) {
    document.value = undefined;
    source.value = undefined;
    failure.value = error instanceof DocxPackageError ? error.code : String(error);
  }
}

/** Generates the sample report in this page, then reads it back like any other file. */
async function openSample(): Promise<void> {
  await open(await generateDocx(createReport(english.value ? "en" : "zh-CN")), english.value ? "q3-review.docx" : "第三季度经营简报.docx");
}

/** @param details - Files accepted by the upload field. */
async function choose(details: { files: File[] }): Promise<void> {
  const file = details.files[0];
  // parseDocx takes bytes, not a Blob.
  if (file) await open(new Uint8Array(await file.arrayBuffer()), file.name);
}

/** Clears the current file so the drop zone appears again. */
function replaceFile(): void {
  document.value = undefined;
  source.value = undefined;
  failure.value = "";
}

watch(mode, value => { replaceFile(); if (value === "sample") void openSample(); });
watch(english, () => { if (mode.value === "sample") void openSample(); });
onMounted(openSample);

/** Rebuilds the opened document from its LitAST and saves it. */
async function download(): Promise<void> {
  if (!document.value) return;
  busy.value = true;
  try {
    const bytes = await generateDocx(document.value);
    const url = URL.createObjectURL(new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
    const link = Object.assign(window.document.createElement("a"), { href: url, download: fileName.value.replace(/\.docx$/iu, "") + ".docx" });
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 0);
  } finally {
    busy.value = false;
  }
}

/** @param node - LitAST subtree. @param counts - Accumulated counts by node type. @returns Counts by node type. */
function countTypes(node: LitNode, counts = new Map<string, number>()): Map<string, number> {
  counts.set(node.type, (counts.get(node.type) ?? 0) + 1);
  for (const child of node.children ?? []) countTypes(child, counts);
  return counts;
}
const stats = computed(() => {
  const counts = document.value ? countTypes(document.value) : new Map<string, number>();
  return [
    { label: t.value.paragraphs, value: (counts.get("paragraph") ?? 0) + (counts.get("heading") ?? 0) },
    { label: t.value.tables, value: counts.get("table") ?? 0 },
    { label: t.value.images, value: counts.get("image") ?? 0 },
    { label: t.value.warnings, value: warnings.value.length },
  ];
});
// Large documents carry long internal attributes and image data; trim them so the tree stays readable.
const tree = computed(() => document.value ? JSON.stringify(document.value, (key, value: unknown) =>
  key.startsWith("_") || key === "rawXml" ? undefined : typeof value === "string" && value.startsWith("data:") ? `${value.slice(0, 32)}…` : value, 2) : "");
const tabs = computed(() => [{ value: "preview", label: t.value.preview }, { value: "tree", label: t.value.tree }]);
</script>

<template>
  <QuireSpace orientation="vertical" gap="surface" block class="p-qk-surface" data-demo="round-trip">
    <QuireSegmented v-model="mode" :items="[{ value: 'sample', label: t.sample }, { value: 'upload', label: t.upload }]" :label="t.source" size="sm" />
    <QuireFileUpload v-if="mode === 'upload' && !document && !failure" :label="t.upload"
      :accept="{ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'] }" :max-files="1" data-demo-upload @file-accept="choose">
      <template #default="{ openFilePicker, addTransferFiles }">
        <div class="round-trip__picker" @dragover.prevent @drop.prevent="event => event.dataTransfer && addTransferFiles(event.dataTransfer)">
          <QuireButton size="sm" variant="outline" @click="openFilePicker">
            <template #leading><QuireIcon name="upload" /></template>{{ t.choose }}
          </QuireButton>
          <QuireTypography as="span" variant="helper" class="text-qk-text-tertiary">{{ t.hint }}</QuireTypography>
        </div>
      </template>
    </QuireFileUpload>
    <QuireAlert v-if="failure" color="danger" :title="t.failed" :description="`${fileName} · ${failure}`" role="alert">
      <template #actions><QuireButton size="sm" variant="outline" @click="replaceFile">{{ t.replace }}</QuireButton></template>
    </QuireAlert>
    <section v-if="document" :aria-label="t.result">
      <header class="round-trip__header">
        <QuireSpace orientation="vertical" gap="inline">
          <QuireTypography as="span" variant="control" data-demo-file>{{ fileName }}</QuireTypography>
          <QuireTypography as="span" variant="helper" class="text-qk-text-tertiary" data-demo-summary>
            {{ stats.map(item => `${item.label} ${item.value}`).join(" · ") }}
          </QuireTypography>
        </QuireSpace>
        <QuireSpace gap="surfaceCompact">
          <QuireButton v-if="mode === 'upload'" size="sm" variant="ghost" @click="replaceFile">{{ t.replace }}</QuireButton>
          <QuireButton size="sm" variant="solid" :busy="busy" :title="t.downloadHint" data-demo-download @click="download">
            <template #leading><QuireIcon name="download" /></template>{{ t.download }}
          </QuireButton>
        </QuireSpace>
      </header>
      <QuireTabs :items="tabs" :label="t.result" content-padding="none">
        <template #content="{ item }">
          <template v-if="item.value === 'preview'">
            <QuireTypography variant="helper" class="text-qk-text-tertiary round-trip__note">{{ t.previewNote }}</QuireTypography>
            <div class="round-trip__preview">
              <QuireProse><LitDocxView v-if="source" :source="source" /></QuireProse>
            </div>
          </template>
          <QuireSpace v-else orientation="vertical" gap="inline" block class="round-trip__tree">
            <QuireTypography variant="helper" class="text-qk-text-tertiary">{{ t.treeNote }}</QuireTypography>
            <QuireJsonViewer :source="tree" :label="t.treeLabel" :max-tree-characters="5_000_000" :max-height="480" />
          </QuireSpace>
        </template>
      </QuireTabs>
    </section>
  </QuireSpace>
</template>

<style scoped>
.round-trip__header { display: flex; align-items: center; justify-content: space-between; gap: var(--qk-space-surface); padding-block-end: var(--qk-space-surface-compact); flex-wrap: wrap; }
.round-trip__preview { max-height: 520px; overflow: auto; padding: var(--qk-space-group) var(--qk-space-major); border: 1px solid var(--qk-color-border-subtle); border-radius: var(--qk-radius-group); margin-block-start: var(--qk-space-surface-compact); }
.round-trip__preview :deep(.lit-docx-numbered) { margin-block: var(--qk-space-inline); }
/* Prose turns tables into scroll boxes; the preview pane already scrolls, so restore table layout. */
.round-trip__preview :deep(table) { display: table; }
.round-trip__note { padding-block-start: var(--qk-space-surface-compact); }
.round-trip__picker { display: flex; align-items: center; flex-wrap: wrap; gap: var(--qk-space-surface-compact); }
.round-trip__tree { padding-block-start: var(--qk-space-surface-compact); }
</style>

```

### shared/report.ts

```ts
import { docx, type LitAstNode } from "@quirekit/lit-docx";

/** Language of the sample report text. */
export type ReportLocale = "zh-CN" | "en";

const copy = {
  "zh-CN": {
    title: "第三季度经营简报", byline: "2026 年 10 月 · 财务部",
    summary: "摘要", lead: ["本季度收入同比增长 ", "12.4%", "，三个区域全部完成目标。"],
    highlights: ["华东区首次突破 500 万元", "新签客户 38 家，续约率 94%", "运营成本较上季度下降 3.1%"],
    regions: "分区域收入", columns: ["区域", "收入（万元）", "同比"],
    rows: [["华东", "512.0", "+18.2%"], ["华南", "376.5", "+9.7%"], ["华北", "298.3", "+6.1%"]],
    more: ["完整数据见", "经营看板", "。"], header: "ACME · 第三季度经营简报", footer: "内部资料，请勿外传",
  },
  en: {
    title: "Q3 Business Review", byline: "October 2026 · Finance",
    summary: "Summary", lead: ["Revenue grew ", "12.4%", " year over year, and every region met its target."],
    highlights: ["East passed 5M for the first time", "38 new customers, 94% renewal rate", "Operating cost down 3.1% quarter over quarter"],
    regions: "Revenue by region", columns: ["Region", "Revenue (k)", "YoY"],
    rows: [["East", "5,120", "+18.2%"], ["South", "3,765", "+9.7%"], ["North", "2,983", "+6.1%"]],
    more: ["Full figures are on the ", "dashboard", "."], header: "ACME · Q3 Business Review", footer: "Internal use only",
  },
} as const;

/** @param text - Paragraph text. @param align - Horizontal alignment. @param bold - Whether the text is bold. @returns One paragraph. */
function line(text: string, align: "left" | "right" = "left", bold = false): LitAstNode {
  return docx.blocks.paragraph({
    children: [docx.inline.text(text, { marks: bold ? [docx.marks.bold()] : [] })],
    properties: docx.styles.paragraphProperties({ textAlign: align }),
  });
}

const rule = { style: "single", size: 4, color: "C9D1DE" };

/**
 * Adds thin grid lines; the table helper has no border option, so they go straight into `attrs`.
 * @param table - Table node from `docx.tables.table`.
 * @returns The same table with outer and inner borders.
 */
function withBorders(table: LitAstNode): LitAstNode {
  return { ...table, attrs: { ...table.attrs, borders: { top: rule, bottom: rule, left: rule, right: rule, insideH: rule, insideV: rule } } };
}

/**
 * Builds a one-page business review: title, summary with a highlighted figure, a bullet
 * list, a bordered table with a shaded header row, a link, and an A4 page with header and footer.
 * @param locale - Language of the report text.
 * @returns LitAST document root.
 */
export function createReport(locale: ReportLocale = "en"): LitAstNode {
  const text = copy[locale];
  // Cells are built by position: the first column is a label, the rest are right-aligned figures.
  const row = (cells: readonly string[], header = false): LitAstNode => docx.tables.row({
    isHeader: header,
    cells: cells.map((value, index) => header
      ? docx.tables.shadedCell({ fill: "DCE4F2", width: 3000, children: [line(value, index ? "right" : "left", true)] })
      : docx.tables.cell({ width: 3000, children: [line(value, index ? "right" : "left")] })),
  });
  const body: LitAstNode[] = [
    docx.blocks.heading({ level: 1, children: [docx.inline.text(text.title)] }),
    docx.blocks.paragraph({ children: [docx.inline.text(text.byline, { marks: [docx.marks.textStyle({ color: "6B7280", fontSize: "10pt" })] })] }),
    docx.blocks.heading({ level: 2, children: [docx.inline.text(text.summary)] }),
    docx.blocks.paragraph({ children: [
      docx.inline.text(text.lead[0]),
      docx.inline.text(text.lead[1], { marks: [docx.marks.bold(), docx.marks.textStyle({ color: "1F6F43" })] }),
      docx.inline.text(text.lead[2]),
    ] }),
    // Lists have no helper. Each `listItem` is one numbered paragraph and holds inline nodes directly.
    { type: "list", attrs: { ordered: false }, children: text.highlights.map(item => ({ type: "listItem", children: [docx.inline.text(item)] })) },
    docx.blocks.heading({ level: 2, children: [docx.inline.text(text.regions)] }),
    withBorders(docx.tables.table({ width: 9000, layout: "fixed", columns: [3000, 3000, 3000], rows: [row(text.columns, true), ...text.rows.map(cells => row(cells))] })),
    docx.blocks.paragraph({ children: [
      docx.inline.text(text.more[0]), docx.inline.link(text.more[1], { href: "https://example.com/dashboard" }), docx.inline.text(text.more[2]),
    ] }),
  ];
  // A section closes the content before it; A4 size and 1-inch margins are in twips.
  const section: LitAstNode = {
    type: "section",
    attrs: { pageWidth: 11906, pageHeight: 16838, margins: { top: 1440, right: 1440, bottom: 1440, left: 1440, header: 720, footer: 720, gutter: 0 } },
    children: [
      { type: "header", attrs: { headerType: "default" }, children: [line(text.header, "right")] },
      { type: "footer", attrs: { footerType: "default" }, children: [line(text.footer, "left")] },
    ],
  };
  return { type: "document", children: [...body, section] };
}

```

## Read a DOCX

`parseDocx` takes the bytes of a complete file (`Uint8Array`) and returns the document tree `ast` together with non-fatal `warnings`. A file that is not DOCX throws `DocxPackageError`.

Start with two steps: obtain bytes, then parse them. Here `file` is a browser `File` selected by the user:

```ts [Read a browser file]
import { parseDocx } from '@quirekit/lit-docx'
const bytes = new Uint8Array(await file.arrayBuffer())
const { ast, warnings } = await parseDocx(bytes)
```

`ast` is the tree to read or modify; `warnings` describes parsing issues to explain to the user. In Node, obtain bytes with `readFile()` and use the same parser.

For text extraction, inspect `recipes/read-text.ts` for reading and parsing, and `shared/text.ts` for tree traversal.

### recipes/read-text.ts

```ts
import { readFile } from "node:fs/promises";
import { DocxPackageError, parseDocx } from "@quirekit/lit-docx";
import { collectParagraphText } from "../shared/text";

/**
 * Reads a DOCX file and prints its paragraphs and parser warnings.
 * @param path - DOCX file on disk.
 * @returns Resolves after printing.
 */
export async function printDocx(path: string): Promise<void> {
  try {
    const { ast, warnings } = await parseDocx(new Uint8Array(await readFile(path)));
    for (const line of collectParagraphText(ast)) console.log(line);
    for (const warning of warnings) console.warn(`[${warning.code}] ${warning.message}`);
  } catch (error) {
    // Not a DOCX ZIP package at all, or an encrypted / legacy .doc container.
    if (error instanceof DocxPackageError) console.error(`Cannot open ${path}: ${error.code}`);
    else throw error;
  }
}

```

### shared/text.ts

```ts
import type { LitNode } from "@quirekit/lit-docx";

/**
 * Collects the visible text of every paragraph-like block, in document order.
 * @param node - Any LitAST node.
 * @param lines - Output buffer shared across the walk.
 * @returns The same buffer, for chaining.
 */
export function collectParagraphText(node: LitNode, lines: string[] = []): string[] {
  if (node.type === "paragraph" || node.type === "heading") {
    lines.push(inlineText(node));
    return lines;
  }
  for (const child of node.children ?? []) collectParagraphText(child, lines);
  return lines;
}

/** @param node - Paragraph-level node. @returns Concatenated text of its inline descendants. */
function inlineText(node: LitNode): string {
  if (node.type === "text") return node.text ?? "";
  return (node.children ?? []).map(inlineText).join("");
}

```

The `collectParagraphText` helper used above just walks the tree: the words of paragraphs and headings live in their `text` children.

## Generate a DOCX

LitAST is plain objects. You can write it by hand or with the package's `docx` authoring helpers; both produce exactly the same structure. The report in the example above is built this way: a title, formatted text, a bullet list, a bordered table with a shaded header row, a link, and an A4 page with a header and footer. `shared/report.ts` builds that report; `recipes/create-report.ts` generates bytes and writes them. First, generate just one paragraph:

```ts [Generate one paragraph]
import { generateDocx } from '@quirekit/lit-docx'
const bytes = await generateDocx({
  type: 'document',
  children: [{ type: 'paragraph', children: [{ type: 'text', text: 'Hello' }] }],
})
```

Write `bytes` to a Node file or download them in a browser. Inspect the complete report by file below:

### recipes/create-report.ts

```ts
import { writeFile } from "node:fs/promises";
import { generateDocx } from "@quirekit/lit-docx";
import { createReport } from "../shared/report";

/**
 * Writes the report to disk.
 * @param path - Destination `.docx` path.
 * @returns Resolves once the file is written.
 */
export async function writeReport(path: string): Promise<void> {
  await writeFile(path, await generateDocx(createReport()));
}

```

### shared/report.ts

```ts
import { docx, type LitAstNode } from "@quirekit/lit-docx";

/** Language of the sample report text. */
export type ReportLocale = "zh-CN" | "en";

const copy = {
  "zh-CN": {
    title: "第三季度经营简报", byline: "2026 年 10 月 · 财务部",
    summary: "摘要", lead: ["本季度收入同比增长 ", "12.4%", "，三个区域全部完成目标。"],
    highlights: ["华东区首次突破 500 万元", "新签客户 38 家，续约率 94%", "运营成本较上季度下降 3.1%"],
    regions: "分区域收入", columns: ["区域", "收入（万元）", "同比"],
    rows: [["华东", "512.0", "+18.2%"], ["华南", "376.5", "+9.7%"], ["华北", "298.3", "+6.1%"]],
    more: ["完整数据见", "经营看板", "。"], header: "ACME · 第三季度经营简报", footer: "内部资料，请勿外传",
  },
  en: {
    title: "Q3 Business Review", byline: "October 2026 · Finance",
    summary: "Summary", lead: ["Revenue grew ", "12.4%", " year over year, and every region met its target."],
    highlights: ["East passed 5M for the first time", "38 new customers, 94% renewal rate", "Operating cost down 3.1% quarter over quarter"],
    regions: "Revenue by region", columns: ["Region", "Revenue (k)", "YoY"],
    rows: [["East", "5,120", "+18.2%"], ["South", "3,765", "+9.7%"], ["North", "2,983", "+6.1%"]],
    more: ["Full figures are on the ", "dashboard", "."], header: "ACME · Q3 Business Review", footer: "Internal use only",
  },
} as const;

/** @param text - Paragraph text. @param align - Horizontal alignment. @param bold - Whether the text is bold. @returns One paragraph. */
function line(text: string, align: "left" | "right" = "left", bold = false): LitAstNode {
  return docx.blocks.paragraph({
    children: [docx.inline.text(text, { marks: bold ? [docx.marks.bold()] : [] })],
    properties: docx.styles.paragraphProperties({ textAlign: align }),
  });
}

const rule = { style: "single", size: 4, color: "C9D1DE" };

/**
 * Adds thin grid lines; the table helper has no border option, so they go straight into `attrs`.
 * @param table - Table node from `docx.tables.table`.
 * @returns The same table with outer and inner borders.
 */
function withBorders(table: LitAstNode): LitAstNode {
  return { ...table, attrs: { ...table.attrs, borders: { top: rule, bottom: rule, left: rule, right: rule, insideH: rule, insideV: rule } } };
}

/**
 * Builds a one-page business review: title, summary with a highlighted figure, a bullet
 * list, a bordered table with a shaded header row, a link, and an A4 page with header and footer.
 * @param locale - Language of the report text.
 * @returns LitAST document root.
 */
export function createReport(locale: ReportLocale = "en"): LitAstNode {
  const text = copy[locale];
  // Cells are built by position: the first column is a label, the rest are right-aligned figures.
  const row = (cells: readonly string[], header = false): LitAstNode => docx.tables.row({
    isHeader: header,
    cells: cells.map((value, index) => header
      ? docx.tables.shadedCell({ fill: "DCE4F2", width: 3000, children: [line(value, index ? "right" : "left", true)] })
      : docx.tables.cell({ width: 3000, children: [line(value, index ? "right" : "left")] })),
  });
  const body: LitAstNode[] = [
    docx.blocks.heading({ level: 1, children: [docx.inline.text(text.title)] }),
    docx.blocks.paragraph({ children: [docx.inline.text(text.byline, { marks: [docx.marks.textStyle({ color: "6B7280", fontSize: "10pt" })] })] }),
    docx.blocks.heading({ level: 2, children: [docx.inline.text(text.summary)] }),
    docx.blocks.paragraph({ children: [
      docx.inline.text(text.lead[0]),
      docx.inline.text(text.lead[1], { marks: [docx.marks.bold(), docx.marks.textStyle({ color: "1F6F43" })] }),
      docx.inline.text(text.lead[2]),
    ] }),
    // Lists have no helper. Each `listItem` is one numbered paragraph and holds inline nodes directly.
    { type: "list", attrs: { ordered: false }, children: text.highlights.map(item => ({ type: "listItem", children: [docx.inline.text(item)] })) },
    docx.blocks.heading({ level: 2, children: [docx.inline.text(text.regions)] }),
    withBorders(docx.tables.table({ width: 9000, layout: "fixed", columns: [3000, 3000, 3000], rows: [row(text.columns, true), ...text.rows.map(cells => row(cells))] })),
    docx.blocks.paragraph({ children: [
      docx.inline.text(text.more[0]), docx.inline.link(text.more[1], { href: "https://example.com/dashboard" }), docx.inline.text(text.more[2]),
    ] }),
  ];
  // A section closes the content before it; A4 size and 1-inch margins are in twips.
  const section: LitAstNode = {
    type: "section",
    attrs: { pageWidth: 11906, pageHeight: 16838, margins: { top: 1440, right: 1440, bottom: 1440, left: 1440, header: 720, footer: 720, gutter: 0 } },
    children: [
      { type: "header", attrs: { headerType: "default" }, children: [line(text.header, "right")] },
      { type: "footer", attrs: { footerType: "default" }, children: [line(text.footer, "left")] },
    ],
  };
  return { type: "document", children: [...body, section] };
}

```

## Next steps

- [Reading DOCX](/en/guide/reading): input types, parse options, warnings and errors.
- [Generating DOCX](/en/guide/generating): hand-written nodes, the `docx` helpers, and unit conventions.
- [The LitAST node model](/en/guide/litast): which nodes and marks exist and what they look like.
- [Round trips and preservation](/en/guide/round-trip): what survives reading a file and writing it back, and what does not.
- [Support matrix](/en/support): how each Word feature is supported for reading and for generating.
