2021-07-28, 20:10–20:20 (UTC), Red
HypertextLiteral is a Julia package for generating HTML, SVG, and other SGML tagged content. It works similar to Julia string interpolation, appropriately escaping interpolated values and providing handy data conversions dependent upon context. The implementation compiles templates to functions, with a custom IO proxy for escaping.
For those building dynamic hypertext, HTL is fast: 40x faster than object-based serializations; 8x faster than naive list comprehensions with string interpolation.
Generating HTML + SVG output is a common requirement for applications, especially when building scientific dashboards. The faster the better. Being able to use proven hypertext fragments as templates is especially important. The ability to encapsulate and re-use these templates as functions is critical.
@htl macro translates an HTML template into a function closure. Here is an example.
books = [ (name="Who Gets What & Why", year=2012, authors=["Alvin Roth"]), (name="Switch", year=2010, authors=["Chip Heath", "Dan Heath"]), (name="Governing The Commons", year=1990, authors=["Elinor Ostrom"])] render_row(book) = @htl(""" <tr><td>$(book.name) ($(book.year))<td>$(join(book.authors, " & ")) """) render_table(books) = @htl(""" <table><caption><h3>Selected Books</h3></caption> <thead><tr><th>Book<th>Authors<tbody> $((render_row(b) for b in books))</tbody></table>""") display("text/html", render_table(books)) #=> <table><caption><h3>Selected Books</h3></caption> <thead><tr><th>Book<th>Authors<tbody> <tr><td>Who Gets What & Why (2012)<td>Alvin Roth <tr><td>Switch (2010)<td>Chip Heath & Dan Heath <tr><td>Governing The Commons (1990)<td>Elinor Ostrom </tbody></table> =#
HTL is contextual. At macro expansion time, the string template is passed through a light-weight HTML/SGML lexer. This is used to track the context of each interpolated Julia expression: is it part of element content, an attribute value, or is it inside an element tag where several attributes might be expanded? There is also a rawtext context used when content is inside a
HTL is extensible. With multiple dispatch, custom data types can provide their own contextual serialization. This permits us to omit boolean attributes that are false. It also lets us expand vectors differently dependent upon context: within element content, they are simply appended; while within attribute values, they are space separated.
HTL is fast. A template rendering that takes 500μs with HTL, takes 4.5ms with naive string interpolation and list comprehension. Object based alternatives, such as Hyperscript, take even longer (21ms). Memory usage of HTL is likewise low. It uses 1/3rd less memory than naive string approach, and 1/6th the memory of an object based approach.
This efficiency was achieved by emulating Julia's documentation system. Each component of the template is converted into an object which prints its content to a given
IO. During macro processing, we build a Julia program that relies upon three primitive structures:
- Bypass is used for content that should be emitted as-is.
- Render is used for content that should be properly escaped.
- Reprint is a function closure used for composing content.
As the template is converted, leaf nodes are converted into either Render or Bypass, depending if they are part of the template, or part of variables that are to be escaped. Reprint is used to concatenate adjacent components that appear in the template or are generated by a list comprehension.
HTL is safe. Escaping code is layered using an
IO proxy. Each of the 3 primitives has their own dispatch with regard to this proxy. This way, so long as the template translation properly distinguishes between
Render chunks, escaping is always performed. Handled as an exception,
<script> content is checked to ensure it does not contain the
"</script>" literal but is otherwise unescaped.
camelCase attribute names, which must be left as-is for SVG. Instead, we only convert
snake_case names to their
kebab-case equivalent. Moreover, if attribute sets are constants, we can pre-compute their serialization at macro expansion time.
It is notable how nicely the Julia implementation flowed together. Julia's excellent macro facility lets us easily convert embedded functions and list comprehensions into relevant template logic. Julia's handling of tiny function closures was outstanding: not only does it let us write code that is easy to read, the approach turned out to be surprisingly fast. Julia's
IO interface lets us easily insert a proxy that was trivial to write, and, yet again, surprisingly efficient. Finally, multiple dispatch enables user-defined types to have their own serialization. Kudos Julia.
This approach could be used to make similar template libraries for other structured notations, such as JSON.
Collaborator on YAML, HTSQL, DataKnots, and other projects that advance the usability of software systems.