diff --git a/src/execute_request.jl b/src/execute_request.jl index fe4c3fc3..88da0bc6 100644 --- a/src/execute_request.jl +++ b/src/execute_request.jl @@ -5,17 +5,49 @@ 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") @@ -23,36 +55,63 @@ include("magics.jl") # 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 diff --git a/test/execute_request.jl b/test/execute_request.jl index 52e125fe..3ac73b09 100644 --- a/test/execute_request.jl +++ b/test/execute_request.jl @@ -1,5 +1,7 @@ using Test +using Base64, JSON +import IJulia import IJulia: helpmode, error_content, docdict content = error_content(UndefVarError(:a)) @@ -7,3 +9,49 @@ content = error_content(UndefVarError(:a)) @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