// Package render wires the HTML template engine: an embed.FS of templates // (self-contained binary — no template files to ship beside it), a Render // helper, and the FuncMap seam the template/format port fills in. // // The templates themselves are owned elsewhere (the Jinja -> html/template // port); this package only establishes HOW they are parsed and executed: // // - Files live in internal/render/templates/*.tmpl and are named by base // filename (html/template's ParseFS convention), e.g. "city.html.tmpl". // Jinja's {% extends %}/{% block %} maps to {{define}}/{{template}} with a // shared base layout; each page template {{define}}s the blocks the base // references and is executed by its own file name. // - Formatting helpers arrive via the FuncMap passed to New. html/template // FuncMaps are engine-global, so anything unit-scoped (the Python used a // ContextVar the handlers set per request — format.py's unit_scope) must // NOT be a bare FuncMap closure over mutable state; either pre-format in // the handler (content.py already did for most values) or pass a // formatter value in the render data and call its methods. // // It also owns the response-side ETag behavior ported from content.py's // _respond_html and app.py's _page: a weak SHA-1 ETag over the rendered // bytes, and the two If-None-Match comparison flavors the Python had. package render import ( "bytes" "crypto/sha1" "embed" "encoding/hex" "fmt" "html/template" "io" "net/http" "strings" ) //go:embed templates var templatesFS embed.FS // Engine holds the parsed template set. Templates only change on a redeploy // (a fresh process), so everything is parsed exactly once, at New — the Go // analogue of the Jinja environment's auto_reload=False. type Engine struct { t *template.Template } // New parses every embedded template with the given FuncMap. Call it once at // boot with the full FuncMap — the function NAMES must all be registered // before parsing, because html/template resolves them at parse time. // // The FuncMap keys the templates rely on (the Jinja globals/filters // content.py registered; implementations come from the format port): // // "temp" func(f *float64) template.HTML — N°F // "temp_bare" func(f *float64) template.HTML — bare-degree variant // "temp_class" func(f *float64) string — diverging-palette tier name // "ordinal" func(pct any) string — percentile -> "66th", floored into 1..99 // "head_verify" func() template.HTML — search-console tags // "comment" func(s string) template.HTML — a visible that // survives parsing (html/template strips literal HTML // comments; wrapping the string template.HTML inserts it as // trusted content instead of markup the parser interprets) // "jscomment" func(s string) template.JS — a visible "// ..." line // that survives inside