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).
§Features
| Feature | Description |
|---|---|
default | Enables the std feature. |
std | Enables std-dependent features such as std::error::Error on error types, 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
| Syntax | Description |
|---|---|
{{ 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_name | Apply 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;NaNis falsy andNaNcompared to anything isfalse) - 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
| Filter | Description |
|---|---|
upper | Convert to uppercase |
lower | Convert to lowercase |
trim | Trim leading/trailing whitespace |
escape | HTML-escape the value (Safe result, no double-escape) |
safe | Mark a string as safe (bypasses auto-escaping) |
length | Length of string (character count), array, or map |
default(val) | Return val if the input is falsy (i.e. false, 0, 0.0, NaN, "", [], null) |
capitalize | Uppercase first character, lowercase the rest |
title | Title case (capitalize each word) |
join(sep) | Join array elements with separator |
reverse | Reverse a string (by Unicode scalar value) or array |
first | First element of an array or first character of a string |
last | Last element of an array or last character of a string |
urlencode | URL-encode (form-style, + for spaces) |
§Error behavior
add_templatereturns 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) │
└──────────────┘Macros§
- context
- Build a context map for
Engine::renderwithout requiringSerialize.
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.
- Serde
Error - Error returned when serializing a value fails.
Enums§
- Escape
Mode - Controls auto-escaping behavior.
Traits§
- Into
Context - Trait for types that can be converted into a
Contextfor template rendering.