Maud(Rust HTML 宏)
捕获于 2026-07-10(maud 0.27.0)。如需更新:请重新对照 maud.lambda.xyz + docs.rs/maud 进行验证,然后对比差异。
Maud 是一个编译时 HTML 宏(html!);标记代码内联在 Rust 中,编译时进行类型检查。此技能面向 maud 0.27.x(0.27.0,发布于 2025-02-02)。
Maud 可在 稳定版 Rust 和 nightly 上运行(nightly 提供更好的错误信息)。自 0.22.1(2020-11-02)起支持稳定版 Rust;早期版本需要 nightly 专有的编译器特性。未记录最低支持的 Rust 版本(MSRV)。
1. 设置
Cargo.toml
[dependencies]
maud = "0.27"
2. 渲染 API
html! { ... } 返回一个 Markup 值。Markup 是 PreEscaped<String> 的类型别名,它是一个包装器,用于存储一个已经转义过的 HTML 字符串。
| 操作 | 结果 | 说明 |
|---|---|---|
html! { ... } | Markup | 宏表达式的值。 |
markup.into_string() | String | 消耗 Markup,返回构建好的字符串。 |
html! 内部的 (DOCTYPE) | 输出 <!DOCTYPE html> | maud::DOCTYPE 是一个常量,等于字符串字面量 <!DOCTYPE html>。 |
use maud::{html, DOCTYPE};
let page = html! {
(DOCTYPE)
html {
head { title { "My site" } }
body { p { "Hello" } }
}
};
渲染 Display 值:maud::display(x) 是一个自由函数,它会在模板内部通过其 Display 实现来渲染任意值。它替代了 0.24.0 中移除的旧版 Render for Display 实现。
use maud::{html, display};
let n = 42;
html! { p { (display(n)) } };
注意:Markup/PreEscaped 没有 Display 实现(已对照 docs.rs 0.27 检查过;它实现了 Render,而不是 Display)。当需要原始字符串时,使用 .into_string() 转换为 String。没有 html_to! 或写入缓冲区的宏(html_debug! 已在 0.25.0 中移除)。要向现有缓冲区写入,请使用 Render trait 的 render_to(见第 9 节)。
3. Web 框架集成
启用下表中对应的特性标志,Markup 就会实现相应的 trait。这样,处理程序就可以直接返回 Markup。
| 框架 | 特性标志 | Markup 实现的 trait | 依赖固定版本 |
|---|---|---|---|
| 默认 | — | — | 无任何启用 |
| Actix-web | actix-web | actix_web::Responder | actix-web-dep + futures-util |
| Axum | axum | IntoResponse | axum-core ^0.5 + http ^1 |
| Rocket | rocket | Rocket 的 Responder | rocket ^0.5 |
| Warp | warp | warp::Reply | warp ^0.3.6 |
| Poem | poem | poem::IntoResponse | poem ^3 |
| Tide | tide | From<PreEscaped<String>> for tide::Response | tide ^0.16.0 |
| Submillisecond | submillisecond(0.27.0 新增) | IntoResponse | submillisecond ^0.4.1 |
| Rouille | 手动,见下文 | 无 | — |
依赖版本固定来自 docs.rs 特性图,单一来源。如果精确范围很重要,请对照 Cargo.toml 验证。
maud = { version = "0.27", features = ["axum"] }
处理程序示例
Rocket:
#[get("/<name>")]
fn hello(name: &str) -> Markup {
html! { h1 { "Hello, " (name) "!" } }
}
Actix-web 和 Tide 处理程序将返回值包装在 Result 中:Actix async fn index() -> AwResult<Markup> { Ok(html! { ... }) };Tide 从闭包中返回 Ok(html! { ... }),该闭包被推断为 tide::Result<impl Into<Response>>(其特性添加了 From<PreEscaped<String>>,而不是 IntoResponse,因此返回裸 -> Markup 的处理程序无法编译)。Axum、Poem、Submillisecond 可以直接从同步或异步处理程序返回 Markup。trait 实现会将其转换为各自期望的响应类型。
手动路径(无特性标志)
调用 .into_string() 并手动包装字符串。
// Rouille 没有 trait 实现:
Response::html(html! { h1 { "Hello, " (name) "!" } })
4. 元素与嵌套
包含内容的元素使用大括号;文本和嵌套元素放在里面。
空元素以 ; 结尾,不带主体。Maud 输出 HTML5 语法(<br>),而不是 XHTML(<br />)。
html! {
link rel="stylesheet" href="poetry.css";
p {
"Rock, you are a rock."
br;
"Gray, you are gray,"
}
}
支持带连字符的元素和属性(自定义元素、data-*、ARIA)。
“编译时类型检查”仅指 Rust 拼接表达式的类型。Maud 不会根据 HTML5 规范验证元素或属性名称。它也不会检查内容模型嵌套(<div> 在 <p> 内部也能正常编译)。任何有效的 Rust 标识符都可以编译为标签,包括拼写错误。
文本
普通双引号 Rust 字符串字面量在 html! 内部是裸表达式。原始字符串字面量可处理长文本或特殊字符文本。
html! {
pre {
r#"
Rocks, these are my rocks.
Smooth and round,
Asleep in the ground.
"#
}
}
5. 属性
值属性:name="value"。
html! {
a href="about:blank" { "Apple Bloom" }
li class="lower-middle" { "Sweetie Belle" }
}
布尔值/空属性:只有名称,没有 =。
html! {
input type="checkbox" name="cupcakes" checked;
label for="cupcakes" { "Do you like cupcakes?" }
}
#id / .class 简写
html! {
input #cannon .big.scary.bright-red type="button" value="Launch";
div."col-sm-2" { "Bootstrap column!" }
}
- 多个 class 链式连接:
.big.scary.bright-red。 - 带引号的名称允许使用不能作为裸标识符的字符:
."col-sm-2"。 - Rust 2021 空格陷阱:
#前面必须有一个空格(input #pinkie;)。旧版允许input#pinkie;。
隐式 div
当存在 #id / .class 简写时,省略标签名称将默认元素设为 div。
html! {
#main {
"Main content!"
.tip { "Storing food in a refrigerator can make it 20% cooler." }
}
}
切换 [condition]
一个用方括号括起来的布尔表达式,用于打开或关闭布尔属性或 class。
let allow_editing = true;
html! {
p contenteditable[allow_editing] { "Edit me." }
}
let cuteness = 95;
html! {
p.cute[cuteness > 50] { "Squee!" }
}
可选属性值 attr=[Option<T>]
当使用 = 时,方括号内的 Option 仅在 Some 时设置属性的值。None 则完全省略该属性。这与裸 [cond] 不同,后者切换属性的存在性。
html! {
p title=[Some("Good password")] { "Correct horse" }
@let value = Some(42);
input value=[value];
@let title: Option<&str> = None;
p title=[title] { "Battery staple" } // title 被省略
}
动态名称
使用 #(...) 拼接计算出的 id。使用 .{ } 从字面量加拼接构建 class。
let name = "rarity";
let severity = "critical";
html! {
aside #(name) {
p.{ "color-" (severity) } { "This is the worst! Possible! Thing!" }
}
}
6. 拼接与插值
(expr) 在标记中插入运行时值。默认情况下,值中的 HTML 特殊字符会被转义。
let best_pony = "Pinkie Pie";
let numbers = [1, 2, 3, 4];
html! {
p { "Hi, " (best_pony) "!" }
p {
"I have " (numbers.len()) " numbers, "
"and the first one is " (numbers[0])
}
}
拼接任何实现了 maud::Render 的类型。大多数基本类型(str、i32 等)已经实现了。
块表达式拼接用于任意 Rust 逻辑:
html! {
p {
({
let f: Foo = something_convertible_to_foo()?;
f.time().format("%H%Mh")
})
}
}
属性中的拼接
使用 = 的单个值:
let secret_message = "Surprise!";
html! {
p title=(secret_message) { "Nothing to see here." }
}
连接字面量和拼接需要使用 { } 包装:
const GITHUB: &'static str = "https://github.com";
html! {
a href={ (GITHUB) "/lambda-fairy/maud" } { "Fork me on GitHub" }
}
7. 控制结构
每个控制关键字都以 @ 为前缀。在 html! 内部,裸的 if / for / match / let 不会被识别为控制流。每个分支都必须使用大括号。
@if / @else if / @else
#[derive(PartialEq)]
enum Princess { Celestia, Luna, Cadance, TwilightSparkle }
let user = Princess::Celestia;
html! {
@if user == Princess::Luna {
h1 { "Super secret woona to-do list" }
ul { li { "Evil laugh" } }
} @else if user == Princess::Celestia {
p { "Sister, please stop reading my diary." }
} @else {
p { "Nothing to see here; move along." }
}
}
@if let 同样有效(条件是一个普通的 Rust 表达式,因此 let PAT = EXPR 可以原生解析):
let user = Some("Pinkie Pie");
html! {
p {
"Hello, "
@if let Some(name) = user { (name) } @else { "stranger" }
"!"
}
}
@for
let names = ["Applejack", "Rarity", "Fluttershy"];
html! {
ol {
@for name in &names {
li { (name) }
}
}
}
@while
存在于解析器 AST(WhileExpr)中,但未出现在已发布的书中。没有文档化的示例。其结构与 @if 匹配,因此预计 @while cond { ... } 以及类似的 @while let PAT = EXPR { ... } 可以工作。在编译之前,请将 @while let 视为未经验证的。
@match
html! {
@match user {
Princess::Luna => h1 { "Woona's here" },
Princess::Celestia | Princess::Cadance => p { "A princess." },
_ => p { "Nothing to see here; move along." }
}
}
复用上面 @if 中的 Princess 枚举和 user 绑定。根据解析器源代码,匹配分支接受完整的 Rust 模式:或模式(A | B,如上所示)、绑定、范围、解构、守卫(pat if cond =>)。书中只展示了基本分支,因此请通过编译验证守卫/或模式分支。
@let
在 html! 内部声明一个变量,在循环中最有用。接受任何 Rust let 模式,包括类型注解。
let names = ["Applejack", "Rarity", "Fluttershy"];
html! {
@for name in &names {
@let first_letter = name.chars().next().unwrap();
p {
"The first letter of " b { (name) } " is " b { (first_letter) } "."
}
}
}
8. 转义与原始 HTML
拼接和文本默认会被转义。Maud 恰好转义四个字符:&、<、>、"。单引号直接通过,不转义。
使用 maud::PreEscaped 跳过转义,它会逐字呈现其内部值。
use maud::PreEscaped;
html! {
"<script>alert(\"XSS\")</script>" // <script>...
(PreEscaped("<script>alert(\"XSS\")</script>")) // <script>...
}
只包装你已清理过的内容。在不可信输入上使用 PreEscaped 是一个 XSS 漏洞。PreEscaped 上的 AsRef<str> 绑定限制已在 0.26.0 中移除,因此现在它可以包装更广泛的内置类型。
9. 自定义渲染与组合
Render Trait
编写一个 maud::Render 实现,以教导 Maud 如何拼接你自己的类型。该 trait 有两个方法,它们都有默认实现,互相调用:
pub trait Render {
fn render(&self) -> Markup {
let mut buffer = String::new();
self.render_to(&mut buffer);
PreEscaped(buffer)
}
fn render_to(&self, buffer: &mut String) {
buffer.push_str(&self.render().into_string());
}
}
至少覆盖其中一个。如果两个都不覆盖,会导致无限递归(每个默认实现都调用另一个)。对于简单情况,覆盖 render。当你想直接写入输出缓冲区时,覆盖 render_to,例如,无需中间 Markup 即可追加多个片段。
一个 render_to 覆盖需要自行处理转义。直接写入 buffer 的内容不会自动转义。如果你需要在 render_to 内部进行转义,请使用 maud::Escaper,它会在写入时进行转义。
render/render_to 仅支持同步。没有异步的 Render trait 或流式变体。在进入 html! 之前,将异步数据(数据库或 HTTP 调用)解析为一个值,然后拼接该解析后的值。
组件就是函数
一个局部或组件就是一个返回 Markup 的普通函数。通过拼接调用进行组合。
use maud::{html, Markup, DOCTYPE};
fn header(title: &str) -> Markup {
html! {
head { title { (title) } }
}
}
fn page(title: &str, body: Markup) -> Markup {
html! {
(DOCTYPE)
html {
(header(title))
body { (body) }
}
}
}
// 使用示例
let markup = page("Home", html! { h1 { "Hello" } });
拼接一个 Markup 值永远不会重新转义它,因此嵌套组件是安全的。Maud 目前没有导出一个 RenderOnce trait(它在 0.8.0 到 0.15.0 之间有过,于 2017-01-26 移除);不要将它与 horrorshow 仍然存在的 RenderOnce trait 混淆。
10. 陷阱与注意事项
每个都在各自的章节中内联标记;第 11 节进行了汇总。索引:
- 空元素使用
;而非自闭合;控制流需要@(每个分支都需要大括号);§4,§7 [cond](切换) vsattr=[Option<T>](仅在Some时设置值);属性连接需要{ };§5,§6- 拼接默认转义;
PreEscaped在不可信输入上是 XSS 漏洞;仅转义&<>"(单引号不转义);§6,§8 - Rust 2021 中的
#间距(input #pinkie;);§5 - 如果
Render的任一方法未覆盖,会导致无限递归;render_to覆盖需要自行处理转义(maud::Escaper);§9 render/render_to仅支持同步;没有异步Rendertrait,没有流式变体;在html!之前解析异步值(§9)Markup没有Display实现(使用.into_string()/maud::display(x));没有模板文件,没有html_to!;§2- “类型检查”仅涵盖 Rust 拼接类型;没有 HTML5 标签/属性验证,没有内容模型嵌套检查(§4)
11. 快速参考卡
值: html! { ... } -> Markup 获取字符串:.into_string()
文档类型: (DOCTYPE) -> <!DOCTYPE html>
元素: p { "text" 嵌套 { ... } }
[后续内容已截断,因篇幅限制]