Markdown to PDF
Convert one or more Markdown files into styled PDFs using WeasyPrint and the Python markdown library. The default styling is neutral; apply your own brand stylesheet via --css.
How to use
Run the conversion script via uv run:
uv run --with markdown --with weasyprint python3 "${SKILL_DIR}/scripts/convert.py" <files...> [-o output_dir] [--css custom.css]
${SKILL_DIR} is the directory containing this SKILL.md. The script resolves assets/style.css relative to its own location, so it works at any path.
Steps
Identify target
.mdfiles from the request. If none specified, look in the current directory and ask which to convert.Run the conversion:
uv run --with markdown --with weasyprint python3 <skill-dir>/scripts/convert.py file1.md file2.md- Use
-o <dir>to place PDFs in a specific output directory. - Use
--css <path>to override the default stylesheet (e.g.,netresearch-branding-skill/assets/markdown-pdf.css). - Glob patterns like
*.mdare supported.
- Use
Report which PDF files were created and where.
Default styling
The bundled assets/style.css provides:
- system fonts (no external font fetches)
- neutral grayscale headers
- monospace code blocks
- A4 page size, sensible margins
- page numbers in footer
Companion skills
netresearch-branding-skillships amarkdown-pdf.cssbrand asset. Netresearch users: install both skills, then pass--css "$CLAUDE_PLUGIN_ROOT/.../netresearch-branding-skill/.../assets/markdown-pdf.css".- That brand CSS expects two wrapper elements this script does not generate:
.page-header(with an.header-logoimage) and.page-footer(with.footer-info/.footer-page). Passing--csswith a branded stylesheet yields a PDF with no logo and no footer — no error, just missing brand elements. For compliant output, build the HTML yourself: wrap the body in the.page-header/.page-footerdivs the CSS expects, then callweasyprintdirectly. See netresearch-branding-skill'sSKILL.md.
Known pitfalls
- Bullet list right after a lead-in line, no blank line between them:
python-markdown(unlike CommonMark) starts a list only when a blank line precedes it.Intro:\n- itemrenders as literal- itemtext, not a<ul>— silently. Always leave a blank line between a lead-in sentence and its list. - Such rendering bugs raise no error and don't affect the exit code. Before calling a conversion done, render the first page to a PNG (
pymupdf/fitz:page.get_pixmap(dpi=150).save(...)) and inspect it — don't trust✓ converted ... (N KB)alone.
Output format
Per file:
✓ converted README.md → README.pdf (12.3 KB)
✓ converted RFC-001.md → RFC-001.pdf (4.7 KB)
Errors
| Error | Action |
|---|---|
No .md files matched |
List directory contents and ask user |
| WeasyPrint missing | uv run should auto-resolve it; if not, suggest uv pip install weasyprint |
--css file not found |
Surface the missing path; do not fall back silently |