Zero-config Markdown β PDF documentation engine
Transform any README.md into a premium, print-ready PDF report β no configuration, no templates, no LaTeX.
pipx install doc-engine-cli
Built and maintained by one person, in the open. Sponsorship keeps it that way.
doc-engine-cli is a developer-first CLI tool that converts Markdown files into professionally styled PDF documents using Typst as its rendering backend. It is designed for teams and individual developers who need high-quality documentation artifacts without the complexity of LaTeX or manual typesetting.
The tool auto-detects your README.md, extracts metadata from Git, and produces an IEEE-inspired technical document β complete with cover page, table of contents, and premium typography β in a single command.
doc-engine buildThat's it. Zero configuration required.
| Feature | Description |
|---|---|
| Zero-Config | Auto-detects README.md, Git author, and document title. No setup files needed. |
| Seven Templates | Academic, article, book, minimal, modern, report, and technical layouts, each with a configurable accent color. Point --template at your own .typ file to go further. |
| Front Matter | An optional --- metadata block sets the title, subtitle, author, template, and accent right inside the file. |
| Watch Mode | --watch rebuilds the PDF every time you save the source. |
| Diagrams | ```mermaid and ```svg blocks are rendered as real diagrams, in pure Python β no Node, no headless browser. |
| Alerts | > [!NOTE] and friends render as coloured callouts, the way GitHub shows them. |
| Math | LaTeX math, inline with $β¦$ and display with $$β¦$$, translated into native Typst math. |
| Multi-File | A doc-engine.md manifest builds one PDF from many files, diagrams, figures, and a bibliography. |
| Page Sizes | A4 by default, plus A3βA6, ISO/JIS B5, and US letter, legal, and tabloid. |
| Project Config | A .doc-engine.toml keeps a team's defaults out of every document. |
| Rich Markdown | Embeds local images, renders GitHub task lists as real checkboxes, and turns [^1] footnotes into native Typst footnotes. |
| Error Checking | Reports source problems with line and column before compiling. A --dry-run mode runs the check on its own. |
| Non-Destructive | Never overwrites an existing PDF β writes report (1).pdf, report (2).pdf, β¦ unless you pass --force. |
| Premium Typography | Font stacks that end in a font Typst ships, so a document looks the same in a bare container as on a laptop. |
| Pure Python | No external binaries required (no Pandoc, no LaTeX). Ships as a single pip install. |
| Cross-Platform | Works on Windows, macOS, and Linux with Python 3.10+. |
Cite with the usual [@citation-key] syntax and drop a refs.bib,
references.bib, or bibliography.bib next to your document. It is picked up
automatically and rendered as an IEEE-styled references section:
As shown in [@smith2020], results vary.Point --bib at a different file, or name one in front matter, to override the
search. Combine it with the academic, article, or report layout and LaTeX
math for a paper that needs no LaTeX toolchain.
pipx install doc-engine-cli(If you don't have pipx, you can install it via pip install pipx)
Note
Installing with a pipx older than 1.17 could crash with
InvalidVersion: Invalid version: ''. That came from a dependency and was
fixed upstream in mermaidx 0.9.5, so a fresh install is fine. If you are
holding an older mermaidx, upgrade it, or use
uv:
uv tool install doc-engine-cliNavigate to any project directory containing a README.md and run:
doc-engine buildThe tool will:
- Auto-detect
README.mdin the current directory - Extract the document title from the first
# heading - Read your Git
user.namefor the author field - Generate a
README_doc.pdfwith cover page, ToC, and formatted content
doc-engine build path/to/file.md -o output.pdf -t "Custom Title" -a "Author Name"doc-engine build [INPUT_FILE] Convert a Markdown file into a PDF
doc-engine info Show version, repository, and what is supported
doc-engine info --json The same, as JSON, for tools that drive the CLI
doc-engine --version Print the version and exit
doc-engine --help Show all commands and flags
| Flag | Default | Description |
|---|---|---|
INPUT_FILE |
auto-detect README.md |
Path to the Markdown file to convert. |
-o, --output |
<input>_doc.pdf |
Output PDF path. |
-t, --title |
first # heading |
Document title override. |
-s, --subtitle |
none | Subtitle shown under the title on the cover. |
-a, --author |
git config user.name |
Author name override. |
--date |
today | Date shown on the cover. |
--template |
academic |
A built-in layout (academic, article, book, minimal, modern, report, technical) or a path to your own .typ file. |
--accent |
template default | Accent color as a hex value (#2563eb) or a name (blue, teal, rose, ...). |
--paper |
a4 |
Page size: a3βa6, iso-b5, jis-b5, us-letter, us-legal, us-tabloid. |
--bib |
auto-detect refs.bib |
Path to a custom .bib file for the bibliography. |
--pdf-standard |
off | Write an archival PDF/A file: a-2b or a-3b. |
--code-theme |
Typst default | Syntax highlighting theme: github, solarized, monochrome, or a path to a .tmTheme. |
--tall-images |
fit |
What to do with a picture taller than a page: fit scales it onto one page, split cuts it across several. |
--fetch-images |
off | Download images linked by URL instead of rendering their alt text. |
--no-branding |
off | Hide the doc-engine attribution from the PDF. |
--dry-run |
off | Check the Markdown for errors and exit without writing a PDF. |
-w, --watch |
off | Rebuild automatically whenever the source file changes. |
-f, --force |
off | Overwrite the output file instead of writing a numbered copy. |
--open |
off | Open the PDF after it is generated. |
Any flag can also be set in the front matter (see below); a flag on the command line always wins.
Basic β zero-config mode:
cd my-project
doc-engine build
# β Generates README_doc.pdfSpecify input and output:
doc-engine build CONTRIBUTING.md -o contributing_guide.pdfOverride metadata:
doc-engine build -t "API Reference v2.0" -a "Engineering Team"Pick a template and accent color:
doc-engine build --template modern --accent teal
doc-engine build --template technical --accent "#7c3aed"Check for errors before building:
doc-engine build --dry-runDrop the engine attribution from the PDF:
doc-engine build --no-brandingGenerate and open immediately:
doc-engine build --openRebuild on every save:
doc-engine build --watchUse as Python module:
python -m doc_engine build README.mdAny Markdown file can open with a --- block to carry its own settings, so the
document renders the same way for everyone β no flags to remember:
---
title: Payments API
subtitle: Integration Guide
author: Platform Team
template: technical
accent: teal
---
# Payments API
...Supported keys: title, subtitle, author, date, template, accent,
paper, and bib. A flag passed on the command line overrides the matching front-matter key,
which in turn overrides the auto-detected value.
Pass --watch to keep doc-engine running and rebuild the PDF whenever you save
the source. It's the fastest way to tweak a template or accent and see the result:
doc-engine build --watch --template modern --accent tealThe output path is chosen once when watch starts, then rewritten in place on each
change. Press Ctrl+C to stop.
Fenced blocks tagged mermaid or svg become pictures instead of code:
```mermaid
flowchart LR
Client --> API --> Ledger
```Mermaid is rendered through an embedded JavaScript engine, so there is no Node
install and no headless browser β it stays a plain pip install. A svg block
is passed straight through, since Typst draws SVG natively.
If a diagram has a syntax error, the build stops and reports Mermaid's own message rather than producing a broken document.
LaTeX math is translated into native Typst math, inline with $β¦$ and as a
display block with $$β¦$$:
The quadratic formula is $x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$.
$$
P(A \mid B) = \frac{P(B \mid A)\,P(A)}{P(B)}
$$Greek letters, relations, fractions, roots, sub- and superscripts, font
commands, matrices, and cases are covered. Anything unrecognized passes
through with its backslash removed, which lands on the right Typst symbol most
of the time.
A $ that is not math stays untouched, so prices and shell variables survive:
$10, $HOME, and export $PATH all render as written.
A blockquote that opens with a marker becomes a coloured callout, matching what GitHub shows on the page:
> [!NOTE]
> Useful information worth knowing.
> [!WARNING]
> Something that needs attention.NOTE, TIP, IMPORTANT, WARNING, and CAUTION are all recognized. A
blockquote without a marker stays an ordinary quote.
Code blocks are highlighted with Typst's own colours by default. --code-theme
swaps that for something else:
doc-engine build --code-theme github
doc-engine build --code-theme monochrome # for printing in black and white| Theme | Look |
|---|---|
github |
GitHub's light palette |
solarized |
Solarized light |
monochrome |
Greys only β keeps code legible on a black-and-white printer |
Any TextMate .tmTheme file works too, so a theme from your editor can be
pointed at directly:
doc-engine build --code-theme ~/themes/my-editor.tmThemeInside a manifest build, a link from one included file to another becomes a jump within the PDF rather than a link to a file the reader does not have:
For the full picture see [the data model](model.md).That resolves to the place where model.md was merged in. Links to anything
outside the build β a URL, a file that is not part of the manifest β are left
exactly as they are.
A project that has outgrown a single file lists its parts in doc-engine.md,
using ordinary Markdown links so the manifest still reads as a table of
contents on GitHub:
---
title: Payments Platform
subtitle: Engineering Handbook
template: report
---
- [Overview](doc/overview.md)
- [Architecture](diagrams/architecture.mmd)
- [Schema](img/schema.svg)
- [References](bib/references.bib)Then just build:
doc-engine buildEach entry is handled by what it is:
| Entry | What happens |
|---|---|
.md |
Appended as a section, headings intact |
.mmd, .mermaid |
Rendered as a diagram at that point |
.png, .svg, .jpg, β¦ |
Placed as a captioned figure |
.bib |
Registered as the bibliography for the document |
Paths resolve against the manifest's folder, and every included file resolves
its own images relative to itself β so a file builds the same way alone as it
does inside the manifest. --watch follows every file the manifest names.
Team defaults belong in a .doc-engine.toml next to the project, not repeated
in every document:
[doc-engine]
template = "report"
accent = "teal"
paper = "us-letter"A [tool.doc-engine] table in pyproject.toml works the same way. Precedence
runs command-line flag, then front matter, then this file.
Every key it understands:
| Key | Values |
|---|---|
template |
A built-in layout name, or a path to a .typ file |
paper |
a3βa6, iso-b5, jis-b5, us-letter, us-legal, us-tabloid |
accent |
A hex value or a colour name |
author |
Author name, used instead of the Git user |
bib |
Path to a .bib file |
branding |
false hides the doc-engine attribution |
code_theme |
github, solarized, monochrome, or a path to a .tmTheme |
pdf_standard |
a-2b or a-3b |
tall_images |
fit or split |
fetch_images |
true downloads images linked by URL |
Anything else in the table is ignored, so a typo cannot quietly change how a document is built.
A picture that does not fit the text block is scaled down until it does, so nothing is ever clipped. For a tall diagram β a top-down flowchart, a long schema β scaling it onto one page can leave it unreadable, so it can be cut across pages at full size instead:
doc-engine build --tall-images splitFor documents that have to stay readable for decades:
doc-engine build --pdf-standard a-2ba-3b is also accepted, which additionally allows embedded attachments.
doc-engine ships with seven layouts. Switch with --template <name>, and recolor any of them with --accent.
| Template | Look |
|---|---|
academic |
Serif IEEE-style report with cover page, table of contents, and running headers. The default. |
modern |
Clean sans-serif layout with generous spacing and a left-aligned cover. |
minimal |
No cover or table of contents β a compact title block, then straight into the content. |
technical |
Bold layout with a filled accent banner and section markers. Good for engineering docs. |
book |
Classic centered title page with chapter-style section breaks. |
article |
A LaTeX paper: New Computer Modern, numbered sections, title block on page one. |
report |
Roomy and easy on the eyes β 12pt on generous leading, wide margins, lots of air. |
doc-engine build --template book
doc-engine build --template modern --accent roseAccent colors take a hex value (#0ea5e9) or one of these names: blue, sky, indigo, violet, purple, red, rose, orange, amber, green, emerald, teal, slate, black.
--template also accepts a path to a .typ file, so you can ship a house style
without forking the project:
doc-engine build --template ./corporate.typThe quickest way to start is to copy one of the files in
doc_engine/templates/ and edit it. A template exposes a
single setup_doc entry point, and the compiler passes it the document metadata:
#let setup_doc(
title: "",
subtitle: "",
author: "Anonymous",
date: datetime.today().display(),
bibliography_file: none,
accent: none,
branding: true,
version: "",
body,
) = { ... }Before compiling, doc-engine scans the Markdown for problems and reports them with the exact line and column, so you can jump straight to the fix:
README.md:42:8: error: link URL must not be empty
README.md:51:1: warning: image source is empty
Errors stop the build; warnings don't. Use --dry-run to run the check on its own without producing a PDF β handy in CI:
doc-engine build --dry-run βββββββββββββββ
β README.md β
ββββββββ¬βββββββ
β
ββββββββΌβββββββ
β CLI Layer β click + rich
β (cli.py) β arg parsing, git detection
ββββββββ¬βββββββ
β
ββββββββββββββΌβββββββββββββ
β β
ββββββββΌβββββββ βββββββββΌβββββββ
β Converter β β Compiler β
β(converter.py)β β(compiler.py) β
β β β β
β Markdown AST β β Typst β PDF β
β β Typst β β via typst-pyβ
ββββββββ¬βββββββ βββββββββ¬βββββββ
β β
β ββββββββββββββββ β
ββββββΊ templates/ βββββββ
β *.typ β
ββββββββ¬βββββββ
β
ββββββββΌβββββββ
β output.pdf β
βββββββββββββββ
| Stage | Module | Responsibility |
|---|---|---|
| 1. Input Resolution | cli.py |
Locate Markdown file, detect Git metadata |
| 2. Source Checking | linter.py |
Report empty links and unclosed fences with line/column |
| 3. Markdown Parsing | converter.py |
Parse Markdown AST via mistune, emit Typst markup |
| 4. Template Injection | compiler.py |
Merge converted content with the selected template |
| 5. PDF Compilation | compiler.py |
Compile via typst Python bindings |
The converter module parses Markdown using mistune and generates equivalent Typst markup:
| Markdown | Typst Output |
|---|---|
# Heading |
= Heading |
**bold** |
*bold* |
*italic* |
_italic_ |
`code` |
`code` |
[text](url) |
#link("url")[text] |
- item |
- item |
1. item |
+ item |
- [x] task |
rendered checkbox |
text[^1] |
#footnote[...] |
 |
#image("local.png") |
<img src="local.png" width="300"> |
Embedded image, using the same path as Markdown images |
> blockquote |
#block(...) |
--- |
#line(...) |
Special characters (#, $, @, *, _, etc.) are automatically escaped to prevent Typst interpretation.
A width or height on an HTML image is read as CSS pixels, a 96th of an inch each, and no picture is drawn wider than the text block however large the number is. Percentages are ignored, since fitting the text block is already the default.
Each template lives in doc_engine/templates/ and exposes the same setup_doc entry point, so the compiler can swap between them with --template. The default academic template provides:
- Cover page with title, author, and date
- Table of contents with depth-3 navigation
- Running headers with document title and author
- Page footer with page numbers and engine attribution
- Code blocks with rounded corners and subtle borders
- Heading hierarchy with accent-colored H2 sections
The other templates (modern, minimal, technical, book) keep the same content but change the fonts, layout, and cover. The accent color is injected at compile time, so --accent recolors any of them.
doc-engine-cli/
βββ doc_engine/
β βββ __init__.py # Package version
β βββ __main__.py # python -m doc_engine entrypoint
β βββ cli.py # Click-based CLI + Git detection
β βββ help.py # Rich help screens
β βββ config.py # .doc-engine.toml project settings
β βββ settings.py # flag / front matter / project precedence
β βββ frontmatter.py # Leading --- metadata block
β βββ manifest.py # doc-engine.md multi-file builds
β βββ converter.py # Markdown β Typst transpiler
β βββ latex.py # LaTeX math β Typst math
β βββ diagrams.py # Mermaid and SVG blocks
β βββ images.py # Cutting pictures taller than a page
β βββ remote.py # Downloading linked images
β βββ compiler.py # Typst β PDF compilation engine
β βββ linter.py # Source checks (line/column reporting)
β βββ templates/
β βββ academic.typ # Default IEEE-style report
β βββ article.typ # LaTeX paper, numbered sections
β βββ report.typ # Roomy and legible
β βββ modern.typ # Clean sans-serif layout
β βββ minimal.typ # Compact, no cover page
β βββ technical.typ # Accent banner + section markers
β βββ book.typ # Centered title page, chapter breaks
β βββ themes/
β βββ github.tmTheme # Syntax highlighting themes
β βββ solarized.tmTheme
β βββ monochrome.tmTheme
βββ tests/ # 181 tests across every module
βββ pyproject.toml # Package configuration + dependencies
βββ LICENSE # MIT License
βββ .gitignore
βββ README.md
| Package | Purpose | License |
|---|---|---|
click |
CLI framework | BSD-3 |
rich |
Terminal formatting and progress indicators | MIT |
mistune |
Markdown parser (pure Python) | BSD-3 |
typst |
Typst compiler bindings | Apache-2.0 |
mermaidx |
Mermaid rendering without Node | MIT |
pillow |
Cutting pictures taller than a page | MIT-CMU |
All dependencies are pure Python β no external binaries (Pandoc, LaTeX, etc.) are required.
git clone https://github.com/leonardosalasd/doc-engine-cli.git
cd doc-engine-cli
pip install -e ".[dev]"python -m pytest tests/ -v# Generate PDF from this project's README
python -m doc_engine build
# Run with verbose error output
python -m doc_engine build README.md -o docs_output.pdfA container image is published to GitHub Container Registry on every release. Mount your project into /workspace and run build as usual:
docker run --rm -v "$PWD:/workspace" ghcr.io/leonardosalasd/doc-engine-cli buildThe entrypoint is doc-engine, so you can pass any command or flag:
docker run --rm -v "$PWD:/workspace" ghcr.io/leonardosalasd/doc-engine-cli build --template modern --accent teal- Headings (H1βH6)
- Bold, italic, strikethrough
- Inline code and fenced code blocks (with language hints)
- Links
- Ordered and unordered lists
- Nested lists
- Blockquotes
- Tables
- Horizontal rules
- Line breaks (
<br>) - Task lists (
- [x]/- [ ]) - Footnotes (
[^1]) - Local images, and remote ones with
--fetch-images - Raw HTML
<img>tags (inline or block), sized bywidthandheightin pixels - Math blocks (LaTeX
$β¦$and$$β¦$$) - Mermaid and SVG diagram blocks
- GitHub alerts (
> [!NOTE],[!TIP],[!IMPORTANT],[!WARNING],[!CAUTION])
- Template selection via
--templateflag - Configurable accent color via
--accent - Source error checking with line/column and
--dry-run - User-supplied template files (point
--templateat a path) - YAML front-matter support for metadata override
- Local image embedding
- Watch mode for continuous rebuilds
- Math expressions (LaTeX-style
$...$) - Multi-file documentation merge
- Mermaid diagram rendering
- Image downloading and embedding for remote URLs
- PDF/A compliance for archival
- Page size selection
- Project-level configuration file
- Syntax highlighting themes for code blocks
- Cross-references between documents
doc-engine-cli is written and maintained by one person, in the open, and it is
free under the MIT licence for everyone β no paid tier, no telemetry, no
upsell. If it saves you the afternoon you would have spent fighting LaTeX, you
can put something behind it:
Sponsorship pays for the time that goes into the parts nobody sees: testing on Windows, chasing a rendering bug across five Python versions, answering issues, and keeping the release quality where it is.
It is entirely optional. Starring the repository, reporting a bug you hit, or telling someone the tool exists all help too, and cost nothing.
Contributions are welcome β bug reports, documentation fixes, new templates, and features. The contributing guide covers setup, testing across Python versions, code style, and how to add a template.
| Contributing guide | Setup, tests, style, pull requests |
| Code of conduct | Expected behavior in community spaces |
| Security policy | Reporting a vulnerability privately |
| Support | Where to ask questions and report problems |
Questions belong in Discussions; bugs belong in Issues.
This project is licensed under the MIT License.


