std/html
Escaping untrusted text for HTML output.
from "std/html" import { Html }This is a security primitive, so read what it is not first: nothing here is a sanitizer. It does not parse markup, strip tags, or make attacker-supplied HTML safe to insert as HTML — that needs a parser and an element allowlist. These functions take text that should appear literally and make it appear literally.
The two contexts
escapeText turns &, <, >, " and ' into character references and copies everything else, so UTF-8 passes through intact. It is safe in element text, in RCDATA elements (<title>, <textarea>), and in an attribute value you wrote quotes around. Quotes are escaped even though element text does not need it — that way the common mistake of reaching for the text escaper inside a quoted attribute is still safe.
escapeAttr escapes all of that plus the bytes that terminate or extend an unquoted attribute value: space, tab, LF, FF, CR, = and backtick. The result is safe whether or not the template quoted the attribute.
let name = "x' onerror='alert(1)"
print("<div title='" + Html.escapeAttr(name) + "'>")
// <div title='x' onerror='alert(1)'>The noise is the point. Correctness must not depend on the caller having remembered quotes at a call site the escaper cannot see. If your templates always quote their attributes, escapeText produces cleaner output and is equally safe.
Where escaping is the wrong tool
There is deliberately no function here for any of these:
| Context | Why escaping does not help |
|---|---|
inside <script> / <style> | entities are not decoded there, and </script> still ends the element. Emit data in a <script type="application/json"> block and read it from JS. |
href / src / action | escaping does not stop javascript:. Check the scheme — see below. |
| tag or attribute names | only a fixed set you control belongs there. |
| inside an HTML comment | -- and > have meaning escaping misses. |
| CSS values, URL components | those have their own encoders. |
URL schemes
Html.isSafeUrl is true for a relative URL, or an absolute one whose scheme is http, https, mailto or tel. False for javascript:, data:, vbscript:, file:, and for any input containing a C0 control byte or DEL — browsers strip some of those before resolving the scheme, so java\tscript:x navigates exactly where javascript:x does.
let href = "javascript:alert(1)"
if Html.isSafeUrl(href) {
print("<a href=\"" + Html.escapeAttr(href) + "\">link</a>")
} else {
print("<span>link</span>")
}A scheme allowlist is all it is. It says nothing about where the URL points, so it is not an open-redirect check and not an SSRF check, and a true answer does not remove the need to escape.
Decoding
Html.unescape decodes every named character reference in the HTML5 table (all 2125 names: &, , ’, é, the MathML symbols) and numeric references (', '), leaving every other &-sequence exactly as it was. The terminating ; is required: the legacy bare forms (&, ©) are an HTML parser recovery rule and stay as written. Numeric references follow HTML5: € to Ÿ read as Windows-1252 (’ is ’), and references to U+0000, surrogates or out-of-range code points decode to U+FFFD rather than producing bytes that would not decode back as UTF-8.
The full table rather than the 252 names of HTML 4: what a browser shows for a document is what a decoder should produce, and the HTML5 additions (✓, [, ≂̸) do turn up in real pages. The table is generated from the WHATWG list by scripts/gen-html-entities.ts and searched by binary search, so lookup allocates nothing; it costs programs that import std/html about 50 KB of source to compile.
This is a display decoder, not a security function. Never decode before a security check and then emit the escaped original — that is the classic way to make a check and a renderer disagree.
API reference
Html
pub struct HtmlHtml — HTML escaping and URL-scheme checking.
Html.escapeAttr
fn Html.escapeAttr(s: &string): stringEscape s for an HTML attribute value under any quoting — double, single, or none. Everything escapeText escapes, plus the bytes that can terminate or extend an unquoted attribute value: space, tab, LF, FF, CR, = and backtick (old IE treats a backtick as an attribute delimiter).
The cost is noise: title= with a space in the value comes out as hello world. That is the same string to a browser, and the reason to pay it is that correctness here must not depend on the caller having remembered quotes at a call site the escaper cannot see. If your template always quotes its attributes, escapeText produces cleaner output and is equally safe.
Still not safe as an href/src/action value — see isSafeUrl — nor as an event-handler attribute (onclick=…), whose value is script, not text.
Html.escapeText
fn Html.escapeText(s: &string): stringEscape s for HTML element text: &, <, >, " and ' become character references, every other byte is copied unchanged (so UTF-8 passes through intact).
Safe in element text, in RCDATA elements such as <title> and <textarea>, and in an attribute value you wrote quotes around. Quotes are escaped even though element text does not need it, so that the common mistake of reaching for the text escaper inside a quoted attribute is still safe.
Not safe inside <script>/<style>, in an unquoted attribute value (use escapeAttr), or as a URL (see isSafeUrl). See the module comment.
Html.isSafeUrl
fn Html.isSafeUrl(url: &string): boolWhether url is safe to place in an href/src/action attribute: true for a relative URL, or an absolute one whose scheme is http, https, mailto or tel. False for javascript:, data:, vbscript:, file: and anything else — including any input containing a C0 control byte or DEL, because browsers strip some of those before resolving the scheme and "java\tscript:x" navigates exactly where "javascript:x" does.
A scheme allowlist is all this is. It says nothing about where the URL points, so it is not an open-redirect check and not an SSRF check; for those, compare the parsed host against your own allowlist. A true answer also does not remove the need to escape: run the value through escapeAttr on the way into the document.
Html.unescape
fn Html.unescape(s: &string): stringDecode character references: every named entity in the HTML5 table (&, , ’, ≂̸; 2125 names, generated from the WHATWG list) and numeric ones (', '), leaving every other &-sequence exactly as it was. The terminating ; is required: the legacy bare forms (&, ©) are an HTML parser recovery rule, not names.
Numeric references follow HTML5: €..Ÿ read as Windows-1252 (’ is ’), and surrogates, out-of-range code points, or U+0000 decode to U+FFFD rather than producing bytes that would not decode back as UTF-8 — or, for �, a NUL that would truncate the string wherever it is next written.
Do not decode and then re-emit into HTML without escaping again, and never decode before a security check — running a scheme or content check on unescaped text and then emitting the escaped original is the classic way to get a check and a renderer to disagree.