Attributes & Style
Interpolation within single and double quoted attribute values are supported. Regardless of context, all four characters, <
, &
, '
, and "
are escaped.
using HypertextLiteral
qval = "\"&'"
@htl("""<tag double="$qval" single='$qval' />""")
#-> <tag double=""&'" single='"&'' />
Unquoted or bare attributes are also supported. These are serialized using the single quoted style so that spaces and other characters do not need to be escaped.
arg = "book='Strunk & White'"
@htl("<tag bare=$arg />")
#-> <tag bare='book='Strunk & White'' />
In this document, we discuss interpolation within attribute values.
Boolean Attributes
Within bare attributes, boolean values provide special support for boolean HTML properties, such as "disabled"
. When a value is false
, the attribute is removed. When the value is true
then the attribute is kept, with value being an empty string (''
).
@htl("<button disabled=$(true)>Disabled</button>")
#-> <button disabled=''>Disabled</button>
@htl("<button disabled=$(false)>Clickable</button>")
#-> <button>Clickable</button>
Within a quoted attribute, boolean values are printed as-is.
@htl("<input type='text' value='$(true)'>")
#-> <input type='text' value='true'>
@htl("<input type='text' value='$(false)'>")
#-> <input type='text' value='false'>
Nothing
Within bare attributes, nothing
is treated as false
, and the attribute is removed.
@htl("<button disabled=$(nothing)>Clickable</button>")
#-> <button>Clickable</button>
Within quoted attributes, nothing
is treated as the empty string.
@htl("<input type='text' value='$(nothing)'>")
#-> <input type='text' value=''>
This is designed for consistency with nothing
within element content.
Vectors
Vectors and tuples are flattened using the space as a separator.
class = ["text-center", "text-left"]
@htl("<div class=$class>...</div>")
#-> <div class='text-center text-left'>...</div>
@htl("<div class='$class'>...</div>")
#-> <div class='text-center text-left'>...</div>
@htl("<tag att=$([:one, [:two, "three"]])/>")
#-> <tag att='one two three'/>
@htl("<tag att='$((:one, (:two, "three")))'/>")
#-> <tag att='one two three'/>
This behavior supports attributes having name tokens, such as Cascading Style Sheets' "class"
.
Pairs & Dictionaries
Pairs, named tuples, and dictionaries are given treatment to support attributes such as CSS's "style"
.
style = Dict(:padding_left => "2em", :width => "20px")
@htl("<div style=$style>...</div>")
#-> <div style='padding-left: 2em; width: 20px;'>...</div>
@htl("<div style='font-size: 25px; $(:padding_left=>"2em")'/>")
#-> <div style='font-size: 25px; padding-left: 2em;'/>
@htl("<div style=$((padding_left="2em", width="20px"))/>")
#-> <div style='padding-left: 2em; width: 20px;'/>
For each pair, keys are separated from their value with a colon (:
). Adjacent pairs are delimited by the semi-colon (;
). Moreover, for Symbol
keys, snake_case
values are converted to kebab-case
.
General Case
Beyond these rules for booleans, nothing
, and collections, values are reproduced with their print
representation.
@htl("<div att=$((:a_symbol, "string", 42, 3.1415))/>")
#-> <div att='a_symbol string 42 3.1415'/>
This permits the serialization of all sorts of third party objects.
using Hyperscript
typeof(2em)
#-> Hyperscript.Unit{:em, Int64}
@htl "<div style=$((border=2em,))>...</div>"
#-> <div style='border: 2em;'>...</div>
Extensions
Often times the default print representation of a custom type isn't desirable for use inside an attribute value.
struct Custom data::String end
@htl "<tag att=$(Custom("A&B"))/>"
#-> <tag att='…Custom("A&B")'/>
This can be sometimes addressed by implementing Base.print()
.
Base.print(io::IO, c::Custom) = print(io, c.data)
print(@htl "<tag att=$(Custom("A&B"))/>")
#-> <tag att='A&B'/>
However, sometimes this isn't possible or desirable. A tailored representation specifically for use within an attribute_value
can be provided.
HypertextLiteral.attribute_value(x::Custom) = x.data
@htl "<tag att=$(Custom("A&B"))/>"
#-> <tag att='A&B'/>
Like content
extensions, Bypass
and Reprint
work identically.
Inside a Tag
Attributes may also be provided by any combination of dictionaries, named tuples, and pairs. Attribute names are normalized, where snake_case
becomes kebab-case
. We do not convert camelCase
due to XML (MathML and SVG) attribute case sensitivity. Moreover, String
attribute names are passed along as-is.
attributes = Dict(:data_style => :green, "data_value" => 42, )
@htl("<div $attributes/>")
#-> <div data-style='green' data_value='42'/>
@htl("<div $(:data_style=>:green) $(:dataValue=>42)/>")
#-> <div data-style='green' dataValue='42'/>
@htl("<div $((:data_style=>:green, "data_value"=>42))/>")
#-> <div data-style='green' data_value='42'/>
@htl("<div $((data_style=:green, dataValue=42))/>")
#-> <div data-style='green' dataValue='42'/>
A Pair
inside a tag is treated as an attribute.
@htl "<div $(:data_style => "green")/>"
#-> <div data-style='green'/>
A Symbol
or String
inside a tag is an empty attribute.
@htl "<div $(:data_style)/>"
#-> <div data-style=''/>
#? VERSION >= v"1.6.0-DEV"
@htl "<div $("data_style")/>"
#-> <div data_style=''/>
To expand an object into a set of attributes, implement inside_tag()
. For example, let's suppose we have an object that represents both a list of CSS classes and a custom style.
using HypertextLiteral: attribute_pair, Reprint
struct CustomCSS class::Vector{Symbol}; style end
HypertextLiteral.inside_tag(s::CustomCSS) = begin
myclass = join((string(x) for x in s.class), " ")
Reprint() do io::IO
print(io, attribute_pair(:class, myclass))
print(io, attribute_pair(:style, s.style))
end
end
style = CustomCSS([:one, :two], :background_color => "#92a8d1")
print(@htl "<div $style>Hello</div>")
#-> <div class='one two' style='background-color: #92a8d1;'>Hello</div>
Style Tag
Within a <style>
tag, Julia values are interpolated using the same rules as they would be if they were encountered within an attribute value, only that ampersand escaping is not done.
style = Dict(:padding_left => "2em", :width => "20px")
@htl """<style>span {$style}</style>"""
#-> <style>span {padding-left: 2em; width: 20px;}</style>
In this context, content is validated to ensure it doesn't contain "</style>"
.
expr = """<style>span {display: inline;}</style>"""
@htl "<style>$expr</style>"
#-> …ERROR: "Content within a style tag must not contain `</style>`"⋮
Edge Cases
Attribute names should be non-empty and not in a list of excluded characters.
@htl "<tag $("" => "value")/>"
#-> ERROR: LoadError: "Attribute name must not be empty."⋮
@htl "<tag $("&att" => "value")/>"
#=>
ERROR: LoadError: DomainError with &att:
Invalid character ('&') found within an attribute name.⋮
=#
We don't permit adjacent unquoted attribute values.
@htl("<tag bare=$(true)$(:invalid)")
#=>
ERROR: LoadError: DomainError with :invalid:
Unquoted attribute interpolation is limited to a single component⋮
=#
Unquoted interpolation adjacent to a raw string is also an error.
@htl("<tag bare=literal$(:invalid)")
#=>
ERROR: LoadError: DomainError with :invalid:
Unquoted attribute interpolation is limited to a single component⋮
=#
@htl("<tag bare=$(invalid)literal")
#=>
ERROR: LoadError: DomainError with bare=literal:
Unquoted attribute interpolation is limited to a single component⋮
=#
Ensure that dictionary style objects are serialized. See issue #7.
let
h = @htl("<div style=$(Dict("color" => "red"))>asdf</div>")
repr(MIME"text/html"(), h)
end
#-> "<div style='color: red;'>asdf</div>"
Let's ensure that attribute values in a dictionary are escaped.
@htl "<tag escaped=$(Dict(:esc=>"'&\"<"))/>"
#-> <tag escaped='esc: '&"<;'/>
When we normalize attribute names, we strip leading underscores.
@htl "<tag $(:__att => :value)/>"
#-> <tag att='value'/>
We don't expand into attributes things that don't look like attributes.
@htl "<tag $(3)/>"
#-> ERROR: MethodError: no method matching inside_tag(::Int64)⋮
One can add additional attributes following a bare name.
@htl "<tag bing $(:att)/>"
#-> <tag bing att=''/>
Inside a tag, tuples can have many kinds of pairs.
a1 = "a1"
@htl "<tag $((a1,:a2,:a3=3,a4=4))/>"
#-> <tag a1='' a2='' a3='3' a4='4'/>
The macro attempts to expand attributes inside a tag. To ensure the runtime dispatch also works, let's do a few things once indirect.
hello = "Hello"
defer(x) = x
@htl "<tag $(defer(:att => hello))/>"
#-> <tag att='Hello'/>
@htl "<tag $(defer((att=hello,)))/>"
#-> <tag att='Hello'/>
@htl "<tag $(:att => defer(hello))/>"
#-> <tag att='Hello'/>
@htl "<tag $(defer(:att) => hello)/>"
#-> <tag att='Hello'/>
It's a lexing error to have an attribute lacking a name.
@htl "<tag =value/>"
#=>
ERROR: LoadError: DomainError with =value/>:
unexpected equals sign before attribute name⋮
=#
It's a lexing error to have an attribute lacking a value.
@htl "<tag att=>"
#=>
ERROR: LoadError: DomainError with =>:
missing attribute value⋮
=#
Attribute names and values can be spaced out.
@htl "<tag one two = value />"
#-> <tag one two = value />
Invalid attribute names are reported.
@htl "<tag at<ribute='val'/>"
#=>
ERROR: LoadError: DomainError with t<ribute=…
unexpected character in attribute name⋮
=#
@htl "<tag at'ribute='val'/>"
#=>
ERROR: LoadError: DomainError with t'ribute=…
unexpected character in attribute name⋮
=#
@htl """<tag at"ribute='val'/>"""
#=>
ERROR: LoadError: DomainError with t"ribute=…
unexpected character in attribute name⋮
=#
While assignment operator is permitted in Julia string interpolation, we exclude it to guard it against accidently forgetting a comma.
@htl "<div $((data_value=42,))/>"
#-> <div data-value='42'/>
@htl("<div $((data_value=42))/>")
#=>
ERROR: LoadError: DomainError with data_value = 42:
assignments are not permitted in an interpolation⋮
=#
@htl("<div $(data_value=42)/>")
#=>
ERROR: LoadError: DomainError with data_value = 42:
assignments are not permitted in an interpolation⋮
=#
Interpolation of adjacent values should work.
x = 'X'; y = 'Y';
@htl("<span att='$x$y'/>")
#-> <span att='XY'/>