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//! # Features
27//!
28//! | Feature | Description |
29//! |------------|-----------------------------------------------------------------------------|
30//! | `default` | Enables the `std` feature. |
31//! | `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`]. |
32//!
33//! # Working with slices and vectors
34//!
35//! Iterate over a list with `{% for %}`:
36//!
37//! ```rust
38//! use template::{Engine, EscapeMode, context};
39//!
40//! let mut engine = Engine::new(EscapeMode::Text);
41//! engine.add_template("list", "{% for item in items %}- {{ item }}
42//! {% endfor %}").unwrap();
43//!
44//! let result = engine.render("list", context! {
45//! items: vec!["apple", "banana", "cherry"],
46//! }).unwrap();
47//! assert_eq!(result, "- apple\n- banana\n- cherry\n");
48//! ```
49//!
50//! Access elements by index, including nested fields:
51//!
52//! ```rust
53//! use template::{Engine, EscapeMode, context};
54//!
55//! let mut engine = Engine::new(EscapeMode::Text);
56//! engine.add_template("t", "{{ users[0].name }}, {{ users[1].name }}").unwrap();
57//!
58//! let result = engine.render("t", context! {
59//! users: vec![
60//! context! { name: "Alice", age: 30 },
61//! context! { name: "Bob", age: 25 },
62//! ],
63//! }).unwrap();
64//! assert_eq!(result, "Alice, Bob");
65//! ```
66//!
67//! Use filters on arrays — `join`, `first`, `last`, `length`, `reverse`:
68//!
69//! ```rust
70//! use template::{Engine, EscapeMode, context};
71//!
72//! let mut engine = Engine::new(EscapeMode::Text);
73//! engine.add_template("t", "\
74//! join: {{ items | join(\", \") }}
75//! first: {{ items | first }}
76//! last: {{ items | last }}
77//! count: {{ items | length }}
78//! rev: {{ items | reverse | join(\", \") }}
79//! ").unwrap();
80//!
81//! let result = engine.render("t", context! {
82//! items: vec!["a", "b", "c"],
83//! }).unwrap();
84//! assert_eq!(result, "\
85//! join: a, b, c
86//! first: a
87//! last: c
88//! count: 3
89//! rev: c, b, a
90//! ");
91//! ```
92//!
93//! Check membership with the `in` operator:
94//!
95//! ```rust
96//! use template::{Engine, EscapeMode, context};
97//!
98//! let mut engine = Engine::new(EscapeMode::Text);
99//! engine.add_template("t",
100//! "{% if \"admin\" in roles %}Welcome, admin!{% endif %}"
101//! ).unwrap();
102//!
103//! let result = engine.render("t", context! {
104//! roles: vec!["user", "admin", "moderator"],
105//! }).unwrap();
106//! assert_eq!(result, "Welcome, admin!");
107//! ```
108//!
109//! Render directly from a Rust `Vec`:
110//!
111//! ```rust
112//! use template::{Engine, EscapeMode, context};
113//!
114//! let mut engine = Engine::new(EscapeMode::Text);
115//! engine.add_template("t", "{% for n in numbers %}{{ n }} {% endfor %}").unwrap();
116//!
117//! let result = engine.render("t", context! {
118//! numbers: vec![10, 20, 30],
119//! }).unwrap();
120//! assert_eq!(result, "10 20 30 ");
121//! ```
122//!
123//! Filter across a nested array field:
124//!
125//! ```rust
126//! use template::{Engine, EscapeMode, context};
127//!
128//! let mut engine = Engine::new(EscapeMode::Text);
129//! engine.add_template("t",
130//! "{% for tag in post.tags %}{{ tag | upper }} {% endfor %}"
131//! ).unwrap();
132//!
133//! let result = engine.render("t", context! {
134//! post: context! {
135//! title: "Hello",
136//! tags: vec!["rust", "template", "dev"],
137//! },
138//! }).unwrap();
139//! assert_eq!(result, "RUST TEMPLATE DEV ");
140//! ```
141//!
142//! # Modes
143//!
144//! - `EscapeMode::Html` — auto-escapes `{{ ... }}` output (escapes `&`, `<`, `>`, `"`, `'`)
145//! - `EscapeMode::Text` — no escaping, raw output
146//!
147//! # Template syntax
148//!
149//! | Syntax | Description |
150//! |--------|-------------|
151//! | `{{ expr }}` | Output expression value (auto-escaped in HTML mode) |
152//! | `{% if cond %}...{% elif %}...{% else %}...{% endif %}` | Conditional |
153//! | `{% for item in items %}...{% endfor %}` | Loop |
154//! | `{% include "name" %}` | Include another template |
155//! | `{% extends "base" %}` | Template inheritance |
156//! | `{% block name %}...{% endblock %}` | Overridable block |
157//! | `{{ super() }}` | Render parent's block content (only inside `{% block %}`) |
158//! | `{% set var = expr %}` | Assign a variable |
159//! | `{% raw %}...{% endraw %}` | Raw text (no parsing; can contain `{%` sequences) |
160//! | `{# comment #}` | Comment (ignored) |
161//! | `expr \| filter_name` | Apply a filter |
162//!
163//! # Expressions
164//!
165//! - Variable access: `name`, `user.email`, `items[0]`
166//! - String literals: `"hello"`, `'world'`
167//! - Number literals: `42`, `3.14` (scientific notation and hex are not supported)
168//! - Boolean: `true`, `false`
169//! - Comparisons: `==`, `!=`, `<`, `>`, `<=`, `>=` (floats follow IEEE 754; `NaN` is falsy and `NaN` compared to anything is `false`)
170//! - Logical: `and`, `or`, `not`
171//! - Arithmetic: `+`, `-`, `*`, `/`, `%` (dividing or modulo by zero returns an error)
172//! - Containment: `item in list`
173//! - Grouping: `(expr)`
174//! - Filters: `expr | filter_name`, `expr | filter(arg1, arg2)`
175//! - Function calls: `super()`, `range(n)`, `range(start, end)`
176//!
177//! ## Safety boundaries
178//!
179//! - **Integer division/modulo by zero** is rejected with a render error (not a panic).
180//! Float division/modulo by zero follows IEEE 754 (returns `inf` / `-inf` / `NaN`).
181//! - **Circular includes** (`a -> b -> a`) are detected by a depth limit (64).
182//! - **Circular extends** (`a extends b extends a`) are detected by a depth limit (128).
183//! - **Unknown functions** (`{{ myfunc() }}`) return a render error.
184//!
185//! # Built-in filters
186//!
187//! | Filter | Description |
188//! |--------|-------------|
189//! | `upper` | Convert to uppercase |
190//! | `lower` | Convert to lowercase |
191//! | `trim` | Trim leading/trailing whitespace |
192//! | `escape` | HTML-escape the value (`Safe` result, no double-escape) |
193//! | `safe` | Mark a string as safe (bypasses auto-escaping) |
194//! | `length` | Length of string (character count), array, or map |
195//! | `default(val)` | Return `val` if the input is falsy (i.e. `false`, `0`, `0.0`, `NaN`, `""`, `[]`, `null`) |
196//! | `capitalize` | Uppercase first character, lowercase the rest |
197//! | `title` | Title case (capitalize each word) |
198//! | `join(sep)` | Join array elements with separator |
199//! | `reverse` | Reverse a string (by Unicode scalar value) or array |
200//! | `first` | First element of an array or first character of a string |
201//! | `last` | Last element of an array or last character of a string |
202//! | `urlencode` | URL-encode (form-style, `+` for spaces) |
203//!
204//! # Error behavior
205//!
206//! - `add_template` returns an error if a template with the same name already exists.
207//! - Unknown filter names (`{{ x | unknown }}`) produce a parse-time error.
208//! - Exceeding the include depth (64) or extend depth (128) returns a render error.
209//!
210//! # Architecture
211//!
212//! ```text
213//! ┌──────────────┐
214//! add_template │ │
215//! ────────────▶│ PARSER │
216//! (source) │ (recursive │
217//! │ descent │
218//! │ parser) │
219//! └──────┬───────┘
220//! │ AST
221//! ┌──────▼───────┐
222//! │ ENGINE │
223//! │ (template │
224//! │ cache) │
225//! └──────┬───────┘
226//! │ render(name, ctx)
227//! ┌──────▼───────┐
228//! │ RENDERER │
229//! │ (tree-walk │
230//! │ VM with │
231//! │ extends / │
232//! │ blocks / │
233//! │ includes │
234//! │ resolution)│
235//! └──────┬───────┘
236//! │ output
237//! ┌──────▼───────┐
238//! │ fmt::Write │
239//! │ (String, │
240//! │ Vec<u8>, │
241//! │ io::Write) │
242//! └──────────────┘
243//! ```
244
245extern crate alloc;
246
247#[cfg(feature = "std")]
248extern crate std;
249
250mod ast;
251mod context;
252mod engine;
253mod error;
254mod escapers;
255mod expr;
256mod filters;
257mod parser;
258mod value;
259mod vm;
260
261pub use context::{Context, IntoContext};
262pub use engine::{Engine, EscapeMode};
263pub use error::{Error, SerdeError};
264
265#[doc(hidden)]
266pub mod __macro_support {
267 pub use alloc::{collections::BTreeMap, rc::Rc, string::String};
268 pub use core::convert::Into;
269
270 pub fn into_value<T: Into<crate::value::Value>>(v: T) -> crate::value::Value {
271 v.into()
272 }
273
274 pub fn build_context(map: BTreeMap<String, crate::value::Value>) -> crate::context::Context {
275 crate::context::Context(crate::value::Value::Map(Rc::new(map)))
276 }
277}
278
279/// Build a context map for [`Engine::render`] without requiring `Serialize`.
280///
281/// Unquoted identifiers (e.g. `name`) are stringified automatically.
282/// Quoted string keys (e.g. `"my-key"`) are also accepted for programmatic
283/// construction from external data.
284///
285/// ```rust
286/// use template::{Engine, EscapeMode, context};
287///
288/// let mut engine = Engine::new(EscapeMode::Text);
289/// engine.add_template("t", "Hello, {{ name }}!").unwrap();
290///
291/// let result = engine.render("t", context! { name: "World" }).unwrap();
292/// assert_eq!(result, "Hello, World!");
293/// ```
294///
295/// Supports nesting, vectors, and all types that implement [`Into<Value>`]:
296///
297/// ```rust
298/// use template::{Engine, EscapeMode, context};
299///
300/// let mut engine = Engine::new(EscapeMode::Text);
301/// engine.add_template("t", "\
302/// {% for item in items %}- {{ item }}
303/// {% endfor %}").unwrap();
304///
305/// let result = engine.render("t", context! {
306/// items: vec!["apple", "banana"],
307/// }).unwrap();
308/// assert_eq!(result, "- apple\n- banana\n");
309/// ```
310#[macro_export]
311macro_rules! context {
312 (@key $key:ident) => { stringify!($key) };
313 (@key $key:expr) => { $key };
314
315 ($($key:tt : $value:expr),* $(,)?) => {{
316 let mut __map = $crate::__macro_support::BTreeMap::new();
317 $(
318 __map.insert(
319 $crate::context!(@key $key).to_string(),
320 $crate::__macro_support::into_value($value),
321 );
322 )*
323 $crate::__macro_support::build_context(__map)
324 }};
325}