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 | @matchA 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=valuealways emits a quoted value rendered throughRenderHtml.name=[optional]omits the attribute fornoneand renders the payload forsome.name[condition]emits a valueless Boolean attribute only when true.Bare
namealways 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)))
}
}