template/template.rs
1#![no_std]
2
3//! A fast, safe template engine for HTML and text rendering, inspired by Jinja2
4//! with `no_std`support.
5//!
6//! `template` provides a runtime template engine that compiles templates into
7//! an AST and renders them via a tree-walking interpreter. It supports both
8//! HTML mode (with automatic escaping) and text mode (no escaping).
9//!
10//! # Quick start
11//!
12//! ```rust
13//! use template::{Engine, EscapeMode, context};
14//!
15//! let mut engine = Engine::new(EscapeMode::Html);
16//! engine.add_template("hello", "<p>Hello, {{ name }}!</p>");
17//!
18//! let result = engine.render("hello", context! { name: "World" });
19//! assert_eq!(result.unwrap(), "<p>Hello, World!</p>");
20//! ```
21//!
22//! The [`context!`] macro builds a context map.
23//! You can also pass any `#[derive(Serialize)]` struct (requires the `std`
24//! feature), using `&` to pass a reference: `engine.render("t", &my_struct)`.
25//!
26//! # Trust model
27//!
28//! Templates are **trusted code**, exactly like Jinja2 or MiniJinja. A template
29//! can read every variable in the rendering context and can consume CPU and
30//! memory within the configured limits (nesting depth, `range()` cap). Never
31//! compile untrusted *template source* with [`Engine::add_template`]. Untrusted
32//! *data* belongs in the context, where it is escaped automatically in
33//! [`EscapeMode::Html`].
34//!
35//! # Features
36//!
37//! | Feature | Description |
38//! |------------|-----------------------------------------------------------------------------|
39//! | `default` | Enables the `std` feature. |
40//! | `std` | Enables `std`-dependent features such as `serde` integration and the ability to pass `#[derive(Serialize)]` structs directly to [`Engine::render`]. |
41//!
42//! # Working with slices and vectors
43//!
44//! Iterate over a list with `{% for %}`:
45//!
46//! ```rust
47//! use template::{Engine, EscapeMode, context};
48//!
49//! let mut engine = Engine::new(EscapeMode::Text);
50//! engine.add_template("list", "{% for item in items %}- {{ item }}
51//! {% endfor %}").unwrap();
52//!
53//! let result = engine.render("list", context! {
54//! items: vec!["apple", "banana", "cherry"],
55//! }).unwrap();
56//! assert_eq!(result, "- apple\n- banana\n- cherry\n");
57//! ```
58//!
59//! Access elements by index, including nested fields:
60//!
61//! ```rust
62//! use template::{Engine, EscapeMode, context};
63//!
64//! let mut engine = Engine::new(EscapeMode::Text);
65//! engine.add_template("t", "{{ users[0].name }}, {{ users[1].name }}").unwrap();
66//!
67//! let result = engine.render("t", context! {
68//! users: vec![
69//! context! { name: "Alice", age: 30 },
70//! context! { name: "Bob", age: 25 },
71//! ],
72//! }).unwrap();
73//! assert_eq!(result, "Alice, Bob");
74//! ```
75//!
76//! Use filters on arrays — `join`, `first`, `last`, `length`, `reverse`:
77//!
78//! ```rust
79//! use template::{Engine, EscapeMode, context};
80//!
81//! let mut engine = Engine::new(EscapeMode::Text);
82//! engine.add_template("t", "\
83//! join: {{ items | join(\", \") }}
84//! first: {{ items | first }}
85//! last: {{ items | last }}
86//! count: {{ items | length }}
87//! rev: {{ items | reverse | join(\", \") }}
88//! ").unwrap();
89//!
90//! let result = engine.render("t", context! {
91//! items: vec!["a", "b", "c"],
92//! }).unwrap();
93//! assert_eq!(result, "\
94//! join: a, b, c
95//! first: a
96//! last: c
97//! count: 3
98//! rev: c, b, a
99//! ");
100//! ```
101//!
102//! Check membership with the `in` operator:
103//!
104//! ```rust
105//! use template::{Engine, EscapeMode, context};
106//!
107//! let mut engine = Engine::new(EscapeMode::Text);
108//! engine.add_template("t",
109//! "{% if \"admin\" in roles %}Welcome, admin!{% endif %}"
110//! ).unwrap();
111//!
112//! let result = engine.render("t", context! {
113//! roles: vec!["user", "admin", "moderator"],
114//! }).unwrap();
115//! assert_eq!(result, "Welcome, admin!");
116//! ```
117//!
118//! Render directly from a Rust `Vec`:
119//!
120//! ```rust
121//! use template::{Engine, EscapeMode, context};
122//!
123//! let mut engine = Engine::new(EscapeMode::Text);
124//! engine.add_template("t", "{% for n in numbers %}{{ n }} {% endfor %}").unwrap();
125//!
126//! let result = engine.render("t", context! {
127//! numbers: vec![10, 20, 30],
128//! }).unwrap();
129//! assert_eq!(result, "10 20 30 ");
130//! ```
131//!
132//! Filter across a nested array field:
133//!
134//! ```rust
135//! use template::{Engine, EscapeMode, context};
136//!
137//! let mut engine = Engine::new(EscapeMode::Text);
138//! engine.add_template("t",
139//! "{% for tag in post.tags %}{{ tag | upper }} {% endfor %}"
140//! ).unwrap();
141//!
142//! let result = engine.render("t", context! {
143//! post: context! {
144//! title: "Hello",
145//! tags: vec!["rust", "template", "dev"],
146//! },
147//! }).unwrap();
148//! assert_eq!(result, "RUST TEMPLATE DEV ");
149//! ```
150//!
151//! # Modes
152//!
153//! - `EscapeMode::Html` — auto-escapes `{{ ... }}` output (escapes `&`, `<`, `>`, `"`, `'`)
154//! - `EscapeMode::Text` — no escaping, raw output
155//!
156//! # Template syntax
157//!
158//! | Syntax | Description |
159//! |--------|-------------|
160//! | `{{ expr }}` | Output expression value (auto-escaped in HTML mode) |
161//! | `{% if cond %}...{% elif %}...{% else %}...{% endif %}` | Conditional |
162//! | `{% for item in items %}...{% endfor %}` | Loop |
163//! | `{% include "name" %}` | Include another template |
164//! | `{% extends "base" %}` | Template inheritance |
165//! | `{% block name %}...{% endblock %}` | Overridable block (`{% endblock name %}` labels are accepted and validated) |
166//! | `{{ super() }}` | Render parent's block content (only inside `{% block %}`) |
167//! | `{% set var = expr %}` | Assign a variable |
168//! | `{% raw %}...{% endraw %}` | Raw text (no parsing; can contain `{%` sequences) |
169//! | `{# comment #}` | Comment (ignored) |
170//! | `expr \| filter_name` | Apply a filter |
171//!
172//! # Expressions
173//!
174//! - Variable access: `name`, `user.email`, `items[0]`
175//! Identifiers are ASCII only (`[A-Za-z_][A-Za-z0-9_]*`). Non-ASCII identifiers
176//! are rejected **intentionally**, to rule out Unicode confusable/homoglyph and
177//! normalization attacks.
178//! - String literals: `"hello"`, `'world'` (may contain `}}`/`%}`; backslash escapes
179//! `\n`, `\t`, `\r`, `\\`, `\'`, `\"` are supported)
180//! - Number literals: `42`, `3.14` (scientific notation and hex are not supported)
181//! - Boolean: `true`, `false`
182//! - Comparisons: `==`, `!=`, `<`, `>`, `<=`, `>=` (floats follow IEEE 754; `NaN`
183//! is never equal and every ordering comparison involving `NaN` is `false`)
184//! - Logical: `and`, `or`, `not` (**strict booleans**, see below)
185//! - Arithmetic: `+`, `-`, `*`, `/`, `%` (integer division/modulo by zero and
186//! integer overflow return an error; float operations follow IEEE 754)
187//! - Containment: `item in list`
188//! - Grouping: `(expr)`
189//! - Filters: `expr | filter_name`, `expr | filter(arg1, arg2)`
190//! - Function calls: `super()`, `range(n)`, `range(start, end)`, `is_set(expr)`
191//!
192//! # Booleans are strict
193//!
194//! Logical operators and conditions do **not** use implicit truthiness. The
195//! decision operand of `and` / `or` / `not`, and every `{% if %}` / `{% elif %}`
196//! condition, must evaluate to a boolean:
197//!
198//! ```rust
199//! # use template::{Engine, EscapeMode, context};
200//! let mut engine = Engine::new(EscapeMode::Text);
201//! engine.add_template("t",
202//! "{% if is_set(user.bio) and user.verified %}ok{% else %}no{% endif %}"
203//! ).unwrap();
204//! let result = engine.render("t", context! { user: context! { verified: true } }).unwrap();
205//! assert_eq!(result, "no");
206//! ```
207//!
208//! `{{ 0 or "fallback" }}` is a type error (an integer is not a boolean); write
209//! `{{ x | default("fallback") }}` or compare explicitly (`{% if count > 0 %}`).
210//! `and` / `or` still return their operand values: with a boolean decision, the
211//! result is the other operand untouched (`false or "x"` renders `x`).
212//!
213//! ## `is_set`
214//!
215//! `is_set(expr)` returns `true` when `expr` evaluates to anything other than
216//! `null`/missing, and `false` for missing variables or fields. It never fails:
217//! nested access such as `is_set(a.b.c)` is `false` when `b` or `c` is absent.
218//!
219//! ## Safety boundaries
220//!
221//! - **Integer division/modulo by zero and integer overflow** are rejected with a
222//! render error (never a panic), in both debug and release builds.
223//! Float division/modulo by zero follows IEEE 754 (returns `inf` / `-inf` / `NaN`).
224//! - **Circular includes** (`a -> b -> a`) are limited by a depth of 64.
225//! - **Circular extends** (`a extends b extends a`) are limited by a depth of 128.
226//! - **Template nesting** (`{% if %}`/`{% for %}`/`{% block %}`) is limited to 128
227//! at parse time, and render/eval nesting to 500, so adversarial templates
228//! return an error instead of overflowing the stack.
229//! - **`range()`** is capped (default 32,000 items, see [`Engine::set_range_limit`])
230//! to bound memory use.
231//! - **Unknown functions** (`{{ myfunc() }}`) return a render error and **unknown
232//! filters** a parse error.
233//!
234//! # Built-in filters
235//!
236//! | Filter | Description |
237//! |--------|-------------|
238//! | `upper` | Convert to uppercase |
239//! | `lower` | Convert to lowercase |
240//! | `trim` | Trim leading/trailing whitespace |
241//! | `escape` | HTML-escape the value (`Safe` result, no double-escape) |
242//! | `safe` | Mark a string as safe (bypasses auto-escaping) |
243//! | `length` | Length of string (character count), array, or map |
244//! | `default(val)` | Return `val` if the input is `null`/missing (does **not** trigger on other falsy values) |
245//! | `capitalize` | Uppercase first character, lowercase the rest |
246//! | `title` | Title case (capitalize each word) |
247//! | `join(sep)` | Join array elements with separator |
248//! | `reverse` | Reverse a string (by Unicode scalar value) or array |
249//! | `first` | First element of an array or first character of a string |
250//! | `last` | Last element of an array or last character of a string |
251//! | `urlencode` | URL-encode (form-style, `+` for spaces) |
252//!
253//! Filters that only transform characters (`upper`, `lower`, `trim`,
254//! `capitalize`, `title`, `reverse`, `first`, `last`, `join`) preserve the
255//! `Safe` marker of already-safe values, so `{{ x | safe | reverse }}` is not
256//! escaped again.
257//!
258//! # Error behavior
259//!
260//! - `add_template` returns an error if a template with the same name already
261//! exists, on syntax errors, when nesting limits are exceeded, and on
262//! statically-detectable type errors (e.g. `{{ 0 or "x" }}`).
263//! - Unknown filter names (`{{ x | unknown }}`) produce a parse-time error, as do
264//! wrong filter argument counts.
265//! - Exceeding the include depth (64) or extend depth (128) returns a render error.
266//!
267//! # Architecture
268//!
269//! ```text
270//! ┌──────────────┐
271//! add_template │ │
272//! ────────────▶│ PARSER │
273//! (source) │ (recursive │
274//! │ descent │
275//! │ parser) │
276//! └──────┬───────┘
277//! │ AST
278//! ┌──────▼───────┐
279//! │ ENGINE │
280//! │ (template │
281//! │ cache) │
282//! └──────┬───────┘
283//! │ render(name, ctx)
284//! ┌──────▼───────┐
285//! │ RENDERER │
286//! │ (tree-walk │
287//! │ VM with │
288//! │ extends / │
289//! │ blocks / │
290//! │ includes │
291//! │ resolution)│
292//! └──────┬───────┘
293//! │ output
294//! ┌──────▼───────┐
295//! │ fmt::Write │
296//! │ (String, │
297//! │ Vec<u8>, │
298//! │ any sink) │
299//! └──────────────┘
300//! ```
301
302extern crate alloc;
303
304#[cfg(feature = "std")]
305extern crate std;
306
307mod ast;
308mod context;
309mod engine;
310mod error;
311mod escapers;
312mod expr;
313mod filters;
314mod parser;
315mod value;
316mod vm;
317
318pub use context::{Context, IntoContext};
319pub use engine::{Engine, EscapeMode};
320pub use error::{Error, SerdeError};
321
322#[doc(hidden)]
323pub mod __macro_support {
324 pub use alloc::{collections::BTreeMap, rc::Rc, string::String};
325 pub use core::convert::Into;
326
327 pub fn into_value<T: Into<crate::value::Value>>(v: T) -> crate::value::Value {
328 v.into()
329 }
330
331 pub fn build_context(map: BTreeMap<String, crate::value::Value>) -> crate::context::Context {
332 crate::context::Context(crate::value::Value::Map(Rc::new(map)))
333 }
334}
335
336/// Build a context map for [`Engine::render`] without requiring `Serialize`.
337///
338/// Unquoted identifiers (e.g. `name`) are stringified automatically.
339/// Quoted string keys (e.g. `"my-key"`) are also accepted for programmatic
340/// construction from external data.
341///
342/// ```rust
343/// use template::{Engine, EscapeMode, context};
344///
345/// let mut engine = Engine::new(EscapeMode::Text);
346/// engine.add_template("t", "Hello, {{ name }}!").unwrap();
347///
348/// let result = engine.render("t", context! { name: "World" }).unwrap();
349/// assert_eq!(result, "Hello, World!");
350/// ```
351///
352/// Supports nesting, vectors, and all types that implement [`Into<Value>`]:
353///
354/// ```rust
355/// use template::{Engine, EscapeMode, context};
356///
357/// let mut engine = Engine::new(EscapeMode::Text);
358/// engine.add_template("t", "\
359/// {% for item in items %}- {{ item }}
360/// {% endfor %}").unwrap();
361///
362/// let result = engine.render("t", context! {
363/// items: vec!["apple", "banana"],
364/// }).unwrap();
365/// assert_eq!(result, "- apple\n- banana\n");
366/// ```
367#[macro_export]
368macro_rules! context {
369 (@key $key:ident) => { stringify!($key) };
370 (@key $key:expr) => { $key };
371
372 ($($key:tt : $value:expr),* $(,)?) => {{
373 let mut __map = $crate::__macro_support::BTreeMap::new();
374 $(
375 __map.insert(
376 $crate::context!(@key $key).to_string(),
377 $crate::__macro_support::into_value($value),
378 );
379 )*
380 $crate::__macro_support::build_context(__map)
381 }};
382}