Files
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

99 lines
2.8 KiB
Typst

// report.typ — mopac-pdf "report" template: clean business report with
// title page, TOC, numbered headings, headers/footers with classification.
#let report(
title: "Untitled",
subtitle: "",
author: "",
date: "",
classification: "",
doc,
) = {
let accent = rgb("#1f4e79")
let ink = rgb("#1a1a1a")
let rule = luma(205)
set page(
paper: "a4",
margin: (top: 2.6cm, bottom: 2.6cm, x: 2.4cm),
)
set text(font: "Libertinus Serif", size: 10.5pt, fill: ink, lang: "en")
set par(justify: true, leading: 0.65em, spacing: 1.15em)
set heading(numbering: "1.1.")
show heading: set block(above: 1.5em, below: 1.0em)
show heading.where(level: 1): it => {
text(size: 16pt, fill: accent, weight: "bold")[#it]
v(-0.6em)
line(length: 100%, stroke: 0.8pt + accent)
v(0.3em)
}
show heading.where(level: 2): it => {
text(size: 13pt, weight: "bold")[#it]
v(-0.55em)
line(length: 100%, stroke: 0.4pt + rule)
v(0.25em)
}
show heading.where(level: 3): it => text(size: 11.5pt, weight: "bold")[#it]
show heading: it => {
if it.level >= 4 { text(size: 10.5pt, weight: "bold", style: "italic")[#it] } else { it }
}
// Running header from page 2; footer everywhere with classification.
set page(header: context {
if counter(page).get().first() > 1 [
#set text(size: 8.5pt, fill: luma(95))
#grid(
columns: (1fr, auto),
align: (left + horizon, right + horizon),
text(style: "italic")[#title],
if classification != "" [#classification],
)
#v(-0.55em)
#line(length: 100%, stroke: 0.4pt + rule)
]
})
set page(footer: context {
set text(size: 8.5pt, fill: luma(95))
grid(
columns: (1fr, auto, 1fr),
align: (left + horizon, center + horizon, right + horizon),
if date != "" [#date],
counter(page).display("1 / 1", both: true),
if classification != "" [#classification],
)
})
// Title page.
align(center + horizon)[
#v(1fr)
#block(width: 76%)[
#if classification != "" [
#text(size: 10pt, fill: accent, tracking: 2.5pt, weight: "semibold")[#upper(classification)]
#v(1.4em)
]
#text(size: 26pt, weight: "bold", fill: ink)[#title]
#if subtitle != "" [#v(0.6em) #text(size: 14pt, fill: luma(80))[#subtitle]]
#v(1.3em)
#line(length: 30%, stroke: 1.2pt + accent)
#v(1.3em)
#text(size: 11pt, fill: luma(60))[
#if author != "" [#author]
#if author != "" and date != "" [#h(1.6em)]
#if date != "" [#date]
]
]
#v(1fr)
]
pagebreak()
// TOC when there is structure to show.
context {
let heads = query(heading)
if heads.len() >= 2 [
#outline(title: [Table of Contents], depth: 2)
#pagebreak()
]
}
doc
}