diff --git a/.gitignore b/.gitignore
index 3a88d5b..5c59bec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
*.Manifest.toml
docs/build
*ipynb*
+*.DS_Store
diff --git a/Project.toml b/Project.toml
index f7510b4..82626d7 100644
--- a/Project.toml
+++ b/Project.toml
@@ -5,6 +5,7 @@ version = "0.5.3"
[deps]
DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85"
+EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
diff --git a/docs/make.jl b/docs/make.jl
index 488c681..ab69186 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -2,24 +2,22 @@ using Cobweb
using Cobweb: h, Page, CSS
using Markdown
-css = Dict(
- "html" => Dict(
- "font-family" => "Arial"
- )
-)
-
page = h.html(
h.head(
h.meta(charset="UTF-8"),
h.meta(name="viewport", content="width=device-width, initial-scale=1.0"),
h.title("Cobweb.jl Docs"),
- CSS(css),
+ css"""
+ html {
+ font-family: Arial;
+ }
+ """
),
h.body(
h.h1("This page was built with ", h.code("Cobweb.jl"), "."),
- Markdown.parse("""
+ md"""
Take a look at [`docs/make.jl`](https://github.com/joshday/Cobweb.jl/blob/main/docs/make.jl) inside the [`Cobweb.jl` repo](https://github.com/joshday/Cobweb.jl).
- """),
+ """,
h.button("Click Me for an alert!", onclick="buttonClicked()"),
Cobweb.Javascript("const buttonClicked = () => alert('This button was clicked!')"),
)
@@ -27,4 +25,4 @@ page = h.html(
index_html = touch(joinpath(mkpath(joinpath(@__DIR__, "build")), "index.html"))
-Cobweb.save(Page(page), index_html)
+open(io -> write(io, page), index_html, "w")
diff --git a/src/Cobweb.jl b/src/Cobweb.jl
index 937dc8f..ae1380b 100644
--- a/src/Cobweb.jl
+++ b/src/Cobweb.jl
@@ -3,37 +3,62 @@ module Cobweb
using DefaultApplication: DefaultApplication
using Scratch: @get_scratch!
using OrderedCollections: OrderedDict
+using EnumX
-export Page, Tab, IFrame
+export Page, Tab, h, preview, @js_str, @css_str, IFrame
#-----------------------------------------------------------------------------# init
-struct CobwebDisplay <: AbstractDisplay end
-
function __init__()
global DIR = @get_scratch!("CobWeb")
- isdefined(Main, :VSCodeServer) || pushdisplay(CobwebDisplay())
end
+#-----------------------------------------------------------------------------# preview
+function preview(content; reuse=true)
+ file = reuse ? joinpath(DIR, "index.html") : string(tempname(), ".html")
+ content2 = showable("text/html", content) ? content : HTML(content)
+ Base.open(io -> show(io, MIME("text/html"), content2), touch(file), "w")
+ DefaultApplication.open(file)
+end
+
+function Page(x)
+ Base.depwarn("Page(x) is deprecated. Use preview(x) instead.", :Page; force=true)
+ preview(x)
+end
+function Tab(x)
+ Base.depwarn("Tab(x) is deprecated. Use preview(x; reuse=false) instead.", :Tab; force=true)
+ preview(x; reuse=false)
+end
+
+#-----------------------------------------------------------------------------# consts & "enums"
+const HTML5_TAGS = [:a,:abbr,:address,:area,:article,:aside,:audio,:b,:base,:bdi,:bdo,:blockquote,:body,:br,:button,:canvas,:caption,:cite,:code,:col,:colgroup,:command,:datalist,:dd,:del,:details,:dfn,:dialog,:div,:dl,:dt,:em,:embed,:fieldset,:figcaption,:figure,:footer,:form,:h1,:h2,:h3,:h4,:h5,:h6,:head,:header,:hgroup,:hr,:html,:i,:iframe,:img,:input,:ins,:kbd,:label,:legend,:li,:link,:main,:map,:mark,:math,:menu,:menuitem,:meta,:meter,:nav,:noscript,:object,:ol,:optgroup,:option,:output,:p,:param,:picture,:pre,:progress,:q,:rb,:rp,:rt,:rtc,:ruby,:s,:samp,:script,:section,:select,:slot,:small,:source,:span,:strong,:style,:sub,:summary,:sup,:svg,:table,:tbody,:td,:template,:textarea,:tfoot,:th,:thead,:time,:title,:tr,:track,:u,:ul,:var,:video,:wbr]
+
+const VOID_ELEMENTS = [:area,:base,:br,:col,:command,:embed,:hr,:img,:input,:keygen,:link,:meta,:param,:source,:track,:wbr]
+
+const SVG2_TAGS = [:a,:animate,:animateMotion,:animateTransform,:audio,:canvas,:circle,:clipPath,: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,:foreignObject,:g,:iframe,:image,:line,:linearGradient,:marker,:mask,:metadata,:mpath,:path,:pattern,:polygon,:polyline,:radialGradient,:rect,:script,:set,:stop,:style,:svg,:switch,:symbol,:text,:textPath,:title,:tspan,:unknown,:use,:video,:view]
+
+const css_units = (ch="ch", cm="cm", em="em", ex="ex", fr="fr", in="in", mm="mm", pc="pc", percent="%",
+ pt="pt", px="px", rem="rem", vh="vh", vmax="vmax", vmin="vmin", vw="vw")
#-----------------------------------------------------------------------------# Node
"""
- Node(tag::String, attrs::Dict{String,String}, children::Vector)
+ Node(tag::Symbol, attrs::OrderedDict{Symbol,String}, children::Vector)
Should not often be used directly. See `?Cobweb.h`.
"""
struct Node
- tag::String
- attrs::OrderedDict{String,String}
+ tag::Symbol
+ attrs::OrderedDict{Symbol, String}
children::Vector{Any}
- function Node(tag::AbstractString, attrs::AbstractDict, children::AbstractVector)
- new(string(tag), OrderedDict(string(k) => string(v) for (k,v) in pairs(attrs)), collect(children))
- end
+end
+function Node(tag::Symbol, attrs::OrderedDict{Symbol, String}, children::AbstractVector)
+ tag in HTML5_TAGS || @warn "<$tag> is not a valid HTML5 tag."
+ Node(tag, attrs, collect(children))
end
tag(o::Node) = getfield(o, :tag)
attrs(o::Node) = getfield(o, :attrs)
children(o::Node) = getfield(o, :children)
-attrs(kw::AbstractDict) = OrderedDict(string(k) => string(v) for (k,v) in kw)
+attrs(kw::AbstractDict) = OrderedDict(Symbol(k) => string(v) for (k,v) in pairs(kw))
(o::Node)(x...; kw...) = Node(tag(o), merge(attrs(o), attrs(kw)), vcat(children(o), x...))
@@ -44,8 +69,8 @@ Base.getproperty(o::Node, class::String) = o(class = lstrip(get(o, :class, "") *
# methods that pass through to attrs(o)
Base.propertynames(o::Node) = Symbol.(keys(o))
-Base.getproperty(o::Node, name::Symbol) = attrs(o)[string(name)]
-Base.setproperty!(o::Node, name::Symbol, x) = attrs(o)[string(name)] = string(x)
+Base.getproperty(o::Node, name::Symbol) = attrs(o)[name]
+Base.setproperty!(o::Node, name::Symbol, x) = attrs(o)[name] = string(x)
Base.get(o::Node, name, val) = get(attrs(o), string(name), string(val))
Base.get!(o::Node, name, val) = get!(attrs(o), string(name), string(val))
Base.haskey(o::Node, name) = haskey(attrs(o), string(name))
@@ -61,6 +86,60 @@ Base.iterate(o::Node, state) = iterate(children(o), state)
Base.push!(o::Node, x) = push!(children(o), x)
Base.append!(o::Node, x) = append!(children(o), x)
+#-----------------------------------------------------------------------------# show Node
+function print_opening_tag(io::IO, o::Node; self_close::Bool = false)
+ print(io, '<', tag(o))
+ for (k,v) in attrs(o)
+ v == "true" ? print(io, ' ', k) : v != "false" && print(io, ' ', k, '=', '"', v, '"')
+ end
+ self_close && Symbol(tag(o)) ∉ VOID_ELEMENTS && length(children(o)) == 0 ?
+ print(io, " />") :
+ print(io, '>')
+end
+
+function Base.show(io::IO, o::Node)
+ p(args...) = print(io, args...)
+ print_opening_tag(io, o)
+ foreach(x -> showable("text/html", x) ? show(io, MIME("text/html"), x) : p(x), children(o))
+ p("", tag(o), '>')
+end
+
+Base.show(io::IO, ::MIME"text/html", node::Node) = show(io, node)
+Base.show(io::IO, ::MIME"text/xml", node::Node) = show(io, node)
+Base.show(io::IO, ::MIME"application/xml", node::Node) = show(io, node)
+
+Base.write(io::IO, node::Node) = show(io, MIME"text/html"(), node)
+
+function pretty(io::IO, o::Node; depth=get(io, :depth, 0), indent=get(io, :indent, " "), self_close = get(io, :self_close, true))
+ p(args...) = print(io, args...)
+ p(indent ^ depth)
+ print_opening_tag(io, o; self_close)
+ if length(children(o)) == 1 && !(only(o) isa Node)
+ x = only(o)
+ txt = showable("text/html", x) ? repr("text/html", x) : string(x)
+ if occursin('\n', txt)
+ println(io)
+ foreach(line -> p(indent ^ (depth+1), line, '\n'), lstrip.(split(txt, '\n')))
+ p(indent ^ depth, "", tag(o), '>')
+ else
+ p(txt)
+ p("", tag(o), '>')
+ end
+ elseif length(children(o)) > 1
+ child_io = IOContext(io, :depth => depth + 1, :indent => indent)
+ for child in children(o)
+ println(io)
+ pretty(child_io, child)
+ end
+ p('\n', indent ^ depth, "", tag(o), '>')
+ end
+end
+function pretty(io::IO, x; depth=get(io, :depth, 0), indent=get(io, :indent, " "))
+ print(io, indent ^ depth)
+ showable("text/html", x) ? show(io, MIME("text/html"), x) : print(io, x)
+end
+pretty(x; kw...) = (io = IOBuffer(); pretty(io, x; kw...); String(take!(io)))
+
#-----------------------------------------------------------------------------# h
"""
h(tag, children...; kw...)
@@ -81,16 +160,11 @@ h(tag, children...; kw...) = Node(tag, attrs(kw), collect(children))
h(tag, attrs::AbstractDict, children...) = Node(tag, attrs, collect(children))
-Base.getproperty(::typeof(h), tag::Symbol) = h(string(tag))
+Base.getproperty(::typeof(h), tag::Symbol) = h(tag)
+
Base.propertynames(::typeof(h)) = HTML5_TAGS
#-----------------------------------------------------------------------------# @h
-const HTML5_TAGS = [:a,:abbr,:address,:area,:article,:aside,:audio,:b,:base,:bdi,:bdo,:blockquote,:body,:br,:button,:canvas,:caption,:cite,:code,:col,:colgroup,:data,:datalist,:dd,:del,:details,:dfn,:dialog,:div,:dl,:dt,:em,:embed,:fieldset,:figcaption,:figure,:footer,:form,:h1,:h2,:h3,:h4,:h5,:h6,:head,:header,:hgroup,:hr,:html,:i,:iframe,:img,:input,:ins,:kbd,:label,:legend,:li,:link,:main,:map,:mark,:math,:menu,:menuitem,:meta,:meter,:nav,:noscript,:object,:ol,:optgroup,:option,:output,:p,:param,:picture,:pre,:progress,:q,:rb,:rp,:rt,:rtc,:ruby,:s,:samp,:script,:section,:select,:slot,:small,:source,:span,:strong,:style,:sub,:summary,:sup,:svg,:table,:tbody,:td,:template,:textarea,:tfoot,:th,:thead,:time,:title,:tr,:track,:u,:ul,:var,:video,:wbr]
-
-const VOID_ELEMENTS = [:area,:base,:br,:col,:command,:embed,:hr,:img,:input,:keygen,:link,:meta,:param,:source,:track,:wbr]
-
-SVG2_TAGS = [:a,:animate,:animateMotion,:animateTransform,:audio,:canvas,:circle,:clipPath,: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,:foreignObject,:g,:iframe,:image,:line,:linearGradient,:marker,:mask,:metadata,:mpath,:path,:pattern,:polygon,:polyline,:radialGradient,:rect,:script,:set,:stop,:style,:svg,:switch,:symbol,:text,:textPath,:title,:tspan,:unknown,:use,:video,:view]
-
macro h(ex)
esc(_h(ex))
end
@@ -106,12 +180,12 @@ function _h(ex::Expr)
end
ex
end
-_h(x::Symbol) = x in HTML5_TAGS ? Expr(:., :(Cobweb.h), QuoteNode(x)) : x
+_h(x::Symbol) = x in propertynames(typeof(h)) ? Expr(:., :(Cobweb.h), QuoteNode(x)) : x
#-----------------------------------------------------------------------------# escape
escape_chars = ['&' => "&", '"' => """, ''' => "'", '<' => "<", '>' => ">"]
-function escape(x; patterns = escape_chars)
+function escape(x::AbstractString; patterns = escape_chars)
for pat in patterns
x = replace(x, pat)
end
@@ -120,106 +194,64 @@ end
unescape(x::AbstractString) = escape(x; patterns = reverse.(escape_chars))
-#-----------------------------------------------------------------------------# show (html)
-function print_opening_tag(io::IO, o::Node; self_close::Bool = false)
- print(io, '<', tag(o))
- for (k,v) in attrs(o)
- v == "true" ? print(io, ' ', k) : v != "false" && print(io, ' ', k, '=', '"', v, '"')
- end
- self_close && Symbol(tag(o)) ∉ VOID_ELEMENTS && length(children(o)) == 0 ?
- print(io, " />") :
- print(io, '>')
-end
-
-function Base.show(io::IO, o::Node)
- p(args...) = print(io, args...)
- print_opening_tag(io, o)
- foreach(x -> showable("text/html", x) ? show(io, MIME("text/html"), x) : p(x), children(o))
- p("", tag(o), '>')
-end
-Base.show(io::IO, ::MIME"text/html", node::Node) = show(io, node)
-Base.show(io::IO, ::MIME"text/xml", node::Node) = show(io, node)
-Base.show(io::IO, ::MIME"application/xml", node::Node) = show(io, node)
-
-function pretty(io::IO, o::Node; depth=get(io, :depth, 0), indent=get(io, :indent, " "), self_close = get(io, :self_close, true))
- p(args...) = print(io, args...)
- p(indent ^ depth)
- print_opening_tag(io, o; self_close)
- if length(children(o)) == 1 && !(only(o) isa Node)
- x = only(o)
- txt = showable("text/html", x) ? repr("text/html", x) : string(x)
- if occursin('\n', txt)
- println(io)
- foreach(line -> p(indent ^ (depth+1), line, '\n'), lstrip.(split(txt, '\n')))
- p(indent ^ depth, "", tag(o), '>')
- else
- p(txt)
- p("", tag(o), '>')
- end
- elseif length(children(o)) > 1
- child_io = IOContext(io, :depth => depth + 1, :indent => indent)
- for child in children(o)
- println(io)
- pretty(child_io, child)
- end
- p('\n', indent ^ depth, "", tag(o), '>')
- end
-end
-function pretty(io::IO, x; depth=get(io, :depth, 0), indent=get(io, :indent, " "))
- print(io, indent ^ depth)
- showable("text/html", x) ? show(io, MIME("text/html"), x) : print(io, x)
-end
-pretty(x; kw...) = (io = IOBuffer(); pretty(io, x; kw...); String(take!(io)))
+#-----------------------------------------------------------------------------# Javascript
+"""
+ Javascript(content::String)
-#-----------------------------------------------------------------------------# show (javascript)
+String wrapper to identify content as Javascript. Will be displayed appropriately in text/javascript and text/html mime types.
+"""
struct Javascript
x::String
end
Base.show(io::IO, ::MIME"text/javascript", j::Javascript) = print(io, j.x)
Base.show(io::IO, ::MIME"text/html", j::Javascript) = print(io, "")
-#-----------------------------------------------------------------------------# CSS
+
+# TODO: validate Javascript content
"""
- CSS(::AbstractDict)
+ js"content"
-Write CSS with a nested dictionary with keys (`selector => (property => value)`).
+Same as `Javascript("content")`.
+"""
+macro js_str(x)
+ esc(Javascript(string(x)))
+end
-### Example
+#-----------------------------------------------------------------------------# CSS
+"""
+ CSS(content::String)
- CSS(Dict(
- "p" => Dict(
- "font-family" => "Arial",
- "text-transform" => "uppercase"
- )
- ))
+Wrapper to identify content as CSS. Will be displayed appropriately in text/css and text/html mime types.
"""
struct CSS
- content::OrderedDict{String, OrderedDict{String,String}}
- function CSS(o::AbstractDict)
- new(OrderedDict(string(k) => OrderedDict(string(k2) => string(v2) for (k2,v2) in pairs(v)) for (k,v) in pairs(o)))
- end
+ x::String
end
-function Base.show(io::IO, o::CSS)
- for (k,v) in o.content
+CSS(x::AbstractDict) = CSS(OrderedDict(string(k) => OrderedDict(string(k2) => string(v2) for (k2,v2) in pairs(v)) for (k,v) in pairs(x)))
+function CSS(x::OrderedDict{String, OrderedDict{String, String}})
+ io = IOBuffer()
+ for (k,v) in pairs(x)
println(io, k, " {")
for (k2, v2) in v
println(io, " ", k2, ':', ' ', v2, ';')
end
println(io, '}')
end
+ CSS(String(take!(io)))
end
-Base.show(io::IO, ::MIME"text/css", o::CSS) = print(io, o)
-Base.show(io::IO, ::MIME"text/html", o::CSS) = show(io, h.style(repr("text/css", o)))
-save(file::String, o::CSS) = save(o, file)
-save(o::CSS, file::String) = open(io -> show(io, x), touch(file), "w")
-
-#-----------------------------------------------------------------------------# CSSUnits
-baremodule CSSUnits
- using Base: @eval, string
- for k in [:ch,:cm,:em,:ex,:fr,:in,:mm,:pc,:percent,:pt,:px,:rem,:vh,:vmax,:vmin,:vw]
- @eval ($k = string($(QuoteNode(k))); export $k)
- end
+
+Base.show(io::IO, ::MIME"text/css", c::CSS) = print(io, c.x)
+Base.show(io::IO, ::MIME"text/html", c::CSS) = print(io, "")
+
+
+# TODO: validate css content
+"""
+ css"content"
+
+Same as `CSS("content")`.
+"""
+macro css_str(x)
+ esc(CSS(string(x)))
end
#-----------------------------------------------------------------------------# Doctype
@@ -227,8 +259,7 @@ struct Doctype
content::String
end
Doctype() = Doctype("")
-Base.show(io::IO, o::Doctype) = print(io, "")
-Base.show(io::IO, ::MIME"text/html", o::Doctype) = show(io, o)
+Base.show(io::IO, ::MIME"text/html", o::Doctype) = print(io, "")
#-----------------------------------------------------------------------------# Comment
"""
@@ -240,71 +271,23 @@ struct Comment
x::String
Comment(x) = new(string(x))
end
-Base.show(io::IO, o::Comment) = print(io, "")
-Base.show(io::IO, ::MIME"text/html", o::Comment) = show(io, o)
-
-#-----------------------------------------------------------------------------# Page
-"""
- Page(content)
-
-Wrapper to display `content` in your web browser. Assumes `content` has an available
-show method for `MIME("text/html")`.
-"""
-struct Page
- content
- function Page(content)
- is_html = showable("text/html", content)
- !is_html && @warn "Content ($(typeof(content))) does not have an HTML representation. Returning `Page(HTML(content))`."
- new(is_html ? content : HTML(content))
- end
-end
-Page(pg::Page) = pg
-
-save(file::String, page::Page) = save(page, file)
-
-function save(page::Page, file=joinpath(DIR, "index.html"))
- Base.open(io -> show(io, page), touch(file), "w")
- file
-end
-
-function Base.show(io::IO, o::Page)
- print(io, "")
- show(io, MIME("text/html"), o.content)
-end
-
-Base.show(io::IO, ::MIME"text/html", page::Page) = show(io, page)
-
-Base.display(::CobwebDisplay, page::Page) = DefaultApplication.open(save(page))
-
-#-----------------------------------------------------------------------------# Tab
-struct Tab
- content
-end
-save(file::String, tab::Tab) = save(tab, file)
-save(tab::Tab, file=string(tempname(), ".html")) = save(Page(tab.content), file)
-Base.display(::CobwebDisplay, t::Tab) = DefaultApplication.open(save(t))
+Base.show(io::IO, ::MIME"text/html", o::Comment) = print(io, "")
#-----------------------------------------------------------------------------# IFrame
"""
- IFrame(content; kw...)
+ IFrame(content; attrs...)
-Create an `