Skip to content

Commit 5488fc6

Browse files
twavvstevengj
authored andcommitted
Add custom MIME types to display_dict. (#755)
* 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!
1 parent cc78660 commit 5488fc6

File tree

2 files changed

+143
-36
lines changed

2 files changed

+143
-36
lines changed

src/execute_request.jl

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,54 +5,113 @@
55
import Base.Libc: flush_cstdio
66
import Pkg
77

8-
const text_plain = MIME("text/plain")
9-
const image_svg = MIME("image/svg+xml")
10-
const image_png = MIME("image/png")
11-
const image_jpeg = MIME("image/jpeg")
12-
const text_markdown = MIME("text/markdown")
13-
const text_html = MIME("text/html")
14-
const text_latex = MIME("text/latex") # Jupyter expects this
15-
const text_latex2 = MIME("application/x-latex") # but this is more standard?
16-
const application_vnd_vega_v3 = MIME("application/vnd.vega.v3+json")
17-
const application_vnd_vegalite_v2 = MIME("application/vnd.vegalite.v2+json")
18-
const application_vnd_dataresource = MIME("application/vnd.dataresource+json")
8+
Base.showable(a::AbstractVector{<:MIME}, x) = any(m -> showable(m, x), a)
9+
10+
"""
11+
A vector of MIME types (or vectors of MIME types) that IJulia will try to
12+
render. IJulia will try to render every MIME type specified in the first level
13+
of the vector. If a vector of MIME types is specified, IJulia will include only
14+
the first MIME type that is renderable (this allows for the expression of
15+
priority and exclusion of redundant data).
16+
17+
For example, since "text/plain" is specified as a first-child of the array,
18+
IJulia will always try to include a "text/plain" representation of anything that
19+
is displayed. Since markdown and latex are specified within a sub-vector, IJulia
20+
will always try to render "text/markdown", and will only try to render
21+
"text/latex" if markdown isn't possible.
22+
"""
23+
const ijulia_mime_types = Vector{Union{MIME, AbstractVector{MIME}}}([
24+
MIME("text/plain"),
25+
MIME("image/svg+xml"),
26+
[MIME("image/png"),MIME("image/jpeg")],
27+
[
28+
MIME("text/markdown"),
29+
MIME("text/html"),
30+
MIME("text/latex"), # Jupyter expects this
31+
MIME("application/x-latex"), # but this is more standard?
32+
],
33+
])
34+
35+
"""
36+
MIME types that when rendered (via stringmime) return JSON data. See
37+
`ijulia_mime_types` for a description of how MIME types are selected.
38+
39+
This is necessary to embed the JSON as is in the displaydata bundle (rather than
40+
as stringify'd JSON).
41+
"""
42+
const ijulia_jsonmime_types = Vector{Union{MIME, Vector{MIME}}}([
43+
[MIME("application/vnd.vegalite.v2+json"), MIME("application/vnd.vega.v3+json")],
44+
MIME("application/vnd.dataresource+json"),
45+
])
46+
47+
register_mime(x::Union{MIME, Vector{MIME}})= push!(ijulia_mime_types, x)
48+
register_mime(x::AbstractVector{<:MIME}) = push!(ijulia_mime_types, Vector{Mime}(x))
49+
register_jsonmime(x::Union{MIME, Vector{MIME}}) = push!(ijulia_jsonmime_types, x)
50+
register_jsonmime(x::AbstractVector{<:MIME}) = push!(ijulia_jsonmime_types, Vector{Mime}(x))
1951

2052
include("magics.jl")
2153

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

26-
# return a String=>String dictionary of mimetype=>data
27-
# for passing to Jupyter display_data and execute_result messages.
28-
function display_dict(x)
29-
data = Dict{String,Any}("text/plain" => limitstringmime(text_plain, x))
30-
if showable(application_vnd_vegalite_v2, x)
31-
data[string(application_vnd_vegalite_v2)] = JSON.JSONText(limitstringmime(application_vnd_vegalite_v2, x))
32-
elseif showable(application_vnd_vega_v3, x) # don't send vega if we have vega-lite
33-
data[string(application_vnd_vega_v3)] = JSON.JSONText(limitstringmime(application_vnd_vega_v3, x))
34-
end
35-
if showable(application_vnd_dataresource, x)
36-
data[string(application_vnd_dataresource)] = JSON.JSONText(limitstringmime(application_vnd_dataresource, x))
58+
"""
59+
Generate the preferred MIME representation of x.
60+
61+
Returns a tuple with the selected MIME type and the representation of the data
62+
using that MIME type.
63+
"""
64+
function display_mimestring(mime_array::Vector{MIME}, x)
65+
for m in mime_array
66+
if showable(mime_array, x)
67+
return display_mimestring(m, x)
68+
end
3769
end
38-
if showable(image_svg, x)
39-
data[string(image_svg)] = limitstringmime(image_svg, x)
70+
error("No displayable MIME types in mime array.")
71+
end
72+
73+
display_mimestring(m::MIME, x) = (m, limitstringmime(m, x))
74+
75+
"""
76+
Generate the preferred json-MIME representation of x.
77+
78+
Returns a tuple with the selected MIME type and the representation of the data
79+
using that MIME type (as a `JSONText`).
80+
"""
81+
function display_mimejson(mime_array::Vector{MIME}, x)
82+
for m in mime_array
83+
if showable(mime_array, x)
84+
return display_mimejson(m, x)
85+
end
4086
end
41-
if showable(image_png, x)
42-
data[string(image_png)] = limitstringmime(image_png, x)
43-
elseif showable(image_jpeg, x) # don't send jpeg if we have png
44-
data[string(image_jpeg)] = limitstringmime(image_jpeg, x)
87+
error("No displayable MIME types in mime array.")
88+
end
89+
90+
display_mimejson(m::MIME, x) = (m, JSON.JSONText(limitstringmime(m, x)))
91+
92+
"""
93+
Generate a dictionary of `mime_type => data` pairs for all registered MIME
94+
types. This is the format that Jupyter expects in display_data and
95+
execute_result messages.
96+
"""
97+
function display_dict(x)
98+
data = Dict{String, Union{String, JSONText}}()
99+
for m in ijulia_mime_types
100+
if showable(m, x)
101+
mime, mime_repr = display_mimestring(m, x)
102+
data[string(mime)] = mime_repr
103+
end
45104
end
46-
if showable(text_markdown, x)
47-
data[string(text_markdown)] = limitstringmime(text_markdown, x)
48-
elseif showable(text_html, x)
49-
data[string(text_html)] = limitstringmime(text_html, x)
50-
elseif showable(text_latex, x)
51-
data[string(text_latex)] = limitstringmime(text_latex, x)
52-
elseif showable(text_latex2, x)
53-
data[string(text_latex)] = limitstringmime(text_latex2, x)
105+
106+
for m in ijulia_jsonmime_types
107+
if showable(m, x)
108+
mime, mime_repr = display_mimejson(m, x)
109+
data[string(mime)] = mime_repr
110+
end
54111
end
112+
55113
return data
114+
56115
end
57116

58117
# queue of objects to display at end of cell execution

test/execute_request.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
using Test
2+
using Base64, JSON
23

4+
import IJulia
35
import IJulia: helpmode, error_content, docdict
46

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

810
@test haskey(docdict("import"), "text/plain")
911
@test haskey(docdict("sum"), "text/plain")
12+
13+
struct FriendlyData
14+
name::AbstractString
15+
end
16+
17+
@testset "Custom MIME types" begin
18+
friend = FriendlyData("world")
19+
20+
FRIENDLY_MIME_TYPE = MIME"application/vnd.ijulia.friendly-text"
21+
FRIENDLY_MIME = FRIENDLY_MIME_TYPE()
22+
Base.Multimedia.istextmime(::FRIENDLY_MIME_TYPE) = true
23+
Base.show(io, ::FRIENDLY_MIME_TYPE, x::FriendlyData) = write(io, "Hello, $(x.name)!")
24+
IJulia.register_mime(FRIENDLY_MIME)
25+
26+
BINARY_MIME_TYPE = MIME"application/vnd.ijulia.friendly-binary"
27+
BINARY_MIME = BINARY_MIME_TYPE()
28+
Base.Multimedia.istextmime(::BINARY_MIME_TYPE) = false
29+
Base.show(io, ::BINARY_MIME_TYPE, x::FriendlyData) = write(io, "Hello, $(x.name)!")
30+
IJulia.register_mime(BINARY_MIME)
31+
32+
JSON_MIME_TYPE = MIME"application/vnd.ijulia.friendly-json"
33+
JSON_MIME = JSON_MIME_TYPE()
34+
Base.Multimedia.istextmime(::JSON_MIME_TYPE) = true
35+
Base.show(io, ::JSON_MIME_TYPE, x::FriendlyData) = write(io, JSON.json(Dict("name" => x.name)))
36+
IJulia.register_jsonmime(JSON_MIME)
37+
38+
FRIENDLY_MIME_TYPE_1 = MIME"application/vnd.ijulia.friendly-text-1"
39+
FRIENDLY_MIME_TYPE_2 = MIME"application/vnd.ijulia.friendly-text-2"
40+
FRIENDLY_MIME_1 = FRIENDLY_MIME_TYPE_1()
41+
FRIENDLY_MIME_2 = FRIENDLY_MIME_TYPE_2()
42+
FRIENDLY_MIME_TYPE_UNION = Union{FRIENDLY_MIME_TYPE_1, FRIENDLY_MIME_TYPE_2}
43+
Base.Multimedia.istextmime(::FRIENDLY_MIME_TYPE_UNION) = true
44+
Base.show(io, ::FRIENDLY_MIME_TYPE_UNION, x::FriendlyData) = write(io, "Hello, $(x.name)!")
45+
IJulia.register_mime([FRIENDLY_MIME_1, FRIENDLY_MIME_2])
46+
47+
# We stringify then re-parse the dict so that JSONText's are parsed as
48+
# actual JSON objects and we can index into them.
49+
data = JSON.parse(JSON.json(IJulia.display_dict(friend)))
50+
@test data[string(FRIENDLY_MIME)] == "Hello, world!"
51+
@test data[string(BINARY_MIME)] == base64encode("Hello, world!")
52+
@test data[string(JSON_MIME)]["name"] == "world"
53+
@test data[string(FRIENDLY_MIME_1)] == "Hello, world!"
54+
@test !haskey(data, string(FRIENDLY_MIME_2))
55+
56+
57+
end

0 commit comments

Comments
 (0)