Community写作与编辑github.com

uwuclxdy/agenticat

适用于多种 AI 编码工具的部分技能与代理

agenticat 是什么?

这是一个针对 Maud(Rust HTML 宏库)的 AI 代理技能,旨在帮助开发者高效使用 Maud 0.27.x 版本。Maud 是一个编译时 HTML 宏(`html!`),允许在 Rust 代码中内联编写标记,并在编译时进行类型检查。该技能详细涵盖了 Maud 的设置、渲染 API、与多个 Web 框架(如 Actix-web、Axum、Rocket 等)的集成、元素和嵌套、属性、控制结构(如 `@if`、`@for`、`@match` 和 `@let`)、转义和原始 HTML、自定义渲染和组件化编程,以及各种陷阱和注意事项。它支持在稳定版 Rust 和 nightly 版本上运行,并兼容 Cursor、Claude Code 和 Codex 等 AI 编码工具。通过明确的表格、代码示例和快速参考卡片,该技能使开发者能够快速上手 Maud,避免常见错误,并利用其类型安全特性高效生成 HTML。

兼容平台~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 专有的编译器特性。未记录最低支持的 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-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>> for 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 从闭包中返回 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>"                // &lt;script&gt;...
    (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](切换) vs 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 trait,没有流式变体;在 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" 嵌套 { ... } }

[后续内容已截断,因篇幅限制]

相关技能