Sigils & Templating
Some kinds of text aren’t really “just a string”: a chunk of TOML, an HTML template, an
XML document. March’s sigils let you write that text as a literal, right in your
source, and have the compiler hand it to a parser or template builder instead of
treating it as a plain String. The most-used one, ~H, builds HTML with
contextual auto-escaping: every interpolation is escaped for the exact place it
lands, worked out at compile time. The rest of this page starts with the general
mechanism, then goes deep on ~H.
The general mechanism
A sigil is written ~Name"..." (or ~Name"""...multi-line...""" for longer content).
The uppercase (or lowercase-word) name right after the ~ selects which handler runs:
~toml"""
port = 8080
host = "localhost"
"""
Under the hood, ~Name"content" is sugar for a plain function call: ~toml"..."
becomes Sigil.toml("..."), ~xml"..." becomes Sigil.xml("..."), and so on; a
sigil is just a call with unusual syntax around the argument, not a separate kind of
value. That means you can always see exactly what a sigil does by looking up
Sigil.<name> in the stdlib.
March ships three general-purpose sigils out of the box:
| Sigil | Calls | Returns |
|---|---|---|
~toml"..." |
Toml.parse_exn |
a TomlValue |
~xml"..." |
Xml.parse_exn |
a parsed XML document |
~yaml"..." |
Yaml.parse_exn |
a parsed YAML document |
All three panic if the content doesn’t parse: they’re meant for literals you
control (embedding a known-good config as source, not parsing untrusted input at
runtime). If you need to handle a malformed document gracefully, parse a runtime string
with the ordinary Toml.parse/Xml.parse/Yaml.parse functions instead, which return
a Result rather than panicking.
They do not accept interpolation. ~toml, ~xml and ~yaml hand their content to
a parser, so a ${...} hole would be spliced into the source text before parsing and
could change the parsed structure rather than appear as a value in it; an interpolated
</name><admin>true</admin><name> would add a whole element. That is a compile error:
~xml"<user><name>${v}</name></user>"
-- error: Cannot interpolate into a ~xml sigil. Its content is parsed, so an
-- interpolated value would be spliced into the source text and could change
-- the parsed structure rather than appear as a value in it.
Build the value programmatically instead (parse a literal document and set fields on
it), so the value arrives as data rather than as source text. ~H is the exception,
and the next section explains why it can afford to be.
let config = ~toml"""
port = 8080
host = "localhost"
"""
-- config : TomlValue, built at the call site — a typo here panics immediately,
-- which is exactly what you want for a literal baked into your source
The ~H sigil: HTML templates
~H is the one sigil with truly special treatment: instead of calling a
Sigil.h function at runtime, the compiler builds the HTML directly into an efficient
multi-segment structure (an IOList) at compile time; there’s no intermediate string
concatenation, even for a template with many interpolated pieces.
let name = "<script>alert(1)</script>"
let html = ~H"<p>Hello, ${name}!</p>"
-- renders: <p>Hello, <script>alert(1)</script>!</p>
Interpolation is ${expr}, and you never choose how it is escaped. The compiler
works out where in the HTML each hole lands and applies the escaping that position
needs. That is the headline reason to use ~H over string concatenation: with
concatenation you have to remember to escape every value and pick the right escaping
for its position, and getting either wrong is how injection bugs happen.
What happens where
The same ${name} is treated differently depending on where you put it:
| Where the hole is | What ~H does |
|---|---|
element content: <p>${x}</p> |
HTML entity-encoding |
an attribute value: <div class="${x}"> |
entity-encoding, plus a backtick |
the start of a URL attribute: <a href="${x}"> |
URL scheme allowlist |
later in a URL: <a href="/s?q=${x}"> |
percent-encoding |
a style attribute: <div style="color:${x}"> |
CSS escaping, allowlisted functions |
a CSS url(): style="background:url(${x})" |
URL rules that also survive CSS |
inside <script>: <script>var n="${x}"</script> |
JavaScript string escaping |
Worked through:
let u = "javascript:alert(1)"
~H"<a href=\"${u}\">x</a>" -- <a href="about:invalid#zSoyz">x</a>
let q = "a b&c"
~H"<a href=\"/s?q=${q}\">x</a>" -- <a href="/s?q=a%20b%26c">x</a>
let col = "var(--accent)"
~H"<div style=\"color:${col}\">" -- <div style="color:var(--accent)">
A URL that fails the scheme allowlist becomes about:invalid#zSoyz: inert, and
visible in the page source so the problem is obvious rather than silent. http,
https, mailto, tel, ftp and any relative reference are allowed.
Unquoted attributes are quoted for you. <div class=${x}> emits
<div class="...">, so a value containing a space cannot start a new attribute.
What will not compile
Some positions cannot be made safe by escaping at all; an attacker-chosen attribute
name like onerror contains no character an encoder could touch. Those are compile
errors rather than silently-wrong output:
~H"<div ${attr}=1>x</div>" -- error: Cannot interpolate where an attribute name
-- is expected...
~H"<${tag}>x</${tag}>" -- error: Cannot interpolate an element name...
~H"<!-- ${x} -->" -- error: Cannot interpolate inside an HTML comment...
~H"<div class=\"${x}" -- error: This ~H template does not end in a
-- well-formed state...
The last one catches a template that stops mid-tag or mid-attribute: whatever you concatenate after it would be spliced into that position.
Multi-line templates use triple quotes, exactly like triple-quoted strings elsewhere in March:
fn render_card(title : String, body : String) : IOList do
~H"""
<div class="card">
<h2>${title}</h2>
<p>${body}</p>
</div>
"""
end
Trusted content, and why trust does not travel
Sometimes you want to interpolate real markup: an icon’s <svg>, or HTML you
produced yourself. The Html.trust_* functions say so, and each names the context the
trust applies to:
let icon = Html.trust_html("<svg>...</svg>")
~H"<button>${icon} Click me</button>" -- <button><svg>...</svg> Click me</button>
The context matters, and this is the part worth internalising. Trusting a string as
HTML states no fact about whether it is a safe URL, so the same value in an href is
still escaped:
let h = Html.trust_html("<em>ok</em>")
~H"<p>${h}</p>" -- <p><em>ok</em></p> verbatim
~H"<a href=\"${h}\">x</a>" -- href="<em>ok</em>" escaped
Trust does not travel between contexts. Use the one that matches where the value is going:
| Function | Trusted in |
|---|---|
Html.trust_html |
element content |
Html.trust_attr |
an ordinary attribute value |
Html.trust_url |
a URL attribute; bypasses the scheme allowlist |
Html.trust_css |
a style attribute or <style> body |
Html.trust_js |
inside <script> |
Each has a matching Html.untrust_* to get the plain String back.
Only reach for these on content you generated or verified yourself, never on user
input, since that is exactly the escaping ~H exists to give you automatically. Most
templates need none of them.
Html.rawis deprecated. It still works and is treated as HTML trust, soHtml.rawin element content behaves as it always did. But it is context-free (it cannot say where the content is trusted), soHtml.raw("javascript:...")in anhrefis escaped toabout:invalid#zSoyzrather than inserted. PreferHtml.trust_html, which states what it means.
Html.tagis deprecated too. It builds markup outside the sigil, so it never gets this analysis and has to validate at runtime instead: it will not accept element and attribute names it cannot prove safe, and rejectson*handlers entirely. Use~H.
Composing templates
A ~H template returns an IOList, and an IOList interpolates cleanly into another
~H template without being re-escaped or double-wrapped, so partials compose the way
you’d expect:
fn render_user(u : User) : IOList do
~H"""
<li class="user-row">
<strong>${u.name}</strong>
<span class="email">${u.email}</span>
</li>
"""
end
fn render_list(users : List(User)) : IOList do
let items = Html.list(users, render_user) -- IOList of all the rendered rows
~H"""
<ul class="user-list">${items}</ul>
"""
end
There’s no template-level for loop inside ~H itself; Html.list (map a render
function over a list, producing one combined IOList) is the idiomatic way to render a
collection, and ordinary recursion works too for anything more custom. See HTML
cookbook for a complete worked example
(layouts, partials, a full user-list page) built entirely from these pieces.
A gotcha worth knowing: CSRF injection
If you’re building a web app with March’s HTTP framework, ~H automatically injects a
CSRF protection tag into <form method="post|put|patch|delete"> tags it emits, but
only when a conn binding is lexically in scope at that ~H call site. If you’re
rendering a form outside of a request-handling context (no conn in scope), the tag
isn’t injected and you won’t get a compile error about it; so if you rely on this
protection, make sure the form-rendering function actually has conn available, rather
than assuming every ~H form is automatically protected.
Do not add your own token as well. Injection is automatic, so writing
${CSRF.tag(conn)} inside the form used to emit two hidden inputs. ~H now notices an
explicit token and skips its own injection, warning that yours is redundant, but the
clean form is simply to leave it out:
~H"<form method=\"post\">...</form>" -- token injected for you
Explicit token helpers are for markup built outside ~H, by string concatenation,
where no content is injected.
Next Steps
- HTML cookbook: this page covers the mechanism; the cookbook shows you how to build a full page with it (layouts, partials, a complete example).
- Type System: what
TomlValueand friends look like as ordinary ADTs. - Capabilities: the permission system that guards the file/network access templating code often sits next to.