diff --git a/README.md b/README.md
index 22d5c17def..824fb1b002 100644
--- a/README.md
+++ b/README.md
@@ -69,9 +69,9 @@ Lastly, here's _**one more feature**_: Pluto notebooks have a `@bind` macro to c
-You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the sample notebooks inside Pluto!
+You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the featured notebooks inside Pluto!
-But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript sample notebook inside Pluto!
+But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript featured notebook inside Pluto!
@@ -160,9 +160,9 @@ Pluto.jl is open source! Specifically, it is [MIT Licensed](https://github.com/f
If you want to reference Pluto.jl in scientific writing, you can use our DOI: [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4792401.svg)](https://doi.org/10.5281/zenodo.4792401)
-### Sample notebooks
+### Featured notebooks
-The included sample notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use sample notebook code however you like - you do not need to credit us!
+Unless otherwise specified, the included featured notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use them however you like - you do not need to credit us!
Your notebook files are _yours_, you also do not need to credit us. Have fun!
diff --git a/frontend-bundler/package.json b/frontend-bundler/package.json
index 0ee5bd1416..c05068d4c9 100644
--- a/frontend-bundler/package.json
+++ b/frontend-bundler/package.json
@@ -6,8 +6,8 @@
"version": "1.0.0",
"description": "",
"scripts": {
- "start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html",
- "build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html",
+ "start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html",
+ "build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
diff --git a/frontend/common/Binder.js b/frontend/common/Binder.js
index 8ac593a9f9..2ecbcd3e99 100644
--- a/frontend/common/Binder.js
+++ b/frontend/common/Binder.js
@@ -124,10 +124,17 @@ export const start_binder = async ({ setStatePromise, connect, launch_params })
let open_response = new Response()
if (launch_params.notebookfile.startsWith("data:")) {
- open_response = await fetch(with_token(new URL("notebookupload", binder_session_url)), {
- method: "POST",
- body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(),
- })
+ open_response = await fetch(
+ with_token(
+ with_query_params(new URL("notebookupload", binder_session_url), {
+ name: new URLSearchParams(window.location.search).get("name"),
+ })
+ ),
+ {
+ method: "POST",
+ body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(),
+ }
+ )
} else {
for (const [p1, p2] of [
["path", launch_params.notebookfile],
@@ -145,6 +152,12 @@ export const start_binder = async ({ setStatePromise, connect, launch_params })
}
}
+ if (!open_response.ok) {
+ let b = await open_response.blob()
+ window.location.href = URL.createObjectURL(b)
+ return
+ }
+
// Opening a notebook gives us the notebook ID, which means that we have a running session! Time to connect.
const new_notebook_id = await open_response.text()
diff --git a/frontend/common/Bond.js b/frontend/common/Bond.js
index a3a89af925..6efc6b2b4d 100644
--- a/frontend/common/Bond.js
+++ b/frontend/common/Bond.js
@@ -152,7 +152,7 @@ export const set_bound_elements_to_their_value = (bond_nodes, bond_values) => {
export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) => {
bond_nodes.forEach((bond_node) => {
const listener = (e) => {
- if (e.target.closest(".bonds_disabled.offer_binder")) {
+ if (e.target.closest(".bonds_disabled:where(.offer_binder, .offer_local)")) {
open_pluto_popup({
type: "info",
source_element: e.target,
@@ -163,6 +163,7 @@ export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) =>
//@ts-ignore
window.open_edit_or_run_popup()
e.preventDefault()
+ window.dispatchEvent(new CustomEvent("close pluto popup"))
}}
>Run this notebook
diff --git a/frontend/common/RunLocal.js b/frontend/common/RunLocal.js
new file mode 100644
index 0000000000..546c0f7d86
--- /dev/null
+++ b/frontend/common/RunLocal.js
@@ -0,0 +1,80 @@
+import immer from "../imports/immer.js"
+import { BackendLaunchPhase } from "./Binder.js"
+import { timeout_promise } from "./PlutoConnection.js"
+import { with_query_params } from "./URLTools.js"
+
+// This file is very similar to `start_binder` in Binder.js
+
+/**
+ *
+ * @param {{
+ * launch_params: import("../components/Editor.js").LaunchParameters,
+ * setStatePromise: any,
+ * connect: () => Promise,
+ * }} props
+ */
+export const start_local = async ({ setStatePromise, connect, launch_params }) => {
+ try {
+ if (launch_params.pluto_server_url == null || launch_params.notebookfile == null) throw Error("Invalid launch parameters for starting locally.")
+
+ await setStatePromise(
+ immer((state) => {
+ state.backend_launch_phase = BackendLaunchPhase.created
+ state.disable_ui = false
+ })
+ )
+
+ const with_token = (x) => String(x)
+ const binder_session_url = new URL(launch_params.pluto_server_url, window.location.href)
+
+ let open_response
+
+ // We download the notebook file contents, and then upload them to the Pluto server.
+ const notebook_contents = await (
+ await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity ?? undefined }))
+ ).arrayBuffer()
+
+ open_response = await fetch(
+ with_token(
+ with_query_params(new URL("notebookupload", binder_session_url), {
+ name: new URLSearchParams(window.location.search).get("name"),
+ clear_frontmatter: "yesplease",
+ })
+ ),
+ {
+ method: "POST",
+ body: notebook_contents,
+ }
+ )
+
+ if (!open_response.ok) {
+ let b = await open_response.blob()
+ window.location.href = URL.createObjectURL(b)
+ return
+ }
+
+ const new_notebook_id = await open_response.text()
+ const edit_url = with_query_params(new URL("edit", binder_session_url), { id: new_notebook_id })
+ console.info("notebook_id:", new_notebook_id)
+
+ window.history.replaceState({}, "", edit_url)
+
+ await setStatePromise(
+ immer((state) => {
+ state.notebook.notebook_id = new_notebook_id
+ state.backend_launch_phase = BackendLaunchPhase.notebook_running
+ })
+ )
+ console.log("Connecting WebSocket")
+
+ const connect_promise = connect()
+ await timeout_promise(connect_promise, 20_000).catch((e) => {
+ console.error("Failed to establish connection within 20 seconds. Navigating to the edit URL directly.", e)
+
+ window.parent.location.href = with_token(edit_url)
+ })
+ } catch (err) {
+ console.error("Failed to initialize binder!", err)
+ alert("Something went wrong! 😮\n\nWe failed to open this notebook. Please try again with a different browser, or come back later.")
+ }
+}
diff --git a/frontend/components/BinderButton.js b/frontend/components/EditOrRunButton.js
similarity index 93%
rename from frontend/components/BinderButton.js
rename to frontend/components/EditOrRunButton.js
index 725ec61122..181008b02e 100644
--- a/frontend/components/BinderButton.js
+++ b/frontend/components/EditOrRunButton.js
@@ -1,6 +1,25 @@
import { BackendLaunchPhase } from "../common/Binder.js"
import { html, useEffect, useState, useRef } from "../imports/Preact.js"
+export const RunLocalButton = ({ show, start_local }) => {
+ //@ts-ignore
+ window.open_edit_or_run_popup = () => {
+ start_local()
+ }
+
+ return html`
`
}
// /open will execute a script from your hard drive, so we include a token in the URL to prevent a mean person from getting a bad file on your computer _using another hypothetical intrusion_, and executing it using Pluto
diff --git a/frontend/components/welcome/Recent.js b/frontend/components/welcome/Recent.js
index 7bcfdfca28..815987456e 100644
--- a/frontend/components/welcome/Recent.js
+++ b/frontend/components/welcome/Recent.js
@@ -181,7 +181,7 @@ export const Recent = ({ client, connected, remote_notebooks, CustomRecent }) =>
})}
>
if (CustomRecent == null) {
return html`
-