2 changed files with 305 additions and 1 deletions
@ -1,5 +1,308 @@
@@ -1,5 +1,308 @@
|
||||
module Examples |
||||
|
||||
# package code goes here |
||||
import Compat: replace, popfirst! |
||||
|
||||
import JSON |
||||
|
||||
# # Some simple rules: |
||||
# |
||||
# * All lines starting with `#'` are considered markdown, everything else is considered code |
||||
# * The file is parsed in "chunks" of code and markdown. A new chunk is created when the |
||||
# lines switch context from markdown to code and vice versa. |
||||
# * Lines starting with `#-` can be used to start a new chunk. |
||||
# * Lines starting with `#md` are filtered out unless creating a markdown file |
||||
# * Lines starting with `#nb` are filtered out unless creating a notebook |
||||
# * Lines starting with, or ending with, `#jl` are filtered out unless creating a script file |
||||
# * Whitespace within a chunk is preserved |
||||
# * Empty chunks are removed, leading and trailing empty lines in a chunk are also removed |
||||
|
||||
# Parser |
||||
abstract type Chunk end |
||||
struct MDChunk <: Chunk |
||||
lines::Vector{String} |
||||
end |
||||
MDChunk() = MDChunk(String[]) |
||||
mutable struct CodeChunk <: Chunk |
||||
lines::Vector{String} |
||||
continued::Bool |
||||
end |
||||
CodeChunk() = CodeChunk(String[], false) |
||||
|
||||
function parse(content) |
||||
lines = collect(eachline(IOBuffer(content))) |
||||
|
||||
chunks = Chunk[] |
||||
push!(chunks, startswith(lines[1], "#'") ? MDChunk() : CodeChunk()) |
||||
|
||||
for line in lines |
||||
if startswith(line, "#-") # new chunk |
||||
# assume same as last chunk, will be cleaned up otherwise |
||||
push!(chunks, typeof(chunks[end])()) |
||||
elseif startswith(line, "#'") # markdown |
||||
if !(chunks[end] isa MDChunk) |
||||
push!(chunks, MDChunk()) |
||||
end |
||||
# remove "#' " and "#'\n" |
||||
line = replace(replace(line, r"^#' " => ""), r"^#'$" => "") |
||||
push!(chunks[end].lines, line) |
||||
else # code |
||||
if !(chunks[end] isa CodeChunk) |
||||
push!(chunks, CodeChunk()) |
||||
end |
||||
push!(chunks[end].lines, line) |
||||
end |
||||
end |
||||
|
||||
# clean up the chunks |
||||
## remove empty chunks |
||||
filter!(x -> !isempty(x.lines), chunks) |
||||
filter!(x -> !all(y -> isempty(y), x.lines), chunks) |
||||
## remove leading/trailing empty lines |
||||
for chunk in chunks |
||||
while isempty(chunk.lines[1]) |
||||
popfirst!(chunk.lines) |
||||
end |
||||
while isempty(chunk.lines[end]) |
||||
pop!(chunk.lines) |
||||
end |
||||
end |
||||
|
||||
# find code chunks that are continued |
||||
last_code_chunk = 0 |
||||
for (i, chunk) in enumerate(chunks) |
||||
isa(chunk, MDChunk) && continue |
||||
if startswith(last(chunk.lines)," ") |
||||
chunk.continued = true |
||||
end |
||||
if startswith(first(chunk.lines)," ") |
||||
chunks[last_code_chunk].continued = true |
||||
end |
||||
last_code_chunk = i |
||||
end |
||||
|
||||
return chunks |
||||
end |
||||
|
||||
""" |
||||
Examples.script(input, output; kwargs...) |
||||
|
||||
Create a script file. |
||||
""" |
||||
function script(input, output; kwargs...) |
||||
str = script(input; kwargs...) |
||||
write(output, str) |
||||
return output |
||||
end |
||||
|
||||
function script(input; preprocess = identity, postprocess = identity, |
||||
name = first(splitext(last(splitdir(input)))), kwargs...) |
||||
# read content |
||||
content = read(input, String) |
||||
|
||||
# run custom pre-processing from user |
||||
content = preprocess(content) |
||||
|
||||
# run built in pre-processing: |
||||
## - normalize line endings |
||||
## - remove #md lines |
||||
## - remove #nb lines |
||||
## - remove leading and trailing #jl |
||||
## - replace @__NAME__ |
||||
for repl in ["\r\n" => "\n", |
||||
r"^#md.*\n?"m => "", |
||||
r"^#nb.*\n?"m => "", |
||||
r"^#jl "m => "", |
||||
r" #jl$"m => "", |
||||
"@__NAME__" => name] |
||||
content = replace(content, repl) |
||||
end |
||||
|
||||
# create the script file |
||||
chunks = parse(content) |
||||
ioscript = IOBuffer() |
||||
for chunk in chunks |
||||
if isa(chunk, CodeChunk) |
||||
for line in chunk.lines |
||||
write(ioscript, line, '\n') |
||||
end |
||||
write(ioscript, '\n') # add a newline between each chunk |
||||
end |
||||
end |
||||
|
||||
# custom post-processing from user |
||||
content = postprocess(String(take!(ioscript))) |
||||
|
||||
return content |
||||
end |
||||
|
||||
""" |
||||
Examples.markdown(input, output; kwargs...) |
||||
|
||||
Generate a markdown file from the `input` file and write the result to the `output` file. |
||||
""" |
||||
function markdown(input, output; kwargs...) |
||||
str = markdown(input; kwargs...) |
||||
write(output, str) |
||||
return output |
||||
end |
||||
|
||||
function markdown(input; preprocess = identity, postprocess = identity, |
||||
name = first(splitext(last(splitdir(input)))), |
||||
codefence::Pair = "```@example $(name)" => "```", kwargs...) |
||||
# read content |
||||
content = read(input, String) |
||||
|
||||
# run custom pre-processing from user |
||||
content = preprocess(content) |
||||
|
||||
# run built in pre-processing: |
||||
## - normalize line endings |
||||
## - remove #nb lines |
||||
## - remove leading and trailing #jl lines |
||||
## - remove leading #md |
||||
## - replace @__NAME__ |
||||
for repl in ["\r\n" => "\n", |
||||
r"^#nb.*\n?"m => "", |
||||
r"^#jl.*\n?"m => "", |
||||
r".*#jl$\n?"m => "", |
||||
r"^#md "m => "", |
||||
"@__NAME__" => name] |
||||
content = replace(content, repl) |
||||
end |
||||
|
||||
# create the markdown file |
||||
chunks = parse(content) |
||||
iomd = IOBuffer() |
||||
continued = false |
||||
for chunk in chunks |
||||
if isa(chunk, MDChunk) |
||||
for line in chunk.lines |
||||
write(iomd, line, '\n') |
||||
end |
||||
else # isa(chunk, CodeChunk) |
||||
write(iomd, codefence.first) |
||||
# make sure the code block is finalized if we are printing to ```@example |
||||
if chunk.continued && startswith(codefence.first, "```@example") |
||||
write(iomd, "; continued = true") |
||||
end |
||||
write(iomd, '\n') |
||||
for line in chunk.lines |
||||
write(iomd, line, '\n') |
||||
end |
||||
write(iomd, codefence.second, '\n') |
||||
end |
||||
write(iomd, '\n') # add a newline between each chunk |
||||
end |
||||
|
||||
# custom post-processing from user |
||||
content = postprocess(String(take!(iomd))) |
||||
|
||||
return content |
||||
end |
||||
|
||||
""" |
||||
Examples.markdown(notebook, output; kwargs...) |
||||
|
||||
Generate a notebook from the `input` file and write the result to the `output` file. |
||||
""" |
||||
function notebook(input, output; execute::Bool=false, kwargs...) |
||||
str = notebook(input; kwargs...) |
||||
# return str |
||||
write(output, str) |
||||
if execute |
||||
try |
||||
run(`jupyter nbconvert --ExecutePreprocessor.timeout=-1 --to notebook --execute $(abspath(output)) --output $(first(splitext(last(splitdir(output))))).ipynb`) |
||||
catch err |
||||
@error("Error while executing notebook") |
||||
rethrow(err) |
||||
end |
||||
end |
||||
return output |
||||
end |
||||
|
||||
function notebook(input; preprocess = identity, postprocess = identity, |
||||
name = first(splitext(last(splitdir(input)))), kwargs...) |
||||
# read content |
||||
content = read(input, String) |
||||
|
||||
# run custom pre-processing from user |
||||
content = preprocess(content) |
||||
|
||||
# run built in pre-processing: |
||||
## - normalize line endings |
||||
## - remove #md lines |
||||
## - remove leading and trailing #jl lines |
||||
## - remove leading #nb |
||||
## - replace @__NAME__ |
||||
## - replace ```math ... ``` with \begin{equation} ... \end{equation} |
||||
for repl in Pair{Any,Any}[ |
||||
"\r\n" => "\n", |
||||
r"^#md.*\n?"m => "", |
||||
r"^#jl.*\n?"m => "", |
||||
r".*#jl$\n?"m => "", |
||||
r"^#nb "m => "", |
||||
"@__NAME__" => name, |
||||
r"```math(.*?)```"s => s"\\begin{equation}\1\\end{equation}", |
||||
] |
||||
content = replace(content, repl) |
||||
end |
||||
|
||||
# custom post-processing from user |
||||
content = postprocess(content) |
||||
|
||||
# create the notebook |
||||
nb = Dict() |
||||
nb["nbformat"] = 4 |
||||
nb["nbformat_minor"] = 2 |
||||
|
||||
## create the notebook cells |
||||
chunks = parse(content) |
||||
cells = [] |
||||
for chunk in chunks |
||||
cell = Dict() |
||||
if isa(chunk, MDChunk) |
||||
cell["cell_type"] = "markdown" |
||||
cell["metadata"] = Dict() |
||||
@views map!(x -> x*'\n', chunk.lines[1:end-1], chunk.lines[1:end-1]) |
||||
cell["source"] = chunk.lines |
||||
cell["outputs"] = [] |
||||
else # isa(chunk, CodeChunk) |
||||
cell["cell_type"] = "code" |
||||
cell["metadata"] = Dict() |
||||
@views map!(x -> x*'\n', chunk.lines[1:end-1], chunk.lines[1:end-1]) |
||||
cell["source"] = chunk.lines |
||||
cell["execution_count"] = nothing |
||||
cell["outputs"] = [] |
||||
end |
||||
push!(cells, cell) |
||||
end |
||||
nb["cells"] = cells |
||||
|
||||
## create metadata |
||||
metadata = Dict() |
||||
|
||||
kernelspec = Dict() |
||||
kernelspec["language"] = "julia" |
||||
kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)" |
||||
kernelspec["display_name"] = "Julia $(VERSION.major).$(VERSION.minor).$(VERSION.patch)" |
||||
metadata["kernelspec"] = kernelspec |
||||
|
||||
language_info = Dict() |
||||
language_info["file_extension"] = ".jl" |
||||
language_info["mimetype"] = "application/julia" |
||||
language_info["name"]= "julia" |
||||
language_info["version"] = "$(VERSION.major).$(VERSION.minor).$(VERSION.patch)" |
||||
metadata["language_info"] = language_info |
||||
|
||||
nb["metadata"] = metadata |
||||
|
||||
ionb = IOBuffer() |
||||
JSON.print(ionb, nb, 2) |
||||
|
||||
# custom post-processing from user |
||||
content = postprocess(String(take!(ionb))) |
||||
|
||||
return content |
||||
end |
||||
|
||||
end # module |
||||
|
||||
Loading…
Reference in new issue