This version is in beta. Some features may change before release.

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.

NameKindPurposeUsage
imgfilterBuild 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") }}
markdownfilterRender CommonMark + GFM (tables, strikethrough, task lists, footnotes) to HTML, then sanitize through ammonia. Output is marked safe.{{ body \| markdown }}
sanitizefilterClean stored HTML (e.g. the admin RTE widget's output) down to ammonia's safe allowlist and mark it safe.{{ body \| sanitize }}
nowfunctionCurrent UTC time. No argument → RFC 3339; a chrono strftime string → formatted.{{ now("%Y-%m-%d") }}
currencyfilterFormat 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") }}
staticfunctionResolve 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_urlfunctionResolve 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_withfunctionRebuild 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_stylesfunctionEmit the base16-ocean.dark syntect token stylesheet (wrapped in <style>) for \| markdown server-side code highlighting. Call once in <head>.{{ highlight_styles() }}
csrf_tokenglobalThe 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_inputglobalA ready-made <input type="hidden" name="csrf_token" ...>, marked safe. Drop it straight into a form.<form method="post">{{ csrf_input }}</form>
userglobalThe 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.

Code
html
{# 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") }}
Info

None and Undefined render as the empty string. umbral installs a custom formatter so an `Option

` field that is `None`, or an undefined variable, renders as `""` instead of the literal tokens `none` / `undefined`. This means `` on a fresh form yields `value=""`, not `value="none"`. Non-null values pass through the normal escaping formatter unchanged.

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

FilterPurposeUsage
upperConvert to uppercase{{ name | upper }}
lowerConvert to lowercase{{ name | lower }}
titleTitle-case each word{{ name | title }}
capitalizeUppercase first character, lowercase the rest{{ name | capitalize }}
trimStrip leading and trailing whitespace{{ text | trim }}
replace(from, to)Replace all occurrences of from with to{{ s | replace("old", "new") }}
safeMark a value as HTML-safe, bypassing autoescape{{ content | safe }}
escapeHTML-escape the value (same as the autoescaper, but explicit){{ text | escape }}
urlencodePercent-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) }}
Code
html
{# templates/profile.html #}
<h1>{{ user.name | title }}</h1>
<p>{{ bio | default("No bio.") }}</p>

Number filters

FilterPurposeUsage
absAbsolute value{{ value | abs }}
round(precision)Round to precision decimal places (default 0){{ price | round(2) }}
floatCast to float{{ value | float }}
intCast to integer{{ value | int }}
Code
html
<p>Price: {{ product.price | round(2) }}</p>
<p>In stock: {{ inventory | abs }}</p>

List and collection filters

FilterPurposeUsage
lengthNumber of items in a sequence or characters in a string{{ items | length }}
firstFirst element of a sequence{{ items | first }}
lastLast element of a sequence{{ items | last }}
reverseReverse a sequence{{ items | reverse }}
sortSort a sequence (ascending by default){{ items | sort }}
uniqueRemove duplicate values from a sequence{{ items | unique }}
join(sep)Join items with a separator string (default ""){{ tags | join(", ") }}
listCoerce a value to a list{{ value | list }}
minSmallest value in a sequence{{ scores | min }}
maxLargest value in a sequence{{ scores | max }}
sumSum 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) }}
Code
html
{# 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

FilterPurposeUsage
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.") }}
dictsortSort a dict by its keys{{ mapping | dictsort }}
itemsReturn (key, value) pairs from a dict{{ mapping | items }}
tojsonSerialize the value to a JSON string{{ payload | tojson }}
Code
html
<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.

Code
html
{# 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.

FunctionPurposeUsage
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() }}
Code
html
{% 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 &lt;script&gt; automatically. To render pre-trusted HTML - for example, a sanitised rich-text field - pipe it through safe:

Code
html
{# 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:

Code
rust
// handler: pre-compute the human-readable string
let context = context! {
article,
created_label => humantime::format_duration(elapsed).to_string(),
};
let html = umbral::templates::render("article.html", &context)?;
Code
html
{# 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.
templatesfiltershelpersminijinja