make.sh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. #!/usr/bin/env bash
  2. # make.sh — minimax-pdf unified CLI
  3. # Usage: bash make.sh <command> [options]
  4. #
  5. # Commands:
  6. # check Verify all dependencies
  7. # fix Auto-install missing dependencies
  8. # run --title T --type TYPE Full pipeline → output.pdf
  9. # --out FILE Output path (default: output.pdf)
  10. # --author A --date D
  11. # --subtitle S
  12. # --abstract A Optional abstract text for cover
  13. # --cover-image URL Optional cover image URL/path
  14. # --content FILE Path to content.json (optional)
  15. # demo Build a full-featured demo to demo.pdf
  16. #
  17. # Document types:
  18. # report proposal resume portfolio academic general
  19. # minimal stripe diagonal frame editorial
  20. # magazine darkroom terminal poster
  21. #
  22. # Content block types:
  23. # h1 h2 h3 body bullet numbered callout table
  24. # image figure code math chart flowchart bibliography
  25. # divider caption pagebreak spacer
  26. #
  27. # Exit codes: 0 success, 1 usage error, 2 dep missing, 3 runtime error
  28. set -euo pipefail
  29. SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  30. PY="python3"
  31. NODE="node"
  32. # ── Colour helpers ─────────────────────────────────────────────────────────────
  33. red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
  34. green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
  35. yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
  36. bold() { printf '\033[1m%s\033[0m\n' "$*"; }
  37. # ── check ──────────────────────────────────────────────────────────────────────
  38. cmd_check() {
  39. local ok=true
  40. bold "Checking dependencies..."
  41. # Python
  42. if command -v python3 &>/dev/null; then
  43. green " ✓ python3 $(python3 --version 2>&1 | awk '{print $2}')"
  44. else
  45. red " ✗ python3 not found"
  46. ok=false
  47. fi
  48. # reportlab
  49. if python3 -c "import reportlab" 2>/dev/null; then
  50. green " ✓ reportlab"
  51. else
  52. yellow " ⚠ reportlab not installed (run: make.sh fix)"
  53. ok=false
  54. fi
  55. # pypdf
  56. if python3 -c "import pypdf" 2>/dev/null; then
  57. green " ✓ pypdf"
  58. else
  59. yellow " ⚠ pypdf not installed (run: make.sh fix)"
  60. ok=false
  61. fi
  62. # Node.js
  63. if command -v node &>/dev/null; then
  64. green " ✓ node $(node --version)"
  65. else
  66. red " ✗ node not found — cover rendering unavailable"
  67. ok=false
  68. fi
  69. # Playwright
  70. if node -e "require('playwright')" 2>/dev/null || \
  71. node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then
  72. green " ✓ playwright"
  73. else
  74. yellow " ⚠ playwright not found (run: make.sh fix)"
  75. ok=false
  76. fi
  77. # matplotlib (optional — required for math/chart/flowchart; degrades gracefully)
  78. if python3 -c "import matplotlib" 2>/dev/null; then
  79. green " ✓ matplotlib (math, chart, flowchart blocks enabled)"
  80. else
  81. yellow " ⚠ matplotlib not installed — math/chart/flowchart blocks degrade to text (run: make.sh fix)"
  82. fi
  83. if $ok; then
  84. green "\nAll dependencies satisfied."
  85. exit 0
  86. else
  87. yellow "\nSome dependencies missing. Run: bash make.sh fix"
  88. exit 2
  89. fi
  90. }
  91. # ── fix ────────────────────────────────────────────────────────────────────────
  92. cmd_fix() {
  93. bold "Installing missing dependencies..."
  94. local rc=0
  95. # Python packages
  96. if command -v python3 &>/dev/null; then
  97. python3 -m pip install --break-system-packages -q reportlab pypdf matplotlib 2>/dev/null \
  98. || python3 -m pip install -q reportlab pypdf matplotlib 2>/dev/null \
  99. || { yellow " pip install failed — try: pip install reportlab pypdf matplotlib"; rc=3; }
  100. green " ✓ Python packages installed (reportlab, pypdf, matplotlib)"
  101. fi
  102. # Playwright
  103. if command -v npm &>/dev/null; then
  104. npm install -g playwright --silent 2>/dev/null && \
  105. npx playwright install chromium --silent 2>/dev/null && \
  106. green " ✓ Playwright + Chromium installed" || \
  107. { yellow " playwright install failed — try manually"; rc=3; }
  108. else
  109. yellow " npm not found — cannot install Playwright automatically"
  110. rc=2
  111. fi
  112. if [[ $rc -eq 0 ]]; then
  113. green "\nAll dependencies installed. Run: bash make.sh check"
  114. fi
  115. exit $rc
  116. }
  117. # ── run ────────────────────────────────────────────────────────────────────────
  118. cmd_run() {
  119. local title="Untitled Document"
  120. local type="general"
  121. local author=""
  122. local date=""
  123. local subtitle=""
  124. local abstract=""
  125. local cover_image=""
  126. local accent=""
  127. local cover_bg=""
  128. local content_file=""
  129. local out="output.pdf"
  130. local workdir
  131. workdir="$(mktemp -d)"
  132. # Parse options
  133. while [[ $# -gt 0 ]]; do
  134. case "$1" in
  135. --title) title="$2"; shift 2 ;;
  136. --type) type="$2"; shift 2 ;;
  137. --author) author="$2"; shift 2 ;;
  138. --date) date="$2"; shift 2 ;;
  139. --subtitle) subtitle="$2"; shift 2 ;;
  140. --abstract) abstract="$2"; shift 2 ;;
  141. --cover-image) cover_image="$2"; shift 2 ;;
  142. --accent) accent="$2"; shift 2 ;;
  143. --cover-bg) cover_bg="$2"; shift 2 ;;
  144. --content) content_file="$2"; shift 2 ;;
  145. --out) out="$2"; shift 2 ;;
  146. *) echo "Unknown option: $1"; exit 1 ;;
  147. esac
  148. done
  149. bold "Building: $title"
  150. echo " Type : $type"
  151. echo " Output : $out"
  152. # Step 1: tokens
  153. echo ""
  154. bold "Step 1/4 Generating design tokens..."
  155. local accent_args=()
  156. [[ -n "$accent" ]] && accent_args+=(--accent "$accent")
  157. [[ -n "$cover_bg" ]] && accent_args+=(--cover-bg "$cover_bg")
  158. $PY "$SCRIPTS/palette.py" \
  159. --title "$title" --type "$type" \
  160. --author "$author" --date "$date" \
  161. --out "$workdir/tokens.json" \
  162. "${accent_args[@]+"${accent_args[@]}"}"
  163. # Inject optional cover fields into tokens.json
  164. if [[ -n "$abstract" || -n "$cover_image" ]]; then
  165. PDF_ABSTRACT="$abstract" PDF_COVER_IMAGE="$cover_image" PDF_TOKENS="$workdir/tokens.json" \
  166. $PY - <<'PYEOF'
  167. import json, os
  168. with open(os.environ["PDF_TOKENS"]) as f:
  169. t = json.load(f)
  170. abstract = os.environ.get("PDF_ABSTRACT", "")
  171. cover_image = os.environ.get("PDF_COVER_IMAGE", "")
  172. if abstract:
  173. t["abstract"] = abstract
  174. if cover_image:
  175. t["cover_image"] = cover_image
  176. with open(os.environ["PDF_TOKENS"], "w") as f:
  177. json.dump(t, f, indent=2)
  178. PYEOF
  179. fi
  180. cat "$workdir/tokens.json" | $PY -c "
  181. import json,sys
  182. t=json.load(sys.stdin)
  183. print(f' Mood : {t[\"mood\"]}')
  184. print(f' Pattern : {t[\"cover_pattern\"]}')
  185. print(f' Fonts : {t[\"font_display\"]} / {t[\"font_body\"]}')"
  186. # Step 2: cover HTML + render
  187. echo ""
  188. bold "Step 2/4 Rendering cover..."
  189. local subtitle_args=()
  190. [[ -n "$subtitle" ]] && subtitle_args=(--subtitle "$subtitle")
  191. $PY "$SCRIPTS/cover.py" \
  192. --tokens "$workdir/tokens.json" \
  193. --out "$workdir/cover.html" \
  194. "${subtitle_args[@]+"${subtitle_args[@]}"}"
  195. $NODE "$SCRIPTS/render_cover.js" \
  196. --input "$workdir/cover.html" \
  197. --out "$workdir/cover.pdf"
  198. green " ✓ Cover rendered"
  199. # Step 3: body
  200. echo ""
  201. bold "Step 3/4 Rendering body pages..."
  202. if [[ -z "$content_file" ]]; then
  203. # Generate a minimal placeholder body
  204. cat > "$workdir/content.json" <<'JSON'
  205. [
  206. {"type":"h1", "text":"Document Body"},
  207. {"type":"body", "text":"Replace this with your content.json file using --content path/to/content.json"},
  208. {"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."}
  209. ]
  210. JSON
  211. content_file="$workdir/content.json"
  212. yellow " No content file provided — using placeholder body."
  213. fi
  214. $PY "$SCRIPTS/render_body.py" \
  215. --tokens "$workdir/tokens.json" \
  216. --content "$content_file" \
  217. --out "$workdir/body.pdf"
  218. green " ✓ Body rendered"
  219. # Step 4: merge
  220. echo ""
  221. bold "Step 4/4 Merging and QA..."
  222. $PY "$SCRIPTS/merge.py" \
  223. --cover "$workdir/cover.pdf" \
  224. --body "$workdir/body.pdf" \
  225. --out "$out" \
  226. --title "$title"
  227. # Cleanup
  228. rm -rf "$workdir"
  229. }
  230. # ── fill ──────────────────────────────────────────────────────────────────────
  231. cmd_fill() {
  232. local input="" out="" values="" data_file="" inspect_only=false
  233. while [[ $# -gt 0 ]]; do
  234. case "$1" in
  235. --input) input="$2"; shift 2 ;;
  236. --out) out="$2"; shift 2 ;;
  237. --values) values="$2"; shift 2 ;;
  238. --data) data_file="$2"; shift 2 ;;
  239. --inspect) inspect_only=true; shift ;;
  240. *) echo "Unknown option: $1"; exit 1 ;;
  241. esac
  242. done
  243. if [[ -z "$input" ]]; then
  244. echo "Usage: make.sh fill --input form.pdf [--out filled.pdf] [--values '{...}'] [--data values.json] [--inspect]"
  245. exit 1
  246. fi
  247. if $inspect_only || [[ -z "$out" && -z "$values" && -z "$data_file" ]]; then
  248. bold "Inspecting form fields in: $input"
  249. $PY "$SCRIPTS/fill_inspect.py" --input "$input"
  250. return
  251. fi
  252. bold "Filling form: $input → $out"
  253. local val_args=""
  254. if [[ -n "$values" ]]; then val_args="--values $values"; fi
  255. if [[ -n "$data_file" ]]; then val_args="--data $data_file"; fi
  256. $PY "$SCRIPTS/fill_write.py" --input "$input" --out "$out" $val_args
  257. }
  258. # ── reformat ───────────────────────────────────────────────────────────────────
  259. cmd_reformat() {
  260. local input="" title="Reformatted Document" type="general"
  261. local author="" date="" out="output.pdf" subtitle=""
  262. local tmpdir
  263. tmpdir="$(mktemp -d)"
  264. while [[ $# -gt 0 ]]; do
  265. case "$1" in
  266. --input) input="$2"; shift 2 ;;
  267. --title) title="$2"; shift 2 ;;
  268. --type) type="$2"; shift 2 ;;
  269. --author) author="$2"; shift 2 ;;
  270. --date) date="$2"; shift 2 ;;
  271. --subtitle) subtitle="$2"; shift 2 ;;
  272. --out) out="$2"; shift 2 ;;
  273. *) echo "Unknown option: $1"; exit 1 ;;
  274. esac
  275. done
  276. if [[ -z "$input" ]]; then
  277. echo "Usage: make.sh reformat --input source.md --title T --type TYPE --out output.pdf"
  278. exit 1
  279. fi
  280. bold "Parsing: $input"
  281. $PY "$SCRIPTS/reformat_parse.py" --input "$input" --out "$tmpdir/content.json"
  282. green " ✓ Parsed to content.json"
  283. bold "Applying design and building PDF..."
  284. local sub_args=()
  285. [[ -n "$subtitle" ]] && sub_args=(--subtitle "$subtitle")
  286. cmd_run \
  287. --title "$title" --type "$type" \
  288. --author "$author" --date "$date" \
  289. --content "$tmpdir/content.json" \
  290. --out "$out" \
  291. "${sub_args[@]+"${sub_args[@]}"}"
  292. rm -rf "$tmpdir"
  293. }
  294. # ── demo ──────────────────────────────────────────────────────────────────────
  295. cmd_demo() {
  296. local tmpdir
  297. tmpdir="$(mktemp -d)"
  298. cat > "$tmpdir/content.json" <<'JSON'
  299. [
  300. {"type":"h1", "text":"Executive Summary"},
  301. {"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."},
  302. {"type":"callout", "text":"Key insight: design tokens flow from palette.py through every renderer, keeping cover and body visually consistent."},
  303. {"type":"h1", "text":"How It Works"},
  304. {"type":"h2", "text":"The Token Pipeline"},
  305. {"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."},
  306. {"type":"numbered","text":"palette.py generates color tokens, font selection, and the cover pattern"},
  307. {"type":"numbered","text":"cover.py renders the cover HTML using the selected pattern"},
  308. {"type":"numbered","text":"render_cover.js uses Playwright to convert the HTML cover to PDF"},
  309. {"type":"numbered","text":"render_body.py builds inner pages from content.json using ReportLab"},
  310. {"type":"numbered","text":"merge.py combines cover + body and runs final QA checks"},
  311. {"type":"h2", "text":"Cover Patterns"},
  312. {"type":"table",
  313. "headers": ["Pattern", "Document type", "Visual character"],
  314. "rows": [
  315. ["fullbleed", "report, general", "Deep background · dot-grid texture"],
  316. ["split", "proposal", "Left dark panel · right dot-grid"],
  317. ["typographic", "resume, academic", "Oversized display type · first-word accent"],
  318. ["atmospheric", "portfolio", "Dark bg · radial glow · dot-grid"],
  319. ["magazine", "magazine", "Cream bg · centered · hero image"],
  320. ["darkroom", "darkroom", "Navy bg · centered · grayscale image"],
  321. ["terminal", "terminal", "Near-black · grid lines · monospace"],
  322. ["poster", "poster", "White · thick sidebar · oversized title"]
  323. ]
  324. },
  325. {"type":"h1", "text":"Data Visualisation"},
  326. {"type":"h2", "text":"Performance Metrics (Chart)"},
  327. {"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."},
  328. {"type":"chart",
  329. "chart_type": "bar",
  330. "title": "Quarterly Performance",
  331. "labels": ["Q1", "Q2", "Q3", "Q4"],
  332. "datasets": [
  333. {"label": "Revenue", "values": [120, 145, 132, 178]},
  334. {"label": "Expenses", "values": [95, 108, 99, 122]}
  335. ],
  336. "y_label": "USD (thousands)",
  337. "caption": "Quarterly revenue vs. expenses"
  338. },
  339. {"type":"h2", "text":"Market Share (Pie Chart)"},
  340. {"type":"chart",
  341. "chart_type": "pie",
  342. "labels": ["Product A", "Product B", "Product C", "Other"],
  343. "datasets": [{"values": [42, 28, 18, 12]}],
  344. "caption": "Annual market share by product line"
  345. },
  346. {"type":"pagebreak"},
  347. {"type":"h1", "text":"Mathematics"},
  348. {"type":"body", "text":"Display math is rendered via matplotlib mathtext — no LaTeX binary installation required. Inline references use standard [N] notation in body text."},
  349. {"type":"math", "text":"E = mc^2", "label":"(1)"},
  350. {"type":"math", "text":"\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", "label":"(2)"},
  351. {"type":"math", "text":"\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}", "caption":"Basel problem (Euler, 1734)"},
  352. {"type":"h1", "text":"Process Flow"},
  353. {"type":"body", "text":"Flowcharts are drawn directly using matplotlib patches — no Graphviz or external tools needed. Supported node shapes: rect, diamond, oval, parallelogram."},
  354. {"type":"flowchart",
  355. "nodes": [
  356. {"id":"start", "label":"Start", "shape":"oval"},
  357. {"id":"input", "label":"Receive Input", "shape":"parallelogram"},
  358. {"id":"valid", "label":"Valid?", "shape":"diamond"},
  359. {"id":"proc", "label":"Process Data", "shape":"rect"},
  360. {"id":"err", "label":"Return Error", "shape":"rect"},
  361. {"id":"out", "label":"Return Result", "shape":"parallelogram"},
  362. {"id":"end", "label":"End", "shape":"oval"}
  363. ],
  364. "edges": [
  365. {"from":"start", "to":"input"},
  366. {"from":"input", "to":"valid"},
  367. {"from":"valid", "to":"proc", "label":"Yes"},
  368. {"from":"valid", "to":"err", "label":"No"},
  369. {"from":"proc", "to":"out"},
  370. {"from":"err", "to":"end"},
  371. {"from":"out", "to":"end"}
  372. ],
  373. "caption": "Data validation and processing flow"
  374. },
  375. {"type":"h1", "text":"Code Example"},
  376. {"type":"code", "language":"python",
  377. "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)"},
  378. {"type":"h1", "text":"Design Principles"},
  379. {"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."},
  380. {"type":"h2", "text":"Restraint over decoration"},
  381. {"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."},
  382. {"type":"callout", "text":"A PDF passes the quality bar when a designer would not be embarrassed to hand it to a client."},
  383. {"type":"pagebreak"},
  384. {"type":"bibliography",
  385. "title": "References",
  386. "items": [
  387. {"id":"1","text":"Bringhurst, R. (2004). The Elements of Typographic Style (3rd ed.). Hartley & Marks."},
  388. {"id":"2","text":"Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders."},
  389. {"id":"3","text":"Hochuli, J. & Kinross, R. (1996). Designing Books: Practice and Theory. Hyphen Press."}
  390. ]
  391. }
  392. ]
  393. JSON
  394. cmd_run \
  395. --title "minimax-pdf demo" \
  396. --type "report" \
  397. --author "minimax-pdf skill" \
  398. --date "$(date '+%B %Y')" \
  399. --subtitle "A demonstration of the token-based design pipeline" \
  400. --content "$tmpdir/content.json" \
  401. --out "demo.pdf"
  402. rm -rf "$tmpdir"
  403. }
  404. # ── dispatch ───────────────────────────────────────────────────────────────────
  405. main() {
  406. if [[ $# -lt 1 ]]; then
  407. bold "minimax-pdf — make.sh"
  408. echo ""
  409. echo "Usage: bash make.sh <command> [options]"
  410. echo ""
  411. echo "Commands:"
  412. echo " check Verify all dependencies"
  413. echo " fix Auto-install missing deps"
  414. echo " run --title T --type TYPE CREATE: full pipeline → PDF"
  415. echo " [--author A] [--date D] [--subtitle S]"
  416. echo " [--abstract A] [--cover-image URL]"
  417. echo " [--accent #HEX] [--cover-bg #HEX]"
  418. echo " [--content content.json] [--out output.pdf]"
  419. echo " fill --input f.pdf FILL: inspect or fill form fields"
  420. echo " reformat --input doc.md REFORMAT: parse doc → apply design → PDF"
  421. echo " demo Build a full-featured demo PDF"
  422. exit 0
  423. fi
  424. case "$1" in
  425. check) cmd_check ;;
  426. fix) cmd_fix ;;
  427. run) shift; cmd_run "$@" ;;
  428. fill) shift; cmd_fill "$@" ;;
  429. reformat) shift; cmd_reformat "$@" ;;
  430. demo) cmd_demo ;;
  431. *) echo "Unknown command: $1"; exit 1 ;;
  432. esac
  433. }
  434. main "$@"