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:
@@ -0,0 +1,93 @@
|
||||
package typdoc
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"ukrrs.com/mopac/pdf/internal/frontmatter"
|
||||
"ukrrs.com/mopac/pdf/internal/markdown"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "rewrite golden files")
|
||||
|
||||
// Golden: fixture markdown -> composed typst document. The chart renderer
|
||||
// and image stager are stubbed with deterministic bytes; golden files pin
|
||||
// the emitted typst markup and template composition.
|
||||
func TestGoldenCompose(t *testing.T) {
|
||||
for _, fixture := range []string{"sample-report", "sample-brief"} {
|
||||
t.Run(fixture, func(t *testing.T) {
|
||||
src, err := os.ReadFile(filepath.Join("..", "..", "testdata", fixture+".md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meta, body, err := frontmatter.Split(string(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := meta.Template
|
||||
if name == "" {
|
||||
name = "report"
|
||||
}
|
||||
assets := Assets{}
|
||||
rendered, err := RenderBody(markdown.Parse(body), assets, Funcs{
|
||||
RenderChart: func(*markdown.Chart, string) ([]byte, error) { return []byte("PNG-STUB"), nil },
|
||||
StageImage: func(string, string) ([]byte, error) { return []byte("IMG-STUB"), nil },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc := Compose(name, meta, rendered)
|
||||
golden := filepath.Join("..", "..", "testdata", fixture+".typ.golden")
|
||||
if *update {
|
||||
if err := os.WriteFile(golden, []byte(doc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(golden)
|
||||
if err != nil {
|
||||
t.Fatalf("golden missing (run go test ./internal/typdoc -update): %v", err)
|
||||
}
|
||||
if doc != string(want) {
|
||||
t.Errorf("composed typst drifted from golden %s", golden)
|
||||
}
|
||||
if len(assets) == 0 {
|
||||
t.Errorf("expected chart asset to be staged")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInlineEscapes(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"a # b": `a \# b`,
|
||||
"cost $5 @here": `cost \$5 \@here`,
|
||||
"x [y] z": `x \[y\] z`,
|
||||
"**b** and *i*": `*b* and _i_`,
|
||||
"`c`": "`c`",
|
||||
"[t](https://x.co/a)": `#link("https://x.co/a")[t]`,
|
||||
"see [docs](/a/b) here": `see #link("/a/b")[docs] here`,
|
||||
"~tilde": `\~tilde`,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := inline(in); got != want {
|
||||
t.Errorf("inline(%q) = %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypstString(t *testing.T) {
|
||||
if got := typstString(`a "b" \c` + "\n"); got != `"a \"b\" \\c\n"` {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposePreamble(t *testing.T) {
|
||||
doc := Compose("brief", frontmatter.Meta{Title: "T"}, "BODY")
|
||||
want := "#import \"/template.typ\": brief\n#show: brief.with(\n title: \"T\",\n subtitle: \"\",\n author: \"\",\n date: \"\",\n classification: \"\",\n)\n\nBODY"
|
||||
if doc != want {
|
||||
t.Fatalf("got:\n%s", doc)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user