Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Output Pluto-flavored notebooks. #120

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ Literate is a package for [Literate Programming](https://en.wikipedia.org/wiki/L
The main purpose is to facilitate writing Julia examples/tutorials that can be included in
your package documentation.

Literate can generate markdown pages
(for e.g. [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl)), and
[Jupyter notebooks](http://jupyter.org/), from the same source file. There is also
an option to "clean" the source from all metadata, and produce a pure Julia script.
Using a single source file for multiple purposes reduces maintenance, and makes sure
your different output formats are synced with each other.
Literate can generate multiple outputs from a single source file: Markdown pages
(for e.g. [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl)),
[Jupyter notebooks](http://jupyter.org/), and
[Pluto notebooks](https://github.com/fonsp/Pluto.jl).
There is also an option to "clean" the source from all metadata, and produce a
pure Julia script. Using a single source file for multiple purposes reduces maintenance,
and makes sure your different output formats are synced with each other.

This README was generated directly from
[this source file](https://github.com/fredrikekre/Literate.jl/blob/master/examples/README.jl)
Expand Down
17 changes: 13 additions & 4 deletions docs/src/outputformats.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,17 @@ Literate.markdown
Literate can output markdown in different flavors. The flavor is specified using the
`flavor` keyword argument. The following flavors are currently supported:

- `flavor = Literate.DocumenterFlavor()` this is the default flavor and the output is
- `flavor = Literate.DocumenterFlavor()`: this is the default flavor and the output is
meant to be used as input to [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl).
- `flavor = Literate.FranklinFlavor()` this outputs markdown meant to be used as input
- `flavor = Literate.FranklinFlavor()`: this outputs markdown meant to be used as input
to [Franklin.jl](https://franklinjl.org/).


## [**4.2.** Notebook output](@id Notebook-output)

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).
Notebook output is generated by [`Literate.notebook`](@ref). The Jupyter notebook output
of the source snippet can be seen here: [notebook.ipynb](generated/notebook.ipynb),
and the Pluto notebook output can be seen here: [notebook.jl](generated/notebook.jl).

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
Expand All @@ -103,6 +104,14 @@ output of [`Literate.notebook`](@ref).
Literate.notebook
```

### Notebook flavors

Literate can output both Jupyter notebooks and Pluto notebooks. The flavor is specified
using the `flavor` keyword argument:

- `flavor = Literate.JupyterFlavor()` (default)
- `flavor = Literate.PlutoFlavor()`

### Notebook metadata

Jupyter notebook cells (both code cells and markdown cells) can contain metadata. This is enabled
Expand Down
13 changes: 7 additions & 6 deletions examples/README.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
# The main purpose is to facilitate writing Julia examples/tutorials that can be included in
# your package documentation.

# Literate can generate markdown pages
# (for e.g. [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl)), and
# [Jupyter notebooks](http://jupyter.org/), from the same source file. There is also
# an option to "clean" the source from all metadata, and produce a pure Julia script.
# Using a single source file for multiple purposes reduces maintenance, and makes sure
# your different output formats are synced with each other.
# Literate can generate multiple outputs from a single source file: Markdown pages
# (for e.g. [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl)),
# [Jupyter notebooks](http://jupyter.org/), and
# [Pluto notebooks](https://github.com/fonsp/Pluto.jl).
# There is also an option to "clean" the source from all metadata, and produce a
# pure Julia script. Using a single source file for multiple purposes reduces maintenance,
# and makes sure your different output formats are synced with each other.
#
# This README was generated directly from
# [this source file](https://github.com/fredrikekre/Literate.jl/blob/master/examples/README.jl)
Expand Down
113 changes: 104 additions & 9 deletions src/Literate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ struct DefaultFlavor <: AbstractFlavor end
struct DocumenterFlavor <: AbstractFlavor end
struct CommonMarkFlavor <: AbstractFlavor end
struct FranklinFlavor <: AbstractFlavor end
struct JupyterFlavor <: AbstractFlavor end
struct PlutoFlavor <: AbstractFlavor end

# # Some simple rules:
#
Expand Down Expand Up @@ -251,7 +253,7 @@ function create_configuration(inputfile; user_config, user_kwargs, type=nothing)
cfg["name"] = filename(inputfile)
cfg["preprocess"] = identity
cfg["postprocess"] = identity
cfg["flavor"] = type === (:md) ? DocumenterFlavor() : DefaultFlavor()
cfg["flavor"] = type === (:md) ? DocumenterFlavor() : type === (:nb) ? JupyterFlavor() : DefaultFlavor()
cfg["credit"] = true
cfg["mdstrings"] = false
cfg["keep_comments"] = false
Expand Down Expand Up @@ -348,14 +350,17 @@ Available options:
`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.
- `keep_comments` (default: `false`): When `true`, keeps markdown lines as comments in the
output script. Only applicable for `Literate.script`.
output script. Only applicable for [`Literate.script`](@ref)
- `execute` (default: `true` for notebook, `false` for markdown): Whether to execute and
capture the output. Only applicable for `Literate.notebook` and `Literate.markdown`.
capture the output. Only applicable for [`Literate.notebook`](@ref) and
[`Literate.markdown`](@ref).
- `codefence` (default: `````"````@example \$(name)" => "````"````` for `DocumenterFlavor()`
and `````"````julia" => "````"````` otherwise): Pair containing opening and closing
code fence for wrapping code blocks.
- `flavor` (default: `Literate.DocumenterFlavor()`) Output flavor for markdown, see
[Markdown flavors](@ref). Only applicable for `Literate.markdown`.
- `flavor` (default: `Literate.DocumenterFlavor()` for `Literate.markdown` and
`Literate.JupyterFlavor()` for `Literate.notebook`) Output flavor for markdown and
notebook output, see [Markdown flavors](@ref) and [Notebook flavors](@ref).
Not used for `Literate.script`.
- `devurl` (default: `"dev"`): URL for "in-development" docs, see [Documenter docs]
(https://juliadocs.github.io/Documenter.jl/). Unused if `repo_root_url`/
`nbviewer_root_url`/`binder_root_url` are set.
Expand Down Expand Up @@ -393,7 +398,9 @@ function preprocessor(inputfile, outputdir; user_config, user_kwargs, type)
# Add some information for passing around Literate methods
config["literate_inputfile"] = inputfile
config["literate_outputdir"] = outputdir
config["literate_ext"] = type === (:nb) ? ".ipynb" : ".$(type)"
config["literate_ext"] = type === (:nb) ? (
config["flavor"]::AbstractFlavor isa JupyterFlavor ? ".ipynb" : ".jl") :
".$(type)"

# read content
content = read(inputfile, String)
Expand Down Expand Up @@ -610,14 +617,15 @@ function notebook(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...)
preprocessor(inputfile, outputdir; user_config=config, user_kwargs=kwargs, type=:nb)

# create the notebook
nb = jupyter_notebook(chunks, config)
nb = create_notebook(config["flavor"]::AbstractFlavor, chunks, config)

# write to file
outputfile = write_result(nb, config; print = (io, c)->JSON.print(io, c, 1))
print = config["flavor"]::AbstractFlavor isa JupyterFlavor ? (io, c) -> JSON.print(io, c, 1) : Base.print
outputfile = write_result(nb, config; print = print)
return outputfile
end

function jupyter_notebook(chunks, config)
function create_notebook(::JupyterFlavor, chunks, config)
nb = Dict()
nb["nbformat"] = JUPYTER_VERSION.major
nb["nbformat_minor"] = JUPYTER_VERSION.minor
Expand Down Expand Up @@ -742,6 +750,93 @@ function execute_notebook(nb; inputfile::String="<unknown>")
return nb
end

function create_notebook(::PlutoFlavor, chunks, config)
ionb = IOBuffer()
# Print header
write(ionb, """
### A Pluto.jl notebook ###
# v0.16.0

using Markdown
using InteractiveUtils

""")

# Print cells
uuids = Base.UUID[]
folds = Bool[]
default_fold = Dict{String,Bool}("markdown"=>true, "code"=>false) # toggleable ???
for (i, chunk) in enumerate(chunks)
io = IOBuffer()

# Jupyter style metadata # TODO: factor out, identical to jupyter notebook
chunktype = isa(chunk, MDChunk) ? "markdown" : "code"
fold = default_fold[chunktype]
if !isempty(chunk.lines) && line_is_nbmeta(chunk.lines[1])
@show chunk.lines
metatype, metadata = parse_nbmeta(chunk.lines[1])
metatype !== nothing && metatype != chunktype && error("specifying a different cell type is not supported")
popfirst!(chunk.lines)
fold = get(metadata, "fold", fold)
end

if isa(chunk, MDChunk)
if length(chunk.lines) == 1
line = escape_string(chunk.lines[1].second, '"')
write(io, "md\"", line, "\"\n")
else
write(io, "md\"\"\"\n")
for line in chunk.lines
write(io, line.second, '\n') # Skip indent
end
write(io, "\"\"\"\n")
end
content = String(take!(io))
else # isa(chunk, CodeChunk)
for line in chunk.lines
write(io, line, '\n')
end
content = String(take!(io))
# Compute number of expressions in the code block and perhaps wrap in begin/end
nexprs, idx = 0, 1
while true
ex, idx = Meta.parse(content, idx)
ex === nothing && break
nexprs += 1
end
if nexprs > 1
io = IOBuffer()
print(io, "begin\n")
foreach(l -> print(io, " ", l, '\n'), eachline(IOBuffer(content)))
print(io, "end\n")
content = String(take!(io))
end
end
uuid = uuid4(content, i)
push!(uuids, uuid)
push!(folds, fold)
print(ionb, "# ╔═╡ ", uuid, '\n')
write(ionb, content, '\n')
end

# Print cell order
print(ionb, "# ╔═╡ Cell order:\n")
foreach(((x, f),) -> print(ionb, "# $(f ? "╟─" : "╠═")", x, '\n'), zip(uuids, folds))

# custom post-processing from user
nb = config["postprocess"](String(take!(ionb)))
return nb
end

# UUID v4 from cell content and cell number (to keep it somewhat stable)
function uuid4(c, n)
fredrikekre marked this conversation as resolved.
Show resolved Hide resolved
c, n = hash(c), hash(n)
u = (convert(UInt128, c) << 64) ⊻ convert(UInt128, n)
u &= 0xffffffffffff0fff3fffffffffffffff
u |= 0x00000000000040008000000000000000
return Base.UUID(u)
end

# Create a sandbox module for evaluation
function sandbox()
m = Module(gensym())
Expand Down