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

Custom template tags

Add your own filters and functions to umbral templates from a plugin via Plugin::template_registrars.

Custom template tags

A plugin can extend the template engine with its own filters and tags through the Plugin::template_registrars hook: the plugin returns closures that mutate the minijinja Environment at engine-build time, adding filters, functions, and globals that every app-level template can then use.

Info
This is the app-level surface. The admin plugin keeps its own private environment with its own filters (e.g. `naturaltime`); those are not visible to app templates. A registrar returned from `template_registrars` *is*.

The hook

Code
rust
use umbral::prelude::*;
use umbral::templates::TemplateRegistrar;
 
struct HumanizePlugin;
 
impl Plugin for HumanizePlugin {
fn name(&self) -> &'static str { "humanize" }
 
fn template_registrars(&self) -> Vec<TemplateRegistrar> {
vec![Box::new(|env| {
// A filter: {{ "hello" | shout }} -> "HELLO"
env.add_filter("shout", |s: String| s.to_uppercase());
 
// A function: {{ pluralize(count, "item", "items") }}
env.add_function("pluralize", |n: i64, one: String, many: String| {
if n == 1 { one } else { many }
});
})]
}
}
Code
html
<p>{{ heading | shout }}</p>
<p>{{ cart.count }} {{ pluralize(cart.count, "item", "items") }}</p>

Register the plugin on the builder as usual and the tags are live:

Code
rust
App::builder()
.plugin(HumanizePlugin)
.build()?;

Rules

Closures are owned and 'static

A registrar is a `Box) + Send + Sync>`. It can't borrow `self`, so capture any per-plugin config **by value** into the closure. This is what lets umbral stash the registrar and re-run it on every dev-mode hot-reload rebuild.

Applied after the built-ins

Registrars run after the framework's own filters/functions (`img`, `markdown`, `sanitize`, `currency`, `static`, `media_url`, `now`, `querystring_with`, `highlight_styles`) and in plugin dependency order. Because minijinja's `add_filter` / `add_function` overwrite by name, a plugin *can* deliberately replace a built-in by registering the same name. Do this sparingly and document it.

Fn, not FnOnce

The engine is rebuilt whenever a template file changes in dev mode, so each registrar may run many times over a process's life. Keep them pure, with no one-shot side effects.

What you can add

A registrar has the full minijinja Environment API:

CallAddsUsed in a template as
env.add_filter("name", f)a pipe filter{{ value \| name(arg) }}
env.add_function("name", f)a callable{{ name(arg) }}
env.add_global("name", value)a constant value{{ name }}

Filter and function arguments are deserialized from the template call: take String, i64, f64, bool, Option<T> for optional args, or any serde::Deserialize type. Return anything serde::Serialize, or minijinja::Value::from_safe_string(...) to mark generated HTML safe from autoescape (as the markdown filter does).

See also

templatespluginsfiltersminijinja