Skip to main content

Crate template

Crate template 

Source
Expand description

A fast, safe template engine for HTML and text rendering, inspired by Jinja2 with no_stdsupport.

template provides a runtime template engine that compiles templates into an AST and renders them via a tree-walking interpreter. It supports both HTML mode (with automatic escaping) and text mode (no escaping).

§Quick start

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Html);
engine.add_template("hello", "<p>Hello, {{ name }}!</p>");

let result = engine.render("hello", context! { name: "World" });
assert_eq!(result.unwrap(), "<p>Hello, World!</p>");

The context! macro builds a context map. You can also pass any #[derive(Serialize)] struct (requires the std feature), using & to pass a reference: engine.render("t", &my_struct).

§Trust model

Templates are trusted code, exactly like Jinja2 or MiniJinja. A template can read every variable in the rendering context and can consume CPU and memory within the configured limits (nesting depth, range() cap). Never compile untrusted template source with Engine::add_template. Untrusted data belongs in the context, where it is escaped automatically in EscapeMode::Html.

§Features

FeatureDescription
defaultEnables the std feature.
stdEnables std-dependent features such as serde integration and the ability to pass #[derive(Serialize)] structs directly to Engine::render.

§Working with slices and vectors

Iterate over a list with {% for %}:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("list", "{% for item in items %}- {{ item }}
{% endfor %}").unwrap();

let result = engine.render("list", context! {
    items: vec!["apple", "banana", "cherry"],
}).unwrap();
assert_eq!(result, "- apple\n- banana\n- cherry\n");

Access elements by index, including nested fields:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t", "{{ users[0].name }}, {{ users[1].name }}").unwrap();

let result = engine.render("t", context! {
    users: vec![
        context! { name: "Alice", age: 30 },
        context! { name: "Bob", age: 25 },
    ],
}).unwrap();
assert_eq!(result, "Alice, Bob");

Use filters on arrays — join, first, last, length, reverse:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t", "\
join: {{ items | join(\", \") }}
first: {{ items | first }}
last:  {{ items | last }}
count: {{ items | length }}
rev:   {{ items | reverse | join(\", \") }}
").unwrap();

let result = engine.render("t", context! {
    items: vec!["a", "b", "c"],
}).unwrap();
assert_eq!(result, "\
join: a, b, c
first: a
last:  c
count: 3
rev:   c, b, a
");

Check membership with the in operator:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t",
    "{% if \"admin\" in roles %}Welcome, admin!{% endif %}"
).unwrap();

let result = engine.render("t", context! {
    roles: vec!["user", "admin", "moderator"],
}).unwrap();
assert_eq!(result, "Welcome, admin!");

Render directly from a Rust Vec:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t", "{% for n in numbers %}{{ n }} {% endfor %}").unwrap();

let result = engine.render("t", context! {
    numbers: vec![10, 20, 30],
}).unwrap();
assert_eq!(result, "10 20 30 ");

Filter across a nested array field:

use template::{Engine, EscapeMode, context};

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t",
    "{% for tag in post.tags %}{{ tag | upper }} {% endfor %}"
).unwrap();

let result = engine.render("t", context! {
    post: context! {
        title: "Hello",
        tags: vec!["rust", "template", "dev"],
    },
}).unwrap();
assert_eq!(result, "RUST TEMPLATE DEV ");

§Modes

  • EscapeMode::Html — auto-escapes {{ ... }} output (escapes &, <, >, ", ')
  • EscapeMode::Text — no escaping, raw output

§Template syntax

SyntaxDescription
{{ expr }}Output expression value (auto-escaped in HTML mode)
{% if cond %}...{% elif %}...{% else %}...{% endif %}Conditional
{% for item in items %}...{% endfor %}Loop
{% include "name" %}Include another template
{% extends "base" %}Template inheritance
{% block name %}...{% endblock %}Overridable block ({% endblock name %} labels are accepted and validated)
{{ super() }}Render parent’s block content (only inside {% block %})
{% set var = expr %}Assign a variable
{% raw %}...{% endraw %}Raw text (no parsing; can contain {% sequences)
{# comment #}Comment (ignored)
expr | filter_nameApply a filter

§Expressions

  • Variable access: name, user.email, items[0] Identifiers are ASCII only ([A-Za-z_][A-Za-z0-9_]*). Non-ASCII identifiers are rejected intentionally, to rule out Unicode confusable/homoglyph and normalization attacks.
  • String literals: "hello", 'world' (may contain }}/%}; backslash escapes \n, \t, \r, \\, \', \" are supported)
  • Number literals: 42, 3.14 (scientific notation and hex are not supported)
  • Boolean: true, false
  • Comparisons: ==, !=, <, >, <=, >= (floats follow IEEE 754; NaN is never equal and every ordering comparison involving NaN is false)
  • Logical: and, or, not (strict booleans, see below)
  • Arithmetic: +, -, *, /, % (integer division/modulo by zero and integer overflow return an error; float operations follow IEEE 754)
  • Containment: item in list
  • Grouping: (expr)
  • Filters: expr | filter_name, expr | filter(arg1, arg2)
  • Function calls: super(), range(n), range(start, end), is_set(expr)

§Booleans are strict

Logical operators and conditions do not use implicit truthiness. The decision operand of and / or / not, and every {% if %} / {% elif %} condition, must evaluate to a boolean:

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t",
    "{% if is_set(user.bio) and user.verified %}ok{% else %}no{% endif %}"
).unwrap();
let result = engine.render("t", context! { user: context! { verified: true } }).unwrap();
assert_eq!(result, "no");

{{ 0 or "fallback" }} is a type error (an integer is not a boolean); write {{ x | default("fallback") }} or compare explicitly ({% if count > 0 %}). and / or still return their operand values: with a boolean decision, the result is the other operand untouched (false or "x" renders x).

§is_set

is_set(expr) returns true when expr evaluates to anything other than null/missing, and false for missing variables or fields. It never fails: nested access such as is_set(a.b.c) is false when b or c is absent.

§Safety boundaries

  • Integer division/modulo by zero and integer overflow are rejected with a render error (never a panic), in both debug and release builds. Float division/modulo by zero follows IEEE 754 (returns inf / -inf / NaN).
  • Circular includes (a -> b -> a) are limited by a depth of 64.
  • Circular extends (a extends b extends a) are limited by a depth of 128.
  • Template nesting ({% if %}/{% for %}/{% block %}) is limited to 128 at parse time, and render/eval nesting to 500, so adversarial templates return an error instead of overflowing the stack.
  • range() is capped (default 32,000 items, see Engine::set_range_limit) to bound memory use.
  • Unknown functions ({{ myfunc() }}) return a render error and unknown filters a parse error.

§Built-in filters

FilterDescription
upperConvert to uppercase
lowerConvert to lowercase
trimTrim leading/trailing whitespace
escapeHTML-escape the value (Safe result, no double-escape)
safeMark a string as safe (bypasses auto-escaping)
lengthLength of string (character count), array, or map
default(val)Return val if the input is null/missing (does not trigger on other falsy values)
capitalizeUppercase first character, lowercase the rest
titleTitle case (capitalize each word)
join(sep)Join array elements with separator
reverseReverse a string (by Unicode scalar value) or array
firstFirst element of an array or first character of a string
lastLast element of an array or last character of a string
urlencodeURL-encode (form-style, + for spaces)

Filters that only transform characters (upper, lower, trim, capitalize, title, reverse, first, last, join) preserve the Safe marker of already-safe values, so {{ x | safe | reverse }} is not escaped again.

§Error behavior

  • add_template returns an error if a template with the same name already exists, on syntax errors, when nesting limits are exceeded, and on statically-detectable type errors (e.g. {{ 0 or "x" }}).
  • Unknown filter names ({{ x | unknown }}) produce a parse-time error, as do wrong filter argument counts.
  • Exceeding the include depth (64) or extend depth (128) returns a render error.

§Architecture

              ┌──────────────┐
 add_template │              │
 ────────────▶│   PARSER     │
  (source)    │  (recursive  │
              │   descent    │
              │   parser)    │
              └──────┬───────┘
                     │ AST
              ┌──────▼───────┐
              │   ENGINE     │
              │  (template   │
              │   cache)     │
              └──────┬───────┘
                     │ render(name, ctx)
              ┌──────▼───────┐
              │  RENDERER    │
              │  (tree-walk  │
              │   VM with    │
              │   extends /  │
              │   blocks /   │
              │   includes   │
              │   resolution)│
              └──────┬───────┘
                     │ output
              ┌──────▼───────┐
              │  fmt::Write  │
              │  (String,    │
              │   Vec<u8>,   │
              │   any sink)  │
              └──────────────┘

Macros§

context
Build a context map for Engine::render without requiring Serialize.

Structs§

Context
A template rendering context.
Engine
A template engine that compiles templates into an AST and renders them.
Error
An error returned by the template engine.
SerdeError
Error returned when serializing a value fails.

Enums§

EscapeMode
Controls auto-escaping behavior.

Traits§

IntoContext
Trait for types that can be converted into a Context for template rendering.