# 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("""""") #-> 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("") #-> 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("") #-> @htl("") #-> Within a quoted attribute, boolean values are printed as-is. @htl("") #-> @htl("") #-> ## Nothing Within bare attributes, `nothing` is treated as `false`, and the attribute is removed. @htl("") #-> Within quoted attributes, `nothing` is treated as the empty string. @htl("") #-> 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("
...
") #->
...
@htl("
...
") #->
...
@htl("") #-> @htl("") #-> 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("
...
") #->
...
@htl("
") #->
@htl("
") #->
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("
") #->
This permits the serialization of all sorts of third party objects. using Hyperscript typeof(2em) #-> Hyperscript.Unit{:em, Int64} @htl "
...
" #->
...
## 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 "" #-> This can be sometimes addressed by implementing `Base.print()`. Base.print(io::IO, c::Custom) = print(io, c.data) print(@htl "") #-> 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 "" #-> 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("
") #->
@htl("
:green) $(:dataValue=>42)/>") #->
@htl("
:green, "data_value"=>42))/>") #->
@htl("
") #->
A `Pair` inside a tag is treated as an attribute. @htl "
"green")/>" #->
A `Symbol` or `String` inside a tag is an empty attribute. @htl "
" #->
#? VERSION >= v"1.6.0-DEV" @htl "
" #->
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 "
Hello
") #->
Hello
## Style Tag Within a `""" #-> In this context, content is validated to ensure it doesn't contain `""`. expr = """""" @htl "" #-> …ERROR: "Content within a style tag must not contain ``"⋮ ## Edge Cases Attribute names should be non-empty and not in a list of excluded characters. @htl " "value")/>" #-> ERROR: LoadError: "Attribute name must not be empty."⋮ @htl " "value")/>" #=> ERROR: LoadError: DomainError with &att: Invalid character ('&') found within an attribute name.⋮ =# We don't permit adjacent unquoted attribute values. @htl(" 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(" ERROR: LoadError: DomainError with :invalid: Unquoted attribute interpolation is limited to a single component⋮ =# @htl(" 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("
"red"))>asdf
") repr(MIME"text/html"(), h) end #-> "
asdf
" Let's ensure that attribute values in a dictionary are escaped. @htl ""'&\"<"))/>" #-> When we normalize attribute names, we strip leading underscores. @htl " :value)/>" #-> We don't expand into attributes things that don't look like attributes. @htl "" #-> ERROR: MethodError: no method matching inside_tag(::Int64)⋮ One can add additional attributes following a bare name. @htl "" #-> Inside a tag, tuples can have many kinds of pairs. a1 = "a1" @htl "" #-> 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 " hello))/>" #-> @htl "" #-> @htl " defer(hello))/>" #-> @htl " hello)/>" #-> It's a lexing error to have an attribute lacking a name. @htl "" #=> ERROR: LoadError: DomainError with =value/>: unexpected equals sign before attribute name⋮ =# It's a lexing error to have an attribute lacking a value. @htl "" #=> ERROR: LoadError: DomainError with =>: missing attribute value⋮ =# Attribute names and values can be spaced out. @htl "" #-> Invalid attribute names are reported. @htl "" #=> ERROR: LoadError: DomainError with t" #=> ERROR: LoadError: DomainError with t'ribute=… unexpected character in attribute name⋮ =# @htl """""" #=> 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 "
" #->
@htl("
") #=> ERROR: LoadError: DomainError with data_value = 42: assignments are not permitted in an interpolation⋮ =# @htl("
") #=> 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("") #->