#!/usr/bin/env bash # mm-pdf generate — render Markdown → HTML → PDF using nasr-m2o container set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" STYLE="${MM_PDF_STYLE:-dark}" OUT="" OUT_HTML="" CONTAINER="" INPUT="" usage() { echo "Usage: mm-pdf generate [--out output.pdf] [--style dark|purple|light] [--container NAME]" exit 1 } while [[ $# -gt 0 ]]; do case $1 in --out) OUT="$2"; shift 2 ;; --style) STYLE="$2"; shift 2 ;; --container) CONTAINER="$2"; shift 2 ;; --help|-h) usage ;; *) INPUT="$1"; shift ;; esac done [[ -z "$INPUT" ]] && usage [[ ! -f "$INPUT" ]] && { echo "Error: $INPUT not found"; exit 1; } INPUT_ABS="$(realpath "$INPUT")" BASENAME="${INPUT_ABS%.md}" OUT="${OUT:-${BASENAME}.pdf}" OUT_ABS="$(realpath -m "$OUT")" OUT_HTML_ABS="${OUT_ABS%.pdf}.html" # Auto-discover container if [[ -z "$CONTAINER" ]]; then CONTAINER=$(docker ps --format "{{.Names}}" | grep -E "nasr-m2o|m2o" | head -1) [[ -z "$CONTAINER" ]] && { echo "Error: no m2o container running. Start nasr-m2o or pass --container"; exit 1; } fi echo "Using container: $CONTAINER" # Copy files into container TMP="/tmp/mm-pdf-$$" docker exec "$CONTAINER" mkdir -p "$TMP" docker cp "$INPUT_ABS" "$CONTAINER:$TMP/input.md" if [[ -f "$SKILL_DIR/templates/${STYLE}.css" ]]; then docker cp "$SKILL_DIR/templates/${STYLE}.css" "$CONTAINER:$TMP/style.css" else docker cp "$SKILL_DIR/templates/dark.css" "$CONTAINER:$TMP/style.css" fi # Step 1: Markdown → styled HTML (via Node/marked) echo "Step 1: Converting Markdown → HTML..." docker exec "$CONTAINER" bash -c " cd $TMP # Install marked locally in tmp dir so require() can find it [ -d node_modules/marked ] || npm install marked --prefix . -q 2>/dev/null node - <<'JSEOF' const fs = require('fs'); const path = require('path'); const { marked } = require('marked'); const mdRaw = fs.readFileSync('input.md', 'utf8'); const css = fs.readFileSync('style.css', 'utf8'); // Strip YAML frontmatter let body = mdRaw; const fm = {}; const fmMatch = mdRaw.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); if (fmMatch) { fmMatch[1].split('\n').forEach(line => { const i = line.indexOf(':'); if (i > 0) fm[line.slice(0,i).trim().toLowerCase()] = line.slice(i+1).trim(); }); body = mdRaw.slice(fmMatch[0].length); } // Extract first H1 as title let title = fm.title || ''; if (!title) { const h1 = body.match(/^#\s+(.+)$/m); if (h1) { title = h1[1].trim(); body = body.replace(h1[0], ''); } } title = title || 'Machine.Machine'; const subtitle = fm.subtitle || fm.description || ''; const dateLine = [fm.client, fm.date].filter(Boolean).join(' \u00a0·\u00a0 '); marked.setOptions({ gfm: true, breaks: false }); const contentHtml = marked.parse(body); const coverHtml = \`
MACHINE.MACHINE

\${title}

\${subtitle ? '
' + subtitle + '
' : ''}
\${dateLine ? '
' + dateLine + '
' : ''}
\`; const html = \` \${title} \${coverHtml}
\${contentHtml}
\`; fs.writeFileSync('output.html', html); console.log('HTML written:', html.length, 'bytes'); JSEOF " echo "Step 2: Rendering HTML → PDF..." docker exec "$CONTAINER" bash -c " cd $TMP google-chrome \ --headless \ --no-sandbox \ --disable-gpu \ --disable-web-security \ --print-to-pdf=output.pdf \ --print-to-pdf-no-header \ --run-all-compositor-stages-before-draw \ --virtual-time-budget=5000 \ 'file://$TMP/output.html' " # Copy both outputs back docker cp "$CONTAINER:$TMP/output.html" "$OUT_HTML_ABS" docker cp "$CONTAINER:$TMP/output.pdf" "$OUT_ABS" docker exec "$CONTAINER" rm -rf "$TMP" echo "" echo "HTML: $OUT_HTML_ABS ($(du -h "$OUT_HTML_ABS" | cut -f1))" echo "PDF: $OUT_ABS ($(du -h "$OUT_ABS" | cut -f1))"