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

Make published_to_js (now publish_to_js) official API through AbstractPlutoDingetjes.jl (v2) #2608

Merged
merged 13 commits into from
Jul 20, 2023
109 changes: 51 additions & 58 deletions src/runner/PlutoRunner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,13 @@ function formatted_result_of(
output_formatted = if (!ends_with_semicolon || errored)
logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id)
with_logger_and_io_to_logs(logger; capture_stdout, stdio_loglevel=stdout_log_level) do
format_output(ans; context=IOContext(default_iocontext, :extra_items=>extra_items, :module => workspace))
format_output(ans; context=IOContext(
default_iocontext,
:extra_items=>extra_items,
:module => workspace,
:pluto_notebook_id => notebook_id,
:pluto_cell_id => cell_id,
))
end
else
("", MIME"text/plain"())
Expand Down Expand Up @@ -972,6 +978,7 @@ const default_iocontext = IOContext(devnull,
:displaysize => (18, 88),
:is_pluto => true,
:pluto_supported_integration_features => supported_integration_features,
:pluto_published_to_js => (io, x) -> core_published_to_js(io, x),
)

const default_stdout_iocontext = IOContext(devnull,
Expand Down Expand Up @@ -1494,17 +1501,29 @@ const integrations = Integration[
id = Base.PkgId(Base.UUID(reinterpret(UInt128, codeunits("Paul Berg Berlin")) |> first), "AbstractPlutoDingetjes"),
code = quote
@assert v"1.0.0" <= AbstractPlutoDingetjes.MY_VERSION < v"2.0.0"
initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value
transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value
possible_bond_values_ref[] = AbstractPlutoDingetjes.Bonds.possible_values

push!(supported_integration_features,

supported!(xs...) = push!(supported_integration_features, xs...)

# don't need feature checks for these because they existed in every version of AbstractPlutoDingetjes:
supported!(
AbstractPlutoDingetjes,
AbstractPlutoDingetjes.Bonds,
AbstractPlutoDingetjes.Bonds.initial_value,
AbstractPlutoDingetjes.Bonds.transform_value,
AbstractPlutoDingetjes.Bonds.possible_values,
)
initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value
transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value
possible_bond_values_ref[] = AbstractPlutoDingetjes.Bonds.possible_values

# feature checks because these were added in a later release of AbstractPlutoDingetjes
if isdefined(AbstractPlutoDingetjes, :Display)
supported!(AbstractPlutoDingetjes.Display)
if isdefined(AbstractPlutoDingetjes.Display, :published_to_js)
supported!(AbstractPlutoDingetjes.Display.published_to_js)
end
end

end,
),
Integration(
Expand Down Expand Up @@ -2125,74 +2144,48 @@ end"""
"""
const currently_running_cell_id = Ref{UUID}(uuid4())

function _publish(x, id_start, cell_id)::String
id = "$(notebook_id[])/$cell_id/$id_start"
d = get!(Dict{String,Any}, cell_published_objects, cell_id)
function core_published_to_js(io, x)
assertpackable(x)

id_start = objectid2str(x)

_notebook_id = get(io, :pluto_notebook_id, notebook_id[])::UUID
_cell_id = get(io, :pluto_cell_id, currently_running_cell_id[])::UUID

# The unique identifier of this object
id = "$_notebook_id/$id_start"

d = get!(Dict{String,Any}, cell_published_objects, _cell_id)
d[id] = x
return id

write(io, "/* See the documentation for AbstractPlutoDingetjes.Display.published_to_js */ getPublishedObject(\"$(id)\")")

return nothing
end

# TODO? Possibly move this to it's own package, with fallback that actually msgpack?
# ..... Ideally we'd make this require `await` on the javascript side too...
Base.@kwdef struct PublishedToJavascript
# TODO: This is the deprecated old function. Remove me at some point.
struct PublishedToJavascript
published_object
published_id_start
cell_id
end
function Base.show(io::IO, ::MIME"text/javascript", published::PublishedToJavascript)
id = _publish(published.published_object, published.published_id_start, published.cell_id)
# if published.cell_id != currently_running_cell_id[]
# error("Showing result from PlutoRunner.publish_to_js() in a cell different from where it was created, not (yet?) supported.")
# end
write(io, "/* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"$(id)\")")
core_published_to_js(io, published.published_object)
end
Base.show(io::IO, ::MIME"text/plain", published::PublishedToJavascript) = show(io, MIME("text/javascript"), published)
Base.show(io::IO, published::PublishedToJavascript) = show(io, MIME("text/javascript"), published)

"""
publish_to_js(x)
Make the object `x` available to the JS runtime of this cell. The returned string is a JS command that, when executed in this cell's output, gives the object.
!!! warning
This function is not yet public API, it will become public in the next weeks. Only use for experiments.
# Example
```julia
let
x = Dict(
"data" => rand(Float64, 20),
"name" => "juliette",
)
HTML("\""
<script>
// we interpolate into JavaScript:
const x = \$(PlutoRunner.publish_to_js(x))
console.log(x.name, x.data)
</script>
"\"")
end
```
"""
publish_to_js(x) = publish_to_js(x, objectid2str(x))
# TODO: This is the deprecated old function. Remove me at some point.
function publish_to_js(x)
@warn "Deprecated, use `AbstractPlutoDingetjes.Display.published_to_js(x)` instead of `PlutoRunner.publish_to_js(x)`."

function publish_to_js(x, id_start)
assertpackable(x)
PublishedToJavascript(
published_object=x,
published_id_start=id_start,
cell_id=currently_running_cell_id[],
)
PublishedToJavascript(x)
end

const Packable = Union{Nothing,Missing,String,Symbol,Int64,Int32,Int16,Int8,UInt64,UInt32,UInt16,UInt8,Float32,Float64,Bool,MIME,UUID,DateTime}
assertpackable(::Packable) = true
assertpackable(::Packable) = nothing
assertpackable(t::Any) = throw(ArgumentError("Only simple objects can be shared with JS, like vectors and dictionaries. $(string(typeof(t))) is not compatible."))
assertpackable(::Vector{<:Packable}) = true
assertpackable(::Dict{<:Packable,<:Packable}) = true
assertpackable(::Vector{<:Packable}) = nothing
assertpackable(::Dict{<:Packable,<:Packable}) = nothing
assertpackable(x::Vector) = foreach(assertpackable, x)
assertpackable(d::Dict) = let
foreach(assertpackable, keys(d))
Expand Down
72 changes: 57 additions & 15 deletions test/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -199,32 +199,63 @@ end

notebook = Notebook([
Cell("PlutoRunner.notebook_id[] |> Text"),
# These cells tests `core_published_to_js`, which is the function used by the official API (`AbtractPlutoDingetjes.Display.published_to_js`).
Cell(cid, """
let
# not actually public API but we test it anyways
a = PlutoRunner._publish(Dict(
begin
a = Dict(
"hello" => "world",
"xx" => UInt8[6,7,8],
), "aaa", Base.UUID("$cid"))
b = PlutoRunner._publish("cool", "bbb", Base.UUID("$cid"))
Text((a, b))
)
b = "cool"
struct ZZZ
x
y
end
function Base.show(io::IO, ::MIME"text/html", z::ZZZ)
write(io, "<script>\n")
PlutoRunner.core_published_to_js(io, z.x)
PlutoRunner.core_published_to_js(io, z.y)
write(io, "\n</script>")
end
ZZZ(a, b)
end
"""),
Cell("""
begin
struct ABC
x
end
ZZZ(
123,
Dict("a" => 234, "b" => ABC(4)),
)
end
"""),
Cell("3"),
# This is the deprecated API:
Cell("PlutoRunner.publish_to_js(Ref(4))"),
Cell("PlutoRunner.publish_to_js((ref=4,))"),
Cell("PlutoRunner.publish_to_js((ref=5,))"),
Cell("x = Dict(:a => 6)"),
Cell("PlutoRunner.publish_to_js(x)"),
])

update_save_run!(🍭, notebook, notebook.cells)
@test notebook.cells[1].output.body == notebook.notebook_id |> string

@test !notebook.cells[2].errored
a, b = Meta.parse(notebook.cells[2].output.body) |> eval
@test notebook.cells[2] |> noerror
@test notebook.cells[2].output.mime isa MIME"text/html"

ab1, ab2 = keys(notebook.cells[2].published_objects)
@test occursin(ab1, notebook.cells[2].output.body)
@test occursin(ab2, notebook.cells[2].output.body)

ab() = sort(collect(keys(notebook.cells[2].published_objects)); by=(s -> findfirst(s, notebook.cells[2].output.body) |> first))
a, b = ab()

p = notebook.cells[2].published_objects
@test sort(collect(keys(p))) == sort([a,b])
@test isempty(notebook.cells[3].published_objects)

@test p[a] == Dict(
"hello" => "world",
Expand All @@ -236,18 +267,29 @@ end
old_pb = p[b]
update_save_run!(🍭, notebook, notebook.cells)
p = notebook.cells[2].published_objects
a, b = Meta.parse(notebook.cells[2].output.body) |> eval
a, b = ab()
@test p[a] == old_pa
@test p[b] == old_pb

@test !isempty(notebook.cells[2].published_objects)

# display should have failed
@test only(values(notebook.cells[3].published_objects)) == 123
msg = notebook.cells[3].output.body[:msg]
@test occursin("Failed to show value", msg)
@test occursin("ABC is not compatible", msg)



setcode!(notebook.cells[2], "2")
update_save_run!(🍭, notebook, notebook.cells)
@test isempty(notebook.cells[2].published_objects)

@test isempty(notebook.cells[2].published_objects)



@test notebook.cells[4].errored
@test !notebook.cells[5].errored
@test notebook.cells[5] |> noerror
@test !isempty(notebook.cells[5].published_objects)


Expand Down
58 changes: 58 additions & 0 deletions test/frontend/__tests__/published_to_js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import puppeteer from "puppeteer"
import { lastElement, saveScreenshot, getTestScreenshotPath, createPage } from "../helpers/common"
import {
getCellIds,
importNotebook,
waitForCellOutput,
getPlutoUrl,
prewarmPluto,
writeSingleLineInPlutoInput,
waitForNoUpdateOngoing,
shutdownCurrentNotebook,
setupPlutoBrowser,
} from "../helpers/pluto"

describe("published_to_js", () => {
/**
* Launch a shared browser instance for all tests.
* I don't use jest-puppeteer because it takes away a lot of control and works buggy for me,
* so I need to manually create the shared browser.
* @type {puppeteer.Browser}
*/
let browser = null
/** @type {puppeteer.Page} */
let page = null
beforeAll(async () => {
browser = await setupPlutoBrowser()
})
beforeEach(async () => {
page = await createPage(browser)
await page.goto(getPlutoUrl(), { waitUntil: "networkidle0" })
})
afterEach(async () => {
await saveScreenshot(page)
await shutdownCurrentNotebook(page)
await page.close()
page = null
})
afterAll(async () => {
await browser.close()
browser = null
})

it("Should correctly show published_to_js in cell output, and in logs", async () => {
await importNotebook(page, "published_to_js.jl")
await waitForNoUpdateOngoing(page, { polling: 100 })
let output_of_published = await page.evaluate(() => {
return document.querySelector("#to_cell_output")?.textContent
})
expect(output_of_published).toBe("[1,2,3] MAGIC!")

// The log content is not shown, so #to_cell_log does not exist
let log_of_published = await page.evaluate(() => {
return document.querySelector("#to_cell_log")?.textContent
})
// This test is currently broken, due to https://github.com/fonsp/Pluto.jl/issues/2092
// expect(log_of_published).toBe("[4,5,6] MAGIC!")
})
})
50 changes: 50 additions & 0 deletions test/frontend/fixtures/published_to_js.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
### A Pluto.jl notebook ###
# v0.19.27

using Markdown
using InteractiveUtils

# ╔═╡ 2d69377e-23f8-11ee-116b-fb6a8f328528
begin
using Pkg
Pkg.activate(temp=true)
# the latest versions of these packages:
Pkg.add(url="https://github.com/JuliaPluto/AbstractPlutoDingetjes.jl", rev="main")
Pkg.add("HypertextLiteral")
end

# ╔═╡ 2ea26a4b-2d1e-4bcb-8b7b-cace79f7926a
begin
using AbstractPlutoDingetjes.Display: published_to_js
using HypertextLiteral
end

# ╔═╡ 043829fc-af3a-40b9-bb4f-f848ab50eb25
a = [1,2,3];

# ╔═╡ 2f4609fd-7361-4048-985a-2cc74bb25606
@htl """
<script>
const a = JSON.stringify($(published_to_js(a))) + " MAGIC!"
return html`<div id='to_cell_output'>\${a}</div>`
</script>
"""

# ╔═╡ 28eba9fd-0416-49b8-966e-03a381c19ca7
b = [4,5,6];

# ╔═╡ 0a4e8a19-6d43-4161-bb8c-1ebf8f8f68ba
@info @htl """
<script>
const a = JSON.stringify($(published_to_js(b))) + " MAGIC!"
return html`<div id='to_cell_log'>\${a}</div>`
</script>
"""

# ╔═╡ Cell order:
# ╠═2d69377e-23f8-11ee-116b-fb6a8f328528
# ╠═2ea26a4b-2d1e-4bcb-8b7b-cace79f7926a
# ╠═043829fc-af3a-40b9-bb4f-f848ab50eb25
# ╠═2f4609fd-7361-4048-985a-2cc74bb25606
# ╠═28eba9fd-0416-49b8-966e-03a381c19ca7
# ╠═0a4e8a19-6d43-4161-bb8c-1ebf8f8f68ba
Loading