Generate Properly Formatted Chinese Word Documents from Markdown: A Guide to typeset

If you’ve ever tried pandoc x.md -o x.docx to create a Word document from Markdown, you know it works fine—for English blog posts. For Chinese contracts, agreements, service confirmations, proposals, or formal correspondence, the default output falls short in almost every way: paper size, fonts, line spacing, headers and footers, table breaks, heading widows, and signature blocks that drift across pages. Some of these issues only show up when you open the file in Word.

typeset solves this. It turns Markdown into properly formatted Chinese .docx files, following local typographic conventions. It also includes a verification workflow that helps you catch layout problems before you send the document out.

Installation and Dependencies

typeset is an Agent Skill, but you can also use it as a standalone command-line tool. Install the underlying dependencies first:

brew install pandoc poppler
brew install --cask libreoffice
  • 🍂
    pandoc: the core conversion engine.
  • 🍂
    poppler: used by verify.py for PDF processing during rendering checks.
  • 🍂
    LibreOffice: used in headless mode to convert docx to PDF, then render each page as a JPG for visual inspection.

Clone the repository to the appropriate directory. The Agent Skills standard says Codex CLI reads from ~/.agents/skills/, while Claude Code reads from ~/.claude/skills/. Install once and symlink so both tools can use it:

git clone https://github.com/cerul-ai/typeset.git ~/.agents/skills/typeset
ln -s ../../.agents/skills/typeset ~/.claude/skills/typeset

After that, you can tell Claude Code or Codex “turn this contract into a Word document” and it will automatically use typeset.

For command-line usage, the two main scripts are:

  • 🍂
    scripts/build.py: Markdown → .docx
  • 🍂
    scripts/verify.py: structure linting + rendering checks

Typographic Differences from pandoc Defaults

The gap between pandoc’s default docx output and what a formal Chinese document requires goes beyond “which font to use.” Here’s a quick comparison:

Item pandoc default Needed for Chinese formal docs
Paper size Letter A4
Headings #0F4761 cyan, not bold Black, bold, Heiti (sans‑serif)
Latin font Aptos Times New Roman
Chinese font Theme fallback, varies by machine Explicitly specified Songti / Heiti
Line spacing Single 1.5× (for margin notes)
Headers/footers None Horizontal rule + page numbers (Page X of Y)

These differences mean that pandoc’s default template requires manual tweaks for every formal Chinese document. typeset captures all these adjustments in a reusable build script.

Basic Usage

Start from the template:

cp templates/contract.md my-contract.md

Edit the Markdown, then build:

python3 scripts/build.py my-contract.md

By default, this creates my-contract.docx in the same directory, using the most common Chinese contract style (Scheme A).

If you want to compare multiple cover and signature layouts, use --all to generate four variants at once:

python3 scripts/build.py my-contract.md --all

Each output file includes a scheme suffix, so you can open them side‑by‑side and pick the one that fits your scenario.

Verification: Structure Linting + Rendering Checks

verify.py provides two layers of validation. Even if both pass, you still need to do a final check in Word—but these layers catch most common issues early.

python3 scripts/verify.py my-contract.docx --render

Layer 1: Structure linting

This checks the internal structure of the docx without any external rendering. It runs fast and catches things like:

  • 🍂
    Tables split across pages
  • 🍂
    Heading widows (a heading left alone at the bottom of a page)
  • 🍂
    Signature blocks where the first party is on one page and the second party on the next

These are exactly the problems you’d only notice after opening in Word, but they can ruin the professional appearance of a formal document.

Layer 2: Convert to PDF + render each page as JPG

With --render, the script uses LibreOffice to convert the docx to PDF, then renders each page as a separate JPG image. You’ll see output like:

Page 1: Cover
Page 2: Clauses 1-3
Page 3: Clauses 4-6 (table across pages)
...

The real value here is that you actually look at those JPGs. Even if every check passes and the document opens in Word, the layout might still be ugly. Only by flipping through each rendered page can you spot a table that’s been cut in half, a signature block that’s split, or a header rule that’s misaligned.

Font Substitution and Page‑Count Differences

verify.py automatically detects font substitutions and warns you. This matters more than you might think.

On macOS, LibreOffice cannot match “Songti” or “Heiti” by default, so it falls back to Arial Unicode MS. The metrics of the substitute font are different—even the page count can change. The same docx might show 17 pages in Word but 27 pages when rendered by LibreOffice. That’s a 10‑page difference, which shifts signature blocks, appendices, and blank pages to entirely different positions.

So the --render mode using LibreOffice is meant for:

  • 🍂
    Validating structure: table splits, signature block integrity, widow/orphan control
  • 🍂
    Not replacing final layout confirmation in Word

The definitive page breaks and font appearance must be checked in Word. If a LibreOffice rendered page looks “off” but the structure lint passes, open that same document in Word—in many cases, the Word version is correct, and the discrepancy is purely due to font metric differences.

Four Cover and Signature Schemes

The --all flag produces four variants, differing only in cover page and signature block layout. Each suits a different use case:

Scheme Characteristics Best for
A (Reference) Most common domestic contract style Default choice when no special requirement
B (Strict Align) Label‑value two‑column layout with blank lines Printed paper forms filled by hand
C (Formal) Framed info blocks, enlarged titles Official correspondence, external formal documents
D (Modern Clean) Left‑aligned with thin border, no header rule Internal proposals, service confirmations

All schemes share the same body text fonts, line spacing, and header/footer settings. The difference is purely “what the first page looks like” and “how the last page handles signatures.”

If you’re unsure which to choose, run --all once, open all four, and compare the cover pages side‑by‑side. Pick the one that looks most appropriate. When you later update the content, regenerate with that same scheme.

Customising the Style

All typographic parameters are grouped in the constant block at the top of scripts/build.py. Want to change fonts, sizes, or margins? Edit there, not scattered across the code.

The rationale for each parameter is documented in references/house-style.md. For example, why headings use Heiti instead of bolded Songti, or why line spacing is 1.5× rather than “multiple 1.25.” Read the rationale before tweaking, so you don’t make arbitrary changes that break overall harmony.

Quick Reference

  1. Install dependencies: brew install pandoc poppler + brew install --cask libreoffice
  2. Clone the repo to ~/.agents/skills/typeset (or anywhere, then symlink to the appropriate skill directory)
  3. Copy the template: cp templates/contract.md my-contract.md
  4. Edit the Markdown, then build: python3 scripts/build.py my-contract.md
  5. Use --all to generate all four style variants for comparison
  6. Verify: python3 scripts/verify.py my-contract.docx --render
  7. Review each rendered JPG, focusing on table continuity and signature placement
  8. Finally, open the docx in Word to confirm fonts and final pagination
  9. To change style parameters, only modify the constants at the top of scripts/build.py

FAQ

Q: Why not just use pandoc directly?

A: pandoc’s default template is English‑centric. A formal Chinese document requires A4 paper, explicit Songti/Heiti fonts, 1.5× line spacing, and page numbers in headers/footers—none of which are in the default. You could tweak them each time, but it’s easier to bake them into a repeatable build process.

Q: If verify.py --render passes, do I still need to check in Word?

A: Yes. --render relies on LibreOffice, which substitutes fonts on macOS and can produce different pagination from Word. The render check is for structure (tables, signatures, widows). Final font appearance and page breaks must be verified in Word.

Q: Do the four --all documents have different content?

A: No, the body content is identical. Only the cover and signature block layouts differ.

Q: What if font substitution is detected?

A: verify.py will warn you. If you need more accurate rendering in a headless Linux/macOS environment, install Chinese fonts (e.g., fonts-noto-cjk). However, even with fonts installed, LibreOffice and Word rendering engines are not identical—Word remains the final authority.

Q: I only want to change the header text, nothing else. Where do I do that?

A: Look for HEADER_TEXT‑related variables in the constants block in scripts/build.py. Do not manually edit the generated docx—it will be overwritten on the next build. All customisation stays in the Markdown source and the constants block.

Q: Is this only for contracts?

A: No. It works for agreements, service confirmations, proposals, and formal letters. The template is contract‑oriented, but the typographic rules are general for formal Chinese documents.