Skip to content

Instantly share code, notes, and snippets.

@schwartz1375
Last active August 11, 2026 18:30
Show Gist options
  • Select an option

  • Save schwartz1375/e34eb85a427504bffacab7aaad62b6b8 to your computer and use it in GitHub Desktop.

Select an option

Save schwartz1375/e34eb85a427504bffacab7aaad62b6b8 to your computer and use it in GitHub Desktop.
Converts a Markdown file (with Mermaid diagrams) to PDF.
#!/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 ![diagram](./<name>-1.svg),
# 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))"
@schwartz1375

Copy link
Copy Markdown
Author

Public commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment