Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @register_renderable macro. #306

Merged
merged 8 commits into from
Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/api/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ See the [Extending WebIO](@ref) documentation for more information.

## Internal API
```@docs
WebIO.@register_renderable
WebIO.register_renderable
WebIO.render
```
Expand Down
54 changes: 0 additions & 54 deletions src/node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"""
<div
class="webio-mountpoint"
data-webio-mountpoint="$(mountpoint_id)"
>
<script>
if (window.require && require.defined && require.defined("nbextensions/webio/main")) {
console.log("Jupyter WebIO extension detected, not mounting.");
} else if (window.WebIO) {
WebIO.mount(
document.querySelector('[data-webio-mountpoint="$(mountpoint_id)"]'),
$(escape_json(x)),
window,
);
} else {
document
.querySelector('[data-webio-mountpoint="$(mountpoint_id)"]')
.innerHTML = '<strong>WebIO not detected.</strong>';
}
</script>
</div>
"""
)
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...)
Expand Down
183 changes: 166 additions & 17 deletions src/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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,
"""
<div
class="webio-mountpoint"
data-webio-mountpoint="$(mountpoint_id)"
>
<script>
if (window.require && require.defined && require.defined("nbextensions/webio/main")) {
console.log("Jupyter WebIO extension detected, not mounting.");
} else if (window.WebIO) {
WebIO.mount(
document.querySelector('[data-webio-mountpoint="$(mountpoint_id)"]'),
$(escape_json(x)),
window,
);
} else {
document
.querySelector('[data-webio-mountpoint="$(mountpoint_id)"]')
.innerHTML = '<strong>WebIO not detected.</strong>';
}
</script>
</div>
"""
)
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
8 changes: 8 additions & 0 deletions test/deprecations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
63 changes: 63 additions & 0 deletions test/render.jl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ include("util-tests.jl")
include("ijulia-tests.jl")
include("syntax.jl")
include("deprecations.jl")
include("render.jl")
include("asset.jl")