785 lines
30 KiB
Bash
Raw Normal View History

2026-05-20 21:39:12 +08:00
#!/usr/bin/env bash
# make.sh — minimax-pdf unified CLI
# Usage: bash make.sh <command> [options]
#
# Commands:
# check Verify all dependencies
# fix Auto-install missing dependencies
# run --title T --type TYPE Full pipeline → output.pdf
# --out FILE Output path (default: output.pdf)
# --author A --date D
# --subtitle S
# --abstract A Optional abstract text for cover
# --cover-image URL Optional cover image URL/path
# --content FILE Path to content.json (optional)
# demo Build a full-featured demo to demo.pdf
#
# Document types:
# report proposal resume portfolio academic general
# minimal stripe diagonal frame editorial
# magazine darkroom terminal poster
#
# Content block types:
# h1 h2 h3 body bullet numbered callout table
# image figure code math chart flowchart bibliography
# divider caption pagebreak spacer
#
# Exit codes: 0 success, 1 usage error, 2 dep missing, 3 runtime error
set -euo pipefail
SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NODE="node"
# ── Auto-detect Python (cross-platform) ────────────────────────────────────────
detect_python() {
local candidate
# 1. PATH 中的 python3 / python排除 Windows Store 空壳)
for candidate in python3 python; do
if command -v "$candidate" &>/dev/null \
&& "$candidate" -c "import sys; assert sys.version_info >= (3,9)" 2>/dev/null; then
echo "$candidate"; return
fi
done
# 2. Windows 常见安装路径(官方安装器 / Chocolatey / Scoop / Anaconda / Miniconda
local win_candidates=()
# 官方安装器 — 当前用户
for d in "/c/Users/$USER/AppData/Local/Programs/Python"/Python3*/; do
[[ -x "${d}python.exe" ]] && win_candidates+=("${d}python.exe")
done
# 官方安装器 — 全局
for d in "/c/Python3"*/ "/c/Program Files/Python3"*/; do
[[ -x "${d}python.exe" ]] && win_candidates+=("${d}python.exe")
done
# Chocolatey
[[ -x "/c/ProgramData/chocolatey/bin/python.exe" ]] && \
win_candidates+=("/c/ProgramData/chocolatey/bin/python.exe")
# Scoop
[[ -x "/c/Users/$USER/scoop/apps/python/current/python.exe" ]] && \
win_candidates+=("/c/Users/$USER/scoop/apps/python/current/python.exe")
# Anaconda / Miniconda
for d in "/c/Users/$USER/anaconda3" "/c/Users/$USER/miniconda3" \
"/c/ProgramData/anaconda3" "/c/ProgramData/miniconda3"; do
[[ -x "$d/python.exe" ]] && win_candidates+=("$d/python.exe")
done
for candidate in "${win_candidates[@]}"; do
if "$candidate" -c "import sys; assert sys.version_info >= (3,9)" 2>/dev/null; then
echo "$candidate"; return
fi
done
# 3. macOS Homebrew
for candidate in /opt/homebrew/bin/python3 /usr/local/bin/python3; do
if [[ -x "$candidate" ]] \
&& "$candidate" -c "import sys; assert sys.version_info >= (3,9)" 2>/dev/null; then
echo "$candidate"; return
fi
done
# 未找到 — 返回空字符串,由调用方处理
echo ""
}
PY="$(detect_python)"
# ── Interactive prompt helper ──────────────────────────────────────────────────
# 询问用户选择:自动安装 or 手动安装
# Usage: ask_install "Python" → sets REPLY to "auto" or "manual" or "skip"
ask_install() {
local dep_name="$1"
echo ""
bold " $dep_name is not installed. What would you like to do?"
echo " [1] Auto-install (let this script install it for you)"
echo " [2] Manual install (show me the instructions, I'll do it myself)"
echo " [3] Skip (continue without it)"
echo ""
read -rp " Choose [1/2/3]: " choice
case "$choice" in
1) REPLY="auto" ;;
2) REPLY="manual" ;;
*) REPLY="skip" ;;
esac
}
# ── Auto-install helpers ───────────────────────────────────────────────────────
install_python_auto() {
bold " Attempting to install Python..."
if command -v winget &>/dev/null; then
echo " Using: winget install Python.Python.3.12"
winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements 2>&1 | tail -5
elif command -v choco &>/dev/null; then
echo " Using: choco install python3"
choco install python3 -y 2>&1 | tail -5
elif command -v scoop &>/dev/null; then
echo " Using: scoop install python"
scoop install python 2>&1 | tail -5
elif command -v brew &>/dev/null; then
echo " Using: brew install python3"
brew install python3 2>&1 | tail -5
elif command -v apt-get &>/dev/null; then
echo " Using: sudo apt-get install python3 python3-pip"
sudo apt-get install -y python3 python3-pip 2>&1 | tail -5
elif command -v dnf &>/dev/null; then
echo " Using: sudo dnf install python3 python3-pip"
sudo dnf install -y python3 python3-pip 2>&1 | tail -5
else
red " No supported package manager found. Please install manually."
show_python_manual_instructions
return 1
fi
echo ""
green " Python installed. Please restart your terminal and re-run this script."
echo " (New PATH entries require a terminal restart to take effect)"
return 0
}
show_python_manual_instructions() {
echo ""
bold " Install Python 3.9+ manually:"
echo " Windows: winget install Python.Python.3.12"
echo " or https://www.python.org/downloads/"
echo " (IMPORTANT: check 'Add Python to PATH' during install)"
echo " macOS: brew install python3"
echo " Linux: sudo apt install python3 python3-pip (Debian/Ubuntu)"
echo " sudo dnf install python3 python3-pip (Fedora)"
echo ""
}
install_node_auto() {
bold " Attempting to install Node.js..."
if command -v winget &>/dev/null; then
echo " Using: winget install OpenJS.NodeJS.LTS"
winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements 2>&1 | tail -5
elif command -v choco &>/dev/null; then
echo " Using: choco install nodejs-lts"
choco install nodejs-lts -y 2>&1 | tail -5
elif command -v scoop &>/dev/null; then
echo " Using: scoop install nodejs-lts"
scoop install nodejs-lts 2>&1 | tail -5
elif command -v brew &>/dev/null; then
echo " Using: brew install node"
brew install node 2>&1 | tail -5
elif command -v apt-get &>/dev/null; then
echo " Using: sudo apt-get install nodejs npm"
sudo apt-get install -y nodejs npm 2>&1 | tail -5
else
red " No supported package manager found. Please install manually."
show_node_manual_instructions
return 1
fi
echo ""
green " Node.js installed. Please restart your terminal and re-run this script."
return 0
}
show_node_manual_instructions() {
echo ""
bold " Install Node.js 18+ manually:"
echo " Windows: winget install OpenJS.NodeJS.LTS"
echo " or https://nodejs.org/en/download/"
echo " macOS: brew install node"
echo " Linux: sudo apt install nodejs npm (Debian/Ubuntu)"
echo " or https://nodejs.org/en/download/"
echo ""
}
# ── Colour helpers ─────────────────────────────────────────────────────────────
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
# ── check ──────────────────────────────────────────────────────────────────────
cmd_check() {
local ok=true
bold "Checking dependencies..."
# Python
if [[ -n "$PY" ]] && $PY -c "import sys" 2>/dev/null; then
green " ✓ python ($PY) $($PY --version 2>&1 | awk '{print $2}')"
else
red " ✗ Python 3.9+ not found"
ask_install "Python"
case "$REPLY" in
auto) install_python_auto; exit $? ;;
manual) show_python_manual_instructions; exit 2 ;;
*) yellow " Skipped Python — PDF generation will not work without it." ;;
esac
ok=false
fi
# reportlab
if [[ -n "$PY" ]] && $PY -c "import reportlab" 2>/dev/null; then
green " ✓ reportlab"
else
yellow " ⚠ reportlab not installed (run: make.sh fix)"
ok=false
fi
# pypdf
if [[ -n "$PY" ]] && $PY -c "import pypdf" 2>/dev/null; then
green " ✓ pypdf"
else
yellow " ⚠ pypdf not installed (run: make.sh fix)"
ok=false
fi
# Node.js
if command -v node &>/dev/null; then
green " ✓ node $(node --version)"
else
red " ✗ Node.js not found"
ask_install "Node.js"
case "$REPLY" in
auto) install_node_auto; exit $? ;;
manual) show_node_manual_instructions; exit 2 ;;
*) yellow " Skipped Node.js — cover rendering will not work without it." ;;
esac
ok=false
fi
# Playwright + Browser (prefer local Chrome/Edge)
local browser_found=false
if node -e "
const pw = (() => { try { return require('playwright'); } catch(_) {
const {execSync} = require('child_process');
try { return require(execSync('npm root -g').toString().trim()+'/playwright'); } catch(_) { return null; }
}})();
if (!pw) process.exit(1);
(async () => {
for (const ch of ['chrome','msedge','chromium']) {
try { const b = await pw.chromium.launch({channel:ch}); await b.close(); console.log(ch); process.exit(0); } catch(_) {}
}
try { const b = await pw.chromium.launch(); await b.close(); console.log('bundled-chromium'); process.exit(0); } catch(_) {}
process.exit(1);
})();
" 2>/dev/null; then
green " ✓ playwright + browser detected"
browser_found=true
fi
if ! $browser_found; then
yellow " ⚠ playwright or browser not found (run: make.sh fix)"
ok=false
fi
# matplotlib (optional)
if [[ -n "$PY" ]] && $PY -c "import matplotlib" 2>/dev/null; then
green " ✓ matplotlib (math, chart, flowchart blocks enabled)"
else
yellow " ⚠ matplotlib not installed — math/chart/flowchart blocks degrade to text (run: make.sh fix)"
fi
if $ok; then
green "\nAll dependencies satisfied."
exit 0
else
yellow "\nSome dependencies missing. Run: bash make.sh fix"
exit 2
fi
}
# ── fix ────────────────────────────────────────────────────────────────────────
cmd_fix() {
bold "Installing missing dependencies..."
local rc=0
# Python
if [[ -n "$PY" ]] && $PY -c "import sys" 2>/dev/null; then
green " ✓ Python found ($PY)"
else
red " ✗ Python 3.9+ not found"
ask_install "Python"
case "$REPLY" in
auto)
install_python_auto || rc=2
if [[ $rc -ne 0 ]]; then
yellow " Auto-install may have failed. Please restart terminal and re-run."
exit $rc
fi
# Re-detect after install
PY="$(detect_python)"
if [[ -z "$PY" ]]; then
yellow " Python still not detected. Please restart terminal and re-run: bash make.sh fix"
exit 2
fi
;;
manual)
show_python_manual_instructions
echo " After installing Python, re-run: bash make.sh fix"
exit 2
;;
*)
yellow " Skipped Python — cannot install Python packages."
rc=2
;;
esac
fi
# Python packages
if [[ -n "$PY" ]] && $PY -c "import sys" 2>/dev/null; then
$PY -m pip install --break-system-packages -q reportlab pypdf matplotlib 2>/dev/null \
|| $PY -m pip install -q reportlab pypdf matplotlib 2>/dev/null \
|| { yellow " pip install failed — try: $PY -m pip install reportlab pypdf matplotlib"; rc=3; }
green " ✓ Python packages installed (reportlab, pypdf, matplotlib)"
fi
# Node.js
if ! command -v node &>/dev/null; then
red " ✗ Node.js not found"
ask_install "Node.js"
case "$REPLY" in
auto)
install_node_auto || rc=2
if [[ $rc -ne 0 ]]; then
yellow " Auto-install may have failed. Please restart terminal and re-run."
exit $rc
fi
;;
manual)
show_node_manual_instructions
echo " After installing Node.js, re-run: bash make.sh fix"
exit 2
;;
*)
yellow " Skipped Node.js — cover rendering will not work."
rc=2
;;
esac
fi
# Playwright + Browser: prefer local Chrome/Edge, only download Chromium as last resort
if command -v node &>/dev/null && command -v npm &>/dev/null; then
# Ensure playwright is installed
if ! node -e "require('playwright')" 2>/dev/null && \
! node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then
npm install -g playwright --silent 2>/dev/null || { yellow " playwright npm install failed"; rc=3; }
fi
# Check if local Chrome or Edge is available — skip Chromium download if so
local has_local_browser=false
for ch in chrome msedge; do
if node -e "
const pw = (() => { try { return require('playwright'); } catch(_) {
try { return require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright'); } catch(_) { return null; }
}})();
if (!pw) process.exit(1);
pw.chromium.launch({channel:'$ch'}).then(b => b.close().then(() => process.exit(0))).catch(() => process.exit(1));
" 2>/dev/null; then
green " ✓ Local browser detected ($ch) — skipping Chromium download"
has_local_browser=true
break
fi
done
if ! $has_local_browser; then
yellow " No local Chrome/Edge found — downloading Chromium..."
npx playwright install chromium --silent 2>/dev/null && \
green " ✓ Playwright Chromium installed" || \
{ yellow " Chromium install failed — install Chrome or Edge manually"; rc=3; }
fi
else
yellow " npm not found — cannot install Playwright automatically"
rc=2
fi
if [[ $rc -eq 0 ]]; then
green "\nAll dependencies installed. Run: bash make.sh check"
fi
exit $rc
}
# ── run ────────────────────────────────────────────────────────────────────────
cmd_run() {
# Pre-flight: ensure critical deps exist before doing any work
if [[ -z "$PY" ]] || ! $PY -c "import sys" 2>/dev/null; then
red "Error: Python 3.9+ is required but not found."
echo "Run: bash make.sh check (to diagnose and install)"
exit 2
fi
if ! command -v node &>/dev/null; then
red "Error: Node.js is required but not found."
echo "Run: bash make.sh check (to diagnose and install)"
exit 2
fi
local title="Untitled Document"
local type="general"
local author=""
local date=""
local subtitle=""
local abstract=""
local cover_image=""
local accent=""
local cover_bg=""
local content_file=""
local out="output.pdf"
local workdir
workdir="$(mktemp -d)"
# Parse options
while [[ $# -gt 0 ]]; do
case "$1" in
--title) title="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--author) author="$2"; shift 2 ;;
--date) date="$2"; shift 2 ;;
--subtitle) subtitle="$2"; shift 2 ;;
--abstract) abstract="$2"; shift 2 ;;
--cover-image) cover_image="$2"; shift 2 ;;
--accent) accent="$2"; shift 2 ;;
--cover-bg) cover_bg="$2"; shift 2 ;;
--content) content_file="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
bold "Building: $title"
echo " Type : $type"
echo " Output : $out"
# Step 1: tokens
echo ""
bold "Step 1/4 Generating design tokens..."
local accent_args=()
[[ -n "$accent" ]] && accent_args+=(--accent "$accent")
[[ -n "$cover_bg" ]] && accent_args+=(--cover-bg "$cover_bg")
$PY "$SCRIPTS/palette.py" \
--title "$title" --type "$type" \
--author "$author" --date "$date" \
--out "$workdir/tokens.json" \
"${accent_args[@]+"${accent_args[@]}"}"
# Inject optional cover fields into tokens.json
if [[ -n "$abstract" || -n "$cover_image" ]]; then
PDF_ABSTRACT="$abstract" PDF_COVER_IMAGE="$cover_image" PDF_TOKENS="$workdir/tokens.json" \
$PY - <<'PYEOF'
import json, os
with open(os.environ["PDF_TOKENS"]) as f:
t = json.load(f)
abstract = os.environ.get("PDF_ABSTRACT", "")
cover_image = os.environ.get("PDF_COVER_IMAGE", "")
if abstract:
t["abstract"] = abstract
if cover_image:
t["cover_image"] = cover_image
with open(os.environ["PDF_TOKENS"], "w") as f:
json.dump(t, f, indent=2)
PYEOF
fi
cat "$workdir/tokens.json" | $PY -c "
import json,sys
t=json.load(sys.stdin)
print(f' Mood : {t[\"mood\"]}')
print(f' Pattern : {t[\"cover_pattern\"]}')
print(f' Fonts : {t[\"font_display\"]} / {t[\"font_body\"]}')"
# Step 2: cover HTML + render
echo ""
bold "Step 2/4 Rendering cover..."
local subtitle_args=()
[[ -n "$subtitle" ]] && subtitle_args=(--subtitle "$subtitle")
$PY "$SCRIPTS/cover.py" \
--tokens "$workdir/tokens.json" \
--out "$workdir/cover.html" \
"${subtitle_args[@]+"${subtitle_args[@]}"}"
$NODE "$SCRIPTS/render_cover.js" \
--input "$workdir/cover.html" \
--out "$workdir/cover.pdf"
green " ✓ Cover rendered"
# Step 3: body
echo ""
bold "Step 3/4 Rendering body pages..."
if [[ -z "$content_file" ]]; then
# Generate a minimal placeholder body
cat > "$workdir/content.json" <<'JSON'
[
{"type":"h1", "text":"Document Body"},
{"type":"body", "text":"Replace this with your content.json file using --content path/to/content.json"},
{"type":"body", "text":"See the content.json schema in the skill README for the full list of supported block types: h1, h2, h3, body, bullet, callout, table, pagebreak, spacer."}
]
JSON
content_file="$workdir/content.json"
yellow " No content file provided — using placeholder body."
fi
# Validate content.json before rendering
if ! $PY -c "
import json, sys
try:
with open(sys.argv[1], encoding='utf-8') as f:
data = json.load(f)
blocks = data if isinstance(data, list) else data.get('blocks', data.get('content', []))
if not isinstance(blocks, list) or len(blocks) == 0:
print('Error: content.json must contain a non-empty array of blocks', file=sys.stderr)
sys.exit(1)
print(f' content.json validated: {len(blocks)} blocks')
except json.JSONDecodeError as e:
print(f'Error: content.json is not valid JSON — {e}', file=sys.stderr)
print(f'Hint: For long documents, write Markdown and use: make.sh reformat --input doc.md ...', file=sys.stderr)
sys.exit(1)
" "$content_file"; then
red " ✗ content.json validation failed"
exit 3
fi
$PY "$SCRIPTS/render_body.py" \
--tokens "$workdir/tokens.json" \
--content "$content_file" \
--out "$workdir/body.pdf"
green " ✓ Body rendered"
# Step 4: merge
echo ""
bold "Step 4/4 Merging and QA..."
$PY "$SCRIPTS/merge.py" \
--cover "$workdir/cover.pdf" \
--body "$workdir/body.pdf" \
--out "$out" \
--title "$title"
# Cleanup
rm -rf "$workdir"
}
# ── fill ──────────────────────────────────────────────────────────────────────
cmd_fill() {
local input="" out="" values="" data_file="" inspect_only=false
while [[ $# -gt 0 ]]; do
case "$1" in
--input) input="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;;
--values) values="$2"; shift 2 ;;
--data) data_file="$2"; shift 2 ;;
--inspect) inspect_only=true; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "$input" ]]; then
echo "Usage: make.sh fill --input form.pdf [--out filled.pdf] [--values '{...}'] [--data values.json] [--inspect]"
exit 1
fi
if $inspect_only || [[ -z "$out" && -z "$values" && -z "$data_file" ]]; then
bold "Inspecting form fields in: $input"
$PY "$SCRIPTS/fill_inspect.py" --input "$input"
return
fi
bold "Filling form: $input$out"
local val_args=""
if [[ -n "$values" ]]; then val_args="--values $values"; fi
if [[ -n "$data_file" ]]; then val_args="--data $data_file"; fi
$PY "$SCRIPTS/fill_write.py" --input "$input" --out "$out" $val_args
}
# ── reformat ───────────────────────────────────────────────────────────────────
cmd_reformat() {
local input="" title="Reformatted Document" type="general"
local author="" date="" out="output.pdf" subtitle=""
local tmpdir
tmpdir="$(mktemp -d)"
while [[ $# -gt 0 ]]; do
case "$1" in
--input) input="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--author) author="$2"; shift 2 ;;
--date) date="$2"; shift 2 ;;
--subtitle) subtitle="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "$input" ]]; then
echo "Usage: make.sh reformat --input source.md --title T --type TYPE --out output.pdf"
exit 1
fi
bold "Parsing: $input"
$PY "$SCRIPTS/reformat_parse.py" --input "$input" --out "$tmpdir/content.json"
green " ✓ Parsed to content.json"
bold "Applying design and building PDF..."
local sub_args=()
[[ -n "$subtitle" ]] && sub_args=(--subtitle "$subtitle")
cmd_run \
--title "$title" --type "$type" \
--author "$author" --date "$date" \
--content "$tmpdir/content.json" \
--out "$out" \
"${sub_args[@]+"${sub_args[@]}"}"
rm -rf "$tmpdir"
}
# ── demo ──────────────────────────────────────────────────────────────────────
cmd_demo() {
local tmpdir
tmpdir="$(mktemp -d)"
cat > "$tmpdir/content.json" <<'JSON'
[
{"type":"h1", "text":"Executive Summary"},
{"type":"body", "text":"This document was generated by minimax-pdf — a skill for creating visually polished PDFs. Every design decision is rooted in the document type and content, not a generic template."},
{"type":"callout", "text":"Key insight: design tokens flow from palette.py through every renderer, keeping cover and body visually consistent."},
{"type":"h1", "text":"How It Works"},
{"type":"h2", "text":"The Token Pipeline"},
{"type":"body", "text":"The palette.py script infers a color palette and typography pair from the document type. These tokens are written to tokens.json and consumed by every downstream script."},
{"type":"numbered","text":"palette.py generates color tokens, font selection, and the cover pattern"},
{"type":"numbered","text":"cover.py renders the cover HTML using the selected pattern"},
{"type":"numbered","text":"render_cover.js uses Playwright to convert the HTML cover to PDF"},
{"type":"numbered","text":"render_body.py builds inner pages from content.json using ReportLab"},
{"type":"numbered","text":"merge.py combines cover + body and runs final QA checks"},
{"type":"h2", "text":"Cover Patterns"},
{"type":"table",
"headers": ["Pattern", "Document type", "Visual character"],
"rows": [
["fullbleed", "report, general", "Deep background · dot-grid texture"],
["split", "proposal", "Left dark panel · right dot-grid"],
["typographic", "resume, academic", "Oversized display type · first-word accent"],
["atmospheric", "portfolio", "Dark bg · radial glow · dot-grid"],
["magazine", "magazine", "Cream bg · centered · hero image"],
["darkroom", "darkroom", "Navy bg · centered · grayscale image"],
["terminal", "terminal", "Near-black · grid lines · monospace"],
["poster", "poster", "White · thick sidebar · oversized title"]
]
},
{"type":"h1", "text":"Data Visualisation"},
{"type":"h2", "text":"Performance Metrics (Chart)"},
{"type":"body", "text":"Charts are rendered natively using matplotlib with a color palette derived from the document accent. No external chart services or image files required."},
{"type":"chart",
"chart_type": "bar",
"title": "Quarterly Performance",
"labels": ["Q1", "Q2", "Q3", "Q4"],
"datasets": [
{"label": "Revenue", "values": [120, 145, 132, 178]},
{"label": "Expenses", "values": [95, 108, 99, 122]}
],
"y_label": "USD (thousands)",
"caption": "Quarterly revenue vs. expenses"
},
{"type":"h2", "text":"Market Share (Pie Chart)"},
{"type":"chart",
"chart_type": "pie",
"labels": ["Product A", "Product B", "Product C", "Other"],
"datasets": [{"values": [42, 28, 18, 12]}],
"caption": "Annual market share by product line"
},
{"type":"pagebreak"},
{"type":"h1", "text":"Mathematics"},
{"type":"body", "text":"Display math is rendered via matplotlib mathtext — no LaTeX binary installation required. Inline references use standard [N] notation in body text."},
{"type":"math", "text":"E = mc^2", "label":"(1)"},
{"type":"math", "text":"\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", "label":"(2)"},
{"type":"math", "text":"\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}", "caption":"Basel problem (Euler, 1734)"},
{"type":"h1", "text":"Process Flow"},
{"type":"body", "text":"Flowcharts are drawn directly using matplotlib patches — no Graphviz or external tools needed. Supported node shapes: rect, diamond, oval, parallelogram."},
{"type":"flowchart",
"nodes": [
{"id":"start", "label":"Start", "shape":"oval"},
{"id":"input", "label":"Receive Input", "shape":"parallelogram"},
{"id":"valid", "label":"Valid?", "shape":"diamond"},
{"id":"proc", "label":"Process Data", "shape":"rect"},
{"id":"err", "label":"Return Error", "shape":"rect"},
{"id":"out", "label":"Return Result", "shape":"parallelogram"},
{"id":"end", "label":"End", "shape":"oval"}
],
"edges": [
{"from":"start", "to":"input"},
{"from":"input", "to":"valid"},
{"from":"valid", "to":"proc", "label":"Yes"},
{"from":"valid", "to":"err", "label":"No"},
{"from":"proc", "to":"out"},
{"from":"err", "to":"end"},
{"from":"out", "to":"end"}
],
"caption": "Data validation and processing flow"
},
{"type":"h1", "text":"Code Example"},
{"type":"code", "language":"python",
"text":"# Design token pipeline\ntokens = palette.build_tokens(\n title=\"Annual Report\",\n doc_type=\"report\",\n author=\"J. Smith\",\n date=\"March 2026\",\n)\nhtml = cover.render(tokens)\npdf = render_cover(html)"},
{"type":"h1", "text":"Design Principles"},
{"type":"body", "text":"The aesthetic system is documented in design/design.md. The core rule: every design decision must be rooted in the document content and purpose. A color chosen because it fits the content will always outperform a color chosen because it seems safe."},
{"type":"h2", "text":"Restraint over decoration"},
{"type":"body", "text":"The page is done when there is nothing left to remove. Accent color appears on section rules only — not on headings, not on bullets. No card components, no drop shadows."},
{"type":"callout", "text":"A PDF passes the quality bar when a designer would not be embarrassed to hand it to a client."},
{"type":"pagebreak"},
{"type":"bibliography",
"title": "References",
"items": [
{"id":"1","text":"Bringhurst, R. (2004). The Elements of Typographic Style (3rd ed.). Hartley & Marks."},
{"id":"2","text":"Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders."},
{"id":"3","text":"Hochuli, J. & Kinross, R. (1996). Designing Books: Practice and Theory. Hyphen Press."}
]
}
]
JSON
cmd_run \
--title "minimax-pdf demo" \
--type "report" \
--author "minimax-pdf skill" \
--date "$(date '+%B %Y')" \
--subtitle "A demonstration of the token-based design pipeline" \
--content "$tmpdir/content.json" \
--out "demo.pdf"
rm -rf "$tmpdir"
}
# ── dispatch ───────────────────────────────────────────────────────────────────
main() {
if [[ $# -lt 1 ]]; then
bold "minimax-pdf — make.sh"
echo ""
echo "Usage: bash make.sh <command> [options]"
echo ""
echo "Commands:"
echo " check Verify all dependencies"
echo " fix Auto-install missing deps"
echo " run --title T --type TYPE CREATE: full pipeline → PDF"
echo " [--author A] [--date D] [--subtitle S]"
echo " [--abstract A] [--cover-image URL]"
echo " [--accent #HEX] [--cover-bg #HEX]"
echo " [--content content.json] [--out output.pdf]"
echo " fill --input f.pdf FILL: inspect or fill form fields"
echo " reformat --input doc.md REFORMAT: parse doc → apply design → PDF"
echo " demo Build a full-featured demo PDF"
exit 0
fi
case "$1" in
check) cmd_check ;;
fix) cmd_fix ;;
run) shift; cmd_run "$@" ;;
fill) shift; cmd_fill "$@" ;;
reformat) shift; cmd_reformat "$@" ;;
demo) cmd_demo ;;
*) echo "Unknown command: $1"; exit 1 ;;
esac
}
main "$@"