@xemahq/document-templates
v0.1.0
Published
Layer 1 SDK: a pure, deterministic, domain-agnostic document assembler. Takes agent-authored structured content blocks + a theme and produces a paginated document — section numbering, table of contents, running headers/footers, page-break control — emitti
Readme
@xemahq/document-templates
This package belongs to Layer 1 — a pure SDK with zero runtime
dependencies (only npm dev tooling). It is owned by the xema-kernel-sdk
carved repo, so any Layer 2 service (e.g. document-render-api, which owns the
puppeteer PDF / pandoc DOCX engines) and any Layer 3 biome may depend on it
without crossing the release DAG.
What it is
The deterministic block → document assembly layer. Xema already renders raw HTML → PDF/DOCX; what was missing is the step that turns agent-authored structured content blocks + a theme into a complete, paginated document: section numbering, a generated table of contents, running headers/footers, page numbers, and block composition.
assembleDocument({ title, blocks, theme, metadata }) produces:
- a self-contained, print-ready HTML string (
@pagesize/margins, margin-box running headers/footers via CSS counters + a running string, page-break control) that the existing puppeteer PDF path can consume directly; and - a normalized
DocumentModel(heading-derived section tree + resolved TOC) so a DOCX path can assemble from the same tree without being locked to HTML.
It is pure: no LLM, no network, no clock, no randomness — the same input always yields byte-identical output.
Block model (BlockKind)
A closed discriminated union of generic document primitives — no domain concepts:
heading · paragraph · list · table · image · callout ·
page_break · toc · key_value
Usage
import { assembleDocument, BlockKind } from '@xemahq/document-templates';
const { html, model, pdfHeaderTemplate, pdfFooterTemplate } = assembleDocument({
title: 'Quarterly Report',
blocks: [
{ kind: BlockKind.Toc },
{ kind: BlockKind.Heading, level: 1, text: 'Overview' },
{ kind: BlockKind.Paragraph, runs: [{ text: 'Hello ' }, { text: 'world', bold: true }] },
{ kind: BlockKind.Heading, level: 2, text: 'Details' },
{ kind: BlockKind.PageBreak },
{ kind: BlockKind.Heading, level: 1, text: 'Appendix' },
],
// theme defaults to DEFAULT_THEME (A4, decimal numbering)
});
// html → puppeteer page.setContent(html); page.pdf({ preferCSSPageSize: true })
// model → a DOCX renderer walks model.sections / model.tocMalformed block trees fail fast with a typed DocumentTemplateError citing the
offending path (e.g. Invalid block at blocks[2].runs[0].text: expected string).
Markdown authoring
LLM producers can author content as Markdown instead of hand-writing block
JSON. parseMarkdownToBlocks(markdown, opts?) losslessly expands a documented
CommonMark(+GFM) subset into the same canonical Block[] — it is
input-only sugar (the assembler/renderer are unchanged) that cuts tokens and
the invalid-JSON surface. It is pure and deterministic: identical Markdown always
yields an identical Block[].
import { parseMarkdownToBlocks, assembleDocument } from '@xemahq/document-templates';
const blocks = parseMarkdownToBlocks(`
# Overview
A paragraph with **bold**, _italic_, \`code\` and a [link](https://xema.dev).
- one
- nested
- two
> [!WARNING]
> Handle with care.
`);
const { html } = assembleDocument({ title: 'Report', blocks });Supported subset (each maps onto an existing block/mark — nothing new is invented):
| Markdown | Block |
| --- | --- |
| ATX headings #..###### | HeadingBlock (plain text — inline marks inside a heading are flattened to their text, as HeadingBlock has no runs) |
| paragraphs with **strong**, _em_, `code`, ~~strike~~, [label](href) | ParagraphBlock rich-text runs |
| ordered / unordered lists, nested by indentation | ListBlock |
| GFM pipe tables | TableBlock (first row = header) |
| standalone  on its own line | ImageBlock (alt → caption + alt) |
| blockquotes, incl. a leading GFM alert [!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION] | CalloutBlock (variant from the marker; default note) |
| fenced code ```/~~~ | ParagraphBlock with one monospace (code) run preserving the text (BlockKind is closed — there is deliberately no CodeBlock) |
| thematic break ---/***/___ | PageBreakBlock (the closest structural divider the canonical model offers) |
Deliberately not interpreted (documented, fail-fast — never silently
dropped): raw HTML blocks/autolinks, setext headings, reference-style links,
footnotes, definition lists, YAML frontmatter, and inline images embedded in
rich text. By default (onUnsupported: 'error') these throw a typed
DocumentTemplateError naming the construct and 1-based line. Pass
{ onUnsupported: MarkdownUnsupportedMode.Paragraph } to instead emit a
block-level unsupported construct as a raw literal ParagraphBlock (an inline
image inside a list/table/callout has no clean block fallback and always fails
fast). A GFM table row whose column count differs from the header is malformed
input and always fails fast.
Theme
DocumentTheme describes page size/orientation/margins, typography, per-level
heading styles, header/footer templates (tokens {pageNumber}, {totalPages},
{title}, {sectionTitle}), TOC style, and the numbering scheme. One built-in
theme, DEFAULT_THEME, is provided.
