diff --git a/Project.toml b/Project.toml index c8cfde1..bfdd70a 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ version = "2.15.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" diff --git a/src/Literate.jl b/src/Literate.jl index f7c964d..93951be 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -6,7 +6,7 @@ https://fredrikekre.github.io/Literate.jl/ for documentation. """ module Literate -import JSON, REPL, IOCapture +import JSON, REPL, IOCapture, CommonMark include("IJulia.jl") import .IJulia @@ -16,6 +16,7 @@ struct DefaultFlavor <: AbstractFlavor end struct DocumenterFlavor <: AbstractFlavor end struct CommonMarkFlavor <: AbstractFlavor end struct FranklinFlavor <: AbstractFlavor end +struct CarpentriesFlavor <: AbstractFlavor end # # 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) -function parse(content; allow_continued = true) +function parse(content; allow_continued=true) lines = collect(eachline(IOBuffer(content))) chunks = Chunk[] @@ -123,10 +124,10 @@ function parse(content; allow_continued = true) end function replace_default(content, sym; - config::Dict, - branch = "gh-pages", - commit = "master" - ) + config::Dict, + branch="gh-pages", + commit="master" +) repls = Pair{Any,Any}[] # add some shameless advertisement @@ -228,7 +229,7 @@ filename(str) = first(splitext(last(splitdir(str)))) isdocumenter(cfg) = cfg["flavor"]::AbstractFlavor isa DocumenterFlavor _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 const HEAD_BRANCH_CACHE = Dict{String,String}() @@ -246,7 +247,7 @@ function edit_commit(inputfile, user_config) readchomp( pipeline( setenv(`$(git) rev-parse --show-toplevel`; dir=dirname(inputfile)); - stderr=devnull, + stderr=devnull ) ) 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 type === :md Base.depwarn("The documenter=$(d) keyword to Literate.markdown is deprecated." * - " Pass `flavor = Literate.$(d ? "DocumenterFlavor" : "CommonMarkFlavor")()`" * - " instead.", Symbol("Literate.markdown")) + " Pass `flavor = Literate.$(d ? "DocumenterFlavor" : "CommonMarkFlavor")()`" * + " instead.", Symbol("Literate.markdown")) user_config["flavor"] = d ? DocumenterFlavor() : CommonMarkFlavor() elseif type === :nb 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 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 @@ -420,7 +421,7 @@ Available options: `$(_DEFAULT_IMAGE_FORMATS)`. Results which are `showable` with a MIME type are saved with 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) # 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) # parse the content into chunks - chunks = parse(content; allow_continued = type !== :nb) + chunks = parse(content; allow_continued=type !== :nb) return chunks, config end @@ -527,6 +528,56 @@ function script(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwargs. 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...) @@ -540,12 +591,37 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg # preprocessing and parsing chunks, config = preprocessor(inputfile, outputdir; user_config=config, user_kwargs=kwargs, type=:md) - # create the markdown file - sb = sandbox() 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) 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 write(iomd, line.second, '\n') # skip indent here end @@ -574,34 +650,30 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg if execute cd(config["literate_outputdir"]) do execute_markdown!(iomd, sb, join(chunk.lines, '\n'), outputdir; - inputfile=config["literate_inputfile"], - fake_source=config["literate_outputfile"], - flavor=config["flavor"], - image_formats=config["image_formats"], - file_prefix="$(config["name"])-$(chunknum)", + inputfile=config["literate_inputfile"], + fake_source=config["literate_outputfile"], + flavor=config["flavor"], + image_formats=config["image_formats"], + file_prefix="$(config["name"])-$(chunknum)" ) end end end write(iomd, '\n') # add a newline between each chunk 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; - inputfile::String, fake_source::String, - flavor::AbstractFlavor, image_formats::Vector, file_prefix::String) + inputfile::String, fake_source::String, + flavor::AbstractFlavor, image_formats::Vector, file_prefix::String) # TODO: Deal with explicit display(...) calls r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source) # issue #101: consecutive codefenced blocks need newline # 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 (flavor isa FranklinFlavor || flavor isa DocumenterFlavor) && 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 m = match(r"^%% ([^[{]+)?\s*(?:\[(\w+)\])?\s*(\{.*)?$", line) typ = m.captures[2] - 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]) + 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]) return typ, merge(name, meta) end 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) # 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 end @@ -690,11 +762,11 @@ function jupyter_notebook(chunks, config) metatype !== nothing && metatype != chunktype && error("specifying a different cell type is not supported") popfirst!(chunk.lines) else - metadata = Dict{String, Any}() + metadata = Dict{String,Any}() end lines = isa(chunk, MDChunk) ? - String[x.second for x in chunk.lines] : # skip indent - chunk.lines + String[x.second for x in chunk.lines] : # skip indent + chunk.lines @views map!(x -> x * '\n', lines[1:end-1], lines[1:end-1]) cell["cell_type"] = chunktype cell["metadata"] = metadata @@ -711,15 +783,15 @@ function jupyter_notebook(chunks, config) metadata = Dict() kernelspec = Dict() - kernelspec["language"] = "julia" - kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)" + kernelspec["language"] = "julia" + kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)" kernelspec["display_name"] = "Julia $(string(VERSION))" metadata["kernelspec"] = kernelspec language_info = Dict() language_info["file_extension"] = ".jl" language_info["mimetype"] = "application/julia" - language_info["name"]= "julia" + language_info["name"] = "julia" language_info["version"] = string(VERSION) metadata["language_info"] = language_info @@ -733,7 +805,7 @@ function jupyter_notebook(chunks, config) try cd(config["literate_outputdir"]) do nb = execute_notebook(nb; inputfile=config["literate_inputfile"], - fake_source=config["literate_outputfile"]) + fake_source=config["literate_outputfile"]) end catch err @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["output_type"] = "stream" 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) end @@ -769,7 +841,7 @@ function execute_notebook(nb; inputfile::String, fake_source::String) 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)) + dict[mime] = collect(Any, eachline(IOBuffer(dict[mime]), keep=true)) end end return dict @@ -849,7 +921,7 @@ function execute_block(sb::Module, block::String; inputfile::String, fake_source # - c.output: combined stdout and stderr # `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). - c = IOCapture.capture(rethrow = Union{}) do + c = IOCapture.capture(rethrow=Union{}) do include_string(sb, block, fake_source) end popdisplay(disp) # IOCapture.capture has a try-catch so should always end up here