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")