Template Expansion
This chapter will explain how the different parts of the templates are translated into Rust code.
⚠️ Please note that the generated code might change in the future so the following examples might not be up-to-date.
Basic explanations
When you add #[derive(Template)]
and #[template(...)]
on your type, the
Template
derive proc-macro will then generate an implementation of the
askama::Template
trait which will be a Rust version of the template.
It will also implement the std::fmt::Display
trait on your type which will
internally call the askama::Template
trait.
Let's take a small example:
#![allow(unused)] fn main() { #[derive(Template)] #[template(source = "{% set x = 12 %}", ext = "html")] struct Mine; }
will generate:
#![allow(unused)] fn main() { impl ::askama::Template for YourType { fn render_into( &self, writer: &mut (impl ::std::fmt::Write + ?Sized), ) -> ::askama::Result<()> { let x = 12; ::askama::Result::Ok(()) } const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some( "html", ); const SIZE_HINT: ::std::primitive::usize = 0; const MIME_TYPE: &'static ::std::primitive::str = "text/html; charset=utf-8"; } impl ::std::fmt::Display for YourType { #[inline] fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { ::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {}) } } }
For simplicity, we will only keep the content of the askama::Template::render_into
function from now on.
Text content
If you have "text content" (for example HTML) in your template:
<h1>{{ title }}</h1>
It will generate it like this:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "<h1>{0}</h1>", &::askama::MarkupDisplay::new_unsafe(&(self.title), ::askama::Html), ), )?; ::askama::Result::Ok(()) }
About MarkupDisplay
: we need to use this type in order to prevent generating
invalid HTML. Let's take an example: if title
is "<a>"
and we display it as
is, in the generated HTML, you won't see <a>
but instead a new HTML element
will be created. To prevent this, we need to escape some characters.
In this example, <a>
will become <a>
. And this is why there is the
safe
builtin filter, in case you want it to be displayed as is.
Variables
Variables creation
If you create a variable in your template, it will be created in the generated Rust code as well. For example:
{% set x = 12 %}
{% let y = x + 1 %}
will generate:
#![allow(unused)] fn main() { let x = 12; let y = x + 1; ::askama::Result::Ok(()) }
Variables usage
By default, variables will reference a field from the type on which the askama::Template
trait is implemented:
{{ y }}
This template will expand as follows:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "{0}", &::askama::MarkupDisplay::new_unsafe(&(self.y), ::askama::Html), ), )?; ::askama::Result::Ok(()) }
This is why if the variable is undefined, it won't work with Askama and why we can't check if a variable is defined or not.
You can still access constants and statics by using paths. Let's say you have in your Rust code:
#![allow(unused)] fn main() { const FOO: u32 = 0; }
Then you can use them in your template by referring to them with a path:
{{ crate::FOO }}{{ super::FOO }}{{ self::FOO }}
It will generate:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "{0}{1}{2}", &::askama::MarkupDisplay::new_unsafe(&(crate::FOO), ::askama::Html), &::askama::MarkupDisplay::new_unsafe(&(super::FOO), ::askama::Html), &::askama::MarkupDisplay::new_unsafe(&(self::FOO), ::askama::Html), ), )?; ::askama::Result::Ok(()) }
(Note: crate::
is to get an item at the root level of the crate, super::
is
to get an item in the parent module and self::
is to get an item in the
current module.)
You can also access items from the type that implements Template
as well using
as Self::
, it'll use the same logic.
Control blocks
if/else
The generated code can be more complex than expected, as seen with if
/else
conditions:
{% if x == "a" %}
gateau
{% else %}
tarte
{% endif %}
It will generate:
#![allow(unused)] fn main() { if *(&(self.x == "a") as &bool) { writer.write_str("gateau")?; } else { writer.write_str("tarte")?; } ::askama::Result::Ok(()) }
Very much as expected except for the &(self.x == "a") as &bool
. Now about why
the as &bool
is needed:
The following syntax *(&(...) as &bool)
is used to trigger Rust's automatic
dereferencing, to coerce e.g. &&&&&bool
to bool
. First &(...) as &bool
coerces e.g. &&&bool
to &bool
. Then *(&bool)
finally dereferences it to
bool
.
In short, it allows to fallback to a boolean as much as possible, but it also explains why you can't do:
{% set x = "a" %}
{% if x %}
{{ x }}
{% endif %}
Because it fail to compile because:
error[E0605]: non-primitive cast: `&&str` as `&bool`
if let
{% if let Some(x) = x %}
{{ x }}
{% endif %}
will generate:
#![allow(unused)] fn main() { if let Some(x) = &(self.x) { writer .write_fmt( format_args!( "{0}", &::askama::MarkupDisplay::new_unsafe(&(x), ::askama::Html), ), )?; } }
Loops
{% for user in users %}
{{ user }}
{% endfor %}
will generate:
#![allow(unused)] fn main() { { let _iter = (&self.users).into_iter(); for (user, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) { writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe(&(user), ::askama::Html), ), )?; } } ::askama::Result::Ok(()) }
Now let's see what happens if you add an else
condition:
{% for user in x if x.len() > 2 %}
{{ user }}
{% else %}
{{ x }}
{% endfor %}
Which generates:
#![allow(unused)] fn main() { { let mut _did_loop = false; let _iter = (&self.users).into_iter(); for (user, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) { _did_loop = true; writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe(&(user), ::askama::Html), ), )?; } if !_did_loop { writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe( &(self.x), ::askama::Html, ), ), )?; } } ::askama::Result::Ok(()) }
It creates a _did_loop
variable which will check if we entered the loop. If
we didn't (because the iterator didn't return any value), it will enter the
else
condition by checking if !_did_loop {
.
We can extend it even further if we add an if
condition on our loop:
{% for user in users if users.len() > 2 %}
{{ user }}
{% else %}
{{ x }}
{% endfor %}
which generates:
#![allow(unused)] fn main() { { let mut _did_loop = false; let _iter = (&self.users).into_iter(); let _iter = _iter.filter(|user| -> bool { self.users.len() > 2 }); for (user, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) { _did_loop = true; writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe(&(user), ::askama::Html), ), )?; } if !_did_loop { writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe( &(self.x), ::askama::Html, ), ), )?; } } ::askama::Result::Ok(()) }
It generates an iterator but filters it based on the if
condition (users.len() > 2
).
So once again, if the iterator doesn't return any value, we enter the else
condition.
Of course, if you only have a if
and no else
, the generated code is much
shorter:
{% for user in users if users.len() > 2 %}
{{ user }}
{% endfor %}
Which generates:
#![allow(unused)] fn main() { { let _iter = (&self.users).into_iter(); let _iter = _iter.filter(|user| -> bool { self.users.len() > 2 }); for (user, _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) { writer .write_fmt( format_args!( "\n {0}\n", &::askama::MarkupDisplay::new_unsafe(&(user), ::askama::Html), ), )?; } } ::askama::Result::Ok(()) }
Filters
Example of using the abs
built-in filter:
{{ -2|abs }}
Which generates:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "{0}", &::askama::MarkupDisplay::new_unsafe( &(::askama::filters::abs(-2)?), ::askama::Html, ), ), )?; ::askama::Result::Ok(()) }
The filter is called with -2
as first argument. You can add further arguments
to the call like this:
{{ "a"|indent(4) }}
Which generates:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "{0}", &::askama::MarkupDisplay::new_unsafe( &(::askama::filters::indent("a", 4)?), ::askama::Html, ), ), )?; ::askama::Result::Ok(()) }
No surprise there, 4
is added after "a"
. Now let's check when we chain the filters:
{{ "a"|indent(4)|capitalize }}
Which generates:
#![allow(unused)] fn main() { writer .write_fmt( format_args!( "{0}", &::askama::MarkupDisplay::new_unsafe( &(::askama::filters::capitalize( &(::askama::filters::indent("a", 4)?), )?), ::askama::Html, ), ), )?; ::askama::Result::Ok(()) }
As expected, capitalize
's first argument is the value returned by the indent
call.
Macros
This code:
{% macro heading(arg) %}
<h1>{{arg}}</h1>
{% endmacro %}
{% call heading("title") %}
generates:
#![allow(unused)] fn main() { { let (arg) = (("title")); writer .write_fmt( format_args!( "\n<h1>{0}</h1>\n", &::askama::MarkupDisplay::new_unsafe(&(arg), ::askama::Html), ), )?; } ::askama::Result::Ok(()) }
As you can see, the macro itself isn't present in the generated code, only its internal code is generated as well as its arguments.