The convert engine renders markdown→HTML/PDF through named doctype templates selected by the document's `template:` front matter, with per-project/per-party overrides. convert package: - embed.go now embeds the whole templates/ dir (all: prefix so _-prefixed partials are included) as an embed.FS; drop the single viewer-template.html + custom.css embeds. New TemplateSet type + DefaultTemplateSet(name) returning the chosen doctype + its partials. - ToHTML/ToPDF take a TemplateSet; writeTemplateSetToScratch materialises the template + partials flat into the per-call scratch dir (pandoc resolves $partial()$ from the template's own directory). handler: - converttemplate.go: templateNameFromFrontMatter (YAML front-matter scan, sanitized to a bare basename) + resolveTemplateSet, which overlays <level>/.zddc.d/templates/<name>.html overrides onto the embedded defaults, walking docDir→fsRoot so a party dir beats the project-global dir. An override may replace a doctype, a partial, or add a brand-new doctype. - buildAndStore threads fsRoot + source into the html/pdf paths. build: pandoc/templates/ is the single source of truth; shared/build-lib.sh sync_pandoc_templates mirrors it into the embed dir on every build (cmp-guarded, stale-pruning). convert.TestEmbeddedTemplatesMatchSource fails on drift. Tests: drift + DefaultTemplateSet (convert); front-matter parse + cascade override precedence (handler). Full ./... suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
3.3 KiB
Go
95 lines
3.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestTemplateNameFromFrontMatter(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
src string
|
|
want string
|
|
}{
|
|
{"plain", "---\ntemplate: specification\n---\n\n# H\n", "specification"},
|
|
{"quoted", "---\ntemplate: \"letter\"\n---\n", "letter"},
|
|
{"absent", "---\ntitle: X\n---\n", ""},
|
|
{"no-frontmatter", "# Just a heading\n", ""},
|
|
{"traversal-rejected", "---\ntemplate: ../../etc/passwd\n---\n", ""},
|
|
{"slash-rejected", "---\ntemplate: a/b\n---\n", ""},
|
|
{"crlf", "---\r\ntemplate: report\r\n---\r\n", "report"},
|
|
{"dots-terminator", "---\ntemplate: letter\n...\nbody", "letter"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := templateNameFromFrontMatter([]byte(c.src)); got != c.want {
|
|
t.Errorf("templateNameFromFrontMatter(%q) = %q, want %q", c.src, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateSet_DefaultsAndCascade(t *testing.T) {
|
|
root := t.TempDir()
|
|
party := filepath.Join(root, "working", "AcmeCo")
|
|
if err := os.MkdirAll(party, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// No overrides, no front matter → embedded report, partials present.
|
|
ts := resolveTemplateSet(root, party, []byte("# Hi\n"))
|
|
if ts.Name != "report.html" {
|
|
t.Fatalf("default doctype: got %q, want report.html", ts.Name)
|
|
}
|
|
if ts.Files["_head.html"] == nil {
|
|
t.Errorf("partial _head.html missing from default set")
|
|
}
|
|
embeddedReport := string(ts.Files["report.html"])
|
|
|
|
// Front matter selects a doctype.
|
|
if ts := resolveTemplateSet(root, party, []byte("---\ntemplate: letter\n---\n")); ts.Name != "letter.html" {
|
|
t.Errorf("front-matter doctype: got %q, want letter.html", ts.Name)
|
|
}
|
|
|
|
// Project-global override at <root>/.zddc.d/templates/report.html.
|
|
projDir := filepath.Join(root, ".zddc.d", "templates")
|
|
if err := os.MkdirAll(projDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(projDir, "report.html"), []byte("PROJECT-REPORT"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ts = resolveTemplateSet(root, party, []byte("# Hi\n"))
|
|
if string(ts.Files["report.html"]) != "PROJECT-REPORT" {
|
|
t.Errorf("project override not applied: %q", ts.Files["report.html"])
|
|
}
|
|
if string(ts.Files["report.html"]) == embeddedReport {
|
|
t.Errorf("override identical to embedded — overlay didn't happen")
|
|
}
|
|
|
|
// Party override wins over project-global.
|
|
partyDir := filepath.Join(party, ".zddc.d", "templates")
|
|
if err := os.MkdirAll(partyDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(partyDir, "report.html"), []byte("PARTY-REPORT"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ts = resolveTemplateSet(root, party, []byte("# Hi\n"))
|
|
if string(ts.Files["report.html"]) != "PARTY-REPORT" {
|
|
t.Errorf("party override should win: got %q", ts.Files["report.html"])
|
|
}
|
|
|
|
// A brand-new doctype provided only as an override is adopted as primary.
|
|
if err := os.WriteFile(filepath.Join(partyDir, "memo.html"), []byte("MEMO-TEMPLATE"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ts = resolveTemplateSet(root, party, []byte("---\ntemplate: memo\n---\n"))
|
|
if ts.Name != "memo.html" || string(ts.Files["memo.html"]) != "MEMO-TEMPLATE" {
|
|
t.Errorf("custom doctype override: name=%q bytes=%q", ts.Name, ts.Files["memo.html"])
|
|
}
|
|
if ts.Files["_head.html"] == nil {
|
|
t.Errorf("partials should still ride along with a custom doctype override")
|
|
}
|
|
}
|