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
181 lines
5.8 KiB
Markdown
181 lines
5.8 KiB
Markdown
# 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 `` 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.
|