From 912e94a8830771f767b0dbe5a49a8a5760c1cb65 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 13 Dec 2022 02:11:03 +0100 Subject: [PATCH 1/7] Make featured notebook sources a frontend launch param --- frontend/components/welcome/Featured.js | 95 +++++++++++++------------ frontend/components/welcome/Welcome.js | 47 +++++++++++- frontend/index.js | 20 +++++- 3 files changed, 113 insertions(+), 49 deletions(-) diff --git a/frontend/components/welcome/Featured.js b/frontend/components/welcome/Featured.js index 5457e6904a..51f1e5b9bc 100644 --- a/frontend/components/welcome/Featured.js +++ b/frontend/components/welcome/Featured.js @@ -1,28 +1,26 @@ -import featured_sources from "../../featured_sources.js" import _ from "../../imports/lodash.js" import { html, useEffect, useState } from "../../imports/Preact.js" +import register from "../../imports/PreactCustomElement.js" import { FeaturedCard } from "./FeaturedCard.js" -const run = (f) => f() - /** * @typedef SourceManifestNotebookEntry * @type {{ * id: String, * hash: String, - * html_path: String, - * statefile_path: String, - * notebookfile_path: String, - * frontmatter: Record, + * html_path?: String, + * statefile_path?: String, + * notebookfile_path?: String, + * frontmatter?: Record, * }} */ /** * @typedef SourceManifestCollectionEntry * @type {{ - * title: String?, - * description: String?, - * tags: Array?, + * title?: String, + * description?: String, + * tags?: Array | "everything", * }} */ @@ -30,16 +28,17 @@ const run = (f) => f() * @typedef SourceManifest * @type {{ * notebooks: Record, - * collections: Array?, - * pluto_version: String, - * julia_version: String, - * format_version: String, - * source_url: String, - * title: String?, - * description: String?, + * collections?: Array, + * pluto_version?: String, + * julia_version?: String, + * format_version?: String, + * source_url?: String, + * title?: String, + * description?: String, * }} */ +/** @type {SourceManifest[]} */ const placeholder_data = [ { title: "Featured Notebooks", @@ -50,7 +49,7 @@ const placeholder_data = [ tags: [], }, ], - notebooks: [], + notebooks: {}, }, ] @@ -77,21 +76,18 @@ const offline_html = html` ` -export const Featured = () => { - // Option 1: Dynamically load source list from a json: - // const [sources, set_sources] = useState(/** @type{Array<{url: String, integrity: String?}>?} */ (null)) - // useEffect(() => { - // run(async () => { - // const data = await (await fetch("featured_sources.json")).json() - - // set_sources(data.sources) - // }) - // }, []) - - // Option 2: From a JS file. This means that the source list can be bundled together. - const sources = featured_sources.sources +/** + * @typedef FeaturedSource + * @type {{url: String, integrity?: String}} + */ - const [source_data, set_source_data] = useState(/** @type{Array} */ ([])) +/** + * @param {{ + * sources: FeaturedSource[]? + * }} props + */ +export const Featured = ({ sources }) => { + const [source_data, set_source_data] = useState(/** @type{Record} */ ({})) useEffect(() => { if (sources != null) { @@ -102,13 +98,13 @@ export const Featured = () => { throw new Error(`Invalid format version: ${data.format_version}`) } - set_source_data((old) => [ + set_source_data((old) => ({ ...old, - { + [url]: { ...data, source_url: url, }, - ]) + })) }) Promise.any(promises).catch((e) => { @@ -119,7 +115,7 @@ export const Featured = () => { }, [sources]) useEffect(() => { - if (source_data?.length > 0) { + if (Object.entries(source_data).length > 0) { console.log("Sources:", source_data) } }, [source_data]) @@ -131,23 +127,29 @@ export const Featured = () => { }, 8 * 1000) }, []) - const no_data = !(source_data?.length > 0) + const no_data = Object.entries(source_data).length === 0 return no_data && waited_too_long ? offline_html : html` - ${(no_data ? placeholder_data : source_data).map( - (data) => html` + ${(no_data ? placeholder_data : Object.values(source_data)).map((/** @type {SourceManifest} */ data) => { + let collections = data?.collections ?? [ + { + title: "Notebooks", + tags: "everything", + }, + ] + return html` diff --git a/frontend/components/welcome/FeaturedCard.js b/frontend/components/welcome/FeaturedCard.js index b12f0529dc..98424a5626 100644 --- a/frontend/components/welcome/FeaturedCard.js +++ b/frontend/components/welcome/FeaturedCard.js @@ -10,27 +10,42 @@ const str_to_degree = (s) => ([...s].reduce((a, b) => a + b.charCodeAt(0), 0) * /** * @param {{ * entry: import("./Featured.js").SourceManifestNotebookEntry, - * source_url: String, + * source_url?: string, + * direct_html_links: boolean, * }} props */ -export const FeaturedCard = ({ entry, source_url }) => { +export const FeaturedCard = ({ entry, source_url, direct_html_links }) => { const title = entry.frontmatter?.title - const u = (x) => (x == null ? null : new URL(x, source_url).href) - const href = with_query_params(`editor.html`, { - statefile: u(entry.statefile_path), - notebookfile: u(entry.notebookfile_path), - notebookfile_integrity: `sha256-${base64url_to_base64(entry.hash)}`, - disable_ui: `true`, - pluto_server_url: `.`, - name: title == null ? null : `sample ${title}`, - }) + const u = (/** @type {string | null | undefined} */ x) => + source_url == null + ? x + : x == null + ? null + : // URLs are relative to the source URL... + new URL( + x, + // ...and the source URL is relative to the current location + new URL(source_url, window.location.href) + ).href + + // `direct_html_links` means that we will navigate you directly to the exported HTML file. Otherwise, we use our local editor, with the exported state as parameters. This lets users run the featured notebooks locally. + const href = direct_html_links + ? u(entry.html_path) + : with_query_params(`editor.html`, { + statefile: u(entry.statefile_path), + notebookfile: u(entry.notebookfile_path), + notebookfile_integrity: `sha256-${base64url_to_base64(entry.hash)}`, + disable_ui: `true`, + pluto_server_url: `.`, + name: title == null ? null : `sample ${title}`, + }) const author = author_info(entry.frontmatter) return html` - + ${author?.name == null ? null : html` @@ -38,8 +53,8 @@ export const FeaturedCard = ({ entry, source_url }) => { ${author.name} `} -

${entry.frontmatter.title}

-

${entry.frontmatter.description}

+

${entry?.frontmatter?.title ?? entry.id}

+

${entry?.frontmatter?.description}

` } diff --git a/frontend/components/welcome/Welcome.js b/frontend/components/welcome/Welcome.js index d437906d14..dafcdc8c6a 100644 --- a/frontend/components/welcome/Welcome.js +++ b/frontend/components/welcome/Welcome.js @@ -16,19 +16,21 @@ import default_featured_sources from "../../featured_sources.js" /** * @typedef NotebookListEntry * @type {{ - * notebook_id: String, - * path: String, - * in_temp_dir: Boolean, - * shortpath: String, + * notebook_id: string, + * path: string, + * in_temp_dir: boolean, + * shortpath: string, * }} */ /** * @typedef LaunchParameters * @type {{ - * featured_sources: import("./Featured.js").FeaturedSource[]?, - * featured_source_url?: String, - * featured_source_integrity?: String, + * featured_static: boolean, + * featured_direct_html_links: boolean, + * featured_sources: import("./Featured.js").FeaturedSource[]?, + * featured_source_url?: string, + * featured_source_integrity?: string, * }} */ @@ -53,6 +55,8 @@ export const Welcome = ({ launch_params }) => { const client_ref = useRef(/** @type {import('../../common/PlutoConnection').PlutoConnection} */ ({})) useEffect(() => { + if (launch_params.featured_static) return + const on_update = ({ message, type }) => { if (type === "notebook_list") { // a notebook list updates happened while the welcome screen is open, because a notebook started running for example @@ -93,7 +97,6 @@ export const Welcome = ({ launch_params }) => { // When block_screen_with_this_text is null (default), all is fine. When it is a string, we show a big banner with that text, and disable all other UI. https://github.com/fonsp/Pluto.jl/pull/2292 const [block_screen_with_this_text, set_block_screen_with_this_text] = useState(/** @type {string?} */ (null)) - const on_start_navigation = (value, expect_navigation = true) => { if (expect_navigation) { // Instead of calling set_block_screen_with_this_text(value) directly, we wait for the beforeunload to happen, and then we do it. If this event does not happen within 1 second, then that means that the user right-clicked, or Ctrl+Clicked (to open in a new tab), and we don't want to clear the main menu. https://github.com/fonsp/Pluto.jl/issues/2301 @@ -120,14 +123,27 @@ export const Welcome = ({ launch_params }) => { [launch_params] ) - return block_screen_with_this_text != null - ? html`` + if (block_screen_with_this_text != null) { + return html` + + ` + } + + const featured_html = html` + + ` + + return launch_params.featured_static + ? featured_html : html`

welcome to

-
@@ -151,11 +167,7 @@ export const Welcome = ({ launch_params }) => { />
- + ${featured_html} ` } diff --git a/frontend/index.js b/frontend/index.js index 8948625c9f..a32bb4f230 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -10,6 +10,11 @@ const url_params = new URLSearchParams(window.location.search) * @type {import("./components/welcome/Welcome.js").LaunchParameters} */ const launch_params = { + //@ts-ignore + featured_static: !!(url_params.get("featured_static") ?? window.pluto_featured_static), + //@ts-ignore + featured_direct_html_links: !!(url_params.get("featured_direct_html_links") ?? window.pluto_featured_direct_html_links), + //@ts-ignore featured_sources: window.pluto_featured_sources, @@ -21,5 +26,7 @@ const launch_params = { featured_source_integrity: url_params.get("featured_source_integrity") ?? window.pluto_featured_source_integrity, } +console.log("Launch parameters: ", launch_params) + // @ts-ignore render(html`<${Welcome} launch_params=${launch_params} />`, document.querySelector("#app")) diff --git a/src/notebook/Export.jl b/src/notebook/Export.jl index c32d3442ce..1948d61aa6 100644 --- a/src/notebook/Export.jl +++ b/src/notebook/Export.jl @@ -181,9 +181,6 @@ function frontmatter_html(frontmatter::Dict{String,Any}; default_frontmatter::Di end - - - replace_substring(s::String, sub::SubString, newval::AbstractString) = *( SubString(s, 1, prevind(s, sub.offset + 1, 1)), newval, @@ -210,3 +207,37 @@ function replace_with_cdn(cdnify::Function, s::String, idx::Integer=1) end end end + +""" +Generate a custom index.html that is designed to display a custom set of featured notebooks, without the file UI or Pluto logo. This is to be used by [PlutoSliderServer.jl](https://github.com/JuliaPluto/PlutoSliderServer.jl) to show a fancy index page. +""" +function generate_index_html(; + version::Union{Nothing,VersionNumber,AbstractString}=nothing, + pluto_cdn_root::Union{Nothing,AbstractString}=nothing, + + featured_static::Bool=false, + featured_direct_html_links::Bool=false, + featured_sources_js::AbstractString="undefined", +) + cdnified = cdnified_html("index.html"; version, pluto_cdn_root) + + meta = """ + + """ + + parameters = """ + + """ + + inserted_html(cdnified; meta, parameters) +end diff --git a/src/notebook/path helpers.jl b/src/notebook/path helpers.jl index 8f486a57ea..ddba7f3415 100644 --- a/src/notebook/path helpers.jl +++ b/src/notebook/path helpers.jl @@ -119,6 +119,9 @@ const pluto_file_extensions = [ endswith_pluto_file_extension(s) = any(endswith(s, e) for e in pluto_file_extensions) +""" +Extract the Julia notebook file contents from a Pluto-exported HTML file. +""" function embedded_notebookfile(html_contents::AbstractString)::String if !occursin("", html_contents) throw(ArgumentError("Pass the contents of a Pluto-exported HTML file as argument.")) diff --git a/test/Notebook.jl b/test/Notebook.jl index 7e7d5480ec..e49a268c14 100644 --- a/test/Notebook.jl +++ b/test/Notebook.jl @@ -541,7 +541,7 @@ end end end - @testset "Import & export HTML" begin + @testset "Export HTML" begin nb = basic_notebook() nb.metadata["frontmatter"] = Dict{String,Any}( "title" => "My "\"") + @test occursin("My<Title", export_html) @test occursin("""""", export_html) @test occursin("""""", export_html) @@ -570,6 +571,15 @@ end export_html = Pluto.generate_html(nb; notebookfile_js=filename) @test occursin(filename, export_html) @test_throws ArgumentError Pluto.embedded_notebookfile(export_html) + + + export_html = Pluto.generate_index_html() + @test occursin("", export_html) + @test !occursin(" Date: Tue, 13 Dec 2022 17:06:44 +0100 Subject: [PATCH 4/7] Update Welcome.js --- frontend/components/welcome/Welcome.js | 58 ++++++++++++-------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/frontend/components/welcome/Welcome.js b/frontend/components/welcome/Welcome.js index dafcdc8c6a..d6d2e3b742 100644 --- a/frontend/components/welcome/Welcome.js +++ b/frontend/components/welcome/Welcome.js @@ -131,44 +131,38 @@ export const Welcome = ({ launch_params }) => { ` } - const featured_html = html` + return html` +
+

welcome to

+
+
+
+ <${Recent} + client=${client_ref.current} + connected=${connected} + remote_notebooks=${remote_notebooks} + CustomRecent=${CustomRecent} + on_start_navigation=${on_start_navigation} + /> +
+
+
+
+ <${Open} + client=${client_ref.current} + connected=${connected} + CustomPicker=${CustomPicker} + show_samples=${show_samples} + on_start_navigation=${on_start_navigation} + /> +
+
` - - return launch_params.featured_static - ? featured_html - : html` -
-

welcome to

-
-
-
- <${Recent} - client=${client_ref.current} - connected=${connected} - remote_notebooks=${remote_notebooks} - CustomRecent=${CustomRecent} - on_start_navigation=${on_start_navigation} - /> -
-
-
-
- <${Open} - client=${client_ref.current} - connected=${connected} - CustomPicker=${CustomPicker} - show_samples=${show_samples} - on_start_navigation=${on_start_navigation} - /> -
-
- ${featured_html} - ` } // Option 1: Dynamically load source list from a json: From 269291176df70ea5e557373123759c5b194316fe Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 14 Dec 2022 00:11:10 +0100 Subject: [PATCH 5/7] remove featured_static --- frontend/components/welcome/Welcome.js | 3 --- frontend/index.js | 2 -- src/notebook/Export.jl | 2 -- test/Notebook.jl | 2 +- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/components/welcome/Welcome.js b/frontend/components/welcome/Welcome.js index 650f60efb4..606e1fe57e 100644 --- a/frontend/components/welcome/Welcome.js +++ b/frontend/components/welcome/Welcome.js @@ -26,7 +26,6 @@ import default_featured_sources from "../../featured_sources.js" /** * @typedef LaunchParameters * @type {{ - * featured_static: boolean, * featured_direct_html_links: boolean, * featured_sources: import("./Featured.js").FeaturedSource[]?, * featured_source_url?: string, @@ -55,8 +54,6 @@ export const Welcome = ({ launch_params }) => { const client_ref = useRef(/** @type {import('../../common/PlutoConnection').PlutoConnection} */ ({})) useEffect(() => { - if (launch_params.featured_static) return - const on_update = ({ message, type }) => { if (type === "notebook_list") { // a notebook list updates happened while the welcome screen is open, because a notebook started running for example diff --git a/frontend/index.js b/frontend/index.js index a32bb4f230..1c918a2115 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -10,8 +10,6 @@ const url_params = new URLSearchParams(window.location.search) * @type {import("./components/welcome/Welcome.js").LaunchParameters} */ const launch_params = { - //@ts-ignore - featured_static: !!(url_params.get("featured_static") ?? window.pluto_featured_static), //@ts-ignore featured_direct_html_links: !!(url_params.get("featured_direct_html_links") ?? window.pluto_featured_direct_html_links), diff --git a/src/notebook/Export.jl b/src/notebook/Export.jl index 1948d61aa6..0b7a35cba8 100644 --- a/src/notebook/Export.jl +++ b/src/notebook/Export.jl @@ -215,7 +215,6 @@ function generate_index_html(; version::Union{Nothing,VersionNumber,AbstractString}=nothing, pluto_cdn_root::Union{Nothing,AbstractString}=nothing, - featured_static::Bool=false, featured_direct_html_links::Bool=false, featured_sources_js::AbstractString="undefined", ) @@ -233,7 +232,6 @@ function generate_index_html(; parameters = """ diff --git a/test/Notebook.jl b/test/Notebook.jl index e49a268c14..c295fcc6dd 100644 --- a/test/Notebook.jl +++ b/test/Notebook.jl @@ -577,7 +577,7 @@ end @test occursin("", export_html) @test !occursin(" Date: Wed, 14 Dec 2022 00:11:17 +0100 Subject: [PATCH 6/7] comments and cleanup --- frontend/components/welcome/Featured.js | 33 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/frontend/components/welcome/Featured.js b/frontend/components/welcome/Featured.js index 07164f38bb..03b9e97484 100644 --- a/frontend/components/welcome/Featured.js +++ b/frontend/components/welcome/Featured.js @@ -38,7 +38,10 @@ import { FeaturedCard } from "./FeaturedCard.js" * }} */ -/** @type {SourceManifest[]} */ +/** + * This data is used as placeholder while the real data is loading from the network. + * @type {SourceManifest[]} + */ const placeholder_data = [ { title: "Featured Notebooks", @@ -53,6 +56,7 @@ const placeholder_data = [ }, ] +/** This HTML is shown instead of the featured notebooks if the user is offline. */ const offline_html = html` ` +/** + * If no collections are defined, then this special collection will just show all notebooks under the "Notebooks" category. + * No collections are defined if no `pluto_export_configuration.json` file was provided to PlutoSliderServer.jl. + * @type {SourceManifestCollectionEntry[]} + */ +const fallback_collections = [ + { + title: "Notebooks", + tags: "everything", + }, +] + /** * @typedef FeaturedSource * @type {{url: String, integrity?: String}} @@ -88,10 +104,12 @@ const offline_html = html` * }} props */ export const Featured = ({ sources, direct_html_links }) => { - const [source_data, set_source_data] = useState(/** @type{Record} */ ({})) + // source_data will be a mapping from [source URL] => [data from that source] + const [source_data, set_source_data] = useState(/** @type {Record} */ ({})) useEffect(() => { if (sources != null) { + // Start downloading the sources const promises = sources.map(async ({ url, integrity }) => { const data = await (await fetch(new Request(url, { integrity: integrity ?? undefined }))).json() @@ -134,12 +152,8 @@ export const Featured = ({ sources, direct_html_links }) => { ? offline_html : html` ${(no_data ? placeholder_data : Object.values(source_data)).map((/** @type {SourceManifest} */ data) => { - let collections = data?.collections ?? [ - { - title: "Notebooks", - tags: "everything", - }, - ] + let collections = data?.collections ?? fallback_collections + return html`