Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "WebIO"
uuid = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29"
license = "MIT"
version = "0.8.21"
version = "0.8.22"

[deps]
AssetRegistry = "bf4720bc-e11a-5d0c-854e-bdca1663c893"
Expand All @@ -23,7 +23,7 @@ Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62"
AssetRegistry = "0.1.0"
FunctionalCollections = "0.5.0"
JSExpr = "0.5"
JSON = "0.18, 0.19, 0.20, 0.21"
JSON = "0.18, 0.19, 0.20, 0.21, 1"
Observables = "0.5"
Requires = "0.4.4, 0.5, 1.0.0"
WebSockets = "1.5.0, 1.6.0"
Expand Down
1 change: 1 addition & 0 deletions src/WebIO.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Base.Multimedia.istextmime(::WEBIO_NODE_MIME) = true
const WEBIO_APPLICATION_MIME = MIME"application/vnd.webio.application+html"
Base.Multimedia.istextmime(::WEBIO_APPLICATION_MIME) = true

include("jsoncompat.jl")
include("util.jl")
include("connection.jl")
include("syntax.jl")
Expand Down
17 changes: 17 additions & 0 deletions src/jsoncompat.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using JSON

# JSON.jl compatibility
#
# JSON.jl < 1 writes non-finite floats as `null`, whereas JSON.jl v1 throws on
# them by default. `jsonstring` keeps the JSON.jl < 1 behavior for everything
# WebIO serializes for the frontend (messages, rendered nodes and scopes). See
# also `JSEvalSerialization` in `syntax.jl`, which does the same for `js"..."`
# interpolation.
@static if isdefined(JSON, :JSONStyle) # JSON.jl >= 1
struct WebIOSerialization <: JSON.JSONStyle end
JSON.lower(::WebIOSerialization, x::AbstractFloat) = isfinite(x) ? x : nothing

jsonstring(x) = JSON.json(x; style=WebIOSerialization())
else
jsonstring(x) = JSON.json(x)
end
2 changes: 1 addition & 1 deletion src/providers/generic_http.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct WSConnection{T} <: WebIO.AbstractConnection
sock::T
end

Sockets.send(p::WSConnection, data) = writeguarded(p.sock, JSON.json(data))
Sockets.send(p::WSConnection, data) = writeguarded(p.sock, WebIO.jsonstring(data))
Base.isopen(p::WSConnection) = isopen(p.sock)

if !isfile(GENERIC_HTTP_BUNDLE_PATH)
Expand Down
2 changes: 1 addition & 1 deletion src/providers/mux.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function create_socket(req)
end

function Sockets.send(p::WebSockConnection, data)
write(p.sock, sprint(io->JSON.print(io,data)))
write(p.sock, WebIO.jsonstring(data))
end

Base.isopen(p::WebSockConnection) = isopen(p.sock)
Expand Down
2 changes: 1 addition & 1 deletion src/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ function register_renderable(::Type{T}) where T
end

function Base.show(io::IO, m::WEBIO_NODE_MIME, node::Node)
write(io, JSON.json(node))
write(io, jsonstring(node))
end

function Base.show(io::IO, m::MIME"text/html", x::Observable)
Expand Down
7 changes: 6 additions & 1 deletion src/rpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ WebIO-internal method to handle a request to invoke an RPC from the browser.
Looks up the requested RPC from the `registered_rpcs` dict and invokes the function using
the provided arguments and returns the result.
"""
function handle_rpc_request(request::Dict)
# `request` is annotated `AbstractDict` rather than `Dict` because it comes
# straight off the wire: JSON.jl v1 materializes objects as `JSON.Object`, which
# is an `AbstractDict` but not a `Dict`. A `Dict` annotation here fails to match,
# and `dispatch_request` turns the resulting MethodError into an error response,
# so the RPC silently never runs.
function handle_rpc_request(request::AbstractDict)
rpc_id = get(request, "rpcId", nothing)
rpc_hash = try parse(UInt, rpc_id) catch nothing end
rpc = get(registered_rpcs, rpc_hash, nothing)
Expand Down
2 changes: 1 addition & 1 deletion src/scope.jl
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ function onjs(ob::AbstractObservable, f)
end

function Base.show(io::IO, ::WEBIO_NODE_MIME, x::Scope)
write(io, JSON.json(render(x)))
write(io, jsonstring(render(x)))
end
function Base.show(io::IO, m::MIME"text/html", x::Scope)
show(io, m, render(x))
Expand Down
62 changes: 46 additions & 16 deletions src/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,13 @@ tojs(x) = x

Print Javascript code to `io` that constructs the equivalent of `x`.
"""
showjs(io, x::Any) = JSON.show_json(io, JSEvalSerialization(), x)
@static if isdefined(JSON, :JSONStyle) # JSON.jl >= 1
# `sort_keys=false` keeps object keys in `Dict` iteration order, which is
# what JSON.jl < 1 does; JSON.jl v1 sorts `Dict` keys by default.
showjs(io, x::Any) = JSON.json(io, x; style=JSEvalSerialization(), sort_keys=false)
else
showjs(io, x::Any) = JSON.show_json(io, JSEvalSerialization(), x)
end
showjs(io, x::AbstractString) = write(io, JSON.json(x))

"""
Expand Down Expand Up @@ -226,23 +232,47 @@ Base.:(==)(x::JSString, y::JSString) = x.s==y.s

JSON.lower(x::JSString) = JSON.lower(x.s)

const JSONContext = JSON.Writer.StructuralContext
const JSONSerialization = JSON.Serializations.CommonSerialization

struct JSEvalSerialization <: JSONSerialization end

# Only honoured on JSON.jl < 1; see below.
const verbose_json = Ref(false)

# adapted (very slightly) from JSON.jl test/serializer.jl
function JSON.show_json(io::JSONContext, ::JSEvalSerialization, x::JSString)
if verbose_json[]
first = true
for line in split(x.s, '\n')
!first && JSON.indent(io)
first = false
Base.print(io, line)
# `JSEvalSerialization` is the serialization under which a `JSString` is spliced
# in as raw JavaScript rather than quoted as a JSON string. It is what `showjs`,
# and hence interpolation into a `js"..."` literal, uses. The default
# serialization still quotes `JSString`s (see `JSON.lower` above), which is what
# keeps node props such as event handlers valid JSON.
#
# JSON.jl v1 replaced the `show_json`/serialization-context API with `JSON.json`
# and `JSONStyle`, so the two versions need different spellings of the same
# thing. `JSONContext` and `JSONSerialization` are aliases for JSON.jl < 1
# internals that have no v1 equivalent, so they are defined only on that branch.
@static if isdefined(JSON, :JSONStyle) # JSON.jl >= 1
struct JSEvalSerialization <: JSON.JSONStyle end

# A `JSONText` is written to the output verbatim. Note that this means
# `verbose_json[]` has no effect here: there is no v1 equivalent of
# re-indenting a raw fragment across lines.
JSON.lower(::JSEvalSerialization, x::JSString) = JSON.JSONText(x.s)

# Non-finite floats are written as `null` as JSON.jl < 1 does, where
# JSON.jl v1 would throw instead.
JSON.lower(::JSEvalSerialization, x::AbstractFloat) = isfinite(x) ? x : nothing
else
const JSONContext = JSON.Writer.StructuralContext
const JSONSerialization = JSON.Serializations.CommonSerialization

struct JSEvalSerialization <: JSONSerialization end

# adapted (very slightly) from JSON.jl test/serializer.jl
function JSON.show_json(io::JSONContext, ::JSEvalSerialization, x::JSString)
if verbose_json[]
first = true
for line in split(x.s, '\n')
!first && JSON.indent(io)
first = false
Base.print(io, line)
end
else
Base.print(io, x.s)
end
else
Base.print(io, x.s)
end
end
2 changes: 1 addition & 1 deletion src/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ function escape_json(s::String)
return replace(s, "/" => "\\/")
end

escape_json(x::Any) = escape_json(JSON.json(x))
escape_json(x::Any) = escape_json(jsonstring(x))
28 changes: 28 additions & 0 deletions test/communication.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,31 @@ import WebIO: dispatch
# end
#
# end

@testset "dispatching parsed messages" begin
# Messages reach `dispatch` as whatever the JSON parser produced. On
# JSON.jl v1 that is a `JSON.Object`, which is an `AbstractDict` but not a
# `Dict`, so a handler annotated `::Dict` fails to match and
# `dispatch_request` reports the MethodError back to the browser instead of
# running the RPC. Parse the message here rather than building a `Dict`, so
# the argument type is the one that actually comes off the wire.
conn = TestConn(Channel{Any}(1))
WebIO.tojs(split) # register `split` as an RPC

request = WebIO.JSON.parse("""
{
"type": "request",
"request": "rpc",
"requestId": "parsed",
"rpcId": "$(hash(split))",
"arguments": ["foo bar"]
}
""")
dispatch(conn, request)

response = take!(conn.channel)
@test response["requestId"] == "parsed"
@test !haskey(response, "error") # the handler itself failed to run
@test !haskey(response, "exception") # the RPC ran but threw
@test response["result"] == ["foo", "bar"]
end
22 changes: 22 additions & 0 deletions test/syntax.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using JSON
using Test
using WebIO

Expand Down Expand Up @@ -41,4 +42,25 @@ using WebIO
myvalue = js"a + 1"
@test js"x = $myvalue".s == "x = a + 1"
end

@testset "JSStrings nested in interpolated values" begin
handler = Dict("onclick" => js"alert(1)")

# Interpolating into a `js"..."` literal splices JSStrings in as raw
# JavaScript, at any depth.
@test js"f($handler)".s == "f({\"onclick\":alert(1)})"

# The default JSON serialization instead quotes them, which is what
# keeps node props such as event handlers valid JSON.
@test JSON.json(handler) == "{\"onclick\":\"alert(1)\"}"
end

@testset "Non-finite floats interpolate as null" begin
# JSON.jl v1 refuses to write NaN/Inf; interpolation into a `js"..."`
# literal writes them as `null`, as JSON.jl < 1 does.
data = [1.5, NaN, Inf, -Inf]
@test js"$data".s == "[1.5,null,null,null]"
trace = Dict("y" => data)
@test js"f($trace)".s == "f({\"y\":[1.5,null,null,null]})"
end
end
15 changes: 15 additions & 0 deletions test/util-tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ using WebIO: kebab2camel, camel2kebab
@test camelstrs[i] == kebabstrs[i] || camelstrs[i] == kebab_also_trues[i]
end
end

@testset "Non-finite floats serialize as null" begin
# JSON.jl v1 refuses to write NaN/Inf; WebIO writes them as `null`, as
# JSON.jl < 1 does.
data = [1.5, NaN, Inf, -Inf]
@test WebIO.jsonstring(data) == "[1.5,null,null,null]"
@test WebIO.escape_json(Dict("x" => data)) == "{\"x\":[1.5,null,null,null]}"

n = node(:div, style=Dict(:width => NaN))
@test occursin("\"width\":null", sprint(show, WebIO.WEBIO_NODE_MIME(), n))

s = Scope()
s["obs"] = Observable(NaN)
@test occursin("null", sprint(show, WebIO.WEBIO_NODE_MIME(), s))
end
Loading