785 lines
30 KiB
Bash
785 lines
30 KiB
Bash
#!/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 "$@"
|