From 0f9e836d68f238becd3e193b22ebdad06e4d7ffa Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Fri, 22 Nov 2019 12:53:55 +0100 Subject: [PATCH] Support configuration with a config::Dict kwarg. (#83) --- docs/Manifest.toml | 14 ++- docs/make.jl | 26 +++--- docs/src/outputformats.md | 46 +++++++--- src/Literate.jl | 177 +++++++++++++++++++------------------- 4 files changed, 143 insertions(+), 120 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 002dd43..8822535 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -58,11 +58,9 @@ version = "0.8.1" [[Documenter]] deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "fb16819c1bcf5e99b8e316e329e5e0958550334e" -repo-rev = "master" -repo-url = "https://github.com/JuliaDocs/Documenter.jl.git" +git-tree-sha1 = "0e52069b5970cb27234153f578227947565152c5" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.24.0-DEV" +version = "0.24.0" [[FFMPEG]] deps = ["BinaryProvider", "Libdl"] @@ -116,7 +114,7 @@ uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" deps = ["Base64", "JSON", "REPL"] path = ".." uuid = "98b081ad-f1c9-55d3-8b20-4c87d4299306" -version = "2.1.0" +version = "2.1.1" [[Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -153,12 +151,12 @@ version = "1.1.0" [[Parsers]] deps = ["Dates", "Test"] -git-tree-sha1 = "a23968e107c0544aca91bfab6f7dd34de1206a54" +git-tree-sha1 = "0139ba59ce9bc680e2925aec5b7db79065d60556" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "0.3.9" +version = "0.3.10" [[Pkg]] -deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [[PlotThemes]] diff --git a/docs/make.jl b/docs/make.jl index 6262e7d..c22a658 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,24 +20,24 @@ Literate.script(EXAMPLE, OUTPUT, preprocess = preprocess) # generate the example notebook for the documentation, keep in sync with outputformats.md Literate.markdown(joinpath(@__DIR__, "src/outputformats.jl"), OUTPUT; credit = false, name = "name") -Literate.notebook(joinpath(@__DIR__, "src/outputformats.jl"), OUTPUT, name = "notebook") -Literate.script(joinpath(@__DIR__, "src/outputformats.jl"), OUTPUT, credit = false) - -# replace the link in outputformats.md -travis_tag = get(ENV, "TRAVIS_TAG", "") -folder = isempty(travis_tag) ? "latest" : travis_tag -url = "https://nbviewer.jupyter.org/github/fredrikekre/Literate.jl/blob/gh-pages/$(folder)/" -if get(ENV, "HAS_JOSH_K_SEAL_OF_APPROVAL", "") == "true" - str = read(joinpath(@__DIR__, "src/outputformats.md"), String) - str = replace(str, "[notebook.ipynb](generated/notebook.ipynb)." => "[notebook.ipynb]($(url)generated/notebook.ipynb).") - write(joinpath(@__DIR__, "src/outputformats.md"), str) -end +Literate.notebook(joinpath(@__DIR__, "src/outputformats.jl"), OUTPUT; name = "notebook") +Literate.script(joinpath(@__DIR__, "src/outputformats.jl"), OUTPUT; credit = false) + +# # replace the link in outputformats.md +# travis_tag = get(ENV, "TRAVIS_TAG", "") +# folder = isempty(travis_tag) ? "latest" : travis_tag +# url = "https://nbviewer.jupyter.org/github/fredrikekre/Literate.jl/blob/gh-pages/$(folder)/" +# if get(ENV, "HAS_JOSH_K_SEAL_OF_APPROVAL", "") == "true" +# str = read(joinpath(@__DIR__, "src/outputformats.md"), String) +# str = replace(str, "[notebook.ipynb](generated/notebook.ipynb)." => "[notebook.ipynb]($(url)generated/notebook.ipynb).") +# write(joinpath(@__DIR__, "src/outputformats.md"), str) +# end makedocs( format = Documenter.HTML( assets = ["assets/custom.css"], - + prettyurls = haskey(ENV, "GITHUB_ACTIONS"), ), modules = [Literate], sitename = "Literate.jl", diff --git a/docs/src/outputformats.md b/docs/src/outputformats.md index 012dfdd..74d1645 100644 --- a/docs/src/outputformats.md +++ b/docs/src/outputformats.md @@ -12,7 +12,8 @@ and see how this is rendered in each of the output formats. ## [**4.1.** Markdown Output](@id Markdown-Output) -The (default) markdown output of the source snippet above is as follows +Markdown output is generated by [`Literate.markdown`](@ref). The (default) markdown output +of the source snippet above is as follows: ```@eval import Markdown @@ -28,8 +29,8 @@ an `@meta` block have been added, that sets the `EditURL` variable. This is used by Documenter to redirect the "Edit on GitHub" link for the page, see [Interaction with Documenter](@ref). -Some of the output rendering can be controlled with keyword arguments to -[`Literate.markdown`](@ref): +See the section about [Configuration](@ref) for how to configure the behavior and resulting +output of [`Literate.markdown`](@ref). ```@docs Literate.markdown @@ -37,15 +38,16 @@ Literate.markdown ## [**4.2.** Notebook Output](@id Notebook-Output) -The (default) notebook output of the source snippet can be seen here: -[notebook.ipynb](generated/notebook.ipynb). +Notebook output is generated by [`Literate.notebook`](@ref). The (default) notebook output +of the source snippet can be seen here: [notebook.ipynb](generated/notebook.ipynb). We note that lines starting with `# ` are placed in markdown cells, and the code lines have been placed in code cells. By default the notebook is also executed and output cells populated. The current working directory is set to the specified output directory the notebook is executed. -Some of the output rendering can be controlled with keyword -arguments to [`Literate.notebook`](@ref): + +See the section about [Configuration](@ref) for how to configure the behavior and resulting +output of [`Literate.notebook`](@ref). ```@docs Literate.notebook @@ -70,7 +72,8 @@ and the [reveal.js](https://github.com/hakimel/reveal.js) notebook extension ## [**4.3.** Script Output](@id Script-Output) -The (default) script output of the source snippet above is as follows +Script output is generated by [`Literate.script`](@ref). The (default) script output of the +source snippet above is as follows: ```@eval import Markdown @@ -81,9 +84,32 @@ Markdown.parse(str) ``` We note that lines starting with `# ` are removed and only the -code lines have been kept. Some of the output rendering can be controlled -with keyword arguments to [`Literate.script`](@ref): +code lines have been kept. + +See the section about [Configuration](@ref) for how to configure the behavior and resulting +output of [`Literate.script`](@ref). ```@docs Literate.script ``` + +## [**4.4.** Configuration](@id Configuration) + +The behavior of [`Literate.markdown`](@ref), [`Literate.notebook`](@ref) and +[`Literate.script`](@ref) can be configured by keyword arguments. There are two +ways to do this; pass `config::Dict` as a keyword argument, or pass individual +keyword arguments. + +!!! note "Configuration precedence" + Individual keyword arguments takes precedence over the `config` dictionary, so for e.g. + `Literate.markdown(...; config = Dict("name" => "hello"), name = "world")` the + resulting configuration for `name` will be `"world"`. Both individual keyword arguments + and the `config` dictionary takes precedence over the default. + +Available configurations with description and default values are given in the reference for +[`Literate.DEFAULT_CONFIGURATION`](@ref) just below. + + +```@docs +Literate.DEFAULT_CONFIGURATION +``` diff --git a/src/Literate.jl b/src/Literate.jl index 85feb74..4960394 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -118,16 +118,14 @@ function parse(content; allow_continued = true) end function replace_default(content, sym; - name = error("required kwarg"), - documenter = true, - credit = true, + config::Dict, branch = "gh-pages", commit = "master" ) repls = Pair{Any,Any}[] # add some shameless advertisement - if credit + if config["credit"]::Bool if sym === :jl content *= """ @@ -174,7 +172,7 @@ function replace_default(content, sym; end # name - push!(repls, "@__NAME__" => name) + push!(repls, "@__NAME__" => config["name"]::String) # fix links if get(ENV, "DOCUMENTATIONGENERATOR", "") == "true" @@ -230,7 +228,7 @@ function replace_default(content, sym; end # run some Documenter specific things - if documenter && sym !== :md + if config["documenter"]::Bool && sym !== :md ## - remove documenter style `@ref`s and `@id`s push!(repls, r"\[(.*?)\]\(@ref\)" => s"\1") # [foo](@ref) => foo push!(repls, r"\[(.*?)\]\(@ref .*?\)" => s"\1") # [foo](@ref bar) => foo @@ -247,29 +245,61 @@ end filename(str) = first(splitext(last(splitdir(str)))) +function create_configuration(inputfile; user_config, user_kwargs) + # Combine user config with user kwargs + user_config = Dict{String,Any}(string(k) => v for (k, v) in user_config) + user_kwargs = Dict{String,Any}(string(k) => v for (k, v) in user_kwargs) + user_config = merge!(user_config, user_kwargs) + + # Add default config + cfg = Dict{String,Any}() + cfg["name"] = filename(inputfile) + cfg["preprocess"] = identity + cfg["postprocess"] = identity + cfg["documenter"] = true + cfg["credit"] = true + cfg["keep_comments"] = false + cfg["codefence"] = get(user_config, "documenter", true) ? + ("```@example $(get(user_config, "name", cfg["name"]))" => "```") : ("```julia" => "```") + cfg["execute"] = true + + # Merge default_config with user_config + merge!(cfg, user_config) + return cfg +end + +""" + DEFAULT_CONFIGURATION + +Default configuration for [`Literate.markdown`](@ref), [`Literate.notebook`](@ref) and +[`Literate.script`] which is used for everything not specified by the user. +See the manual section about [Configuration](@ref) for more information. + +| Configuration key | Description | Default value | Comment | +| ----------------- |:----------- |:------------- |:------- | +| `name` | Name of the output file (excluding file extension). | `filename(inputfile)` | | +| `preprocess` | Custom preprocessing function mapping `String` to `String`. | `identity` | See [Custom pre- and post-processing](@ref Custom-pre-and-post-processing). | +| `postprocess` | Custom preprocessing function mapping `String` to `String`. | `identity` | See [Custom pre- and post-processing](@ref Custom-pre-and-post-processing). | +| `documenter` | Boolean signaling that the source contains Documenter.jl elements. | `true` | See [Interaction with Documenter](@ref Interaction-with-Documenter). | +| `credit` | Boolean for controlling the addition of `This file was generated with Literate.jl ...` to the bottom of the page. If you find Literate.jl useful then feel free to keep this. | `true` | | +| `keep_comments` | When `true`, keeps markdown lines as comments in the output script. | `false` | Only applicable for `Literate.script`. | +| `codefence` | Pair containing opening and closing fence for wrapping code blocks. | `````"```julia" => "```"````` | If `documenter` is `true` the default is `````"```@example"=>"```"`````. | +| `execute` | Whether to execute and capture the output. | `true` | Only applicable for `Literate.notebook`. | +""" +const DEFAULT_CONFIGURATION=nothing # Dummy const for documentation + """ - Literate.script(inputfile, outputdir; kwargs...) + Literate.script(inputfile, outputdir; config::Dict=Dict(), kwargs...) Generate a plain script file from `inputfile` and write the result to `outputdir`. -Keyword arguments: -- `name`: name of the output file, excluding `.jl`. `name` is also used to - replace `@__NAME__`. Defaults to the filename of `inputfile`. -- `preprocess`, `postprocess`: custom pre- and post-processing functions, - see the [Custom pre- and post-processing](@ref Custom-pre-and-post-processing) - section of the manual. Defaults to `identity`. -- `documenter`: boolean that says if the source contains Documenter.jl specific things - to filter out during script generation. Defaults to `true`. See the the manual - section on [Interaction with Documenter](@ref Interaction-with-Documenter). -- `keep_comments`: boolean that, if set to `true`, keeps markdown lines - as comments in the output script. Defaults to `false`. -- `credit`: boolean that controls the addition of `This file was generated with - Literate.jl ...` to the bottom of the page. If you find Literate.jl useful then - feel free to keep this to the default, which is `true`. +See the manual section on [Configuration](@ref) for documentation +of possible configuration with `config` and other keyword arguments. """ -function script(inputfile, outputdir; preprocess = identity, postprocess = identity, - name = filename(inputfile), documenter = true, credit = true, - keep_comments::Bool=false, kwargs...) +function script(inputfile, outputdir; config::Dict=Dict(), kwargs...) + # Create configuration by merging default and userdefined + config = create_configuration(inputfile; user_config=config, user_kwargs=kwargs) + # normalize paths inputfile = normpath(inputfile) isfile(inputfile) || throw(ArgumentError("cannot find inputfile `$(inputfile)`")) @@ -282,10 +312,10 @@ function script(inputfile, outputdir; preprocess = identity, postprocess = ident content = read(inputfile, String) # run custom pre-processing from user - content = preprocess(content) + content = config["preprocess"](content) # default replacements - content = replace_default(content, :jl; name = name, documenter = documenter, credit = credit) + content = replace_default(content, :jl; config=config) # create the script file chunks = parse(content) @@ -296,7 +326,7 @@ function script(inputfile, outputdir; preprocess = identity, postprocess = ident write(ioscript, line, '\n') end write(ioscript, '\n') # add a newline between each chunk - elseif isa(chunk, MDChunk) && keep_comments + elseif isa(chunk, MDChunk) && config["keep_comments"]::Bool for line in chunk.lines write(ioscript, rstrip(line.first * "# " * line.second * '\n')) end @@ -305,11 +335,11 @@ function script(inputfile, outputdir; preprocess = identity, postprocess = ident end # custom post-processing from user - content = postprocess(String(take!(ioscript))) + content = config["postprocess"](String(take!(ioscript))) # write to file isdir(outputdir) || error("not a directory: $(outputdir)") - outputfile = joinpath(outputdir, name * ".jl") + outputfile = joinpath(outputdir, config["name"]::String * ".jl") @info "writing result to `$(Base.contractuser(outputfile))`" write(outputfile, content) @@ -318,38 +348,18 @@ function script(inputfile, outputdir; preprocess = identity, postprocess = ident end """ - Literate.markdown(inputfile, outputdir; kwargs...) + Literate.markdown(inputfile, outputdir; config::Dict=Dict(), kwargs...) Generate a markdown file from `inputfile` and write the result to the directory `outputdir`. -Keyword arguments: -- `name`: name of the output file, excluding `.md`; `name` is also used to name - all the `@example` blocks, and to replace `@__NAME__`. - Defaults to the filename of `inputfile`. -- `preprocess`, `postprocess`: custom pre- and post-processing functions, - see the [Custom pre- and post-processing](@ref Custom-pre-and-post-processing) - section of the manual. Defaults to `identity`. -- `documenter`: boolean that tells if the output is intended to use with Documenter.jl. - Defaults to `true`. See the manual section on - [Interaction with Documenter](@ref Interaction-with-Documenter). -- `codefence`: A `Pair` of opening and closing code fence. Defaults to - ```` - "```@example \$(name)" => "```" - ```` - if `documenter = true` and - ```` - "```julia" => "```" - ```` - if `documenter = false`. -- `credit`: boolean that controls the addition of `This file was generated with - Literate.jl ...` to the bottom of the page. If you find Literate.jl useful then - feel free to keep this to the default, which is `true`. +See the manual section on [Configuration](@ref) for documentation +of possible configuration with `config` and other keyword arguments. """ -function markdown(inputfile, outputdir; preprocess = identity, postprocess = identity, - name = filename(inputfile), documenter::Bool = true, credit = true, - codefence::Pair = documenter ? "```@example $(name)" => "```" : "```julia" => "```", - kwargs...) +function markdown(inputfile, outputdir; config::Dict=Dict(), kwargs...) + # Create configuration by merging default and userdefined + config = create_configuration(inputfile; user_config=config, user_kwargs=kwargs) + # normalize paths inputfile = normpath(inputfile) isfile(inputfile) || throw(ArgumentError("cannot find inputfile `$(inputfile)`")) @@ -362,10 +372,10 @@ function markdown(inputfile, outputdir; preprocess = identity, postprocess = ide content = read(inputfile, String) # run custom pre-processing from user - content = preprocess(content) + content = config["preprocess"](content) # run some Documenter specific things - if documenter + if config["documenter"]::Bool # change the Edit on GitHub link repo = get(ENV, "TRAVIS_REPO_SLUG", get(ENV, "GITHUB_REPOSITORY", nothing)) if repo === nothing @@ -390,7 +400,7 @@ function markdown(inputfile, outputdir; preprocess = identity, postprocess = ide end # default replacements - content = replace_default(content, :md; name = name, documenter = documenter, credit = credit) + content = replace_default(content, :md; config=config) # create the markdown file chunks = parse(content) @@ -402,9 +412,10 @@ function markdown(inputfile, outputdir; preprocess = identity, postprocess = ide write(iomd, line.second, '\n') # skip indent here end else # isa(chunk, CodeChunk) + codefence = config["codefence"]::Pair write(iomd, codefence.first) # make sure the code block is finalized if we are printing to ```@example - if chunk.continued && startswith(codefence.first, "```@example") && documenter + if chunk.continued && startswith(codefence.first, "```@example") && config["documenter"]::Bool write(iomd, "; continued = true") end write(iomd, '\n') @@ -413,7 +424,7 @@ function markdown(inputfile, outputdir; preprocess = identity, postprocess = ide write(iomd, line, '\n') last_line = line end - if documenter && REPL.ends_with_semicolon(last_line) + if config["documenter"]::Bool && REPL.ends_with_semicolon(last_line) write(iomd, "nothing #hide\n") end write(iomd, codefence.second, '\n') @@ -422,11 +433,11 @@ function markdown(inputfile, outputdir; preprocess = identity, postprocess = ide end # custom post-processing from user - content = postprocess(String(take!(iomd))) + content = config["postprocess"](String(take!(iomd))) # write to file isdir(outputdir) || error("not a directory: $(outputdir)") - outputfile = joinpath(outputdir, name * ".md") + outputfile = joinpath(outputdir, config["name"]::String * ".md") @info "writing result to `$(Base.contractuser(outputfile))`" write(outputfile, content) @@ -450,29 +461,17 @@ line_is_nbmeta(line::Pair) = line_is_nbmeta(line.second) line_is_nbmeta(line) = startswith(line, "%% ") """ - Literate.notebook(inputfile, outputdir; kwargs...) + Literate.notebook(inputfile, outputdir; config::Dict=Dict(), kwargs...) Generate a notebook from `inputfile` and write the result to `outputdir`. -Keyword arguments: -- `name`: name of the output file, excluding `.ipynb`. `name` is also used to - replace `@__NAME__`. Defaults to the filename of `inputfile`. -- `preprocess`, `postprocess`: custom pre- and post-processing functions, - see the [Custom pre- and post-processing](@ref Custom-pre-and-post-processing) - section of the manual. Defaults to `identity`. -- `execute`: a boolean deciding if the generated notebook should also - be executed or not. Defaults to `true`. The current working directory - is set to `outputdir` when executing the notebook. -- `documenter`: boolean that says if the source contains Documenter.jl specific things - to filter out during notebook generation. Defaults to `true`. See the the manual - section on [Interaction with Documenter](@ref Interaction-with-Documenter). -- `credit`: boolean that controls the addition of `This file was generated with - Literate.jl ...` to the bottom of the page. If you find Literate.jl useful then - feel free to keep this to the default, which is `true`. +See the manual section on [Configuration](@ref) for documentation +of possible configuration with `config` and other keyword arguments. """ -function notebook(inputfile, outputdir; preprocess = identity, postprocess = identity, - execute::Bool=true, documenter::Bool=true, credit = true, - name = filename(inputfile), kwargs...) +function notebook(inputfile, outputdir; config::Dict=Dict(), kwargs...) + # Create configuration by merging default and userdefined + config = create_configuration(inputfile; user_config=config, user_kwargs=kwargs) + # normalize paths inputfile = normpath(inputfile) isfile(inputfile) || throw(ArgumentError("cannot find inputfile `$(inputfile)`")) @@ -485,10 +484,10 @@ function notebook(inputfile, outputdir; preprocess = identity, postprocess = ide content = read(inputfile, String) # run custom pre-processing from user - content = preprocess(content) + content = config["preprocess"](content) # default replacements - content = replace_default(content, :nb; name = name, documenter = documenter, credit = credit) + content = replace_default(content, :nb; config=config) # parse chunks = parse(content; allow_continued = false) @@ -544,10 +543,10 @@ function notebook(inputfile, outputdir; preprocess = identity, postprocess = ide nb["metadata"] = metadata # custom post-processing from user - nb = postprocess(nb) + nb = config["postprocess"](nb) - if execute - @info "executing notebook `$(name * ".ipynb")`" + if config["execute"]::Bool + @info "executing notebook `$(config["name"] * ".ipynb")`" try cd(outputdir) do nb = execute_notebook(nb) @@ -560,7 +559,7 @@ function notebook(inputfile, outputdir; preprocess = identity, postprocess = ide # write to file isdir(outputdir) || error("not a directory: $(outputdir)") - outputfile = joinpath(outputdir, name * ".ipynb") + outputfile = joinpath(outputdir, config["name"]::String * ".ipynb") @info "writing result to `$(Base.contractuser(outputfile))`" ionb = IOBuffer() @@ -635,7 +634,7 @@ function execute_notebook(nb) end end - nb + return nb end end # module