|
| 1 | +#!/bin/bash |
| 2 | +# |
| 3 | +# Renders docs/zap-edit-architecture.md, with its mermaid diagrams, into |
| 4 | +# docs/zap-edit-architecture.pdf. |
| 5 | +# |
| 6 | +# The markdown is the source of truth and is what lives in git. The PDF is a |
| 7 | +# local convenience for reading away from GitHub (which renders the diagrams |
| 8 | +# itself) and is not committed. Regenerate it whenever the document changes. |
| 9 | +# |
| 10 | +# Requirements: |
| 11 | +# - Google Chrome or Chromium on PATH, used headless to lay out and print. |
| 12 | +# - Network access on the first run: marked and mermaid are fetched from a CDN. |
| 13 | +# |
| 14 | +# It has only ever been tested on Linux. |
| 15 | + |
| 16 | +set -u |
| 17 | + |
| 18 | +DOC=zap-edit-architecture |
| 19 | +HERE=$(cd "$(dirname "$0")" && pwd) |
| 20 | +SOURCE=${HERE}/${DOC}.md |
| 21 | +PDF=${HERE}/${DOC}.pdf |
| 22 | +WORK=$(mktemp -d) |
| 23 | +trap 'rm -rf "${WORK}"' EXIT |
| 24 | + |
| 25 | +CHROME="" |
| 26 | +for candidate in google-chrome chromium chromium-browser google-chrome-stable; do |
| 27 | + if command -v ${candidate} >/dev/null 2>&1; then |
| 28 | + CHROME=${candidate} |
| 29 | + break |
| 30 | + fi |
| 31 | +done |
| 32 | + |
| 33 | +if [ -z "${CHROME}" ]; then |
| 34 | + echo "Can't find Chrome or Chromium on PATH." |
| 35 | + echo "One of them is needed to lay out the document and print it to PDF." |
| 36 | + exit 1 |
| 37 | +fi |
| 38 | + |
| 39 | +if [ ! -s "${SOURCE}" ]; then |
| 40 | + echo "Can't find ${SOURCE}." |
| 41 | + exit 1 |
| 42 | +fi |
| 43 | + |
| 44 | +echo "Building ${PDF} from ${SOURCE} using ${CHROME} ..." |
| 45 | + |
| 46 | +# The markdown is inlined into the page rather than fetched, so that the render |
| 47 | +# does not depend on how the browser treats local files. |
| 48 | +node -e ' |
| 49 | +const fs = require("fs") |
| 50 | +const markdown = fs.readFileSync(process.argv[1], "utf8") |
| 51 | +const css = ` |
| 52 | + :root { --ink:#1a1a1a; --muted:#5b6472; --rule:#d7dce3; --accent:#0b5cad; } |
| 53 | + * { box-sizing: border-box } |
| 54 | + body { margin:0 auto; max-width:190mm; color:var(--ink); background:#fff; |
| 55 | + font-family:Charter,Georgia,"DejaVu Serif",serif; font-size:10.2pt; line-height:1.5 } |
| 56 | + h1 { font-family:"Helvetica Neue",Helvetica,"DejaVu Sans",sans-serif; font-size:24pt; |
| 57 | + line-height:1.15; letter-spacing:-0.01em; margin:0 0 4mm; padding-bottom:3mm; |
| 58 | + border-bottom:2px solid var(--ink) } |
| 59 | + h2 { font-family:"Helvetica Neue",Helvetica,"DejaVu Sans",sans-serif; font-size:14pt; |
| 60 | + margin:9mm 0 3mm; padding-bottom:1.5mm; border-bottom:1px solid var(--rule); break-after:avoid } |
| 61 | + h3 { font-family:"Helvetica Neue",Helvetica,"DejaVu Sans",sans-serif; font-size:11.5pt; |
| 62 | + margin:6mm 0 2mm; break-after:avoid } |
| 63 | + h4 { font-family:"Helvetica Neue",Helvetica,"DejaVu Sans",sans-serif; font-size:10.2pt; |
| 64 | + margin:5mm 0 1.5mm; color:var(--muted); break-after:avoid } |
| 65 | + p { margin:0 0 3mm } |
| 66 | + ul, ol { margin:0 0 3mm; padding-left:6mm } li { margin-bottom:1mm } |
| 67 | + a { color:var(--accent); text-decoration:none } |
| 68 | + code, kbd { font-family:"DejaVu Sans Mono","Liberation Mono",monospace; font-size:8.8pt; |
| 69 | + background:#f4f6f8; padding:0.4mm 1mm; border-radius:2px } |
| 70 | + pre { background:#f7f8fa; border:1px solid var(--rule); border-left:3px solid var(--accent); |
| 71 | + padding:3mm 4mm; margin:0 0 4mm; break-inside:avoid; border-radius:3px } |
| 72 | + pre code { background:none; padding:0; font-size:8.4pt; line-height:1.45; white-space:pre-wrap } |
| 73 | + table { border-collapse:collapse; width:100%; margin:0 0 4mm; font-size:8.9pt; break-inside:avoid } |
| 74 | + th, td { border:1px solid var(--rule); padding:1.6mm 2.2mm; text-align:left; vertical-align:top } |
| 75 | + th { background:#eef1f5; font-family:"Helvetica Neue",Helvetica,"DejaVu Sans",sans-serif; |
| 76 | + font-size:8.4pt; text-transform:uppercase; letter-spacing:0.03em; color:var(--muted) } |
| 77 | + tr:nth-child(even) td { background:#fafbfc } |
| 78 | + hr { border:0; border-top:1px solid var(--rule); margin:7mm 0 } |
| 79 | + .mermaid { text-align:center; margin:0 0 5mm; break-inside:avoid; background:#fbfcfd; |
| 80 | + border:1px solid var(--rule); border-radius:3px; padding:3mm } |
| 81 | + .mermaid svg { max-width:100%; height:auto } |
| 82 | + em { color:var(--muted) } |
| 83 | + @page { size:A4; margin:16mm 12mm } |
| 84 | + @media print { body { max-width:none } h1 { break-after:avoid } } |
| 85 | +` |
| 86 | +const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"> |
| 87 | +<title>The zap edit CLI: architecture</title><style>${css}</style></head> |
| 88 | +<body><div id="doc"></div> |
| 89 | +<script src="https://cdn.jsdelivr.net/npm/marked@14.1.2/marked.min.js"></script> |
| 90 | +<script type="module"> |
| 91 | +import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11.4.0/dist/mermaid.esm.min.mjs" |
| 92 | +const markdown = ${JSON.stringify(markdown)} |
| 93 | +const renderer = new marked.Renderer() |
| 94 | +const code = renderer.code.bind(renderer) |
| 95 | +renderer.code = (token) => |
| 96 | + token.lang === "mermaid" |
| 97 | + ? "<pre class=\\"mermaid\\">" + token.text + "</pre>" |
| 98 | + : code(token) |
| 99 | +document.getElementById("doc").innerHTML = marked.parse(markdown, { renderer }) |
| 100 | +mermaid.initialize({ |
| 101 | + startOnLoad: false, theme: "base", |
| 102 | + themeVariables: { |
| 103 | + background: "#fbfcfd", primaryColor: "#eef4fb", primaryBorderColor: "#0b5cad", |
| 104 | + primaryTextColor: "#1a1a1a", lineColor: "#5b6472", secondaryColor: "#f2f4f7", |
| 105 | + tertiaryColor: "#ffffff", |
| 106 | + fontFamily: "Helvetica Neue, Helvetica, DejaVu Sans, sans-serif", fontSize: "13px" |
| 107 | + }, |
| 108 | + flowchart: { curve: "basis", useMaxWidth: true, htmlLabels: true }, |
| 109 | + sequence: { useMaxWidth: true, mirrorActors: false } |
| 110 | +}) |
| 111 | +await mermaid.run({ querySelector: ".mermaid" }) |
| 112 | +</script></body></html>` |
| 113 | +fs.writeFileSync(process.argv[2], page) |
| 114 | +' "${SOURCE}" "${WORK}/${DOC}.html" |
| 115 | + |
| 116 | +if [ ! -s "${WORK}/${DOC}.html" ]; then |
| 117 | + echo "Failed to build the intermediate page." |
| 118 | + exit 1 |
| 119 | +fi |
| 120 | + |
| 121 | +# The virtual time budget gives the fetched libraries and the diagrams time to |
| 122 | +# finish before the page is printed. Headless Chrome has been seen to write the |
| 123 | +# file and then not exit, so it is given a deadline and judged by its output. |
| 124 | +rm -f "${PDF}" |
| 125 | +timeout 240 ${CHROME} --headless=new --disable-gpu --no-sandbox \ |
| 126 | + --no-pdf-header-footer --virtual-time-budget=30000 \ |
| 127 | + --print-to-pdf="${PDF}" "${WORK}/${DOC}.html" 2>/dev/null |
| 128 | + |
| 129 | +if [ ! -s "${PDF}" ]; then |
| 130 | + echo "Chrome did not produce ${PDF}." |
| 131 | + echo "If this machine has no network access, the diagram library cannot be fetched." |
| 132 | + exit 1 |
| 133 | +fi |
| 134 | + |
| 135 | +echo "Wrote ${PDF} ($(du -h "${PDF}" | cut -f1))" |
0 commit comments