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, "") + 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, "") + # 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`

child12

` +# Can render mixed-type children inside an array +@renders m("p", [m("span", "child", 1), 2]) s`

child12

` -# 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