Element Content
Hypertext literal provides interpolation via $
. Within element content, the ampersand (&
), less-than (<
), single-quote ('
) and double-quote ("
) are escaped.
using HypertextLiteral
book = "Strunk & White"
@htl "<span>Today's Reading: $book</span>"
#-> <span>Today's Reading: Strunk & White</span>
Julia expressions can be interpolated using the $(expr)
notation.
@htl "2+2 = $(2+2)"
#-> 2+2 = 4
To include $
in the output, use \$
. Other escape sequences, such as \"
also work.
@htl "They said, \"your total is \$42.50\"."
#-> They said, "your total is $42.50".
Within tripled double-quotes, single double-quoted strings can go unescaped, however, we still need to escape the dollar sign ($
).
@htl """They said, "your total is \$42.50"."""
#-> They said, "your total is $42.50".
In this document, we discuss interpolation within regular tagged content. Interpolation within attribute values and within <script>
or <style>
tags is treated differently.
Strings & Numbers
Strings, symbols, integers, booleans, and floating point values are reproduced with their standard print()
representation. Output produced in this way is properly escaped.
@htl "<enabled>$(false)</enabled><color>$(:blue)</color>"
#-> <enabled>false</enabled><color>blue</color>
@htl "<int>$(42)</int><float>$(6.02214076e23)</float>"
#-> <int>42</int><float>6.02214076e23</float>
We include AbstractString
for the performant serialization of SubString
and other string-like objects.
@htl "<slice>$(SubString("12345", 2:4))</slice>"
#-> <slice>234</slice>
All other types, such as Irrational
, have special treatment. Explicit conversion to a String
is a simple way to avoid the remaining rules.
#? VERSION >= v"1.3.0-DEV"
@htl "<value>$(string(π))</value>"
#-> <value>π</value>
HTML Values
Since values translated by the @htl
macro are "text/html"
, they can be used in a nested manner, permitting us to build template functions.
sq(x) = @htl("<span>$(x*x)</span>")
@htl "<div>3^2 is $(sq(3))</div>"
#-> <div>3^2 is <span>9</span></div>
Values showable
as "text/html"
will bypass ampersand escaping.
@htl "<div>$(HTML("<span>unescaped 'literal'</span>"))</div>"
#-> <div><span>unescaped 'literal'</span></div>
Custom datatypes can provide their own representation by implementing show
for "text/html"
.
struct Showable data::String end
function Base.show(io::IO, mime::MIME"text/html", c::Showable)
value = replace(replace(c.data, "&"=>"&"), "<"=>"<")
print(io, "<showable>$(value)</showable>")
end
print(@htl "<span>$(Showable("a&b"))</span>")
#-> <span><showable>a&b</showable></span>
HypertextLiteral trusts that "text/html"
content is properly escaped.
Nothing
Within element content, nothing
is simply omitted.
@htl "<span>$nothing</span>"
#-> <span></span>
Use something()
to provide an alternative representation.
@htl "<span>$(something(nothing, "N/A"))</span>"
#-> <span>N/A</span>
This design supports template functions that return nothing
.
choice(x) = x ? @htl("<span>yes</span>") : nothing
@htl "<div>$(choice(true))$(choice(false))</div>"
#-> <div><span>yes</span></div>
Note that missing
has default treatment, see below.
Vectors & Tuples
Within element content, vector and tuple elements are concatenated (with no delimiter).
@htl "<tag>$([1,2,3])</tag>"
#-> <tag>123</tag>
@htl "<tag>$((1,2,3))</tag>"
#-> <tag>123</tag>
This interpretation enables nesting of templates.
books = ["Who Gets What & Why", "Switch", "Governing The Commons"]
@htl "<ul>$([@htl("<li>$b") for b in books])</ul>"
#=>
<ul><li>Who Gets What & Why<li>Switch<li>Governing The Commons</ul>
=#
The splat operator (...
) is supported as a noop.
@htl "$([x for x in 1:3]...)"
#-> 123
Generators are also treated in this manner.
print(@htl "<ul>$((@htl("<li>$b") for b in books))</ul>")
#=>
<ul><li>Who Gets What & Why<li>Switch<li>Governing The Commons</ul>
=#
The map(container) do item; … ;end
construct works and is performant.
@htl "<ul>$(map(books) do b @htl("<li>$b") end)</ul>"
#=>
<ul><li>Who Gets What & Why<li>Switch<li>Governing The Commons</ul>
=#
General Case
Within element content, values are wrapped in a <span>
tag.
@htl """<div>$missing</div>"""
#-> <div><span class="Base-Missing">missing</span></div>
This wrapping lets CSS style output. The following renders missing
as "N/A"
.
<style>
span.Base-Missing {visibility: collapse;}
span.Base-Missing::before {content: "N/A"; visibility: visible;}
</style>
The <span>
tag's class
attribute includes the module and type name.
using Dates
@htl "<div>$(Date("2021-07-28"))</div>"
#-> <div><span class="Dates-Date">2021-07-28</span></div>
This handwork is accomplished with a generated function when an object is not showable
as "text/html"
. If the datatype's module is Main
then it is not included in the class
.
struct Custom data::String; end
Base.print(io::IO, c::Custom) = print(io, c.data)
print(@htl "<div>$(Custom("a&b"))</div>")
#-> <div><span class="Custom">a&b</span></div>
Bypassing <span>
wrapping can be accomplished with string()
.
print(@htl "<div>$(string(Custom("a&b")))</div>")
#-> <div>a&b</div>
Extensions
Sometimes it's useful to extend @htl
so that it knows how to print your object without constructing this <span>
wrapper. This can be done by implementing a method of the content()
function.
struct Custom data::String end
HypertextLiteral.content(c::Custom) =
"They said: '$(c.data)'"
@htl "<div>$(Custom("Hello"))</div>"
#-> <div>They said: 'Hello'</div>
You can use @htl
to produce tagged content.
HypertextLiteral.content(c::Custom) =
@htl("<custom>$(c.data)</custom>")
@htl "<div>$(Custom("a&b"))</div>"
#-> <div><custom>a&b</custom></div>
With our primitives, you could have even more control. If your datatype builds its own tagged content, you can Bypass
ampersand escaping.
HypertextLiteral.content(c::Custom) =
HypertextLiteral.Bypass("<custom>$(c.data)</custom>")
@htl "<div>$(Custom("Hello"))</div>"
#-> <div><custom>Hello</custom></div>
Unfortunately, this won't escape the content of your custom object.
@htl "<div>$(Custom("<script>alert('whoops!);"))</div>"
#-> <div><custom><script>alert('whoops!);</custom></div>
The Reprint
primitive can help with composite templates.
using HypertextLiteral: Bypass, Reprint
HypertextLiteral.content(c::Custom) =
Reprint(io::IO -> begin
print(io, Bypass("<custom>"))
print(io, c.data)
print(io, Bypass("</custom>"))
end)
print(@htl "<div>$(Custom("a&b"))</div>")
#-> <div><custom>a&b</custom></div>
In fact, the @htl
macro produces exactly this translation.
HypertextLiteral.content(c::Custom) =
@htl("<custom>$(c.data)</custom>")
print(@htl "<div>$(Custom("a&b"))</div>")
#-> <div><custom>a&b</custom></div>
Tag Names
Interpolation works within tag names, both with symbols and strings.
tagname = "div"
@htl """<$tagname class=active></$tagname>"""
#-> <div class=active></div>
tagname = :div
@htl """<$tagname class=active></$tagname>"""
#-> <div class=active></div>
tagname = "htl-code-block"
@htl """<$tagname class=active></$tagname>"""
#-> <htl-code-block class=active></htl-code-block>
tagname = "my-web-component"
@htl """<$tagname/>"""
#-> <my-web-component/>
tagname = "open-file"
@htl """<icon-$tagname/>"""
#-> <icon-open-file/>
tagname = Symbol("open-file")
@htl """<icon-$tagname/>"""
#-> <icon-open-file/>
tagname = "code"
@htl """<htl-$tagname class=julia>import HypertextLiteral</htl-$tagname>"""
#-> <htl-code class=julia>import HypertextLiteral</htl-code>
prefix = "htl"
@htl """<$prefix-code class=julia>import HypertextLiteral</$prefix-code>"""
#-> <htl-code class=julia>import HypertextLiteral</htl-code>
Because with tags there isn't much fancy interpolation work we can do, you can't put in any complex object.
complex_prefix = Dict(:class => :julia)
@htl """<$complex_prefix>import HypertextLiteral</$complex_prefix>"""
#-> ERROR: "Can't use complex objects as tag name"
According to the HTML specification, only the first character has to be /[a-z]/i
, and the rest can be anything but /
, >
and (space). We are a bit more restrictive.
contains_space = "import HypertextLiteral"
@htl """<$contains_space></$contains_space>"""
#-> ERROR: "Content within a tag name can only contain latin letters, numbers or hyphens (`-`)"
contains_bigger_than = "a<div>"
@htl """<$contains_bigger_than></$contains_bigger_than>"""
#-> ERROR: "Content within a tag name can only contain latin letters, numbers or hyphens (`-`)"
contains_slash = "files/extra.js"
@htl """<$contains_slash></$contains_slash>"""
#-> ERROR: "Content within a tag name can only contain latin letters, numbers or hyphens (`-`)"
starts_with_hyphen = "-secret-tag-name"
@htl """<$starts_with_hyphen></$starts_with_hyphen>"""
#-> ERROR: "A tag name can only start with letters, not `-`"
empty = ""
@htl """<$empty></$empty>"""
#-> ERROR: "A tag name can not be empty"
empty = ""
@htl """<$empty/>"""
#-> ERROR: "A tag name can not be empty"
technically_valid_but_weird = "Technically⨝ValidTag™"
@htl """<$technically_valid_but_weird></$technically_valid_but_weird>"""
#-> ERROR: "Content within a tag name can only contain latin letters, numbers or hyphens (`-`)"
@htl """<$technically_valid_but_weird/>"""
#-> ERROR: "Content within a tag name can only contain latin letters, numbers or hyphens (`-`)"
technically_valid_starts_with_hyphen = "-secret-tag-name"
@htl """<prefix$technically_valid_starts_with_hyphen/>"""
#-> ERROR: "A tag name can only start with letters, not `-`"
technically_valid_but_empty = ""
@htl """<prefix-$technically_valid_but_empty/>"""
#-> ERROR: "A tag name can not be empty"
Edge Cases
Within element content, even though it isn't strictly necessary, we ampersand escape the single and double quotes.
v = "<'\"&"
@htl "<span>$v</span>"
#-> <span><'"&</span>
Symbols are likewise escaped.
v = Symbol("<'\"&")
@htl "<span>$v</span>"
#-> <span><'"&</span>
Interpolation within the xmp
, iframe
, noembed
, noframes
, and noscript
tags are not supported.
@htl "<iframe>$var</iframe>"
#=>
ERROR: LoadError: DomainError with iframe:
Only script and style rawtext tags are supported.⋮
=#
String escaping by @htl
is handled by Julia itself.
@htl "\"\t\\"
#-> " \
@htl "(\\\")"
#-> (\")
Literal content can contain Unicode values.
x = "Hello"
@htl "⁅$(x)⁆"
#-> ⁅Hello⁆
Escaped content may also contain Unicode.
x = "⁅Hello⁆"
@htl "<tag>$x</tag>"
#-> <tag>⁅Hello⁆</tag>
String interpolation is limited to symbols or parenthesized expressions (see Julia #37817).
@htl("$[1,2,3]")
#=>
ERROR: syntax: invalid interpolation syntax: "$["⋮
=#
@htl("$(1,2,3)")
#=>
ERROR: syntax: invalid interpolation syntax⋮
=#
Before v1.6, we cannot reliably detect string literals using the @htl
macro, so they are errors (when we can detect them).
#? VERSION < v"1.6.0-DEV"
@htl "Look, Ma, $("<i>automatic escaping</i>")!"
#-> ERROR: LoadError: "interpolated string literals are not supported"⋮
#? VERSION < v"1.6.0-DEV"
@htl "$("even if they are the only content")"
#-> ERROR: LoadError: "interpolated string literals are not supported"⋮
However, you can fix by wrapping a value in a string
function.
@htl "Look, Ma, $(string("<i>automatic escaping</i>"))!"
#-> Look, Ma, <i>automatic escaping</i>!
In particular, before v1.6, there are edge cases where unescaped string literal is undetectable and content can leak.
x = ""
#? VERSION < v"1.6.0-DEV"
@htl "$x$("<script>alert(\"Hello\")</script>")"
#-> <script>alert("Hello")</script>
Julia #38501 was fixed in v1.6.
#? VERSION >= v"1.6.0-DEV"
@htl "<tag>$("escape&me")</tag>"
#-> <tag>escape&me</tag>