Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

模板语法

语法概览

语法描述
{{ ... }}要求值的表达式,会被转义并输出
{{ ... | ... }}带过滤器的表达式
{% 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>

在上面的示例中,hrefclass 属性之间保留了一个空白字符。

还有第三种可能。如果你希望抑制所有空白字符,但保留一个("minimize"),可以使用 ~:

{% if something ~%}
Hello
{%~ endif %}

需要注意的是,如果被修剪的字符中包含换行符,那么最终保留下来的唯一字符将是一个换行符。

空白字符控制也可以通过配置文档或在派生宏中定义。这些定义的优先级遵循从全局到局部的顺序:

  1. Inline (-, +, ~)
  2. Derive (#[template(whitespace = "suppress")])
  3. Configuration (in askama.toml, whitespace = "preserve")

两个内联空白控制可能指向同一个空白范围。在这种情况下,它们按以下优先级解析:

  1. Suppress (-)
  2. Minimize (~)
  3. 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 内容时对变量进行转义。它根据模板文件名的扩展名推断转义上下文,如果扩展名是 htmlhtmxml,则默认转义。当在属性中将模板指定为 source 时,必须使用 ext 属性参数来指定类型。此外,你也可以通过设置 escape 属性参数值(为 nonehtml)来显式指定模板的转义模式。

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(),
        "&#x2f;&#x2f; my &lt;html&gt; is &quot;unsafe&quot; &amp; \
         should be &#x27;escaped&#x27;"
    );
}

控制结构

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 中分别称为 &|^)被重命名为 bitandbitorxor,以避免与过滤器表达式混淆。它们仍然具有与 Rust 中相同的运算符优先级。例如,要测试整数字段中最低有效位是否被设置:

{% if my_bitset bitand 1 != 0 %}
    It is set!
{% endif %}

类型转换

你可以在 {{ … }} 表达式和 {% … %} 块中使用 as 运算符。它的工作方式与 Rust 中相同,但有一些故意限制:

  • 你只能使用基本类型,如 i32f64,作为源变量类型和目标类型。
  • 如果源是基本类型的引用,例如 &&&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) }}