ZDDC/zddc/internal/convert/embed.go
ZDDC 1d816ae43a feat(server): multi-template MD→HTML with .zddc.d/templates cascade
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>
2026-06-04 14:18:40 -05:00

80 lines
2.7 KiB
Go

package convert
import (
"embed"
"io/fs"
"path"
"sort"
)
// Default pandoc HTML templates, mirrored verbatim from /pandoc/templates/ by
// the top-level ./build (shared/build-lib.sh: sync_pandoc_templates). The runner
// writes the chosen template + its partials to a host scratch dir on each HTML
// conversion and bind-mounts them into the sandbox so pandoc can `--template`
// against them.
//
// pandoc/templates/ is the single source of truth; this directory is a build
// artifact kept in sync and guarded by TestEmbeddedTemplatesMatchSource. There's
// no symlink because go:embed paths must resolve under the containing module, and
// we want the binary to ship the bytes verbatim, not depend on the source tree at
// runtime.
//
// The set holds named doctype templates (report.html, letter.html,
// specification.html) plus the shared partials they include (_head.html,
// _doc.html, _scripts.html). A document picks one via its `template:` front
// matter; operators override individual files through the .zddc.d/templates/
// cascade (see internal/handler).
// `all:` is required so the `_`-prefixed partials (_head.html, _doc.html,
// _scripts.html) are embedded — a bare `//go:embed templates` excludes names
// beginning with `_` or `.`.
//
//go:embed all:templates
var templatesFS embed.FS
// DefaultTemplateName is used when a document declares no `template:` field or
// names one that doesn't resolve.
const DefaultTemplateName = "report"
// embeddedTemplate returns the bytes of a baked-in template/partial by base file
// name (e.g. "report.html", "_head.html"), or nil if there is no such default.
func embeddedTemplate(name string) []byte {
b, err := templatesFS.ReadFile(path.Join("templates", name))
if err != nil {
return nil
}
return b
}
// embeddedTemplateFiles returns all baked-in template/partial files keyed by
// base name. The returned map is a fresh copy the caller may mutate (e.g. to
// overlay .zddc.d/templates overrides).
func embeddedTemplateFiles() map[string][]byte {
out := make(map[string][]byte)
entries, _ := fs.ReadDir(templatesFS, "templates")
for _, e := range entries {
if e.IsDir() {
continue
}
if b := embeddedTemplate(e.Name()); b != nil {
out[e.Name()] = b
}
}
return out
}
// EmbeddedTemplateNames lists the baked-in doctype template names (no extension,
// partials excluded — i.e. the names a `template:` field may select), sorted.
func EmbeddedTemplateNames() []string {
var names []string
entries, _ := fs.ReadDir(templatesFS, "templates")
for _, e := range entries {
n := e.Name()
if e.IsDir() || n == "" || n[0] == '_' || path.Ext(n) != ".html" {
continue
}
names = append(names, n[:len(n)-len(".html")])
}
sort.Strings(names)
return names
}