# Template Helpers Gastro provides 24 built-in template functions available in all templates without registration. You can also add custom helpers. ## String Functions ```go {{ .Name | upper }} // "ALICE" {{ .Name | lower }} // "alice" {{ .Bio | trim }} // trims whitespace {{ .Tags | join ", " }} // "go, web, ssr" ``` | Function | Description | |----------|-------------| | `upper` | Converts string to uppercase | | `lower` | Converts string to lowercase | | `trim` | Trims leading and trailing whitespace | | `join` | Joins a slice of strings with a separator | | `split` | Splits a string by separator | | `contains` | Checks if a string contains a substring | | `replace` | Replaces occurrences in a string | ## Safety Functions These functions mark content as safe for specific contexts, bypassing `html/template`'s automatic escaping. Use them only with trusted content: ```go // Render trusted HTML {{ .Body | safeHTML }} // Safe attribute values
// Safe URLs // Safe CSS
// Safe JS ``` | Function | Marks safe for | |----------|----------------| | `safeHTML` | HTML content (renders without escaping) | | `safeAttr` | HTML attribute values | | `safeURL` | URL values in `href`/`src` attributes | | `safeCSS` | CSS property values | | `safeJS` | JavaScript values | ## Utility Functions ```go // Default values {{ .Name | default "Anonymous" }} // Time formatting {{ .CreatedAt | timeFormat "Jan 2, 2006" }} // JSON output {{ .Config | json }} // Build maps and lists inline {{ dict "key" "value" "other" 42 }} {{ list "a" "b" "c" }} // String operations {{ split .Tags "," }} {{ contains .Title "Go" }} {{ replace .Text "old" "new" }} ``` | Function | Description | |----------|-------------| | `default` | Returns value, or fallback if empty/zero | | `timeFormat` | Formats a `time.Time` using Go's layout syntax | | `json` | JSON-encodes a value | | `dict` | Creates a `map[string]any` from key-value pairs | | `list` | Creates a `[]any` from arguments | ## Membership and Lookup Templates often need to ask "is this thing in that thing?" — e.g. "is this tab the active one?", "does this dict carry an optional field?". Without helpers, authors end up declaring `activeSet := map[string]bool{...}` in frontmatter; these helpers let templates ask the question directly. ```go // Slice membership {{ if has .Tag .ActiveTags }}{{ end }} // Variadic form (no slice needed) {{ if has .Status "open" "in_progress" }}⚠️{{ end }} // Map key presence (works against any map) {{ if hasKey "Avatar" .User }}{{ end }} // The active-set idiom: build a set once, query repeatedly. {{ $active := set "home" "about" "contact" }} {{ range .Tabs }} {{ . }} {{ end }} ``` | Function | Description | |----------|-------------| | `has` | Reports whether `needle` appears in `haystack`. Accepts a slice/array or variadic arguments. Uses `reflect.DeepEqual`. | | `hasKey` | Reports whether `key` is present in `m`. Works on any map (string-keyed, int-keyed, `map[any]bool`). Returns false for non-maps rather than panicking. | | `set` | Builds a `map[any]bool` from the given items. Combine with `hasKey` for efficient repeated membership tests. Unhashable items (slices, maps, funcs) are skipped silently. | ## Attribute Forwarding These funcs render a component's `gastro.Attrs` bag and merge Tailwind class lists. See [Components → Forwarding attributes](components.md#forwarding-attributes) for the full pattern. ```go // Render a bag of forwarded attributes, with base defaults. ``` The binder runs once per request. The closures it returns capture `r`, so `{{ t "Welcome" }}` resolves against the request's locale, CSRF cookie, CSP nonce, or whatever else your middleware attached to the request context. ### When to use it | Pattern | Library | Helpers registered | |---|---|---| | Internationalisation (gettext-style) | `gotext`, `go-i18n`, hand-rolled | `t`, `tn`, `tc` | | CSRF protection | `gorilla/csrf`, custom | `csrfToken`, `csrfField` | | CSP nonces | custom (a few lines of `crypto/rand`) | `cspNonce` | | Named-route reversal | custom | `routePath` | | Asset hashing | custom | `asset` | | Feature flags | flag library of choice | `flag` | The common thread: anything that needs to read **per-request state** at template time, where pre-computing in frontmatter would be repetitive across many pages. ### Rendering from handlers and SSE When you call `gastro.Render.X(props)` from a Go handler, the static FuncMap is used — binders are *not* invoked, so request-aware helpers resolve to placeholders (typically the empty string). To bind a render call to a specific request, use `Render.With(r)`: ```go func handleUpdate(w http.ResponseWriter, r *http.Request) { html, _ := gastro.Render.With(r).Card(gastro.CardProps{Title: "Hello"}) datastar.NewSSE(w, r).PatchElements(html) } ``` The returned `*renderAPI` is reusable within a single request — store it in a local and render multiple components from it. It is **not** goroutine-safe and must not be retained beyond the request. ### Multiple binders compose You can register `WithRequestFuncs` multiple times — e.g. one for i18n, one for CSRF — and the helper sets are merged. The only constraint is that helper names must be unique across the union of: - Gastro built-ins (`upper`, `lower`, `dict`, … — see top of this page) - `WithFuncs` registrations - All `WithRequestFuncs` binders A collision panics at `gastro.New()` with both sources named: ``` gastro: helper name "t" registered twice - WithFuncs - WithRequestFuncs[1] ``` This is intentional — silent shadowing of a built-in or another binder would make template behaviour depend on registration order, which is brittle and hard to debug. ### The binder contract A `WithRequestFuncs` binder is a `func(*http.Request) template.FuncMap`. It MUST: - Return a **stable key set** — the *names* returned must not depend on request state. (Closure *bodies* may read request state freely; that's the whole point.) Gastro probes each binder once at `New()` with a synthetic request to discover its key set for collision detection. - Not panic during top-level execution when fed a probe request whose context carries no adopter-installed values. In particular, your `FromCtx`-style accessors must tolerate a missing locale / cookie / nonce and return safe zero defaults. The probe never invokes the *closures* inside the FuncMap — only the map's keys are read — but top-level statements in the binder body do run. A binder SHOULD: - Be cheap. It runs on every request. - Return a **literal `template.FuncMap{...}`** so the Gastro LSP can extract helper names via static analysis and surface them in completion / hover / go-to-definition. Dynamically constructed maps (`m := make(template.FuncMap); m["t"] = …; return m`) work at runtime but degrade the editor experience for those helpers. ### Runtime panic recovery If a binder or any helper it returned panics during request handling, Gastro recovers the panic, logs it with the panicking binder's registration index, and dispatches to your `WithErrorHandler` (default: `500 Internal Server Error`). One bad binder cannot crash the server. ### Components, slots, and wrap blocks Request-aware helpers propagate through every layer of a page render: - Page templates (`pages/foo.gastro`). - Components invoked from a page via `{{ Component . }}` or `{{ wrap Component (dict ...) }}`. - Slot content rendered inside a wrap block. - Components rendered programmatically via `gastro.Render.With(r).Component(props)`. In every case, helpers like `{{ t "…" }}`, `{{ csrfField }}`, and `{{ cspNonce }}` resolve against the right per-request state. You don't need to translate strings in the page's frontmatter and pass them as props — the helper just works inside the component template body. Under the hood, each request Clones the page's parsed template (or, in dev mode, re-parses it) and applies the per-request FuncMap to the clone. Bare component invocations are then dispatched through closures that thread the request all the way down the component tree. Cost is proportional to nesting depth; on an Apple M3 a typical component template clones in ~1.2 µs, so a 5-deep tree adds ~6 µs per request — well below the per-request budget of any real handler. See the `BenchmarkNestedClone` suite in `internal/compiler/` for the per-depth roll-up. ### Editor support The Gastro LSP discovers `WithRequestFuncs` binder helpers by AST- parsing your project's `main.go`. As long as the binder returns a literal `template.FuncMap{…}` (either inline or via a one-hop named function reference in the same file), helper names show up in: - Template completion (`{{ t` suggests `t` with detail *"request-aware helper"*). - Template parse — no spurious *"function not defined"* diagnostic. - Hover on `{{ t "…" }}` shows the binder index and a source link pointing at the FuncMap key in `main.go`. - Go-to-definition on a helper name jumps to that same FuncMap entry. Binders that build their FuncMap dynamically (e.g. by ranging over a slice, or returning a `template.FuncMap` constructed in another package) still work at runtime, but the LSP can't statically extract their keys — so completion / hover / go-to-def don't list them. To make the trade-off visible, the LSP publishes an **info-level diagnostic** on the `gastro.WithRequestFuncs(...)` call site explaining the situation and pointing at the literal-`FuncMap` workaround. ### Worked examples Three example apps in `examples/` exercise `WithRequestFuncs` along different axes: | Example | What it stresses | |---|---| | `examples/i18n/` | The motivating case. Three helpers (`t`, `tn`, `tc`) from one binder, locale detection middleware, gettext-style PO catalogues. | | `examples/csrf/` | Mixed return types in one binder (`csrfToken` returns `string`, `csrfField` returns `template.HTML`). Middleware mints + verifies tokens; helpers only read. | | `examples/csp/` | Helper-to-middleware coordination: the middleware writes a `Content-Security-Policy: nonce-X` header and the `cspNonce` helper renders the matching `