Browse Source

Merge 42548626b3 into 5047103527

pull/220/merge
Simon Christ 2 years ago committed by GitHub
parent
commit
5099c391d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Project.toml
  2. 158
      src/Literate.jl

1
Project.toml

@ -4,6 +4,7 @@ version = "2.15.0"
[deps] [deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6"
IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89" IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

158
src/Literate.jl

@ -6,7 +6,7 @@ https://fredrikekre.github.io/Literate.jl/ for documentation.
""" """
module Literate module Literate
import JSON, REPL, IOCapture import JSON, REPL, IOCapture, CommonMark
include("IJulia.jl") include("IJulia.jl")
import .IJulia import .IJulia
@ -16,6 +16,7 @@ struct DefaultFlavor <: AbstractFlavor end
struct DocumenterFlavor <: AbstractFlavor end struct DocumenterFlavor <: AbstractFlavor end
struct CommonMarkFlavor <: AbstractFlavor end struct CommonMarkFlavor <: AbstractFlavor end
struct FranklinFlavor <: AbstractFlavor end struct FranklinFlavor <: AbstractFlavor end
struct CarpentriesFlavor <: AbstractFlavor end
# # Some simple rules: # # Some simple rules:
# #
@ -45,7 +46,7 @@ CodeChunk() = CodeChunk(String[], false)
ismdline(line) = (occursin(r"^\h*#$", line) || occursin(r"^\h*# .*$", line)) && !occursin(r"^\h*##", line) ismdline(line) = (occursin(r"^\h*#$", line) || occursin(r"^\h*# .*$", line)) && !occursin(r"^\h*##", line)
function parse(content; allow_continued = true) function parse(content; allow_continued=true)
lines = collect(eachline(IOBuffer(content))) lines = collect(eachline(IOBuffer(content)))
chunks = Chunk[] chunks = Chunk[]
@ -123,10 +124,10 @@ function parse(content; allow_continued = true)
end end
function replace_default(content, sym; function replace_default(content, sym;
config::Dict, config::Dict,
branch = "gh-pages", branch="gh-pages",
commit = "master" commit="master"
) )
repls = Pair{Any,Any}[] repls = Pair{Any,Any}[]
# add some shameless advertisement # add some shameless advertisement
@ -228,7 +229,7 @@ filename(str) = first(splitext(last(splitdir(str))))
isdocumenter(cfg) = cfg["flavor"]::AbstractFlavor isa DocumenterFlavor isdocumenter(cfg) = cfg["flavor"]::AbstractFlavor isa DocumenterFlavor
_DEFAULT_IMAGE_FORMATS = [(MIME("image/svg+xml"), ".svg"), (MIME("image/png"), ".png"), _DEFAULT_IMAGE_FORMATS = [(MIME("image/svg+xml"), ".svg"), (MIME("image/png"), ".png"),
(MIME("image/jpeg"), ".jpeg")] (MIME("image/jpeg"), ".jpeg")]
# Cache of inputfile => head branch # Cache of inputfile => head branch
const HEAD_BRANCH_CACHE = Dict{String,String}() const HEAD_BRANCH_CACHE = Dict{String,String}()
@ -246,7 +247,7 @@ function edit_commit(inputfile, user_config)
readchomp( readchomp(
pipeline( pipeline(
setenv(`$(git) rev-parse --show-toplevel`; dir=dirname(inputfile)); setenv(`$(git) rev-parse --show-toplevel`; dir=dirname(inputfile));
stderr=devnull, stderr=devnull
) )
) )
catch catch
@ -292,15 +293,15 @@ function create_configuration(inputfile; user_config, user_kwargs, type=nothing)
if (d = get(user_config, "documenter", nothing); d !== nothing) if (d = get(user_config, "documenter", nothing); d !== nothing)
if type === :md if type === :md
Base.depwarn("The documenter=$(d) keyword to Literate.markdown is deprecated." * Base.depwarn("The documenter=$(d) keyword to Literate.markdown is deprecated." *
" Pass `flavor = Literate.$(d ? "DocumenterFlavor" : "CommonMarkFlavor")()`" * " Pass `flavor = Literate.$(d ? "DocumenterFlavor" : "CommonMarkFlavor")()`" *
" instead.", Symbol("Literate.markdown")) " instead.", Symbol("Literate.markdown"))
user_config["flavor"] = d ? DocumenterFlavor() : CommonMarkFlavor() user_config["flavor"] = d ? DocumenterFlavor() : CommonMarkFlavor()
elseif type === :nb elseif type === :nb
Base.depwarn("The documenter=$(d) keyword to Literate.notebook is deprecated." * Base.depwarn("The documenter=$(d) keyword to Literate.notebook is deprecated." *
" It is not used anymore for notebook output.", Symbol("Literate.notebook")) " It is not used anymore for notebook output.", Symbol("Literate.notebook"))
elseif type === :jl elseif type === :jl
Base.depwarn("The documenter=$(d) keyword to Literate.script is deprecated." * Base.depwarn("The documenter=$(d) keyword to Literate.script is deprecated." *
" It is not used anymore for script output.", Symbol("Literate.script")) " It is not used anymore for script output.", Symbol("Literate.script"))
end end
end end
@ -420,7 +421,7 @@ Available options:
`$(_DEFAULT_IMAGE_FORMATS)`. Results which are `showable` with a MIME type are saved with `$(_DEFAULT_IMAGE_FORMATS)`. Results which are `showable` with a MIME type are saved with
the first match, with the corresponding extension. the first match, with the corresponding extension.
""" """
const DEFAULT_CONFIGURATION=nothing # Dummy const for documentation const DEFAULT_CONFIGURATION = nothing # Dummy const for documentation
function preprocessor(inputfile, outputdir; user_config, user_kwargs, type) function preprocessor(inputfile, outputdir; user_config, user_kwargs, type)
# Create configuration by merging default and userdefined # Create configuration by merging default and userdefined
@ -474,7 +475,7 @@ function preprocessor(inputfile, outputdir; user_config, user_kwargs, type)
content = replace_default(content, type; config=config) content = replace_default(content, type; config=config)
# parse the content into chunks # parse the content into chunks
chunks = parse(content; allow_continued = type !== :nb) chunks = parse(content; allow_continued=type !== :nb)
return chunks, config return chunks, config
end end
@ -527,6 +528,56 @@ function script(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwargs.
end end
#_______________________________________________________________________________________
# Define general functions needed for admonitions formating.
function containsAdmonition(chunk)
for line in chunk.lines
if startswith(strip(line.first * line.second), "!!!")
return true
end
end
return false
end
function chunkToMD(chunk)
parser = CommonMark.Parser()
CommonMark.enable!(parser, CommonMark.AdmonitionRule())
buffer = IOBuffer()
for line in chunk.lines
write(buffer, line.first * line.second, '\n')
end
str = String(take!(buffer))
return parser(str)
end
function rewriteContent!(mdContent)
for (node, entering) in mdContent
if isa(node.t, CommonMark.Admonition)
admonition = node.t
node.t = CommonMark.Paragraph()
if admonition.category == "yaml"
node.first_child.t = CommonMark.Text()
node.first_child.nxt.t = CommonMark.Paragraph()
CommonMark.insert_after(node.first_child.nxt.last_child, CommonMark.text("\n---\n"))
CommonMark.insert_before(node.first_child.nxt, CommonMark.text("---\n"))
else
CommonMark.insert_before(node, CommonMark.text(""":::::: $(admonition.category)
## $(admonition.title)
"""))
CommonMark.insert_after(node, CommonMark.text("::::::\n\n"))
end
end
end
mdContent
end
#_______________________________________________________________________________________
""" """
Literate.markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwargs...) Literate.markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwargs...)
@ -540,12 +591,37 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg
# preprocessing and parsing # preprocessing and parsing
chunks, config = chunks, config =
preprocessor(inputfile, outputdir; user_config=config, user_kwargs=kwargs, type=:md) preprocessor(inputfile, outputdir; user_config=config, user_kwargs=kwargs, type=:md)
# create the markdown file # create the markdown file
sb = sandbox()
iomd = IOBuffer() iomd = IOBuffer()
write_md_chunks!(iomd, chunks, outputdir, config)
# custom post-processing from user
content = config["postprocess"](String(take!(iomd)))
# write to file
outputfile = write_result(content, config)
return outputfile
end
function write_md_chunks!(iomd, chunks, outputdir, config)
flavor = config["flavor"]
sb = sandbox()
for (chunknum, chunk) in enumerate(chunks) for (chunknum, chunk) in enumerate(chunks)
if isa(chunk, MDChunk) if isa(chunk, MDChunk)
if flavor isa CarpentriesFlavor
if containsAdmonition(chunk)
md_chunk = chunkToMD(chunk)
rewriteContent!(md_chunk)
CommonMark.markdown(iomd, md_chunk)
continue
end
end
for line in chunk.lines for line in chunk.lines
write(iomd, line.second, '\n') # skip indent here write(iomd, line.second, '\n') # skip indent here
end end
@ -574,34 +650,30 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg
if execute if execute
cd(config["literate_outputdir"]) do cd(config["literate_outputdir"]) do
execute_markdown!(iomd, sb, join(chunk.lines, '\n'), outputdir; execute_markdown!(iomd, sb, join(chunk.lines, '\n'), outputdir;
inputfile=config["literate_inputfile"], inputfile=config["literate_inputfile"],
fake_source=config["literate_outputfile"], fake_source=config["literate_outputfile"],
flavor=config["flavor"], flavor=config["flavor"],
image_formats=config["image_formats"], image_formats=config["image_formats"],
file_prefix="$(config["name"])-$(chunknum)", file_prefix="$(config["name"])-$(chunknum)"
) )
end end
end end
end end
write(iomd, '\n') # add a newline between each chunk write(iomd, '\n') # add a newline between each chunk
end end
end
# custom post-processing from user
content = config["postprocess"](String(take!(iomd)))
# write to file
outputfile = write_result(content, config)
return outputfile
end
function execute_markdown!(io::IO, sb::Module, block::String, outputdir; function execute_markdown!(io::IO, sb::Module, block::String, outputdir;
inputfile::String, fake_source::String, inputfile::String, fake_source::String,
flavor::AbstractFlavor, image_formats::Vector, file_prefix::String) flavor::AbstractFlavor, image_formats::Vector, file_prefix::String)
# TODO: Deal with explicit display(...) calls # TODO: Deal with explicit display(...) calls
r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source) r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source)
# issue #101: consecutive codefenced blocks need newline # issue #101: consecutive codefenced blocks need newline
# issue #144: quadruple backticks allow for triple backticks in the output # issue #144: quadruple backticks allow for triple backticks in the output
plain_fence = "\n````\n" => "\n````" plain_fence = "\n````$(flavor == CarpentriesFlavor() ? "output" : "")\n" => "\n````"
# Here CarpentiresFlavor fork...
if r !== nothing && !REPL.ends_with_semicolon(block) if r !== nothing && !REPL.ends_with_semicolon(block)
if (flavor isa FranklinFlavor || flavor isa DocumenterFlavor) && if (flavor isa FranklinFlavor || flavor isa DocumenterFlavor) &&
Base.invokelatest(showable, MIME("text/html"), r) Base.invokelatest(showable, MIME("text/html"), r)
@ -647,8 +719,8 @@ function parse_nbmeta(line)
# Cf. https://jupytext.readthedocs.io/en/latest/formats.html#the-percent-format # Cf. https://jupytext.readthedocs.io/en/latest/formats.html#the-percent-format
m = match(r"^%% ([^[{]+)?\s*(?:\[(\w+)\])?\s*(\{.*)?$", line) m = match(r"^%% ([^[{]+)?\s*(?:\[(\w+)\])?\s*(\{.*)?$", line)
typ = m.captures[2] typ = m.captures[2]
name = m.captures[1] === nothing ? Dict{String, String}() : Dict("name" => m.captures[1]) name = m.captures[1] === nothing ? Dict{String,String}() : Dict("name" => m.captures[1])
meta = m.captures[3] === nothing ? Dict{String, Any}() : JSON.parse(m.captures[3]) meta = m.captures[3] === nothing ? Dict{String,Any}() : JSON.parse(m.captures[3])
return typ, merge(name, meta) return typ, merge(name, meta)
end end
line_is_nbmeta(line::Pair) = line_is_nbmeta(line.second) line_is_nbmeta(line::Pair) = line_is_nbmeta(line.second)
@ -671,7 +743,7 @@ function notebook(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg
nb = jupyter_notebook(chunks, config) nb = jupyter_notebook(chunks, config)
# write to file # write to file
outputfile = write_result(nb, config; print = (io, c)->JSON.print(io, c, 1)) outputfile = write_result(nb, config; print=(io, c) -> JSON.print(io, c, 1))
return outputfile return outputfile
end end
@ -690,11 +762,11 @@ function jupyter_notebook(chunks, config)
metatype !== nothing && metatype != chunktype && error("specifying a different cell type is not supported") metatype !== nothing && metatype != chunktype && error("specifying a different cell type is not supported")
popfirst!(chunk.lines) popfirst!(chunk.lines)
else else
metadata = Dict{String, Any}() metadata = Dict{String,Any}()
end end
lines = isa(chunk, MDChunk) ? lines = isa(chunk, MDChunk) ?
String[x.second for x in chunk.lines] : # skip indent String[x.second for x in chunk.lines] : # skip indent
chunk.lines chunk.lines
@views map!(x -> x * '\n', lines[1:end-1], lines[1:end-1]) @views map!(x -> x * '\n', lines[1:end-1], lines[1:end-1])
cell["cell_type"] = chunktype cell["cell_type"] = chunktype
cell["metadata"] = metadata cell["metadata"] = metadata
@ -711,15 +783,15 @@ function jupyter_notebook(chunks, config)
metadata = Dict() metadata = Dict()
kernelspec = Dict() kernelspec = Dict()
kernelspec["language"] = "julia" kernelspec["language"] = "julia"
kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)" kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)"
kernelspec["display_name"] = "Julia $(string(VERSION))" kernelspec["display_name"] = "Julia $(string(VERSION))"
metadata["kernelspec"] = kernelspec metadata["kernelspec"] = kernelspec
language_info = Dict() language_info = Dict()
language_info["file_extension"] = ".jl" language_info["file_extension"] = ".jl"
language_info["mimetype"] = "application/julia" language_info["mimetype"] = "application/julia"
language_info["name"]= "julia" language_info["name"] = "julia"
language_info["version"] = string(VERSION) language_info["version"] = string(VERSION)
metadata["language_info"] = language_info metadata["language_info"] = language_info
@ -733,7 +805,7 @@ function jupyter_notebook(chunks, config)
try try
cd(config["literate_outputdir"]) do cd(config["literate_outputdir"]) do
nb = execute_notebook(nb; inputfile=config["literate_inputfile"], nb = execute_notebook(nb; inputfile=config["literate_inputfile"],
fake_source=config["literate_outputfile"]) fake_source=config["literate_outputfile"])
end end
catch err catch err
@error "error when executing notebook based on input file: " * @error "error when executing notebook based on input file: " *
@ -759,7 +831,7 @@ function execute_notebook(nb; inputfile::String, fake_source::String)
stream = Dict{String,Any}() stream = Dict{String,Any}()
stream["output_type"] = "stream" stream["output_type"] = "stream"
stream["name"] = "stdout" stream["name"] = "stdout"
stream["text"] = collect(Any, eachline(IOBuffer(String(str)), keep = true)) stream["text"] = collect(Any, eachline(IOBuffer(String(str)), keep=true))
push!(cell["outputs"], stream) push!(cell["outputs"], stream)
end end
@ -769,7 +841,7 @@ function execute_notebook(nb; inputfile::String, fake_source::String)
function split_mime(dict) function split_mime(dict)
for mime in ("image/svg+xml", "text/html") for mime in ("image/svg+xml", "text/html")
if haskey(dict, mime) if haskey(dict, mime)
dict[mime] = collect(Any, eachline(IOBuffer(dict[mime]), keep = true)) dict[mime] = collect(Any, eachline(IOBuffer(dict[mime]), keep=true))
end end
end end
return dict return dict
@ -849,7 +921,7 @@ function execute_block(sb::Module, block::String; inputfile::String, fake_source
# - c.output: combined stdout and stderr # - c.output: combined stdout and stderr
# `rethrow = Union{}` means that we try-catch all the exceptions thrown in the do-block # `rethrow = Union{}` means that we try-catch all the exceptions thrown in the do-block
# and return them via the return value (they get handled below). # and return them via the return value (they get handled below).
c = IOCapture.capture(rethrow = Union{}) do c = IOCapture.capture(rethrow=Union{}) do
include_string(sb, block, fake_source) include_string(sb, block, fake_source)
end end
popdisplay(disp) # IOCapture.capture has a try-catch so should always end up here popdisplay(disp) # IOCapture.capture has a try-catch so should always end up here

Loading…
Cancel
Save