← html · API reference · version 0.1.3
3 file(s), 136 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
escape_html fn | 21 | HTM-002 | 9 10 11 12 13 18 … (15 total) |
src/html.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// html — escape text so it is safe inside HTML markup.
// HTML-escape the standard five entities for safe interpolation
// into HTML output:
//
// & → &
// < → <
// > → >
// " → "
// ' → '
//
// Use whenever user-supplied text gets dropped into an HTML
// element body or attribute value. Each character is replaced
// once, in one pass, so the entities it writes are not escaped
// again within the call — but a second call escapes them.
// Example: @HTM-001 — escape once, where the text enters markup.
// Example: @HTM-002 — a single-quoted attribute stays closed.
pub fn escape_html(self: text) -> text {
out = "";
for c in self {
if c == '&' {
out += "&";
} else if c == '<' {
out += "<";
} else if c == '>' {
out += ">";
} else if c == '"' {
out += """;
} else if c == '\'' {
out += "'";
} else {
out += "{c}";
}
}
out
}
#pure
tests/01-escape.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// escape_html: each entity, the one-pass rule, text that needs no escaping.
use html;
fn test_basic_entities() {
assert("<".escape_html() == "<", "lt");
assert(">".escape_html() == ">", "gt");
assert("&".escape_html() == "&", "amp");
assert("\"".escape_html() == """, "quot");
assert("'".escape_html() == "'", "apos");
}
fn test_safe_chars_pass_through() {
s = "Hello, World! 123 _-./";
assert(s.escape_html() == s, "safe chars unchanged");
}
fn test_empty() {
assert("".escape_html() == "", "empty");
}
fn test_amp_first() {
// & must be escaped FIRST so the substituted entity strings
// don't get re-escaped. `<` → `<` already starts with `&`,
// and naive ordering would turn that into `&lt;`.
assert("<".escape_html() == "<", "lt not double-escaped");
// Existing entity strings DO get double-escaped (intentional —
// we treat input as raw text, not pre-escaped).
assert("&".escape_html() == "&amp;", "& double-escaped");
}
fn test_attribute_safe() {
// The five entities make the output safe for both element
// bodies AND attribute values (with either quote style).
s = "<img src=\"x\" alt='y'>";
expected = "<img src="x" alt='y'>";
assert(s.escape_html() == expected, "attribute-style mixed quotes");
}
fn test_unicode_passes_through() {
// Non-ASCII characters don't need escaping in UTF-8 HTML.
s = "→ snowman ☃";
assert(s.escape_html() == s, "unicode passes through");
}
fn test_xss_payload() {
// The classic <script> payload — safe after escape.
payload = "<script>alert('x')</script>";
out = payload.escape_html();
assert(!out.starts_with("<"), "no leading <");
assert(out.find("<script") == null, "no live script tag");
}
fn main() {
test_basic_entities();
test_safe_chars_pass_through();
test_empty();
test_amp_first();
test_attribute_safe();
test_unicode_passes_through();
test_xss_payload();
println("escape-html: ok");
}
tests/02-worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `html`. Each test is a call-site contract the signature
// `escape_html(self: text) -> text` cannot show, cited from the function with
// `// Example: @HTM-00N`.
use html::*;
// @HTM-001 — escape ONCE, at the point the text enters markup. Escaping is not
// idempotent: an already-escaped `&` escapes again to `&amp;`, which a browser
// shows as the literal text "&". So keep the raw text in your data and escape where
// it is written into HTML — never store the escaped form and escape it again on output.
fn test_escaping_twice_shows_the_entity() {
once = "Tom & Jerry".escape_html();
assert(once == "Tom & Jerry", "once: {once}");
twice = once.escape_html();
assert(twice == "Tom &amp; Jerry", "twice is visible as '&' on the page: {twice}");
}
// @HTM-002 — one escape serves every HTML text context: an element body, and an
// attribute value in EITHER quoting style. The apostrophe is escaped too, so a value
// placed between single quotes cannot close its attribute and start another.
fn test_a_single_quoted_attribute_stays_closed() {
hostile = "x' onmouseover='alert(1)";
attr = "<a title='{hostile.escape_html()}'>";
assert(attr == "<a title='x' onmouseover='alert(1)'>", "{attr}");
quotes = 0;
for c in attr { if c == '\'' { quotes += 1; } }
assert(quotes == 2, "only the attribute's own two quotes remain: {quotes}");
}