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 專屬的編譯器功能。未記錄 MSRV。
1. 設定
Cargo.toml
[dependencies]
maud = "0.27"
2. 渲染 API
html! { ... } 會回傳一個 Markup 值。Markup 是 PreEscaped<String> 的型別別名,是已跳脫的 HTML 字串的包裝。
| 操作 | 結果 | 備註 |
|---|---|---|
html! { ... } | Markup | 巨集表達式的值。 |
markup.into_string() | String | 消耗 Markup,回傳建構好的字串。 |
(DOCTYPE) 在 html! 內部 | 產生 <!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 特質的 render_to(請參閱第 9 節)。
3. 網頁框架整合
從下表啟用對應的特性標誌,Markup 就會實作對應的特質。然後處理程序就可以直接回傳 Markup。
| 框架 | 特性標誌 | Markup 實作的特質 | 相依性釘選 |
|---|---|---|---|
| 預設 | — | — | 未啟用任何項目 |
| 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>> 用於 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 從推斷為 tide::Result<impl Into<Response>> 的閉包回傳 Ok(html! { ... })(其特性新增了 From<PreEscaped<String>>,而非 IntoResponse,因此裸的 -> Markup 處理程序無法編譯)。Axum、Poem、Submillisecond 從同步或非同步處理程序直接回傳 Markup。特質實作會將其轉換為每個框架預期的回應型別。
手動路徑(無特性標誌)
呼叫 .into_string() 並自行包裝字串。
// Rouille 沒有特質實作:
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!" }
}
- 多個類別可串連:
.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." }
}
}
開關 [條件]
一個括號包圍的布林值表達式,用於開啟或關閉布林值屬性或類別。
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。在 .{ } 內部,從字面值加上拼接來建立類別。
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. 控制結構
每個控制關鍵字都帶有 @ 前綴。裸的 if / for / match / let 在 html! 內部不會被識別為控制流程。每個分支都必須使用大括號。
@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 繫結。根據解析器原始碼,match 分支接受完整的 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 特質
撰寫一個 maud::Render 實作,來教導 maud 如何拼接你自己的型別。該特質有兩個方法,都有互相呼叫的預設實作:
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。當你想要直接寫入輸出緩衝區時(例如附加多個片段而無需中間的 Markup),請覆寫 render_to。
render_to 的覆寫負責自身的跳脫。原始寫入 buffer 不會為你跳脫。在內部需要這個功能嗎?請使用 maud::Escaper,它會在寫入時進行跳脫。
render/render_to 僅是同步的。沒有非同步的 Render 特質或串流變體。在進入 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 特質(它在 0.8.0 到 0.15.0 之間有過一個,已於 2017-01-26 移除);不要將這個名稱與 horrorshow 仍然存在的 RenderOnce 特質混淆。
10. 陷阱與潛在問題
每個都在其小節中標示;第 11 節會匯總。索引:
- 空元素使用
;而非自閉合;控制流程需要@(每個分支都必須使用大括號);第 4 節、第 7 節 [cond](開關)與attr=[Option<T>](僅在Some時才有值);屬性串接需要{ };第 5 節、第 6 節- 預設會跳脫拼接;
PreEscaped對不受信任的輸入是 XSS 漏洞;僅跳脫&<>"(單引號不會);第 6 節、第 8 節 - Rust 2021
#間距(input #pinkie;);第 5 節 - 兩個方法都未覆寫的
Render會永久遞迴;render_to覆寫會進行自身的跳脫(maud::Escaper);第 9 節 render/render_to僅是同步的;沒有非同步的Render特質,沒有串流變體;在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" nested {
…
[為翻譯而截斷]