markdown-to-pdf
Turn a Markdown file into a plain, print-grade PDF where every hyperlink stays clickable, pipe tables become real tables, and ```mermaid fences are rendered locally (no network) and embedded as diagrams. Output reads as an internal working document: no cover page, no table of contents, no brand tokens. For the polished branded look use branded-docx; for e-reader output use markdown-to-epub.
Why this exists
Three converters were converged into this one. A reportlab briefing renderer had clickable links but printed tables as raw pipe characters. A headless-Chrome print script had beautiful tables and Mermaid but produced PDFs with zero /URI link annotations (measured; no Chrome flag fixes it). This skill keeps the winning half of each: one reportlab engine with links everywhere (including inside table cells), tables, Mermaid via local mermaid-cli, and the proven header/footer stamp scheme (Page N of M top-left, version and date top-right, and one audience-chosen value centered in the footer: the source filename when printing your own research, your contact when sending a deliverable).
Step 0 - dependencies
The script carries a PEP 723 header, so uv run resolves Python deps (markdown, reportlab, pillow) automatically. Without uv: pip install "markdown>=3.6" "reportlab>=4" pillow and run with plain python.
mermaid-cli is required ONLY when the document contains mermaid fences: npm install -g @mermaid-js/mermaid-cli. Documents without diagrams never touch it.
Usage
uv run "<SKILL_DIR>/scripts/print_markdown.py" doc.md
uv run "<SKILL_DIR>/scripts/print_markdown.py" architecture.md --author "jane.doe@example.com" --docversion v1.0.3
uv run "<SKILL_DIR>/scripts/print_markdown.py" notes.md --accent "#0b5394"
| Flag | Default | Meaning |
|---|---|---|
--out |
<stem>.pdf beside the source |
Output path |
--author |
daniel@magmainc.ca |
Footer contact line; pass '' to suppress |
--docversion |
empty | Version label in the top-right header |
--date |
today | Date shown top-right; pass the document's own date for briefings |
--accent |
#c96442 |
Link and h2 color; #0b5394 gives a conservative internal-blue look |
--source-label |
last 2 path components of the source | Provenance text, so a printed page names the markdown it came from; pass '' to suppress |
--stamp-preset |
research |
Which values get stamped: research (footer = source filename), client (footer = author contact), minimal (page numbers only) |
REBRAND: the defaults worth changing are DEFAULT_AUTHOR, DEFAULT_ACCENT and DEFAULT_STAMP_PRESET at the top of the script, or just pass the flags. STAMP_PRESETS beside them maps each preset to its slots if you want a different mix.
Verify (do this for link-heavy documents)
python -c "from pypdf import PdfReader; r=PdfReader('doc.pdf'); print(sum(1 for p in r.pages for a in (p.get('/Annots') or []) if a.get_object().get('/A')))"
A link-bearing document must report a nonzero count. assets/sample.md exercises every supported construct (links in table cells, nested lists, code wrap, two Mermaid diagram types); render it after install and walk its bottom checklist.
Limitations (known, by design)
- Wide Mermaid sequence diagrams (aspect ratio above ~2, common at 5+ participants) are structurally illegible at portrait body width in every renderer; split the diagram or accept reference scale. Not fixable here.
- Standard PDF base-14 fonts: exotic Unicode glyphs (arrows, box drawing) may not render; standard punctuation is safe.
- Supported Markdown is the working-document subset: h1-h4, paragraphs, nested lists, pipe tables, fenced code, mermaid fences, blockquotes, rules, bold/italic/code/links, bare URLs (autolinked). YAML front matter and HTML comments are stripped.
Do not build
The single script is the whole engine. Do NOT add: a cover page, TOC, brand tokens (that is branded-docx territory), a config file, or an HTML intermediate for styling. Stamp POSITIONS stay fixed: a preset chooses what is stamped, never where. Do not add per-position flags, a layout string, or a slot DSL - Anthropic's own best-practices doc names "offering too many options" as an anti-pattern and prescribes "a default with an escape hatch," which is what the presets are.