Skip to main content

Crate template

Crate template 

Source
Expand description

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

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, args};

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

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

The args! macro builds a context map without requiring serde_json. You can also pass any #[derive(Serialize)] struct, or serde_json::Value.

§Passing context with args!

The args! macro builds a context map without requiring serde_json:

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

let mut engine = Engine::new(EscapeMode::Text);
engine.add_template("t", "{{ name }} is {{ age }}").unwrap();

let result = engine.render("t", args! {
    name: "Alice",
    age: 30,
}).unwrap();
assert_eq!(result, "Alice is 30");

Quoted keys (e.g. "my-key") are also accepted for programmatic context construction from external data, though template variables must currently be valid Rust identifiers.

§Working with slices and vectors

Iterate over a list with {% for %}:

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

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

let result = engine.render("list", args! {
    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, args};

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

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

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

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

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", args! {
    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, args};

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

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

Render directly from a Rust Vec:

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

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

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

Filter across a nested array field:

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

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", args! {
    post: args! {
        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
{{ 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]
  • String literals: "hello", 'world'
  • Number literals: 42, 3.14 (scientific notation and hex are not supported)
  • Boolean: true, false
  • Comparisons: ==, !=, <, >, <=, >= (floats follow IEEE 754; NaN is falsy and NaN compared to anything is false)
  • Logical: and, or, not
  • Arithmetic: +, -, *, /, % (dividing or modulo by zero returns an error)
  • Containment: item in list
  • Grouping: (expr)
  • Filters: expr | filter_name, expr | filter(arg1, arg2)
  • Function calls: super(), range(n), range(start, end)

§Safety boundaries

  • Integer division/modulo by zero is rejected with a render error (not a panic). Float division/modulo by zero follows IEEE 754 (returns inf / -inf / NaN).
  • Circular includes (a -> b -> a) are detected by a depth limit (64).
  • Circular extends (a extends b extends a) are detected by a depth limit (128).
  • Unknown functions ({{ myfunc() }}) return a render 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 falsy (i.e. false, 0, 0.0, NaN, "", [], null)
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)

§Error behavior

  • add_template returns an error if a template with the same name already exists.
  • Unknown filter names ({{ x | unknown }}) produce a parse-time error.
  • 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>,   │
              │   io::Write) │
              └──────────────┘

Re-exports§

pub use engine::Engine;
pub use engine::EscapeMode;
pub use error::Error;
pub use value::Value;

Modules§

engine
error
Error types for parse, syntax, render, and type errors with source positions.
expr
Expression lexer and recursive-descent parser for template expressions.
value
Dynamic value type used during template rendering, with serde serialization support.

Macros§

args
Build a context map for Engine::render without requiring serde_json.