agentsclimarketplace

Maud

Skill uwuclxdy/agenticat/skills/maud

Some of my Agents & Skills, compatible with most AI coding tools

Install
npx -y skills add uwuclxdy/agenticat --skill maud

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Maud Rust `html!` macro reference (0.27).

SKILL.md

15.2 KB, as published. Nobody here has run it

Maud (Rust HTML Macro)

Captured 2026-07-10 (maud 0.27.0). To update: re-verify against maud.lambda.xyz + docs.rs/maud, then diff for changes.

Maud is a compile-time HTML macro (html!); markup lives inline in Rust, type-checked at compile time. This skill targets maud 0.27.x (0.27.0, released 2025-02-02).

Maud runs on stable Rust and nightly (better error messages on nightly). Stable-Rust-capable since 0.22.1 (2020-11-02); earlier it needed nightly-only compiler features. MSRV is not documented.


1. Setup

Cargo.toml

[dependencies]
maud = "0.27"

2. Rendering API

html! { ... } returns a Markup value. Markup is a type alias for PreEscaped<String>, a wrapper around an already-escaped HTML string.

OperationResultNotes
html! { ... }MarkupThe macro expression's value.
markup.into_string()StringConsumes the Markup, hands back the built string.
(DOCTYPE) inside html!emits <!DOCTYPE html>maud::DOCTYPE is a constant equal to the literal string <!DOCTYPE html>.
use maud::{html, DOCTYPE};

let page = html! {
    (DOCTYPE)
    html {
        head { title { "My site" } }
        body { p { "Hello" } }
    }
};

Rendering a Display value: maud::display(x) is a free function that renders any value through its Display impl inside a template. It replaced the old blanket Render for Display impl removed in 0.24.0.

use maud::{html, display};

let n = 42;
html! { p { (display(n)) } };

Note: Markup/PreEscaped has no Display impl (checked against docs.rs 0.27; it implements Render, not Display). Convert to String with .into_string() when you need the raw string. There is no html_to! / write-into-buffer macro (html_debug! was removed in 0.25.0). To write into an existing buffer, use the Render trait's render_to (see §9).


3. Web Framework Integration

Turn on the matching feature flag from the table below and Markup implements the corresponding trait. A handler can then just return Markup directly.

FrameworkFeature flagTrait Markup implementsDep pins
defaultnothing enabled
Actix-webactix-webactix_web::Responderactix-web-dep + futures-util
AxumaxumIntoResponseaxum-core ^0.5 + http ^1
RocketrocketRocket's Responderrocket ^0.5
Warpwarpwarp::Replywarp ^0.3.6
Poempoempoem::IntoResponsepoem ^3
TidetideFrom<PreEscaped<String>> for tide::Responsetide ^0.16.0
Submillisecondsubmillisecond (new in 0.27.0)IntoResponsesubmillisecond ^0.4.1
Rouillemanual, see belownone

Dependency version pins came from the docs.rs feature graph, single source. Verify against Cargo.toml if exact ranges matter.

maud = { version = "0.27", features = ["axum"] }

Handler Example

Rocket:

#[get("/<name>")]
fn hello(name: &str) -> Markup {
    html! { h1 { "Hello, " (name) "!" } }
}

Actix-web and Tide handlers wrap the return in a Result: Actix async fn index() -> AwResult<Markup> { Ok(html! { ... }) }; Tide returns Ok(html! { ... }) from a closure inferred as tide::Result<impl Into<Response>> (its feature adds From<PreEscaped<String>>, not IntoResponse, so a bare -> Markup handler won't compile). Axum, Poem, Submillisecond return Markup directly from a sync or async handler. The trait impl converts it to the response type each one expects.

Manual Path (No Feature Flag)

Call .into_string() and wrap the string yourself.

// Rouille has no trait impl:
Response::html(html! { h1 { "Hello, " (name) "!" } })

4. Elements and Nesting

Elements with content use braces; text and nested elements go inside.

Void elements end with ; and take no body. Maud emits HTML5 syntax (<br>), not XHTML (<br />).

html! {
    link rel="stylesheet" href="poetry.css";
    p {
        "Rock, you are a rock."
        br;
        "Gray, you are gray,"
    }
}

Elements and attributes with hyphens are supported (custom elements, data-*, ARIA).

"Type-checked at compile time" means Rust splice-expression types only. Maud does not validate element or attribute names against the HTML5 spec. It does not check content-model nesting either (a <div> inside a <p> compiles fine). Any valid Rust ident compiles as a tag, typos included.

Text

Plain double-quoted Rust string literals are bare expressions inside html!. Raw string literals handle long or special-character text.

html! {
    pre {
        r#"
            Rocks, these are my rocks.
            Smooth and round,
            Asleep in the ground.
        "#
    }
}

5. Attributes

Value attribute: name="value".

html! {
    a href="about:blank" { "Apple Bloom" }
    li class="lower-middle" { "Sweetie Belle" }
}

Boolean / empty attribute: bare name, no =.

html! {
    input type="checkbox" name="cupcakes" checked;
    label for="cupcakes" { "Do you like cupcakes?" }
}

#id / .class Shorthand

html! {
    input #cannon .big.scary.bright-red type="button" value="Launch";
    div."col-sm-2" { "Bootstrap column!" }
}
  • Multiple classes chain: .big.scary.bright-red.
  • Quoted names allow characters not valid as bare idents: ."col-sm-2".
  • Rust 2021 whitespace gotcha: # must be preceded by a space (input #pinkie;). Older editions allowed input#pinkie;.

Implicit div

Omitting the tag name when a #id / .class shorthand is present defaults the element to div.

html! {
    #main {
        "Main content!"
        .tip { "Storing food in a refrigerator can make it 20% cooler." }
    }
}

Toggles [condition]

A bracketed bool expression toggles a boolean attribute or a class on or off.

let allow_editing = true;
html! {
    p contenteditable[allow_editing] { "Edit me." }
}

let cuteness = 95;
html! {
    p.cute[cuteness > 50] { "Squee!" }
}

Optional Attribute Values attr=[Option<T>]

With =, a bracketed Option sets the attribute's value only when Some. None omits the attribute entirely. This differs from bare [cond], which toggles presence.

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 omitted
}

Dynamic Names

Splice a computed id with #(...). Build a class from a literal plus splice inside .{ }.

let name = "rarity";
let severity = "critical";
html! {
    aside #(name) {
        p.{ "color-" (severity) } { "This is the worst! Possible! Thing!" }
    }
}

6. Splices and Interpolation

(expr) inserts a runtime value into markup. HTML special characters in the value are escaped by default.

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])
    }
}

Splice any type that has a maud::Render impl. Most primitives (str, i32, ...) already do.

Block-expression splice for arbitrary Rust logic:

html! {
    p {
        ({
            let f: Foo = something_convertible_to_foo()?;
            f.time().format("%H%Mh")
        })
    }
}

Splices in Attributes

Single value with =:

let secret_message = "Surprise!";
html! {
    p title=(secret_message) { "Nothing to see here." }
}

Concatenating literal plus splice needs { } wrapping:

const GITHUB: &'static str = "https://github.com";
html! {
    a href={ (GITHUB) "/lambda-fairy/maud" } { "Fork me on GitHub" }
}

7. Control Structures

Every control keyword takes an @ prefix. Bare if / for / match / let are not recognized as control flow inside html!. Braces are mandatory on every branch.

@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 works (the condition is a plain Rust expression, so let PAT = EXPR parses natively):

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

Present in the parser AST (WhileExpr), absent from the published book. No documented example exists. Structure matches @if, so @while cond { ... } and by analogy @while let PAT = EXPR { ... } are expected to work. Treat @while let as unverified until you compile it.

@match

html! {
    @match user {
        Princess::Luna => h1 { "Woona's here" },
        Princess::Celestia | Princess::Cadance => p { "A princess." },
        _ => p { "Nothing to see here; move along." }
    }
}

Reuses the Princess enum and user binding from @if above. Match arms accept full Rust patterns per the parser source: or-patterns (A | B, shown above), bindings, ranges, destructuring, guards (pat if cond =>). The book shows only basic arms, so verify guard / or-pattern arms by compiling.

@let

Declares a variable inside html!, most useful inside loops. Accepts any Rust let pattern including type ascription.

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. Escaping and Raw HTML

Splices and text are escaped by default. Maud escapes exactly four characters: &, <, >, ". Single quotes pass through unescaped.

Bypass escaping with maud::PreEscaped, which renders its inner value verbatim.

use maud::PreEscaped;
html! {
    "<script>alert(\"XSS\")</script>"                // &lt;script&gt;...
    (PreEscaped("<script>alert(\"XSS\")</script>"))  // <script>...
}

Only wrap content you have already sanitized. PreEscaped on untrusted input is an XSS hole. The AsRef<str> bound restriction on PreEscaped was removed in 0.26.0, so it wraps a wider range of inner types now.


9. Custom Rendering and Composition

The Render Trait

Write a maud::Render impl to teach maud how to splice your own type. The trait has two methods, both with default impls that call each other:

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());
    }
}

Override at least one. Overriding neither causes infinite recursion (each default calls the other). Override render for the simple case. Override render_to when you want to write straight into the output buffer, for example to append multiple pieces without an intermediate Markup.

A render_to override is responsible for its own escaping. Raw writes into buffer are not escaped for you. Need that inside one? Reach for maud::Escaper, it escapes as it writes.

render/render_to are sync-only. There is no async Render trait or streaming variant. Resolve async data (a DB or HTTP call) to a value before entering html! and splice the resolved value.

Components Are Just Functions

A partial or component is a plain function returning Markup. Compose by splicing the call.

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) }
        }
    }
}

// usage
let markup = page("Home", html! { h1 { "Hello" } });

Splicing a Markup value never re-escapes it, so nesting components is safe. Maud does not currently export a RenderOnce trait (it had one from 0.8.0 through 0.15.0, removed 2017-01-26); don't confuse the name with horrorshow's still-existing RenderOnce trait.


10. Gotchas and Footguns

Each is flagged inline at its section; §11 consolidates. Index:

  • Void ; not self-close; control flow needs @ (every branch braced); §4, §7
  • [cond] (toggle) vs attr=[Option<T>] (value only when Some); attr concat needs { }; §5, §6
  • Splices escape by default; PreEscaped is an XSS hole on untrusted input; only & < > " escaped (single quote not); §6, §8
  • Rust 2021 # spacing (input #pinkie;); §5
  • Render with neither method overridden recurses forever; a render_to override does its own escaping (maud::Escaper); §9
  • render/render_to are sync-only; no async Render trait, no streaming variant; resolve async values before html! (§9)
  • Markup has no Display impl (.into_string() / maud::display(x)); no template files, no html_to!; §2
  • "Type-checked" covers Rust splice types only; no HTML5 tag/attribute validation, no content-model nesting checks (§4)

11. Quick Reference Card

Value:         html! { ... } -> Markup          Get String: .into_string()
Doctype:       (DOCTYPE)                          -> <!DOCTYPE html>
Element:       p { "text" nested { } }
Void element:  br;   link rel="x" href="y";       (trailing ; , renders <br>)
Text:          "literal"   r#"raw literal"#
Attr:          name="value"
Bool attr:     checked        (bare name, no =)
Shorthand:     input #id .class1.class2;          .class -> implies <div> if no tag
Toggle:        p contenteditable[cond] { }        p.cute[n > 50] { }
Optional attr: input value=[Some(42)];            title=[opt]  (None omits attr)
Splice:        (expr)                             escapes by default
Attr splice:   title=(x)   href={ (a) "/" (b) }
Dyn name:      #(id_expr)   .{ "color-" (sev) }
Block splice:  ({ let x = f()?; x })
If:            @if c { } @else if d { } @else { }
If let:        @if let Some(x) = opt { (x) } @else { }
For:           @for x in &xs { li { (x) } }
Match:         @match v { A => { }, _ => p { } }  (guards / or-pats via Rust patterns)
While:         @while cond { }                    (@while let unverified)
Let:           @let x = expr;                      @let y: Option<&str> = None;
Raw HTML:      (PreEscaped(safe_html))            disables escaping
Display value: (display(x))
Component:     fn c(...) -> Markup { html! { } }   splice with (c(...))
Render trait:  fn render(&self) -> Markup          fn render_to(&self, buf: &mut String)  (both have default impls, both sync-only)
Escapes:       & < > "   (single quote NOT escaped)

Book: https://maud.lambda.xyz API docs: https://docs.rs/maud

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.