html

func html(input: MacroInput, use_site: SyntaxContext, context: QuoteContext) -> SyntaxResult<Expr>

Expands an HTML template into a category-checked Markup expression.

Import the macro and place its template in braces:

use html::{ html }

let page = @html {
    main #content .page {
        h1 { "Hello" }
    }
}

The result is trusted Markup. Call Markup.into_string() to transfer its rendered string, interpolate it into another template to compose fragments, or return it from an API that retains the trust boundary.

Template grammar

The complete template grammar is:

template       := node*
node           := string | "(" expression ")" | element | control
element        := name shortcut* attribute* (";" | "{" template "}")
name           := word ("-" word)*
shortcut       := "." shortcut-value ["[" expression "]"]
                | "#" shortcut-value
shortcut-value := name | string | "(" expression ")" | "{" template "}"
attribute      := name
                | name "?"
                | name "[" expression "]"
                | name "=" rendered-value
                | name "=[" expression "]"
rendered-value := string | "(" expression ")" | "{" template "}"
control        := @if | @for | @let | @match

A word starts with an ASCII letter or underscore. Hyphenated element and attribute names such as data-user-id are accepted. The macro does not keep layout whitespace between nodes; include whitespace in a string literal when the output requires it.

Text and interpolation

String literals and parenthesized expressions are content nodes:

@html {
    p { "Hello, " (user.name) "!" }
}

Every expression is captured in the caller's syntax context, evaluated in source order, and rendered through RenderHtml. String literals use the same rendering path, so literal &, <, >, double quotes, and single quotes are escaped rather than treated as markup.

A braced template is accepted where a rendered value is expected. This is useful for constructing a dynamic attribute or shortcut from several pieces:

@html {
    a href={ "/users/" (user.id) } { (user.name) }
}

Elements

Braces render an explicit closing tag. A semicolon renders only the opening tag and is the appropriate spelling for HTML void elements:

@html {
    section { "content" }
    br;
    input type="checkbox" checked?;
}

DOCTYPE; is the one special element spelling and emits <!DOCTYPE html>. It is case-sensitive and must use the semicolon form.

Element and attribute names are syntactic names, not expressions. The macro accepts custom-element names but does not validate the HTML vocabulary, void element rules, nesting, duplicate attributes, URL schemes, ARIA constraints, or other document semantics.

ID and class shortcuts

#name sets an ID and .name appends a class. A template beginning with a shortcut implicitly uses div:

@html {
    #dialog .modal { "implicit div" }
    article #entry .card.featured { "explicit article" }
}

Shortcut values may be names, strings, parenthesized expressions, or braced templates. Adjacent classes are combined into one space-separated class attribute in source order:

@html {
    div #(identifier) .base."quoted-class".{ "severity-" (severity) } {}
}

A class shortcut followed by [condition] is included only when the Boolean condition is true. ID shortcuts cannot be conditional. When several ID shortcuts are written, the last shortcut supplies the generated ID; an explicit id= attribute is separate and can therefore produce a duplicate.

Attributes

The supported attribute forms are:

  • name=value always emits a quoted value rendered through RenderHtml.

  • name=[optional] omits the attribute for none and renders the payload for some.

  • name[condition] emits a valueless Boolean attribute only when true.

  • Bare name always emits a valueless Boolean attribute.

  • name? is the Maud-compatible spelling for an unconditional valueless attribute.

Attribute values must be string literals, parenthesized expressions, braced templates, or bracketed optional expressions. Examples:

@html {
    input
        type="text"
        value=(current_value)
        title=[optional_title]
        disabled[is_disabled]
        required?;
}

Attribute values are HTML-escaped, but escaping is not application-level validation: callers must still enforce policies for links, script sources, CSS, and other context-sensitive values.

Conditional templates

@if accepts an ordinary Boolean condition, @else if, and @else:

@html {
    @if signed_in {
        p { "Welcome" }
    } @else if registration_open {
        a href="/register" { "Register" }
    } @else {
        p { "Closed" }
    }
}

Pattern conditions use @if let pattern = expression:

@html {
    @if let .some(title) = optional_title {
        h1 { (title) }
    }
}

A missing branch contributes an empty string. Conditions and selected branch expressions follow ordinary Talk evaluation and ownership rules.

Iteration

@for pattern in expression renders its body once per element in iteration order. The pattern may destructure each element:

@html {
    ul {
        @for (name, href) in links {
            li { a href=(href) { (name) } }
        }
    }
}

The sequence must conform to Iterable. Each body result is already-safe macro output and is concatenated directly without another escaping pass.

Local bindings

@let introduces a pattern binding for the remainder of the current template. It requires a terminating semicolon and may include a type annotation:

@html {
    @let heading: String = page.title;
    h1 { (heading) }
    title { (heading) }
}

The initializer is evaluated once. The binding's scope includes subsequent sibling nodes but not content that precedes the declaration.

Pattern matching

@match captures normal Talk patterns. Each arm uses ->; braced arm bodies are recommended, and commas may separate arms:

@html {
    @match state {
        .ready(message) -> { strong { (message) } },
        .waiting -> { em { "waiting" } }
    }
}

At least one complete arm is required. Pattern binding, exhaustiveness, and ownership behavior are checked as part of the generated Talk expression.

Escaping and trusted markup

The default RenderHtml implementation escapes a value's Showable text. Built-in conformances cover strings, numbers, Booleans, and characters. Markup and PreEscaped deliberately bypass escaping:

use html::{ html, PreEscaped }

@html {
    (untrusted_text)                         // escaped
    (PreEscaped(value: trusted_fragment))    // inserted unchanged
}

Constructing Markup or PreEscaped is an explicit trust boundary. Never wrap untrusted input merely to preserve tags. Prefer a RenderHtml conformance when a domain value has one well-defined safe representation.

Escaping protects HTML syntax only. It does not make JavaScript, CSS, URLs, or arbitrary browser-active content safe for their own sublanguages.

Output and evaluation

The expansion is an expression returning one Markup. Static fragments and rendered expressions are concatenated without indentation or pretty-printing. Interpolations, conditions, loop sequences, and attribute expressions retain use-site hygiene and normal Talk effects. Definition-site references to macro runtime helpers cannot be captured by caller bindings.

Compile-time failures

Invalid templates fail during macro expansion with a source span and a stable html.* diagnostic code. Diagnosed errors include unknown controls, missing conditions or bodies, malformed @if let, missing in, missing @let semicolons or values, incomplete match arms, invalid shortcut toggles, missing attribute values, unclosed elements, and macro invocations without a delimited body. Captured Talk expressions, patterns, and types report their normal parser diagnostics.

View Source
pub func html(input: MacroInput, use_site: SyntaxContext, context: QuoteContext) -> SyntaxResult<Expr> {
	match input.tree {
		.group(group) -> {
			let parser = HtmlParser(source_id: input.source_id, source: input.source.to_string(), trees: copy_trees(trees: group.children), index: 0, use_site: copy_syntax_context(context: use_site), context: copy_quote_context(context: context))
			let parsed = parser.parse_all()
			if let .some(failure) = parsed.failure {
				return SyntaxResult<Expr>(value: .none, failure: .some(failure))
			}
			if let .some(value) = parsed.value {
				return quote { Markup(value: $value) }
			}
			SyntaxResult<Expr>(value: .none, failure: .some(SyntaxFailure(code: "html.empty", message: "HTML macro produced no output", span: group.open.span.lower..<group.close.span.upper)))
		},
		.leaf(token) -> SyntaxResult<Expr>(value: .none, failure: .some(SyntaxFailure(code: "html.expected-group", message: "HTML macro requires a delimited body", span: token.span)))
	}
}