askama · v1.5 · 2026-07-19 · sha256 662d6def001e6ff1

askama v1.5A

Immutable. This exact content is served forever at /api/v1/blob/662d6def001e6ff1.

---
name: askama
description: "Askama Rust templating reference (0.16)."
metadata:
  author: uwuclxdy
  version: "1.5"
---

# Askama (Rust Templating)

> _Captured 2026-07-10 (askama 0.16.0). To update: re-verify against the askama book + docs.rs/askama, then diff for changes._

Askama is a compile-time, Jinja-like template engine for Rust. Templates are parsed at build time and generate type-checked Rust code. This skill targets the **unified askama crate, version 0.16.x** (MSRV 1.88, raised in 0.15.0 from 1.83; was 1.81 through 0.13.x, then 1.83 in 0.14.0). Anything older than 0.13 wired web frameworks through dedicated `askama_*` crates that no longer exist; see the breaking-changes section if migrating.

> Quick history: Askama was forked into `rinja` in 2024, then the two projects re-unified as `askama` in early 2025. If the user mentions `rinja` or `rinja_axum`, those are deprecated. Migrate to `askama` / `askama_web`.

---

## 1. Current State & Setup

### Cargo.toml

```toml
[dependencies]
askama = "0.16"
# For web frameworks, ADD askama_web separately (see §3):
askama_web = { version = "0.16", features = ["axum-0.8"] }
```

Do not depend on `askama_axum`, `askama_actix`, `askama_warp`, or `askama_rocket` (deprecated when 0.13 unified them), nor `askama_gotham` / `askama_tide` (never deprecated; just abandoned since 2023 on askama ^0.12). All dead now. There is no `askama_actix_web` crate.

### Minimal Example

`templates/hello.html`:
```
Hello, {{ name }}!
```

```rust
use askama::Template;

#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> { name: &'a str }

HelloTemplate { name: "world" }.render()? // -> Result<String, askama::Error>
```

Templates live in `templates/` at the crate root (next to `Cargo.toml`) by default. Override via `askama.toml` (see §11).

---

## 2. Rendering API: Use the Right Method

Prefer these over `.to_string()` or `format!()`, which route through a vtable at runtime and run 100-200% slower:

| Method | Output | When to use |
|---|---|---|
| `tmpl.render()` | `Result<String, askama::Error>` | Default. Allocates a new `String`. |
| `tmpl.render_into(&mut writer)` | writes into `impl fmt::Write` | Reusing a buffer, nested rendering. |
| `tmpl.write_into(&mut writer)` | writes into `impl io::Write` | Writing directly to a socket / file. |

The `Template` trait exposes `render_with_values(&dyn Values)` for runtime-injected values that aren't on the struct.

The derive auto-computes a `SIZE_HINT` const from the template's static text; `render()` / `render_with_values` preallocate their `String` with it (no effect on the caller-owned writers of `render_into` / `write_into`). There is no `size_hint` attribute; `#[template(size_hint = ...)]` is a compile error (`unsupported template attribute`). Override the hint only by hand-implementing `Template` with a custom `const SIZE_HINT`.

**Removed in 0.13+**: `Template::EXTENSION` and `Template::MIME_TYPE` associated fields no longer exist. Do not reference them.

---

## 3. Web Framework Integration

**Two paths. Pick one.**

### Path A: Manual (Simplest, No Extra Crate)

Call `.render()` and wrap the string yourself.

```rust
// axum
use axum::response::{Html, IntoResponse};

async fn index() -> impl IntoResponse {
    Html(IndexTemplate { title: "Home" }.render().unwrap())
}
```

For proper error handling, return `Result<Html<String>, AppError>` where `AppError: IntoResponse` and `AppError::from(askama::Error)` exists.

### Path B: `askama_web` with `WebTemplate`

One derive implements the response trait for whichever feature flag you turned on (see the list below).

```toml
askama_web = { version = "0.16", features = ["axum-0.8"] }
```

```rust
use askama::Template;
use askama_web::WebTemplate;

#[derive(Template, WebTemplate)]
#[template(path = "hello.html")]
struct HelloTemplate { name: String }

async fn hello() -> HelloTemplate {
    HelloTemplate { name: "world".into() }
}
```

Returns `200 OK`, `Content-Type: text/html; charset=utf-8`. Render errors become `500`.

**Per-target feature flags for askama_web**:
- `axum-0.8` / `axum-0.7` / `axum-core-0.5` / `axum-core-0.4`
- `actix-web-4`
- `rocket-0.5`
- `warp-0.4` / `warp-0.3`
- `poem-3`
- `trillium-0.2`
- `cot-0.3` / `cot-0.4` / `cot-0.5` / `cot-0.6` / `cot_core-0.6`
- `derive` (WebTemplate derive, on by default) and `eprintln`. Standalone logging-backend flags `log-0.4` / `tracing-0.1` (one of each, shared across all integrations)

---

## 4. `#[template(...)]` Attribute Options

| Key | Example | Purpose |
|---|---|---|
| `path` | `path = "hello.html"` | File in templates dir. Extension drives escaping. |
| `source` | `source = "Hi {{ name }}"` | Inline template; requires `ext`. |
| `ext` | `ext = "html"` | Content-type hint; `"jinja"` / `"jinja2"` aliases for editor syntax highlighting. |
| `escape` | `escape = "none"` or `"html"` | Override auto-escape. |
| `whitespace` | `whitespace = "suppress"` | Per-template WS mode (`"preserve"` / `"suppress"` / `"minimize"`); overrides `askama.toml`. |
| `syntax` | `syntax = "foo"` | Use a custom syntax defined in `askama.toml`. |
| `config` | `config = "config.toml"` | Override config path (default `askama.toml`). |
| `print` | `print = "code"` / `"ast"` / `"all"` / `"none"` | Compile-time debug: prints generated code to stdout. |
| `block` | `block = "hello"` | Render just one block. Fields outside that block aren't required. Good for HTMX fragments. |
| `blocks` | `blocks = ["title", "content"]` | Generates `as_<block>()` accessor methods on the struct, one per block. |
| `in_doc` | `in_doc = true` | Source is in the struct's doc comment (inside an ```askama fenced block). Requires `ext`. |
| `askama` | `askama = $crate::__askama` | Override the `askama` crate path. Needed when re-exporting from a macro-defining crate. |

Cannot combine `path` and `source`.

`path` and `source` must be string literals known at compile time, the same constraint `{% include %}` paths have (§10). The template can't be chosen at runtime from a variable or computed string.

---

## 5. Expressions

### Literals and Types
- Struct construction: `{{ MyStruct { field1: 1, field2: "x" }.to_string() }}`
- Tuple: `{{ (1, 2) }}`, array: `{{ [1, 2, 3] }}`, array-repeat (0.15): `{{ [0; 4] }}`
- Struct expressions are values, not just via `.to_string()` (0.15): `{{ Point { x: 1, y: 2 } }}`

### Variable Access
- `{{ name }}`: field on template struct
- `{{ user.name }}`: dotted path (fields or methods)
- `{{ crate::MAX_USERS }}`: use constants from your crate
- Reading follows Rust borrow rules. Methods can be called; beware self-recursion.

### Operators (Mostly like Rust)
- **Bitwise are RENAMED** (to avoid filter-pipe conflict):
  - `|` -> `bitor`
  - `&` -> `bitand`
  - `^` -> `xor`
  - Precedence unchanged.
  - `{% if my_bitset bitand 1 != 0 %}set!{% endif %}`
- **Concat `~`**: `{{ a ~ b ~ c }}` is shorthand for `{{ a }}{{ b }}{{ c }}`. **Must have spaces around it** to disambiguate from whitespace control.
- **`as` cast**: `{{ x as i64 }}`. Only primitive types. Automatically derefs `&&&bool` etc.

### Function and Method Calls
- `{{ method() }}` -> `self.method()` (method on the template struct)
- `{{ self::function() }}` -> free function in the current module
- `{{ super::b::f() }}` -> function in another module
- `{{ (closure)(12) }}` -> calling a closure stored in a field; parens required
- `{{ some_macro!(field) }}` -> Rust macro. **Askama won't infer field references inside macro args.** Pass them explicitly, or the macro sees the literal token instead.

---

## 6. Control Flow

### if / else if / else
`{% if a %}…{% else if b %}…{% else %}…{% endif %}`

### for
`{% for user in users %}<li>{{ user.name }}</li>{% endfor %}`

- **Filter clause**: `{% for user in users if user.active %}`
- **Else clause** (runs if iter was empty or all filtered out):
  ```
  {% for u in users %}...{% else %}No one here.{% endfor %}
  ```
- **Loop variables** (the complete list; Askama exposes fewer of them than Jinja does):
  - `loop.index` (1-based)
  - `loop.index0` (0-based)
  - `loop.first`
  - `loop.last`
  - `loop.cycle([...])` (method call, array-literal arg): `{{ loop.cycle(["a", "b"]) }}` cycles per iteration
- No `loop.length`, `loop.revindex`, `loop.changed`, `loop.previtem`, `loop.nextitem`. If you need these, compute in Rust or use modulo on `loop.index`.

### match / when (for Enums, Options, Results)
```
{% match item %}
  {% when Some with ("foo") %}
    Found literal foo
  {% when Some with (val) %}
    Found {{ val }}
  {% when None %}
    Nothing
{% endmatch %}
```
- Tuple variants: `{% when Variant with (a, b) %}`
- Struct variants: `{% when Variant { field } %}` or `{% when Variant { field: val } %}` to rename (with-less form since 0.11; 0.8 through 0.10 required the `with` keyword: `{% when Variant with { field } %}`)
- Enum path variants: `{% when Self::Circle { radius } %}`. `#[derive(Template)]` works on the enum itself, giving each variant its own `#[template(...)]`.
- Slice patterns with rest: `{% when [first, ..] %}`
- Wildcard: `{% when _ %}` or `{% else %}`
- Literal patterns: `{% when 3 %}`

### Assignments and Variable Declarations
```
{% let name = user.name %}           {# immutable binding #}
{% let mut it = xs.iter() %}         {# mutable binding #}
{% set x = 4 %}                       {# alias for let (Jinja compat) #}
{% decl val %}                        {# declare WITHOUT value; let/set can't since 0.16 #}
{% if cond %}
  {% let val = "a" %}
{% else %}
  {% let val = "b" %}
{% endif %}
{{ val }}
```
- Shadowing is allowed, same as Rust.
- **`decl` (alias `declare`) is the only valueless declaration since 0.16.** Bare `{% let x %}` / `{% set x %}` with no `=` now starts a *let/set block* instead (below).
- **let/set blocks (0.16)**: capture a rendered block into a string variable:
  ```
  {% let heading %}{{ title }} on {{ site }}{% endlet %}
  {{ heading }}
  ```
- **Compound assignment uses the `{% mut %}` tag (0.16)**, not bare `let`: `{% mut counter += i %}`. Every Rust compound operator (`+=`, `-=`, `*=`, …) works; the target must be a `mut` binding.
- **Do not prefix variables with `__askama`, name one `caller` (reserved since 0.15), or use Rust keywords.**

### Filter Blocks
Apply one or more filters to a whole block:
```
{% filter lower | capitalize %}
  {{ t }} / HELLO / {{ u }}
{% endfilter %}
```

---

## 7. Whitespace Control: Three Operators, Not One

| Operator | Name | Effect |
|---|---|---|
| `-` | suppress | Remove all whitespace on that side |
| `~` | minimize | Collapse to a single space/newline |
| `+` | preserve | Keep whitespace as-is (overrides config) |

Usage: put right after `{%`/`{{` or right before `%}`/`}}`.
```
<div>
{%- if x %}             {# strip before this tag #}
  <p>{{ x }}</p>
{%- endif %}
</div>

{% for x in xs ~%}      {# minimize trailing whitespace #}
  {{ x }}
{%~ endfor %}
```

Global default comes from `askama.toml`'s `whitespace = "preserve" | "suppress" | "minimize"`. Inline operators override globals. When two inline controls point at the same span, **Suppress always wins**; between Minimize and Preserve, whichever operator sits on the tag AFTER the gap wins (positional, not a fixed priority). The book's flat `Suppress > Minimize > Preserve` table is wrong vs 0.16.0 for the Minimize/Preserve case.

Whitespace-control operators (`-`/`+`/`~`) on `{% extends %}` are accepted but a no-op, **not** a compile error. The child's top-level content is dropped anyway, so a trim has nothing to act on. (The book still describes this as "rejected"; that's stale vs 0.16.0.)

---

## 8. Comments

Nested block comments are supported:
```
{# outer {# nested #} still in outer #}
```

---

## 9. Inheritance (extends / block / super)

`base.html`:
```
<title>{% block title %}{{ title }} · Site{% endblock %}</title>
<main>{% block content %}<p>default</p>{% endblock %}</main>
```

`page.html`:
```
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block content %}
  {{ super() }}   {# inject the parent block's contents #}
  <h1>Hello!</h1>
{% endblock %}
```

Rules:
- Multi-layer inheritance is supported.
- **Blocks inside `if`/`else`/`for`**: the book forbids this, but 0.16.0 compiles it and inheritance overrides correctly (verified empirically). Top-level or block-nested stays the safe form.
- The extending template's top-level content outside blocks is ignored.
- A base template must define at least one block for inheritance to work.
- Askama looks for the extended template relative to the extending one first, then relative to the config'd template dirs.
- Duplicate `{% block %}` names in the same template are a hard compile error since 0.16, previously only a warning (see §15).

### Single-Block & Per-Block Sub-Templates

`block` / `blocks` (see §4): `block = "content"` renders one block (fields outside it not required, good for HTMX partials); `blocks = ["title", "content"]` generates `as_<block>()` accessor methods on the struct itself, e.g. `page.as_title().render()` / `page.as_content().render()` (no new struct types).

---

## 10. Includes, Macros, and Composition

### `{% include %}`
```
{% for item in items %}
  {% include "item.html" %}
{% endfor %}
```
- Path must be a string literal (known at compile time).
- The included template has full access to the caller's scope, including loop locals.
- Lookup: relative to including template first, then template dirs.

### Macros
Define with `{% macro name(args) %}...{% endmacro %}`, call with `{% call name(args) %}`:
```
{% macro heading(arg) %}
  <h1>{{ arg }}</h1>
{% endmacro %}

{% call heading("Title") %}
```
- **Expression-call syntax (0.15)**: invoke a macro like a function, `{{ heading("Title") }}` (named args allowed too). Only works for macros that don't require a `caller` body.
- Macro args can carry type annotations and default-value generics (0.15/0.16).
- Optional named endmacro: `{% endmacro heading %}`
- Named arguments (must come AFTER positional):
  ```
  {% macro h(arg, bold) %}<h1>{{arg}}<b>{{bold}}</b></h1>{% endmacro %}
  {% call h(bold="x", arg="y") %}
  ```

### Importing Macros from Another File
```
{%- import "macros.html" as m -%}
{% call m::heading("hi") %}
```

### Call-Block Syntax (Macros with Inner Content)
```
{% macro card() %}
  <div class="card">{{ caller() }}</div>
{% endmacro %}

{% call card() %}
  <p>Body passed into the macro.</p>
{% endcall %}
```
`caller()` inside the macro renders the block passed to `{% call %}...{% endcall %}`. To pass arguments, the call-block declares them up front: `{% call(user) dump_users(list) %}...{% endcall %}` makes the macro's `caller(user)` render the body once per invocation. Guard an optional body with `{% if caller is defined %}`. `caller` is a reserved variable name since 0.15.

### Render-in-Place (Nested Template Structs)
Embed a Template-derived type as a field; it auto-renders via its `Display` impl:
```rust
#[derive(Template)]
#[template(source = "Section 1: {{ s1 }}", ext = "txt")]
struct Outer { s1: Inner }

#[derive(Template)]
#[template(source = "A={{ a }}", ext = "txt")]
struct Inner { a: String }
```

### Recursive Rendering
Self-include does NOT work. Do recursion with an explicit `.render()` call:
```rust
#[derive(Template)]
#[template(source = r#"
{{ name }} {
  {% for child in children %}
    {{ child.render()? }}
  {% endfor %}
}
"#, ext = "txt", escape = "none")]
struct Item<'a> {
    name: &'a str,
    children: &'a [Item<'a>],
}
```
(Prefer a custom iterator + plain loop when possible.)

---

## 11. Configuration: `askama.toml`

Place at crate root (next to `Cargo.toml`). All sections are optional.

```toml
[general]
dirs = ["templates"]                  # default; globs OK (0.16): ["templates/*"], ["templates/**"]
whitespace = "preserve"               # or "suppress" | "minimize"

# Custom delimiters (e.g. to avoid collisions with LaTeX or Vue)
[[syntax]]
name = "vue"
block_start = "[%"
block_end = "%]"
expr_start = "[["
expr_end = "]]"
comment_start = "[#"
comment_end = "#]"

# Custom escaper: apply askama::filters::Text (no escaping) to .js files
[[escaper]]
path = "askama::filters::Text"
extensions = ["js"]
```

Use a custom syntax per-template with `#[template(path = "...", syntax = "vue")]`.

**Default escaping extensions**:
- HTML escaper: `askama`, `html`, `htm`, `j2`, `jinja`, `jinja2`, `rinja`, `svg`, `xml`
- Text (no escape): `md`, `yml`, `none`, `txt` (and the empty extension)

Escape modes override via `#[template(escape = "html")]` or `escape = "none"`.

---

## 12. Auto-Escaping

Askama escapes `<`, `>`, `&`, `"`, `'` per OWASP recommendations when the extension implies HTML.

Per-expression bypass:
- `{{ value | safe }}`: don't escape this value
- `{{ value | escape }}` or `{{ value | e }}`: force escape in an unescaped context

---

## 13. Built-in Filters

All filters use `value | filter_name(args)`. Named arguments are supported: `{{ count | pluralize(plural = "gies") }}`.

### Always Available
| Filter | Purpose |
|---|---|
| `capitalize` | First char upper, rest lower |
| `center` | Center in a field of given width |
| `escape` / `e` | Force HTML escape |
| `filesizeformat` | Bytes -> "1.4 KB" |
| `fmt(fmtstr)` | Apply Rust format string |
| `format(fmtstr, ...)` | Like `format!()` |
| `indent(n)` / `indent(prefix)` | Indent each line |
| `join(sep)` | Join iterable into string |
| `linebreaks` | Plain text -> `<p>`/`<br>` |
| `linebreaksbr` | Newlines -> `<br>` |
| `lower` / `lowercase` | Lowercase |
| `paragraphbreaks` | Only `\n\n` -> `<p>` |
| `pluralize(singular="", plural="s")` | Suffix based on ±1 |
| `reject` / `reject_with` | Filter iterator |
| `safe` | Mark as HTML-safe |
| `title` / `titlecase` | Title Case |
| `trim` | Strip whitespace |
| `truncate(n)` | Truncate with "…" |
| `unique` | Dedup iterator (requires `std`) |
| `upper` / `uppercase` | Uppercase |
| `wordcount` | Count words |
| `assigned_or(fallback)` | Fallback if the value equals its type default (0.15) |
| `defined_or(fallback)` | Fallback if the identifier is undefined; LHS must be an identifier (0.15) |
| `default(val[, bool])` | Jinja-compat: acts as `defined_or`, or as `assigned_or` when 2nd arg is `true`. Prefer the two above (0.15) |

### Feature-Gated
| Filter | Feature flag |
|---|---|
| `json` (indent arg selects pretty: `{{ value | json(2) }}`) | `serde_json` |
| `urlencode` (does NOT encode `/`) | `urlencode` |
| `urlencode_strict` (encodes `/`) | `urlencode` |

Add to `Cargo.toml`: `askama = { version = "0.16", features = ["serde_json", "urlencode"] }`

### Filters Removed Since Older Askama
- `humansize`: always available now, no flag needed
- `markdown`: removed, use `comrak` directly
- `serde-yaml`: removed, use `yaml-rust2` directly
- `serde-json` renamed to `serde_json`

---

## 14. Custom Filters

Since **0.15** a custom filter is a plain fn annotated with **`#[askama::filter_fn]`**. Put it in a module named `filters` in scope of the `#[derive(Template)]` struct, or call it via an explicit path (`{{ x | mymod::myfilter }}`).

**Mandatory signature**: first arg is the piped-in value (any type; `impl Display` or a generic bound is typical, accepting owned and borrowed), second is `&dyn askama::Values` (askama's runtime-values env). Returns `askama::Result<T>`. In a chain only the *last* filter's `T` must be `Display`; earlier ones may return any `T`.

```rust
use askama::Template;

#[derive(Template)]
#[template(source = "{{ s | shout }}", ext = "txt")]
struct Msg<'a> { s: &'a str }

mod filters {
    #[askama::filter_fn]
    pub fn shout<T: std::fmt::Display>(
        s: T,
        _env: &dyn askama::Values,
    ) -> askama::Result<String> {
        Ok(s.to_string().to_uppercase() + "!")
    }
}
```

**Extra arguments** come after the two mandatory ones:
- Required args are just more params: `fn repeat(s: impl Display, _: &dyn Values, n: usize)` invoked as `{{ s | repeat(4) }}`.
- Optional args (must follow every required one) declare their default with the `#[optional(...)]` attribute:
  ```rust
  #[askama::filter_fn]
  pub fn f(
      value: impl Display,
      _env: &dyn askama::Values,
      #[optional(None)] a: Option<&str>,   // omitted -> None
      #[optional("hi")] b: &str,           // omitted -> "hi"
  ) -> askama::Result<String> { /* ... */ }
  ```
- Named-argument invocation works (named must follow positional): `{{ x | f(b = "yo") }}`. Lifetimes on the fn are allowed since 0.15.1, `where` bounds since 0.15.2.

Notes:
- Built-in filter names take precedence over custom ones; be explicit with `{{ x | filters::myfilter }}` if you shadow one.
- **Migrating from ≤0.14**: the old bare-fn form (no attribute) no longer compiles. Add `#[askama::filter_fn]`; it's what unlocked the named/optional args the old form couldn't express.
- **Filter fns and `render()` / `render_into()` are synchronous only.** No async support; an `async fn` doesn't satisfy the `#[askama::filter_fn]` signature.

---

## 15. Gotchas and Footguns

1. **`askama_axum` / `askama_actix` / etc. are gone**: use `#[derive(WebTemplate)]` (askama_web) or manual `.render()` + `Html(...)`.
2. **`|`, `&`, `^` are not bitwise in templates**: use `bitor`, `bitand`, `xor`.
3. **Self-include doesn't work**: recurse via an embedded child struct (Display) or explicit `{{ child.render()? }}`.
4. **Blocks inside `if`/`for`**: book forbids it, 0.16.0 compiles it (§9); top-level/block-nested is safe.
5. **Whitespace control on `{% extends %}` is a silent no-op**, not a compile error (§7).
6. **Variable names starting with `__askama`, the reserved `caller`, or Rust keywords are banned.**
7. **A method returning `self`** infinite-loops via Display; don't write `{{ self }}`.
8. **Rust macro calls don't infer field names** (§5): `{{ my_macro!(field) }}` sees `field`, not `self.field`.
9. **Prefer `.render()` / `.render_into()` over `.to_string()`** (2-3x slower).
10. **`~` is concat (with spaces) vs. whitespace-minimize (no spaces)**: keep spaces around concat.
11. **Auto-escape depends on extension** (`.html` escaped, `.txt` not); inline `source` needs explicit `ext` / `escape`.
12. **Rinja is dead**: `rinja` / `rinja_axum` / `rinja_derive` -> `askama` / `askama_web` (see intro).
13. **Custom filters need `#[askama::filter_fn]`** since 0.15; a bare `fn` in `mod filters` no longer compiles (§14).
14. **0.16 upgrade breakage**: valueless `{% let x %}` / `{% set x %}` now opens a *block* (use `{% decl x %}`); compound assignment moved to `{% mut %}`; duplicate block names are now a hard compile error (was a warning).
15. **`path` / `source` take string literals only** (§4), same as `{% include %}` (§10). No picking the template from a variable or computed string at runtime.
16. **Filter fns and `render()` / `render_into()` are sync only** (§14). No async filter or async render path exists.

---

## 16. Debugging Tips

- **Playground**: https://askama.rs has a web playground that shows the generated code for any template.
- **Compile errors**: Askama's error messages point at the template's `file:line:column` location. Lifetime / ownership errors in generated code usually mean a field got moved. Take a reference, or use `&` in the template.
- **Book**: https://askama.rs for the canonical syntax reference.
- **docs.rs**: https://docs.rs/askama for API docs and the full filter list.

---

## 17. Quick Reference Card

```
Expressions:   {{ expr }}               Tags:   {% stmt %}       Comments: {# ... #}
WS control:    {%- strip    -%}  {%~ min ~%}  {%+ keep +%}
Inheritance:   {% extends "base.html" %}  {% block x %}...{% endblock %}  {{ super() }}
Loops:         {% for x in xs if cond %}...{% else %}...{% endfor %}     loop.{index,index0,first,last,cycle([…])}
Conditionals:  {% if %}...{% else if %}...{% else %}...{% endif %}
Match:         {% match v %}{% when Some with (x) %}...{% when None %}...{% endmatch %}
Vars:          {% let x = y %}  {% let mut it = … %}  {% set x = 1 %}  {% decl x %}  {% mut x += 1 %}
Let block:     {% let s %}…rendered…{% endlet %}   (captures block output into `s`)
Macros:        {% macro m(a) %}...{% endmacro %}   {% call m(a) %}   expr-call: {{ m(a) }}
               {% call(x) m(a) %}body{% endcall %}    inside macro: {{ caller() }} / {{ caller(x) }}
Imports:       {% import "m.html" as ns %}   {% call ns::m(a) %}
Include:       {% include "part.html" %}
Filters:       {{ x | lower | trim }}   block: {% filter lower %}...{% endfilter %}
Concat:        {{ a ~ b ~ c }}          (spaces required)
Cast:          {{ x as i64 }}           (primitives only)
Bitwise:       a bitor b, a bitand b, a xor b   (NOT |, &, ^)
Crate consts:  {{ crate::MY_CONST }}
Struct ctor:   {{ Foo { a: 1 }.to_string() }}
Escape:        {{ x | safe }}   {{ x | e }}   or #[template(escape = "none"|"html")]
```