Last active
August 11, 2026 18:30
-
-
Save schwartz1375/e34eb85a427504bffacab7aaad62b6b8 to your computer and use it in GitHub Desktop.
Converts a Markdown file (with Mermaid diagrams) to PDF.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Converts a Markdown file (with Mermaid diagrams) to PDF. | |
| # brew install pandoc | |
| # npm install -g @mermaid-js/mermaid-cli | |
| # npx puppeteer browsers install chrome-headless-shell # one-time | |
| # | |
| # v3 changes (August 2026): | |
| # - Space-safe intermediate filenames. An input .md whose name contains | |
| # spaces (e.g. "System Design Notes.md") broke the mmdc→pandoc | |
| # handoff: mmdc rewrites each ```mermaid fence to , | |
| # and GFM cannot parse an image destination with unencoded spaces, so the | |
| # diagrams silently degraded to literal link text in the PDF. Temp basenames | |
| # are now sanitized (spaces → underscores); the output PDF path is untouched. | |
| # | |
| # v2 changes (July 2026): | |
| # - Print CSS injected into the HTML. This is the fix for "incomplete" PDFs: | |
| # without it, Chrome's --print-to-pdf silently CLIPS wide tables and long | |
| # code lines at the page edge, uses no margins, and breaks pages mid-row. | |
| # - puppeteer config auto-generated from the resolved browser path (no more | |
| # manual puppeteer_config.json, and fixes bare-mmdc "Could not find Chrome"). | |
| # - mmdc renders on a white background (-b white); transparent SVGs look | |
| # broken over shaded elements in print. | |
| # - --from gfm so GitHub-flavored tables/strikethrough/task lists parse | |
| # exactly as they render on GitHub. | |
| # - --virtual-time-budget so Chrome finishes layout before printing large | |
| # embedded SVGs (prevents blank/missing trailing content). | |
| set -euo pipefail | |
| [[ $# -lt 1 ]] && { echo "Usage: $0 <input.md> [output.pdf]"; exit 1; } | |
| [[ -f "$1" ]] || { echo "Error: $1 not found"; exit 1; } | |
| INPUT="$1" | |
| OUTPUT="$(cd "$(dirname "${2:-${INPUT%.md}.pdf}")" && pwd)/$(basename "${2:-${INPUT%.md}.pdf}")" | |
| CHROME="$(ls -d "$HOME/.cache/puppeteer/chrome-headless-shell"/*/chrome-headless-shell-mac-arm64/chrome-headless-shell 2>/dev/null | sort -V | tail -1)" | |
| for cmd in mmdc pandoc; do | |
| command -v "$cmd" &>/dev/null || { echo "Error: $cmd not found. See install instructions above."; exit 1; } | |
| done | |
| [[ -x "$CHROME" ]] || { echo "Error: chrome-headless-shell not found. Run: npx puppeteer browsers install chrome-headless-shell"; exit 1; } | |
| WORK_DIR="$(mktemp -d)" | |
| trap 'rm -rf "$WORK_DIR"' EXIT | |
| # spaces → '_' so mmdc's image links stay GFM-parseable (see v3 changes above) | |
| BASENAME="$(basename "${INPUT%.md}" | tr ' ' '_')" | |
| PROCESSED_MD="$WORK_DIR/${BASENAME}_processed.md" | |
| HTML_OUT="$WORK_DIR/${BASENAME}.html" | |
| PRINT_CSS="$WORK_DIR/print.css" | |
| PUPPETEER_CFG="$WORK_DIR/puppeteer_config.json" | |
| # puppeteer config generated from the resolved browser (used by mmdc) | |
| printf '{"executablePath": "%s", "args": ["--no-sandbox"]}\n' "$CHROME" > "$PUPPETEER_CFG" | |
| # Print stylesheet: margins, wrapping, and page-break control. | |
| cat > "$PRINT_CSS" <<'CSS' | |
| @page { size: Letter; margin: 0.8in 0.75in; } | |
| html { -webkit-print-color-adjust: exact; print-color-adjust: exact; } | |
| body { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; | |
| font-size: 10pt; line-height: 1.45; color: #222; | |
| max-width: none; margin: 0; padding: 0; } | |
| h1, h2, h3, h4 { color: #1f3864; break-after: avoid; page-break-after: avoid; } | |
| h1 { font-size: 19pt; border-bottom: 2px solid #1f3864; padding-bottom: 4pt; } | |
| h2 { font-size: 14pt; border-bottom: 1px solid #b9c8dd; padding-bottom: 3pt; margin-top: 16pt; } | |
| h3 { font-size: 11.5pt; } | |
| pre { font-family: "SF Mono", Menlo, monospace; font-size: 7.5pt; line-height: 1.25; | |
| background: #f8f8f8; border: 0.5pt solid #ddd; padding: 6pt; | |
| white-space: pre-wrap; overflow-wrap: anywhere; } | |
| code { font-family: "SF Mono", Menlo, monospace; font-size: 88%; background: #f3f3f3; padding: 0 2px; } | |
| pre code { background: none; font-size: 100%; } | |
| table { border-collapse: collapse; width: 100%; font-size: 8.5pt; margin: 8pt 0; } | |
| th { background: #e6edf6; border: 0.5pt solid #b9c8dd; padding: 4pt 5pt; text-align: left; } | |
| td { border: 0.5pt solid #ccc; padding: 4pt 5pt; vertical-align: top; overflow-wrap: anywhere; } | |
| tr { break-inside: avoid; page-break-inside: avoid; } | |
| blockquote { border-left: 3pt solid #2e5f8a; background: #f2f7fb; margin: 8pt 0; padding: 4pt 10pt; } | |
| img, svg { max-width: 100%; height: auto; } | |
| p:has(> img:only-child) { text-align: center; break-inside: avoid; } | |
| a { color: #2e5f8a; } | |
| CSS | |
| echo "→ Pre-rendering Mermaid diagrams..." | |
| mmdc \ | |
| -i "$INPUT" \ | |
| -o "$PROCESSED_MD" \ | |
| -e svg \ | |
| -b white \ | |
| -p "$PUPPETEER_CFG" \ | |
| --quiet | |
| echo "→ Converting to HTML..." | |
| pandoc \ | |
| "$PROCESSED_MD" \ | |
| --from gfm \ | |
| --standalone \ | |
| --embed-resources \ | |
| --resource-path="$WORK_DIR:$(cd "$(dirname "$INPUT")" && pwd)" \ | |
| --css "$PRINT_CSS" \ | |
| --metadata title="" \ | |
| -o "$HTML_OUT" | |
| echo "→ Printing to PDF..." | |
| "$CHROME" \ | |
| --headless=new \ | |
| --no-sandbox \ | |
| --disable-gpu \ | |
| --virtual-time-budget=20000 \ | |
| --print-to-pdf="$OUTPUT" \ | |
| --no-pdf-header-footer \ | |
| "file://$HTML_OUT" 2>/dev/null | |
| echo "✓ Saved: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Public commit