From 79615c90f1fea99bae3364e530cdf705075213bf Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 8 Jul 2021 20:12:39 +0200 Subject: [PATCH] Adds a concept of "flavors" for different output. (#156) Adds `Literate.FranklinFlavor` that supports text/html output when executing the markdown file. fixes #146, closes #147 --- docs/src/outputformats.md | 15 +++++++++++++++ src/Literate.jl | 21 +++++++++++++++++++-- test/runtests.jl | 10 +++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/src/outputformats.md b/docs/src/outputformats.md index 2a63547..2e8222d 100644 --- a/docs/src/outputformats.md +++ b/docs/src/outputformats.md @@ -72,6 +72,21 @@ behavior and resulting output of [`Literate.markdown`](@ref). Literate.markdown ``` +### Markdown flavors + +Literate supports different "flavors" to the markdown output. To specify a flavor pass the +`flavor` keyword argument to [`Literate.markdown`](@ref). The default flavor +(`Literate.DefaultFlavor`) is the Documenter compatible output used throughout most examples. +Another supported flavor is `Literate.FranklinFlavor` for output compatible with the +[Franklin.jl](https://franklinjl.org/) static site generator. Currently the only difference +is that the Franklin flavor supports `text/html` MIME output when executing the markdown +file, e.g. with +```julia +Literate.markdown(input, output; execute=true, flavor=Literate.FranklinFlavor()) +``` +!!! compat "Literate 2.9" + The Franklin flavor requires at least Literate version 2.9. + ## [**4.2.** Notebook Output](@id Notebook-Output) Notebook output is generated by [`Literate.notebook`](@ref). The (default) notebook output diff --git a/src/Literate.jl b/src/Literate.jl index 7870b28..2733cc9 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -11,6 +11,9 @@ import JSON, REPL, IOCapture include("IJulia.jl") import .IJulia +abstract type AbstractFlavor end +struct DefaultFlavor <: AbstractFlavor end + # # Some simple rules: # # * All lines starting with `# ` are considered markdown, everything else is considered code @@ -226,6 +229,7 @@ function create_configuration(inputfile; user_config, user_kwargs, type=nothing) cfg["execute"] = type === :md ? false : true cfg["codefence"] = get(user_config, "documenter", true) && !get(user_config, "execute", cfg["execute"]) ? ("````@example $(get(user_config, "name", cfg["name"]))" => "````") : ("````julia" => "````") + cfg["flavor"] = DefaultFlavor() # Guess the package (or repository) root url edit_commit = "master" # TODO: Make this configurable like Documenter? deploy_branch = "gh-pages" # TODO: Make this configurable like Documenter? @@ -297,6 +301,7 @@ See the manual section about [Configuration](@ref) for more information. | `keep_comments` | When `true`, keeps markdown lines as comments in the output script. | `false` | Only applicable for `Literate.script`. | | `execute` | Whether to execute and capture the output. | `true` (notebook), `false` (markdown) | Only applicable for `Literate.notebook` and `Literate.markdown`. For markdown this requires at least Literate 2.4. | | `codefence` | Pair containing opening and closing fence for wrapping code blocks. | `````"````julia" => "````"````` | If `documenter` is `true` the default is `````"````@example"=>"````"`````. | +| `flavor` | Output flavor for markdown. | `Literate.DefaultFlavor()` | See [Markdown flavors](@ref). | | `devurl` | URL for "in-development" docs. | `"dev"` | See [Documenter docs](https://juliadocs.github.io/Documenter.jl/). Unused if `repo_root_url`/`nbviewer_root_url`/`binder_root_url` are set. | | `repo_root_url` | URL to the root of the repository. | - | Determined automatically on Travis CI, GitHub Actions and GitLab CI. Used for `@__REPO_ROOT_URL__`. | | `nbviewer_root_url` | URL to the root of the repository as seen on nbviewer. | - | Determined automatically on Travis CI, GitHub Actions and GitLab CI. Used for `@__NBVIEWER_ROOT_URL__`. | @@ -410,6 +415,10 @@ function script(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...) return outputfile end + +# struct Documenter <: Flavor end # TODO: Use this instead of documenter=true? +struct FranklinFlavor <: AbstractFlavor end + """ Literate.markdown(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...) @@ -459,7 +468,8 @@ function markdown(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...) write_code = !(all(l -> endswith(l, "#hide"), chunk.lines) && !(config["documenter"]::Bool)) write_code && write(iomd, seekstart(iocode)) if config["execute"]::Bool - execute_markdown!(iomd, sb, join(chunk.lines, '\n'), outputdir; inputfile=config["literate_inputfile"]) + execute_markdown!(iomd, sb, join(chunk.lines, '\n'), outputdir; + inputfile=config["literate_inputfile"], flavor=config["flavor"]) end end write(iomd, '\n') # add a newline between each chunk @@ -473,13 +483,20 @@ function markdown(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...) return outputfile end -function execute_markdown!(io::IO, sb::Module, block::String, outputdir; inputfile::String="") +function execute_markdown!(io::IO, sb::Module, block::String, outputdir; + inputfile::String="", flavor::AbstractFlavor) # TODO: Deal with explicit display(...) calls r, str, _ = execute_block(sb, block; inputfile=inputfile) # issue #101: consecutive codefenced blocks need newline # issue #144: quadruple backticks allow for triple backticks in the output plain_fence = "\n````\n" => "\n````" if r !== nothing && !REPL.ends_with_semicolon(block) + if flavor isa FranklinFlavor && showable(MIME("text/html"), r) + write(io, "\n~~~\n") + Base.invokelatest(show, io, MIME("text/html"), r) + write(io, "\n~~~\n") + return + end for (mime, ext) in [(MIME("image/png"), ".png"), (MIME("image/jpeg"), ".jpeg")] if showable(mime, r) file = string(hash(block) % UInt32) * ext diff --git a/test/runtests.jl b/test/runtests.jl index 0626438..393dea6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -710,6 +710,8 @@ end end #- struct MD end Base.show(io::IO, mime::MIME"text/markdown", ::MD) = print(io, "# " * "MD") + Base.show(io::IO, mime::MIME"text/html", ::MD) = + print(io, "

" * "MD" * "

") MD() #- print("hello"); print(stdout, ", "); print(stderr, "world") @@ -730,12 +732,18 @@ end end @test occursin("```\n2×2 $(Matrix{Int}):\n 1 2\n 3 4\n```", markdown) # text/plain @test occursin(r"!\[\]\(\d+\.png\)", markdown) # image/png @test occursin(r"!\[\]\(\d+\.jpeg\)", markdown) # image/jpeg - @test occursin(r"# MD", markdown) # text/markdown + @test occursin("# MD", markdown) # text/markdown + @test !occursin("~~~\n

MD

\n~~~", markdown) # text/html @test occursin("```\nhello, world\n```", markdown) # stdout/stderr @test occursin("```\n42\n```", markdown) # result over stdout/stderr @test !occursin("246", markdown) # empty output because trailing ; @test !occursin("```\nnothing\n```", markdown) # empty output because nothing as return value @test occursin("```\nhello there\n```", markdown) # nothing as return value, non-empty stdout + # FranklinFlavor + Literate.markdown(inputfile, outdir; execute=true, flavor=Literate.FranklinFlavor()) + markdown = read(joinpath(outdir, "inputfile.md"), String) + @test !occursin("# MD", markdown) # text/markdown + @test occursin("~~~\n

MD

\n~~~", markdown) # text/html # verify that inputfile exists @test_throws ArgumentError Literate.markdown("nonexistent.jl", outdir)