Skip to content

Commit

Permalink
Add custom MIME types to display_dict. (#755)
Browse files Browse the repository at this point in the history
* Add custom MIME types to display_dict.

* Add tests, fix markdown/latex priority.

* Improve documentation.

* Update MIME priority of png/jpeg and html.

* register_ijulia_mime -> register_mime

* Vector -> AbstractVector, add test cases for vector-of-MIMEs

* Fix more types!
  • Loading branch information
twavv authored and stevengj committed Oct 9, 2018
1 parent cc78660 commit 5488fc6
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 36 deletions.
131 changes: 95 additions & 36 deletions src/execute_request.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,113 @@
import Base.Libc: flush_cstdio
import Pkg

const text_plain = MIME("text/plain")
const image_svg = MIME("image/svg+xml")
const image_png = MIME("image/png")
const image_jpeg = MIME("image/jpeg")
const text_markdown = MIME("text/markdown")
const text_html = MIME("text/html")
const text_latex = MIME("text/latex") # Jupyter expects this
const text_latex2 = MIME("application/x-latex") # but this is more standard?
const application_vnd_vega_v3 = MIME("application/vnd.vega.v3+json")
const application_vnd_vegalite_v2 = MIME("application/vnd.vegalite.v2+json")
const application_vnd_dataresource = MIME("application/vnd.dataresource+json")
Base.showable(a::AbstractVector{<:MIME}, x) = any(m -> showable(m, x), a)

"""
A vector of MIME types (or vectors of MIME types) that IJulia will try to
render. IJulia will try to render every MIME type specified in the first level
of the vector. If a vector of MIME types is specified, IJulia will include only
the first MIME type that is renderable (this allows for the expression of
priority and exclusion of redundant data).
For example, since "text/plain" is specified as a first-child of the array,
IJulia will always try to include a "text/plain" representation of anything that
is displayed. Since markdown and latex are specified within a sub-vector, IJulia
will always try to render "text/markdown", and will only try to render
"text/latex" if markdown isn't possible.
"""
const ijulia_mime_types = Vector{Union{MIME, AbstractVector{MIME}}}([
MIME("text/plain"),
MIME("image/svg+xml"),
[MIME("image/png"),MIME("image/jpeg")],
[
MIME("text/markdown"),
MIME("text/html"),
MIME("text/latex"), # Jupyter expects this
MIME("application/x-latex"), # but this is more standard?
],
])

"""
MIME types that when rendered (via stringmime) return JSON data. See
`ijulia_mime_types` for a description of how MIME types are selected.
This is necessary to embed the JSON as is in the displaydata bundle (rather than
as stringify'd JSON).
"""
const ijulia_jsonmime_types = Vector{Union{MIME, Vector{MIME}}}([
[MIME("application/vnd.vegalite.v2+json"), MIME("application/vnd.vega.v3+json")],
MIME("application/vnd.dataresource+json"),
])

register_mime(x::Union{MIME, Vector{MIME}})= push!(ijulia_mime_types, x)
register_mime(x::AbstractVector{<:MIME}) = push!(ijulia_mime_types, Vector{Mime}(x))
register_jsonmime(x::Union{MIME, Vector{MIME}}) = push!(ijulia_jsonmime_types, x)
register_jsonmime(x::AbstractVector{<:MIME}) = push!(ijulia_jsonmime_types, Vector{Mime}(x))

include("magics.jl")

# return a String=>Any dictionary to attach as metadata
# in Jupyter display_data and pyout messages
metadata(x) = Dict()

# return a String=>String dictionary of mimetype=>data
# for passing to Jupyter display_data and execute_result messages.
function display_dict(x)
data = Dict{String,Any}("text/plain" => limitstringmime(text_plain, x))
if showable(application_vnd_vegalite_v2, x)
data[string(application_vnd_vegalite_v2)] = JSON.JSONText(limitstringmime(application_vnd_vegalite_v2, x))
elseif showable(application_vnd_vega_v3, x) # don't send vega if we have vega-lite
data[string(application_vnd_vega_v3)] = JSON.JSONText(limitstringmime(application_vnd_vega_v3, x))
end
if showable(application_vnd_dataresource, x)
data[string(application_vnd_dataresource)] = JSON.JSONText(limitstringmime(application_vnd_dataresource, x))
"""
Generate the preferred MIME representation of x.
Returns a tuple with the selected MIME type and the representation of the data
using that MIME type.
"""
function display_mimestring(mime_array::Vector{MIME}, x)
for m in mime_array
if showable(mime_array, x)
return display_mimestring(m, x)
end
end
if showable(image_svg, x)
data[string(image_svg)] = limitstringmime(image_svg, x)
error("No displayable MIME types in mime array.")
end

display_mimestring(m::MIME, x) = (m, limitstringmime(m, x))

"""
Generate the preferred json-MIME representation of x.
Returns a tuple with the selected MIME type and the representation of the data
using that MIME type (as a `JSONText`).
"""
function display_mimejson(mime_array::Vector{MIME}, x)
for m in mime_array
if showable(mime_array, x)
return display_mimejson(m, x)
end
end
if showable(image_png, x)
data[string(image_png)] = limitstringmime(image_png, x)
elseif showable(image_jpeg, x) # don't send jpeg if we have png
data[string(image_jpeg)] = limitstringmime(image_jpeg, x)
error("No displayable MIME types in mime array.")
end

display_mimejson(m::MIME, x) = (m, JSON.JSONText(limitstringmime(m, x)))

"""
Generate a dictionary of `mime_type => data` pairs for all registered MIME
types. This is the format that Jupyter expects in display_data and
execute_result messages.
"""
function display_dict(x)
data = Dict{String, Union{String, JSONText}}()
for m in ijulia_mime_types
if showable(m, x)
mime, mime_repr = display_mimestring(m, x)
data[string(mime)] = mime_repr
end
end
if showable(text_markdown, x)
data[string(text_markdown)] = limitstringmime(text_markdown, x)
elseif showable(text_html, x)
data[string(text_html)] = limitstringmime(text_html, x)
elseif showable(text_latex, x)
data[string(text_latex)] = limitstringmime(text_latex, x)
elseif showable(text_latex2, x)
data[string(text_latex)] = limitstringmime(text_latex2, x)

for m in ijulia_jsonmime_types
if showable(m, x)
mime, mime_repr = display_mimejson(m, x)
data[string(mime)] = mime_repr
end
end

return data

end

# queue of objects to display at end of cell execution
Expand Down
48 changes: 48 additions & 0 deletions test/execute_request.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,57 @@
using Test
using Base64, JSON

import IJulia
import IJulia: helpmode, error_content, docdict

content = error_content(UndefVarError(:a))
@test "UndefVarError" == content["ename"]

@test haskey(docdict("import"), "text/plain")
@test haskey(docdict("sum"), "text/plain")

struct FriendlyData
name::AbstractString
end

@testset "Custom MIME types" begin
friend = FriendlyData("world")

FRIENDLY_MIME_TYPE = MIME"application/vnd.ijulia.friendly-text"
FRIENDLY_MIME = FRIENDLY_MIME_TYPE()
Base.Multimedia.istextmime(::FRIENDLY_MIME_TYPE) = true
Base.show(io, ::FRIENDLY_MIME_TYPE, x::FriendlyData) = write(io, "Hello, $(x.name)!")
IJulia.register_mime(FRIENDLY_MIME)

BINARY_MIME_TYPE = MIME"application/vnd.ijulia.friendly-binary"
BINARY_MIME = BINARY_MIME_TYPE()
Base.Multimedia.istextmime(::BINARY_MIME_TYPE) = false
Base.show(io, ::BINARY_MIME_TYPE, x::FriendlyData) = write(io, "Hello, $(x.name)!")
IJulia.register_mime(BINARY_MIME)

JSON_MIME_TYPE = MIME"application/vnd.ijulia.friendly-json"
JSON_MIME = JSON_MIME_TYPE()
Base.Multimedia.istextmime(::JSON_MIME_TYPE) = true
Base.show(io, ::JSON_MIME_TYPE, x::FriendlyData) = write(io, JSON.json(Dict("name" => x.name)))
IJulia.register_jsonmime(JSON_MIME)

FRIENDLY_MIME_TYPE_1 = MIME"application/vnd.ijulia.friendly-text-1"
FRIENDLY_MIME_TYPE_2 = MIME"application/vnd.ijulia.friendly-text-2"
FRIENDLY_MIME_1 = FRIENDLY_MIME_TYPE_1()
FRIENDLY_MIME_2 = FRIENDLY_MIME_TYPE_2()
FRIENDLY_MIME_TYPE_UNION = Union{FRIENDLY_MIME_TYPE_1, FRIENDLY_MIME_TYPE_2}
Base.Multimedia.istextmime(::FRIENDLY_MIME_TYPE_UNION) = true
Base.show(io, ::FRIENDLY_MIME_TYPE_UNION, x::FriendlyData) = write(io, "Hello, $(x.name)!")
IJulia.register_mime([FRIENDLY_MIME_1, FRIENDLY_MIME_2])

# We stringify then re-parse the dict so that JSONText's are parsed as
# actual JSON objects and we can index into them.
data = JSON.parse(JSON.json(IJulia.display_dict(friend)))
@test data[string(FRIENDLY_MIME)] == "Hello, world!"
@test data[string(BINARY_MIME)] == base64encode("Hello, world!")
@test data[string(JSON_MIME)]["name"] == "world"
@test data[string(FRIENDLY_MIME_1)] == "Hello, world!"
@test !haskey(data, string(FRIENDLY_MIME_2))


end

0 comments on commit 5488fc6

Please sign in to comment.