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

Notebook metadata #2016

Merged
merged 15 commits into from
May 17, 2022
14 changes: 12 additions & 2 deletions frontend/components/CellOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ const is_displayable = (result) => result instanceof Element && result.nodeType
* @typedef PlutoScript
* @type {HTMLScriptElement | { pluto_is_loading_me?: boolean }}
*/
const execute_scripttags = async ({ root_node, script_nodes, previous_results_map, invalidation }) => {
const execute_scripttags = async ({ root_node, script_nodes, previous_results_map, invalidation, pluto_actions }) => {
let results_map = new Map()

// Reattach DOM results from old scripts, you might want to skip reading this
Expand Down Expand Up @@ -333,6 +333,15 @@ const execute_scripttags = async ({ root_node, script_nodes, previous_results_ma
currentScript: currentScript,
invalidation: invalidation,
getPublishedObject: (id) => cell.getPublishedObject(id),
getNotebookMetadataExperimental: (key) => pluto_actions.get_notebook()?.metadata[key],
setNotebookMetadataExperimental: (key, value) =>
pluto_actions.update_notebook((notebook) => {
notebook.metadata[key] = value
}),
deleteNotebookMetadataExperimental: (key) =>
pluto_actions.update_notebook((notebook) => {
delete notebook.metadata[key]
}),
...observablehq_for_cells,
},
code: node.innerText,
Expand Down Expand Up @@ -444,8 +453,9 @@ export let RawHTMLContainer = ({ body, className = "", persist_js_state = false,
previous_results_map.current = await execute_scripttags({
root_node: container.current,
script_nodes: new_scripts,
invalidation: invalidation,
invalidation,
previous_results_map: persist_js_state ? previous_results_map.current : new Map(),
pluto_actions,
})

if (pluto_actions != null) {
Expand Down
11 changes: 6 additions & 5 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import { HijackExternalLinksToOpenInNewTab } from "./HackySideStuff/HijackExtern
export const default_path = "..."
const DEBUG_DIFFING = false

// Be sure to keep this in sync with DEFAULT_METADATA in Cell.jl
const DEFAULT_METADATA = {
// Be sure to keep this in sync with DEFAULT_CELL_METADATA in Cell.jl
const DEFAULT_CELL_METADATA = {
disabled: false,
show_logs: true,
}
Expand Down Expand Up @@ -211,6 +211,7 @@ const first_true_key = (obj) => {
* published_objects: { [objectid: string]: any},
* bonds: { [name: string]: any },
* nbpkg: NotebookPkgData?,
* metadata: object,
* }}
*/

Expand Down Expand Up @@ -385,7 +386,7 @@ export class Editor extends Component {
// Fill the cell with empty code remotely, so it doesn't run unsafe code
code: "",
metadata: {
...DEFAULT_METADATA,
...DEFAULT_CELL_METADATA,
},
}
}
Expand Down Expand Up @@ -425,7 +426,7 @@ export class Editor extends Component {
code: code,
code_folded: false,
metadata: {
...DEFAULT_METADATA,
...DEFAULT_CELL_METADATA,
},
}
})
Expand Down Expand Up @@ -484,7 +485,7 @@ export class Editor extends Component {
cell_id: id,
code,
code_folded: false,
metadata: { ...DEFAULT_METADATA },
metadata: { ...DEFAULT_CELL_METADATA },
}
notebook.cell_order = [...notebook.cell_order.slice(0, index), id, ...notebook.cell_order.slice(index, Infinity)]
})
Expand Down
9 changes: 3 additions & 6 deletions src/notebook/Cell.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import UUIDs: UUID, uuid1
import .ExpressionExplorer: SymbolsState, UsingsImports

# Make sure to keep this in sync with DEFAULT_METADATA in ../frontend/components/Editor.js
const DEFAULT_METADATA = Dict{String, Any}(
# Make sure to keep this in sync with DEFAULT_CELL_METADATA in ../frontend/components/Editor.js
const DEFAULT_CELL_METADATA = Dict{String, Any}(
"disabled" => false,
"show_logs" => true,
)
Expand Down Expand Up @@ -52,7 +52,7 @@ Base.@kwdef mutable struct Cell

depends_on_disabled_cells::Bool=false

metadata::Dict{String,Any}=copy(DEFAULT_METADATA)
metadata::Dict{String,Any}=copy(DEFAULT_CELL_METADATA)
end

Cell(cell_id, code) = Cell(cell_id=cell_id, code=code)
Expand All @@ -72,9 +72,6 @@ function Base.convert(::Type{UUID}, string::String)
UUID(string)
end

create_metadata(metadata::Dict{String,<:Any}) = merge(DEFAULT_METADATA, metadata)
get_cell_metadata(cell::Cell)::Dict{String,Any} = cell.metadata
get_cell_metadata_no_default(cell::Cell)::Dict{String,Any} = Dict{String,Any}(setdiff(pairs(cell.metadata), pairs(DEFAULT_METADATA)))

"Returns whether or not the cell is **explicitely** disabled."
is_disabled(c::Cell) = get(c.metadata, "disabled", false)
Expand Down
79 changes: 67 additions & 12 deletions src/notebook/Notebook.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import .PkgCompat: PkgCompat, PkgContext
import Pkg
import TOML


const DEFAULT_NOTEBOOK_METADATA = Dict{String, Any}()

mutable struct BondValue
value::Any
end
Expand Down Expand Up @@ -56,6 +59,8 @@ Base.@kwdef mutable struct Notebook
last_hot_reload_time::typeof(time())=zero(time())

bonds::Dict{Symbol,BondValue}=Dict{Symbol,BondValue}()

metadata::Dict{String, Any}=copy(DEFAULT_NOTEBOOK_METADATA)
end

_collect_cells(cells_dict::Dict{UUID,Cell}, cells_order::Vector{UUID}) =
Expand Down Expand Up @@ -91,7 +96,12 @@ function Base.getproperty(notebook::Notebook, property::Symbol)
end
end


emptynotebook(args...) = Notebook([Cell()], args...)


const _notebook_header = "### A Pluto.jl notebook ###"
const _notebook_metadata_prefix = "#> "
# We use a creative delimiter to avoid accidental use in code
# so don't get inspired to suddenly use these in your code!
const _cell_id_delimiter = "# ╔═╡ "
Expand All @@ -106,7 +116,6 @@ const _disabled_suffix = "\n ╠═╡ =#"
const _ptoml_cell_id = UUID(1)
const _mtoml_cell_id = UUID(2)

emptynotebook(args...) = Notebook([Cell()], args...)

"""
Save the notebook to `io`, `file` or to `notebook.path`.
Expand All @@ -118,6 +127,17 @@ Have a look at our [JuliaCon 2020 presentation](https://youtu.be/IAF8DjrQSSk?t=1
function save_notebook(io, notebook::Notebook)
println(io, _notebook_header)
println(io, "# ", PLUTO_VERSION_STR)

# Notebook metadata
let nb_metadata_toml = strip(sprint(TOML.print, get_metadata_no_default(notebook)))
if !isempty(nb_metadata_toml)
println(io)
for line in split(nb_metadata_toml, "\n")
println(io, _notebook_metadata_prefix, line)
end
end
end

# Anything between the version string and the first UUID delimiter will be ignored by the notebook loader.
println(io, "")
println(io, "using Markdown")
Expand All @@ -134,13 +154,16 @@ function save_notebook(io, notebook::Notebook)

for c in cells_ordered
println(io, _cell_id_delimiter, string(c.cell_id))
metadata_toml = strip(sprint(TOML.print, get_cell_metadata_no_default(c)))
if metadata_toml != ""
for line in split(metadata_toml, "\n")
println(io, _cell_metadata_prefix, line)

let metadata_toml = strip(sprint(TOML.print, get_metadata_no_default(c)))
if metadata_toml != ""
for line in split(metadata_toml, "\n")
println(io, _cell_metadata_prefix, line)
end
end
end
cell_running_disabled = c.metadata["disabled"]

cell_running_disabled = get(c.metadata, "disabled", false)::Bool
if cell_running_disabled || c.depends_on_disabled_cells
print(io, _disabled_prefix)
print(io, replace(c.code, _cell_id_delimiter => "# "))
Expand Down Expand Up @@ -210,7 +233,10 @@ save_notebook(notebook::Notebook) = save_notebook(notebook, notebook.path)

"Load a notebook without saving it or creating a backup; returns a `Notebook`. REMEMBER TO CHANGE THE NOTEBOOK PATH after loading it to prevent it from autosaving and overwriting the original file."
function load_notebook_nobackup(@nospecialize(io::IO), @nospecialize(path::AbstractString))::Notebook
firstline = String(readline(io))::String

## HEADER

firstline = String(readline(io))

if firstline != _notebook_header
error("File is not a Pluto.jl notebook")
Expand All @@ -220,12 +246,28 @@ function load_notebook_nobackup(@nospecialize(io::IO), @nospecialize(path::Abstr
if file_VERSION_STR != PLUTO_VERSION_STR
# @info "Loading a notebook saved with Pluto $(file_VERSION_STR). This is Pluto $(PLUTO_VERSION_STR)."
end

# Read all remaining file contents before the first cell delimiter.
header_content = readuntil(io, _cell_id_delimiter)
header_lines = split(header_content, "\n")

nb_prefix_length = ncodeunits(_notebook_metadata_prefix)
nb_metadata_toml_lines = String[
line[begin+nb_prefix_length:end]
for line in header_lines if startswith(line, _notebook_metadata_prefix)
]

notebook_metadata = try
create_notebook_metadata(TOML.parse(join(nb_metadata_toml_lines, "\n")))
catch e
@error "Failed to parse embedded TOML content" exception=(e, catch_backtrace())
DEFAULT_NOTEBOOK_METADATA
end


### CELLS

collected_cells = Dict{UUID,Cell}()

# ignore first bits of file
readuntil(io, _cell_id_delimiter)

while !eof(io)
cell_id_str = String(readline(io))
if cell_id_str == "Cell order:"
Expand Down Expand Up @@ -257,7 +299,12 @@ function load_notebook_nobackup(@nospecialize(io::IO), @nospecialize(path::Abstr
code = code_normalised[1:prevind(code_normalised, end, length(_cell_suffix))]

# parse metadata
metadata = Dict{String, Any}(DEFAULT_METADATA..., TOML.parse(join(metadata_toml_lines, "\n"))...)
metadata = try
create_cell_metadata(TOML.parse(join(metadata_toml_lines, "\n")))
catch
@error "Failed to parse embedded TOML content" cell_id exception=(e, catch_backtrace())
DEFAULT_CELL_METADATA
end

read_cell = Cell(; cell_id, code, metadata)
collected_cells[cell_id] = read_cell
Expand Down Expand Up @@ -335,6 +382,7 @@ function load_notebook_nobackup(@nospecialize(io::IO), @nospecialize(path::Abstr
path=path,
nbpkg_ctx=nbpkg_ctx,
nbpkg_installed_versions_cache=nbpkg_cache(nbpkg_ctx),
metadata=notebook_metadata,
)
end

Expand Down Expand Up @@ -422,3 +470,10 @@ function sample_notebook(name::String)
nb.path = tempname() * ".jl"
nb
end

create_cell_metadata(metadata::Dict{String,<:Any}) = merge(DEFAULT_CELL_METADATA, metadata)
create_notebook_metadata(metadata::Dict{String,<:Any}) = merge(DEFAULT_NOTEBOOK_METADATA, metadata)
get_metadata(cell::Cell)::Dict{String,Any} = cell.metadata
get_metadata(notebook::Notebook)::Dict{String,Any} = notebook.metadata
get_metadata_no_default(cell::Cell)::Dict{String,Any} = Dict{String,Any}(setdiff(pairs(cell.metadata), pairs(DEFAULT_CELL_METADATA)))
get_metadata_no_default(notebook::Notebook)::Dict{String,Any} = Dict{String,Any}(setdiff(pairs(notebook.metadata), pairs(DEFAULT_NOTEBOOK_METADATA)))
7 changes: 7 additions & 0 deletions src/webserver/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ function notebook_to_js(notebook::Notebook)
"value" => bondvalue.value,
)
for (key, bondvalue) in notebook.bonds),
"metadata" => notebook.metadata,
"nbpkg" => let
ctx = notebook.nbpkg_ctx
Dict{String,Any}(
Expand Down Expand Up @@ -267,6 +268,12 @@ const effects_of_changed_state = Dict(
Firebasey.applypatch!(request.notebook, patch)
[BondChanged(name, patch isa Firebasey.AddPatch)]
end,
),
"metadata" => Dict(
Wildcard() => function(property; request::ClientRequest, patch::Firebasey.JSONPatch)
Firebasey.applypatch!(request.notebook, patch)
[FileChanged()]
end
)
)

Expand Down
Loading