diff --git a/docs/src/api/render.md b/docs/src/api/render.md
index 00e4de0f..48ce1096 100644
--- a/docs/src/api/render.md
+++ b/docs/src/api/render.md
@@ -6,6 +6,7 @@ See the [Extending WebIO](@ref) documentation for more information.
## Internal API
```@docs
+WebIO.@register_renderable
WebIO.register_renderable
WebIO.render
```
diff --git a/src/node.jl b/src/node.jl
index 50f4466e..5892bf9d 100644
--- a/src/node.jl
+++ b/src/node.jl
@@ -113,60 +113,6 @@ function mergeprops(n::Node, p, ps...)
setprops(n, out)
end
-# Rendering as HTML
-function JSON.lower(n::Node)
- result = Dict{String, Any}(
- "type" => "node",
- "nodeType" => nodetype(n),
- "instanceArgs" => JSON.lower(n.instanceof),
- "children" => map!(
- render,
- Vector{Any}(undef, length(children(n))),
- children(n),
- ),
- "props" => props(n),
- )
- return result
-end
-
-function Base.show(io::IO, m::MIME"text/html", x::Node)
- mountpoint_id = rand(UInt64)
- # Is there any way to only include the `require`-guard below for IJulia?
- # I think IJulia defines their own ::IO type.
- write(
- io,
- """
-
-
-
- """
- )
-end
-
-function Base.show(io::IO, m::WEBIO_NODE_MIME, node::Node)
- write(io, JSON.json(node))
-end
-
-Base.show(io::IO, m::MIME"text/html", x::Observable) = show(io, m, WebIO.render(x))
-Base.show(io::IO, m::WEBIO_NODE_MIME, x::Union{Observable, AbstractWidget}) = show(io, m, WebIO.render(x))
-
# Element extension syntax
(n::Node)(x, args...) = append(n, (x, args...))
(n::Node)(props::AbstractDict...) = mergeprops(n, props...)
diff --git a/src/render.jl b/src/render.jl
index deef846f..08b5513d 100644
--- a/src/render.jl
+++ b/src/render.jl
@@ -20,23 +20,6 @@ render(::Nothing) = ""
render(x::Any) =
dom"div"(; setInnerHtml=richest_html(x))
-const renderable_types = Type[]
-"""
- `WebIO.register_renderable(MyType::Type)`
-
-Registers that a WebIO.render method is available for instances of `MyType`.
-Allows WebIO to hook into the display machinery of backends such as Atom and
-IJulia to display the WebIO rendered version of the type as appropriate.
-
-Also defines a `Base.show(io::IO, m::MIME"text/html", x::MyType)` as
-`Base.show(io, m, WebIO.render(x))`
-"""
-function register_renderable(::Type{T}) where T
- @eval Base.show(io::IO, m::MIME"text/html", x::$T) = Base.show(io, m, WebIO.render(x))
- push!(renderable_types, T)
- return true
-end
-
"""
Called after a provider is setup
"""
@@ -174,4 +157,170 @@ end
render(w::AbstractWidget) = render(Widgets.render(w))
+function JSON.lower(n::Node)
+ result = Dict{String, Any}(
+ "type" => "node",
+ "nodeType" => nodetype(n),
+ "instanceArgs" => JSON.lower(n.instanceof),
+ "children" => map!(
+ render,
+ Vector{Any}(undef, length(children(n))),
+ children(n),
+ ),
+ "props" => props(n),
+ )
+ return result
+end
+
+function Base.show(io::IO, m::MIME"text/html", x::Node)
+ mountpoint_id = rand(UInt64)
+ # Is there any way to only include the `require`-guard below for IJulia?
+ # I think IJulia defines their own ::IO type.
+ write(
+ io,
+ """
+
+
+
+ """
+ )
+end
+
+"""
+A vector of types that are renderable by WebIO.
+
+This exists because for some providers, we need to create some methods when
+certain providers are initialized to allow those providers to display custom
+types.
+This will be removed in the (hopefully near-term) future as we remove providers
+out from WebIO and into the appropriate packages.
+"""
+const renderable_types = Type[]
+
+"""
+ @register_renderable(MyType)
+ @register_renderable(MyType) do
+ # Render definition
+ end
+
+Register a type as renderable by WebIO.
+This enables your type to be displayed in the appropriate WebIO frontends
+(e.g. Jupyter) without any additional work.
+
+This macro may be called either with just the type that you wish to mark as
+renderable or with the body of the [`WebIO.render`](@ref) method using do-block
+syntax.
+
+The do-block syntax requires parentheses around the typename.
+Additionally, due to inconsistencies in the way macros are resolved, the
+do-block syntax must be invoked using `@WebIO.register_renderable`
+(**not** `WebIO.@register_renderable`).
+If the `@WebIO.register_renderable` syntax looks ugly, it might be preferable
+to directly import the macro and use it without qualifying its name.
+
+This macro also defines a method for `Base.show` with the `text/html` MIME so
+you should not need to define your own.
+
+# Examples
+```julia
+struct ScatterPlot
+ x::Vector{Float64}
+ y::Vector{Float64}
+end
+
+# Do-block syntax
+# Note that the `@` comes before `WebIO`
+@WebIO.register_renderable(ScatterPlot) do plot
+ # Construct the scatter plot using DOM primitives...
+ return node(...)
+end
+
+# Do-block syntax with explicit import
+using WebIO: @register_renderable
+@register_renderable(ScatterPlot) do plot ... end
+
+# Type name syntax
+WebIO.render(plot::ScatterPlot) = node(...)
+@WebIO.register_renderable ScatterPlot
+```
+"""
+macro register_renderable(typename)
+ return register_renderable_macro_helper(esc(typename))
+end
+
+macro register_renderable(f::Expr, typename)
+ render_method_expr = quote
+ local f = $(esc(f))
+ WebIO.render(x::$(esc(typename))) = f(x)
+ end
+
+ result_expr = register_renderable_macro_helper(esc(typename))
+ push!(result_expr.args, render_method_expr)
+ return result_expr
+end
+
+function register_renderable_macro_helper(
+ typename::Union{Symbol, Expr, Type}
+)::Expr
+ return quote
+ push!(renderable_types, $typename)
+ function Base.show(
+ io::IO,
+ m::Union{MIME"text/html", WEBIO_NODE_MIME},
+ x::$typename,
+ )
+ return Base.show(io, m, WebIO.render(x))
+ end
+ end
+end
+
+"""
+ register_renderable(MyType)
+
+This function is deprecated. Please use [`WebIO.@register_renderable`](@ref)
+instead.
+
+This function was deprecated because it contained too much *magic* (since
+*magic* is firmly within the domain of macros).
+In particular, this function resorts to `eval`-ing new method definitions for
+the types passed into it which is not what a normal function is supposed to do.
+"""
+function register_renderable(::Type{T}) where T
+ Base.depwarn(
+ "`WebIO.register_renderable(Type)` is deprecated; use the "
+ * "`@WebIO.register_renderable Type` macro instead.",
+ :webio_register_renderable_function,
+ )
+ @eval $(register_renderable_macro_helper(T))
+end
+
+function Base.show(io::IO, m::WEBIO_NODE_MIME, node::Node)
+ write(io, JSON.json(node))
+end
+
+function Base.show(io::IO, m::MIME"text/html", x::Observable)
+ show(io, m, WebIO.render(x))
+end
+
+function Base.show(io::IO, m::WEBIO_NODE_MIME, x::Union{Observable, AbstractWidget})
+ show(io, m, WebIO.render(x))
+end
+
@deprecate render_internal render
diff --git a/test/deprecations.jl b/test/deprecations.jl
index 90dbbf78..5e189e86 100644
--- a/test/deprecations.jl
+++ b/test/deprecations.jl
@@ -9,3 +9,11 @@ using Test
# This surfaced in https://github.com/JuliaGizmos/Interact.jl/issues/315.
@test_deprecated !isempty(Scope("foo"; imports=["foo.js"]).imports)
end
+
+@testset "register_renderable function deprecation" begin
+ MyTypeName = gensym()
+ @eval struct $MyTypeName end
+ @eval WebIO.render(x::$MyTypeName) = x.dom
+ @test_deprecated @eval WebIO.register_renderable($MyTypeName)
+ @test hasmethod(Base.show, (IO, MIME"text/html", @eval($MyTypeName)))
+end
diff --git a/test/render.jl b/test/render.jl
new file mode 100644
index 00000000..70f71a8d
--- /dev/null
+++ b/test/render.jl
@@ -0,0 +1,63 @@
+using WebIO
+using Test
+
+# NOTE: We use `@eval` a lot here for two reasons.
+# 1. Because `struct` definitions are only allowed at the top level.
+# 2. To delay execution of the `@register_renderable` macro so that the macro
+# evaluation happens at test time (this avoids invalid invocations from
+# preventing the rest of the test suite from even running and is also
+# necessary to try to catch errors raised by the macro).
+
+@testset "WebIO.@register_renderable" begin
+
+ @testset "@register_renderable for symbol" begin
+ MyTypeName = gensym()
+ @eval struct $MyTypeName dom::WebIO.Node end
+ @eval WebIO.render(x::$MyTypeName) = x.dom
+ @eval WebIO.@register_renderable($MyTypeName)
+
+ MyType = @eval $MyTypeName
+ @test hasmethod(show, (IO, WebIO.WEBIO_NODE_MIME, MyType))
+ myinstance = MyType(node(:p, "Hello, world!"))
+ myinstance_json = sprint(show, WebIO.WEBIO_NODE_MIME(), myinstance)
+ myinstance_html = sprint(show, MIME("text/html"), myinstance)
+ @test occursin("Hello, world!", myinstance_json)
+ @test occursin("Hello, world!", myinstance_html)
+ @test MyType in WebIO.renderable_types
+ end
+
+ @testset "@register_renderable with do-block syntax" begin
+ MyTypeName = gensym()
+ @eval struct $MyTypeName dom::WebIO.Node end
+
+ MyType = @eval $MyTypeName
+ @eval @WebIO.register_renderable($MyType) do x
+ return x.dom
+ end
+
+ @test hasmethod(show, (IO, WebIO.WEBIO_NODE_MIME, MyType))
+ myinstance = MyType(node(:p, "Hello, world!"))
+ myinstance_json = sprint(show, WebIO.WEBIO_NODE_MIME(), myinstance)
+ myinstance_html = sprint(show, MIME("text/html"), myinstance)
+ @test occursin("Hello, world!", myinstance_json)
+ @test occursin("Hello, world!", myinstance_html)
+ end
+
+ @testset "@register_renderable multiple dispatch" begin
+ # This test is meant to make sure that the methods generated refer to
+ # the correct types.
+ TypeAName, TypeBName = gensym("TypeA"), gensym("TypeB")
+ @eval struct $TypeAName end
+ @eval struct $TypeBName end
+ TypeA, TypeB = (@eval $TypeAName, @eval $TypeBName)
+ @eval @WebIO.register_renderable($TypeA) do typea
+ return "Type A"
+ end
+ @eval @WebIO.register_renderable($TypeB) do typeb
+ return "Type B"
+ end
+
+ @test WebIO.render(TypeA()) == "Type A"
+ @test WebIO.render(TypeB()) == "Type B"
+ end
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index 65d4d5bf..c1db9e37 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -83,4 +83,5 @@ include("util-tests.jl")
include("ijulia-tests.jl")
include("syntax.jl")
include("deprecations.jl")
+include("render.jl")
include("asset.jl")