CARLOS rastrillo docs

🤖 tmpl

amadan.net/rastrillo/rastrillo/tmpl

Templates that run as compiled Go when compiled code exists, and as html/template when it does not. A tmpl.Template has the part of *html/template.Template's API that Rastrillo apps use, so moving an app over is usually an import change: replace html/template with amadan.net/rastrillo/rastrillo/tmpl. It does not have AddParseTree, the Tree field, or package helpers such as HTMLEscapeString. Keep html/template for code that needs them.

An app on ui.Pages changes nothing: Pages already builds its templates with tmpl.

t := tmpl.Must(tmpl.New("layout").Funcs(funcs).ParseFS(fsys, "*.html"))
err := t.ExecuteTemplate(w, "layout", data)

FuncMap and the content types are html/template's own types, so your functions and your tmpl.HTML values need no change.

The API

func New(name string) *Template
func Must(t *Template, err error) *Template
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)

func (t *Template) Name() string
func (t *Template) New(name string) *Template
func (t *Template) Funcs(funcMap FuncMap) *Template
func (t *Template) Option(opt ...string) *Template
func (t *Template) Delims(left, right string) *Template
func (t *Template) Parse(text string) (*Template, error)
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
func (t *Template) ParseGlob(pattern string) (*Template, error)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
func (t *Template) Lookup(name string) *Template
func (t *Template) Templates() []*Template
func (t *Template) DefinedTemplates() string
func (t *Template) Defined(name string) bool
func (t *Template) Clone() (*Template, error)
func (t *Template) Execute(w io.Writer, data any) error
func (t *Template) ExecuteTemplate(w io.Writer, name string, data any) error
func (t *Template) IsCompiled(name string) bool
func (t *Template) Inspect(visit func(node parse.Node))
func (t *Template) InspectNamed(visit func(name string, node parse.Node))

type (
	FuncMap  = template.FuncMap
	HTML     = template.HTML
	HTMLAttr = template.HTMLAttr
	URL      = template.URL
	JS       = template.JS
	JSStr    = template.JSStr
	CSS      = template.CSS
	Srcset   = template.Srcset
)

const (
	ShadowEnv = "RASTRILLO_TEMPLATE_SHADOW"
	DevEnv    = "RASTRILLO_DEV"
	TraceEnv  = "RASTRILLO_TEMPLATE_TRACE"
)

Each method does what the one on *html/template.Template does, and Template is backed by a real one, so parsing, functions and errors are html/template's. Parse and Clone fail once a template has run, as they do there.

Defined reports whether the set has a template of that name, the same answer as Lookup(name) != nil. A template made with New and never parsed is in the set, and running it fails, as on html/template.

When code is compiled

rastrillo generate compiles the templates of your app's sets to Go. The code is registered under each template's fingerprint, a hash of:

The first time a set runs, each template looks up its fingerprint. A match renders through the compiled code. No match renders through html/template. Edit a template and its fingerprint changes, so old compiled code never renders new HTML.

The compiled code goes with the package that built the set. For a set that ui.Pages builds, that is your app.

When html/template runs

A template runs on html/template when:

Run rastrillo generate after you change a template. Nothing breaks if you forget, because the page renders as it always did. It is only slower.

Change a set after it has run and its compiled code turns off for the whole set. That covers Funcs, Option, Delims, and New with a name the set already has. Build the set completely before the first Execute.

The stale warning

A built binary that finds templates changed since rastrillo generate logs one warning for the set, the first time it runs:

tmpl: templates changed since the last rastrillo generate; run rastrillo generate

The entry has owner, the package the set belongs to, and templates, the names that changed. Run rastrillo generate and rebuild.

A template rastrillo generate could not compile does not warn. Edit it, and it warns like any other.

An app that has never generated code logs nothing. rastrillo dev builds your app without its compiled code, so it never warns. An app started with RASTRILLO_DEV=1 (DevEnv), as rastrillo dev starts it, logs the message at debug level.

Shadow mode

Set RASTRILLO_TEMPLATE_SHADOW=1 (ShadowEnv) to render each compiled template both ways. The page is html/template's output, and Execute fails if the compiled bytes differ. The error names the template, the entry and the first byte that differs, with the text around it.

Turn it on in tests and CI. It renders every page twice, so leave it off in production.

Shadow mode compares each entry with html/template running it first on a fresh copy of the set, which is what compiled code matches. On a set shared by several entries, html/template can write different bytes for an entry that runs after another: a partial run on its own, then called inside <script>, is escaped twice.

Each template runs twice, so data that changes while it renders, such as a channel it reads or a method that changes its receiver, comes out different the second time. Shadow mode reports that as a difference too. Turn shadow mode off for that test.

Tracing

Set RASTRILLO_TEMPLATE_TRACE=1 (TraceEnv) to log every execution and whether it ran compiled code:

INFO tmpl: executed template=layout compiled=true

Use it in a test that can only see your running app, such as a smoke test of the served pages. In a Go test, use IsCompiled. Leave tracing off in production: it logs every page.

IsCompiled

if !t.IsCompiled("layout") {
	log.Println("layout runs on html/template")
}

IsCompiled reports whether the template called name renders through compiled code. Use it in a test to catch a page that quietly fell back after a change. You can ask before the first Execute without stopping yourself from adding templates.

Inspect

t.Lookup("layout").Inspect(func(node parse.Node) {
	if text, ok := node.(*parse.TextNode); ok {
		fmt.Printf("%q\n", text.Text)
	}
})

Inspect calls visit for every node in the template and in each template it calls with {{template}}. It visits each template once. Use it to inspect what a page uses before it runs. ui.Pages uses it to find the scripts and hooks a page needs. Call it before the template set runs for the first time. Do not change the nodes.

InspectNamed is Inspect that also passes the name of the template each node is in. ui.Pages uses it to tell the layout's language and scheme menus, which are forms, from a page's own forms.

Capture

rastrillo generate compiles the sets your app really builds, so it runs your app to find them. It builds, with the rastrillo_capture tag, each program under cmd/ whose func main calls rastrillo.Run or rastrillo.Serve, and starts it with RASTRILLO_TEMPLATE_CAPTURE (CaptureEnv) set to a file. While that is set, every set your app creates is recorded. rastrillo.Serve writes them to the file with WriteCapture and exits without listening.

rastrillo generate finds the call by reading the source, without running anything, so the call must be in func main itself, not in a function it calls. Anything your program does before that call still runs during generate. Nothing after it does: capture mode exits the process after rastrillo.Serve writes the capture. A program that calls neither, such as a migration, or one that serves rastrillo.Handler on its own server, is never started: generate names it on one line, and its templates render through html/template.

The app runs in a scratch directory with only PATH, HOME, TMPDIR and LANG from your environment. A database it opens through rastrillo.OpenDB or db.Open is a new file in that directory, whatever path is configured, so capture never opens a real one.

You never call these yourself. They are exported so that generate can read the file.

const CaptureEnv = "RASTRILLO_TEMPLATE_CAPTURE"

func WriteCapture(path string) error

type Capture struct {
	Version          int
	GoVersion        string
	GeneratorVersion int
	Sets             []CapturedSet
}

type CapturedSet struct {
	Name                 string
	Owner                string
	Templates            map[string]string
	Fingerprints         map[string]string
	Funcs                map[string]FuncSig
	Option               string
	MetaContentURLEscape bool
	Uncompilable         map[string]string
}

type FuncSig struct {
	Expr     string
	Imports  []string
	Refs     []TypeRef
	Nameable bool
}

type TypeRef struct {
	PkgPath, Name string
}

A CapturedSet holds each template's source as parsed, the fingerprint the set gives it, and the Go type of each function in its FuncMap as a FuncSig. Owner is the package the compiled code goes in. Uncompilable names the templates generate must leave on html/template, with the reason.

If your app cannot start this way, for example because it needs a secret that has no default, generate names the problem and your templates stay on html/template.

Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.