From e78989e9bd2882a5d2561ed4b13a4e96fe5e53e4 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 30 Oct 2023 11:54:48 +0100 Subject: Add new chapter in the askama book about template expansion --- book/src/template_expansion.md | 485 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 book/src/template_expansion.md (limited to 'book') diff --git a/book/src/template_expansion.md b/book/src/template_expansion.md new file mode 100644 index 0000000..a1c39fe --- /dev/null +++ b/book/src/template_expansion.md @@ -0,0 +1,485 @@ +# 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 + +Whwn 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: + +```rust +#[derive(Template)] +#[template(source = "{% set x = 12 %}", ext = "html")] +struct Mine; +``` + +will generate: + +```rust +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: + +```html +

{{ title }}

+``` + +It will generate it like this: + +```rust +writer + .write_fmt( + format_args!( + "

{0}

", + &::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 `""` and we display it as +is, in the generated HTML, you won't see `` but instead a new HTML element +will be created. To prevent this, we need to escape some characters. + +In this example, `` 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: + +```jinja +{% set x = 12 %} +{% let y = x + 1 %} +``` + +will generate: + +```rust +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: + +```jinja +{{ y }} +``` + +This template will expand as follows: + +```rust +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: + +```rust +const FOO: u32 = 0; +``` + +Then you can use them in your template by referring to them with a path: + +```jinja +{{ crate::FOO }}{{ super::FOO }}{{ self::FOO }} +``` + +It will generate: + +```rust +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: + +```jinja +{% if x == "a" %} +gateau +{% else %} +tarte +{% endif %} +``` + +It will generate: + +```rust +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: + +```jinja +{% set x = "a" %} +{% if x %} + {{ x }} +{% endif %} +``` + +Because it fail to compile because: + +```console +error[E0605]: non-primitive cast: `&&str` as `&bool` +``` + +### if let + +```jinja +{% if let Some(x) = x %} + {{ x }} +{% endif %} +``` + +will generate: + +```rust +if let Some(x) = &(self.x) { + writer + .write_fmt( + format_args!( + "{0}", + &::askama::MarkupDisplay::new_unsafe(&(x), ::askama::Html), + ), + )?; +} +``` + +### Loops + +```html +{% for user in users %} + {{ user }} +{% endfor %} +``` + +will generate: + +```rust +{ + 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: + +```jinja +{% for user in x if x.len() > 2 %} + {{ user }} +{% else %} + {{ x }} +{% endfor %} +``` + +Which generates: + +```rust +{ + 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: + +```jinja +{% for user in users if users.len() > 2 %} + {{ user }} +{% else %} + {{ x }} +{% endfor %} +``` + +which generates: + +```rust +{ + 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: + +```jinja +{% for user in users if users.len() > 2 %} + {{ user }} +{% endfor %} +``` + +Which generates: + +```rust +{ + 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: + +```jinja +{{ -2|abs }} +``` + +Which generates: + +```rust +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: + +```jinja +{{ "a"|indent(4) }} +``` + +Which generates: + +```rust +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: + +```jinja +{{ "a"|indent(4)|capitalize }} +``` + +Which generates: + +```rust +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: + +```html +{% macro heading(arg) %} +

{{arg}}

+{% endmacro %} + +{% call heading("title") %} +``` + +generates: + +```rust +{ + let (arg) = (("title")); + writer + .write_fmt( + format_args!( + "\n

{0}

\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. -- cgit