Skip to main content

Crate gotmpl

Crate gotmpl 

Source
Expand description

§gotmpl

Test GitHub License Crates.io Version docs.rs Crates.io MSRV

A Rust port of Go’s text/template.

Supports the full template syntax (pipelines, control flow, custom functions, template composition, whitespace trimming) with Go compatible output. no_std compatible (with alloc).

The crate forbids unsafe code and denies the panic-family lints (panic!, unwrap, expect, unreachable!, todo!, unimplemented!). User-provided functions that panic are caught under std; see no_std notes below.

§Quick start

use gotmpl::{Template, tmap};

let data = tmap! { "Name" => "World" };
let result = Template::new("hello")
    .parse("Hello, {{.Name}}!")
    .unwrap()
    .execute_to_string(&data)
    .unwrap();
assert_eq!(result, "Hello, World!");

For one-shot renders with no configuration, use gotmpl::execute (source string) or gotmpl::execute_file (reads from disk):

use gotmpl::{execute, tmap};

let result = execute("Hello, {{.Name}}!", &tmap! { "Name" => "World" }).unwrap();
assert_eq!(result, "Hello, World!");

§HTML auto-escaping (html feature)

With the html feature, gotmpl::html::Template is a drop-in analog of Template that context-aware auto-escapes its output, exactly like Go’s html/template. It is a distinct type on purpose: escaping can’t be forgotten, and passing a non-escaping Template where escaped output is required won’t compile.

use gotmpl::html::Template;
use gotmpl::tmap;

let data = tmap! {
    "Comment" => "<script>alert('xss')</script>",
    "Link"    => "javascript:alert(1)",
};
let out = Template::new("page")
    .parse(r#"<p>{{.Comment}}</p><a href="{{.Link}}">x</a>"#)
    .unwrap()
    .execute_to_string(&data)
    .unwrap();
assert_eq!(
    out,
    r#"<p>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</p><a href="#ZgotmplZ">x</a>"#,
);

Each interpolation is escaped for its surrounding context — HTML text, tag and attribute names, quoted/unquoted attribute values, RCDATA (<textarea>, <title>), URLs, srcset, JavaScript (value/string/regexp/template-literal), and CSS. An unsafe URL scheme becomes #ZgotmplZ, and other rejected values become ZgotmplZ, matching Go.

Trusted content that should bypass escaping is wrapped in one of the content types — html::HTML, HTMLAttr, JS, JSStr, CSS, URL, Srcset (analogs of Go’s template.HTML etc.), which produce a Value::Safe:

use gotmpl::html::{Template, HTML};
use gotmpl::tmap;

let data = tmap! { "Body" => HTML::from("<b>trusted</b>") };
let out = Template::new("t")
    .parse("{{.Body}}")
    .unwrap()
    .execute_to_string(&data)
    .unwrap();
assert_eq!(out, "<b>trusted</b>"); // emitted verbatim in HTML text context

Escaping runs once, lazily, on the first execute* call (so {{template "x"}} can reference templates added beforehand); after that the template can no longer be parsed into. The feature is no_std-compatible. See examples/html.rs (cargo run --example html --features html).

§Serde integration (serde feature)

With the serde feature, any serde::Serialize type becomes template data with no hand-written conversion. gotmpl::to_value (mirroring serde_json::to_value) turns it into a Value, and the ToSerdeValue extension trait adds an equivalent .to_serde_value() method:

use gotmpl::{Template, ToSerdeValue, to_value};
use serde::Serialize;

// Go templates access exported PascalCase fields (`{{.Name}}`), but serde
// derives field names verbatim, so add `rename_all` to line the keys up.
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
struct User { name: String, age: u8, roles: Vec<String> }

let user = User { name: "Alice".into(), age: 30, roles: vec!["admin".into()] };

let tmpl = Template::new("t")
    .parse("{{.Name}} ({{.Age}}){{range .Roles}} {{.}}{{end}}")
    .unwrap();

let data = to_value(&user).unwrap();        // or: user.to_serde_value().unwrap()
assert_eq!(tmpl.execute_to_string(&data).unwrap(), "Alice (30) admin");

Both entry points return Result<Value> (serde serialization can fail on a non-string map key or an out-of-range 128-bit integer) and, like ToValue, materialize the whole value eagerly. Structs and maps become Value::Map, so field access uses map semantics: a missing field yields <no value> by default. The feature is no_std-compatible. See examples/serde.rs (cargo run --example serde --features serde) and the ser module docs for the rest of the mapping (map-key stringification and ordering-vs-Go).

§Template syntax

Actions are delimited by {{ and }} (configurable via .delims()).

§Data access

{{.}}              Current context (dot)
{{.Name}}          Field access on dot
{{.User.Email}}    Nested field access
{{$}}              Top-level data (root context)
{{$x}}             Variable access
{{$x.Name}}        Field access on variable

§Pipelines

{{.Name | printf "%s!"}}
{{"hello" | len | printf "%d chars"}}

§Control flow

{{if .Condition}}...{{end}}
{{if .Cond}}...{{else}}...{{end}}
{{if eq .X 1}}...{{else if eq .X 2}}...{{else}}...{{end}}

{{range .Items}}...{{end}}
{{range .Items}}...{{else}}empty{{end}}
{{range $i, $v := .Items}}{{$i}}: {{$v}}{{end}}
{{range .Items}}{{if eq . 3}}{{break}}{{end}}{{.}}{{end}}
{{range .Items}}{{if eq . 3}}{{continue}}{{end}}{{.}}{{end}}
{{range 5}}{{.}} {{end}}

{{with .Value}}...{{end}}
{{with .Value}}...{{else}}fallback{{end}}

§Variables

{{$x := .Name}}            Declare variable
{{$x = "new value"}}       Assign to existing variable
{{$i, $v := range .List}}  Range with index and value (declaration)
{{$i, $v = range .List}}   Range with index and value (assignment)

§Template composition

{{define "name"}}...{{end}}        Define a named template
{{template "name" .}}              Invoke a named template
{{block "name" .}}default{{end}}   Define and invoke (with default)

§Comments and whitespace trimming

{{/* This is a comment */}}
{{- .X}}     Trim whitespace before
{{.X -}}     Trim whitespace after
{{- .X -}}   Trim both sides

§Built-in functions

FunctionDescription
printConcatenate args (spaces between non-string adjacent args)
printfFormatted output (see below)
printlnPrint with spaces between args, trailing newline
lenLength of string, list, or map
indexIndex into list or map: index .List 0, index .Map "key"
sliceSlice a list or string: slice .List 1 3
callCall a function value: call .Func arg1 arg2
eq, ne, lt, le, gt, geComparison operators. eq supports multi-arg: eq .X 1 2 3
and, orShort-circuit logic, return the deciding value
notBoolean negation
html, js, urlqueryEscape for HTML, JavaScript, URL query

§printf verbs and flags

Format strings follow Go’s fmt syntax: %[flags][width][.precision][argument index]verb.

VerbApplies toOutput
%sstringThe string itself
%qstring, int(rune)Go-quoted string, or single-quoted rune literal
%vanyDefault formatted value
%dintDecimal
%bintBinary
%ointOctal
%x, %Xint, stringLower/upper hex (on strings: hex of each byte)
%cintUnicode scalar
%UintUnicode U+XXXX (with #: appends 'c')
%ffloatDecimal, no exponent
%e, %EfloatScientific notation (lower/upper e)
%g, %Gfloat%e/%E for large exponents, else %f
%tbooltrue / false
%%Literal %

The integer verbs (%d, %b, %o, %x/%X, %c, %U, and %q for runes) accept both signed (int) and unsigned (uint) values; uint values above i64::MAX print their true magnitude.

Flags: - (left-align), + (always sign numerics), (leading space for non-negative numerics), # (alternate form: 0b/0o/0x/0X prefix, or quoted rune for %U), 0 (zero-pad numerics).

Width and .precision accept either a literal number or * / .* to read the value from the next argument (which must be an integer — floats and other types yield %!(BADWIDTH) / %!(BADPREC)).

Argument indexing with %[N]verb selects the N-th (1-based) argument and resets the sequential cursor. Out-of-range or malformed indices produce %!verb(BADINDEX).

Mismatched verb/argument pairs produce Go’s error markers rather than panicking: %!v(BADVERB), %!v(MISSING), %!(EXTRA …) for unconsumed args, and the BADWIDTH / BADPREC / BADINDEX forms above.

§Custom functions

Register functions before parsing:

use gotmpl::{Template, tmap};
use gotmpl::Value;

let result = Template::new("test")
    .func("upper", |args| {
        match args.first() {
            Some(Value::String(s)) => Ok(Value::String(s.to_uppercase().into())),
            _ => Ok(Value::Nil),
        }
    })
    .parse("{{.Name | upper}}")
    .unwrap()
    .execute_to_string(&tmap! { "Name" => "hello" })
    .unwrap();
assert_eq!(result, "HELLO");

§Function values and call

Value::Function allows passing callable values through templates:

extern crate alloc;
use alloc::sync::Arc;
use gotmpl::{Template, tmap};
use gotmpl::{Value, ValueFunc};

let adder: ValueFunc = Arc::new(|args| {
    let sum: i64 = args.iter().filter_map(|a| a.as_int()).sum();
    Ok(Value::Int(sum))
});

let result = Template::new("test")
    .func("getAdder", move |_| Ok(Value::Function(adder.clone())))
    .parse("{{call (getAdder) 3 4}}")
    .unwrap()
    .execute_to_string(&tmap!{})
    .unwrap();
assert_eq!(result, "7");

§Options

use gotmpl::{Template, MissingKey};

let tmpl = Template::new("t")
    .missing_key(MissingKey::Error)   // error on missing map keys
    .delims("<<", ">>")              // custom delimiters
    .parse("<< .Name >>")
    .unwrap();

MissingKey implements FromStr, so you can parse from strings (useful for config files or CLI args):

use gotmpl::MissingKey;

let mk: MissingKey = "error".parse().unwrap();
MissingKey variantFromStr valueBehavior
Invalid (default)"invalid", "default"Return <no value>
ZeroValue"zero"Return <no value>
Error"error"Return an error

ZeroValue exists for parity with Go’s {{options "missingkey=zero"}} directive. Since Value is untyped, it behaves the same as Invalid; the variant is there so callers can still opt in to the named option.

§Number literals

Go-compatible number literal syntax:

{{42}}          Decimal
{{3.14}}        Float
{{0xFF}}        Hexadecimal
{{0o77}}        Octal
{{0377}}        Octal (legacy leading zero)
{{0b1010}}      Binary
{{1_000_000}}   Underscore separators
{{'a'}}         Character literal (97)
{{0x1.ep+2}}    Hex float (7.5)

§Data model

Template data uses the Value enum:

VariantRust typeGo equivalent
Niln/anil
Bool(bool)boolbool
Int(i64)i64int
Uint(u64)u64uint
Float(f64)f64float64
String(Arc<str>)Stringstring
List(Arc<[Value]>)Vec<Value>[]any
Map(Arc<BTreeMap<Arc<str>, Value>>)BTreeMapmap[string]any
Function(ValueFunc)Arc<dyn Fn>func(...)

The tmap! macro builds data maps:

use gotmpl::{tmap, ToValue};

let data = tmap! {
    "Name" => "Alice",
    "Age" => 30i64,
    "Scores" => vec![95i64, 87, 92],
    "Address" => tmap! {
        "City" => "Paris",
    },
};

Custom types reach the engine two ways: implement ToValue by hand, or enable the serde feature, derive serde::Serialize, and convert via gotmpl::to_value / .to_serde_value().

§no_std support

The crate works in no_std environments (requires alloc). Disable the default std feature:

[dependencies]
gotmpl = { version = "0.3", default-features = false }

Without std, execute_fmt and execute_to_string are available. The io::Write-based execute/execute_template methods and parse_files require the std feature. User-defined functions that panic will propagate instead of being caught.

§Differences from Go

Rust has no runtime reflection, so:

  • No struct field access: use Value::Map instead
  • No method calls: .Foo is always a field lookup against a Value::Map, never a method invocation. Register the callable via .func(), or store it as Value::Function and dispatch with the call builtin.
  • No pointer/interface indirection: Value is always concrete
  • No complex numbers, channels, or iter.Seq
  • No typed-nil: Option::<T>::None collapses to untyped Value::Nil. Go’s (*Foo)(nil) != nil distinction does not exist here.
  • NaN comparisons return an error instead of Go’s silently wrong results

API shape is also a bit different:

  • Template::lookup returns the parsed body (Option<&ListNode>) rather than a re-executable *Template. To run a named definition, use execute_template_to_string("name", ...) on the parent.
  • Template::templates returns the list of defined names, not Template objects — definitions share the parent’s func map and options, so there’s no per-definition handle to hand back.
  • parse_files requires the files to be valid UTF-8. Go’s os.ReadFile + string(b) is a zero-copy reinterpret and accepts any bytes; we use std::fs::read_to_string, which validates.
  • parse_glob is gated behind the glob cargo feature (on by default, pulls in the glob crate). It accepts a strict superset of Go’s filepath.Match: *, ?, [abc] / [!abc] classes, and ** for recursive descent (Go does not support **). Disabling the feature drops the dependency and removes the API. Match ordering follows the glob crate (sorted within each directory, depth-first across directories) and may differ from Go’s filepath.Glob when the pattern crosses multiple directories — relevant if templates redefine each other (last-wins). Matching is case-sensitive and * matches leading-dot files (the glob::glob default — MatchOptions::new(), case_sensitive = true, require_literal_leading_dot = false), so *.tmpl matches .hidden.tmpl. Both match Go’s filepath.Match / filepath.Glob, which also match dotfiles (unlike shell globbing).
  • Empty / identical custom delimiters: delims("", "}}") and delims("{{", "") produce a parse error rather than substituting the defaults (Go silently substitutes); delims("##", "##") is accepted (Go forbids identical left/right). These are pinned by tests so changes to validation are intentional.

Error reporting differs in a couple of places:

  • Runtime errors carry no source position. MissingKey, UndefinedFunction, and other exec-time errors print just the message; Go prefixes them with template: NAME:LINE:COL:. Tracked as a known gap.
  • The parser stops at the first error. Go’s parser collects multiple parse errors per pass; we surface exactly one.

A few data and formatting edge cases diverge too:

  • \NNN octal escapes with value ≥ 0x80 encode as the Unicode codepoint U+0080..U+00FF (a 2-byte UTF-8 sequence) where Go emits the single byte 0xNN. Same root cause as the slice divergence below: Value::String is UTF-8, Go strings are byte-slices.
  • %#v falls back to %v. Go’s Go-syntax output ([]interface {}{1, 2, 3}, map[string]interface {}{"a":1}) needs concrete-type info we don’t carry.
  • %#U quotes some non-ASCII codepoints that Go skips. Our gate is char::is_control; Go uses the full strconv.IsPrint table, which also rejects non-ASCII spacing characters like U+00A0.
  • slice on a string at a mid-codepoint offset returns an error. Go would hand back the raw bytes (potentially invalid UTF-8); we can’t store invalid UTF-8 in Value::String, so we reject the slice instead.

§Go cross-check

The test suite can optionally run every template through Go’s text/template and assert output parity:

cargo test --features go-crosscheck

A Go helper (tests/testdata/go_crosscheck.go) is compiled once per test run. It reads templates and typed data from stdin as JSON, executes them via Go’s text/template, and prints the result.

Re-exports§

pub use ser::ToSerdeValue;serde
pub use ser::to_value;serde

Modules§

htmlhtml
Go html/template-style context-aware auto-escaping.
parse
Parser, lexer, and AST node types for the Go template language.
serserde
serde integration: convert any serde::Serialize type into a Value.

Macros§

tmap
Creates a Value::Map from key-value pairs, similar to Go’s map literals.

Structs§

Template
A parsed template, equivalent to Go’s template.Template.

Enums§

EscapeErrorCodehtml
The code for a context-aware escaping error, mirroring the ErrorCode values of Go’s html/template package (its error.go). Available only with the html feature.
MissingKey
Controls behavior when accessing a missing key on a Value::Map. Set via Template::missing_key.
SafeKindhtml
The content type of a Value::Safe — the seven trusted-content kinds of Go’s html/template (template.HTML, template.JS, …). Available only with the html feature.
TemplateError
The error type returned by all template operations.
Value
The dynamic type for template data.

Traits§

ToValue
Trait for converting Rust types into template Values.

Functions§

execute
Parse and execute a template in one shot.
execute_filestd
Parse a template file and execute it in one shot.
html_escape
HTML-escape a string, replacing &, <, >, ", ', and NUL bytes.
is_true
Reports whether a Value is “true” according to Go’s template truthiness rules.
js_escape
JavaScript-escape a string for safe embedding in JS string literals.
url_encode
Percent-encode a string for use in URL query parameters (form-encoding).

Type Aliases§

FuncMap
A map from function names to template functions, equivalent to Go’s template.FuncMap. Pass one to Template::funcs to register several functions at once.
Result
Alias for Result<T, TemplateError>, the return type of every fallible operation in this crate.
ValueFunc
Alias for a callable function stored inside Value::Function.