Html
Html module: HTML escaping, safe HTML helpers, and template composition.
Provides utilities for escaping HTML entities, marking strings as safe (pre-escaped) HTML, rendering lists of items into IOLists, and composing template partials and layouts.
IOList is the canonical "rendered HTML fragment" type throughout this module. Fragments can be stored in Vault for fragment caching and re-embedded into parent templates via ~H sigils without double-escaping.
Types
Html.Safe wraps a string that should NOT be escaped (already safe HTML). DEPRECATED in favour of the context-indexed Trusted* types below: Safe is context-free, so a value trusted anywhere is trusted everywhere. Kept working unchanged because bastion is published at 0.2.3.
Markup trusted as HTML element content. Inserted verbatim in a PCDATA position; escaped anywhere else.
A string trusted as an attribute value. Inserted verbatim in an ordinary attribute; escaped anywhere else.
A string trusted as a URL. Inserted verbatim in a url attribute (href, src, ...); escaped anywhere else -- notably NOT trusted as HTML.
A string trusted as CSS. Inserted verbatim in a style attribute or a <style> body; escaped anywhere else.
A string trusted as JavaScript. Inserted verbatim inside <script>; escaped anywhere else.
Functions
Compute a content hash over an IOList for ETag generation.
march> Html.content_hash(IOList.from_string("<p>Hello</p>"))
"34d60b7d015e7b44"A ~H"<p>Hello</p>" template renders the same bytes, so it hashes to the same value.
Delegates to IOList.hash (FNV-1a 64-bit over segments, no flattening). The returned hex string is suitable for use as an HTTP ETag value or as a Vault cache key for fragment caching.
Cost: O(total bytes across all segments).
Escape HTML entities in a string: & < > " '
march> Html.escape("a & b")
"a & b"
march> Html.escape("<script>")
"<script>"
march> Html.escape("he said \"hi\"")
"he said "hi""
march> Html.escape("it's")
"it's"Replaces & first to avoid double-escaping, then < > " '.
Cost: O(n) per replacement pass.
Escape a string for use in an HTML attribute value context.
march> Html.escape_attr("hello & world")
"hello & world"
march> Html.escape_attr("<script>")
"<script>"Currently identical to Html.escape. Kept as a separate function so attribute-specific escaping rules can be tightened in the future without breaking call sites.
Join a list of IOLists with a separator string.
march> IOList.to_string(Html.join([IOList.from_string("a"), IOList.from_string("b")], ", "))
"a, b"Cost: O(n) where n is the length of the list.
Wrap content in a standard HTML document structure.
march> IOList.to_string(Html.layout("My Page", IOList.empty(), IOList.from_string("<p>Hi</p>")))
"<!DOCTYPE html><html><head><title>My Page</title></head><body><p>Hi</p></body></html>"titleis HTML-escaped automatically.head_extrais an IOList inserted after</title>and before</head>.
Use it for `<link>`, `<meta>`, and `<style>` tags.bodyis an IOList (result of ~H or render_partial) inserted inside<body>.
Typical use:
fn page(conn) do
Html.layout(
"Dashboard",
IOList.from_string("<link rel=\"stylesheet\" href=\"/app.css\">"),
body_content
)
endCost: O(1) for IOList construction; O(total bytes) when flushed to string.
Render a list of items into an IOList using a template function.
march> let items = Html.list(["a", "b"], fn item -> IOList.from_string("<li>" ++ item ++ "</li>"))
march> IOList.to_string(items)
"<li>a</li><li>b</li>"Cost: O(n) where n is the length of the list.
Mark a string as safe HTML (bypass escaping).
march> Html.raw("<b>bold</b>")
Safe("<b>bold</b>")Use this when you have pre-escaped HTML that should be inserted verbatim.
Map a template partial over a list of items, returning a single IOList.
march> let users = [{name: "Ada"}, {name: "Alan"}]
march> IOList.to_string(Html.render_collection(users, fn user -> IOList.from_string("<li>" ++ user.name ++ "</li>")))
"<li>Ada</li><li>Alan</li>"Equivalent to Html.list(items, partial_fn). The Rails-flavoured name makes the intent clear when building list views.
The returned IOList can be embedded in a parent ~H sigil without double-escaping or stored in Vault as a cached fragment.
Cost: O(n) where n is the length of the list.
Call a template partial function with a single argument, returning IOList.
march> let user_card = fn user -> IOList.from_string("<div>" ++ user.name ++ "</div>")
march> IOList.to_string(Html.render_partial(user_card, {name: "Ada"}))
"<div>Ada</div>"Partials are just functions that return IOList. This helper makes the intent explicit and ensures the return type is IOList so the result can be safely embedded in a parent ~H sigil without double-escaping, or stored in Vault as a cached fragment.
Cost: O(1) — just calls the function.
Extract the string from a Safe value.
march> Html.safe_to_string(Html.raw("<b>bold</b>"))
"<b>bold</b>"Build an HTML tag with properly escaped attributes and an IOList body.
march> IOList.to_string(Html.tag("div", [("class", "greeting")], IOList.from_string("Hi")))
"<div class=\"greeting\">Hi</div>"
march> IOList.to_string(Html.tag("br", [], IOList.empty())) -- no self-closing form
"<br></br>"DEPRECATED — prefer a ~H template. ~H walks its literal chunks through the HTML context automaton at compile time and escapes every interpolation for the position it actually lands in, with unsafe positions reported as compile errors. Html.tag builds markup outside the sigil, so none of that analysis applies and it must check at runtime instead.
Attribute VALUES are escaped for the context their attribute name implies: a url attribute (href, src, action, …) gets the URL scheme allowlist, style gets CSS declaration escaping, everything else gets attribute escaping.
Element and attribute NAMES are validated, not escaped, and an invalid one PANICS. Escaping cannot help there — onerror has no character to escape — so a name built from untrusted input is a programming error, not a data condition. Event-handler attributes (on*) are refused outright: their value is JavaScript, and Html.tag has no way to know whether the caller meant that.
The content is an IOList so it can be a composed partial or ~H result. Passing the result of a ~H sigil as content is safe: it will not be double-escaped.
Cost: O(k) for attribute serialisation where k = number of attributes, plus O(1) for IOList wrapping.
Trust a string as an ordinary attribute value. See trust_html.
Examples
march> Html.trust_attr("btn primary")
TrustedAttr("btn primary")Trust a string as CSS. See trust_html.
Examples
march> Html.trust_css("color: red")
TrustedCss("color: red")Trust a string as HTML element content.
Inserted verbatim where the surrounding template is element content, and escaped anywhere else — trusting a string as HTML says nothing about whether it is a safe URL, so the same value in an href still gets the scheme allowlist.
Prefer building markup with the ~H sigil, which needs no trust at all. Reach for this only when a string genuinely is markup from a source you control. The string is stored as given: nothing is checked or escaped.
Examples
march> Html.trust_html("<b>bold</b>")
TrustedHtml("<b>bold</b>")Trust a string as JavaScript. See trust_html.
Examples
march> Html.trust_js("init();")
TrustedJs("init();")Trust a string as a URL. It bypasses the scheme allowlist, so only use it on a URL you constructed yourself. See trust_html.
Examples
march> Html.trust_url("/search?q=1")
TrustedUrl("/search?q=1")Unwrap a trusted attribute value back to a plain String. The string is returned unchanged (not escaped).
Examples
march> Html.untrust_attr(Html.trust_attr("btn"))
"btn"Unwrap trusted CSS back to a plain String. The string is returned unchanged (not escaped).
Examples
march> Html.untrust_css(Html.trust_css("color: red"))
"color: red"Unwrap trusted HTML back to a plain String. The string is returned unchanged (not escaped).
Examples
march> Html.untrust_html(Html.trust_html("<b>bold</b>"))
"<b>bold</b>"Unwrap trusted JavaScript back to a plain String. The string is returned unchanged (not escaped).
Examples
march> Html.untrust_js(Html.trust_js("init();"))
"init();"Unwrap a trusted URL back to a plain String. The string is returned unchanged (not escaped).
Examples
march> Html.untrust_url(Html.trust_url("/home"))
"/home"