Template Filters and Helpers
Built-in filters, tests, and globals available in umbral templates via minijinja 2.
umbral templates run on minijinja 2, which ships a Jinja2-compatible filter set out of the box. On top of that standard library, umbral-core registers a handful of custom filters and functions of its own (img, markdown, sanitize, currency, static, media_url, now, querystring_with, highlight_styles) and injects ambient globals into every render (csrf_token, csrf_input, plus the anonymous-safe user). Plugins can add their own; see Custom template tags.
This page catalogues the filters and globals umbral users reach for most, with examples written in the umbral template style.
umbral's own filters and globals
These are registered by umbral-core's template engine (crates/umbral-core/src/templates.rs) and are available in every .html template the framework renders, on top of the minijinja standard library below.
| Name | Kind | Purpose | Usage |
|---|---|---|---|
img | filter | Build an <img> with loading="lazy", decoding="async", and explicit width/height (CLS-safe). All attributes are HTML-escaped. | {{ url \| img(alt="Logo", width=400, height=300, class="rounded") }} |
markdown | filter | Render CommonMark + GFM (tables, strikethrough, task lists, footnotes) to HTML, then sanitize through ammonia. Output is marked safe. | {{ body \| markdown }} |
sanitize | filter | Clean stored HTML (e.g. the admin RTE widget's output) down to ammonia's safe allowlist and mark it safe. | {{ body \| sanitize }} |
now | function | Current UTC time. No argument → RFC 3339; a chrono strftime string → formatted. | {{ now("%Y-%m-%d") }} |
currency | filter | Format a number as money: two decimals, thousands grouping, leading symbol for common ISO codes (USD/EUR/GBP/JPY/…). Unknown code → 1,234.56 CODE. | {{ price \| currency("EUR") }} |
static | function | Resolve a developer-shipped asset path through the configured static_url (and the cache-busting manifest when collectstatic --hashed has run). | {{ static("admin/admin.css") }} |
media_url | function | Resolve a stored file/image key through the ambient Storage backend's public URL. ImageField/FileField serialize as the bare key. | {{ media_url(plugin.logo) }} |
querystring_with | function | Rebuild the current querystring replacing one key, preserving the rest - for pagination links that carry filters. No leading ?. | ?{{ querystring_with(current_query, "page", page.next_page_number) }} |
highlight_styles | function | Emit the base16-ocean.dark syntect token stylesheet (wrapped in <style>) for \| markdown server-side code highlighting. Call once in <head>. | {{ highlight_styles() }} |
csrf_token | global | The current request's CSRF token string. Injected ambiently when a CSRF middleware scoped a token for the request. | <input name="csrf_token" value="{{ csrf_token }}"> |
csrf_input | global | A ready-made <input type="hidden" name="csrf_token" ...>, marked safe. Drop it straight into a form. | <form method="post">{{ csrf_input }}</form> |
user | global | The current user, injected into every render. Authenticated requests get the full serialized user when AuthPlugin::with_user_in_templates() is on; otherwise the anonymous sentinel { is_authenticated: false, is_staff: false, is_superuser: false }, so user.is_authenticated always resolves. | {% if user.is_authenticated %}...{% endif %} |
img, markdown, and sanitize all return safe (pre-escaped) values, so autoescape won't double-encode the markup they generate. The kwargs on img are strict: a typo'd key like alt_text raises a clear template error rather than being silently dropped.
{# user-supplied markdown, rendered and sanitized in one pipe #}<div class="prose">{{ plugin.usage \| markdown }}</div> {# CLS-safe responsive image #}{{ logo_url \| img(alt=plugin.name, width=64, height=64, class="logo") }}None and Undefined render as the empty string. umbral installs a custom formatter so an `Option
Plugins register their own app-level filters and functions through Plugin::template_registrars; see Custom template tags. (The admin plugin additionally registers a naturaltime filter against its private environment; that one is scoped to admin templates and isn't available in app-level templates.)
String filters
| Filter | Purpose | Usage |
|---|---|---|
upper | Convert to uppercase | {{ name | upper }} |
lower | Convert to lowercase | {{ name | lower }} |
title | Title-case each word | {{ name | title }} |
capitalize | Uppercase first character, lowercase the rest | {{ name | capitalize }} |
trim | Strip leading and trailing whitespace | {{ text | trim }} |
replace(from, to) | Replace all occurrences of from with to | {{ s | replace("old", "new") }} |
safe | Mark a value as HTML-safe, bypassing autoescape | {{ content | safe }} |
escape | HTML-escape the value (same as the autoescaper, but explicit) | {{ text | escape }} |
urlencode | Percent-encode a string for use in a URL query parameter | {{ query | urlencode }} |
indent(width) | Indent each line of a multiline string by width spaces | {{ code | indent(4) }} |
{# templates/profile.html #}<h1>{{ user.name | title }}</h1><p>{{ bio | default("No bio.") }}</p>Number filters
| Filter | Purpose | Usage |
|---|---|---|
abs | Absolute value | {{ value | abs }} |
round(precision) | Round to precision decimal places (default 0) | {{ price | round(2) }} |
float | Cast to float | {{ value | float }} |
int | Cast to integer | {{ value | int }} |
<p>Price: {{ product.price | round(2) }}</p><p>In stock: {{ inventory | abs }}</p>List and collection filters
| Filter | Purpose | Usage |
|---|---|---|
length | Number of items in a sequence or characters in a string | {{ items | length }} |
first | First element of a sequence | {{ items | first }} |
last | Last element of a sequence | {{ items | last }} |
reverse | Reverse a sequence | {{ items | reverse }} |
sort | Sort a sequence (ascending by default) | {{ items | sort }} |
unique | Remove duplicate values from a sequence | {{ items | unique }} |
join(sep) | Join items with a separator string (default "") | {{ tags | join(", ") }} |
list | Coerce a value to a list | {{ value | list }} |
min | Smallest value in a sequence | {{ scores | min }} |
max | Largest value in a sequence | {{ scores | max }} |
sum | Sum of numeric values in a sequence | {{ prices | sum }} |
map(attribute=...) | Extract an attribute from every item in a sequence | {{ items | map(attribute="name") }} |
select(test) | Keep items that pass a test | {{ nums | select("odd") }} |
reject(test) | Drop items that pass a test | {{ nums | reject("odd") }} |
selectattr(attr) | Keep items whose attribute is truthy (or passes a test) | {{ items | selectattr("active") }} |
rejectattr(attr) | Drop items whose attribute is truthy (or passes a test) | {{ items | rejectattr("archived") }} |
batch(size) | Group items into chunks of size | {{ items | batch(3) }} |
slice(count) | Slice a sequence into count parts | {{ items | slice(3) }} |
{# Comma-separated tag names #}<p>Tags: {{ article.tags | map(attribute="name") | join(", ") }}</p> {# First three items only #}{% for item in items | list | slice(3) | first %} <li>{{ item.title }}</li>{% endfor %} {# Total pages: 12 #}<p>Total pages: {{ pages | length }}</p>Default and type-testing filters
| Filter | Purpose | Usage |
|---|---|---|
default(value) | Return value if the variable is undefined or falsy | {{ bio | default("No bio provided.") }} |
d(value) | Alias for default | {{ bio | d("No bio provided.") }} |
dictsort | Sort a dict by its keys | {{ mapping | dictsort }} |
items | Return (key, value) pairs from a dict | {{ mapping | items }} |
tojson | Serialize the value to a JSON string | {{ payload | tojson }} |
<p>{{ user.bio | default("No bio provided.") }}</p><script> const data = {{ payload | tojson }};</script>Control-flow tags (not filters, but commonly confused)
These are Jinja2 tags, not pipe filters. They're included here because they solve the same display problems that humanize-style filters solve.
{# Conditional blocks #}{% if articles | length == 0 %} <p>No articles yet.</p>{% else %} <p>{{ articles | length }} article{% if articles | length != 1 %}s{% endif %}</p>{% endif %} {# Loop helpers inside a for block #}{% for item in items %} {# loop.index is 1-based; loop.index0 is 0-based #} <li class="{% if loop.first %}first{% endif %} {% if loop.last %}last{% endif %}"> {{ loop.index }}. {{ item.name }} </li>{% endfor %}Global functions
minijinja exposes a small set of global functions callable anywhere in a template expression.
| Function | Purpose | Usage |
|---|---|---|
range(start, stop, step) | Generate an integer range (like Python's range) | {% for i in range(5) %} |
dict(...) | Construct a dictionary inline | {% set d = dict(key="value") %} |
namespace(...) | A mutable namespace for storing values across loop iterations | {% set ns = namespace(count=0) %} |
debug(...) | Dump the current context (or an argument) for inspection | {{ debug() }} |
{% for i in range(1, 6) %} <p>Item {{ i }}</p>{% endfor %}The safe filter and autoescape
umbral enables autoescape on every .html and .htm template. A value that contains <script> will be emitted as <script> automatically. To render pre-trusted HTML - for example, a sanitised rich-text field - pipe it through safe:
{# Only do this for content you have already sanitised server-side #}<div class="body">{{ article.rendered_html | safe }}</div>There is no global escape bypass. See Rendering HTML for the security rationale.
What is missing today: humanize helpers
Humanize-style filters - the friendly date/number formatters like naturalday, naturaltime, intcomma, ordinal, and filesizeformat - are a known category of template helper. minijinja 2 does not ship these, and umbral-core does not register app-level equivalents yet (the admin plugin ships a naturaltime filter, but it's scoped to admin templates only). When humanize-style filters land for app templates they will be documented here.
In the meantime, compute humanized representations in the handler and pass them as context values:
// handler: pre-compute the human-readable stringlet context = context! { article, created_label => humantime::format_duration(elapsed).to_string(),};let html = umbral::templates::render("article.html", &context)?;{# template: consume it like any other variable #}<time>{{ created_label }}</time>See also
- minijinja filter reference - full API docs for every built-in filter, including type signatures and edge cases.
- minijinja test reference - built-in tests usable in
{% if value is defined %}/{% if value is divisibleby(3) %}style. - Rendering HTML - the engine wiring, autoescape policy, and handler integration.
arch.md §4.5- the template security rationale and the deferred custom-filter design.