Community寫作與編輯github.com

uwuclxdy/agenticat

我的一些技能和代理,相容於多種 AI 編碼工具

agenticat 是什麼?

這個技能提供對 Maud 的完整支援,Maud 是一個 Rust 的編譯時期 HTML 巨集(`html!`),讓標記語言能與 Rust 程式碼內聯,並在編譯時進行型別檢查。它主要針對 Maud 0.27.x 版本,並詳細說明如何在 Cargo.toml 中加入依賴、使用渲染 API 產生 `Markup` 值、與多種網頁框架(如 Actix-web、Axum、Rocket 等)整合,以及透過特徵標誌簡化處理程序。技能也涵蓋元素與巢狀結構、屬性設定(包括 `#id` 與 `.class` 簡寫、開關 `[條件]` 與選用屬性值 `attr=[Option<T>]`)、插入與內插表示式、控制結構(`@if`、`@for`、`@match`、`@let`)、跳脫與原始 HTML 處理、自訂渲染(`Render` 特質)以及元件組合。此外,它還列出常見陷阱與快速參考,適合使用 Claude Code、Cursor、Codex 等 AI 編碼工具的 Rust 開發者。

相容平台~Claude Code~Codex CLI~Cursor✓Antigravity
npx skills add uwuclxdy/agenticat

Installed? Explore more 寫作與編輯 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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-webactix-webactix_web::Responderactix-web-dep + futures-util
AxumaxumIntoResponseaxum-core ^0.5 + http ^1
RocketrocketRocket 的 Responderrocket ^0.5
Warpwarpwarp::Replywarp ^0.3.6
Poempoempoem::IntoResponsepoem ^3
TidetideFrom<PreEscaped<String>> 用於 tide::Responsetide ^0.16.0
Submillisecondsubmillisecond(0.27.0 新增)IntoResponsesubmillisecond ^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>"                // &lt;script&gt;...
    (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 {

…

[為翻譯而截斷]

相關技能