# mopac-pdf Beautiful PDFs from the markdown that already runs this stack. Redmine notes, Discourse posts and briefing output go in; typeset PDFs — budgets, travel itineraries, project plans, proposals, consulting reports, multi-year financial plans, business plans, engineering docs — come out. Markdown is the composable currency of the MOPAC world; this tool makes it presentable without leaving it. Status: 2026-08-29 — v0 LIVE: two shipped templates (`report`, `brief`), bar charts rendered in pure Go from fenced `chart` data blocks, compile via a digest-pinned typst container, PDF bytes to stdout or `-o`. (Redmine 499; feeds the COS briefing world and TSYS client deliverables.) ## Design in one breath Our code is Go only. The typesetting engine is a prebuilt third-party binary (typst, Rust) pulled as a digest-pinned docker image and shelled by the CLI — supply-chain tooling, never source we maintain. The host runs containers, not toolchains: ```text markdown + front-matter -> internal/frontmatter (split) -> internal/markdown (parse) -> internal/typdoc (render to typst; compose with template) -> internal/chart (```chart blocks -> PNG via go-chart, pure Go) -> internal/engine (docker run --rm --network none typst compile) -> PDF bytes (stdout or -o) ``` ## Quickstart All dev work happens inside a Docker builder (host stays toolchain-free); `docker pull` of the builder and the engine image is pre-authorized. Commands below were verified on 2026-08-29 from a fresh clone. ### Build and test `dev.sh` routes every compile/vet/test path through the digest-pinned builder (or use `make build|vet|test|check`, same routing): ```sh ./dev.sh check # = go build + go vet + go test, all inside the builder ``` Expected output (tail): ```text ok ukrrs.com/mopac/pdf/internal/chart ok ukrrs.com/mopac/pdf/internal/cli ok ukrrs.com/mopac/pdf/internal/engine ok ukrrs.com/mopac/pdf/internal/frontmatter ok ukrrs.com/mopac/pdf/internal/markdown ok ukrrs.com/mopac/pdf/internal/pdfinfo ok ukrrs.com/mopac/pdf/internal/typdoc ``` (Dependencies are vendored — go-chart (MIT) and its BSD deps — so the builder needs no network.) ### Compile a PDF ```sh ./dev.sh build ./bin/mopac-pdf -o out/report.pdf testdata/sample-report.md ./bin/mopac-pdf < testdata/sample-brief.md > out/brief.pdf # stdin->stdout ``` End-to-end smoke (builds, compiles both fixtures through the REAL typst container, checks PDF magic + page counts + stdin/stdout + exit codes + custom template dir): ```sh ./dev.sh smoke ``` Expected output (tail): ```text ok engine failure exit=2 ok -T custom template smoke: all checks passed; samples in out/ ``` ## Input format ```markdown --- title: Q3 Operations Briefing subtitle: Infrastructure & Platform Status author: reachableceo date: 2026-08-29 classification: TSYS INTERNAL template: report --- # Executive Summary Plain markdown: headings, **bold**, _italic_, `code`, [links](https://x.co), lists, blockquotes, GFM tables with per-column alignment. ```chart type: bar title: Revenue by quarter ($K) Q1: 120 Q2: 180 Q3: 241 Q4: 260 ``` ``` - Front-matter keys: `title`, `subtitle`, `author`, `date`, `classification` (footer/header banner), `template`. Unknown keys are tolerated (kept in Extra). - `chart` blocks are `key: value` data; every `label: number` pair is a bar. `unit` labels the y axis, `width` (11-100, %) sizes the figure. v0 ships one chart type (`bar`), proven end to end. - Images: block-level `![caption](path.png)` resolves relative to the input file and embeds as a figure. ## Templates | name | shape | |------|-------| | `report` | title page, TOC (when 2+ headings), numbered headings, running header with classification, page X / Y + classification footer | | `brief` | 1-3 page exec summary: compact title block, classification badge, dense tables, no TOC | Templates are embedded in the binary AND loadable from disk: `-T DIR` picks `DIR/.typ` first. A template exports one function named after the file (`report(title:, subtitle:, author:, date:, classification:, doc)`) applied via `#show:` — see `templates/report.typ`. ## CLI ```text mopac-pdf [flags] [INPUT.md] (default input: stdin) -o FILE output PDF file (default stdout) -t NAME template (front-matter key, else "report") -T DIR template directory override -V version -pages F print page count of an existing PDF Exit codes: 0 ok, 1 usage/input error, 2 engine error ``` Env: `MOPAC_PDF_ENGINE` (engine image, default the pinned digest below), `MOPAC_PDF_DOCKER` (docker binary, for tests), `MOPAC_PDF_KEEP=1` (keep the compile root for debugging). ## Engine `ghcr.io/typst/typst@sha256:032e292249bcd378480cc7c142cfa324b63ef8aadeb88d7e7230320c4c9c422f` (= `ghcr.io/typst/typst:0.15.1`), run `--network none`, assets mounted read-write under `/work`, `--root /work`, PDF on stdout. Engine choice rationale: `docs/ENGINE-DECISION.md`. Builder: `golang@sha256:e8c859f5632dcfde7b32d2012b4351728f6437930887c2f6a91ea242459e5514` (= `golang:1.26-bookworm`, same as the other MOPAC tools). ## Layout ```text cmd/mopac-pdf/ CLI entry internal/cli flags + pipeline wiring (exit codes 0/1/2) internal/frontmatter --- block parsing internal/markdown md subset parser (blocks + charts) internal/typdoc typst rendering + template composition internal/chart go-chart bar renderer (pure Go) internal/engine digest-pinned typst container runner internal/pdfinfo tiny /Pages /Count parser templates/ embedded typst templates (report, brief) testdata/ fixtures + golden .typ files smoke/ host-side end-to-end smoke vendor/ go-chart (MIT) + deps (BSD) ``` ## License AGPLv3 (see LICENSE). The work this tool produces is licensed however its authors choose; the tool itself stays maximally viral.