minimax-pdf Three tasks. One skill. Read before any CREATE or REFORMAT work. --- Route table | User intent | Route | Scripts used | |---|---|---| | Generate a new PDF from scratch | CREATE | → → → → | | Fill / complete form fields in an existing PDF | FILL | → | | Reformat / re-style an existing document | REFORMAT | → then full CREATE pipeline | Rule: when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE. --- Route A: CREATE Full pipeline — content → design tokens → cover → body → merged PDF. Doc types: · · ·…

, r)]\n parsed = []\n for row in data_rows:\n cells = [c.strip() for c in row.strip(\"|\").split(\"|\")]\n parsed.append(cells)\n if len(parsed) >= 2:\n blocks.append({\n \"type\": \"table\",\n \"headers\": parsed[0],\n \"rows\": parsed[1:],\n })\n elif len(parsed) == 1:\n # Single row — treat as paragraph\n blocks.append({\"type\": \"body\", \"text\": \" | \".join(parsed[0])})\n continue\n\n # Horizontal rule → spacer\n if re.match(r'^[-*_]{3,}

minimax-pdf Three tasks. One skill. Read before any CREATE or REFORMAT work. --- Route table | User intent | Route | Scripts used | |---|---|---| | Generate a new PDF from scratch | CREATE | → → → → | | Fill / complete form fields in an existing PDF | FILL | → | | Reformat / re-style an existing document | REFORMAT | → then full CREATE pipeline | Rule: when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE. --- Route A: CREATE Full pipeline — content → design tokens → cover → body → merged PDF. Doc types: · · ·…

, stripped):\n flush_para(para_buf)\n para_buf = []\n blocks.append({\"type\": \"spacer\", \"pt\": 16})\n i += 1\n continue\n\n # Plain text → accumulate into paragraph\n para_buf.append(stripped)\n i += 1\n\n flush_para(para_buf)\n return blocks\n\n\ndef _md_inline(text: str) -> str:\n \"\"\"Convert inline Markdown to ReportLab XML markup.\"\"\"\n # Bold: **text** or __text__\n text = re.sub(r'\\*\\*(.+?)\\*\\*', r'\u003cb>\\1\u003c/b>', text)\n text = re.sub(r'__(.+?)__', r'\u003cb>\\1\u003c/b>', text)\n # Italic: *text* or _text_\n text = re.sub(r'\\*(.+?)\\*', r'\u003ci>\\1\u003c/i>', text)\n text = re.sub(r'_(.+?)_', r'\u003ci>\\1\u003c/i>', text)\n # Inline code: `code`\n text = re.sub(r'`(.+?)`', r'\u003cfont name=\"Courier\">\\1\u003c/font>', text)\n # Strip markdown links, keep text\n text = re.sub(r'\\[(.+?)\\]\\(.+?\\)', r'\\1', text)\n return text\n\n\n# ── PDF text extractor ─────────────────────────────────────────────────────────\ndef parse_pdf(pdf_path: str) -> list:\n \"\"\"\n Extract text from an existing PDF and convert to content.json blocks.\n Best-effort: detects headings by font size heuristics if available,\n otherwise falls back to paragraph splitting.\n \"\"\"\n from pypdf import PdfReader\n\n reader = PdfReader(pdf_path)\n all_text = []\n\n for page in reader.pages:\n text = page.extract_text()\n if text:\n all_text.append(text.strip())\n\n full_text = \"\\n\\n\".join(all_text)\n\n # Treat extracted PDF text as plain text / light markdown\n # (most PDFs lose formatting — we do our best)\n return parse_plain(full_text)\n\n\ndef parse_plain(text: str) -> list:\n \"\"\"\n Heuristic plain-text parser.\n Short ALL-CAPS or title-case lines → headings.\n Everything else → paragraphs.\n \"\"\"\n blocks = []\n paragraphs = re.split(r'\\n{2,}', text.strip())\n\n for para in paragraphs:\n para = para.strip()\n if not para:\n continue\n\n lines = para.splitlines()\n\n # Single short line that looks like a heading\n if len(lines) == 1 and len(para) \u003c 80:\n if para.isupper() or re.match(r'^[A-Z][^.!?]*

minimax-pdf Three tasks. One skill. Read before any CREATE or REFORMAT work. --- Route table | User intent | Route | Scripts used | |---|---|---| | Generate a new PDF from scratch | CREATE | → → → → | | Fill / complete form fields in an existing PDF | FILL | → | | Reformat / re-style an existing document | REFORMAT | → then full CREATE pipeline | Rule: when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE. --- Route A: CREATE Full pipeline — content → design tokens → cover → body → merged PDF. Doc types: · · ·…

, para):\n blocks.append({\"type\": \"h1\", \"text\": para.title()})\n continue\n\n # Bullet lists\n if lines[0].startswith((\"- \", \"• \", \"* \")):\n for line in lines:\n text_part = re.sub(r'^[-•*]\\s+', '', line.strip())\n if text_part:\n blocks.append({\"type\": \"bullet\", \"text\": text_part})\n continue\n\n # Regular paragraph\n blocks.append({\"type\": \"body\", \"text\": \" \".join(lines)})\n\n return blocks\n\n\n# ── Pass-through validator ─────────────────────────────────────────────────────\nVALID_TYPES = {\"h1\",\"h2\",\"h3\",\"body\",\"bullet\",\"numbered\",\"callout\",\"table\",\n \"image\",\"code\",\"math\",\"divider\",\"caption\",\"pagebreak\",\"spacer\"}\n\ndef validate_content_json(data: list) -> tuple[list, list]:\n \"\"\"Return (valid_blocks, warnings).\"\"\"\n valid, warnings = [], []\n for i, block in enumerate(data):\n if not isinstance(block, dict):\n warnings.append(f\"Block {i}: not a dict, skipped\")\n continue\n btype = block.get(\"type\")\n if btype not in VALID_TYPES:\n warnings.append(f\"Block {i}: unknown type '{btype}', kept as-is\")\n valid.append(block)\n return valid, warnings\n\n\n# ── Dispatcher ─────────────────────────────────────────────────────────────────\ndef parse_file(input_path: str) -> tuple[list, list]:\n \"\"\"Return (blocks, warnings).\"\"\"\n ext = Path(input_path).suffix.lower()\n\n if ext in (\".md\", \".txt\", \".markdown\"):\n with open(input_path, encoding=\"utf-8\", errors=\"replace\") as f:\n text = f.read()\n blocks = parse_markdown(text)\n return blocks, []\n\n if ext == \".pdf\":\n blocks = parse_pdf(input_path)\n return blocks, [\"PDF text extraction is best-effort — review content.json before rendering\"]\n\n if ext == \".json\":\n with open(input_path) as f:\n data = json.load(f)\n if isinstance(data, list):\n return validate_content_json(data)\n # Maybe it's a meta-wrapper {\"content\": [...]}\n if isinstance(data, dict) and \"content\" in data:\n return validate_content_json(data[\"content\"])\n return [], [f\"JSON file does not contain a list of content blocks\"]\n\n return [], [f\"Unsupported file type: {ext}. Supported: .md .txt .pdf .json\"]\n\n\n# ── CLI ────────────────────────────────────────────────────────────────────────\ndef main():\n parser = argparse.ArgumentParser(description=\"Parse a document into content.json\")\n parser.add_argument(\"--input\", required=True, help=\"Input file (.md, .txt, .pdf, .json)\")\n parser.add_argument(\"--out\", default=\"content.json\", help=\"Output content.json path\")\n args = parser.parse_args()\n\n if not os.path.exists(args.input):\n print(json.dumps({\"status\": \"error\", \"error\": f\"File not found: {args.input}\"}),\n file=sys.stderr)\n sys.exit(1)\n\n try:\n blocks, warnings = parse_file(args.input)\n except Exception as e:\n import traceback\n print(json.dumps({\"status\": \"error\", \"error\": str(e),\n \"trace\": traceback.format_exc()}), file=sys.stderr)\n sys.exit(3)\n\n if not blocks:\n print(json.dumps({\n \"status\": \"error\",\n \"error\": \"No content blocks extracted\",\n \"warnings\": warnings,\n }), file=sys.stderr)\n sys.exit(3)\n\n with open(args.out, \"w\", encoding=\"utf-8\") as f:\n json.dump(blocks, f, indent=2, ensure_ascii=False)\n\n result = {\n \"status\": \"ok\",\n \"out\": args.out,\n \"block_count\": len(blocks),\n \"warnings\": warnings,\n }\n print(json.dumps(result, indent=2))\n\n print(f\"\\n── Parsed {args.input} ─────────────────────────────────────\",\n file=sys.stderr)\n print(f\" Blocks : {len(blocks)}\", file=sys.stderr)\n\n type_counts: dict = {}\n for b in blocks:\n type_counts[b.get(\"type\",\"?\")] = type_counts.get(b.get(\"type\",\"?\"), 0) + 1\n for t, n in sorted(type_counts.items()):\n print(f\" {t:12} × {n}\", file=sys.stderr)\n\n if warnings:\n print(f\" Warnings:\", file=sys.stderr)\n for w in warnings:\n print(f\" ⚠ {w}\", file=sys.stderr)\n print(f\"\\n Next: bash make.sh run --content {args.out} --title '...' --type ...\",\n file=sys.stderr)\n print(\"\", file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13295,"content_sha256":"1b5618ae2a423e9c0abb8eb7e1bf3c41d97ef24353b4a886432052170942f974"},{"filename":"scripts/render_body.py","content":"#!/usr/bin/env python3\n\"\"\"\nrender_body.py — Build the inner-page PDF from tokens.json + content.json.\n\nUsage:\n python3 render_body.py --tokens tokens.json --content content.json --out body.pdf\n\nBlock types:\n h1 h2 h3 Headings (h1 adds a full-width accent rule below)\n body Justified prose paragraph\n bullet Bullet list item (• prefix)\n numbered Auto-numbered list item (resets when interrupted)\n callout Highlighted insight box with left accent bar\n table Data table with accent header + alternating rows\n image Inline image from file path\n figure Image with auto-numbered \"Figure N:\" caption\n code Monospace code block with accent left border\n math Display math formula via matplotlib mathtext\n chart Bar / line / pie chart rendered via matplotlib\n flowchart Process diagram rendered via matplotlib\n bibliography Numbered reference list\n divider Full-width accent rule\n caption Small muted text (e.g., under a figure)\n pagebreak Force a new page\n spacer Vertical whitespace (pt field, default 12)\n\nExit codes: 0 success, 1 bad args/missing file, 2 missing dep, 3 render error\n\"\"\"\n\nimport argparse\nimport io\nimport json\nimport os\nimport sys\nimport importlib.util\n\n\n# ── Dependency bootstrap ───────────────────────────────────────────────────────\ndef ensure_deps():\n missing = [p for p in (\"reportlab\", \"pypdf\")\n if importlib.util.find_spec(p) is None]\n if missing:\n import subprocess\n subprocess.check_call(\n [sys.executable, \"-m\", \"pip\", \"install\",\n \"--break-system-packages\", \"-q\"] + missing\n )\n\n\nensure_deps()\n\nfrom reportlab.platypus import (\n BaseDocTemplate, PageTemplate, Frame,\n Paragraph, Spacer, Table, TableStyle,\n HRFlowable, PageBreak, Flowable, KeepTogether,\n Preformatted, Image as RLImage,\n)\nfrom reportlab.lib.pagesizes import A4\nfrom reportlab.lib.styles import ParagraphStyle\nfrom reportlab.lib.colors import HexColor\nfrom reportlab.lib.enums import TA_JUSTIFY, TA_CENTER\nfrom reportlab.pdfbase import pdfmetrics\nfrom reportlab.pdfbase.ttfonts import TTFont\n\n\n# ── Font registration ──────────────────────────────────────────────────────────\ndef register_fonts(tokens: dict):\n \"\"\"Register TTF fonts from token font_paths if present.\"\"\"\n for name, fpath in tokens.get(\"font_paths\", {}).items():\n if os.path.exists(fpath):\n try:\n pdfmetrics.registerFont(TTFont(name, fpath))\n except Exception:\n pass\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Custom Flowables\n# ══════════════════════════════════════════════════════════════════════════════\n\nclass CalloutBox(Flowable):\n \"\"\"Highlighted insight box: coloured background + 4px left accent bar.\"\"\"\n\n def __init__(self, text: str, style, accent: str, bg: str):\n super().__init__()\n self._para = Paragraph(text, style)\n self._accent = HexColor(accent)\n self._bg = HexColor(bg)\n\n def wrap(self, aw, ah):\n self._w = aw\n _, ph = self._para.wrap(aw - 36, ah)\n self._h = ph + 22\n return aw, self._h\n\n def draw(self):\n c = self.canv\n c.setFillColor(self._bg)\n c.roundRect(0, 0, self._w, self._h, 5, fill=1, stroke=0)\n c.setFillColor(self._accent)\n c.rect(0, 0, 4, self._h, fill=1, stroke=0)\n self._para.drawOn(c, 18, 11)\n\n\nclass BibliographyItem(Flowable):\n \"\"\"Single hanging-indent bibliography entry rendered as [N] text.\"\"\"\n\n LABEL_W = 28\n\n def __init__(self, ref_id: str, text: str, style, dark: str):\n super().__init__()\n self._id = ref_id\n self._text = text\n self._style = style\n self._dark = HexColor(dark)\n\n def wrap(self, aw, ah):\n self._w = aw\n self._para = Paragraph(self._text, self._style)\n _, ph = self._para.wrap(aw - self.LABEL_W, ah)\n self._h = ph + 4\n return aw, self._h\n\n def draw(self):\n c = self.canv\n c.setFillColor(self._dark)\n c.setFont(\"Helvetica-Bold\", 8.5)\n c.drawString(0, self._h - 12, f\"[{self._id}]\")\n self._para.drawOn(c, self.LABEL_W, 2)\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Page template (header + footer)\n# ══════════════════════════════════════════════════════════════════════════════\n\nclass BeautifulDoc(BaseDocTemplate):\n def __init__(self, path: str, tokens: dict, **kw):\n self._t = tokens\n super().__init__(path, **kw)\n fr = Frame(\n self.leftMargin, self.bottomMargin,\n self.width, self.height, id=\"body\",\n )\n tmpl = PageTemplate(id=\"main\", frames=fr, onPage=self._decorate)\n self.addPageTemplates([tmpl])\n\n def _decorate(self, canv, doc):\n t = self._t\n lm = doc.leftMargin\n rm = doc.rightMargin\n pw = doc.pagesize[0]\n ph = doc.pagesize[1]\n top = ph - doc.topMargin\n\n canv.saveState()\n\n # Header accent rule\n canv.setStrokeColor(HexColor(t[\"accent\"]))\n canv.setLineWidth(1.5)\n canv.line(lm, top + 12, pw - rm, top + 12)\n\n # Header: title (left) + date (right)\n canv.setFillColor(HexColor(t[\"muted\"]))\n canv.setFont(t[\"font_body_rl\"], t[\"size_meta\"])\n canv.drawString(lm, top + 16, t[\"title\"].upper())\n canv.drawRightString(pw - rm, top + 16, t.get(\"date\", \"\"))\n\n # Footer rule\n canv.setStrokeColor(HexColor(\"#DDDDDD\"))\n canv.setLineWidth(0.5)\n canv.line(lm, doc.bottomMargin - 12, pw - rm, doc.bottomMargin - 12)\n\n # Footer: author (left) + page number (right)\n canv.setFillColor(HexColor(t[\"muted\"]))\n canv.setFont(t[\"font_body_rl\"], t[\"size_meta\"])\n canv.drawString(lm, doc.bottomMargin - 22, t.get(\"author\", \"\"))\n canv.drawRightString(pw - rm, doc.bottomMargin - 22, str(doc.page))\n\n canv.restoreState()\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Style factory\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef make_styles(t: dict) -> dict:\n hf = t[\"font_display_rl\"]\n bf = t[\"font_body_rl\"]\n bfb = t[\"font_body_b_rl\"]\n dk = t[\"body_text\"]\n d = t[\"dark\"]\n mu = t[\"muted\"]\n\n return {\n \"h1\": ParagraphStyle(\"H1\",\n fontName=hf, fontSize=t[\"size_h1\"],\n leading=t[\"size_h1\"] * 1.3,\n textColor=HexColor(d),\n spaceBefore=t[\"section_gap\"], spaceAfter=4,\n ),\n \"h2\": ParagraphStyle(\"H2\",\n fontName=hf, fontSize=t[\"size_h2\"],\n leading=t[\"size_h2\"] * 1.4,\n textColor=HexColor(d),\n spaceBefore=18, spaceAfter=5,\n ),\n \"h3\": ParagraphStyle(\"H3\",\n fontName=bfb, fontSize=t[\"size_h3\"],\n leading=t[\"size_h3\"] * 1.5,\n textColor=HexColor(d),\n spaceBefore=12, spaceAfter=3,\n ),\n \"body\": ParagraphStyle(\"Body\",\n fontName=bf, fontSize=t[\"size_body\"],\n leading=t[\"line_gap\"],\n textColor=HexColor(dk),\n spaceAfter=t[\"para_gap\"], alignment=TA_JUSTIFY,\n ),\n \"bullet\": ParagraphStyle(\"Bullet\",\n fontName=bf, fontSize=t[\"size_body\"],\n leading=t[\"line_gap\"] - 1,\n textColor=HexColor(dk),\n spaceAfter=4, leftIndent=14,\n ),\n \"numbered\": ParagraphStyle(\"Numbered\",\n fontName=bf, fontSize=t[\"size_body\"],\n leading=t[\"line_gap\"] - 1,\n textColor=HexColor(dk),\n spaceAfter=4, leftIndent=22, firstLineIndent=-22,\n ),\n \"callout\": ParagraphStyle(\"Callout\",\n fontName=bfb, fontSize=t[\"size_body\"] + 0.5, leading=16,\n textColor=HexColor(d),\n ),\n \"caption\": ParagraphStyle(\"Caption\",\n fontName=bf, fontSize=t[\"size_caption\"], leading=13,\n textColor=HexColor(mu), spaceAfter=6,\n alignment=TA_CENTER,\n ),\n \"table_header\": ParagraphStyle(\"TblH\",\n fontName=bfb, fontSize=9.5, leading=13,\n textColor=HexColor(\"#FFFFFF\"),\n ),\n \"table_cell\": ParagraphStyle(\"TblC\",\n fontName=bf, fontSize=9.5, leading=13,\n textColor=HexColor(dk),\n ),\n \"code\": ParagraphStyle(\"Code\",\n fontName=\"Courier\", fontSize=8.5, leading=12.5,\n textColor=HexColor(dk),\n ),\n \"code_lang\": ParagraphStyle(\"CodeLang\",\n fontName=\"Courier\", fontSize=7, leading=10,\n textColor=HexColor(mu),\n ),\n \"bib\": ParagraphStyle(\"Bib\",\n fontName=bf, fontSize=9, leading=14,\n textColor=HexColor(dk),\n ),\n \"bib_title\": ParagraphStyle(\"BibTitle\",\n fontName=hf, fontSize=t[\"size_h2\"],\n leading=t[\"size_h2\"] * 1.4,\n textColor=HexColor(d),\n spaceBefore=t[\"section_gap\"], spaceAfter=8,\n ),\n \"math_fallback\": ParagraphStyle(\"MathFb\",\n fontName=\"Courier\", fontSize=9, leading=13,\n textColor=HexColor(dk),\n ),\n \"eq_label\": ParagraphStyle(\"EqLabel\",\n fontName=\"Helvetica\", fontSize=9, leading=12,\n textColor=HexColor(mu),\n ),\n }\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Shared helpers\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef _divider(accent: str) -> HRFlowable:\n return HRFlowable(\n width=\"100%\", thickness=1.2,\n color=HexColor(accent),\n spaceBefore=14, spaceAfter=14,\n )\n\n\ndef _image_from_bytes(png_bytes: bytes, usable_w: float,\n max_frac: float = 0.88) -> RLImage:\n \"\"\"Create a scaled RLImage from PNG bytes, bounded to max_frac of usable_w.\"\"\"\n img = RLImage(io.BytesIO(png_bytes))\n max_w = usable_w * max_frac\n if img.drawWidth > max_w:\n scale = max_w / img.drawWidth\n img.drawWidth = max_w\n img.drawHeight = img.drawHeight * scale\n return img\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# PNG renderers (matplotlib)\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef _render_math_png(expr: str, dpi: int = 180) -> bytes | None:\n \"\"\"\n Render a LaTeX math expression via matplotlib mathtext.\n No LaTeX binary required — uses matplotlib's built-in math parser.\n Supports: fractions (\\\\frac), integrals (\\\\int), sums (\\\\sum),\n Greek letters, sub/superscripts, etc.\n \"\"\"\n try:\n import matplotlib\n matplotlib.use(\"Agg\")\n import matplotlib.pyplot as plt\n\n fig = plt.figure(figsize=(8, 1.2))\n fig.patch.set_facecolor(\"white\")\n ax = fig.add_axes([0, 0, 1, 1])\n ax.set_axis_off()\n ax.set_facecolor(\"white\")\n ax.text(0.5, 0.5, f\"${expr}$\",\n fontsize=16, ha=\"center\", va=\"center\",\n transform=ax.transAxes)\n buf = io.BytesIO()\n fig.savefig(buf, format=\"png\", dpi=dpi, bbox_inches=\"tight\",\n facecolor=\"white\", pad_inches=0.1)\n plt.close(fig)\n buf.seek(0)\n return buf.read()\n except Exception:\n return None\n\n\ndef _render_chart_png(item: dict, accent: str, dpi: int = 150) -> bytes | None:\n \"\"\"\n Render bar / line / pie chart to PNG using matplotlib.\n\n Required fields:\n chart_type \"bar\" | \"line\" | \"pie\" (default \"bar\")\n labels list of category strings\n datasets list of {label?, values: list[number]}\n\n Optional fields:\n title chart title\n x_label X-axis label\n y_label Y-axis label\n \"\"\"\n try:\n import matplotlib\n matplotlib.use(\"Agg\")\n import matplotlib.pyplot as plt\n import matplotlib.colors as mcolors\n import colorsys\n import numpy as np\n\n chart_type = item.get(\"chart_type\", \"bar\")\n title_text = item.get(\"title\", \"\")\n labels = item.get(\"labels\", [])\n datasets = item.get(\"datasets\", [])\n\n # Derive a consistent palette from the document accent color\n r, g, b = mcolors.to_rgb(accent)\n h, s, v = colorsys.rgb_to_hsv(r, g, b)\n palette = [\n colorsys.hsv_to_rgb(\n (h + i * 0.13) % 1.0,\n max(0.35, s - i * 0.08),\n min(0.92, v + i * 0.04),\n )\n for i in range(max(len(datasets), 1))\n ]\n\n fig, ax = plt.subplots(figsize=(7, 3.6), dpi=dpi)\n fig.patch.set_facecolor(\"white\")\n ax.set_facecolor(\"white\")\n\n if chart_type == \"bar\":\n x = np.arange(len(labels))\n n = max(len(datasets), 1)\n width = 0.68 / n\n for i, ds in enumerate(datasets):\n offset = (i - (n - 1) / 2) * width\n ax.bar(x + offset, ds.get(\"values\", []), width * 0.88,\n label=ds.get(\"label\", f\"Series {i+1}\"),\n color=palette[i % len(palette)], edgecolor=\"none\")\n ax.set_xticks(x)\n ax.set_xticklabels(labels, fontsize=8.5)\n ax.yaxis.grid(True, alpha=0.25, color=\"#CCCCCC\", linewidth=0.7)\n ax.set_axisbelow(True)\n if item.get(\"x_label\"):\n ax.set_xlabel(item[\"x_label\"], fontsize=8.5)\n if item.get(\"y_label\"):\n ax.set_ylabel(item[\"y_label\"], fontsize=8.5)\n\n elif chart_type == \"line\":\n x = np.arange(len(labels))\n for i, ds in enumerate(datasets):\n ax.plot(x, ds.get(\"values\", []), marker=\"o\", markersize=3.5,\n label=ds.get(\"label\", f\"Series {i+1}\"),\n color=palette[i % len(palette)], linewidth=1.8)\n ax.set_xticks(x)\n ax.set_xticklabels(labels, fontsize=8.5)\n ax.yaxis.grid(True, alpha=0.25, color=\"#CCCCCC\", linewidth=0.7)\n ax.set_axisbelow(True)\n if item.get(\"x_label\"):\n ax.set_xlabel(item[\"x_label\"], fontsize=8.5)\n if item.get(\"y_label\"):\n ax.set_ylabel(item[\"y_label\"], fontsize=8.5)\n\n elif chart_type == \"pie\":\n vals = datasets[0].get(\"values\", []) if datasets else []\n colors = [\n colorsys.hsv_to_rgb(\n (h + i * 0.11) % 1.0,\n max(0.30, s - i * 0.06),\n min(0.92, v + i * 0.03),\n )\n for i in range(len(vals))\n ]\n ax.pie(vals, labels=labels, colors=colors,\n autopct=\"%1.1f%%\", pctdistance=0.82,\n wedgeprops=dict(edgecolor=\"white\", linewidth=1.4),\n textprops=dict(fontsize=8.5))\n\n # Shared styling\n for spine in ax.spines.values():\n spine.set_linewidth(0.5)\n spine.set_color(\"#CCCCCC\")\n ax.tick_params(axis=\"both\", length=0, labelsize=8.5)\n if title_text:\n ax.set_title(title_text, fontsize=10, pad=8,\n color=\"#333333\", fontweight=\"bold\")\n if len(datasets) > 1 and chart_type != \"pie\":\n ax.legend(frameon=False, fontsize=8, loc=\"upper right\")\n\n plt.tight_layout(pad=0.4)\n buf = io.BytesIO()\n fig.savefig(buf, format=\"png\", dpi=dpi, bbox_inches=\"tight\",\n facecolor=\"white\", pad_inches=0.06)\n plt.close(fig)\n buf.seek(0)\n return buf.read()\n except Exception:\n return None\n\n\ndef _render_flowchart_png(item: dict, accent: str, dark: str,\n muted: str, dpi: int = 130) -> bytes | None:\n \"\"\"\n Render a top-to-bottom flowchart using matplotlib patches and arrows.\n\n Node schema: {id, label, shape?}\n shape: \"rect\" (default) | \"diamond\" | \"oval\" | \"parallelogram\"\n\n Edge schema: {from, to, label?}\n Forward edges (to a later node) draw straight arrows.\n Back edges (to an earlier node) draw a curved arc to the right.\n \"\"\"\n try:\n import matplotlib\n matplotlib.use(\"Agg\")\n import matplotlib.pyplot as plt\n import matplotlib.patches as mpatch\n from matplotlib.patches import FancyBboxPatch\n import matplotlib.colors as mcolors\n\n nodes_list = item.get(\"nodes\", [])\n edges = item.get(\"edges\", [])\n if not nodes_list:\n return None\n\n nodes = {n[\"id\"]: n for n in nodes_list}\n order = {n[\"id\"]: i for i, n in enumerate(nodes_list)}\n\n n_nodes = len(nodes_list)\n BOX_W = 4.2\n BOX_H = 0.58\n STEP_Y = 1.25\n CX = 5.0\n\n fig_h = max(3.5, n_nodes * STEP_Y + 0.8)\n fig, ax = plt.subplots(figsize=(6, fig_h), dpi=dpi)\n fig.patch.set_facecolor(\"white\")\n ax.set_facecolor(\"white\")\n ax.set_xlim(0, 10)\n ax.set_ylim(-0.6, n_nodes * STEP_Y + 0.2)\n ax.invert_yaxis()\n ax.axis(\"off\")\n\n acc_rgb = mcolors.to_rgb(accent)\n dark_rgb = mcolors.to_rgb(dark)\n muted_rgb = mcolors.to_rgb(muted)\n\n # Node positions (cx, cy) — preserves input order\n pos = {nid: (CX, i * STEP_Y) for nid, i in order.items()}\n\n # ── Draw edges (behind nodes) ──────────────────────────────────────────\n for edge in edges:\n src, dst = edge.get(\"from\"), edge.get(\"to\")\n if src not in pos or dst not in pos:\n continue\n x1, y1 = pos[src]\n x2, y2 = pos[dst]\n lbl = edge.get(\"label\", \"\")\n\n src_shape = nodes.get(src, {}).get(\"shape\", \"rect\")\n dst_shape = nodes.get(dst, {}).get(\"shape\", \"rect\")\n dy_src = BOX_H * (0.80 if src_shape == \"diamond\" else 0.50)\n dy_dst = BOX_H * (0.80 if dst_shape == \"diamond\" else 0.50)\n\n y_start = y1 + dy_src\n y_end = y2 - dy_dst\n\n # Forward edge: straight; back-edge: curved arc\n conn = \"arc3,rad=0.0\" if y_end > y_start + 0.01 else \"arc3,rad=0.42\"\n\n ax.annotate(\"\",\n xy=(x2, y_end), xytext=(x1, y_start),\n arrowprops=dict(\n arrowstyle=\"-|>\", color=muted_rgb,\n lw=1.0, mutation_scale=10,\n connectionstyle=conn,\n ),\n )\n if lbl:\n mid_x = (x1 + x2) / 2 + 0.28\n mid_y = (y_start + y_end) / 2\n ax.text(mid_x, mid_y, lbl, fontsize=7.5,\n color=muted_rgb, ha=\"left\", va=\"center\")\n\n # ── Draw nodes (in front of edges) ────────────────────────────────────\n for nid, (cx, cy) in pos.items():\n node = nodes[nid]\n shape = node.get(\"shape\", \"rect\")\n label = node.get(\"label\", nid)\n left = cx - BOX_W / 2\n bot = cy - BOX_H / 2\n\n if shape in (\"oval\", \"terminal\"):\n el = mpatch.Ellipse(\n (cx, cy), BOX_W * 0.78, BOX_H * 1.15,\n facecolor=acc_rgb, edgecolor=acc_rgb, linewidth=0,\n )\n ax.add_patch(el)\n ax.text(cx, cy, label, ha=\"center\", va=\"center\",\n fontsize=8.5, fontweight=\"bold\", color=\"white\")\n\n elif shape == \"diamond\":\n d = BOX_W * 0.44\n diamond = plt.Polygon(\n [(cx, cy - d * 0.72), (cx + d, cy),\n (cx, cy + d * 0.72), (cx - d, cy)],\n facecolor=\"#FFFCF0\",\n edgecolor=accent, linewidth=1.2,\n )\n ax.add_patch(diamond)\n ax.text(cx, cy, label, ha=\"center\", va=\"center\",\n fontsize=8, color=dark_rgb)\n\n elif shape == \"parallelogram\":\n skew = 0.30\n para = plt.Polygon(\n [(left + skew, bot), (left + BOX_W + skew, bot),\n (left + BOX_W, bot + BOX_H), (left, bot + BOX_H)],\n facecolor=\"white\",\n edgecolor=accent, linewidth=1.2,\n )\n ax.add_patch(para)\n ax.text(cx, cy, label, ha=\"center\", va=\"center\",\n fontsize=8.5, color=dark_rgb)\n\n else: # rect (default)\n rect = FancyBboxPatch(\n (left, bot), BOX_W, BOX_H,\n boxstyle=\"round,pad=0.04\",\n facecolor=\"white\",\n edgecolor=accent, linewidth=1.2,\n )\n ax.add_patch(rect)\n ax.text(cx, cy, label, ha=\"center\", va=\"center\",\n fontsize=8.5, color=dark_rgb)\n\n plt.tight_layout(pad=0.2)\n buf = io.BytesIO()\n fig.savefig(buf, format=\"png\", dpi=dpi, bbox_inches=\"tight\",\n facecolor=\"white\", pad_inches=0.08)\n plt.close(fig)\n buf.seek(0)\n return buf.read()\n except Exception:\n return None\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Block renderers\n#\n# All functions share the same signature:\n# _add_XXX(story: list, item: dict, ctx: dict)\n#\n# ctx keys:\n# tokens dict design tokens from palette.py\n# styles dict ParagraphStyle objects from make_styles()\n# usable_w float usable page width in points\n# acc str accent hex color\n# acc_lt str light accent hex color\n# mu str muted hex color\n# dark str dark hex color\n# figure_n int auto-incrementing figure counter (mutable)\n# numbered_n int auto-incrementing list counter (mutable)\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef _add_heading(story: list, item: dict, ctx: dict, level: int):\n key = f\"h{level}\"\n para = Paragraph(item[\"text\"], ctx[\"styles\"][key])\n if level == 1:\n story.append(KeepTogether([para, _divider(ctx[\"acc\"])]))\n else:\n story.append(para)\n\n\ndef _add_body(story: list, item: dict, ctx: dict):\n story.append(Paragraph(item[\"text\"], ctx[\"styles\"][\"body\"]))\n\n\ndef _add_bullet(story: list, item: dict, ctx: dict):\n story.append(Paragraph(\n f\"\\u2022\\u2002{item['text']}\", ctx[\"styles\"][\"bullet\"]\n ))\n\n\ndef _add_numbered(story: list, item: dict, ctx: dict):\n ctx[\"numbered_n\"] += 1\n story.append(Paragraph(\n f\"{ctx['numbered_n']}.\\u2002{item['text']}\",\n ctx[\"styles\"][\"numbered\"],\n ))\n\n\ndef _add_callout(story: list, item: dict, ctx: dict):\n story.append(Spacer(1, 8))\n story.append(CalloutBox(\n item[\"text\"], ctx[\"styles\"][\"callout\"], ctx[\"acc\"], ctx[\"acc_lt\"]\n ))\n story.append(Spacer(1, 8))\n\n\ndef _add_table(story: list, item: dict, ctx: dict):\n t = ctx[\"tokens\"]\n styles = ctx[\"styles\"]\n usable_w = ctx[\"usable_w\"]\n acc = ctx[\"acc\"]\n acc_lt = ctx[\"acc_lt\"]\n\n headers = [Paragraph(h, styles[\"table_header\"]) for h in item[\"headers\"]]\n rows = [\n [Paragraph(str(c), styles[\"table_cell\"]) for c in row]\n for row in item.get(\"rows\", [])\n ]\n n_cols = len(item[\"headers\"])\n\n # Optional col_widths as fractions summing to 1.0\n if \"col_widths\" in item and len(item[\"col_widths\"]) == n_cols:\n col_w = [usable_w * f for f in item[\"col_widths\"]]\n else:\n col_w = [usable_w / n_cols] * n_cols\n\n tbl = Table([headers] + rows, colWidths=col_w)\n tbl.setStyle(TableStyle([\n (\"BACKGROUND\", (0, 0), (-1, 0), HexColor(acc)),\n (\"TEXTCOLOR\", (0, 0), (-1, 0), HexColor(\"#FFFFFF\")),\n (\"FONTNAME\", (0, 0), (-1, 0), t[\"font_body_b_rl\"]),\n (\"FONTSIZE\", (0, 0), (-1, 0), 9.5),\n (\"TOPPADDING\", (0, 0), (-1, 0), 7),\n (\"BOTTOMPADDING\", (0, 0), (-1, 0), 7),\n (\"ROWBACKGROUNDS\", (0, 1), (-1, -1),\n [HexColor(\"#FFFFFF\"), HexColor(acc_lt)]),\n (\"FONTNAME\", (0, 1), (-1, -1), t[\"font_body_rl\"]),\n (\"FONTSIZE\", (0, 1), (-1, -1), 9.5),\n (\"TOPPADDING\", (0, 1), (-1, -1), 6),\n (\"BOTTOMPADDING\", (0, 1), (-1, -1), 6),\n (\"LEFTPADDING\", (0, 0), (-1, -1), 10),\n (\"RIGHTPADDING\", (0, 0), (-1, -1), 10),\n (\"BOX\", (0, 0), (-1, -1), 0.5, HexColor(\"#CCCCCC\")),\n (\"LINEBELOW\", (0, 0), (-1, 0), 1.2, HexColor(acc)),\n (\"TEXTCOLOR\", (0, 1), (-1, -1), HexColor(t[\"body_text\"])),\n (\"VALIGN\", (0, 0), (-1, -1), \"MIDDLE\"),\n ]))\n story.append(tbl)\n if item.get(\"caption\"):\n story.append(Spacer(1, 4))\n story.append(Paragraph(item[\"caption\"], styles[\"caption\"]))\n story.append(Spacer(1, 12))\n\n\ndef _add_image(story: list, item: dict, ctx: dict):\n path = str(item.get(\"path\", item.get(\"src\", \"\")))\n if not os.path.exists(path):\n story.append(Paragraph(\n f\"[Image not found: {path}]\", ctx[\"styles\"][\"caption\"]\n ))\n return\n try:\n img = RLImage(path)\n uw = ctx[\"usable_w\"]\n if img.drawWidth > uw:\n scale = uw / img.drawWidth\n img.drawWidth = uw\n img.drawHeight = img.drawHeight * scale\n story.append(img)\n except Exception as e:\n story.append(Paragraph(f\"[Image error: {e}]\", ctx[\"styles\"][\"caption\"]))\n return\n if item.get(\"caption\"):\n story.append(Spacer(1, 4))\n story.append(Paragraph(item[\"caption\"], ctx[\"styles\"][\"caption\"]))\n story.append(Spacer(1, 8))\n\n\ndef _add_figure(story: list, item: dict, ctx: dict):\n \"\"\"Like image but auto-numbers the caption as 'Figure N: ...'.\"\"\"\n ctx[\"figure_n\"] += 1\n raw_cap = item.get(\"caption\", \"\")\n caption = f\"Figure {ctx['figure_n']}: {raw_cap}\" if raw_cap \\\n else f\"Figure {ctx['figure_n']}\"\n _add_image(story, {**item, \"caption\": caption}, ctx)\n\n\ndef _add_code(story: list, item: dict, ctx: dict):\n acc = ctx[\"acc\"]\n acc_lt = ctx[\"acc_lt\"]\n mu = ctx[\"mu\"]\n uw = ctx[\"usable_w\"]\n lang = item.get(\"language\", \"\")\n\n pre = Preformatted(item.get(\"text\", \"\"), ctx[\"styles\"][\"code\"])\n tbl = Table([[pre]], colWidths=[uw])\n tbl.setStyle(TableStyle([\n (\"BACKGROUND\", (0, 0), (-1, -1), HexColor(acc_lt)),\n (\"LINEBEFORE\", (0, 0), ( 0, -1), 3, HexColor(acc)),\n (\"BOX\", (0, 0), (-1, -1), 0.5, HexColor(mu)),\n (\"LEFTPADDING\", (0, 0), (-1, -1), 14),\n (\"RIGHTPADDING\", (0, 0), (-1, -1), 10),\n (\"TOPPADDING\", (0, 0), (-1, -1), 8),\n (\"BOTTOMPADDING\", (0, 0), (-1, -1), 8),\n ]))\n story.append(Spacer(1, 6))\n if lang:\n story.append(Paragraph(lang.upper(), ctx[\"styles\"][\"code_lang\"]))\n story.append(tbl)\n story.append(Spacer(1, 6))\n\n\ndef _add_math(story: list, item: dict, ctx: dict):\n \"\"\"\n Display math block.\n\n Fields:\n text LaTeX math expression (without enclosing $)\n label optional equation label, e.g. \"(1)\" — displayed right-aligned\n caption optional caption below the formula\n\n Example:\n {\"type\": \"math\", \"text\": \"E = mc^2\", \"label\": \"(1)\"}\n {\"type\": \"math\", \"text\": \"\\\\\\\\int_0^\\\\\\\\infty e^{-x^2}\\\\\\\\,dx = \\\\\\\\frac{\\\\\\\\sqrt{\\\\\\\\pi}}{2}\"}\n \"\"\"\n acc = ctx[\"acc\"]\n acc_lt = ctx[\"acc_lt\"]\n uw = ctx[\"usable_w\"]\n expr = item.get(\"text\", \"\").strip()\n label = item.get(\"label\", \"\").strip()\n\n png = _render_math_png(expr)\n\n if png is None:\n # Graceful text fallback if matplotlib unavailable\n story.append(Spacer(1, 6))\n pre = Preformatted(f\" {expr}\", ctx[\"styles\"][\"math_fallback\"])\n tbl = Table([[pre]], colWidths=[uw])\n tbl.setStyle(TableStyle([\n (\"BACKGROUND\", (0, 0), (-1, -1), HexColor(acc_lt)),\n (\"LEFTPADDING\", (0, 0), (-1, -1), 14),\n (\"RIGHTPADDING\", (0, 0), (-1, -1), 14),\n (\"TOPPADDING\", (0, 0), (-1, -1), 8),\n (\"BOTTOMPADDING\", (0, 0), (-1, -1), 8),\n ]))\n story.append(tbl)\n story.append(Spacer(1, 6))\n return\n\n img = _image_from_bytes(png, uw, max_frac=0.72)\n story.append(Spacer(1, 10))\n\n if label:\n label_w = 44\n formula_w = uw - label_w\n lbl_para = Paragraph(label, ctx[\"styles\"][\"eq_label\"])\n row_tbl = Table([[img, lbl_para]], colWidths=[formula_w, label_w])\n row_tbl.setStyle(TableStyle([\n (\"ALIGN\", (0, 0), (0, 0), \"CENTER\"),\n (\"ALIGN\", (1, 0), (1, 0), \"RIGHT\"),\n (\"VALIGN\", (0, 0), (-1, -1), \"MIDDLE\"),\n ]))\n story.append(row_tbl)\n else:\n row_tbl = Table([[img]], colWidths=[uw])\n row_tbl.setStyle(TableStyle([\n (\"ALIGN\", (0, 0), (-1, -1), \"CENTER\"),\n ]))\n story.append(row_tbl)\n\n if item.get(\"caption\"):\n story.append(Spacer(1, 4))\n story.append(Paragraph(item[\"caption\"], ctx[\"styles\"][\"caption\"]))\n story.append(Spacer(1, 10))\n\n\ndef _add_chart(story: list, item: dict, ctx: dict):\n \"\"\"\n Render a chart (bar / line / pie) via matplotlib.\n\n Fields:\n chart_type \"bar\" | \"line\" | \"pie\" (default \"bar\")\n title chart title\n labels list of category strings\n datasets list of {label?, values: list[number]}\n x_label X-axis label (bar/line)\n y_label Y-axis label (bar/line)\n caption caption text below chart\n figure bool (default true) — prefix caption with \"Figure N:\"\n \"\"\"\n uw = ctx[\"usable_w\"]\n png = _render_chart_png(item, ctx[\"acc\"])\n\n if png is None:\n story.append(Paragraph(\n \"[Chart: install matplotlib to render — pip install matplotlib]\",\n ctx[\"styles\"][\"caption\"],\n ))\n return\n\n img = _image_from_bytes(png, uw, max_frac=0.95)\n story.append(Spacer(1, 8))\n row_tbl = Table([[img]], colWidths=[uw])\n row_tbl.setStyle(TableStyle([(\"ALIGN\", (0, 0), (-1, -1), \"CENTER\")]))\n story.append(row_tbl)\n\n raw_cap = item.get(\"caption\", \"\")\n use_fig = item.get(\"figure\", True)\n if raw_cap or use_fig:\n ctx[\"figure_n\"] += 1\n prefix = f\"Figure {ctx['figure_n']}: \" if use_fig else \"\"\n story.append(Spacer(1, 4))\n story.append(Paragraph(prefix + raw_cap, ctx[\"styles\"][\"caption\"]))\n story.append(Spacer(1, 10))\n\n\ndef _add_flowchart(story: list, item: dict, ctx: dict):\n \"\"\"\n Render a flowchart via matplotlib.\n\n Fields:\n nodes list of {id, label, shape?}\n shape: \"rect\" (default) | \"diamond\" | \"oval\" | \"parallelogram\"\n edges list of {from, to, label?}\n caption caption below the diagram\n figure bool (default true) — prefix caption with \"Figure N:\"\n \"\"\"\n uw = ctx[\"usable_w\"]\n png = _render_flowchart_png(item, ctx[\"acc\"], ctx[\"dark\"], ctx[\"mu\"])\n\n if png is None:\n story.append(Paragraph(\n \"[Flowchart: install matplotlib to render — pip install matplotlib]\",\n ctx[\"styles\"][\"caption\"],\n ))\n return\n\n img = _image_from_bytes(png, uw, max_frac=0.78)\n story.append(Spacer(1, 8))\n row_tbl = Table([[img]], colWidths=[uw])\n row_tbl.setStyle(TableStyle([(\"ALIGN\", (0, 0), (-1, -1), \"CENTER\")]))\n story.append(row_tbl)\n\n raw_cap = item.get(\"caption\", \"\")\n use_fig = item.get(\"figure\", True)\n if raw_cap or use_fig:\n ctx[\"figure_n\"] += 1\n prefix = f\"Figure {ctx['figure_n']}: \" if use_fig else \"\"\n story.append(Spacer(1, 4))\n story.append(Paragraph(prefix + raw_cap, ctx[\"styles\"][\"caption\"]))\n story.append(Spacer(1, 10))\n\n\ndef _add_bibliography(story: list, item: dict, ctx: dict):\n \"\"\"\n Numbered reference list with hanging indent.\n\n Fields:\n title section heading (default \"References\"); set \"\" to suppress\n items list of {id, text}\n\n Example:\n {\"type\": \"bibliography\",\n \"items\": [\n {\"id\": \"1\", \"text\": \"Smith, J. (2023). Title. Journal, 10(2), 1–15.\"},\n {\"id\": \"2\", \"text\": \"Doe, A. (2022). Another title. Publisher.\"}\n ]}\n \"\"\"\n heading = item.get(\"title\", \"References\")\n if heading:\n story.append(KeepTogether([\n Paragraph(heading, ctx[\"styles\"][\"bib_title\"]),\n _divider(ctx[\"acc\"]),\n ]))\n\n for ref in item.get(\"items\", []):\n story.append(Spacer(1, 4))\n story.append(BibliographyItem(\n str(ref.get(\"id\", \"\")),\n ref.get(\"text\", \"\"),\n ctx[\"styles\"][\"bib\"],\n ctx[\"dark\"],\n ))\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Story builder\n# ══════════════════════════════════════════════════════════════════════════════\n\n# Block types that break a numbered list sequence\n_RESETS_NUMBERED = frozenset({\n \"h1\", \"h2\", \"h3\", \"body\", \"bullet\", \"callout\", \"table\",\n \"image\", \"figure\", \"code\", \"math\", \"chart\", \"flowchart\",\n \"bibliography\", \"divider\", \"caption\", \"pagebreak\", \"spacer\",\n})\n\n\ndef build_story(content: list, tokens: dict, styles: dict) -> list:\n usable_w = A4[0] - tokens[\"margin_left\"] - tokens[\"margin_right\"]\n\n ctx: dict = {\n \"tokens\": tokens,\n \"styles\": styles,\n \"usable_w\": usable_w,\n \"acc\": tokens[\"accent\"],\n \"acc_lt\": tokens[\"accent_lt\"],\n \"mu\": tokens[\"muted\"],\n \"dark\": tokens[\"dark\"],\n \"figure_n\": 0,\n \"numbered_n\": 0,\n }\n\n story: list = []\n\n for item in content:\n kind = item.get(\"type\", \"body\")\n\n if kind in _RESETS_NUMBERED:\n ctx[\"numbered_n\"] = 0\n\n if kind == \"h1\": _add_heading(story, item, ctx, 1)\n elif kind == \"h2\": _add_heading(story, item, ctx, 2)\n elif kind == \"h3\": _add_heading(story, item, ctx, 3)\n elif kind == \"body\": _add_body(story, item, ctx)\n elif kind == \"bullet\": _add_bullet(story, item, ctx)\n elif kind == \"numbered\": _add_numbered(story, item, ctx)\n elif kind == \"callout\": _add_callout(story, item, ctx)\n elif kind == \"table\": _add_table(story, item, ctx)\n elif kind == \"image\": _add_image(story, item, ctx)\n elif kind == \"figure\": _add_figure(story, item, ctx)\n elif kind == \"code\": _add_code(story, item, ctx)\n elif kind == \"math\": _add_math(story, item, ctx)\n elif kind == \"chart\": _add_chart(story, item, ctx)\n elif kind == \"flowchart\": _add_flowchart(story, item, ctx)\n elif kind == \"bibliography\": _add_bibliography(story, item, ctx)\n elif kind == \"divider\": story.append(_divider(ctx[\"acc\"]))\n elif kind == \"caption\":\n story.append(Paragraph(item[\"text\"], styles[\"caption\"]))\n elif kind == \"pagebreak\": story.append(PageBreak())\n elif kind == \"spacer\": story.append(Spacer(1, item.get(\"pt\", 12)))\n\n return story\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# Main build\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef build(tokens: dict, content: list, out_path: str) -> dict:\n register_fonts(tokens)\n styles = make_styles(tokens)\n\n doc = BeautifulDoc(\n out_path, tokens,\n pagesize=A4,\n leftMargin=tokens[\"margin_left\"],\n rightMargin=tokens[\"margin_right\"],\n topMargin=tokens[\"margin_top\"],\n bottomMargin=tokens[\"margin_bottom\"],\n )\n doc.build(build_story(content, tokens, styles))\n\n size = os.path.getsize(out_path)\n return {\"status\": \"ok\", \"out\": out_path, \"size_kb\": size // 1024}\n\n\n# ══════════════════════════════════════════════════════════════════════════════\n# CLI\n# ══════════════════════════════════════════════════════════════════════════════\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Render body PDF from tokens.json + content.json\"\n )\n parser.add_argument(\"--tokens\", default=\"tokens.json\")\n parser.add_argument(\"--content\", default=\"content.json\")\n parser.add_argument(\"--out\", default=\"body.pdf\")\n args = parser.parse_args()\n\n for fpath in (args.tokens, args.content):\n if not os.path.exists(fpath):\n print(\n json.dumps({\"status\": \"error\",\n \"error\": f\"File not found: {fpath}\"}),\n file=sys.stderr,\n )\n sys.exit(1)\n\n with open(args.tokens, encoding=\"utf-8\") as f:\n tokens = json.load(f)\n with open(args.content, encoding=\"utf-8\") as f:\n content = json.load(f)\n\n try:\n result = build(tokens, content, args.out)\n print(json.dumps(result))\n except Exception as e:\n import traceback\n print(\n json.dumps({\n \"status\": \"error\",\n \"error\": str(e),\n \"trace\": traceback.format_exc(),\n }),\n file=sys.stderr,\n )\n sys.exit(3)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":40661,"content_sha256":"7cdd0ad4cfd845eefabf641c41265c934639f4a43be2290b25c5e23313667c7e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"minimax-pdf","type":"text"}]},{"type":"paragraph","content":[{"text":"Three tasks. One skill.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Read ","type":"text"},{"text":"design/design.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" before any CREATE or REFORMAT work.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Route table","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User intent","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Route","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scripts used","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate a new PDF from scratch","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CREATE","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"palette.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"cover.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"render_cover.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"render_body.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"merge.py","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fill / complete form fields in an existing PDF","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FILL","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fill_inspect.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"fill_write.py","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reformat / re-style an existing document","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"REFORMAT","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"reformat_parse.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → then full CREATE pipeline","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Rule:","type":"text","marks":[{"type":"strong"}]},{"text":" when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Route A: CREATE","type":"text"}]},{"type":"paragraph","content":[{"text":"Full pipeline — content → design tokens → cover → body → merged PDF.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/make.sh run \\\n --title \"Q3 Strategy Review\" --type proposal \\\n --author \"Strategy Team\" --date \"October 2025\" \\\n --accent \"#2D5F8A\" \\\n --content content.json --out report.pdf","type":"text"}]},{"type":"paragraph","content":[{"text":"Doc types:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"report","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"proposal","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"resume","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"portfolio","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"academic","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"general","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"minimal","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"stripe","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"diagonal","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"frame","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"editorial","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"magazine","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"darkroom","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"terminal","type":"text","marks":[{"type":"code_inline"}]},{"text":" · ","type":"text"},{"text":"poster","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cover pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visual identity","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"report","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fullbleed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dark bg, dot grid, Playfair Display","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"proposal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"split","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Left panel + right geometric, Syne","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"resume","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"typographic","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Oversized first-word, DM Serif Display","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"portfolio","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"atmospheric","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Near-black, radial glow, Fraunces","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"academic","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"typographic","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Light bg, classical serif, EB Garamond","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"general","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fullbleed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dark slate, Outfit","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"minimal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"minimal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"White + single 8px accent bar, Cormorant Garamond","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stripe","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stripe","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3 bold horizontal color bands, Barlow Condensed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"diagonal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"diagonal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SVG angled cut, dark/light halves, Montserrat","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"frame","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"frame","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inset border, corner ornaments, Cormorant","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"editorial","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"editorial","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ghost letter, all-caps title, Bebas Neue","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"magazine","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"magazine","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warm cream bg, centered stack, hero image, Playfair Display","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"darkroom","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"darkroom","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Navy bg, centered stack, grayscale image, Playfair Display","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"terminal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"terminal","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Near-black, grid lines, monospace, neon green","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poster","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"poster","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"White bg, thick sidebar, oversized title, Barlow Condensed","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Cover extras (inject into tokens via ","type":"text"},{"text":"--abstract","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--cover-image","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--abstract \"text\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — abstract text block on the cover (magazine/darkroom)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--cover-image \"url\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — hero image URL/path (magazine, darkroom, poster)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Color overrides — always choose these based on document content:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--accent \"#HEX\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — override the accent color; ","type":"text"},{"text":"accent_lt","type":"text","marks":[{"type":"code_inline"}]},{"text":" is auto-derived by lightening toward white","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--cover-bg \"#HEX\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — override the cover background color","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Accent color selection guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"You have creative authority over the accent color. Pick it from the document's semantic context — title, industry, purpose, audience — not from generic \"safe\" choices. The accent appears on section rules, callout bars, table headers, and the cover: it carries the document's visual identity.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Context","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Suggested accent range","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Legal / compliance / finance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deep navy ","type":"text"},{"text":"#1C3A5E","type":"text","marks":[{"type":"code_inline"}]},{"text":", charcoal ","type":"text"},{"text":"#2E3440","type":"text","marks":[{"type":"code_inline"}]},{"text":", slate ","type":"text"},{"text":"#3D4C5E","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Healthcare / medical","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Teal-green ","type":"text"},{"text":"#2A6B5A","type":"text","marks":[{"type":"code_inline"}]},{"text":", cool green ","type":"text"},{"text":"#3A7D6A","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Technology / engineering","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Steel blue ","type":"text"},{"text":"#2D5F8A","type":"text","marks":[{"type":"code_inline"}]},{"text":", indigo ","type":"text"},{"text":"#3D4F8A","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Environmental / sustainability","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Forest ","type":"text"},{"text":"#2E5E3A","type":"text","marks":[{"type":"code_inline"}]},{"text":", olive ","type":"text"},{"text":"#4A5E2A","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Creative / arts / culture","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Burgundy ","type":"text"},{"text":"#6B2A35","type":"text","marks":[{"type":"code_inline"}]},{"text":", plum ","type":"text"},{"text":"#5A2A6B","type":"text","marks":[{"type":"code_inline"}]},{"text":", terracotta ","type":"text"},{"text":"#8A3A2A","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Academic / research","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deep teal ","type":"text"},{"text":"#2A5A6B","type":"text","marks":[{"type":"code_inline"}]},{"text":", library blue ","type":"text"},{"text":"#2A4A6B","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Corporate / neutral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Slate ","type":"text"},{"text":"#3D4A5A","type":"text","marks":[{"type":"code_inline"}]},{"text":", graphite ","type":"text"},{"text":"#444C56","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Luxury / premium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warm black ","type":"text"},{"text":"#1A1208","type":"text","marks":[{"type":"code_inline"}]},{"text":", deep bronze ","type":"text"},{"text":"#4A3820","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Rule:","type":"text","marks":[{"type":"strong"}]},{"text":" choose a color that a thoughtful designer would select for this specific document — not the type's default. Muted, desaturated tones work best; avoid vivid primaries. When in doubt, go darker and more neutral.","type":"text"}]},{"type":"paragraph","content":[{"text":"content.json block types:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Block","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Usage","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key fields","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"h1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Section heading + accent rule","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"h2","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Subsection heading","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"h3","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sub-subsection (bold)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"body","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Justified paragraph; supports ","type":"text"},{"text":"\u003cb>","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"\u003ci>","type":"text","marks":[{"type":"code_inline"}]},{"text":" markup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bullet","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unordered list item (• prefix)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"numbered","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ordered list item — counter auto-resets on non-numbered blocks","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"callout","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Highlighted insight box with accent left bar","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"table","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data table — accent header, alternating row tints","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"headers","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"rows","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"col_widths","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"image","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Embedded image scaled to column width","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"path","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"src","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"figure","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Image with auto-numbered \"Figure N:\" caption","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"path","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"src","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"code","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Monospace code block with accent left border","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"language","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"math","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Display math — LaTeX syntax via matplotlib mathtext","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"label","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"chart","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bar / line / pie chart rendered with matplotlib","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"chart_type","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"labels","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"datasets","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"title","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"x_label","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"y_label","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"figure","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"flowchart","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Process diagram with nodes + edges via matplotlib","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodes","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"edges","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"caption","type":"text","marks":[{"type":"code_inline"}]},{"text":"?, ","type":"text"},{"text":"figure","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bibliography","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Numbered reference list with hanging indent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"items","type":"text","marks":[{"type":"code_inline"}]},{"text":" [{id, text}], ","type":"text"},{"text":"title","type":"text","marks":[{"type":"code_inline"}]},{"text":"?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"divider","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Accent-colored full-width rule","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"caption","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Small muted label","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pagebreak","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Force a new page","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"spacer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vertical whitespace","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pt","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default 12)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"chart / flowchart schemas:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\"type\":\"chart\",\"chart_type\":\"bar\",\"labels\":[\"Q1\",\"Q2\",\"Q3\",\"Q4\"],\n \"datasets\":[{\"label\":\"Revenue\",\"values\":[120,145,132,178]}],\"caption\":\"Q results\"}\n\n{\"type\":\"flowchart\",\n \"nodes\":[{\"id\":\"s\",\"label\":\"Start\",\"shape\":\"oval\"},\n {\"id\":\"p\",\"label\":\"Process\",\"shape\":\"rect\"},\n {\"id\":\"d\",\"label\":\"Valid?\",\"shape\":\"diamond\"},\n {\"id\":\"e\",\"label\":\"End\",\"shape\":\"oval\"}],\n \"edges\":[{\"from\":\"s\",\"to\":\"p\"},{\"from\":\"p\",\"to\":\"d\"},\n {\"from\":\"d\",\"to\":\"e\",\"label\":\"Yes\"},{\"from\":\"d\",\"to\":\"p\",\"label\":\"No\"}]}\n\n{\"type\":\"bibliography\",\"items\":[\n {\"id\":\"1\",\"text\":\"Author (Year). Title. Publisher.\"}]}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Route B: FILL","type":"text"}]},{"type":"paragraph","content":[{"text":"Fill form fields in an existing PDF without altering layout or design.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Step 1: inspect\npython3 scripts/fill_inspect.py --input form.pdf\n\n# Step 2: fill\npython3 scripts/fill_write.py --input form.pdf --out filled.pdf \\\n --values '{\"FirstName\": \"Jane\", \"Agree\": \"true\", \"Country\": \"US\"}'","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Value format","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Any string","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"checkbox","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"false\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"dropdown","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Must match a choice value from inspect output","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"radio","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Must match a radio value (often starts with ","type":"text"},{"text":"/","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Always run ","type":"text"},{"text":"fill_inspect.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" first to get exact field names.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Route C: REFORMAT","type":"text"}]},{"type":"paragraph","content":[{"text":"Parse an existing document → content.json → CREATE pipeline.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/make.sh reformat \\\n --input source.md --title \"My Report\" --type report --out output.pdf","type":"text"}]},{"type":"paragraph","content":[{"text":"Supported input formats:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":".md","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".pdf","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".json","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Environment","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/make.sh check # verify all deps\nbash scripts/make.sh fix # auto-install missing deps\nbash scripts/make.sh demo # build a sample PDF","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Used by","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python 3.9+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"all ","type":"text"},{"text":".py","type":"text","marks":[{"type":"code_inline"}]},{"text":" scripts","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"system","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"reportlab","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"render_body.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip install reportlab","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pypdf","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fill, merge, reformat","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip install pypdf","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Node.js 18+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"render_cover.js","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"system","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"playwright","type":"text","marks":[{"type":"code_inline"}]},{"text":" + Chromium","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"render_cover.js","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm install -g playwright && npx playwright install chromium","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"minimax-pdf","author":"@skillopedia","source":{"stars":12276,"repo_name":"skills","origin_url":"https://github.com/minimax-ai/skills/blob/HEAD/skills/minimax-pdf/SKILL.md","repo_owner":"minimax-ai","body_sha256":"80d88afb221f7098f1bc541d6466a7ff267bc6924234cdfc8b40b8b310257c36","cluster_key":"a41ad9f7a05f9f881436b168a647026dfd8feced7dfbc86b7842dd123459b3e7","clean_bundle":{"format":"clean-skill-bundle-v1","source":"minimax-ai/skills/skills/minimax-pdf/SKILL.md","attachments":[{"id":"3473890b-2336-523f-a1d9-a2ae05c7eccc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3473890b-2336-523f-a1d9-a2ae05c7eccc/attachment.md","path":"README.md","size":8534,"sha256":"ab7f0ee3ec300c87dee6f652b9bf96003ddd552c23a6c210ae1bb22cdd48396d","contentType":"text/markdown; charset=utf-8"},{"id":"ba8022bb-b032-512f-b1af-a0de4f21928b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ba8022bb-b032-512f-b1af-a0de4f21928b/attachment.md","path":"design/design.md","size":17208,"sha256":"870932013e86371697d88406273f8796830270a9171ac8065b13078acef6ffe2","contentType":"text/markdown; charset=utf-8"},{"id":"c3c16e0e-d7ba-5739-ab7c-199963d964e1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c3c16e0e-d7ba-5739-ab7c-199963d964e1/attachment.py","path":"scripts/cover.py","size":48008,"sha256":"ad6c6b927805c8d189869cd309e260e7a82a75cb809c92d4ca0627de4a724cfa","contentType":"text/x-python; charset=utf-8"},{"id":"c86e9680-df39-566a-8651-7d7646ae307b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c86e9680-df39-566a-8651-7d7646ae307b/attachment.py","path":"scripts/fill_inspect.py","size":6402,"sha256":"f048e44f5cc094c18790c4e8ff194d3ff6fe4018fa63bed148dd9730fff83467","contentType":"text/x-python; charset=utf-8"},{"id":"a08a9fab-4eef-5dd1-8bad-0fb94fea9c5f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a08a9fab-4eef-5dd1-8bad-0fb94fea9c5f/attachment.py","path":"scripts/fill_write.py","size":8451,"sha256":"afece596da883cc3bdfe112b9e46798b313312d93a05037b1bc5c3359502f993","contentType":"text/x-python; charset=utf-8"},{"id":"ffdba011-192e-576c-b267-9e0a35ab0b16","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ffdba011-192e-576c-b267-9e0a35ab0b16/attachment.sh","path":"scripts/make.sh","size":19562,"sha256":"c4f5c5a88be4b69f186f3712a357631dbf91be7233942e16144a864a914e25fb","contentType":"application/x-sh; charset=utf-8"},{"id":"317e6cc8-7a97-57ea-8800-82d0ef496ea7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/317e6cc8-7a97-57ea-8800-82d0ef496ea7/attachment.py","path":"scripts/merge.py","size":3750,"sha256":"4e194d8fe6a85a6d1981011c6f495be9a2c25c71bac5446c5386f0c3fb3e5660","contentType":"text/x-python; charset=utf-8"},{"id":"331635e1-4393-53d7-ad47-a7ed720a3e47","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/331635e1-4393-53d7-ad47-a7ed720a3e47/attachment.py","path":"scripts/palette.py","size":19662,"sha256":"520a55d4c3134a074fcfa24f1dab6369f489ca0a24b546f283064c3db17e86c1","contentType":"text/x-python; charset=utf-8"},{"id":"90c22cd5-fa0c-5cfc-8c13-1d1a2918bdeb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/90c22cd5-fa0c-5cfc-8c13-1d1a2918bdeb/attachment.py","path":"scripts/reformat_parse.py","size":13295,"sha256":"1b5618ae2a423e9c0abb8eb7e1bf3c41d97ef24353b4a886432052170942f974","contentType":"text/x-python; charset=utf-8"},{"id":"c7d782ac-babf-528b-872f-9e8650a53f18","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c7d782ac-babf-528b-872f-9e8650a53f18/attachment.py","path":"scripts/render_body.py","size":40661,"sha256":"7cdd0ad4cfd845eefabf641c41265c934639f4a43be2290b25c5e23313667c7e","contentType":"text/x-python; charset=utf-8"},{"id":"b66e2e3a-9955-5ea3-a403-123db429196e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b66e2e3a-9955-5ea3-a403-123db429196e/attachment.js","path":"scripts/render_cover.js","size":3844,"sha256":"5511c5b72d1e95e5fd8dc424c360d7ccd16fb70bc15235db327c1317cb875ba7","contentType":"application/javascript; charset=utf-8"}],"bundle_sha256":"2cb10664b305cc2ee7921c4d278f8e6d8555a237be152253dd43979cb87dc5cf","attachment_count":11,"text_attachments":11,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":3,"skill_md_path":"skills/minimax-pdf/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"documents-office","category_label":"Documents"},"exact_dupes_collapsed_into_this":2},"license":"MIT","version":"v1","category":"documents-office","metadata":{"version":"1.0","category":"document-generation"},"import_tag":"clean-skills-v1","description":"Use this skill when visual quality and design identity matter for a PDF. CREATE (generate from scratch): \"make a PDF\", \"generate a report\", \"write a proposal\", \"create a resume\", \"beautiful PDF\", \"professional document\", \"cover page\", \"polished PDF\", \"client-ready document\". FILL (complete form fields): \"fill in the form\", \"fill out this PDF\", \"complete the form fields\", \"write values into PDF\", \"what fields does this PDF have\". REFORMAT (apply design to an existing doc): \"reformat this document\", \"apply our style\", \"convert this Markdown/text to PDF\", \"make this doc look good\", \"re-style this PDF\". This skill uses a token-based design system: color, typography, and spacing are derived from the document type and flow through every page. The output is print-ready. Prefer this skill when appearance matters, not just when any PDF output is needed.\n"}},"renderedAt":1782981129175}

minimax-pdf Three tasks. One skill. Read before any CREATE or REFORMAT work. --- Route table | User intent | Route | Scripts used | |---|---|---| | Generate a new PDF from scratch | CREATE | → → → → | | Fill / complete form fields in an existing PDF | FILL | → | | Reformat / re-style an existing document | REFORMAT | → then full CREATE pipeline | Rule: when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE. --- Route A: CREATE Full pipeline — content → design tokens → cover → body → merged PDF. Doc types: · · ·…