MarkupBuilder
A tag stack that makes a mismatched close unrepresentable.
MarkupBuilder keeps a stack of open elements, escapes every value, validates every name and closes whatever is left open when you are done.
| Method | Effect |
|---|---|
open(tag) / open(tag, name, value, ...) | Writes the tag, pushes it |
close() | Closes the innermost open element |
el(tag, content) / el(tag, content, name, value, ...) | Open, escaped text, close |
text(value) | Escaped; any type with a : string cast |
raw(html) | Unescaped; the caller owns safety |
comment(text) | An HTML comment |
depth() | Number of elements currently open |
done() | Closes anything still open, returns the string |
Why close takes no argument
The stack already holds the tag name, so a mismatched close is impossible to write. There is no close("div") that can drift out of sync with the open("section") above it.
Void elements — img, input, br, meta, link and the rest — are never pushed, so they need no close at all.
Compile-time and run-time guards
An odd number of attribute arguments is a compile-time error:
h.open("a", "href"); // compilerError: attributes are name/value pairsA tag or attribute name outside [A-Za-z][A-Za-z0-9_:-]* is dropped and logged rather than emitted, because such a name came from somewhere it should not have.
Escaping, and the one hole
text and el escape &, <, >, " and ' — in attribute values as well as in text nodes. raw does not escape, and exists for composing already-safe fragments: the output of another MarkupBuilder, or the return value of island.
Passing request data to
rawis a cross-site scripting bug. If a string reached you from a request, from a file or from a database, it belongs intext, or it belongs in a sanitiser you can point at.
Scoping and conditional classes
defer scopes an element to a Chapel block, and classList builds a conditional class attribute from name/condition pairs:
{
h.open("tr", "class", classList("warn", node.degraded, "down", node.offline));
defer h.close();
h.el("td", node.name);
}Composing fragments
Because done() returns a string, a page can build its parts independently and assemble them:
proc page(ctx: Context, ref meta: PageMeta): string {
var h = new MarkupBuilder();
h.raw(breadcrumbs(doc.urlPath));
h.open("article");
h.raw(doc.html);
return h.done();
}Each raw here takes markup produced by another builder, which is exactly what it is for.