Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement non-standard MIME handling #23

Merged
merged 9 commits into from
Feb 1, 2024
22 changes: 19 additions & 3 deletions src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ function raw_markdown_chunks(path::String)
"Cannot handle an `eval` code cell option with value $(repr(evaluate)), only true or false.",
)
end
push!(raw_chunks, (type = :code, source, file = path, line, evaluate))
push!(
raw_chunks,
(type = :code, source, file = path, line, evaluate, cell_options),
)
end
end
if terminal_line <= length(source_lines)
Expand Down Expand Up @@ -300,7 +303,14 @@ function raw_script_chunks(path::String)
end
push!(
raw_chunks,
(type = :code, source, file = path, line = start_line, evaluate),
(;
type = :code,
source,
file = path,
line = start_line,
evaluate,
cell_options,
),
)
elseif type == :markdown
try
Expand Down Expand Up @@ -407,7 +417,12 @@ function evaluate_raw_cells!(
if chunk.evaluate
# Offset the line number by 1 to account for the triple backticks
# that are part of the markdown syntax for code blocks.
expr = :(render($chunk.source, $(chunk.file), $(chunk.line + 1)))
expr = :(render(
$chunk.source,
$(chunk.file),
$(chunk.line + 1),
$(chunk.cell_options),
))
remote = Malt.remote_eval_fetch(f.worker, expr)
processed = process_results(remote.results)

Expand Down Expand Up @@ -546,6 +561,7 @@ function process_results(dict::Dict{String,@NamedTuple{error::Bool, data::Vector
funcs = Dict(
"application/json" => json_reader,
"text/plain" => String,
"text/markdown" => String,
"text/html" => String,
"text/latex" => String,
"image/svg+xml" => String,
Expand Down
126 changes: 111 additions & 15 deletions src/worker.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,15 @@ function worker_init(f::File)
return nothing
end

function render(code::AbstractString, file::AbstractString, line::Integer)
function render(
code::AbstractString,
file::AbstractString,
line::Integer,
cell_options::AbstractDict,
)
captured =
Base.@invokelatest include_str(WORKSPACE[], code; file = file, line = line)
results = Base.@invokelatest render_mimetypes(captured.value)
results = Base.@invokelatest render_mimetypes(captured.value, cell_options)
return (;
results,
output = captured.output,
Expand Down Expand Up @@ -174,7 +179,23 @@ function worker_init(f::File)
end

# passing our module removes Main.Notebook noise when printing types etc.
with_context(io::IO) = IOContext(io, :module => WORKSPACE[], :color => true)
function with_context(io::IO, cell_options = Dict{String,Any}())
return IOContext(
io,
:module => WORKSPACE[],
:color => true,
# This allows a `show` method implementation to check for
# metadata that may be of relevance to it's rendering. For
# example, if a `typst` table is rendered with a caption
# (available in the `cell_options`) then we need to adjust the
# syntax that is output via the `QuartoNotebookRunner/typst`
# show method to switch between `markdown` and `code` "mode".
#
# TODO: perhaps preprocess the metadata provided here rather
# than just passing it through as-is.
:QuartoNotebookRunner => (; cell_options, frontmatter = FRONTMATTER[]),
)
end

function clean_bt_str(is_error::Bool, bt, err, prefix = "", mimetype = false)
is_error || return UInt8[]
Expand All @@ -200,21 +221,70 @@ function worker_init(f::File)
return take!(buf)
end

function render_mimetypes(value)
# TODO: where is this key?
function _to_format()
fm = FRONTMATTER[]
if haskey(fm, "pandoc")
pandoc = fm["pandoc"]
if isa(pandoc, Dict) && haskey(pandoc, "to")
to = pandoc["to"]
isa(to, String) && return to
end
end
return nothing
end
MichaelHatherly marked this conversation as resolved.
Show resolved Hide resolved

function render_mimetypes(value, cell_options)
to_format = _to_format()
result = Dict{String,@NamedTuple{error::Bool, data::Vector{UInt8}}}()
mimes = [
"text/plain",
"text/html",
"text/latex",
"image/svg+xml",
"image/png",
"application/json",
]
# Some output formats that we want to write to need different
# handling of valid MIME types. Currently `docx` and `typst`. When
# we detect that the `to` format is one of these then we select a
# different set of MIME types to try and render the value as. The
# `QuartoNotebookRunner/*` MIME types are unique to this package and
# are how package authors can hook into the display system used here
# to allow their types to be rendered correctly in different
# outputs.
#
# NOTE: We may revise this approach at any point in time and these
# should be considered implementation details until officially
# documented.
mime_groups = Dict(
"docx" => [
"QuartoNotebookRunner/openxml",
"text/plain",
"text/latex",
"text/html",
MichaelHatherly marked this conversation as resolved.
Show resolved Hide resolved
"image/svg+xml",
"image/png",
],
"typst" => [
"QuartoNotebookRunner/typst",
"text/plain",
"text/latex",
"image/svg+xml",
"image/png",
],
)
mimes = get(mime_groups, to_format) do
[
"text/plain",
"text/html",
"text/latex",
"image/svg+xml",
"image/png",
"application/json",
]
end
for mime in mimes
if showable(mime, value)
buffer = IOBuffer()
try
Base.@invokelatest show(with_context(buffer), mime, value)
Base.@invokelatest show(
with_context(buffer, cell_options),
mime,
value,
)
catch error
backtrace = catch_backtrace()
result[mime] = (;
Expand All @@ -229,20 +299,46 @@ function worker_init(f::File)
)
continue
end
# See whether the current MIME type needs to be handled
# specially and embedded in a raw markdown block and whether
# we should skip attempting to render any other MIME types
# to may match.
skip_other_mimes, new_mime, new_buffer = _transform_output(mime, buffer)
# Only send back the bytes, we do the processing of the
# data on the parent process where we have access to
# whatever packages we need, e.g. working out the size
# of a PNG image or converting a JSON string to an
# actual JSON object that avoids double serializing it
# in the notebook output.
result[mime] = (; error = false, data = take!(buffer))
result[new_mime] = (; error = false, data = take!(new_buffer))
skip_other_mimes && break
end
end
return result
end
render_mimetypes(value::Nothing) =
render_mimetypes(value::Nothing, cell_options) =
Dict{String,@NamedTuple{error::Bool, data::Vector{UInt8}}}()

# Our custom MIME types need special handling. They get rendered to
# `text/markdown` blocks with the original content wrapped in a raw
# markdown block. MIMEs that don't match just get passed through.
function _transform_output(mime::String, buffer::IO)
mapping = Dict(
"QuartoNotebookRunner/openxml" => (true, "text/markdown", "openxml"),
"QuartoNotebookRunner/typst" => (true, "text/markdown", "typst"),
MichaelHatherly marked this conversation as resolved.
Show resolved Hide resolved
)
if haskey(mapping, mime)
(skip_other_mimes, mime, raw) = mapping[mime]
io = IOBuffer()
println(io, "```{=$raw}")
println(io, rstrip(read(seekstart(buffer), String)))
println(io, "```")
return (skip_other_mimes, mime, io)
else
return (false, mime, buffer)
end
end

# Integrations:

function ojs_define(; kwargs...)
Expand Down
Loading