Skip to content

Commit e1a9b38

Browse files
fonspclaude
andauthored
Forward pluto_* args verbatim to launch parameters (#2)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 776f17c commit e1a9b38

4 files changed

Lines changed: 94 additions & 102 deletions

File tree

‎src/builder.jl‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# 2. For blocks with a local `notebook`: run the notebook headlessly, copy the
66
# .jl and generated .plutostate into the build output, and fill in
77
# `resolved_notebook` / `resolved_state` on the block.
8-
# 3. For blocks with a `url`: pass `url`/`state` straight through to the
9-
# resolved fields.
8+
# 3. For blocks without `notebook`: nothing to resolve — the pre-hosted URLs
9+
# are already sitting in `pluto_params` and will be emitted verbatim.
1010
# 4. If any PlutoBlock was seen, push one `RawHTMLHeadContent` onto the
1111
# site's `format.assets` so every page loads Pluto's frontend.
1212

@@ -62,11 +62,7 @@ function _materialise_notebooks!(doc, blocks)
6262
asset_dir = joinpath(build_root, _PLUTO_ASSET_SUBDIR)
6363

6464
for (page, block) in blocks
65-
if block.url !== nothing
66-
block.resolved_notebook = block.url
67-
block.resolved_state = block.state
68-
continue
69-
end
65+
block.notebook === nothing && continue # pre-hosted: pluto_params carries the URLs
7066

7167
nb_rel = block.notebook::String
7268
nb_src = isabspath(nb_rel) ? nb_rel : normpath(joinpath(source_root, nb_rel))

‎src/domify.jl‎

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
# Render a PlutoBlock as the HTML a Pluto frontend needs to bring itself online:
2-
# the `launch-parameters` <script> that sets window.pluto_{notebookfile,state…},
3-
# followed by the <pluto-editor> placeholder that the JS in <head> hydrates.
2+
# the `launch-parameters` <script> that sets window.pluto_* globals, followed
3+
# by the <pluto-editor> placeholder that the JS in <head> hydrates.
44

55
function Documenter.HTMLWriter.domify(dctx::DCtx, ::Node, block::PlutoBlock)
66
page_url = Documenter.HTMLWriter.get_url(dctx.ctx, dctx.navnode.page)
7-
nb_href = _resolve_href(page_url, block.resolved_notebook)
8-
state_href = _resolve_href(page_url, block.resolved_state)
97

10-
launch_params = """
11-
window.pluto_notebookfile = $(_js_string(nb_href));
12-
window.pluto_disable_ui = $(_js_string(block.disable_ui));
13-
window.pluto_binder_url = $(_js_string(block.binder));
14-
window.pluto_statefile = $(_js_string(state_href));
15-
window.pluto_recording_url = undefined;
16-
window.pluto_recording_audio_url = undefined;
17-
"""
8+
# Start from the user's pluto_* params, then overlay the locally-resolved
9+
# notebook/state paths (if any) so they win over anything the user set.
10+
params = copy(block.pluto_params)
11+
if block.resolved_notebook !== nothing
12+
params["pluto_notebookfile"] = Documenter.HTMLWriter.relhref(page_url, block.resolved_notebook)
13+
end
14+
if block.resolved_state !== nothing
15+
params["pluto_statefile"] = Documenter.HTMLWriter.relhref(page_url, block.resolved_state)
16+
end
17+
18+
launch_params = join(
19+
["window.$k = $(_js_string(params[k]));" for k in sort!(collect(keys(params)))],
20+
"\n",
21+
)
1822

1923
editor = DOM.Tag(Symbol("pluto-editor"))[ :class => "loading" ](
2024
DOM.Tag(:progress)[
@@ -30,34 +34,18 @@ function Documenter.HTMLWriter.domify(dctx::DCtx, ::Node, block::PlutoBlock)
3034
]
3135
end
3236

33-
# target is either an external URL (http/https), or a build-root-relative path
34-
# like "pluto_notebooks/foo.jl". For the relative case, turn it into a
35-
# page-relative href via Documenter's own helper so that baseurl/prettyurl
36-
# deployments keep working.
37-
function _resolve_href(page_url::AbstractString, target::Union{String,Nothing})
38-
target === nothing && return ""
39-
_is_absolute_url(target) && return target
40-
return Documenter.HTMLWriter.relhref(page_url, target)
41-
end
42-
43-
_is_absolute_url(s::AbstractString) =
44-
startswith(s, "http://") || startswith(s, "https://") || startswith(s, "//") || startswith(s, "/")
45-
46-
# Minimal JS string literal: JSON encoding is a superset of JS-string syntax for
47-
# our purposes (URLs), and Julia's repr() for String happens to produce a valid JS string too. Use repr.
48-
4937
struct JSLiteral
5038
x
5139
end
5240

53-
54-
_js_string(s::AbstractString) = repr(s)
55-
_js_string(s::Nothing) = "undefined"
56-
_js_string(s::Bool) = repr(s)
57-
_js_string(s::Number) = repr(s)
58-
_js_string(s::JSLiteral) = s.x
59-
60-
61-
62-
63-
41+
# JS literal encoding for the scalar types TOML can produce, plus Nothing
42+
# (→ `undefined`) and JSLiteral (raw unquoted JS, e.g. `undefined`, function
43+
# literals) for user-provided pluto_* values that need to be something other
44+
# than a string/number/bool.
45+
_js_string(s::AbstractString) = repr(String(s))
46+
_js_string(s::Nothing) = "undefined"
47+
_js_string(s::Bool) = s ? "true" : "false"
48+
_js_string(s::Number) = string(s)
49+
_js_string(s::AbstractVector) = "[" * join((_js_string(x) for x in s), ", ") * "]"
50+
_js_string(s::AbstractDict) = "{" * join(("$(repr(String(k))): $(_js_string(v))" for (k,v) in s), ", ") * "}"
51+
_js_string(s::JSLiteral) = s.x

‎src/expander.jl‎

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,36 @@
77
PlutoBlock
88
99
MarkdownAST element produced by the `@pluto` expander. Holds the parsed
10-
attributes of one ```` ```@pluto ```` code block:
10+
attributes of one ```` ```@pluto ```` code block.
11+
12+
The only recognised special key is:
1113
1214
- `notebook :: Union{String, Nothing}` — path to a local `.jl` file, relative to
1315
the Documenter source root. When set, the plugin runs the notebook headlessly
1416
and copies both the `.jl` and the generated `.plutostate` into the build
15-
output.
16-
- `url :: Union{String, Nothing}` — URL of a hosted notebook file
17-
(`pluto_notebookfile`).
18-
- `state :: Union{String, Nothing}` — URL of a hosted `.plutostate` file.
19-
- `binder :: Union{String, Nothing}` — optional Binder launch URL.
20-
- `disable_ui :: Bool` — whether to hide Pluto's UI chrome (default `true`).
17+
output, then exposes them to the frontend as `window.pluto_notebookfile` and
18+
`window.pluto_statefile`.
2119
22-
Exactly one of `notebook` or `url` must be set. When `url` is set, `state` is
23-
required too.
20+
Every other key must start with `pluto_` and is forwarded verbatim to the
21+
frontend as `window.<key> = <value>;` in the launch-parameters script. Typical
22+
keys for pre-hosted notebooks are `pluto_notebookfile` and `pluto_statefile`;
23+
see Pluto's frontend for the full list (`pluto_disable_ui`, `pluto_binder_url`,
24+
`pluto_recording_url`, …).
2425
25-
The resolved URLs used by the frontend (`window.pluto_notebookfile`,
26-
`window.pluto_statefile`) are filled in by the builder step before domify runs.
26+
Exactly one of the following must hold: `notebook` is set, *or* both
27+
`pluto_notebookfile` and `pluto_statefile` are set.
2728
"""
2829
mutable struct PlutoBlock <: Documenter.AbstractDocumenterBlock
29-
notebook :: Union{String, Nothing}
30-
url :: Union{String, Nothing}
31-
state :: Union{String, Nothing}
32-
binder :: Union{String, Nothing}
33-
disable_ui :: Bool
34-
# Filled in by the builder: final URLs the rendered <script> will point at.
35-
# For a local `notebook`, these become build-relative paths.
30+
notebook :: Union{String, Nothing}
31+
pluto_params :: Dict{String, Any}
32+
# Filled in by the builder for local notebooks: build-root-relative paths
33+
# that domify turns into page-relative hrefs.
3634
resolved_notebook :: Union{String, Nothing}
3735
resolved_state :: Union{String, Nothing}
3836
end
3937

40-
function PlutoBlock(; notebook=nothing, url=nothing, state=nothing, binder=nothing, disable_ui=true)
41-
PlutoBlock(notebook, url, state, binder, disable_ui, nothing, nothing)
42-
end
38+
PlutoBlock(; notebook=nothing, pluto_params=Dict{String,Any}()) =
39+
PlutoBlock(notebook, pluto_params, nothing, nothing)
4340

4441
abstract type PlutoExpander <: Documenter.Expanders.ExpanderPipeline end
4542
Selectors.order(::Type{PlutoExpander}) = 7.9
@@ -57,36 +54,36 @@ function Selectors.runner(::Type{PlutoExpander}, node, page, doc)
5754
return
5855
end
5956

60-
# The block body is TOML. Keys accepted (case-insensitive, snake-cased):
61-
# notebook | url | state | binder | disable_ui
57+
# The block body is TOML. `notebook` is special; every other key must start
58+
# with `pluto_` and is forwarded as-is to `window.<key>` in the launch script.
6259
function _parse_pluto_block(code::AbstractString)
6360
raw = isempty(strip(code)) ? Dict{String,Any}() : TOML.parse(code)
64-
# Normalise keys to lowercase so `Notebook=` and `notebook=` both work.
65-
kv = Dict{String,Any}(lowercase(k) => v for (k, v) in raw)
66-
67-
notebook = get(kv, "notebook", nothing)
68-
url = get(kv, "url", nothing)
69-
state = get(kv, "state", nothing)
70-
binder = get(kv, "binder", nothing)
71-
disable_ui = get(kv, "disable_ui", true)
7261

73-
if notebook === nothing && url === nothing
74-
error("`@pluto` block must set either `notebook = \"path/to/notebook.jl\"` or `url = \"…\"` + `state = \"…\"`.")
62+
notebook = nothing
63+
pluto_params = Dict{String,Any}()
64+
for (k, v) in raw
65+
if k == "notebook"
66+
v isa AbstractString || error("`@pluto`: `notebook` must be a string.")
67+
notebook = String(v)
68+
elseif startswith(k, "pluto_")
69+
pluto_params[k] = v
70+
else
71+
error("`@pluto`: unknown key `$k`. Use `notebook = \"…\"` or a key starting with `pluto_`.")
72+
end
7573
end
76-
if notebook !== nothing && url !== nothing
77-
error("`@pluto` block sets both `notebook` and `url`; use exactly one.")
74+
75+
# Default: hide Pluto's UI chrome in docs builds.
76+
get!(pluto_params, "pluto_disable_ui", true)
77+
78+
has_urls = haskey(pluto_params, "pluto_notebookfile") && haskey(pluto_params, "pluto_statefile")
79+
if notebook === nothing && !has_urls
80+
error("`@pluto` block must set either `notebook = \"path/to/notebook.jl\"` or both `pluto_notebookfile` and `pluto_statefile`.")
7881
end
79-
if url !== nothing && state === nothing
80-
error("`@pluto` block with `url` must also set `state = \"…plutostate\"`.")
82+
if notebook !== nothing && (haskey(pluto_params, "pluto_notebookfile") || haskey(pluto_params, "pluto_statefile"))
83+
error("`@pluto` block sets `notebook` together with `pluto_notebookfile`/`pluto_statefile`; use one or the other.")
8184
end
8285

83-
PlutoBlock(;
84-
notebook = notebook isa AbstractString ? String(notebook) : nothing,
85-
url = url isa AbstractString ? String(url) : nothing,
86-
state = state isa AbstractString ? String(state) : nothing,
87-
binder = binder isa AbstractString ? String(binder) : nothing,
88-
disable_ui = Bool(disable_ui),
89-
)
86+
return PlutoBlock(; notebook, pluto_params)
9087
end
9188

9289
# Documenter's search-index and link-check phases flatten pages to plain text

‎test/runtests.jl‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,35 @@ const DP = DocumenterPluto
66

77
@testset "block parsing" begin
88
b = DP._parse_pluto_block("""
9-
url = "https://example.com/n.jl"
10-
state = "https://example.com/n.plutostate"
9+
pluto_notebookfile = "https://example.com/n.jl"
10+
pluto_statefile = "https://example.com/n.plutostate"
1111
""")
1212
@test b isa DP.PlutoBlock
13-
@test b.url == "https://example.com/n.jl"
14-
@test b.state == "https://example.com/n.plutostate"
1513
@test b.notebook === nothing
16-
@test b.disable_ui == true
14+
@test b.pluto_params["pluto_notebookfile"] == "https://example.com/n.jl"
15+
@test b.pluto_params["pluto_statefile"] == "https://example.com/n.plutostate"
16+
@test b.pluto_params["pluto_disable_ui"] == true # default
1717

1818
b2 = DP._parse_pluto_block("""
19-
notebook = "notebooks/intro.jl"
20-
disable_ui = false
19+
notebook = "notebooks/intro.jl"
20+
pluto_disable_ui = false
21+
pluto_binder_url = "https://mybinder.org/…"
2122
""")
2223
@test b2.notebook == "notebooks/intro.jl"
23-
@test b2.disable_ui == false
24+
@test b2.pluto_params["pluto_disable_ui"] == false
25+
@test b2.pluto_params["pluto_binder_url"] == "https://mybinder.org/…"
2426

27+
# empty block rejected
2528
@test_throws Exception DP._parse_pluto_block("")
26-
@test_throws Exception DP._parse_pluto_block("""notebook = "a"
27-
url = "b" """)
29+
# can't mix local notebook with explicit pluto_notebookfile
30+
@test_throws Exception DP._parse_pluto_block("""
31+
notebook = "a.jl"
32+
pluto_notebookfile = "https://example.com/n.jl"
33+
pluto_statefile = "https://example.com/n.plutostate"
34+
""")
35+
# pluto_statefile required alongside pluto_notebookfile
36+
@test_throws Exception DP._parse_pluto_block("""pluto_notebookfile = "https://example.com/n.jl" """)
37+
# unknown (non-pluto_) key rejected
2838
@test_throws Exception DP._parse_pluto_block("""url = "https://example.com/n.jl" """)
2939
end
3040

@@ -43,8 +53,8 @@ end
4353
# Test
4454
4555
```@pluto
46-
url = "https://example.com/n.jl"
47-
state = "https://example.com/n.plutostate"
56+
pluto_notebookfile = "https://example.com/n.jl"
57+
pluto_statefile = "https://example.com/n.plutostate"
4858
```
4959
""")
5060

@@ -63,5 +73,6 @@ end
6373
@test occursin("pluto-editor", html)
6474
@test occursin("https://example.com/n.jl", html)
6575
@test occursin("https://example.com/n.plutostate", html)
76+
@test occursin("window.pluto_disable_ui = true;", html)
6677
end
6778
end

0 commit comments

Comments
 (0)