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
This commit is contained in:
2026-08-29 05:44:24 -05:00
parent 97c8e340ab
commit 10e930e46d
147 changed files with 30793 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
# 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/<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
```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.