Askama
Askama 基于 Jinja 实现的模板渲染引擎。 它会在编译时根据用户定义的用于保存模板上下文的 struct,从你的模板中生成 Rust 代码。 具体示例如下:
欢迎所有反馈!如有 bug、文档需求或其他任何意见,请随时提交到askama github仓库issues.
如果你想在线体验 Askama 的代码生成,可以访问 Askama Playground。
特性亮点
- 使用熟悉且易用的语法构建模板
- 受益于 Rust 类型系统带来的安全性
- 模板代码会被编译到你的 crate 中,以获得最佳性能
- 提供调试功能,协助你进行模板开发
- 模板必须是有效的 UTF-8,并渲染生成 UTF-8 输出
- 可在稳定版 Rust 上运行
模板中支持的功能
- 模板继承
- 循环、if/else 语句和 include 支持
- 宏支持
- 变量(不可变变量)
- 多种内置过滤器,并支持使用自定义过滤器
- 使用 - 标记抑制空白字符
- 可选择关闭 HTML 转义
- 语法自定义
快速开始
首先,在你的 crate 的 Cargo.toml 中添加以下内容:
# in [dependencies] section 版本号根据需要更新
askama = "0.14.0"
然后,在 crate 根目录下创建一个名为 templates 的目录。在其中创建一个名为 hello.html 的文件,内容如下:
Hello, {{ name }}!
在你的 crate 内的任意 Rust 文件中,添加以下代码:
use askama::Template; // bring trait in scope
#[derive(Template)] // this will generate the code...
#[template(path = "hello.html")] // using the template in this path, relative
// to the `templates` dir in the crate root
struct HelloTemplate<'a> { // the name of the struct can be anything
name: &'a str, // the field name should match the variable name
// in your template
}
fn main() {
let hello = HelloTemplate { name: "world" }; // instantiate your struct
println!("{}", hello.render().unwrap()); // then render it.
}
现在你应该能够编译并运行这段代码了,正确运行输出结果应为:
Hello, world!
创建模板
Askama 模板通过定义 struct 来提供模板上下文,并关联一个 UTF-8 编码的文本文件(或内联源码,见下文)。Askama 可用于生成任何基于文本的格式。模板文件的扩展名可用于提供内容类型提示。
模板由文本内容(原样传递)、表达式(渲染时会被替换为内容)以及标签(控制模板逻辑)组成。模板语法 使用非常简单的 Jinja语法,并与Jinja 衍生品Twig or Tera非常相似.
#![allow(unused)]
fn main() {
#[derive(Template)] // 驱动生成模板代码...
#[template(path = "hello.html")] // 使用该路径下的模板,路径相对于 crate 根目录下的 `templates` 目录
struct HelloTemplate<'a> { // 结构体名称可以是任意合法标识符
name: &'a str, // 字段名应与模板中的变量名匹配
}
}
template() 属性
Askama 通过为所有带有 #[derive(Template)] 属性的结构体类型生成一个或多个 trait 实现模板功能。代码生成过程接受一些选项,可通过 template() 属性指定。目前支持以下子属性:
(例如 path = “foo.html”):设置模板文件的路径。该路径相对于配置的模板目录(默认情况下,是 Cargo.toml 旁边的 templates 目录)进行解析。文件扩展名用于推断转义模式(见下文)。在 Web 框架集成中,路径的扩展名也可用于推断生成响应的内容类型。不能与 source 同时使用。
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> { ... }
}
(例如 source = “{{ foo }}”):直接设置模板源码。这对于测试用例或短模板很有用。生成的路径未定义,通常使得无法从其他模板引用此模板。如果指定了 source,则还必须指定 ext(见下文)。不能与 path 同时使用。
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(source = "Hello {{ name }}")]
struct HelloTemplate<'a> {
name: &'a str,
}
}
-
in_doc(例如
in_doc = true):请参阅 “documentation as template code”。 -
ext(例如
ext = "txt"):允许将内容类型指定为文件扩展名。这用于推断转义模式(见下文),某些 Web 框架集成会用它来确定内容类型。不能与 path 同时使用。#![allow(unused)] fn main() { #[derive(Template)] #[template(source = "Hello {{ name }}", ext = "txt")] struct HelloTemplate<'a> { name: &'a str, } } -
print(例如
print = "code"):启用调试,打印内容可以是空(none)、解析后的语法树(ast)、生成的代码(code)或两者都打印(all)。所请求的数据将在编译时打印到标准输出。#![allow(unused)] fn main() { #[derive(Template)] #[template(path = "hello.html", print = "all")] struct HelloTemplate<'a> { ... } } -
block(例如
block = "block_name"):单独渲染块。块外部的表达式不会被结构体要求,并且同样支持继承。当你需要分解模板进行部分渲染,而又不想将局部内容提取到单独的模板或宏中时,此选项很有用。#![allow(unused)] fn main() { #[derive(Template)] #[template(path = "hello.html", block = "hello")] struct HelloTemplate<'a> { ... } } -
blocks(例如
blocks = ["title", "content"]):自动生成(若干)子模板,其行为就像带有 block = “…” 属性一样。你可以通过方法my_template.as_block_name()访问这些子模板,其中 block_name 是块的名称:#![allow(unused)] fn main() { #[derive(Template)] #[template( ext = "txt", source = " {% block title %} ... {% endblock %} {% block content %} ... {% endblock %} ", blocks = ["title", "content"] )] struct News<'a> { title: &'a str, message: &'a str, } let news = News { title: "Announcing Rust 1.84.1", message: "The Rust team has published a new point release of Rust, 1.84.1.", }; assert_eq!( news.as_title().render().unwrap(), "<h1>Announcing Rust 1.84.1</h1>" ); } -
escape(例如
escape = "none"):覆盖模板扩展名,用于确定此模板的转义器。有关配置自定义转义器的更多信息,请参见相关章节。#![allow(unused)] fn main() { #[derive(Template)] #[template(path = "hello.html", escape = "none")] struct HelloTemplate<'a> { ... } } -
syntax(例如
syntax = "foo"):设置配置文件中定义的解析器的语法名称。默认语法是 “default”,即 Askama 提供的语法。#![allow(unused)] fn main() { #[derive(Template)] #[template(path = "hello.html", syntax = "foo")] struct HelloTemplate<'a> { ... } } -
config(例如
config = "config_file_path"):设置要使用的配置文件的路径。该路径相对于你的 crate 根目录进行解析。#![allow(unused)] fn main() { #[derive(Template)] #[template(path = "hello.html", config = "config.toml")] struct HelloTemplate<'a> { ... } } -
askama(例如
askama = askama): 如果你在子项目、库或macro中使用 askama,可能需要指定查找 askama 模块的path:#![allow(unused)] fn main() { #[doc(hidden)] use askama as __askama; #[macro_export] macro_rules! new_greeter { ($name:ident) => { #[derive(Debug, $crate::askama::Template)] #[template( ext = "txt", source = "Hello, world!", askama = $crate::__askama )] struct $name; } } new_greeter!(HelloWorld); assert_eq!(HelloWorld.to_string(), Ok("Hello, world.")); }
枚举实现模板
可以为 struct 和 enum 添加 Template derive。如果仅对条目本身添加 #[template()],则两种条目类型的工作方式完全相同。但对于 enum,你还可以选择为其中一个、部分或所有变体添加专门的实现:
#![allow(unused)]
fn main() {
#[derive(Debug, Template)]
#[template(path = "area.txt")]
enum Area {
Square(f32),
Rectangle { a: f32, b: f32 },
Circle { radius: f32 },
}
}
{%- match self -%}
{%- when Self::Square(side) -%}
{{side}}^2
{%- when Self::Rectangle { a, b} -%}
{{a}} * {{b}}
{%- when Self::Circle { radius } -%}
pi * {{radius}}^2
{%- endmatch -%}
将得到与以下代码相同的结果:
#![allow(unused)]
fn main() {
#[derive(Template, Debug)]
#[template(ext = "txt")]
enum AreaPerVariant {
#[template(source = "{{self.0}}^2")]
Square(f32),
#[template(source = "{{a}} * {{b}}")]
Rectangle { a: f32, b: f32 },
#[template(source = "pi * {{radius}}^2")]
Circle { radius: f32 },
}
}
如你所见,通过 ext 属性,enum 变体会继承 enum 的大部分设置:config、escape、ext、syntax 和 whitespace。不继承的有:block 和 print。
如果某个 enum 变体没有 #[template] 注解,那么该 enum 需要一个默认实现,当 self 为该变体时会使用它。仅在模板上注解或在所有变体上注解之间的一种良好折衷,可能是对成员使用 block 参数:
#![allow(unused)]
fn main() {
#[derive(Template, Debug)]
#[template(path = "area.txt")]
enum AreaWithBlocks {
#[template(block = "square")]
Square(f32),
#[template(block = "rectangle")]
Rectangle { a: f32, b: f32 },
#[template(block = "circle")]
Circle { radius: f32 },
}
}
{%- block square -%}
{{self.0}}^2
{%- endblock -%}
{%- block rectangle -%}
{{a}} * {{b}}
{%- endblock -%}
{%- block circle -%}
pi * {{radius}}^2
{%- endblock -%}
将文档作为模板代码
作为将模板代码放在外部文件(例如 path argument)或字符串(例如 source argument)中的替代方案,你也可以启用 "code-in-doc" 特性。启用后,你可以直接在模板条目的文档中指定模板代码。
此时,不在 #[template] 属性中使用 path = "…" 或 source = "…",而是指定 in_doc = true,并在条目的文档中添加一个带有 askama 属性的代码块:
#![allow(unused)]
fn main() {
/// Here you can put our usual comments.
///
/// ```askama
/// <div>{{ lines|linebreaksbr }}</div>
/// ```
///
/// Any usual docs, including tests can be put in here, too:
///
/// ```rust
/// assert_eq!(
/// Example { lines: "a\nb\nc" }.to_string(),
/// "<div>a<br/>b<br/>c</div>"
/// );
/// ```
///
/// All comments are still optional, though.
#[derive(Template)]
#[template(ext = "html", in_doc = true)]
struct Example<'a> {
lines: &'a str,
}
}
如果你想在注释中提供模板代码,则还必须指定 ext 参数,例如 #[template(ext = "html")]。
除了 askama,你还可以写 jinja 或 jinja2,例如以便更好地与语法高亮工具配合使用。
运行时 Values
可以在运行时定义变量,并通过 value 过滤器或 askama::get_value 函数在模板中使用它们,还可以调用 render 方法的 _with_values 变体。这些方法需要一个额外参数,该参数需实现 Values trait。std库提供的几种类型(如 HashMap)已实现了该 trait
#![allow(unused)]
fn main() {
use std::collections::HashMap;
let mut values: HashMap<&str, Box<dyn Any>> = HashMap::new();
// We add a new value named "name" with the value "Bibop".
values.insert("name", Box::new("Bibop"));
values.insert("age", Box::new(12u32));
}
Values trait 期望存储的数据类型能够配合 Any trait,从而允许存储任意类型的值。
然后,使用这些值进行渲染:
#![allow(unused)]
fn main() {
template_struct.render_with_values(&values).unwrap();
}
有两种方式可以从模板中获取这些值:一种是使用 value 过滤器,另一种是直接调用 askama::get_value 函数:
{% if let Ok(name) = "name"|value::<&str> %}
name is {{ name }}
{% endif %}
{% if let Ok(age) = askama::get_value::<u32>("age") %}
age is {{ age }}
{% endif %}
如果尝试用错误的类型获取值,或者获取一个未设置的值,将得到 Err(askama::Error::ValueType) 或 Err(askama::Error::ValueMissing)。
另一个使用键值元组的示例:
#![allow(unused)]
fn main() {
let value = "value".to_string();
let tuple: (&str, &dyn Any) = ("a", &value);
template_struct.render_with_values(&tuple).unwrap();
}
这样的设置下,只有键 "a" 会返回一个值:
{% if let Ok(name) = "a"|value::<String> %}
a is {{ a }}
{% endif %}
调试与故障排查
你可以通过修改模板结构体上 template 属性的参数列表,来查看模板的解析树以及生成的代码:
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(path = "hello.html", print = "all")]
struct HelloTemplate<'a> { ... }
}
print 键可以取以下四个值之一:
none(默认值)ast(打印解析树)code(print the generated code)all(同时打印解析树和代码)
在编译过程中,生成的输出将打印到 stderr。
对于示例模板,解析树如下所示:
#![allow(unused)]
fn main() {
[Lit("", "Hello,", " "), Expr(WS(false, false), Var("name")), Lit("", "!", "\n")]
}
生成的代码如下所示:
#![allow(unused)]
fn main() {
impl<'a> askama::Template for HelloWorld<'a> {
fn render_into<AskamaW>(&self, __askama_writer: &mut AskamaW) -> askama::Result<()>
where
AskamaW: core::fmt::Write + ?Sized,
{
__askama_writer.write_str("Hello, ")?;
match (
&((&&askama::filters::AutoEscaper::new(
&(self.name),
askama::filters::Html,
))
.askama_auto_escape()?),
) {
(expr2,) => {
(&&askama::filters::Writable(expr2)).askama_write(__askama_writer)?;
}
}
__askama_writer.write_str("!")?;
Ok(())
}
const SIZE_HINT: usize = 11usize;
}
}
配置
在编译时,如果启用了 config 特性(默认启用),Askama 会从 crate 根目录(即 Cargo.toml 所在目录)下的 askama.toml 文件中读取可选的配置值。目前,该配置涵盖模板搜索目录、自定义语法配置和转义器配置。
以下示例文件展示了默认配置:
[general]
# Directories to search for templates, relative to the crate root.
dirs = ["templates"]
# Unless you add a `-` in a block, whitespace characters won't be trimmed.
whitespace = "preserve"
请注意,dirs 支持通配符(*)语法。因此你可以写成:
[general]
dirs = ["templates/*"]
甚至:
[general]
dirs = ["templates/**"]
如果你需要包含子文件夹的话。
空白字符控制
在默认配置下,你可以使用 - 操作符来指示在块之前或之后抑制空白字符。例如:
<div>
{%- if something %}
Hello
{% endif %}
在上面的模板中,只有 <div> 和 {%- 之间的空白字符会被抑制。如果你将 whitespace 设置为 "suppress":
[general]
whitespace = "suppress"
那么每个块前后的空白字符将默认被抑制。要保留空白字符,可以使用 + 操作符:
{% if something +%}
Hello
{%+ endif %}
在这个例子中,Hello 将被换行符包围。
还有第三种可能:如果你希望抑制所有空白字符,但保留一个,可以使用 ~:
{% if something ~%}
Hello
{%~ endif %}
需要注意的是,如果被修剪的字符中包含换行符,那么最终保留下来的唯一字符将是一个换行符。
如果你希望这是默认行为,可以将 whitespace 设置为 "minimize":
[general]
whitespace = "minimize"
需要注意的是,也可以直接在 template 派生过程宏中配置 whitespace:
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(whitespace = "suppress")]
pub struct SomeTemplate;
}
如果你直接在 template 派生过程宏中配置了 whitespace,它将优先于配置文件中的设置。因此,在这种情况下,即使你在配置文件中设置了 whitespace = "minimize",该模板也会被替换为 suppress。
自定义语法
以下示例定义了两个自定义语法:
[general]
default_syntax = "foo"
[[syntax]]
name = "foo"
block_start = "%{"
comment_start = "#{"
expr_end = "^^"
[[syntax]]
name = "bar"
block_start = "%%"
block_end = "%%"
comment_start = "%#"
expr_start = "%{"
一个语法块至少包含 name 属性,用于在项目中唯一标识该语法。
目前可以使用以下键来自定义模板语法:
block_start, defaults to{%block_end, defaults to%}comment_start, defaults to{#comment_end, defaults to#}expr_start, defaults to{{expr_end, defaults to}}
值必须至少为两个字符长。如果省略某个键,则使用默认语法中的对应值。
转义器
以下是一个自定义转义器的示例:
[[escaper]]
path = "::tex_escape::Tex"
extensions = ["tex"]
一个转义器块包含 path 和 extensions 属性。path 包含一个 Rust 标识符,该标识符必须在使用该转义器的模板的作用域内可见。此类型必须实现 Escaper trait。
extensions 定义了一个文件扩展名列表,当模板使用这些扩展名时会触发该转义器。扩展名的匹配顺序从第一个配置的转义器开始,最后是 HTML(扩展名 html、htm、xml、j2、jinja、jinja2)和纯文本(无转义;扩展名 md、yml、none、txt 和空字符串)的默认转义器。请注意,这意味着你也可以定义其他转义器,将不同的扩展名匹配到同一个转义器。
然后,你就可以使用带有该扩展名的模板,或者在模板中使用带有你扩展名名称的 escape 过滤器:
{{ some_string|escape("tex") }}
举个例子,我们希望 .js 文件像 “txt” 文件一样被处理。可以这样做:
[[escaper]]
path = "askama::filters::Text"
extensions = ["js"]
Text 实现了 Escaper trait,因为我们不需要对 .js 文件进行任何转义,所以直接使用它。
你可以在 askama 仓库中查看custom escaper example。
模板语法
语法概览
| 语法 | 描述 |
|---|---|
{{ ... }} | 要求值的表达式,会被转义并输出 |
{{ ... | ... }} | 带过滤器的表达式 |
{% filter ... %} ... {% endfilter %} | 过滤器块 |
{# ... #} | 注释 |
{% let ... = ... %} or {% set ... = ... %} | 变量定义赋值 |
{% decl ... %} or {% declare ... = ... %} | 稍后设置变量值 |
{% if ... %} ... {% else if ... %} ... {% else %} ... {% endif %} | If-Else 条件块 |
{% match ... %} {% when ... %} ... {% else %} ... {% endmatch %} | Match 块 |
{% for ... in ... %} ... {% else %} ... {% endfor %} | For 循环块 |
{% continue %} | 继续循环的下一次迭代 |
{% break %} | 跳出循环 |
{% include "..." %} | 包含另一个模板 |
{% extends "..." %} | 模板继承 |
{% block ... %} ... {% endblock %} | 用于继承的块定义 |
{% macro ...(...) %} ... {% endmacro %} | 宏定义 |
{{ ...(...) }} | 宏调用 |
{% call ...(...) %}{% endcall %} | 宏调用块 |
{% import "..." as ... %} | 从另一个模板导入宏 |
{% raw %} ... {% endraw %} | 原始块 – 原样输出内容(不进行模板处理) |
变量
模板顶层变量由模板的上下文类型定义。你可以使用点号(.)来访问变量的属性或方法。读取变量需遵循通常的借用规则。例如,{{ name }} 会从模板上下文中获取 name 字段,而 {{ user.name }} 会从模板上下文的 user 字段中获取其 name 字段。
在模板中使用常量
你可以使用在 Rust 代码中定义的常量。例如,如果你的 crate 根目录中定义了:
#![allow(unused)]
fn main() {
pub const MAX_NB_USERS: usize = 2;
}
在crate root中定义好常量,可以在模板中通过 crate::MAX_NB_USERS 来使用它:
<p>The user limit is {{ crate::MAX_NB_USERS }}.</p>
{% set value = 4 %}
{% if value > crate::MAX_NB_USERS %}
<p>{{ value }} is bigger than MAX_NB_USERS.</p>
{% else %}
<p>{{ value }} is less than MAX_NB_USERS.</p>
{% endif %}
赋值
在代码块内部,你可以声明变量或为变量赋值。赋值不能被其他模板导入。
赋值使用 let 标签:
{% let name = user.name %}
{% let len = name.len() %}
与 Rust 类似,Askama 也支持变量遮蔽(即重新绑定变量值):
{% let foo = "bar" %}
{{ foo }}
{% let foo = "baz" %}
{{ foo }}
你可以使用 mut 关键字将变量声明为可变:
You can declare variables as mutable with the mut keyword:
{# 在这个例子中,`foo` 是一个迭代器。如果想要迭代它,需要它是可变的 #}
{% let mut foo = [1, 2].iter() %}
{{ foo.next().unwrap() }}
为兼容 Jinja语法,set 可以替代 let 使用。
Let/set 块
你可以创建一个变量,并用一个块计算出的字符串来初始化它:
{% let x %}
{{ crate::some_function() }} = {{ a * b}}
{% endlet %}
稍后设置变量值
如果你想创建一个变量,但根据条件来设置它的值,可以使用 decl(或 declare)关键字声明它但不赋初值:
{% decl val -%}
{% if len == 0 -%}
{% let val = "foo" -%}
{% else -%}
{% let val = name -%}
{% endif -%}
{{ val }}
借用规则
在某些情况下,变量初始化值会被放在引用后面以防止所有权转移。规则如下: In some cases, the value of a variable initialization will be put behind a reference to prevent changing ownership. The rules are as follows:
- 如果值是一个包含多个元素的表达式(如
x + 2),不会被放在引用后面。 - 如果值是在模板中定义的变量,不会被放在引用后面。
- 如果值应用了过滤器(如
x|capitalize),不会被放在引用后面。 - 如果值是一个字段(如
x.y),会被放在引用后面。 - 如果表达式以问号结尾(如
x?),不会被放在引用后面。
复合赋值
使用 mut 关键字,也可以进行复合赋值(也称为“增强赋值”),例如 x += 1 使 x 增加 1:
{%- let mut counter = 0 -%}
{%- for i in 1..=10 -%}
{%- mut counter += i -%}
{{ counter }}
{% endfor -%}
这个示例将输出 1 3 6 10 15……。
目标可以是一个变量或更复杂的表达式。规则与 Rust 相同,例如表达式的左侧(即赋值目标)必须是可变的。Rust 中所有有效的复合赋值运算符在 Askama 中同样有效。
过滤器
变量获取的值可以使用过滤器进行后处理。过滤器使用竖线符号(|)应用于值,并且可以有可选的在括号中的额外参数。过滤器可以链式调用,此时前一个过滤器的输出会传递给下一个。
例如,{{ "{:?}"|format(name|escape) }} 会转义从访问 name 字段获得的值中的 HTML 字符,并将结果字符串作为 Rust 字面量打印。
内置过滤器在过滤器文档中有详细说明。
要定义自己的过滤器,只需在派生 Template impl 的作用域内有一个名为 filters 的模块即可。注意,如果发生名称冲突,内置过滤器优先。
过滤器块
你可以使用过滤器块将过滤器一次性应用于整个块:
{% filter lower %}
{{ t }} / HELLO / {{ u }}
{% endfilter %}
lower 过滤器将应用于整个内容。
与过滤器类似,你也可以组合它们:
{% filter lower|capitalize %}
{{ t }} / HELLO / {{ u }}
{% endfilter %}
这种情况下,会先调用 lower,然后对 lower 返回的结果调用 capitalize。
空白字符控制
Askama 将所有制表符、空格、换行符和回车符都视为空白字符。默认情况下,它会保留模板代码中的所有空白字符,但会抑制末尾的一个换行符。然而,可以通过在开始分隔符后或结束分隔符前直接写入减号来抑制表达式和块分隔符前后的空白字符。
示例如下:
{% if foo %}
{{- bar -}}
{% else if another -%}
nothing
{%- endif %}
这会丢弃 if/else 块内的所有空白字符。如果字面量(模板中未被 {% %} 或 {{ }} 包围的任何部分)只包含空白字符,则两侧的空白抑制将完全抑制该字面量内容。
如果空白字符默认控制设为 “suppress”,而你想保留块或表达式某一侧的空白字符,则需要使用 +。示例:
<a href="/" {#+ #}
class="something">text</a>
在上面的示例中,href 和 class 属性之间保留了一个空白字符。
还有第三种可能。如果你希望抑制所有空白字符,但保留一个("minimize"),可以使用 ~:
{% if something ~%}
Hello
{%~ endif %}
需要注意的是,如果被修剪的字符中包含换行符,那么最终保留下来的唯一字符将是一个换行符。
空白字符控制也可以通过配置文档或在派生宏中定义。这些定义的优先级遵循从全局到局部的顺序:
- Inline (
-,+,~) - Derive (
#[template(whitespace = "suppress")]) - Configuration (in
askama.toml,whitespace = "preserve")
两个内联空白控制可能指向同一个空白范围。在这种情况下,它们按以下优先级解析:
- Suppress (
-) - Minimize (
~) - Preserve (
+)
函数
在模板中调用函数有几种方式,取决于函数定义的位置。这些方式包括:
- 模板结构体字段
- 静态函数
- 结构体/特性 实现
模板结构体字段
当函数是模板结构体的一个字段时,我们可以直接通过调用字段名称(后跟括号包含所需参数)来调用它。例如,对于以下 MyTemplate 结构体,我们可以调用函数 foo:
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(source = "{{ foo(123) }}", ext = "txt")]
struct MyTemplate {
foo: fn(u32) -> String,
}
}
然而,由于我们每次创建 MyTemplate 实例时都需要定义这个函数,这可能不是为模板关联某些行为的最理想方式。
静态函数
当函数与模板定义在同一个 Rust 模块中时,我们可以使用 self 路径前缀来调用它,其中 self 表示模板结构体所在模块的作用域。
例如,这里我们在 MyTemplate 结构体的源码中通过 self::foo(123)调用函数 foo`:
#![allow(unused)]
fn main() {
fn foo(val: u32) -> String {
format!("{}", val)
}
#[derive(Template)]
#[template(source = "{{ self::foo(123) }}", ext = "txt")]
struct MyTemplate;
}
这样做的优点是可以跨多个模板共享功能,而无需将函数公开到其模块之外。 然而,我们不仅限于同一模块内定义的局部函数。我们可以通过在模板源码中指定函数的完整路径来调用任何公共函数。例如,给定一个工具模块如:
#![allow(unused)]
fn main() {
// src/templates/utils/mod.rs
pub fn foo(val: u32) -> String {
format!("{}", val)
}
}
在我们的 MyTemplate 源码中,可以通过以下方式调用 foo 函数:
#![allow(unused)]
fn main() {
// src/templates/my_template.rs
#[derive(Template)]
#[template(source = "{{ crate::templates::utils::foo(123) }}", ext = "txt")]
struct MyTemplate;
}
结构体 / 特性 实现
最后,我们可以调用模板结构体的方法:
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(source = "{{ foo(123) }}", ext = "txt")]
struct MyTemplate {
count: u32,
};
impl MyTemplate {
fn foo(&self, val: u32) -> String {
format!("{} is the count, {} is the value", self.count, val)
}
}
}
你也可以使用 self.foo(123),甚至 Self::foo(self, 123),随你喜欢。
类似地,使用 Self 路径,我们也可以调用已为模板结构体实现的任何 特性 方法:
#![allow(unused)]
fn main() {
trait Hello {
fn greet(name: &str) -> String;
}
#[derive(Template)]
#[template(source = r#"{{ Self::greet("world") }}"#, ext = "txt")]
struct MyTemplate;
impl Hello for MyTemplate {
fn greet(name: &str) -> String {
format!("Hello {}", name)
}
}
}
如果你想调用一个作为字段的闭包,你需要遵循 Rust 的语法,将调用用括号括起来:
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(source = "{{ (closure)(12) }}", ext = "txt")]
struct MyTemplate {
closure: fn(i32) -> i32,
}
}
调用函数
如果你只提供一个函数名,askama 会假定它是一个方法。如果你想调用一个函数,你需要使用路径: If you only provide a function name, askama will assume it’s a method. If you want to call a function, you will need to use a path instead:
{# This is the equivalent of `self.method()`. #}
{{ method() }}
{# This is the equivalent of `self::function()`, which will call the
`function` function from the current module. #}
{{ self::function() }}
{# This is the equivalent of `super::b::f()`. #}
{{ super::b::f() }}
创建结构体
Askama 支持类似于Rust中的方式创建结构体:
{{ MyStruct { field1: 1, field2: "foo" }.to_string() }}
也支持使用 base structs:
{{ MyStruct { field1: 1, ..other_struct } }}
{{ MyStruct { field1: 1, ..Default::default() } }}
模板继承
模板继承允许你构建一个包含通用元素的基础模板,这些元素可以由所有继承模板共享。基础模板定义块,子模板可以覆盖这些块。
基础模板
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{{ title }} - My Site{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<div id="content">
{% block content %}<p>Placeholder content</p>{% endblock %}
</div>
</body>
</html>
block 标签定义了三个块,子模板可以填充这些块。基础模板定义了块的默认版本。基础模板必须定义一个或多个块以启用继承。块只能在模板的顶层或其他块内部指定,不能在 if/else 分支或 for 循环体内指定。
也可以在 endblock 中使用 block 名称(声明和使用时均可):
{% block content %}<p>Placeholder content</p>{% endblock content %}
子模板
这是一个子模板的示例:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
<style>
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p>Hello, world!</p>
{{ super() }}
{% endblock %}
extends 标签告诉代码生成器,此模板继承自另一个模板。它会先相对于自身查找基础模板,然后再相对于模板基础目录查找。它将渲染基础模板的顶层内容,并用子模板的块替换基础模板中的块。在子模板的块内部,可以调用 super() 宏来渲染父块的内容。
由于子模板的顶层内容会被忽略,因此 extends 标签不支持空白字符控制:
{%- extends "base.html" +%}
上述代码会被拒绝,因为我们使用了 - 和 +。有关空白字符控制的更多信息,请查看此处此处。
块片段
此外,块本身也可以单独渲染。当你需要分解模板进行部分渲染,而又不想将局部内容提取到单独的模板或宏中时,这很有用。可以通过 block 参数实现。
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(path = "...", block = "my_block")]
struct BlockFragment {
name: String,
}
}
HTML转义
Askama 默认在分析渲染 HTML 内容时对变量进行转义。它根据模板文件名的扩展名推断转义上下文,如果扩展名是 html、htm 或 xml,则默认转义。当在属性中将模板指定为 source 时,必须使用 ext 属性参数来指定类型。此外,你也可以通过设置 escape 属性参数值(为 none 或 html)来显式指定模板的转义模式。
Askama 根据 OWASP 转义建议 转义 <、>、&、" 和 '。使用 safe 过滤器可以防止对单个表达式进行转义,或者使用 escape(或 e)过滤器在未转义上下文中对单个表达式进行转义。
#[derive(Template)]
#[template(source = "{{strvar}}")]
struct TestTemplate {
strvar: String,
}
fn main() {
let s = TestTemplate {
strvar: "// my <html> is \"unsafe\" & should be 'escaped'".to_string(),
};
assert_eq!(
s.render().unwrap(),
"// my <html> is "unsafe" & \
should be 'escaped'"
);
}
控制结构
For
遍历迭代器中的每个元素。例如:
<h1>Users</h1>
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
你可以通过添加 if 条件来过滤元素:
<h1>Users</h1>
<ul>
{% for user in users if user.is_activated %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
你可以添加一个可选的 {% else %} 块,当循环从未进入时(要么因为迭代器为空,要么因为过滤条件从未匹配)会进入该块。
<h1>Users</h1>
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% else %}
<li>No users</li>
{% endfor %}
</ul>
在 for 循环块内部,可以使用一些可用变量:
- loop.index: 当前循环迭代次数(从 1 开始)
- loop.index0: 当前循环迭代次数(从 0 开始)
- loop.first: 是否是该循环的第一次迭代
- loop.last: 是否是该循环的最后一次迭代
<h1>Users</h1>
<ul>
{% for user in users %}
{% if loop.first %}
<li>First: {{user.name}}</li>
{% else %}
<li>User#{{loop.index}}: {{user.name}}</li>
{% endif %}
{% endfor %}
</ul>
If
if 语句本质上镜像了 Rust 的 [if` 表达式](https://doc.rust-lang.org/reference/expressions/if-expr.html#if-expressions),其用法如你所料:
{% if users.len() == 0 %}
No users
{% else if users.len() == 1 %}
1 user
{% elif users.len() == 2 %}
2 users
{% else %}
{{ users.len() }} users
{% endif %}
If Let
此外,也支持 if let 语句,同样镜像 Rust 的 if let 表达式:
{% if let Some(user) = user %}
{{ user.name }}
{% else %}
No user
{% endif %}
is (not) defined
你可以使用 is (not) defined 来确保变量存在(或不存在):
{% if x is defined %}
x is defined!
{% endif %}
{% if y is not defined %}
y is not defined
{% else %}
y is defined
{% endif %}
你可以将此功能与条件组合,甚至在表达式中使用:
{% if x is defined && x == "12" && y == Some(true) %}
...
{% endif %}
<script>
// It will generate `const x = true;` (or false is `x` is not defined).
const x = {{ x is defined }};
</script>
由于过程宏的限制,askama 只能看到当前类型的字段和模板中声明的变量。因此,你无法检查字段或函数是否已定义:
{% if x.y is defined %}
This code will not compile
{% endif %}
Match
为了以类型安全的方式处理 Rust 的 enum,模板从 0.6 版本开始支持 match 块。以下是一个简单的示例,展示如何展开 Option:
{% match item %}
{% when Some with ("foo") %}
Found literal foo
{% when Some with (val) %}
Found {{ val }}
{% when None %}
{% endmatch %}
也就是说,{% match %} 块可以包含空白字符(但不能包含其他字面量内容)和注释块,后跟若干个 {% when %} 块以及一个可选的 {% else %} 块。
与Rust类似,匹配是针对一个模式进行的。这样的模式可以是字面量,例如:
{% match multiple_choice_answer %}
{% when 3 %} Correct!
{% else %} Sorry, the right answer is "3".
{% endmatch %}
或者一些更复杂的类型,例如 Result<T, E>:
{% match result %}
{% when Ok(val) %} Good: {{ val }}.
{% when Err(err) %} Bad: {{ err }}.
{% endmatch %}
使用占位符 _ 来匹配任何值而不捕获数据也是可行的。通配符运算符 .. 用于匹配任意数量的项,其限制与 Rust 中相同,例如在切片或结构体中只能使用一次:
{% match list_of_ints %}
{% when [first, ..] %} The list starts with a {{ first }}
{% when _ %} The list is empty.
{% endmatch %}
{% else %} 节点是 {% when _ %} 的语法糖。如果使用,它必须放在最后,在所有其他 {% when %} 块之后:
{% match answer %}
{% when Ok(42) %} The answer is "42".
{% else %} No answer wrong answer?
{% endmatch %}
{% match %} 必须是穷尽的,即所有可能的输入都必须有一个分支。最简单的方法是提供一个 {% else %} 分支,如果不是所有可能值都需要单独处理的话。
{% match %} 块无法生成有效代码,必须至少提供一个 {% when %} 分支 和/或 一个 {% else %} 分支。
你也可以一次匹配多个备选模式:
{% match number %}
{% when 1 | 4 | 86 %} Some numbers
{% when n %} Number is {{ n }}
{% endmatch %}
为了与 linter 和自动格式化工具(如 djLint)更好地互操作,你也可以使用可选的 {% endwhen %} 节点来关闭 {% when %} 分支:
{% match number %}
{% when 0 | 2 | 4 | 6 | 8 %}
even
{% endwhen %}
{% when 1 | 3 | 5 | 7 | 9 %}
odd
{% endwhen %}
{% else %}
unknown
{% endmatch %}
变量引用与解引用
如果你需要将某个东西放在引用后面或解引用它,可以使用 & 和 * 运算符:
{% let x = &"bla" %}
{% if *x == "bla" %}
Just talking
{% else if x == &"another" %}
Another?!
{% endif %}
它们的效果与 Rust 中相同,并且你可以放置多个:
{% let x = &&"bla" %}
{% if *&**x == "bla" %}
You got it
{% endif %}
? 运算符
可以像在 Rust 中一样使用? 运算符 ,但仅适用于 Result 类型。
{{ some_result? }}
{% let value = some_result? %}
当运算符展开错误值时,模板渲染将失败,并返回一个包装了该错误的 askama::Error::Custom 错误。
请注意,此运算符目前仅适用于 Result 类型,不支持 Option 类型。
Include
include 语句允许你将大型或重复的块拆分为单独的模板文件。被包含的模板可以完全访问其使用处的上下文,包括循环中的局部变量:
{% for i in iter %}
{% include "item.html" %}
{% endfor %}
item.html file:
* Item: {{ i }}
include 路径必须是字符串字面量,以便在编译时被检测。Askama 会先尝试相对于包含模板的路径查找指定模板,如果查找不到再回退到绝对模板路径。在 if/else 块的分支中使用 include 可以实现更动态的包含。
表达式
Askama 支持字符串字面量("foo")和整数字面量(1)。它支持 Rust 支持的几乎所有二元运算符,包括算术、比较和逻辑运算符。解析器应用与 Rust 编译器相同的运算符优先级。表达式可以使用括号分组。
{{ 3 * 4 / 2 }}
{{ 26 / 2 % 7 }}
{{ 3 % 2 * 6 }}
{{ 1 * 2 + 4 }}
{{ 11 - 15 / 3 }}
{{ (4 + 5) % 3 }}
HTML 特殊字符 &、< 和 > 将被替换为其字符实体,除非模板的 escape 模式被禁用,或者使用了过滤器 | safe。
可以在作用域内的变量上调用方法,包括 self。
警告:如果表达式(一个 {{ }} 块)的结果等同于 self,则可能导致无限递归堆栈溢出。这是因为该表达式的 Display 实现会依次求值该表达式并再次产生 self。
位运算符的表达式
在 Askama 中,二进制 AND、OR 和 XOR 运算符(在 Rust 中分别称为 &、|、^)被重命名为 bitand、bitor、xor,以避免与过滤器表达式混淆。它们仍然具有与 Rust 中相同的运算符优先级。例如,要测试整数字段中最低有效位是否被设置:
{% if my_bitset bitand 1 != 0 %}
It is set!
{% endif %}
类型转换
你可以在 {{ … }} 表达式和 {% … %} 块中使用 as 运算符。它的工作方式与 Rust 中相同,但有一些故意限制:
- 你只能使用基本类型,如
i32或f64,作为源变量类型和目标类型。 - 如果源是基本类型的引用,例如
&&&bool,则 askama 会自动解引用该值,直到获得底层的bool。
字符串拼接
作为 {{ a }}{{ b }}{{ c }} 的简写,你可以使用连接运算符 ~:{{ a ~ b ~ c }}。波浪号 ~ 必须用空格包围,以避免与空白字符控制运算符混淆。
模板中的模板
使用表达式,可以将渲染模板的一部分委托给另一个模板。这使得可以将模块化模板部分注入到其他模板中,并有助于测试和复用。
#![allow(unused)]
fn main() {
use askama::Template;
#[derive(Template)]
#[template(source = "Section 1: {{ s1 }}", ext = "txt")]
struct RenderInPlace<'a> {
s1: SectionOne<'a>
}
#[derive(Template)]
#[template(source = "A={{ a }}\nB={{ b }}", ext = "txt")]
struct SectionOne<'a> {
a: &'a str,
b: &'a str,
}
let t = RenderInPlace { s1: SectionOne { a: "a", b: "b" } };
assert_eq!(t.render().unwrap(), "Section 1: A=a\nB=b")
}
请注意,如果你的内部模板(如 SectionOne)渲染 HTML 内容,那么你可能希望在将其注入外部模板时禁用转义,例如 {{ s1 | safe }}。否则它将字面渲染 HTML 内容,因为 askama 默认会转义 HTML 变量。
如果你不想在模板中每次调用时都为该模板类型禁用转义,你可以选择将模板本身标记为安全:
#![allow(unused)]
fn main() {
impl askama::filters::HtmlSafe for SectionOne<'_> {}
}
请参阅示例 就地渲染,该示例演示了在 for 块中使用模板向量。 See the example
注释
Askama 支持由 {# 和 #} 分隔的块注释。
{# A Comment #}
与 Rust 类似,Askama 也支持嵌套块注释。
{#
A Comment
{# A nested comment #}
#}
递归结构
递归实现最好使用自定义迭代器并使用普通循环。如果无法做到,则可以使用表达式直接调用 .render(),如下所示。
#![allow(unused)]
fn main() {
use askama::Template;
#[derive(Template)]
#[template(source = r#"
{{ name }} {
{% for item in children %}
{{ item.render()? }}
{% endfor %}
}
"#, ext = "html", escape = "none")]
struct Item<'a> {
name: &'a str,
children: &'a [Item<'a>],
}
}
宏
宏是 Jinja 中用于声明可重用代码片段的一种机制。宏可以声明一组必需和可选参数。此外,宏会继承调用点的变量作用域。定义和调用一个简单的宏如下所示:
{% macro heading(required_arg, optional_arg = "default subtitle") %}
<h1>{{required_arg}}</h1>
<h2>{{optional_arg}}</h2>
{{ variable_in_scope }}
{% endmacro %}
{# Variable scope that will be passed into macro invocations #}
{% set variable_in_scope = 5 %}
{# Invoke the macro by supplying all arguments #}
{{ heading("test", "good subtitle") }}
{# Invoke the macro by leaving out the optional argument `optional_arg` #}
{# This will use the default value `default subtitle` #}
{{ heading("test") }}
你可以为宏参数添加类型注解(也适用于默认值):
{%- macro test(value: Option<u32>, extra: Option<u32> = None) -%}
{% if let Some(value) = value -%}value is {{value}}{% endif -%}
{% if let Some(extra) = title -%}extra is {{extra}}{% endif -%}
{% endmacro -%}
可选地,{% endmacro %} 语句也可以包含宏的名称,对于上面的示例,它看起来像这样:
{% macro heading(required_arg, optional_arg = "default subtitle") %}
{# ... #}
{% endmacro heading %}
导入与作用域
为了拥有一个小型的可重用代码片段库,最好将宏声明在某个外部文件中。然后可以通过命名作用域将该文件 import 到你的模板中。将上面的宏声明移动到文件 macro.html 中,然后导入并调用它,如下所示:
{% import "macro.html" as scope %}
{{ scope::heading("test") }}
命名参数
除了按位置指定参数外,你还可以按名称传递参数。这允许以任意顺序传递参数:
{% macro heading(title, font_weight = "normal", font_size = 13) %}
<h1 style="font-weight: {{ font_weight }}; font-size: {{ font_size }};">
{{ title }}
</h1>
{% endmacro %}
{# 使用位置参数 #}
{{ heading("Super Heading", "bold", 13) }}
{# 使用命名参数 #}
{{ heading(title = "Super Heading", font_weight = "bold") }}
{{ heading(title = "Super Heading", font_weight = "bold", font_size = 23) }}
{{ heading(title = "Super Heading", font_size = 42, font_weight = "bold") }}
两种调用宏的方式甚至可以混合使用,但可选参数必须始终放在最后(在所有位置指定的参数之后):
{{ heading("Super Heading", font_weight = "bold", font_size = 26) }}
{{ heading("Super Heading", font_size = 26, font_weight = "bold") }}
{{ heading("Super Heading", "bold", font_size = 26) }}
另请注意,如果命名参数引用的是原本会用于非命名参数的参数,则会导致错误:
{% macro heading(arg1, arg2, arg3, arg4) %}
{% endmacro %}
{{ heading("something", "b", arg4 = "ah", arg2 = "title") }}
这里无效,因为 arg2 是第二个参数,会与 "b" 冲突。所以要么将 "b" 替换为 arg3="b",要么将 "title" 放在前面:
{{ heading("something", arg3 = "b", arg4 = "ah", arg2 = "title") }}
{# Equivalent of: #}
{{ heading("something", "title", "b", arg4 = "ah") }}
宏调用块
还有第二种调用宏的方式,即使用 call 块语法。这种语法允许你的调用有一个“body“。在宏内部,会定义一个名为 caller 的特殊变量,它的行为类似于一个函数,可以在任何位置插入给定的体:
{% macro centered() %}
<center>
{# 在这里插入"body"的内容: #}
{{ caller() }}
<center>
{% endmacro %}
{# 这个宏必须使用 call 块语法,因为它期望一个body: #}
{% call centered() %}
This text will be centered
{% endcall %}
使用上面显示的调用表达式语法({{ centered() }})调用这个宏将会失败,因为宏期望存在变量 caller(),但只有 call 块调用会定义它。
然而,你可以以一种允许同时支持有“body“和无“body“调用的方式声明宏:
{% macro render_dialog(title, class="dialog") -%}
<div class="{{ class }}">
<h2>{{ title }}</h2>
<div class="contents">
{% if caller is defined %}
{{ caller() }}
{% else %}
Empty dialog without content
{% endif %}
</div>
</div>
{%- endmacro %}
{# 无"body"调用:不会定义 #}
{{ render_dialog("Empty Dialog") }}
{# 有"body"调用: #}
{% call render_dialog("Nice Dialog") %}
This is a simple dialog rendered by using a macro and
a call block.
{% endcall %}
你也可以在变量声明中使用 caller:
{%- macro test() -%}
{%- set content = caller() -%}
-> `{{content}}` <-
{%~ endmacro -%}
{% call test() %}bla{% endcall -%}
在这种情况下,它将显示:
-> `bla` <-
宏调用块参数
caller 之所以是函数而不是变量,是有原因的。它允许传递参数以及从宏中多次调用它!下面这个示例宏,它使调用 caller() 方法并传入参数 user。要调用这个宏,call 块本身必须声明参数:
{% macro dump_users(users) -%}
<ul>
{%- for user in users %}
<li><p>{{ user.username }}</p>{{ caller(user) }}</li>
{%- endfor %}
</ul>
{%- endmacro %}
{# 这个 callblock 为 `caller` 声明了参数 `user`: #}
{% call(user) dump_users(list_of_users) %}
<dl>
<dt>Realname</dt>
<dd>{{ user.realname }}</dd>
<dt>Description</dt>
<dd>{{ user.description }}</dd>
</dl>
{% endcall %}
嵌套带内容的宏
在某些抽象层次上,声明一个有body的宏,但将该body传递给另一个宏调用。
宏调用会立即用自身的 caller 变量覆盖原有的 caller。为了能够访问外部的 caller 变量,需要使用一个小技巧:
{% macro container() %}
<div class="container">
{{ caller() }}
</div>
{% endmacro %}
{% macro outer_container() %}
{# 为 `caller` 创建一个别名,以便在 container 内部访问它: #}
{% set outer_caller = caller %}
<div class="outer-container">
{# 嵌套宏调用 - 会覆盖 `caller` 变量: #}
{% call container() %}
{{ outer_caller() }}
{% endcall %}
</div>
{% endmacro %}
调用Rust宏
可以直接在模板中调用 Rust 宏:
{% let s = format!("{}", 12) %}
需要注意的重点,与表达式的其余部分相反,Askama 无法知道传递给宏的标记是变量还是其他内容,因此它总是默认“原样”生成。所以如果有如下代码:
#![allow(unused)]
fn main() {
macro_rules! test_macro{
($entity:expr) => {
println!("{:?}", &$entity);
}
}
#[derive(Template)]
#[template(source = "{{ test_macro!(entity) }}", ext = "txt")]
struct TestTemplate<'a> {
entity: &'a str,
}
}
它将无法编译,告诉你它不知道 entity。它没有像通常那样推断 entity 是当前类型的字段。你可以通过将字段的值绑定到一个变量来绕过这个限制:
{% let entity = entity %}
{{ test_macro!(entity) }}
Opt-in features
Some features in askama are opt-in to reduce the amount of dependencies, and to keep the compilation time low.
To opt-in to a feature, you can use features = […].
E.g. if you want to use the filter |json,
you have to opt-in to the feature "serde_json":
[dependencies]
askama = { version = "0.14.0", features = ["serde_json"] }
Please read the Cargo manual for more information.
Default features
Any semver-compatible upgrade
(e.g. askama = "0.14.1" to askama = "0.14.2") will keep the same list of default features.
We will treat upgrades to a newer dependency version as a semver breaking change.
"default"
You can opt-out of using the feature flags by using
default-features = false:
[dependencies]
askama = { version = "0.14.0", default-features = false }
Without default-features = false, i.e with default features enabled,
the following features are automatically selected for you:
default = ["config", "derive", "std", "urlencode"]
This should encompass most features an average user of askama might need.
If you are writing a library that depends on askama, and if you want it to be usable in by other users and in other projects, then you should probably opt-out of features you do not need.
"derive"
enabled by "default"
This feature enables #[derive(Template)]. Without it the trait askama::Template will still be
available, but if you want to derive a template, you have to manually depend on askama_macros.
askama_macros should be used with the same features as askama.
Not using this feature might be useful e.g. if you are writing a library with manual filters
for askama, without any templates. It might also very slightly speed-up the compilation,
because more dependencies can be compiled in parallel, because askama won’t transitively depend
on e.g. syn or proc-macro2. On the author’s PC the compilation of a trivial hello-world example
was about 0.2s faster without the feature when compiled in release mode.
If you are writing a library that uses askama, consider not using this default-feature.
"config"
enabled by "default"
Enables compile time configurations.
"urlencode"
enabled by "default"
Enables the filters |urlencode and |urlencode_strict.
Addition features
Please note that we reserve the right to add more features to the current list, without labeling it as a semver breaking change. The newly added features might even depend on a newer rustc version than the previous list.
The most useful catch-all feature for a quick start might be "full",
which enables all implemented features, i.e.:
full = ["default", "code-in-doc", "serde_json"]
In production or once your project is “maturing” you might want to manually opt-in to any needed
features with a finer granularity instead of depending on "full".
"serde_json"
enabled by "full"
This feature depends on the crate serde_json.
We won’t treat upgrades to a newer serde_json version as a semver breaking change,
even if it raises the MSRV.
Enables the filter |json.
"code-in-doc"
enabled by "full"
This feature depends on the crate pulldown-cmark.
We won’t treat upgrades to a newer pulldown-cmark version as a semver breaking change,
even if it raises the MSRV.
Enables using documentations as template code.
“Anti-features” in a #![no_std] environment
Opting-out of the default features "std" and "alloc" is only interesting for the use
in a #![no_std] environment.
Please find more information in The Embedded Rust Book.
"alloc"
enabled by "default"
Without the default feature "alloc" askama can be used in a #![no_std] environment.
The method Template::render() will be absent, because askama won’t have access to a default allocator.
Many filters need intermediate allocations, and won’t be usable without this feature.
You can still render templates using e.g.
no_std_io2::io::Cursor or
embedded_io::Write
"std"
enabled by "default"
Without the feature "std" askama can be used in a #![no_std] environment.
The method Template::write_into() will be absent, because askama won’t have access to standard IO operations.
Enabling "std" enables "alloc", too.
Filters
Values such as those obtained from variables can be post-processed
using filters.
Filters are applied to values using the pipe symbol (|) and may
have optional extra arguments in parentheses.
Filters can be chained, in which case the output from one filter
is passed to the next.
{{ "HELLO" | lower }}
Askama has a collection of built-in filters, documented below, but can also include custom filters.
Additionally, the json filter is included in the built-in filters, but is disabled by default.
Enable it with Cargo features (see below for more information).
Built-In Filters
Built-in filters that take (optional) arguments can be called with named arguments, too. The order of the named arguments does not matter, but named arguments must come after positional (i.e. unnamed) arguments.
E.g. the filter pluralize takes two optional arguments: singular and plural
which are singular = "" and plural = "s" by default.
If you are fine with the default empty string for the singular, and you only want to set a
specific plural, then you can call the filter like dog{{ count | pluralize(plural = "gies") }}.
assigned_or
{{ variable_or_expression | assigned_or(fallback) }}
If the variable on the left-hand side is in its “default” state, e.g. an empty "" string,
a 0 integer, a None optional, or an Err(_) result, then the fallback value is printed.
Otherwise the value.
{% let greeting = Some("Hello") %}
{{ greeting.as_ref() | assigned_or("Hi") }}
Hello
If the value is an identifier, then it is first tested of the variable name is defined.
See also [|defined_or][#defined_or].
capitalize
enabled by"alloc"
enabled by"default"
{{ text_to_capitalize | capitalize }}
Capitalize a value. The first character will be uppercase, all others lowercase:
{{ "hello" | capitalize }}
Output:
Hello
center
enabled by"alloc"
enabled by"default"
{{ text_to_center | center(length) }}
Centers the value in a field of a given width:
-{{ "a" | center(5) }}-
Output:
- a -
default
{{ variable_or_expression | default(default_value) }}
{{ variable_or_expression | default(default_value, [[boolean =] true]) }}
This filter works like the Jinja filter of the same name.
If the second argument is not a boolean true, then the filter behaves like [|defined_or][#defined_or].
If it is supplied and true, then the filter behaves like [|assigned_or][#assigned_or].
This filter exists for compatibility with Jinja.
Askama provides [|defined_or][#defined_or] and [|assigned_or][#assigned_or] which both
better express the intention and should generally be used instead of this filter.
defined_or
{{ variable | defined_or(fallback) }}
The left-hand side of the filter must be an identifier.
Return a fallback value if the identifier is undefined:
{% let greeting = "Hello" %}
{{ greeting | defined_or("Hi") }}
Since the variable greeting is defined, the output is its value: Hello.
If you remove the variable, then the output is the default value: Hi:
{{ greeting | defined_or("Hi") }}
See also [|assigned_or][#assigned_or].
deref
{{ expression | deref }}
Dereferences the given argument.
{% let s = String::from("a") | ref %}
{% if s | deref == String::from("b") %}
{% endif %}
will become:
#![allow(unused)]
fn main() {
let s = &String::from("a");
if *s == String::from("b") {}
}
escape | e
{{ text_to_escape | e }}
{{ text_to_escape | escape }}
{{ text_to_escape | escape(escaper) }}
Escapes HTML characters in strings:
{{ "Escape <>&" | e }}
Output:
Escape <>&
Optionally, it is possible to specify and override which escaper is used.
Consider a template where the escaper is configured as escape = "none".
However, somewhere escaping using the HTML escaper is desired.
Then it is possible to override and use the HTML escaper like this:
{{ "Don't Escape <>&" | escape }}
{{ "Don't Escape <>&" | e }}
{{ "Escape <>&" | escape("html") }}
{{ "Escape <>&" | e("html") }}
Output:
Don't Escape <>&
Don't Escape <>&
Escape <>&
Escape <>&
filesizeformat
{{ number_of_bytes | filesizeformat }}
Returns adequate string representation (in KB, ..) of number of bytes:
{{ 1024 | filesizeformat }}
Output:
1.02 KB
Control the resulting precision with the optional precision argument:
{{ 1024 | filesizeformat(precision = 3) }}
Output:
1.024 KB
fmt
enabled by"alloc"
enabled by"default"
{{ expression | fmt("format_string") }}
Formats arguments according to the specified format
The second argument to this filter must be a string literal (as in normal
Rust). The two arguments are passed through to format!() by
the Askama code generator, but the order is swapped to support filter
composition.
{{ value | fmt("{:?}") }}
As an example, this allows filters to be composed like the following.
Which is not possible using the format filter.
{{ value | capitalize | fmt("{:?}") }}
format
enabled by"alloc"
enabled by"default"
{{ "format_string" | format([variables ...]) }}
Formats arguments according to the specified format.
The first argument to this filter must be a string literal (as in normal Rust).
All arguments are passed through to format!() by the Askama code generator.
{{ "{:?}" | format(var) }}
indent
{{ text_to_indent | indent(width, [first], [blank]) }}
Indent newlines with width spaces.
{{ "hello\nfoo\nbar" | indent(4) }}
Output:
hello
foo
bar
The first argument can also be a string that will be used to indent lines.
The first line and blank lines are not indented by default.
The filter has two optional [bool] arguments first and blank, that can be set to true
to indent the first and blank lines, resp.:
{{ "hello\n\nbar" | indent("$ ", true, true) }}
Output:
$ hello
$
$ bar
join
{{ iterable | join(separator) }}
Joins iterable into a string separated by provided argument.
#![allow(unused)]
fn main() {
array = &["foo", "bar", "bazz"]
}
{{ array | join(", ") }}
Output:
foo, bar, bazz
linebreaks
{{ text_to_break | linebreaks }}
Replaces line breaks in plain text with appropriate HTML.
A single newline becomes an HTML line break <br> and a new line followed by a blank line becomes a paragraph break <p>.
{{ "hello\nworld\n\nfrom\naskama" | linebreaks }}
Output:
<p>hello<br />world</p><p>from<br />askama</p>
linebreaksbr
{{ text_to_break | linebreaksbr }}
Converts all newlines in a piece of plain text to HTML line breaks.
{{ "hello\nworld\n\nfrom\naskama" | linebreaks }}
Output:
hello<br />world<br /><br />from<br />askama
paragraphbreaks
{{ text_to_break | paragraphbreaks }}
A new line followed by a blank line becomes <p>, but, unlike linebreaks, single new lines are ignored and no <br/> tags are generated.
Consecutive double line breaks will be reduced down to a single paragraph break.
This is useful in contexts where changing single line breaks to line break tags would interfere with other HTML elements, such as lists and nested <div> tags.
{{ "hello\nworld\n\nfrom\n\n\n\naskama" | paragraphbreaks }}
Output:
<p>hello\nworld</p><p>from</p><p>askama</p>
lower | lowercase
enabled by"alloc"
enabled by"default"
{{ text_to_convert | lower }}
{{ text_to_convert | lowercase }}
Converts to lowercase.
{{ "HELLO" | lower }}
Output:
hello
pluralize
{{ integer | pluralize }}
{{ integer | pluralize([singular = ""], [plural = "s"]) }}
Select a singular or plural version of a word, depending on the input value.
If the value of self.count is +1 or -1, then “cat” is returned, otherwise “cats”:
cat{{ count | pluralize }}
You can override the default empty singular suffix, e.g. to spell “doggo” for a single dog:
dog{{ count | pluralize("go") }}
If the word cannot be declined by simply adding a suffix, then you can also override singular and the plural, too:
{{ count | pluralize("mouse", "mice") }}
More complex languages that know multiple plurals might be impossible to implement with this filter, though.
ref
{{ expression | ref }}
Creates a reference to the given argument.
{{ "a" | ref }}
{{ self.x | ref }}
will become:
#![allow(unused)]
fn main() {
&"a"
&self.x
}
reject
This filter filters out values matching the given value/filter.
With this data:
#![allow(unused)]
fn main() {
vec![1, 2, 3, 1]
}
And this template:
{% for elem in data|reject(1) %}{{ elem }},{% endfor %}
Output will be:
2,3,
For more control over the filtering, you can use a callback instead. Declare a function:
fn is_odd(value: &&u32) -> bool {
**value % 2 != 0
}
Then you can pass the path to the is_odd function:
{% for elem in data|reject(crate::is_odd) %}{{ elem }},{% endfor %}
Output will be:
2,
safe
{{ expression | safe }}
Marks a string (or other Display type) as safe. By default all strings are escaped according to the format.
{{ "<p>I'm Safe</p>" | safe }}
Output:
<p>I'm Safe</p>
title | titlecase
enabled by"alloc"
enabled by"default"
{{ text_to_convert | title }}
{{ text_to_convert | titlecase }}
Return a title cased version of the value. Words will start with uppercase letters, all remaining characters are lowercase.
{{ "hello WORLD" | title }}
Output:
Hello World
trim
enabled by"alloc"
enabled by"default"
{{ text_to_trim | trim }}
Strip leading and trailing whitespace.
{{ " hello " | trim }}
Output:
hello
truncate
{{ text_to_truncate | truncate(length) }}
Limit string length, appends ‘…’ if truncated.
{{ "hello" | truncate(2) }}
Output:
he...
unique
Returns an iterator with all duplicates removed.
This filter is only available with the std feature enabled.
With this data:
#![allow(unused)]
fn main() {
vec!["a", "b", "a", "c"]
}
And this template:
{% for elem in data|unique %}{{ elem }},{% endfor %}
Output will be:
a,b,c,
upper | uppercase
enabled by"alloc"
enabled by"default"
{{ text_to_convert | upper }}
{{ text_to_convert | uppercase }}
Converts to uppercase.
{{ "hello" | upper }}
Output:
HELLO
urlencode | urlencode_strict
enabled by"urlencode"
enabled by"default"
{{ text_to_escape | urlencode }}
{{ text_to_escape | urlencode_strict }}
Percent encodes the string. Replaces reserved characters with the % escape character followed by a byte value as two hexadecimal digits.
{{ "hello?world" | urlencode }}
Output:
hello%3Fworld
With |urlencode all characters except ASCII letters, digits, and _.-~/ are escaped.
With |urlencode_strict a forward slash / is escaped, too.
wordcount
{{ text_with_words | wordcount }}
Count the words in that string.
{{ "askama is sort of cool" | wordcount }}
Output:
5
Optional / feature gated filters
The following filters can be enabled by requesting the respective feature in the Cargo.toml dependencies section, e.g.
[dependencies]
askama = { version = "0.12", features = ["serde_json"] }
json | tojson
enabled by "serde_json"
{{ value_to_serialize | json }}
{{ value_to_serialize | json(indent) }}
Enabling the serde_json feature will enable the use of the json filter.
This will output formatted JSON for any value that implements the required
Serialize trait.
The generated string does not contain ampersands &, chevrons < >, or apostrophes '.
To use it in a <script> you can combine it with the safe filter.
In HTML attributes, you can either use it in quotation marks "{{data | json}}" as is,
or in apostrophes with the (optional) safe filter '{{data | json | safe}}'.
In HTML texts the output of e.g. <pre>{{data | json | safe}}</pre> is safe, too.
Good: <li data-extra="{{data | json}}">…</li>
Good: <li data-extra='{{data | json | safe}}'>…</li>
Good: <pre>{{data | json | safe}}</pre>
Good: <script>var data = {{data | json | safe}};</script>
Bad: <li data-extra="{{data | json | safe}}">…</li>
Bad: <script>var data = {{data | json}};</script>
Bad: <script>var data = "{{data | json | safe}}";</script>
Ugly: <script>var data = "{{data | json}}";</script>
Ugly: <script>var data = '{{data | json | safe}}';</script>
By default, a compact representation of the data is generated, i.e. no whitespaces are generated between individual values. To generate a readable representation, you can either pass an integer how many spaces to use as indentation, or you can pass a string that gets used as prefix:
Prefix with four spaces:
<textarea>{{data | tojson(4)}}</textarea>
Prefix with two characters:
<p>{{data | tojson("\u{a0}\u{a0}")}}</p>
Custom Filters
To define your own filters, either have a module named filters in scope of the context of your
#[derive(Template]) struct, and define the filters as functions within this module;
or call the filter with a path, e.g. {{ value | some_module::my_filter }}. Alternatively, you can also place your custom filter functions in a crate called filters added as dependency to your askama project.
The expressions {{ value | my_filter }} and {{ value | filters::my_filter }} behave identically,
unless “my_filter” happens to be a built-in filter.
Note that built-in filters take precedence, so your custom filters will always be shadowed by built-in filters (if they have the same name). To avoid this, call your custom filters with a full path.
Anatomy of a custom filter function
#![allow(unused)]
fn main() {
#[askama::filter_fn]
pub fn example_filter1(
// Value that's piped into the filter within the jinja template.
// This can be of any type. `impl Display` is just an example.
value: impl Display,
// This is askama's runtime values environment. Together with
// values, these two arguments are always passed into a custom filter.
env: &dyn askama::Values
) -> askama::Result<String> {
Ok(format!("{value} | example_filter1"))
}
}
The basic anatomy of a filter function must always look like this:
- The first argument is the value your custom filter function is applied to during a filter invocation. In this template expression:
{{ 1337 | example_filter1 }}, the value1337will be passed in as first argument to your filter function. - The second argument is always of type
values: &dyn askama::Values, storing askama’s runtime values environment. - Custom filter functions’ return type must be
askama::Result<T>, whereTof the last filter invocation of a filter chain mustimpl Display. In this exemplary filter chain:{{ 1337 | multiply | final_filter }}, themultiplyfilter may returnaskama::Result<MyCustomNonDisplayableStruct>, butfinal_filtermust returnaskama::Result<T>withT: Display, otherwise your template will fail to compile.
Additionally to this basic structure, your custom filter function can also have:
An arbitrary amount of required arguments:
#![allow(unused)]
fn main() {
#[askama::filter_fn]
pub fn example_filter2<T: ToString>(
value: impl Display, env: &dyn askama::Values,
// Custom arguments that need to be specified when calling your filter
required0: impl Display,
required1: T,
required2: usize
) -> askama::Result<String> { /* ... */ }
}
An arbitrary amount of optional arguments (must be located after required arguments in your function signature):
#![allow(unused)]
fn main() {
#[askama::filter_fn]
pub fn example_filter3(
value: impl Display, // filter input
env: &dyn askama::Values, // askama runtime values environment
required0: impl Display,
// Custom arguments that may optionally be specified when calling your filter
#[optional(None)] optional0: Option<&str>,
#[optional(Some("I am the default value"))] optional1: Option<&str>,
#[optional("I am the default value")] optional2: &str,
) -> askama::Result<String> { /* ... */ }
}
Calling custom filters
Thanks to the askama::filter_fn macro, invocations to your custom filter functions can also use named arguments - though currently providing diagnostic compile error messages that are a lot less readable compared to builtin filters, when you’re using them wrong in your templates. All of these are valid invocations of the filter functions above:
{{ 1337 | example_filter2("value0", "value1", 2) }}
{{ "1337" | example_filter2(required0 = "req0", required1 = "req1", required2 = 2) }}
{{ 1337 | example_filter3("req0") }}
{{ 1337 | example_filter3("req0", None) }}
{{ "1337" | example_filter3("req0", None, None) }}
{{ "1337" | example_filter3("req0", None, Some("opt1"), "opt2") }}
{{ "1337" | example_filter3("req0", optional2 = "opt2") }}
{{ "1337" | example_filter3(required0 = "req0", optional2 = "opt2") }}
Reference Value Passing Issues
Due to the nature of askama’s generated code, you will often encounter a mix of by-value and various degrees of references to your variables in the jinja template. If you declare your custom filter input value as one concrete type, like &str, you will often be confronted with the problem of having to (de)-reference your input variables: {{ value | filter }}, {{ *value | filter }}, {{ **value | filter }}, …
To avoid this, try to declare your filter’s input argument using trait bounds, which are also implemented for references. For example, instead of:
#![allow(unused)]
fn main() {
#[askama::filter_fn]
pub fn example_filter4(value: &str, env: &dyn askama::Values) -> askama::Result<String> {}
}
instead specify value with a trait bound of Display:
#![allow(unused)]
fn main() {
#[askama::filter_fn]
pub fn example_filter4(value: impl Display, env: &dyn askama::Values) -> askama::Result<String> {}
}
as Display is implemented for &str as well as for &&str, &&&str, ….
This cleans up your invocation sites, as no dereferencing (and no additional compile roundtripping) is required.
Examples
Implementing a filter that replaces all instances of "oo" for "aa".
use askama::Template;
#[derive(Template)]
#[template(source = "{{ s | myfilter }}", ext = "txt")]
struct MyFilterTemplate<'a> {
s: &'a str,
}
// Any filter defined in the module `filters` is accessible in your template.
mod filters {
// This filter does not have extra arguments
#[askama::filter_fn]
pub fn myfilter<T: std::fmt::Display>(
value: T,
_env: &dyn askama::Values,
) -> askama::Result<String> {
let s = s.to_string();
Ok(s.replace("oo", "aa"))
}
}
fn main() {
let t = MyFilterTemplate { s: "foo" };
assert_eq!(t.render().unwrap(), "faa");
}
Implementing a filter that replaces all instances of "oo" for n times "a".
use askama::Template;
#[derive(Template)]
#[template(source = "{{ s | myfilter(4) }}", ext = "txt")]
struct MyFilterTemplate<'a> {
s: &'a str,
}
// Any filter defined in the module `filters` is accessible in your template.
mod filters {
// This filter requires a `usize` input when called in templates
#[askama::filter_fn]
pub fn myfilter<T: std::fmt::Display>(
s: T,
_env: &dyn askama::Values,
n: usize,
) -> askama::Result<String> {
let s = s.to_string();
let mut replace = String::with_capacity(n);
replace.extend((0..n).map(|_| "a"));
Ok(s.replace("oo", &replace))
}
}
fn main() {
let t = MyFilterTemplate { s: "foo" };
assert_eq!(t.render().unwrap(), "faaaa");
}
Runtime values
It is possible to access runtime values in custom filters:
#![allow(unused)]
fn main() {
// This example contains a custom filter `|cased`.
// Depending on the runtime value `"case"`, the input is either turned
// into lower case, upper case, or left alone, if the runtime value is undefined.
use std::any::Any;
use askama::{Template, Values};
mod filters {
use super::*;
#[askama::filter_fn]
pub fn cased(value: impl ToString, values: &dyn Values) -> askama::Result<String> {
let value = value.to_string();
let case = askama::get_value(values, "case").ok();
Ok(match case {
Some(Case::Lower) => value.to_lowercase(),
Some(Case::Upper) => value.to_uppercase(),
None => value,
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum Case {
Lower,
Upper,
}
#[test]
fn test_runtime_values_in_custom_filters() {
#[derive(Template)]
#[template(ext = "txt", source = "Hello, {{ user | cased }}!")]
struct MyStruct<'a> {
user: &'a str,
}
// The filter source ("wOrLd") should be written in lower case.
let values: (&str, &dyn Any) = ("case", &Case::Lower);
assert_eq!(
MyStruct { user: "wOrLd" }
.render_with_values(&values)
.unwrap(),
"Hello, world!"
);
// The filter source ("wOrLd") should be written in upper case.
let values: (&str, &dyn Any) = ("case", &Case::Upper);
assert_eq!(
MyStruct { user: "wOrLd" }
.render_with_values(&values)
.unwrap(),
"Hello, WORLD!"
);
// The filter source ("wOrLd") should be written as is.
assert_eq!(
MyStruct { user: "wOrLd" }.render().unwrap(),
"Hello, wOrLd!"
);
}
}
HTML-safe types
Askama will try to avoid escaping types that generate string representations that do not contain
“HTML-unsafe characters”.
HTML-safe characters are characters that can be used in any context in HTML texts and attributes.
The “unsafe” characters are: <, >, &, " and '.
In order to know which types do not need to be escaped, askama has the marker trait
askama::filters::HtmlSafe, and any type that implements that trait won’t get automatically
escaped in a {{expr}} expression.
By default e.g. all primitive integer types are marked as HTML-safe.
You can also mark your custom type MyStruct as HTML-safe using:
#![allow(unused)]
fn main() {
impl askama::filters::HtmlSafe for MyStruct {}
}
This automatically marks references &MyStruct as HTML-safe, too.
Safe output of custom filters
Say, you have a custom filter | strip that removes all HTML-unsafe characters:
#![allow(unused)]
fn main() {
fn strip(s: impl ToString) -> Result<String, askama::Error> {
Ok(s.to_string()
.chars()
.filter(|c| !matches!(c, '<' | '>' | '&' | '"' | '\''))
.collect()
)
}
}
Then you can also mark the output as safe using askama::filters::Safe:
#![allow(unused)]
fn main() {
fn strip(s: impl ToString) -> Result<Safe<String>, askama::Error> {
Ok(Safe(...))
}
}
There also is askama::filters::MaybeSafe that can be used to mark some output as safe,
if you know that some inputs for our filter will always result in a safe output:
#![allow(unused)]
fn main() {
fn as_sign(i: i32) -> Result<MaybeSafe<&'static str>, askama::Error> {
match i.into() {
i if i < 0 => Ok(MaybeSafe::NeedsEscaping("<0")),
i if i > 0 => Ok(MaybeSafe::NeedsEscaping(">0")),
_ => Ok(MaybeSafe::Safe("=0")),
}
}
}
Working with web-frameworks
Askama’s Template::render() returns Result<String, askama::Error>.
To make this result work in your preferred web-framework, you’ll need to handle both cases:
converting the String to a web-response with the correct Content-Type,
and the Error case to a proper error message.
While in many cases it will be enough to simply convert the Error to
Box<dyn std::error::Error + Send + Sync>usingerr.into_box()orstd::io::Errorusingerr.into_io_error()
it is recommended to use a custom error type. This way you can display the error message in your app’s layout, and you are better prepared for the likely case that your app grows in the future. Maybe you’ll need to access a database and handle errors? Maybe you’ll add multiple languages and you want to localize error messages?
The crates thiserror and displaydoc can be useful to implement this error type.
Simplified alternative
Alternatively, you can use #[derive(askama_web::WebTemplate)]
to automatically implement e.g. actix-web’s Responder, axum’s IntoResponse or warp’s Reply.
The library implements traits for all web-frameworks mentioned down below (and then some),
but it does not stylize error messages.
If you don’t need custom / stylized error messages,
e.g. because you know that your templates won’t have rendering errors, then using
askama_web might work for you, too.
Actix-Web
Install using the following command to enable the actix-web-4 feature for this framework
(check the available features for other versions):
cargo add askama_web --features "actix-web-4"
See the actix-web example web-app.
To convert the String to an HTML response, you can use
Html::new(_).
#![allow(unused)]
fn main() {
use actix_web::web::Html;
use actix_web::{Responder, handler};
#[handler]
fn handler() -> Result<impl Responder, AppError> {
…
Ok(Html::new(template.render()?))
}
}
To implement your own error type, you can use this boilerplate code:
#![allow(unused)]
fn main() {
use actix_web::{HttpResponse, Responder};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Html;
use askama::Template;
#[derive(Debug, displaydoc::Display, thiserror::Error)]
enum AppError {
/// could not render template
Render(#[from] askama::Error),
}
impl ResponseError for AppError {
fn status_code(&self) -> StatusCode {
match &self {
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl Responder for AppError {
type Body = String;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
#[derive(Debug, Template)]
#[template(path = "error.html")]
struct Tmpl { … }
let tmpl = Tmpl { … };
if let Ok(body) = tmpl.render() {
(Html::new(body), self.status_code()).respond_to(req)
} else {
(String::new(), self.status_code()).respond_to(req)
}
}
}
}
Axum
Install using the following command to enable the axum-0.8 feature for this framework
(check the available features for other versions):
cargo add askama_web --features "axum-0.8"
To convert the String to an HTML response, you can use
Html(_).
#![allow(unused)]
fn main() {
use axum::response::{Html, IntoResponse};
async fn handler() -> Result<impl IntoResponse, AppError> {
…
Ok(Html(template.render()?))
}
}
To implement your own error type, you can use this boilerplate code:
#![allow(unused)]
fn main() {
use axum::response::IntoResponse;
use askama::Template;
#[derive(Debug, displaydoc::Display, thiserror::Error)]
enum AppError {
/// could not render template
Render(#[from] askama::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
#[derive(Debug, Template)]
#[template(path = "error.html")]
struct Tmpl { … }
let status = match &self {
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let tmpl = Tmpl { … };
if let Ok(body) = tmpl.render() {
(status, Html(body)).into_response()
} else {
(status, "Something went wrong").into_response()
}
}
}
}
Poem
Install using the following command to enable the poem-3 feature for this framework
(check the available features for other versions):
cargo add askama_web --features "poem-3"
To convert the String to an HTML response, you can use
Html(_).
#![allow(unused)]
fn main() {
use poem::web::Html;
use poem::{IntoResponse, handler};
#[handler]
async fn handler() -> Result<impl IntoResponse, AppError> {
…
Ok(Html(template.render()?))
}
}
To implement your own error type, you can use this boilerplate code:
#![allow(unused)]
fn main() {
use poem::error::ResponseError;
use poem::http::StatusCode;
use poem::web::Html;
use poem::{IntoResponse, Response};
use askama::Template;
#[derive(Debug, displaydoc::Display, thiserror::Error)]
enum AppError {
/// could not render template
Render(#[from] askama::Error),
}
impl ResponseError for AppError {
fn status(&self) -> StatusCode {
match &self {
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
#[derive(Debug, Template)]
#[template(path = "error.html")]
struct Tmpl { … }
let tmpl = Tmpl { … };
if let Ok(body) = tmpl.render() {
(self.status(), Html(body)).into_response()
} else {
(self.status(), "Something went wrong").into_response()
}
}
}
}
Rocket
Install using the following command to enable the rocket-0.5 feature for this framework
(check the available features for other versions):
cargo add askama_web --features "rocket-0.5"
See the rocket example web-app.
To convert the String to an HTML response, you can use
RawHtml(_).
#![allow(unused)]
fn main() {
use rocket::get;
use rocket::response::content::RawHtml;
use rocket::response::Responder;
#[get(…)]
fn handler<'r>() -> Result<impl Responder<'r, 'static>, AppError> {
…
Ok(RawHtml(template.render()?))
}
}
To implement your own error type, you can use this boilerplate code:
#![allow(unused)]
fn main() {
use askama::Template;
use rocket::http::Status;
use rocket::response::content::RawHtml;
use rocket::response::Responder;
use rocket::{Request, Response};
#[derive(Debug, displaydoc::Display, thiserror::Error)]
enum AppError {
/// could not render template
Render(#[from] askama::Error),
}
impl<'r> Responder<'r, 'static> for AppError {
fn respond_to(
self,
request: &'r Request<'_>,
) -> Result<Response<'static>, Status> {
#[derive(Debug, Template)]
#[template(path = "error.html")]
struct Tmpl { … }
let status = match &self {
AppError::Render(_) => Status::InternalServerError,
};
let template = Tmpl { … };
if let Ok(body) = template.render() {
(status, RawHtml(body)).respond_to(request)
} else {
(status, "Something went wrong").respond_to(request)
}
}
}
}
Warp
Install using the following command to enable the wrap-0.4 feature for this framework
(check the available features for other versions):
cargo add askama_web --features "wrap-0.4"
To convert the String to an HTML response, you can use
html(_).
#![allow(unused)]
fn main() {
use warp::reply::{Reply, html};
fn handler() -> Result<impl Reply, AppError> {
…
Ok(html(template.render()?))
}
}
To implement your own error type, you can use this boilerplate code:
#![allow(unused)]
fn main() {
use http::StatusCode;
use warp::reply::{Reply, Response, html};
#[derive(Debug, displaydoc::Display, thiserror::Error)]
enum AppError {
/// could not render template
Render(#[from] askama::Error),
}
impl Reply for AppError {
fn into_response(self) -> Response {
#[derive(Debug, Template)]
#[template(path = "error.html")]
struct Tmpl { … }
let status = match &self {
AppError::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let template = Tmpl { … };
if let Ok(body) = template.render() {
with_status(html(body), status).into_response()
} else {
status.into_response()
}
}
}
}
Performance
Rendering Performance
When rendering an askama template, you should prefer the methods
.render()(to render the content into a new string),.render_into()(to render the content into anfmt::Writeobject, e.g.String) or.write_into()(to render the content into anio::Writeobject, e.g.Vec<u8>)
over .to_string() or format!().
While .to_string() and format!() give you the same result, they generally perform much worse
than askama’s own methods, because fmt::Write uses dynamic methods calls instead of
monomorphised code. On average, expect .to_string() to be 100% to 200% slower than .render().
Faster Rendering of Custom Types
Every type that implements fmt::Display can be used in askama expressions: {{ value }}.
Rendering with fmt::Display can be slow, though, because it uses dynamic methods calls in its
fmt::Formatter argument. To speed up rendering (by a lot, actually),
askama adds the trait FastWritable. For any custom type you want to render,
it has to implement fmt::Display, but if it also implements FastWritable,
then – using autoref-based specialization – the latter implementation is automatically preferred.
To reduce the amount of code duplication, you can let your fmt::Display implementation call
your FastWritable implementation:
#![allow(unused)]
fn main() {
use std::fmt::{self, Write};
use askama::{FastWritable, NO_VALUES};
// In a real application, please have a look at
// https://github.com/kdeldycke/awesome-falsehood/blob/690a070/readme.md#human-identity
struct Name<'a> {
forename: &'a str,
surname: &'a str,
}
impl fmt::Display for Name<'_> {
// Because the method simply forwards the call, it should be `inline`.
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// `fmt::Write` has no access to runtime values,
// so simply pass `NO_VALUES`.
self.write_into(f, NO_VALUES)?;
Ok(())
}
}
impl FastWritable for Name<'_> {
fn write_into(
&self,
dest: &mut dyn fmt::Write,
_values: &dyn askama::Values,
) -> askama::Result<()> {
dest.write_str(self.surname)?;
dest.write_str(", ")?;
dest.write_str(self.forename)?;
Ok(())
}
}
#[test]
fn both_implementations_should_render_the_same_text() {
let person = Name {
forename: "Max",
surname: "Mustermann",
};
let mut buf_fmt = String::new();
write!(buf_fmt, "{person}").unwrap();
let mut buf_fast = String::new();
person.write_into(&mut buf_fast, NO_VALUES).unwrap();
assert_eq!(buf_fmt, buf_fast);
assert_eq!(buf_fmt, "Mustermann, Max");
}
}
Slow Debug Recompilations
If you experience slow compile times when iterating with lots of templates, you can compile Askama’s derive macros with a higher optimization level. This can speed up recompilation times dramatically.
Add the following to Cargo.toml or .cargo/config.toml:
#![allow(unused)]
fn main() {
[profile.dev.package.askama_derive]
opt-level = 3
}
This may affect clean compile times in debug mode, but incremental compiles will be faster.
With a nightly compiler, you can additionally try using the parallel frontend by
adding the following lines to your .cargo/config.toml:
[build]
rustflags = ["-Z", "threads=16"]
This will mostly show noticeable improvements when you have a single large crate with many askama templates.
Profile-Guided Optimization (PGO)
To optimize askama’s performance, you can compile your application with Profile-Guided Optimization. According to the tests, PGO can improve the library performance by 15%.
Upgrading to new versions
This file only lists breaking changes you need to be aware of when you upgrade to a new askama version. Please see our release notes to get a list of all changes and improvements that might be useful to you.
From askama v0.15 to askama v0.16
-
Variable creation without value cannot be done with
let/setanymore. You need to use the newdecl/declareinstead:{% decl variable_without_value %} -
Duplicated blocks will now error and prevent compilation instead of emitting a warning.
From askama v0.14 to askama v0.15
-
The MSRV of this release is 1.88.
-
Filter functions now need to have the
filter_fnproc-macro used on them. So if you had:#![allow(unused)] fn main() { pub fn to_string(s: impl ToString, _: &dyn Values) -> Result<String> { Ok(s.to_string()) } }It now becomes:
#![allow(unused)] fn main() { #[askama::filter_fn] pub fn to_string(s: impl ToString, _: &dyn Values) -> Result<String> { Ok(s.to_string()) } } -
Variables declared in the templates cannot be named
calleranymore.
From askama v0.13 to askama v0.14
-
The MSRV of this release is 1.83.
-
When assigning a new variable (
{% let var = … %}) with a local variable was value, it is moved or copied, not referenced. -
Try expressions (
{{ expr? }}) are not placed behind a reference. -
FastWritableimplementations have access to runtime values. -
Custom filters have access to runtime values, and must add a second
&dyn askama::Valuesargument as in the example. -
|uniqueis a built-in filter;|titlecaseis an alias for|title.
From askama v0.12 to askama v0.13
A blog post summarizing changes and also explaining the merge of rinja and askama is
available here.
List of breaking changes:
-
The MSRV of this release is 1.81.
-
The integration crates were removed. Instead of depending on e.g.
askama_axum/askama_axum, please usetemplate.render()to render to aResult<String, askama::Error>.Use e.g.
.map_err(|err| err.into_io_error())?if your web-framework expectsstd::io::Errors, orerr.into_box()if it expectsBox<dyn std::error::Error + Send + Sync>.Please read the documentation of your web-framework how to turn a
Stringinto a web-response. -
The fields
Template::EXTENSIONandTemplate::MIME_TYPEwere removed. -
You may not give variables a name starting with
__askama, or the name of a rust keyword. -
#[derive(Template)]cannot be used withunions. -
|linebreaks,|linebreaksbrand|paragraphbreaksescape their input automatically. -
|jsondoes not prettify its output by default anymore. Use e.g.|json(2)for readable output. -
The binary operators
|,&and^are now calledbitor,bitandandxor, resp. -
The feature
"humansize"was removed. The filter|humansizeis always available. -
The feature
"serde-json"is now called"serde_json". -
The feature
"markdown"was removed. Usecomrakdirectly. -
The feature
"serde-yaml"was removed. Use e.g.yaml-rust2directly.
From rinja v0.3 to askama v0.13
-
The MSRV of this release is 1.81.
-
The projects rinja and askama were re-unified into one project. You need to replace instances of
rinjawithaskama, e.g.-use rinja::Template; +use askama::Template;[dependencies] -rinja = "0.3.5" +askama = "0.13.0" -
The integration crates were removed. Instead of depending on e.g.
rinja_axum/askama_axum, please usetemplate.render()to render to aResult<String, askama::Error>.Use e.g.
.map_err(|err| err.into_io_error())?if your web-framework expectsstd::io::Errors, orerr.into_box()if it expectsBox<dyn std::error::Error + Send + Sync>.Please read the documentation of your web-framework how to turn a
Stringinto a web-response. -
The fields
Template::EXTENSIONandTemplate::MIME_TYPEwere removed. -
The feature
"humansize"was removed. The filter|humansizeis always available. -
You may not give variables a name starting with
__rinja, or the name of a rust keyword. -
#[derive(Template)]cannot be used withunions.
From rinja v0.2 to rinja v0.3
- You should be able to upgrade to v0.3 without changes.
From askama v0.12 to rinja v0.2
Have a look at our blog posts that highlight some of the best features of our releases, and give you more in-dept explanations: docs.rs switching jinja template framework from tera to rinja.
-
The MSRV of this release is 1.71.
-
You need to replace instances of
askamawithrinja, e.g.-use askama::Template; +use rinja::Template;[dependencies] -askama = "0.12.1" +rinja = "0.2.0" -
|linebreaks,|linebreaksbrand|paragraphbreaksescape their input automatically. -
|jsondoes not prettify its output by default anymore. Use e.g.|json(2)for readable output. -
The binary operators
|,&and^are now calledbitor,bitandandxor, resp. -
Filter
as_refis now justref -
The feature
"serde-json"is now called"serde_json". -
The feature
"markdown"was removed. Usecomrakdirectly. -
The feature
"serde-yaml"was removed. Use e.g.yaml-rust2directly.
From askama v0.11 to askama v0.12
-
The magic
_parentfield to access&**selfwas removed. -
Integration implementations do not need an
extargument anymore. -
The
ironintegration was removed.