diff --git a/src/IJulia.jl b/src/IJulia.jl index 75ec946c..f1fb3022 100644 --- a/src/IJulia.jl +++ b/src/IJulia.jl @@ -29,6 +29,7 @@ function display_dict(x) elseif showable(image_jpeg, x) # don't send jpeg if we have png data[string(image_jpeg)] = limitstringmime(image_jpeg, x) end + # TODO: IJulia logic has changed? Seems to send both html and latex for DataFrame for example if showable(text_markdown, x) data[string(text_markdown)] = limitstringmime(text_markdown, x) elseif showable(text_html, x) diff --git a/src/Literate.jl b/src/Literate.jl index 92cf0410..03981486 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -475,7 +475,8 @@ function markdown(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...) end function execute_markdown!(io::IO, sb::Module, block::String, outputdir) - r, str = execute_block(sb, block) + # TODO: Deal with explicit display(...) calls + r, str, _ = execute_block(sb, block) plain_fence = "\n```\n" => "\n```" # issue #101: consecutive codefenced blocks need newline if r !== nothing && !REPL.ends_with_semicolon(block) for (mime, ext) in [(MIME("image/png"), ".png"), (MIME("image/jpeg"), ".jpeg")] @@ -621,7 +622,7 @@ function execute_notebook(nb) execution_count += 1 cell["execution_count"] = execution_count block = join(cell["source"]) - r, str = execute_block(sb, block) + r, str, display_dicts = execute_block(sb, block) # str should go into stream if !isempty(str) @@ -632,6 +633,27 @@ function execute_notebook(nb) push!(cell["outputs"], stream) end + # Some mimes need to be split into vectors of lines instead of a single string + # TODO: Seems like text/plain and text/latex are also split now, but not doing + # it seems to work fine. Leave for now. + function split_mime(dict) + for mime in ("image/svg+xml", "text/html") + if haskey(dict, mime) + dict[mime] = collect(Any, eachline(IOBuffer(dict[mime]), keep = true)) + end + end + return dict + end + + # Any explicit calls to display(...) + for dict in display_dicts + display_data = Dict{String,Any}() + display_data["output_type"] = "display_data" + display_data["metadata"] = Dict() + display_data["data"] = split_mime(dict) + push!(cell["outputs"], display_data) + end + # check if ; is used to suppress output r = REPL.ends_with_semicolon(block) ? nothing : r @@ -641,18 +663,10 @@ function execute_notebook(nb) execute_result["output_type"] = "execute_result" execute_result["metadata"] = Dict() execute_result["execution_count"] = execution_count - dd = Base.invokelatest(IJulia.display_dict, r) - # we need to split some mime types into vectors of lines instead of a single string - for mime in ("image/svg+xml", "text/html") - if haskey(dd, mime) - dd[mime] = collect(Any, eachline(IOBuffer(dd[mime]), keep = true)) - end - end - execute_result["data"] = dd - + dict = Base.invokelatest(IJulia.display_dict, r) + execute_result["data"] = split_mime(dict) push!(cell["outputs"], execute_result) end - end return nb end @@ -668,8 +682,32 @@ function sandbox() return m end +# Capture display for notebooks +struct LiterateDisplay <: AbstractDisplay + data::Vector + LiterateDisplay() = new([]) +end +function Base.display(ld::LiterateDisplay, x) + push!(ld.data, Base.invokelatest(IJulia.display_dict, x)) + return nothing +end +# TODO: Problematic to accept mime::MIME here? +function Base.display(ld::LiterateDisplay, mime::MIME, x) + r = Base.invokelatest(IJulia.limitstringmime, mime, x) + display_dicts = Dict{String,Any}(string(mime) => r) + # TODO: IJulia does this part below for unknown mimes + # if istextmime(mime) + # display_dicts["text/plain"] = r + # end + push!(ld.data, display_dicts) + return nothing +end + # Execute a code-block in a module and capture stdout/stderr and the result function execute_block(sb::Module, block::String) + # Push a capturing display on the displaystack + disp = LiterateDisplay() + pushdisplay(disp) # r is the result # status = (true|false) # _: backtrace @@ -677,6 +715,7 @@ function execute_block(sb::Module, block::String) r, status, _, str = Documenter.withoutput() do include_string(sb, block) end + popdisplay(disp) # Documenter.withoutput has a try-catch so should always end up here if !status error(""" $(sprint(showerror, r)) @@ -687,7 +726,7 @@ function execute_block(sb::Module, block::String) ``` """) end - return r, str + return r, str, disp.data end end # module diff --git a/test/runtests.jl b/test/runtests.jl index 2a7b1354..08546716 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -import Literate +import Literate, JSON import Literate: Chunk, MDChunk, CodeChunk using Test @@ -1110,6 +1110,42 @@ end end end write(inputfile, script) Literate.notebook(inputfile, outdir) + + # Calls to display(x) and display(mime, x) + script = """ + struct DF x end + Base.show(io::IO, ::MIME"text/plain", df::DF) = print(io, "DF(\$(df.x)) as text/plain") + Base.show(io::IO, ::MIME"text/html", df::DF) = print(io, "DF(\$(df.x)) as text/html") + Base.show(io::IO, ::MIME"text/latex", df::DF) = print(io, "DF(\$(df.x)) as text/latex") + #- + foreach(display, [DF(1), DF(2)]) + DF(3) + #- + display(MIME("text/latex"), DF(4)) + """ + write(inputfile, script) + Literate.notebook(inputfile, outdir) + json = JSON.parsefile(joinpath(outdir, "inputfile.ipynb")) + cells = json["cells"] + @test length(cells) == 4 + # Cell 2 has 3 outputs: 2 display and one execute result + cellout = cells[2]["outputs"] + @test length(cellout) == 3 + for i in 1:3 + exe_res = i == 3 + @test cellout[i]["output_type"] == (exe_res ? "execute_result" : "display_data") + @test keys(cellout[i]["data"]) == Set(("text/plain", "text/html")) + @test cellout[i]["data"]["text/plain"] == "DF($i) as text/plain" + @test cellout[i]["data"]["text/html"] == Any["DF($i) as text/html"] + @test haskey(cellout[i], "execution_count") == exe_res + end + # Cell 3 has one output from a single display call + cellout = cells[3]["outputs"] + @test length(cellout) == 1 + @test cellout[1]["output_type"] == "display_data" + @test keys(cellout[1]["data"]) == Set(("text/latex",)) + @test cellout[1]["data"]["text/latex"] == "DF(4) as text/latex" + @test !haskey(cellout[1], "execution_count") end end end end