diff --git a/LICENSE.md b/LICENSE.md
index f54095b..1fba60f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The Hyperscript.jl package is licensed under the MIT "Expat" License:
-> Copyright (c) 2017: Yuri Vishnevsky.
+> Copyright (c) 2018: Yuri Vishnevsky.
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
diff --git a/Project.toml b/Project.toml
new file mode 100644
index 0000000..31ed41f
--- /dev/null
+++ b/Project.toml
@@ -0,0 +1,10 @@
+name = "Hyperscript"
+uuid = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91"
+author = ["Yuri Vishnevsky "]
+version = "0.0.3"
+
+[compat]
+julia = "1"
+
+[deps]
+Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
diff --git a/README.md b/README.md
index 87f039a..bf6027c 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,27 @@
# Hyperscript
+Hyperscript is a package for working with HTML, SVG, and CSS in Julia.
-Hyperscript is a Julia package for representing HTML and SVG expressions using native Julia syntax.
+When using this library you automatically get:
-```
-Pkg.clone("https://github.com/yurivish/Hyperscript.jl")
-using Hyperscript
-```
+* A concise DSL for writing HTML, SVG, and CSS.
+* Flexible ways to combine DOM pieces together into larger components.
+* Safe and automatic HTML-escaping.
+* Lightweight and optional support for scoped CSS.
+* Lightweight and optional support for CSS unit arithmetic.
+
+## Usage
Hyperscript introduces the `m` function for creating markup nodes:
-```
+```julia
m("div", class="entry",
m("h1", "An Important Announcement"))
```
-Nodes are validated as they are created. Hyperscript checks for valid tag names, and tag-attribute pairs:
-
-```
-m("snoopy") # ERROR: snoopy is not a valid HTML or SVG tag
-m("div", mood="facetious") # ERROR: mood is not a valid attribute name
-```
-
Nodes can be used as a templates:
-```
+```julia
const div = m("div")
const h1 = m("h1")
div(class="entry", h1("An Important Announcement"))
@@ -32,7 +29,7 @@ div(class="entry", h1("An Important Announcement"))
Dot syntax is supported for setting class attributes:
-```
+```julia
const div = m("div")
const h1 = m("h1")
div.entry(h1("An Important Announcement"))
@@ -40,14 +37,13 @@ div.entry(h1("An Important Announcement"))
Chained dot calls turn into multiple classes:
-```
+```julia
m("div").header.entry
```
-
The convenience macro `@tags` can be used to quickly declare common tags:
-```
+```julia
@tags div h1
const entry = div.entry
entry(h1("An Important Announcement"))
@@ -55,29 +51,172 @@ entry(h1("An Important Announcement"))
Arrays, tuples, and generators are recursively flattened, linearizing nested structures for display:
-```
+```julia
@tags div h1
const entry = div.entry
-div(entry.(["$n Fast $n Furious" for n in 1:10])) # this joke is © Glen Chiacchieri
+div(entry.(["$n Fast $n Furious" for n in 1:10])) # joke © Glen Chiacchieri
```
-Some attribute names, such as those with hyphens, can't be written as Julia identifiers. For those you can use either camelCase or squishcase and Hyperscript will convert them for you:
+Attribute names with hyphens can be written using camelCase:
-```
-# These are both valid:
+```julia
m("meta", httpEquiv="refresh")
-m("meta", httpequiv="refresh")
+# turns into
+```
+
+For attributes that are _meant_ to be camelCase, Hyperscript still does the right thing:
+
+```julia
+m("svg", viewBox="0 0 100 100")
+# turns into
+```
+
+Attribute names that happen to be Julia keywords can be specified with `:attr => value` syntax:
+
+```julia
+m("input"; :type => "text")
+# turns into
+```
+
+Hyperscript automatically HTML-escapes children of DOM nodes:
+
+```julia
+m("p", "I am a paragraph with a < inside it")
+# turns into I am a paragraph with a < inside it
+```
+
+You can disable escaping using `@tags_noescape` for writing an inline style or script:
+
+```julia
+@tags_noescape script
+script("console.log('<(0_0<) <(0_0)> (>0_0)> KIRBY DANCE')")
+```
+
+Nodes can be printed compactly with `print` or `show`, or pretty-printed by wrapping a node in `Pretty`:
+
+```julia
+node = m("div", class="entry", m("h1", "An Important Announcement"))
+
+print(node)
+#
An Important Announcement
+
+print(Pretty(node))
+#
+#
An Important Announcement
+#
+```
+
+Note that the extra white space can affect layout, particularly in conjunction with CSS properties like [white-space](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space).
+
+## CSS
+
+In addition to HTML and SVG, Hyperscript also supports CSS:
+
+```julia
+css(".entry", fontSize="14px")
+# turns into .entry { font-size: 14px; }
+```
+
+CSS nodes can be nested inside each other:
+
+```julia
+css(".entry",
+ fontSize="14px",
+ css("h1", textDecoration="underline"),
+ css("> p", color="#999"))
+# turns into
+# .entry { font-size: 14px; }
+# .entry h1 { text-decoration: underline; }
+# .entry > p { color: #999; }
```
-If you'd like to turn off validation you should use `m_novalidate`, which is just like `m` except that it doesn't validate or perform attribute conversion:
+`@media` queries are also supported:
+```julia
+css("@media (min-width: 1024px)",
+ css("p", color="red"))
+# turns into
+# @media (min-width: 1024px) {
+# p { color: red; }
+# }
```
-import Hyperscript # Note import, not using
-const m = Hyperscript.m_novalidate
-m("snoopy") #
-m("div", mood="facetious") #
+## Scoped Styles
+
+Hyperscript supports scoped styles. They are implemented by adding unique attributes to nodes and selecting them via [attribute selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors):
+
+```julia
+@tags p
+@tags_noescape style
+
+# Create a scoped `Style` object
+s1 = Style(css("p", fontWeight="bold"), css("span", color="red"))
+
+# Apply the style to a DOM node
+s1(p("hello"))
+# turns into hello
+
+# Insert the corresponding styles into a
+
```
+Scoped styles are scoped to the DOM subtree where they are applied. Styled nodes function as cascade barriers — parent styles do not leak into styled child nodes:
+
+```julia
+# Create a second scoped style
+s2 = Style(css("p", color="blue"))
+
+# Apply `s1` to the parent and `s2` to a child.
+# Note the `s1` style does not apply to the child styled with `s2`.
+s1(p(p("outer"), s2(p("inner"))))
+# turns into
+#
+#
outer
+# inner
+#
+
+style(styles(s1), styles(s2))
+# turns into
+#
+```
+
+## CSS Units
+
+Hyperscript supports a concise syntax for CSS unit arithmetic:
+
+```julia
+using Hyperscript
+
+css(".foo", width=50px)
+# turns into .foo {width: 50px;}
+
+css(".foo", width=50px + 2 * 100px)
+# turns into .foo {width: 250px;}
+
+css(".foo", width=(50px + 50px) + 2em)
+# turns into .foo {width: calc(100px + 2em);}
+```
+
+Supported units are `px`, `pt`, `em`,`vh`, `vw`, `vmin`, `vmax`, and `pc` for percent.
+
+---
+
+I'd like to create a more comprehensive guide to the full functionality available in Hyperscript at some point. For now here's a list of some of the finer points:
-To validate more stringently against _just_ HTML or _just_ SVG, you can similarly use `Hyperscript.m_html` or `Hyperscript.m_svg`.
+* Nodes are immutable — any derivation of new nodes from existing nodes will leave existing nodes unchanged.
+* Calling an existing node with with more children creates a new node with the new children appended.
+* Calling an existing node with more attributes creates a new node whose attributes are the `merge` of the existing and new attributes.
+* `div.fooBar` adds the CSS class `foo-bar`. To add the camelCase class `fooBar` you can use the dot syntax with a string: `div."fooBar"`
+* The dot syntax always _adds_ to the CSS class. This is why chaining (`div.foo.bar.baz`) adds all three classes in sequence.
+* Tags defined with `@tags_noescape` only "noescape" one level deep. Children of children will still be escaped according to their own rules.
+* Using `nothing` as the value of a DOM attribute creates a valueless attribute, e.g. ` `.
diff --git a/REQUIRE b/REQUIRE
deleted file mode 100644
index 859ad46..0000000
--- a/REQUIRE
+++ /dev/null
@@ -1 +0,0 @@
-julia 0.7
diff --git a/src/Hyperscript.jl b/src/Hyperscript.jl
index eec51ef..10ca312 100644
--- a/src/Hyperscript.jl
+++ b/src/Hyperscript.jl
@@ -1,137 +1,145 @@
-__precompile__()
+#=
+ High-level Overview
-module Hyperscript
+ Hyperscript is a package for representing and rendering HTML and SVG DOM trees.
-export m, @tags
+ The primary abstraction is a `Node` — with a tag, attributes, and children.
-include("constants.jl")
+ Each `Node` also has a `Context` associated with it which defines the way in which
+ the basic Hyperscript functions operate on that node.
-# To reduce redundancy, we create some constant values here
-const COMBINED_ATTRS = merge(unique∘vcat, SVG_ATTRS, HTML_ATTRS)
-const COMBINED_TAGS = union(SVG_TAGS, HTML_TAGS)
-const COMBINED_ATTR_NAMES = merge(HTML_ATTR_NAMES, SVG_ATTR_NAMES)
+ The basic pipeline is as follows:
-# NOTE: This is still the self-closing-tag list when validation is turned off.
-# How should this work instead?
-isvoid(tag) = tag in COMBINED_VOID_TAGS
+ A `Node` is created with some tag, attributes, and children.
-## Validation types and implementation
+ The tag, attributes, and children are normalized and then validated, using the
+ functions [normalize|validate][tag|attr|child] which can dispatch on context to
+ handle different node types according to their needs.
-"""
-Validations provide checks against common typos and mistakes. They are non-exhaustive
-and enforce valid HTML/SVG tag names as well as enforcing that the passed attributes
-are allowed on the given tag, e.g. you can use `cx` but not `x` on a ` `.
+ Normalization is the conversion to a canonical form (e.g. kebab- or camel-casing) and
+ validation is checking invariants and throwing an error if they are not met (e.g. no
+ spaces in attribute names).
-Validations other than `NoValidate` also enforce that numeric attribute values are non-`NaN`.
+ Each of these functions takes a `ctx` argument, allowing the context of a node to fully
+ dictate the terms of normalization, validation, escapining, and rendering.
-The specific tag and attribute names depend on the chosen validation. The default is
-*combined* validation via `ValidateCombined`, which liberally accepts any mix of valid
-HTML/SVG tags and attributes.
+ Nodes handle escaping of their contents upon render using functions named
+ escape[tag|attrname|attrvalue|child].
-Note that `ValidateCombined` does not enforce inter-attribute consistency. If you use a tag
-that belongs to both SVG and HTML, it will accept any mix of HTML and SVG attributes for
-that tag even when the valid attributes for that HTML tag and SVG tag differ.
+ The full pipeline over the lifecycle of a `Node` involves all of these functions
+ in order: normalize => validate => escape upon render.
-These are all of the tags shared between HTML and SVG:
+ ===
-$HTML_SVG_TAG_INTERSECTION_MD
-"""
+ Node extensibility points
-## Validation
+ Note that these defaults are not defined — it makes more sense to fully define the relevant
+ behaviors independently for each context. The values below are just 'no-op' examples.
-abstract type Validation end
+ # Return the normalized property value
+ normalizetag(ctx, tag) = tag
+ normalizeattr(ctx, tag, attr) = attr
+ normalizechild(ctx, tag, child) = child
-# The nans parameter indicates whether to error on
-# NaN attribute values. While this is not strictly
-# against any spec, it is almost never what you want.
-struct Validate{nans} <: Validation
- name::String
- tags::Set{String}
- tag_to_attrs::Dict{String, Vector{String}}
- sym_to_name::Dict{Symbol, String}
-end
+ # Return the property value or throw a validation error
+ validatetag(ctx, tag) = tag
+ validateattr(ctx, tag, attr) = attr
+ validatechild(ctx, tag, child) = child
-"Does not validate input tag names, attribute names, or attribute values."
-struct NoValidate <: Validation
-end
+ # Return a Dict{Char, String} specifying escape replacements
+ escapetag(ctx) = NO_ESCAPES
+ escapeattrname(ctx) = NO_ESCAPES
+ escapeattrvalue(ctx) = NO_ESCAPES
+ escapechild(ctx) = NO_ESCAPES
+=#
-"Validates generously against the combination of HTML and SVG."
-const VALIDATE_COMBINED = Validate{true}("HTML or SVG", COMBINED_TAGS, COMBINED_ATTRS, COMBINED_ATTR_NAMES)
+module Hyperscript
-"Validates generously against the combination of SVG 1.1, SVG Tiny 1.2, and SVG 2."
-const VALIDATE_SVG = Validate{true}("SVG", SVG_TAGS, SVG_ATTRS, SVG_ATTR_NAMES)
+export @tags, @tags_noescape, m, css, Style, styles, render, Pretty, savehtml, savesvg
-"Validates generously against the combination of HTML 4, W3C HTML 5, and WHATWG HTML 5."
-const VALIDATE_HTML = Validate{true}("HTML", HTML_TAGS, HTML_ATTRS, HTML_ATTR_NAMES)
+# Units
+include(joinpath(@__DIR__, "cssunits.jl"))
+export px, pt, em, #= (this one conflicts with Base) rem, =# vh, vw, vmin, vmax, pc
-function validatetag(v::Validate, tag)
- tag ∈ v.tags || error("$tag is not a valid $(v.name) tag")
- tag
-end
-validatetag(v::NoValidate, tag) = tag
+## Basic definitions
-function validatevalue(v::Validate{true}, tag, attr, value::Number)
- isnan(value) && error("A NaN value was passed to an attribute: $(stringify(tag, attr, value))")
- value
-end
-validatevalue(v, tag, attr, value) = value
+abstract type Context end
-function validateattrs(v::Validate, tag, nt::NamedTuple)
- attrs = Dict{String, Any}()
- for (sym, value) in pairs(nt)
- attr = get(v.sym_to_name, sym) do
- error("$(string(sym)) is not a valid attribute name: $(stringify(tag, sym, value))")
- end
- validatevalue(v, tag, attr, value)
- valid = attr ∈ v.tag_to_attrs[tag] || attr ∈ v.tag_to_attrs["*"]
- valid || error("$attr is not a valid attribute name for $tag tags")
- attrs[attr] = value
- end
- attrs
+struct CSS <: Context
+ allow_nan_attr_values::Bool
end
-function validateattrs(v::NoValidate, tag, nt::NamedTuple)
- Dict{String, Any}(string(sym) => value for (sym, value) in pairs(nt))
+struct HTMLSVG <: Context
+ allow_nan_attr_values::Bool
+ noescape::Bool
end
-# Nice printing in errors
-stringify(tag) = string("<", tag, isvoid(tag) ? " />" : ">")
-stringify(tag, attr, value) = string("<", tag, " ", attr, "=\"", value, "\"", isvoid(tag) ? " />" : ">")
-
-## Node representation and generation
+abstract type AbstractNode{T} end
-struct Node{V<:Validation}
+struct Node{T<:Context} <: AbstractNode{T}
+ context::T
tag::String
- attrs::Dict{String, Any}
children::Vector{Any}
- validation::V
+ attrs::Dict{String, Any}
+end
+
+function Node(ctx::T, tag::AbstractString, children, attrs) where T <: Context
+ tag = validatetag(ctx, normalizetag(ctx, tag))
+ Node{T}(
+ ctx,
+ tag,
+ processchildren(ctx, tag, children),
+ processattrs(ctx, tag, attrs)
+ )
end
-function Node(v::V, tag, children, attrs) where {V <: Validation}
- Node{V}(validatetag(v, tag), validateattrs(v, tag, attrs), flat(children), v)
+function (node::Node{T})(cs...; as...) where T
+ ctx = context(node)
+ Node{T}(
+ ctx,
+ tag(node),
+ isempty(cs) ? children(node) : prepend!(processchildren(ctx, tag(node), cs), children(node)),
+ isempty(as) ? attrs(node) : merge(attrs(node), processattrs(ctx, tag(node), as))
+ )
end
-tag(x::Node) = Base.getfield(x, :tag)
-attrs(x::Node) = Base.getfield(x, :attrs)
+tag(x::Node) = Base.getfield(x, :tag)
+attrs(x::Node) = Base.getfield(x, :attrs)
children(x::Node) = Base.getfield(x, :children)
-validation(x::Node) = Base.getfield(x, :validation)
+context(x::Node) = Base.getfield(x, :context)
-# Allow extending a node using function application syntax.
-# Overrides attributes and appends children.
-function (node::Node{V})(cs...; as...) where {V <: Validation}
- Node{V}(
- tag(node),
- isempty(as) ? attrs(node) : merge(attrs(node), validateattrs(validation(node), tag(node), as)),
- isempty(cs) ? children(node) : prepend!(flat(cs), children(node)),
- validation(node)
+# Experimental concise node specification support
+function Base.typed_hvcat(node::AbstractNode, rows::Tuple{Vararg{Int64}}, xs::Any...)
+ node(xs...)
+end
+Base.typed_hcat(node::AbstractNode, xs::Any...) = node(xs...)
+Base.typed_vcat(node::AbstractNode, xs::Any...) = node(xs...)
+Base.getindex(node::Union{AbstractNode, Type{<:AbstractNode}}, xs::Any...) = node(xs...)
+
+function Base.:(==)(x::Node, y::Node)
+ context(x) == context(y) && tag(x) == tag(y) &&
+ children(x) == children(y) && attrs(x) == attrs(y)
+end
+
+## Node utils
+
+function processchildren(ctx, tag, children)
+ # Any[] for type-stability Node construction (children::Vector{Any})
+ Any[validatechild(ctx, tag, normalizechild(ctx, tag, child)) for child in flat(children)]
+end
+
+# A single attribute is allowed to normalize to multiple attributes,
+# for example when normalizing CSS attribute names into vendor-prefixed versions.
+function processattrs(ctx, tag, attrs)
+ Dict{String, Any}(
+ validateattr(ctx, tag, attr′)
+ for attr in attrs
+ for attr′ in flat(normalizeattr(ctx, tag, attr))
)
end
-# Recursively flatten generators, tuples, and arrays.
-# Wraps scalars in a single-element tuple.
-# Note: We could do something trait-based, so custom lazy collections can opt into compatibility
function flat(xs::Union{Base.Generator, Tuple, Array})
- out = []
+ out = Any[] # Vector{Any} for node children and attribute values
for x in xs
append!(out, flat(x))
end
@@ -139,58 +147,202 @@ function flat(xs::Union{Base.Generator, Tuple, Array})
end
flat(x) = (x,)
-# Allow concise class attribute specification.
-# Classes specified this way will append to an existing class if present.
-function Base.getproperty(x::Node, class::Symbol)
- a = attrs(x)
- x(class=haskey(a, "class") ? string(a["class"], " ", class) : string(class))
+## Rendering
+
+struct RenderContext
+ pretty::Bool
+ indent::String
+ level::Int
end
+const DEFAULT_RCTX = RenderContext(false, " ", 0)
"""
-`m(tag, children...; attrs)`
+Wrapper struct for pretty-printing `Node`s: `Pretty(node)`.
+Line feeds are added along with indentation controlled by `indent`.
+"""
+struct Pretty{T <: AbstractNode}
+ node::T
+ rctx::RenderContext
+ Pretty(node::T; indent=" ") where {T} = new{T}(node, RenderContext(true, indent, 0))
+ Pretty(node::T, rctx::RenderContext) where {T} = new{T}(node, rctx)
+end
-Create a hypertext node with the specified attributes and children. `m` performs
-validation against SVG and HTML tags and attributes; use `m_svg`, `m_html` to
-validate against just SVG or HTML, or use `m_novalidate` to prevent validation
-entirely.
+# Top-level nodes render in their own node context.
+render(io::IO, rctx::RenderContext, x::Node) = render(io, rctx, context(x), x)
+render(io::IO, x::Pretty) = render(io, x.rctx, x.node)
+render(io::IO, x::Node) = render(io, DEFAULT_RCTX, x)
+render(x::AbstractNode) = sprint(render, x)
-The following import pattern is useful for convenient access to your choice
-of validation style:
+Base.show(io::IO, node::Union{Node, Pretty}) = render(io, node)
+Base.show(io::IO, m::MIME"text/html", node::Union{Node, Pretty}) = render(io, node)
-```julia
-import Hyperscript
-const m = Hyperscript.m_svg
-```
+printescaped(io::IO, x::AbstractString, escapes) = for c in x
+ print(io, get(escapes, c, c))
+end
-The `children` can be any Julia values, including other `Node`s creates by `m`.
-Tuples, arrays, and generators will be recursively flattened.
+printescaped(io::IO, x::AbstractChar, escapes) = printescaped(io, string(x), escapes)
-Since attribute names are passed as Julia symbols `m(attrname=value)`, Hyperscript
-accepts both Julia-style (lowercase) and JSX-like (camelCase) attributes:
+# todo: turn the above into something like an escaping IO pipe to avoid string
+# allocation via sprint. future use: sprint(printescaped, x, escapes))
+printescaped(io::IO, x, escapes) = printescaped(io, sprint(print, x), escapes)
-`acceptCharset` turns into the HTML attribute `accept-charset`, as does `acceptcharset`.
-"""
-m(v::Validation, tag, children...; attrs...) = Node(v, tag, children, attrs)
-m(tag, children...; attrs...) = Node(VALIDATE_COMBINED, tag, children, attrs)
-m_svg(tag, children...; attrs...) = Node(VALIDATE_SVG, tag, children, attrs)
-m_html(tag, children...; attrs...) = Node(VALIDATE_HTML, tag, children, attrs)
-m_novalidate(tag, children...; attrs...) = Node(NoValidate(), tag, children, attrs)
+# pass numbers through untrammelled
+kebab(camel::String) = join(islowercase(c) || isnumeric(c) || c == '-' ? c : '-' * lowercase(c) for c in camel)
-"""
-Macro for concisely declaring a number of tags in global scope.
-`@tags h1 h2 span` expands into
+## HTMLSVG
-```
-const h1 = m("h1")
-const h2 = m("h2")
-const span = m("span")
-```
+function render(io::IO, rctx::RenderContext, ctx::HTMLSVG, node::Node{HTMLSVG})
+ etag = escapetag(ctx)
+ eattrname = escapeattrname(ctx)
+ eattrvalue = escapeattrvalue(ctx)
+ if rctx.pretty && rctx.level > 0
+ print(io, "\n", rctx.indent ^ rctx.level, "<")
+ else
+ print(io, "<")
+ end
+ printescaped(io, tag(node), etag)
+ for (name, value) in pairs(attrs(node))
+ print(io, " ")
+ printescaped(io, name, eattrname)
+ if value != nothing
+ print(io, "=\"")
+ printescaped(io, value, eattrvalue)
+ print(io, "\"")
+ end
+ end
-The `const` declaration precludes this macro from being used in
-non-global scopes (e.g. inside a function) since const is disallowed
-on local variables. It is present for performance.
-"""
+ if isvoid(tag(node))
+ @assert isempty(children(node))
+ print(io, " />")
+ else
+ print(io, ">")
+ for child in children(node)
+ renderdomchild(io, RenderContext(rctx.pretty, rctx.indent, rctx.level + 1), ctx, child)
+ end
+ if rctx.pretty && any(x -> isa(x, AbstractNode), children(node))
+ print(io, "\n", rctx.indent ^ rctx.level, "")
+ else
+ print(io, "")
+ end
+ printescaped(io, tag(node), etag)
+ print(io, ">")
+ end
+end
+
+const VOID_TAGS = Set([
+ "track", "hr", "col", "embed", "br", "circle", "input", "base",
+ "use", "source", "polyline", "param", "ellipse", "link", "img",
+ "path", "wbr", "line", "stop", "rect", "area", "meta", "polygon"
+])
+isvoid(tag) = tag ∈ VOID_TAGS
+
+# Rendering HTMLSVG child nodes in their own context
+renderdomchild(io, rctx::RenderContext, ctx::HTMLSVG, node::AbstractNode{HTMLSVG}) = render(io, rctx, node)
+
+# Do nothing for `nothing`; this is similar to using `nothing` as an attribute value for valueless attributes.
+renderdomchild(io, rctx::RenderContext, ctx, x::Nothing) = nothing
+
+# Render and escape other HTMLSVG children, including CSS nodes, in the parent context.
+# If a child is `showable` with text/html, render with that using `repr`.
+renderdomchild(io, rctx::RenderContext, ctx, x) =
+ showable(MIME("text/html"), x) ? print(io, repr(MIME("text/html"), x)) : printescaped(io, x, escapechild(ctx))
+
+# All camelCase attribute names from HTML 4, HTML 5, SVG 1.1, SVG Tiny 1.2, and SVG 2
+const HTML_SVG_CAMELS = Dict(lowercase(x) => x for x in [
+ "preserveAspectRatio", "requiredExtensions", "systemLanguage",
+ "externalResourcesRequired", "attributeName", "attributeType", "calcMode",
+ "keySplines", "keyTimes", "repeatCount", "repeatDur", "requiredFeatures",
+ "requiredFonts", "requiredFormats", "baseFrequency", "numOctaves", "stitchTiles",
+ "focusHighlight", "lengthAdjust", "textLength", "glyphRef", "gradientTransform",
+ "gradientUnits", "spreadMethod", "tableValues", "pathLength", "clipPathUnits",
+ "stdDeviation", "viewBox", "viewTarget", "zoomAndPan", "initialVisibility",
+ "syncBehavior", "syncMaster", "syncTolerance", "transformBehavior", "keyPoints",
+ "defaultAction", "startOffset", "mediaCharacterEncoding", "mediaContentEncodings",
+ "mediaSize", "mediaTime", "maskContentUnits", "maskUnits", "baseProfile",
+ "contentScriptType", "contentStyleType", "playbackOrder", "snapshotTime",
+ "syncBehaviorDefault", "syncToleranceDefault", "timelineBegin", "edgeMode",
+ "kernelMatrix", "kernelUnitLength", "preserveAlpha", "targetX", "targetY",
+ "patternContentUnits", "patternTransform", "patternUnits", "xChannelSelector",
+ "yChannelSelector", "diffuseConstant", "surfaceScale", "refX", "refY",
+ "markerHeight", "markerUnits", "markerWidth", "filterRes", "filterUnits",
+ "primitiveUnits", "specularConstant", "specularExponent", "limitingConeAngle",
+ "pointsAtX", "pointsAtY", "pointsAtZ", "hatchContentUnits", "hatchUnits"])
+
+normalizetag(ctx::HTMLSVG, tag) = strip(tag)
+
+# The simplest normalization — kebab-case and don't pay attention to the tag.
+# Allows both squishcase and camelCase for the attributes above.
+# If the attribute name is a string and not a Symbol (using the Node constructor),
+# then no normalization is performed — this way you can pass any attribute you'd like.
+function normalizeattr(ctx::HTMLSVG, tag, (name, value)::Pair{Symbol, <:Any})
+ name = string(name)
+ get(() -> kebab(name), HTML_SVG_CAMELS, lowercase(name)) => value
+end
+
+function normalizeattr(ctx::HTMLSVG, tag, attr::Pair{<:AbstractString, <:Any})
+ # Note: This must implementation must change if we begin to normalize attr values above.
+ # Right now we only normalize attr names.
+ attr
+end
+
+normalizechild(ctx::HTMLSVG, tag, child) = child
+
+# Nice printing in errors
+stringify(ctx::HTMLSVG, tag, attr::String=" ") = "<$tag>$attr $(isvoid(tag) ? " />" : ">")"
+stringify(ctx::HTMLSVG, tag, (name, value)::Pair) = stringify(ctx, tag, " $name=$value")
+
+function validatetag(ctx::CSS, tag)
+ isempty(tag) && error("Tag cannot be empty.")
+ tag
+end
+
+function validateattr(ctx::HTMLSVG, tag, attr)
+ (name, value) = attr
+ if !ctx.allow_nan_attr_values && typeof(value) <: AbstractFloat && isnan(value)
+ error("NaN values are not allowed for HTML or SVG nodes: $(stringify(ctx, tag, attr))")
+ end
+ if any(isspace, name)
+ error("Spaces are not allowed in HTML or SVG attribute names: $(stringify(ctx, tag, attr))")
+ end
+ attr
+end
+
+function validatechild(ctx::HTMLSVG, tag, child)
+ if isvoid(tag)
+ error("Void tags are not allowed to have children: $(stringify(ctx, tag))")
+ end
+ child
+end
+
+# Creates an HTML or SVG escaping dictionary
+chardict(chars) = Dict(c => "$(Int(c));" for c in chars)
+
+# See: https://stackoverflow.com/questions/7753448/how-do-i-escape-quotes-in-html-attribute-values
+const ATTR_VALUE_ESCAPES = chardict("&<>\"\n\r\t")
+
+# See: https://stackoverflow.com/a/9189067/1175713
+const HTML_ESCAPES = chardict("&<>\"'`!@\$%()=+{}[]")
+
+# Used for CSS nodes, as well as children of tag nodes defined with @tags_noescape
+const NO_ESCAPES = Dict{Char, String}()
+
+escapetag(ctx::HTMLSVG) = HTML_ESCAPES
+escapeattrname(ctx::HTMLSVG) = HTML_ESCAPES
+escapeattrvalue(ctx::HTMLSVG) = ATTR_VALUE_ESCAPES
+escapechild(ctx::HTMLSVG) = ctx.noescape ? NO_ESCAPES : HTML_ESCAPES
+
+# Concise CSS class shorthand
+addclass(attrs, class) = haskey(attrs, "class") ? string(attrs["class"], " ", class) : class
+Base.getproperty(x::Node{HTMLSVG}, class::Symbol) = x(class=addclass(attrs(x), kebab(String(class))))
+Base.getproperty(x::Node{HTMLSVG}, class::String) = x(class=addclass(attrs(x), class))
+
+const DEFAULT_HTMLSVG_CONTEXT = HTMLSVG(false, false)
+const NOESCAPE_HTMLSVG_CONTEXT = HTMLSVG(false, true)
+m(tag::AbstractString, cs...; as...) = Node(DEFAULT_HTMLSVG_CONTEXT, tag, cs, as)
+m(ctx::Context, tag::AbstractString, cs...; as...) = Node(ctx, tag, cs, as)
+
+# HTML/SVG tags macros
macro tags(args::Symbol...)
blk = Expr(:block)
for tag in args
@@ -202,47 +354,189 @@ macro tags(args::Symbol...)
blk
end
-## Markup generation
+macro tags_noescape(args::Symbol...)
+ blk = Expr(:block)
+ for tag in args
+ push!(blk.args, quote
+ const $(esc(tag)) = m(NOESCAPE_HTMLSVG_CONTEXT, $(string(tag)))
+ end)
+ end
+ push!(blk.args, nothing)
+ blk
+end
-# Creates an HTML escaping dictionary
-chardict(chars) = Dict(c => "$(Int(c));" for c in chars)
-# See: https://stackoverflow.com/questions/7753448/how-do-i-escape-quotes-in-html-attribute-values
-const ATTR_ESCAPES = chardict("&<>\"\n\r\t")
-# See: https://stackoverflow.com/a/9189067/1175713
-const HTML_ESCAPES = chardict("&<>\"'`!@\$%()=+{}[]")
+## CSS
+
+ismedia(node::Node{CSS}) = startswith(tag(node), "@media")
+nestchildren(node::Node{CSS}) = startswith(tag(node), "@")
+
+function render(io::IO, rctx::RenderContext, ctx::CSS, node::Node)
+ @assert ctx == context(node)
+
+ etag = escapetag(ctx)
+ eattrname = escapeattrname(ctx)
+ eattrvalue = escapeattrvalue(ctx)
+
+ if rctx.pretty
+ print(io, "\n", rctx.indent ^ rctx.level)
+ end
+ printescaped(io, tag(node), etag)
+ print(io, " {") # \n
+
+ for (name, value) in pairs(attrs(node))
+ printescaped(io, name, eattrname)
+ print(io, ": ")
+ printescaped(io, value, eattrvalue)
+ print(io, ";") # \n
+ end
+
+ for child in children(node)
+ @assert typeof(child) <: Node{CSS} "CSS child elements must be `Node`s."
+ end
+
+ nest = nestchildren(node)
+ nest && for child in children(node)
+ render(io, rctx, child)
+ end
+
+ print(io, "}") # \n
-printescaped(io, x, replacements=HTML_ESCAPES) = for c in x
- print(io, get(replacements, c, c))
+ !nest && for child in children(node)
+ childctx = context(child)
+ render(io, rctx, Node{typeof(childctx)}(childctx, tag(node) * " " * tag(child), children(child), attrs(child)))
+ end
end
-function render(io::IO, x)
- mime = MIME(mimewritable(MIME("text/html"), x) ? "text/html" : "text/plain")
- printescaped(io, sprint(show, mime, x))
+renderdomchild(io, rctx::RenderContext, ctx::Context, node::AbstractNode{CSS}) = render(io, rctx, node)
+
+normalizetag(ctx::CSS, tag) = strip(tag)
+
+stringify(ctx::CSS, tag, (name, value)::Pair) = "$tag { $name: $value; }"
+
+function validatetag(ctx::HTMLSVG, tag)
+ isempty(tag) && error("Tag cannot be empty.")
+ tag
end
-render(io::IO, x::Union{AbstractString, Char}) = printescaped(io, x)
-render(io::IO, x::Number) = printescaped(io, string(x))
-render(node::Node) = sprint(render, node)
-function render(io::IO, node::Node)
- print(io, "<", tag(node))
- for (k, v) in pairs(attrs(node))
- print(io, " ", k, "=\"")
- printescaped(io, v, ATTR_ESCAPES)
- print(io, "\"")
+function validateattr(ctx::CSS, tag, attr)
+ name, value = attr
+ last(attr) == nothing && error("CSS attribute value may not be `nothing`: $(stringify(ctx, tag, attr))")
+ last(attr) == "" && error("CSS attribute value may not be the empty string: $(stringify(ctx, tag, attr))")
+ if !ctx.allow_nan_attr_values && typeof(value) <: AbstractFloat && isnan(value)
+ error("NaN values are not allowed for CSS nodes: $(stringify(ctx, tag, attr))")
end
- if isvoid(tag(node))
- @assert isempty(children(node))
- print(io, " />")
+ attr
+end
+
+function validatechild(ctx::CSS, tag, child)
+ typeof(child) <: Node{CSS} || error("CSS nodes may only have Node{CSS} children. Found $(typeof(child)): $child")
+ child
+end
+
+normalizeattr(ctx::CSS, tag, attr::Pair) = kebab(string(first(attr))) => last(attr)
+normalizechild(ctx::CSS, tag, child) = child
+
+escapetag(ctx::CSS) = NO_ESCAPES
+escapeattrname(ctx::CSS) = NO_ESCAPES
+escapeattrvalue(ctx::CSS) = NO_ESCAPES
+
+const DEFAULT_CSS_CONTEXT = CSS(false)
+css(tag, children...; attrs...) = Node(DEFAULT_CSS_CONTEXT, tag, children, attrs)
+
+## Scoped CSS
+
+# A `Styled` node results from the application of a `Style` to a `Node`.
+# It serves as a cascade barrier — parent styles do not bleed into nested styled nodes.
+struct Styled <: AbstractNode{HTMLSVG}
+ node::Node{HTMLSVG}
+ style
+end
+
+# delegate
+tag(x::Styled) = tag(x.node)
+attrs(x::Styled) = attrs(x.node)
+children(x::Styled) = children(x.node)
+context(x::Styled) = context(x.node)
+(x::Styled)(cs...; as...) = Styled(x.node((augmentdom(x.style.id, c) for c in cs)...; as...), x.style)
+render(io::IO, x::Styled) = render(io, x.node)
+render(io::IO, rctx::RenderContext, x::Styled) = render(io, rctx, x.node)
+
+Base.show(io::IO, x::Styled) = show(io, x.node)
+Base.show(io::IO, m::MIME"text/html", x::Styled) = show(io, x.node)
+
+struct Style
+ id::Int
+ styles::Vector{Node{CSS}}
+ augmentcss(id, node) = Node{CSS}(
+ context(node),
+ isempty(attrs(node)) || ismedia(node) ? tag(node) : tag(node) * "[v-style$id]",
+ augmentcss.(id, children(node)),
+ attrs(node)
+ )
+ Style(id::Int, styles) = new(id, [augmentcss(id, node) for node in styles])
+end
+
+style_id = 0
+function Style(styles...)
+ global style_id
+ Style(style_id += 1, styles)
+end
+
+styles(x::Style) = x.styles
+
+render(io::IO, rctx::RenderContext, x::Style) = for node in x.styles
+ render(io, rctx, node)
+end
+
+augmentdom(id, x) = x # Literals and other non-HTML/SVG objects
+augmentdom(id, x::Styled) = x # `Styled` nodes act as cascade barriers
+augmentdom(id, node::Node{T}) where {T} = Node{T}(
+ context(node),
+ tag(node),
+ augmentdom.(id, children(node)),
+ push!(copy(attrs(node)), "v-style$id" => nothing) # note: makes a defensive copy
+)
+(s::Style)(x::Node) = Styled(augmentdom(s.id, x), s)
+
+
+## Useful utilities for generating HTML files
+
+# todo: can these two functions be merged?
+# todo: verify that this doesn't double-wrap...
+# I think I've seen it happen in the wild.
+function wraphtml(dom)
+ node = if dom isa Node && tag(dom) == "html"
+ dom
else
- print(io, ">")
- for child in children(node)
- render(io, child)
- end
- print(io, "", tag(node), ">")
+ # todo: head, body?
+ m("html", dom)
end
+ preamble = "\n" # HTML 5
+ string(preamble, sprint(show, node))
end
-
-Base.show(io::IO, ::MIME"text/html", node::Node) = render(io, node)
-Base.show(io::IO, node::Node) = render(io, node)
+savehtml(filename, children...) = write(filename, wraphtml(children))
+
+function wrapsvg(dom)
+ preamble = """
+
+
+ """ # SVG 1.1
+ node = tag(dom) == "svg" ? dom : m("svg", dom)
+ haskey(attrs(node), "xmlns") || (node = node(xmlns="http://www.w3.org/2000/svg"))
+ haskey(attrs(node), "version") || (node = node(version="1.1"))
+ string(preamble, sprint(show, node))
+end
+savesvg(filename, children) = write(filename, wrapsvg(children))
+
+#=
+future enhancements
+ - some way to import many common HTML/SVG tags at once
+ - when applying a Style to a node only add the `v-style` marker to those nodes that may be affected by a style selector.
+ - add linting validations for e.g.
+ - autoprefix css attributes based on some criterion, perhaps from caniuse.com
+ process m(k => v) as m(; k=v)? this would break the "all normal arguments are children" invariant.
+ - rename save[x] to write[x]
+=#
end # module
diff --git a/src/aria.json b/src/aria.json
deleted file mode 100644
index 1e32055..0000000
--- a/src/aria.json
+++ /dev/null
@@ -1,38 +0,0 @@
-[
- "aria-activedescendant",
- "aria-atomic",
- "aria-autocomplete",
- "aria-busy",
- "aria-checked",
- "aria-controls",
- "aria-describedby",
- "aria-disabled",
- "aria-dropeffect",
- "aria-expanded",
- "aria-flowto",
- "aria-grabbed",
- "aria-haspopup",
- "aria-hidden",
- "aria-invalid",
- "aria-label",
- "aria-labelledby",
- "aria-level",
- "aria-live",
- "aria-multiline",
- "aria-multiselectable",
- "aria-orientation",
- "aria-owns",
- "aria-posinset",
- "aria-pressed",
- "aria-readonly",
- "aria-relevant",
- "aria-required",
- "aria-selected",
- "aria-setsize",
- "aria-sort",
- "aria-valuemax",
- "aria-valuemin",
- "aria-valuenow",
- "aria-valuetext",
- "role"
-]
diff --git a/src/constants.jl.REMOVED.git-id b/src/constants.jl.REMOVED.git-id
deleted file mode 100644
index 6f903bf..0000000
--- a/src/constants.jl.REMOVED.git-id
+++ /dev/null
@@ -1 +0,0 @@
-49ffff8c001a3db84b6e60bb2eb6c95074fd2312
\ No newline at end of file
diff --git a/src/cssunits.jl b/src/cssunits.jl
new file mode 100644
index 0000000..b569995
--- /dev/null
+++ b/src/cssunits.jl
@@ -0,0 +1,52 @@
+struct Unit{S, T} # S = suffix symbol
+ value::T
+end
+Unit{S}(value) where {S} = Unit{S, typeof(value)}(value)
+
+Base.show(io::IO, x::Unit{S}) where {S} = print(io, x.value, S)
+
+# diagonal dispatch for unit + unit, unit - unit
+Base.:+(x::Unit{S}, y::Unit{S}) where {S} = Unit{S}(x.value + y.value)
+Base.:-(x::Unit{S}, y::Unit{S}) where {S} = Unit{S}(x.value - y.value)
+
+# scalar * unit, unit / scalar
+Base.:*(x::Number, y::Unit{S}) where {S} = Unit{S}(x * y.value)
+Base.:/(x::Unit{S}, y::Number) where {S} = Unit{S}(x.value / y)
+
+# calc() expressions
+struct Calc
+ expr::String
+ Calc(expr) = new("($expr)")
+end
+Base.show(io::IO, x::Calc) = print(io, "calc", x.expr)
+
+# default to calc() for mismatched units
+Base.:+(x::Unit, y::Unit) = Calc("$x + $y")
+Base.:-(x::Unit, y::Unit) = Calc("$x - $y")
+
+# unit + calc(), calc() + unit
+Base.:+(x::Unit, y::Calc) = Calc("$x + $(y.expr)")
+Base.:+(x::Calc, y::Unit) = Calc("$(x.expr) + $y")
+
+# unit - calc(), calc() - unit,
+Base.:-(x::Unit, y::Calc) = Calc("$x - $(y.expr)")
+Base.:-(x::Calc, y::Unit) = Calc("$(x.expr) - $y")
+
+# scalar * calc(), calc() / scalar
+Base.:*(x::Number, y::Calc) = Calc("$x * $(y.expr)")
+Base.:/(x::Calc, y::Number) = Calc("$(x.expr) / $y")
+
+# concise scalar * unit construction, e.g. 5px
+struct BareUnit{T} end
+Base.:*(x::T, ::BareUnit{U}) where {U <: Unit, T<:Number} = U{T}(x)
+
+# common css units (ex, ch excluded)
+const px = BareUnit{Unit{:px}}()
+const pt = BareUnit{Unit{:pt}}()
+const em = BareUnit{Unit{:em}}()
+const rem = BareUnit{Unit{:rem}}()
+const vh = BareUnit{Unit{:vh}}()
+const vw = BareUnit{Unit{:vw}}()
+const vmin = BareUnit{Unit{:vmin}}()
+const vmax = BareUnit{Unit{:vmax}}()
+const pc = BareUnit{Unit{Symbol("%")}}()
\ No newline at end of file
diff --git a/src/generate.jl b/src/generate.jl
deleted file mode 100644
index 1550b47..0000000
--- a/src/generate.jl
+++ /dev/null
@@ -1,125 +0,0 @@
-using Unicode # for attr2symbols
-using Base.Iterators, JSON # for data generation
-
-load(fname) = JSON.parse(read(fname, String); dicttype=Dict{String, Vector{String}})
-
-## Validation data
-
-"""
-Map of SVG elements to allowed attributes. Also contains global attributes under '*'.
-Includes attributes from SVG 1.1, SVG Tiny 1.2, and SVG 2.
-Note: Does not include ARIA attributes (role, aria-*), xml:* or xlink:* attributes, event attributes (on*), or ev:event
-[Source](https://github.com/wooorm/svg-element-attributes/blob/2b028fecbf10df35162e63ee50308138068331a6/index.json)
-"""
-const SVG_ATTRS = load("svg.json")
-
-"""
-List of known SVG tag-names. Includes the elements from SVG 1.1, SVG Tiny 1.2, and SVG 2.
-[Source](https://github.com/wooorm/svg-tag-names/blob/a30d82c127d9959add5c357e44f5822c15eb8540/index.json)
-"""
-const SVG_TAGS = Set{String}(load("svgtags.json"))
-
-"""
-Map of HTML elements to allowed attributes. Also contains global attributes under '*'. Includes attributes from HTML 4, W3C HTML 5, and WHATWG HTML 5.
-Note: Includes deprecated attributes.
-Note: Attributes which were not global in HTML 4 but are in HTML 5, are only included in the list of global attributes.
-[Source](https://github.com/wooorm/html-element-attributes/blob/2d4db7c929552c35e2720a8d99547da72f8dde52/index.json)
-"""
-const HTML_ATTRS = load("html.json")
-
-"""
-List of known HTML tag-names. Includes ancient (for example, nextid and basefont) and modern (for example, shadow and template) tag-names from both W3C and WHATWG.
-[Source](https://github.com/wooorm/html-tag-names/blob/ef96f74a78b4fbe343518a6c156692e12446987a/index.json)
-"""
-const HTML_TAGS = Set{String}(load("htmltags.json"))
-
-"""
-Human-readable list of tags that are both HTML and SVG.
-Destined for a docstring.
-"""
-const HTML_SVG_TAG_INTERSECTION_MD = join(["`$tag`" for tag in intersect(SVG_TAGS, HTML_TAGS)] , ", ", ", and ")
-
-"""
-List of attributes defined by [ARIA](https://www.w3.org/TR/aria-in-html/).
-[Source](https://github.com/wooorm/aria-attributes/blob/b5dc0dfb1a97ed89eee6b3229527f240db054754/index.json)
-"""
-const ARIA_EXTRAS = load("aria.json")
-
-# Section M.2: https://www.w3.org/TR/SVG/attindex.html
-const SVG_PRESENTATION_ATTRIBUTES = ["alignment-baseline", "baseline-shift", "clip-path", "clip-rule", "clip", "color-interpolation-filters", "color-interpolation", "color-profile", "color-rendering", "color", "cursor", "direction", "display", "dominant-baseline", "enable-background", "fill-opacity", "fill-rule", "fill", "filter", "flood-color", "flood-opacity", "font-family", "font-size-adjust", "font-size", "font-stretch", "font-style", "font-variant", "font-weight", "glyph-orientation-horizontal", "glyph-orientation-vertical", "image-rendering", "kerning", "letter-spacing", "lighting-color", "marker-end", "marker-mid", "marker-start", "mask", "opacity", "overflow", "pointer-events", "shape-rendering", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "stroke", "text-anchor", "text-decoration", "text-rendering", "unicode-bidi", "visibility", "word-spacing", "writing-mode"]
-const SVG_PRESENTATION_ELEMENTS = ["a", "altGlyph", "animate", "animateColor", "circle", "clipPath", "defs", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feFlood", "feGaussianBlur", "feImage", "feMerge", "feMorphology", "feOffset", "feSpecularLighting", "feTile", "feTurbulence", "filter", "font", "foreignObject", "g", "glyph", "glyphRef", "image", "line", "linearGradient", "marker", "mask", "missing-glyph", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "stop", "svg", "switch", "symbol", "text", "textPath", "tref", "tspan", "use"]
-for tag in SVG_PRESENTATION_ELEMENTS
- append!(SVG_ATTRS[tag], SVG_PRESENTATION_ATTRIBUTES)
-end
-
-
-# Allow all ARIA attributes on all HTML and SVG elements.
-# This is over-generous.
-append!(SVG_ATTRS["*"], ARIA_EXTRAS)
-append!(HTML_ATTRS["*"], ARIA_EXTRAS)
-
-# Used to validate by default.
-# Allows mixing of HTML and SVG attributes on the same element.
-const COMBINED_ATTRS = merge(unique∘vcat, SVG_ATTRS, HTML_ATTRS)
-const COMBINED_TAGS = union(SVG_TAGS, HTML_TAGS)
-
-function attr2symbols(attr)
- Symbol.(if contains(attr, '-')
- pieces = split(attr, '-')
- (join(pieces), join([first(pieces), map(ucfirst, pieces[2:end])...]))
- else
- if any(isupper, attr)
- (lowercase(attr), attr)
- else
- (attr,)
- end
- end)
-end
-
-function sym_to_attr_dict(attrs)
- Dict{Symbol, String}(sym => attr for attr in unique(flatten(values(attrs))) for sym in attr2symbols(attr))
-end
-
-# Lookup tables from kwargs symbols to attribute strings.
-# e.g. :viewbox => "viewBox", :viewBox => "viewBox"
-# e.g. :stopcolor => "stop-color", :stopColor => "stop-color"
-const HTML_ATTR_NAMES = sym_to_attr_dict(HTML_ATTRS)
-const SVG_ATTR_NAMES = sym_to_attr_dict(SVG_ATTRS)
-const COMBINED_ATTR_NAME = merge(HTML_ATTR_NAMES, SVG_ATTR_NAMES)
-
-# Void elements are not allowed to contain content
-# See: http://www.w3.org/TR/html5/syntax.html#void-elements
-const HTML_VOID_TAGS = Set{String}(["area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"])
-# See: https://github.com/jonschlinkert/self-closing-tags
-const SVG_VOID_TAGS = Set{String}(["circle", "ellipse", "line", "path", "polygon", "polyline", "rect", "stop", "use"])
-const COMBINED_VOID_TAGS = union(SVG_VOID_TAGS, HTML_VOID_TAGS)
-
-# Guard against the unlikely chance that a tag is not void in both
-# HTML and SVG in a future version of either spec; this would invalidate
-# the assumptin made by `isvoid`.
-@assert all(tag -> !(tag ∈ SVG_VOID_TAGS && tag ∈ HTML_VOID_TAGS), COMBINED_VOID_TAGS)
-
-macro vardecl(x)
- name = string(x)
- quote
- string("const ", $name, " = ", sprint(showcompact, $x))
- end
-end
-
-open("constants.jl", "w") do io
- println(io, "# This code was generated with generate.jl")
- println(io, @vardecl(HTML_TAGS))
- println(io, @vardecl(SVG_TAGS))
- # println(io, @vardecl(COMBINED_TAGS))
-
- println(io, @vardecl(HTML_ATTRS))
- println(io, @vardecl(SVG_ATTRS))
- # println(io, @vardecl(COMBINED_ATTRS))
-
- println(io, @vardecl(HTML_ATTR_NAMES))
- println(io, @vardecl(SVG_ATTR_NAMES))
- # println(io, @vardecl(COMBINED_ATTR_NAME))
-
- println(io, @vardecl(COMBINED_VOID_TAGS))
- println(io, @vardecl(HTML_SVG_TAG_INTERSECTION_MD))
-end
\ No newline at end of file
diff --git a/src/html.json b/src/html.json
deleted file mode 100644
index 8b582fe..0000000
--- a/src/html.json
+++ /dev/null
@@ -1,605 +0,0 @@
-{
- "*": [
- "accesskey",
- "class",
- "contenteditable",
- "dir",
- "draggable",
- "hidden",
- "id",
- "is",
- "itemid",
- "itemprop",
- "itemref",
- "itemscope",
- "itemtype",
- "lang",
- "slot",
- "spellcheck",
- "style",
- "tabindex",
- "title",
- "translate"
- ],
- "a": [
- "accesskey",
- "charset",
- "coords",
- "download",
- "href",
- "hreflang",
- "name",
- "ping",
- "referrerpolicy",
- "rel",
- "rev",
- "shape",
- "tabindex",
- "target",
- "type"
- ],
- "abbr": [
- "title"
- ],
- "applet": [
- "align",
- "alt",
- "archive",
- "code",
- "codebase",
- "height",
- "hspace",
- "name",
- "object",
- "vspace",
- "width"
- ],
- "area": [
- "accesskey",
- "alt",
- "coords",
- "download",
- "href",
- "hreflang",
- "nohref",
- "ping",
- "referrerpolicy",
- "rel",
- "shape",
- "tabindex",
- "target",
- "type"
- ],
- "audio": [
- "autoplay",
- "controls",
- "crossorigin",
- "loop",
- "mediagroup",
- "muted",
- "preload",
- "src"
- ],
- "base": [
- "href",
- "target"
- ],
- "basefont": [
- "color",
- "face",
- "size"
- ],
- "bdo": [
- "dir"
- ],
- "blockquote": [
- "cite"
- ],
- "body": [
- "alink",
- "background",
- "bgcolor",
- "link",
- "text",
- "vlink"
- ],
- "br": [
- "clear"
- ],
- "button": [
- "accesskey",
- "autofocus",
- "disabled",
- "form",
- "formaction",
- "formenctype",
- "formmethod",
- "formnovalidate",
- "formtarget",
- "name",
- "tabindex",
- "type",
- "value"
- ],
- "canvas": [
- "height",
- "width"
- ],
- "caption": [
- "align"
- ],
- "col": [
- "align",
- "char",
- "charoff",
- "span",
- "valign",
- "width"
- ],
- "colgroup": [
- "align",
- "char",
- "charoff",
- "span",
- "valign",
- "width"
- ],
- "data": [
- "value"
- ],
- "del": [
- "cite",
- "datetime"
- ],
- "details": [
- "open"
- ],
- "dfn": [
- "title"
- ],
- "dialog": [
- "open"
- ],
- "dir": [
- "compact"
- ],
- "div": [
- "align"
- ],
- "dl": [
- "compact"
- ],
- "embed": [
- "height",
- "src",
- "type",
- "width"
- ],
- "fieldset": [
- "disabled",
- "form",
- "name"
- ],
- "font": [
- "color",
- "face",
- "size"
- ],
- "form": [
- "accept",
- "accept-charset",
- "action",
- "autocomplete",
- "enctype",
- "method",
- "name",
- "novalidate",
- "target"
- ],
- "frame": [
- "frameborder",
- "longdesc",
- "marginheight",
- "marginwidth",
- "name",
- "noresize",
- "scrolling",
- "src"
- ],
- "frameset": [
- "cols",
- "rows"
- ],
- "h1": [
- "align"
- ],
- "h2": [
- "align"
- ],
- "h3": [
- "align"
- ],
- "h4": [
- "align"
- ],
- "h5": [
- "align"
- ],
- "h6": [
- "align"
- ],
- "head": [
- "profile"
- ],
- "hr": [
- "align",
- "noshade",
- "size",
- "width"
- ],
- "html": [
- "manifest",
- "version"
- ],
- "iframe": [
- "align",
- "allowfullscreen",
- "allowpaymentrequest",
- "allowusermedia",
- "frameborder",
- "height",
- "longdesc",
- "marginheight",
- "marginwidth",
- "name",
- "referrerpolicy",
- "sandbox",
- "scrolling",
- "src",
- "srcdoc",
- "width"
- ],
- "img": [
- "align",
- "alt",
- "border",
- "crossorigin",
- "height",
- "hspace",
- "ismap",
- "longdesc",
- "name",
- "referrerpolicy",
- "sizes",
- "src",
- "srcset",
- "usemap",
- "vspace",
- "width"
- ],
- "input": [
- "accept",
- "accesskey",
- "align",
- "alt",
- "autocomplete",
- "autofocus",
- "checked",
- "dirname",
- "disabled",
- "form",
- "formaction",
- "formenctype",
- "formmethod",
- "formnovalidate",
- "formtarget",
- "height",
- "inputmode",
- "ismap",
- "list",
- "max",
- "maxlength",
- "min",
- "minlength",
- "multiple",
- "name",
- "pattern",
- "placeholder",
- "readonly",
- "required",
- "size",
- "src",
- "step",
- "tabindex",
- "title",
- "type",
- "usemap",
- "value",
- "width"
- ],
- "ins": [
- "cite",
- "datetime"
- ],
- "isindex": [
- "prompt"
- ],
- "keygen": [
- "autofocus",
- "challenge",
- "disabled",
- "form",
- "keytype",
- "name"
- ],
- "label": [
- "accesskey",
- "for",
- "form"
- ],
- "legend": [
- "accesskey",
- "align"
- ],
- "li": [
- "type",
- "value"
- ],
- "link": [
- "as",
- "charset",
- "color",
- "crossorigin",
- "href",
- "hreflang",
- "integrity",
- "media",
- "nonce",
- "referrerpolicy",
- "rel",
- "rev",
- "scope",
- "sizes",
- "target",
- "title",
- "type",
- "updateviacache",
- "workertype"
- ],
- "map": [
- "name"
- ],
- "menu": [
- "compact"
- ],
- "meta": [
- "charset",
- "content",
- "http-equiv",
- "name",
- "scheme"
- ],
- "meter": [
- "high",
- "low",
- "max",
- "min",
- "optimum",
- "value"
- ],
- "object": [
- "align",
- "archive",
- "border",
- "classid",
- "codebase",
- "codetype",
- "data",
- "declare",
- "form",
- "height",
- "hspace",
- "name",
- "standby",
- "tabindex",
- "type",
- "typemustmatch",
- "usemap",
- "vspace",
- "width"
- ],
- "ol": [
- "compact",
- "reversed",
- "start",
- "type"
- ],
- "optgroup": [
- "disabled",
- "label"
- ],
- "option": [
- "disabled",
- "label",
- "selected",
- "value"
- ],
- "output": [
- "for",
- "form",
- "name"
- ],
- "p": [
- "align"
- ],
- "param": [
- "name",
- "type",
- "value",
- "valuetype"
- ],
- "pre": [
- "width"
- ],
- "progress": [
- "max",
- "value"
- ],
- "q": [
- "cite"
- ],
- "script": [
- "async",
- "charset",
- "crossorigin",
- "defer",
- "integrity",
- "language",
- "nomodule",
- "nonce",
- "src",
- "type"
- ],
- "select": [
- "autocomplete",
- "autofocus",
- "disabled",
- "form",
- "multiple",
- "name",
- "required",
- "size",
- "tabindex"
- ],
- "slot": [
- "name"
- ],
- "source": [
- "media",
- "sizes",
- "src",
- "srcset",
- "type"
- ],
- "style": [
- "media",
- "nonce",
- "title",
- "type"
- ],
- "table": [
- "align",
- "bgcolor",
- "border",
- "cellpadding",
- "cellspacing",
- "frame",
- "rules",
- "summary",
- "width"
- ],
- "tbody": [
- "align",
- "char",
- "charoff",
- "valign"
- ],
- "td": [
- "abbr",
- "align",
- "axis",
- "bgcolor",
- "char",
- "charoff",
- "colspan",
- "headers",
- "height",
- "nowrap",
- "rowspan",
- "scope",
- "valign",
- "width"
- ],
- "textarea": [
- "accesskey",
- "autocomplete",
- "autofocus",
- "cols",
- "dirname",
- "disabled",
- "form",
- "inputmode",
- "maxlength",
- "minlength",
- "name",
- "placeholder",
- "readonly",
- "required",
- "rows",
- "tabindex",
- "wrap"
- ],
- "tfoot": [
- "align",
- "char",
- "charoff",
- "valign"
- ],
- "th": [
- "abbr",
- "align",
- "axis",
- "bgcolor",
- "char",
- "charoff",
- "colspan",
- "headers",
- "height",
- "nowrap",
- "rowspan",
- "scope",
- "valign",
- "width"
- ],
- "thead": [
- "align",
- "char",
- "charoff",
- "valign"
- ],
- "time": [
- "datetime"
- ],
- "tr": [
- "align",
- "bgcolor",
- "char",
- "charoff",
- "valign"
- ],
- "track": [
- "default",
- "kind",
- "label",
- "src",
- "srclang"
- ],
- "ul": [
- "compact",
- "type"
- ],
- "video": [
- "autoplay",
- "controls",
- "crossorigin",
- "height",
- "loop",
- "mediagroup",
- "muted",
- "playsinline",
- "poster",
- "preload",
- "src",
- "width"
- ]
-}
diff --git a/src/htmltags.json b/src/htmltags.json
deleted file mode 100644
index 395304d..0000000
--- a/src/htmltags.json
+++ /dev/null
@@ -1,150 +0,0 @@
-[
- "a",
- "abbr",
- "acronym",
- "address",
- "applet",
- "area",
- "article",
- "aside",
- "audio",
- "b",
- "base",
- "basefont",
- "bdi",
- "bdo",
- "bgsound",
- "big",
- "blink",
- "blockquote",
- "body",
- "br",
- "button",
- "canvas",
- "caption",
- "center",
- "cite",
- "code",
- "col",
- "colgroup",
- "command",
- "content",
- "data",
- "datalist",
- "dd",
- "del",
- "details",
- "dfn",
- "dialog",
- "dir",
- "div",
- "dl",
- "dt",
- "element",
- "em",
- "embed",
- "fieldset",
- "figcaption",
- "figure",
- "font",
- "footer",
- "form",
- "frame",
- "frameset",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6",
- "head",
- "header",
- "hgroup",
- "hr",
- "html",
- "i",
- "iframe",
- "image",
- "img",
- "input",
- "ins",
- "isindex",
- "kbd",
- "keygen",
- "label",
- "legend",
- "li",
- "link",
- "listing",
- "main",
- "map",
- "mark",
- "marquee",
- "math",
- "menu",
- "menuitem",
- "meta",
- "meter",
- "multicol",
- "nav",
- "nextid",
- "nobr",
- "noembed",
- "noframes",
- "noscript",
- "object",
- "ol",
- "optgroup",
- "option",
- "output",
- "p",
- "param",
- "picture",
- "plaintext",
- "pre",
- "progress",
- "q",
- "rb",
- "rbc",
- "rp",
- "rt",
- "rtc",
- "ruby",
- "s",
- "samp",
- "script",
- "section",
- "select",
- "shadow",
- "slot",
- "small",
- "source",
- "spacer",
- "span",
- "strike",
- "strong",
- "style",
- "sub",
- "summary",
- "sup",
- "svg",
- "table",
- "tbody",
- "td",
- "template",
- "textarea",
- "tfoot",
- "th",
- "thead",
- "time",
- "title",
- "tr",
- "track",
- "tt",
- "u",
- "ul",
- "var",
- "video",
- "wbr",
- "xmp"
-]
diff --git a/src/playground.jl b/src/playground.jl
deleted file mode 100644
index 23f2974..0000000
--- a/src/playground.jl
+++ /dev/null
@@ -1,135 +0,0 @@
-__precompile__()
-module Foo
-include("constants.jl")
-end
-
-
-# writedlm("html_tags.tsv", HTML_TAGS)
-# writedlm("svg_tags.tsv", SVG_TAGS)
-
-
-# SVG_TAGS
-# HTML_ATTRS
-# SVG_ATTRS
-# HTML_ATTR_NAME
-# SVG_ATTR_NAME
-# COMBINED_VOID_TAGS
-# HTML_SVG_TAG_INTERSECTION_MD
-
-
-#=
-using Base.Iterators
-const COMBINED_TAGS = union(SVG_TAGS, HTML_TAGS)
-const COMBINED_ATTR_NAMES = union(collect(values(HTML_ATTR_NAME)), collect(values(SVG_ATTR_NAME)))
-
-const strings = union(COMBINED_TAGS, COMBINED_ATTR_NAMES)
-const codes = UInt16[1:length(strings)...]
-const encoded = Dict(zip(strings, codes))
-const decoded = Dict(zip(codes, strings))
-
-encode(x::String) = UInt16(encoded[x])
-decode(x::UInt16) = decoded[x]
-
-encode(x::String, y::String) = encode(encode(x), encode(y))
-decode(x::UInt32) = decode(UInt16(x >> 16)), decode(UInt16(x & (typemax(UInt32)>>16)))
-
-@show COMBINED_ATTR_NAMES
-
-# COMBINED_ATTR_NAME
-# HTML_ATTR_NAME
-# SVG_ATTR_NAME
-# COMBINED_ATTRS
-# HTML_ATTRS
-# SVG_ATTRS
-# COMBINED_TAGS
-# HTML_TAGS
-# SVG_TAGS
-
-
-# tag ∈ tags(v)
-# valid = attr ∈ ATTRS[tag] || attr ∈ ATTRS["*"]
-# attr = get(sym_to_attr, sym) do error(...) end
-
-# is a tag valid? [does this 16-bit int exist in this set]
-# - is valid for svg = [is it valid and also less than this number]
-# is an attribute valid for the given tag? [does this 32-bit int exist in this set]
-#=
-tag_valid_lookup =
-
-struct Validation
- name::String
- tag_codes::Set
- tag_attr_codes::Set
-end
-
-const V_COMBINED = Validation(
-
-isvalidtag(v::Validation, tag) = tag ∈ v.tag_codes)
-isvalidattr(v::Validation, attr, tag) = haskey(encode(tag, attr), v.tag_attr_codes)
-
-validateattr(v::ValidateCombined, attr, tag) =
-# validateattr(v::ValidateHTML, attr, tag) =
-# validateattr(v::ValidateSVG, attr, tag) =
-# validateattr(v::ValidateNone, attr, tag) =
-
-
-
-
-#=
-HTML_TAGS = Set(["basefont", "figcaption", "rb", "ul", "data"
-SVG_TAGS = Set(["solidcolor", "feBlend", "tspan", "feTile", "
-HTML_ATTRS = Dict("basefont"=>["color", "face", "size"],"ul"=
-SVG_ATTRS = Dict("glyph"=>["alignment-baseline", "arabic-form
-
-HTML_ATTR_NAME = Dict(:for=>"for",:formaction=>"formaction",:
-SVG_ATTR_NAME = Dict(:alignmentBaseline=>"alignment-baseline"
-COMBINED_VOID_TAGS = Set(["param", "ellipse", "link", "hr", "
-HTML_SVG_TAG_INTERSECTION_MD = "`audio`, `svg`, `a`, `canvas`
-=#
-
-
-
-# encode(x::UInt16, y::UInt16) = UInt32(x) << 16 + UInt32(y)
-# decode(x::UInt32) = decode(UInt16(x>>16)), decode(UInt16(x&(typemax(UInt32)>>16)))
-
-
-# @show COMBINED_ATTR_NAMES
-
-# strings = union(COMBINED_TAGS, unique(values(COMBINED_ATTR_NAME)))
-# codes = UInt16[1:length(strings)...]
-# ENCODE = Dict(zip(strings, codes))
-# DECODE = Dict(zip(codes, strings))
-
-# encode(x::UInt16, y::UInt16) = UInt32(x) << 16 + UInt32(y)
-# decode(x::UInt32) = decode(UInt16(x>>16)), decode(UInt16(x&(typemax(UInt32)>>16)))
-
-# encode(x::String) = UInt16(ENCODE[x])
-# decode(x::UInt16) = DECODE[x]
-
-# encode(x::String, y::String) = encode(encode(x), encode(y))
-
-# @show encode("hr")
-# @show encode("hr") |> typeof
-# @show decode(encode("hr"))
-
-# @show encode("hr", "align")
-# @show encode("hr", "align") |> typeof
-# @show decode(encode("hr", "align"))
-
-# keymap(f, d) = Dict(f(key) => value for (key, value) in pairs(d))
-
-# for ATTRS in [COMBINED_ATTRS HTML_ATTRS SVG_ATTRS]
-# star = pop!(ATTRS, "*")
-# for val in values(ATTRS)
-# append!(val, star)
-# end
-# end
-# denormalize(d) = Set(encode(tag, attr) for (tag, attrs) in pairs(d) for attr in attrs)
-
-# D_HTML_ATTRS = denormalize(HTML_ATTRS)
-# D_SVG_ATTRS = denormalize(SVG_ATTRS)
-# D_COMBINED_ATTRS = denormalize(COMBINED_ATTRS)
-# @show length(D_COMBINED_ATTRS)
-
-# # can we get rid of combined attrs and just check if svg || html?=#
-=#
\ No newline at end of file
diff --git a/src/svg.json b/src/svg.json
deleted file mode 100644
index 1d7cf14..0000000
--- a/src/svg.json
+++ /dev/null
@@ -1,1421 +0,0 @@
-{
- "*": [
- "about",
- "class",
- "content",
- "datatype",
- "id",
- "lang",
- "property",
- "rel",
- "resource",
- "rev",
- "tabindex",
- "typeof"
- ],
- "a": [
- "alignment-baseline",
- "download",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "href",
- "hreflang",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "target",
- "transform",
- "type"
- ],
- "altGlyph": [
- "alignment-baseline",
- "dx",
- "dy",
- "externalResourcesRequired",
- "format",
- "glyphRef",
- "requiredExtensions",
- "requiredFeatures",
- "rotate",
- "style",
- "systemLanguage",
- "x",
- "y"
- ],
- "animate": [
- "accumulate",
- "additive",
- "alignment-baseline",
- "attributeName",
- "attributeType",
- "begin",
- "by",
- "calcMode",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "from",
- "href",
- "keySplines",
- "keyTimes",
- "max",
- "min",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "systemLanguage",
- "to",
- "values"
- ],
- "animateColor": [
- "accumulate",
- "additive",
- "alignment-baseline",
- "attributeName",
- "attributeType",
- "begin",
- "by",
- "calcMode",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "from",
- "keySplines",
- "keyTimes",
- "max",
- "min",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "systemLanguage",
- "to",
- "values"
- ],
- "animateMotion": [
- "accumulate",
- "additive",
- "begin",
- "by",
- "calcMode",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "from",
- "href",
- "keyPoints",
- "keySplines",
- "keyTimes",
- "max",
- "min",
- "origin",
- "path",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "rotate",
- "systemLanguage",
- "to",
- "values"
- ],
- "animateTransform": [
- "accumulate",
- "additive",
- "attributeName",
- "attributeType",
- "begin",
- "by",
- "calcMode",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "from",
- "href",
- "keySplines",
- "keyTimes",
- "max",
- "min",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "systemLanguage",
- "to",
- "type",
- "values"
- ],
- "animation": [
- "begin",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "focusHighlight",
- "focusable",
- "height",
- "initialVisibility",
- "max",
- "min",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "preserveAspectRatio",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "syncBehavior",
- "syncMaster",
- "syncTolerance",
- "systemLanguage",
- "transform",
- "width",
- "x",
- "y"
- ],
- "audio": [
- "begin",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "max",
- "min",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "style",
- "syncBehavior",
- "syncMaster",
- "syncTolerance",
- "systemLanguage",
- "type"
- ],
- "canvas": [
- "preserveAspectRatio",
- "requiredExtensions",
- "style",
- "systemLanguage"
- ],
- "circle": [
- "alignment-baseline",
- "cx",
- "cy",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "r",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "clipPath": [
- "alignment-baseline",
- "clipPathUnits",
- "externalResourcesRequired",
- "requiredExtensions",
- "requiredFeatures",
- "style",
- "systemLanguage",
- "transform"
- ],
- "color-profile": [
- "local",
- "name",
- "rendering-intent"
- ],
- "cursor": [
- "externalResourcesRequired",
- "href",
- "requiredExtensions",
- "requiredFeatures",
- "systemLanguage",
- "x",
- "y"
- ],
- "defs": [
- "alignment-baseline",
- "externalResourcesRequired",
- "requiredExtensions",
- "requiredFeatures",
- "style",
- "systemLanguage",
- "transform"
- ],
- "desc": [
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage"
- ],
- "discard": [
- "begin",
- "href",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "systemLanguage"
- ],
- "ellipse": [
- "alignment-baseline",
- "cx",
- "cy",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "rx",
- "ry",
- "style",
- "systemLanguage",
- "transform"
- ],
- "feBlend": [
- "alignment-baseline",
- "height",
- "in",
- "in2",
- "mode",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feColorMatrix": [
- "alignment-baseline",
- "height",
- "in",
- "result",
- "style",
- "type",
- "values",
- "width",
- "x",
- "y"
- ],
- "feComponentTransfer": [
- "alignment-baseline",
- "height",
- "in",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feComposite": [
- "alignment-baseline",
- "height",
- "in",
- "in2",
- "k1",
- "k2",
- "k3",
- "k4",
- "operator",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feConvolveMatrix": [
- "alignment-baseline",
- "bias",
- "divisor",
- "edgeMode",
- "height",
- "in",
- "kernelMatrix",
- "kernelUnitLength",
- "order",
- "preserveAlpha",
- "result",
- "style",
- "targetX",
- "targetY",
- "width",
- "x",
- "y"
- ],
- "feDiffuseLighting": [
- "alignment-baseline",
- "diffuseConstant",
- "height",
- "in",
- "kernelUnitLength",
- "result",
- "style",
- "surfaceScale",
- "width",
- "x",
- "y"
- ],
- "feDisplacementMap": [
- "alignment-baseline",
- "height",
- "in",
- "in2",
- "result",
- "scale",
- "style",
- "width",
- "x",
- "xChannelSelector",
- "y",
- "yChannelSelector"
- ],
- "feDistantLight": [
- "azimuth",
- "elevation"
- ],
- "feDropShadow": [
- "dx",
- "dy",
- "height",
- "in",
- "result",
- "stdDeviation",
- "style",
- "width",
- "x",
- "y"
- ],
- "feFlood": [
- "alignment-baseline",
- "height",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feFuncA": [
- "amplitude",
- "exponent",
- "intercept",
- "offset",
- "slope",
- "tableValues",
- "type"
- ],
- "feFuncB": [
- "amplitude",
- "exponent",
- "intercept",
- "offset",
- "slope",
- "tableValues",
- "type"
- ],
- "feFuncG": [
- "amplitude",
- "exponent",
- "intercept",
- "offset",
- "slope",
- "tableValues",
- "type"
- ],
- "feFuncR": [
- "amplitude",
- "exponent",
- "intercept",
- "offset",
- "slope",
- "tableValues",
- "type"
- ],
- "feGaussianBlur": [
- "alignment-baseline",
- "edgeMode",
- "height",
- "in",
- "result",
- "stdDeviation",
- "style",
- "width",
- "x",
- "y"
- ],
- "feImage": [
- "alignment-baseline",
- "crossorigin",
- "externalResourcesRequired",
- "height",
- "href",
- "preserveAspectRatio",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feMerge": [
- "alignment-baseline",
- "height",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feMergeNode": [
- "in"
- ],
- "feMorphology": [
- "alignment-baseline",
- "height",
- "in",
- "operator",
- "radius",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feOffset": [
- "alignment-baseline",
- "dx",
- "dy",
- "height",
- "in",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "fePointLight": [
- "x",
- "y",
- "z"
- ],
- "feSpecularLighting": [
- "alignment-baseline",
- "height",
- "in",
- "kernelUnitLength",
- "result",
- "specularConstant",
- "specularExponent",
- "style",
- "surfaceScale",
- "width",
- "x",
- "y"
- ],
- "feSpotLight": [
- "limitingConeAngle",
- "pointsAtX",
- "pointsAtY",
- "pointsAtZ",
- "specularExponent",
- "x",
- "y",
- "z"
- ],
- "feTile": [
- "alignment-baseline",
- "height",
- "in",
- "result",
- "style",
- "width",
- "x",
- "y"
- ],
- "feTurbulence": [
- "alignment-baseline",
- "baseFrequency",
- "height",
- "numOctaves",
- "result",
- "seed",
- "stitchTiles",
- "style",
- "type",
- "width",
- "x",
- "y"
- ],
- "filter": [
- "alignment-baseline",
- "externalResourcesRequired",
- "filterRes",
- "filterUnits",
- "height",
- "primitiveUnits",
- "style",
- "width",
- "x",
- "y"
- ],
- "font": [
- "alignment-baseline",
- "externalResourcesRequired",
- "horiz-adv-x",
- "horiz-origin-x",
- "horiz-origin-y",
- "style",
- "vert-adv-y",
- "vert-origin-x",
- "vert-origin-y"
- ],
- "font-face": [
- "accent-height",
- "alphabetic",
- "ascent",
- "bbox",
- "cap-height",
- "descent",
- "externalResourcesRequired",
- "font-family",
- "font-size",
- "font-stretch",
- "font-style",
- "font-variant",
- "font-weight",
- "hanging",
- "ideographic",
- "mathematical",
- "overline-position",
- "overline-thickness",
- "panose-1",
- "slope",
- "stemh",
- "stemv",
- "strikethrough-position",
- "strikethrough-thickness",
- "underline-position",
- "underline-thickness",
- "unicode-range",
- "units-per-em",
- "v-alphabetic",
- "v-hanging",
- "v-ideographic",
- "v-mathematical",
- "widths",
- "x-height"
- ],
- "font-face-format": [
- "string"
- ],
- "font-face-name": [
- "name"
- ],
- "font-face-uri": [
- "externalResourcesRequired"
- ],
- "foreignObject": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "height",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform",
- "width",
- "x",
- "y"
- ],
- "g": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "glyph": [
- "alignment-baseline",
- "arabic-form",
- "d",
- "glyph-name",
- "horiz-adv-x",
- "orientation",
- "style",
- "unicode",
- "vert-adv-y",
- "vert-origin-x",
- "vert-origin-y"
- ],
- "glyphRef": [
- "alignment-baseline",
- "dx",
- "dy",
- "format",
- "glyphRef",
- "style",
- "x",
- "y"
- ],
- "handler": [
- "externalResourcesRequired",
- "type"
- ],
- "hatch": [
- "hatchContentUnits",
- "hatchUnits",
- "href",
- "pitch",
- "rotate",
- "style",
- "transform",
- "x",
- "y"
- ],
- "hatchpath": [
- "d",
- "offset",
- "style"
- ],
- "hkern": [
- "g1",
- "g2",
- "k",
- "u1",
- "u2"
- ],
- "iframe": [
- "requiredExtensions",
- "style",
- "systemLanguage"
- ],
- "image": [
- "alignment-baseline",
- "crossorigin",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "height",
- "href",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "preserveAspectRatio",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform",
- "type",
- "width",
- "x",
- "y"
- ],
- "line": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform",
- "x1",
- "x2",
- "y1",
- "y2"
- ],
- "linearGradient": [
- "alignment-baseline",
- "externalResourcesRequired",
- "gradientTransform",
- "gradientUnits",
- "href",
- "spreadMethod",
- "style",
- "x1",
- "x2",
- "y1",
- "y2"
- ],
- "listener": [
- "defaultAction",
- "event",
- "handler",
- "observer",
- "phase",
- "propagate",
- "target"
- ],
- "marker": [
- "alignment-baseline",
- "externalResourcesRequired",
- "markerHeight",
- "markerUnits",
- "markerWidth",
- "orient",
- "preserveAspectRatio",
- "refX",
- "refY",
- "style",
- "viewBox"
- ],
- "mask": [
- "alignment-baseline",
- "externalResourcesRequired",
- "height",
- "maskContentUnits",
- "maskUnits",
- "requiredExtensions",
- "requiredFeatures",
- "style",
- "systemLanguage",
- "width",
- "x",
- "y"
- ],
- "mesh": [
- "href",
- "requiredExtensions",
- "systemLanguage"
- ],
- "meshgradient": [
- "gradientUnits",
- "href",
- "style",
- "transform",
- "type",
- "x",
- "y"
- ],
- "meshpatch": [
- "style"
- ],
- "meshrow": [
- "style"
- ],
- "metadata": [
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "systemLanguage"
- ],
- "missing-glyph": [
- "alignment-baseline",
- "d",
- "horiz-adv-x",
- "style",
- "vert-adv-y",
- "vert-origin-x",
- "vert-origin-y"
- ],
- "mpath": [
- "externalResourcesRequired",
- "href"
- ],
- "path": [
- "alignment-baseline",
- "d",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "pattern": [
- "alignment-baseline",
- "externalResourcesRequired",
- "height",
- "href",
- "patternContentUnits",
- "patternTransform",
- "patternUnits",
- "preserveAspectRatio",
- "requiredExtensions",
- "requiredFeatures",
- "style",
- "systemLanguage",
- "viewBox",
- "width",
- "x",
- "y"
- ],
- "polygon": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "points",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "polyline": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "points",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "prefetch": [
- "bandwidth",
- "mediaCharacterEncoding",
- "mediaContentEncodings",
- "mediaSize",
- "mediaTime"
- ],
- "radialGradient": [
- "alignment-baseline",
- "cx",
- "cy",
- "externalResourcesRequired",
- "fr",
- "fx",
- "fy",
- "gradientTransform",
- "gradientUnits",
- "href",
- "r",
- "spreadMethod",
- "style"
- ],
- "rect": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "height",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "pathLength",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "rx",
- "ry",
- "style",
- "systemLanguage",
- "transform",
- "width",
- "x",
- "y"
- ],
- "script": [
- "crossorigin",
- "externalResourcesRequired",
- "href",
- "type"
- ],
- "set": [
- "attributeName",
- "attributeType",
- "begin",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "href",
- "max",
- "min",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "systemLanguage",
- "to"
- ],
- "solidcolor": [
- "style"
- ],
- "stop": [
- "alignment-baseline",
- "offset",
- "path",
- "style"
- ],
- "style": [
- "media",
- "title",
- "type"
- ],
- "svg": [
- "alignment-baseline",
- "baseProfile",
- "contentScriptType",
- "contentStyleType",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "height",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "playbackOrder",
- "playbackorder",
- "preserveAspectRatio",
- "requiredExtensions",
- "requiredFeatures",
- "snapshotTime",
- "style",
- "syncBehaviorDefault",
- "syncToleranceDefault",
- "systemLanguage",
- "timelineBegin",
- "timelinebegin",
- "transform",
- "version",
- "viewBox",
- "width",
- "x",
- "y",
- "zoomAndPan"
- ],
- "switch": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform"
- ],
- "symbol": [
- "alignment-baseline",
- "externalResourcesRequired",
- "preserveAspectRatio",
- "refX",
- "refY",
- "style",
- "viewBox"
- ],
- "tbreak": [
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "systemLanguage"
- ],
- "text": [
- "alignment-baseline",
- "dx",
- "dy",
- "editable",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "lengthAdjust",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "rotate",
- "style",
- "systemLanguage",
- "textLength",
- "transform",
- "x",
- "y"
- ],
- "textArea": [
- "editable",
- "focusHighlight",
- "focusable",
- "height",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "systemLanguage",
- "transform",
- "width",
- "x",
- "y"
- ],
- "textPath": [
- "alignment-baseline",
- "externalResourcesRequired",
- "href",
- "lengthAdjust",
- "method",
- "path",
- "requiredExtensions",
- "requiredFeatures",
- "side",
- "spacing",
- "startOffset",
- "style",
- "systemLanguage",
- "textLength"
- ],
- "title": [
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage"
- ],
- "tref": [
- "alignment-baseline",
- "dx",
- "dy",
- "externalResourcesRequired",
- "lengthAdjust",
- "requiredExtensions",
- "requiredFeatures",
- "rotate",
- "style",
- "systemLanguage",
- "textLength",
- "x",
- "y"
- ],
- "tspan": [
- "alignment-baseline",
- "dx",
- "dy",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "lengthAdjust",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "rotate",
- "style",
- "systemLanguage",
- "textLength",
- "x",
- "y"
- ],
- "unknown": [
- "requiredExtensions",
- "style",
- "systemLanguage"
- ],
- "use": [
- "alignment-baseline",
- "externalResourcesRequired",
- "focusHighlight",
- "focusable",
- "height",
- "href",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "style",
- "systemLanguage",
- "transform",
- "width",
- "x",
- "y"
- ],
- "video": [
- "begin",
- "dur",
- "end",
- "externalResourcesRequired",
- "fill",
- "focusHighlight",
- "focusable",
- "height",
- "initialVisibility",
- "max",
- "min",
- "nav-down",
- "nav-down-left",
- "nav-down-right",
- "nav-left",
- "nav-next",
- "nav-prev",
- "nav-right",
- "nav-up",
- "nav-up-left",
- "nav-up-right",
- "overlay",
- "preserveAspectRatio",
- "repeatCount",
- "repeatDur",
- "requiredExtensions",
- "requiredFeatures",
- "requiredFonts",
- "requiredFormats",
- "restart",
- "style",
- "syncBehavior",
- "syncMaster",
- "syncTolerance",
- "systemLanguage",
- "transform",
- "transformBehavior",
- "type",
- "width",
- "x",
- "y"
- ],
- "view": [
- "externalResourcesRequired",
- "preserveAspectRatio",
- "viewBox",
- "viewTarget",
- "zoomAndPan"
- ],
- "vkern": [
- "g1",
- "g2",
- "k",
- "u1",
- "u2"
- ]
-}
diff --git a/src/svgtags.json b/src/svgtags.json
deleted file mode 100644
index 26ab43c..0000000
--- a/src/svgtags.json
+++ /dev/null
@@ -1,103 +0,0 @@
-[
- "a",
- "altGlyph",
- "altGlyphDef",
- "altGlyphItem",
- "animate",
- "animateColor",
- "animateMotion",
- "animateTransform",
- "animation",
- "audio",
- "canvas",
- "circle",
- "clipPath",
- "color-profile",
- "cursor",
- "defs",
- "desc",
- "discard",
- "ellipse",
- "feBlend",
- "feColorMatrix",
- "feComponentTransfer",
- "feComposite",
- "feConvolveMatrix",
- "feDiffuseLighting",
- "feDisplacementMap",
- "feDistantLight",
- "feDropShadow",
- "feFlood",
- "feFuncA",
- "feFuncB",
- "feFuncG",
- "feFuncR",
- "feGaussianBlur",
- "feImage",
- "feMerge",
- "feMergeNode",
- "feMorphology",
- "feOffset",
- "fePointLight",
- "feSpecularLighting",
- "feSpotLight",
- "feTile",
- "feTurbulence",
- "filter",
- "font",
- "font-face",
- "font-face-format",
- "font-face-name",
- "font-face-src",
- "font-face-uri",
- "foreignObject",
- "g",
- "glyph",
- "glyphRef",
- "handler",
- "hatch",
- "hatchpath",
- "hkern",
- "iframe",
- "image",
- "line",
- "linearGradient",
- "listener",
- "marker",
- "mask",
- "mesh",
- "meshgradient",
- "meshpatch",
- "meshrow",
- "metadata",
- "missing-glyph",
- "mpath",
- "path",
- "pattern",
- "polygon",
- "polyline",
- "prefetch",
- "radialGradient",
- "rect",
- "script",
- "set",
- "solidColor",
- "solidcolor",
- "stop",
- "style",
- "svg",
- "switch",
- "symbol",
- "tbreak",
- "text",
- "textArea",
- "textPath",
- "title",
- "tref",
- "tspan",
- "unknown",
- "use",
- "video",
- "view",
- "vkern"
-]
diff --git a/test/runtests.jl b/test/runtests.jl
index 35b20b8..5342fac 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,69 +1,280 @@
-import Hyperscript
-import Hyperscript: m, m_html, m_svg, m_novalidate
+using Hyperscript
using Test
-#=
- tests validation errors
- - HTML
- - SVG
- - Combined
-
- types of errors
- - nan value
- - invalid tag name
- - invalid attribute name in general
- - invalid attribute name for the specific tag
-
-=#
+macro errors(expr)
+ quote
+ @test_throws ErrorException $expr
+ end
+end
-macro test_html_eq(x, s)
+macro renders(x, s)
quote
- Hyperscript.render($x) == $s
+ @test Hyperscript.render($x) == $s
end
end
-# plain tag
-@test_html_eq m("circle") " "
+# Convenience macro for strings with embedded double-quotes
+macro s_cmd(x)
+ x
+end
+
+## Tags
+# Can render tags
+@renders m("p") "
"
+# Cannot render nonempty tags
+@errors m("")
+# Tags can be <: AbstractString
+@renders m(SubString("xspan", 2)) " "
+# Tags *must* be <: AbstractString
+@test_throws MethodError m(1)
+@test_throws MethodError m('1')
+@test_throws MethodError m(1.0)
+# Tags are normalized to strip whitespace
+@test m("p") == m("p")
+@test m(" p ") == m("p")
+@test m("\tp\t") == m("p")
+
+## Attributes
+# Can render a tag with an attribute
+@renders m("p", name="value") "
"
+# Can render a tag with multiple attributes
+@test let x = string(m("p", a="x", b="y"))
+ # Account for the two possible attribute orderings
+ x == s`
` || x == s`
`
+end
+# Render tags with various non-string attribute values
+@renders m("p", name='7') s`
`
+@renders m("p", name=7) s`
`
+@renders m("p", name=7.0) s`
`
+# squishcase renders as squishcase
+@renders m("p"; squishname=7.0) s`
`
+# camelCase renders as kebab-case
+@renders m("p"; camelName=7.0) s`
`
+# kebab-case renders as kebab-case
+@renders m("p"; [Symbol("kebab-name") => 7]...) s`
`
+# Can start attribute names with numbers
+@renders m("p"; [Symbol("7-name") => 7]...) s`
`
+
+# Disallow NaN attribute values by default
+@errors m("p", name=NaN)
+# Disallow spaces in attribute names by default
+@errors m("p"; [Symbol("7 space name") => 7]...)
+
+# Passing a string as an attribute name preserves it un-normalized
+@renders Hyperscript.Node(Hyperscript.DEFAULT_HTMLSVG_CONTEXT, "p", [], ["camelName" => 7.0]) s`
`
+
+## Children
+# Can render children
+@renders m("p", "child") s`child
`
+# Can render multiple children
+@renders m("p", "childOne", "childTwo") s`childOnechildTwo
`
+
+# Can render multiply-typed children
+@renders m("p", "childOne", 2) s`childOne2
`
+# Can render Node children
+@renders m("p", m("p")) s`
`
+# Can render other non-String children
+@renders m("p", 1) s`1
`
+@renders m("p", 1.0) s`1.0
`
+@renders m("p", '1') s`1
`
+# Can render nodes with mixed-type children
+@renders m("p", m("span", "child", 1), 2) s`child1 2
`
+# Can render mixed-type children inside an array
+@renders m("p", [m("span", "child", 1), 2]) s`child1 2
`
-# tag with attribute
-@test_html_eq m("circle", cx=1) " "
+## Accessors
+@test Hyperscript.tag(m("p")) == "p"
+@test Hyperscript.attrs(m("p", attr="value")) == Dict{String,Any}("attr" => "value")
+@test Hyperscript.children(m("p", "child")) == Any["child"]
+
+## Generators, arrays, and tuples
+# Arrays are flattened
+@renders m("p", [1, 2, 3]) s`123
`
+# Generators are flattened
+@renders m("p", (x for x in 1:3)) s`123
`
+# Tuples are flattened
+@renders m("p", (1, 2, 3)) s`123
`
+# Ranges are not flattened
+@renders m("p", 1:3) s`1:3
`
+
+## Normalization of HTML- and SVG-specific attribute nanes
+# we don't normalize tag names
+@renders m("linearGradient") s` `
+@renders m("magicLinearGradient") s` `
+# for those special attributes we preserve camelCase
+@renders m("path", pathLength=7) s` `
+# for those special attributes we convert squishcase
+@renders m("path", pathlength=7) s` `
+# for those special attributes you can still bypass HTML normalization (but not validation)
+# by sending the value in as a String
+@renders Hyperscript.Node(Hyperscript.DEFAULT_HTMLSVG_CONTEXT, "path", [], ["pathlength" => 7]) s` `
+@renders Hyperscript.Node(Hyperscript.DEFAULT_HTMLSVG_CONTEXT, "path", [], ["path-length" => 7]) s` `
+
+# Void tags render as void tags
+@renders m("br") s` `
+@renders m("stop") s` `
+# Void tags are not allowed to have children
+@errors m("stop", "child")
+
+# Non-void tags render as non-void tags
+@renders m("div") s`
`
+@renders m("span") s` `
+
+# @tags
+@tags beep
+# The @tags macro declares a tag
+@renders beep("hello<") s`hello< `
+
+# @tags_noescape
+@tags_noescape boop
+# The @tags_noescape macro declares a tag with unescaped children
+@renders boop("hello<") s`hello< `
+
+
+# escape behavior
+# HTML-relevant characters are escaped
+@renders m("p", "<") s`<
`
+@renders m("p", "\"") s`"
`
+# Non-HTML Non-ASCII characters are not escaped; we assume a utf-8 charset
+@renders m("p", "—") s`—
`
+# Regular characters are not escaped
+@renders m("p", "x") s`x
`
+# Characters are escaped inside attribute names
+@renders m("p", attr="`
+# This is weird. Should we allow it?
+# m("p"; [Symbol(""<`
+@renders q("—") s`— `
+@renders q("\"") s`" `
+@renders q("x") s`x `
+# Noescape does not propagate and only applies to children, not attributes.
+# This is the most useful behavior — you only really want to not-escape the
+# contents of e.g. of