Files
mopac-pdf/README.md
T
mrcharles 10e930e46d mopac-pdf v0: markdown to beautiful PDFs via digest-pinned typst
Go CLI that turns the markdown currency of this stack (Redmine notes,
Discourse posts, briefing output) into typeset PDFs: two embedded typst
templates (report with title page/TOC/headers, dense brief), front-matter
(title/subtitle/author/date/classification/template), GFM tables, and
bar charts rendered in pure Go (go-chart) from fenced chart data blocks.
Engine is a prebuilt typst 0.15.1 container pinned by digest and run
--network none; PDF bytes to stdout or -o. Exit codes 0/1/2.

All dev in docker (dev.sh/Makefile); unit tests + golden typst fixtures
plus a host-side smoke against the real engine container.

Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-29 05:44:24 -05:00

5.8 KiB

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:

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):

./dev.sh check   # = go build + go vet + go test, all inside the builder

Expected output (tail):

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

./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):

./dev.sh smoke

Expected output (tail):

ok   engine failure exit=2
ok   -T custom template

smoke: all checks passed; samples in out/

Input format

---
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/<name>.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

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.