"""Convert a markdown file to PDF using Playwright (Chromium headless). Chromium has full CSS3 support including proper page-break handling, which xhtml2pdf (the previous converter) did not. The trade-off is a ~150 MB Chromium binary dependency, installed once via: pip install playwright playwright install chromium """ from __future__ import annotations import sys from pathlib import Path import markdown from playwright.sync_api import sync_playwright CSS = """ @page { size: letter; margin: 0.5in; } body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 9pt; line-height: 1.35; color: #222; } h1 { font-size: 17pt; color: #1a1a1a; margin: 0.3em 0 0.2em; border-bottom: 2px solid #333; padding-bottom: 3px; page-break-after: avoid; break-after: avoid; } h2 { font-size: 13pt; color: #1a1a1a; margin: 0.8em 0 0.2em; border-bottom: 1px solid #999; padding-bottom: 2px; page-break-after: avoid; break-after: avoid; } h3 { font-size: 11pt; color: #1a1a1a; margin: 0.6em 0 0.15em; page-break-after: avoid; break-after: avoid; } p, li { font-size: 9pt; margin: 0.25em 0; } ul, ol { margin: 0.2em 0; padding-left: 1.3em; } table { border-collapse: collapse; width: 100%; margin: 0.4em 0; font-size: 8.5pt; page-break-inside: avoid; break-inside: avoid; } th, td { border: 1px solid #aaa; padding: 3px 5px; text-align: left; vertical-align: top; } th { background: #eee; font-weight: bold; } thead { display: table-header-group; } tr { page-break-inside: avoid; break-inside: avoid; } code { background: #f0f0f0; padding: 1px 3px; border-radius: 2px; font-family: Consolas, "Courier New", monospace; font-size: 8.5pt; } pre { background: #f5f5f5; border: 1px solid #ddd; padding: 6px; border-radius: 3px; font-family: Consolas, "Courier New", monospace; font-size: 8pt; line-height: 1.25; white-space: pre-wrap; page-break-inside: avoid; break-inside: avoid; } blockquote { border-left: 3px solid #888; margin-left: 0; padding-left: 10px; color: #555; font-style: italic; } hr { border: 0; border-top: 1px solid #ccc; margin: 0.6em 0; } div { break-inside: avoid; page-break-inside: avoid; } """ def convert(md_path: Path, pdf_path: Path) -> None: md_text = md_path.read_text(encoding="utf-8") html_body = markdown.markdown( md_text, extensions=["tables", "fenced_code", "sane_lists", "md_in_html"], ) html = f"""
{html_body} """ with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.set_content(html, wait_until="networkidle") page.pdf( path=str(pdf_path), format="Letter", margin={"top": "0.5in", "bottom": "0.5in", "left": "0.5in", "right": "0.5in"}, print_background=True, ) browser.close() print(f"Wrote {pdf_path}") if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: md_to_pdf.py