|
| 1 | +#!/usr/bin/env bash |
| 2 | +# render.sh - Render WG21 papers from markdown to HTML and PDF. |
| 3 | +# |
| 4 | +# Usage: render.sh <master-checkout> <rendered-worktree> |
| 5 | +# |
| 6 | +# Processes source/d*.md and archive/p*.md from the master checkout, |
| 7 | +# copies markdown into the rendered worktree, and generates HTML + PDF |
| 8 | +# in the rendered worktree root. Skips files whose content has not |
| 9 | +# changed since the last rendered commit. |
| 10 | +# |
| 11 | +# Can be run locally (Git Bash, macOS, Linux) or from CI. |
| 12 | + |
| 13 | +set -uo pipefail |
| 14 | + |
| 15 | +MASTER="${1:?Usage: render.sh <master-checkout> <rendered-worktree>}" |
| 16 | +RENDERED="${2:?Usage: render.sh <master-checkout> <rendered-worktree>}" |
| 17 | + |
| 18 | +# Resolve to absolute paths |
| 19 | +MASTER="$(cd "$MASTER" && pwd)" |
| 20 | +RENDERED="$(cd "$RENDERED" && pwd)" |
| 21 | +TOOLS="$MASTER/tools" |
| 22 | + |
| 23 | +# ── Detect platform ─────────────────────────────────────────── |
| 24 | + |
| 25 | +detect_chrome() { |
| 26 | + if [ "${OS:-}" = "Windows_NT" ]; then |
| 27 | + for candidate in \ |
| 28 | + "${LOCALAPPDATA:-}/Google/Chrome/Application/chrome.exe" \ |
| 29 | + "C:/Program Files/Google/Chrome/Application/chrome.exe" \ |
| 30 | + "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe"; do |
| 31 | + if [ -f "$candidate" ]; then |
| 32 | + echo "$candidate" |
| 33 | + return |
| 34 | + fi |
| 35 | + done |
| 36 | + elif [ "$(uname -s)" = "Darwin" ]; then |
| 37 | + for candidate in \ |
| 38 | + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ |
| 39 | + "/Applications/Chromium.app/Contents/MacOS/Chromium"; do |
| 40 | + if [ -x "$candidate" ]; then |
| 41 | + echo "$candidate" |
| 42 | + return |
| 43 | + fi |
| 44 | + done |
| 45 | + else |
| 46 | + for candidate in google-chrome chromium-browser chromium; do |
| 47 | + if command -v "$candidate" > /dev/null 2>&1; then |
| 48 | + echo "$candidate" |
| 49 | + return |
| 50 | + fi |
| 51 | + done |
| 52 | + fi |
| 53 | + return 1 |
| 54 | +} |
| 55 | + |
| 56 | +if [ "${OS:-}" = "Windows_NT" ]; then |
| 57 | + MERMAID_FILTER="mermaid-filter.cmd" |
| 58 | +else |
| 59 | + MERMAID_FILTER="mermaid-filter" |
| 60 | +fi |
| 61 | + |
| 62 | +CHROME="$(detect_chrome)" || { |
| 63 | + echo "ERROR: Google Chrome or Chromium not found" |
| 64 | + exit 1 |
| 65 | +} |
| 66 | + |
| 67 | +echo "Platform: $(uname -s)" |
| 68 | +echo "Chrome: $CHROME" |
| 69 | +echo "Mermaid filter: $MERMAID_FILTER" |
| 70 | +echo "Master: $MASTER" |
| 71 | +echo "Rendered: $RENDERED" |
| 72 | +echo "" |
| 73 | + |
| 74 | +# ── Track failures ──────────────────────────────────────────── |
| 75 | + |
| 76 | +FAILURES=0 |
| 77 | + |
| 78 | +# ── Helper: extract YAML document field ─────────────────────── |
| 79 | + |
| 80 | +extract_document_field() { |
| 81 | + sed -n '/^---$/,/^---$/p' "$1" \ |
| 82 | + | sed -n 's/^document:[[:space:]]*//p' \ |
| 83 | + | tr -d '[:space:]' |
| 84 | +} |
| 85 | + |
| 86 | +# ── Helper: render markdown to HTML ─────────────────────────── |
| 87 | + |
| 88 | +render_html() { |
| 89 | + local mdfile="$1" # relative to $RENDERED, e.g. source/d2583.md |
| 90 | + local htmlfile="$2" # relative to $RENDERED, e.g. d2583.html |
| 91 | + |
| 92 | + cp "$TOOLS/mermaid-config.json" "$RENDERED/.mermaid-config.json" |
| 93 | + ( |
| 94 | + cd "$RENDERED" |
| 95 | + MERMAID_FILTER_FORMAT=svg pandoc --standalone \ |
| 96 | + --filter "$MERMAID_FILTER" \ |
| 97 | + --embed-resources --toc \ |
| 98 | + --template="$TOOLS/wg21.html5" \ |
| 99 | + --css="$TOOLS/paperstyle.css" \ |
| 100 | + -o "$htmlfile" "$mdfile" |
| 101 | + ) |
| 102 | + local rc=$? |
| 103 | + rm -f "$RENDERED/.mermaid-config.json" |
| 104 | + [ ! -s "$RENDERED/mermaid-filter.err" ] \ |
| 105 | + && rm -f "$RENDERED/mermaid-filter.err" 2>/dev/null || true |
| 106 | + return $rc |
| 107 | +} |
| 108 | + |
| 109 | +# ── Helper: render HTML to PDF ──────────────────────────────── |
| 110 | + |
| 111 | +render_pdf() { |
| 112 | + local htmlfile="$1" # absolute path |
| 113 | + local pdffile="$2" # absolute path |
| 114 | + |
| 115 | + # On MSYS/Cygwin, convert to a Windows path for the file:// URL. |
| 116 | + # MSYS auto-converts bare path arguments but not paths embedded |
| 117 | + # inside a URL string. |
| 118 | + local html_url |
| 119 | + if command -v cygpath > /dev/null 2>&1; then |
| 120 | + html_url="file://$(cygpath -m "$htmlfile")" |
| 121 | + else |
| 122 | + html_url="file://$htmlfile" |
| 123 | + fi |
| 124 | + |
| 125 | + "$CHROME" --headless --no-pdf-header-footer \ |
| 126 | + --run-all-compositor-stages-before-draw \ |
| 127 | + --disable-gpu --no-sandbox \ |
| 128 | + --print-to-pdf="$pdffile" \ |
| 129 | + "$html_url" 2> >(grep -v 'dbus/' >&2) |
| 130 | +} |
| 131 | + |
| 132 | +# ── Process a directory ─────────────────────────────────────── |
| 133 | +# Args: $1 = subdir name ("source" or "archive") |
| 134 | +# $2 = glob pattern ("d*.md" or "p*.md") |
| 135 | +# $3 = "validate" or "skip" (YAML check) |
| 136 | + |
| 137 | +process_dir() { |
| 138 | + local subdir="$1" |
| 139 | + local glob="$2" |
| 140 | + local validate="$3" |
| 141 | + |
| 142 | + local src_dir="$MASTER/$subdir" |
| 143 | + local dst_dir="$RENDERED/$subdir" |
| 144 | + |
| 145 | + mkdir -p "$dst_dir" |
| 146 | + |
| 147 | + # Collect matching markdown files |
| 148 | + local md_files=() |
| 149 | + for f in "$src_dir"/$glob; do |
| 150 | + [ -f "$f" ] || continue |
| 151 | + md_files+=("$(basename "$f")") |
| 152 | + done |
| 153 | + |
| 154 | + if [ ${#md_files[@]} -eq 0 ]; then |
| 155 | + echo "No $glob files in $subdir/" |
| 156 | + return |
| 157 | + fi |
| 158 | + |
| 159 | + echo "=== Processing $subdir/ (${#md_files[@]} file(s)) ===" |
| 160 | + |
| 161 | + for mdname in "${md_files[@]}"; do |
| 162 | + local stem="${mdname%.md}" |
| 163 | + local md_dst="$dst_dir/$mdname" |
| 164 | + local html_dst="$RENDERED/$stem.html" |
| 165 | + local pdf_dst="$RENDERED/$stem.pdf" |
| 166 | + |
| 167 | + echo "" |
| 168 | + echo "-- $subdir/$mdname --" |
| 169 | + |
| 170 | + # Copy markdown to rendered worktree |
| 171 | + cp "$src_dir/$mdname" "$md_dst" |
| 172 | + |
| 173 | + # ── YAML validation (archive only) ──────────────── |
| 174 | + if [ "$validate" = "validate" ]; then |
| 175 | + local doc_field |
| 176 | + doc_field="$(extract_document_field "$md_dst")" || true |
| 177 | + local doc_lower |
| 178 | + doc_lower="$(echo "$doc_field" | tr '[:upper:]' '[:lower:]')" |
| 179 | + local file_prefix="${stem%%-*}" |
| 180 | + |
| 181 | + if [ -z "$doc_field" ]; then |
| 182 | + echo " " |
| 183 | + echo " " |
| 184 | + echo -e " \033[31m*** ERROR\033[0m" |
| 185 | + echo -e " \033[31m*** ERROR: no document field in YAML front matter\033[0m" |
| 186 | + echo -e " \033[31m*** ERROR\033[0m" |
| 187 | + echo " " |
| 188 | + echo " " |
| 189 | + git -C "$RENDERED" rm -f --ignore-unmatch "$(basename "$html_dst")" "$(basename "$pdf_dst")" 2>/dev/null || rm -f "$html_dst" "$pdf_dst" |
| 190 | + FAILURES=$((FAILURES + 1)) |
| 191 | + continue |
| 192 | + fi |
| 193 | + |
| 194 | + if [ "$doc_lower" != "$file_prefix" ]; then |
| 195 | + echo " " |
| 196 | + echo " " |
| 197 | + echo -e " \033[31m*** ERROR\033[0m" |
| 198 | + echo -e " \033[31m*** ERROR: document '$doc_field' does not match filename prefix '$file_prefix'\033[0m" |
| 199 | + echo -e " \033[31m*** ERROR\033[0m" |
| 200 | + echo " " |
| 201 | + echo " " |
| 202 | + git -C "$RENDERED" rm -f --ignore-unmatch "$(basename "$html_dst")" "$(basename "$pdf_dst")" 2>/dev/null || rm -f "$html_dst" "$pdf_dst" |
| 203 | + FAILURES=$((FAILURES + 1)) |
| 204 | + continue |
| 205 | + fi |
| 206 | + echo " YAML OK: $doc_field" |
| 207 | + fi |
| 208 | + |
| 209 | + # ── Detect if HTML needs regeneration ───────────── |
| 210 | + local need_html=false |
| 211 | + local need_pdf=false |
| 212 | + |
| 213 | + # Check if the markdown content changed in the rendered worktree |
| 214 | + if (cd "$RENDERED" && git diff --quiet -- "$subdir/$mdname") 2>/dev/null; then |
| 215 | + # File matches what is committed - but is it tracked at all? |
| 216 | + if (cd "$RENDERED" && git ls-files --error-unmatch "$subdir/$mdname") > /dev/null 2>&1; then |
| 217 | + echo " Markdown unchanged" |
| 218 | + else |
| 219 | + echo " New file" |
| 220 | + need_html=true |
| 221 | + fi |
| 222 | + else |
| 223 | + echo " Markdown changed" |
| 224 | + need_html=true |
| 225 | + fi |
| 226 | + |
| 227 | + # Force regeneration if HTML is missing |
| 228 | + if [ ! -f "$html_dst" ]; then |
| 229 | + echo " HTML missing" |
| 230 | + need_html=true |
| 231 | + fi |
| 232 | + |
| 233 | + # ── Render HTML if needed ───────────────────────── |
| 234 | + if [ "$need_html" = true ]; then |
| 235 | + echo " Generating HTML..." |
| 236 | + if ! render_html "$subdir/$mdname" "$stem.html"; then |
| 237 | + echo " ERROR: HTML generation failed for $mdname" |
| 238 | + git -C "$RENDERED" rm -f --ignore-unmatch "$(basename "$html_dst")" "$(basename "$pdf_dst")" 2>/dev/null || rm -f "$html_dst" "$pdf_dst" |
| 239 | + FAILURES=$((FAILURES + 1)) |
| 240 | + continue |
| 241 | + fi |
| 242 | + need_pdf=true |
| 243 | + fi |
| 244 | + |
| 245 | + # Force regeneration if PDF is missing |
| 246 | + if [ ! -f "$pdf_dst" ]; then |
| 247 | + echo " PDF missing" |
| 248 | + need_pdf=true |
| 249 | + fi |
| 250 | + |
| 251 | + # ── Render PDF if needed ────────────────────────── |
| 252 | + if [ "$need_pdf" = true ]; then |
| 253 | + echo " Generating PDF..." |
| 254 | + if ! render_pdf "$html_dst" "$pdf_dst"; then |
| 255 | + echo " ERROR: PDF generation failed for $mdname" |
| 256 | + git -C "$RENDERED" rm -f --ignore-unmatch "$(basename "$pdf_dst")" 2>/dev/null || rm -f "$pdf_dst" |
| 257 | + FAILURES=$((FAILURES + 1)) |
| 258 | + continue |
| 259 | + fi |
| 260 | + fi |
| 261 | + |
| 262 | + if [ "$need_html" = false ] && [ "$need_pdf" = false ]; then |
| 263 | + echo " Up to date" |
| 264 | + fi |
| 265 | + done |
| 266 | +} |
| 267 | + |
| 268 | +# ── Main ────────────────────────────────────────────────────── |
| 269 | + |
| 270 | +process_dir "source" "d*.md" "skip" |
| 271 | +echo "" |
| 272 | +process_dir "archive" "p*.md" "validate" |
| 273 | + |
| 274 | +echo "" |
| 275 | +if [ "$FAILURES" -gt 0 ]; then |
| 276 | + echo "Completed with $FAILURES failure(s)" |
| 277 | + exit 1 |
| 278 | +else |
| 279 | + echo "All papers rendered successfully" |
| 280 | +fi |
0 commit comments