diff --git a/.gitignore b/.gitignore index b067edd..cdddca6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /Manifest.toml +.DS_Store diff --git a/Project.toml b/Project.toml index 8d36288..8bfbfb9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "PlutoPages" uuid = "d5dc3dd1-4774-47c7-8860-0a1ad9e34b8c" -authors = ["Luca Ferranti and Fons van Der Plas"] +authors = ["Luca Ferranti", "Fons van Der Plas"] version = "0.1.0" [deps] @@ -9,6 +9,7 @@ CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" Gumbo = "708ec375-b3d6-5a57-a7ce-8257bf98657a" HypertextLiteral = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" MarkdownLiteral = "736d6165-7244-6769-4267-6b50796e6954" @@ -16,7 +17,9 @@ Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" PlutoHooks = "0ff47ea0-7a50-410d-8455-4348d5de0774" PlutoLinks = "0ff47ea0-7a50-410d-8455-4348d5de0420" PlutoSliderServer = "2fc8631c-6f24-4c5b-bca7-cbb509c42db4" +PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c" +RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" ThreadsX = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" @@ -28,14 +31,17 @@ CommonMark = "0.8" Gumbo = "0.8" HypertextLiteral = "0.9" InteractiveUtils = "1" +LiveServer = "1.2" Logging = "1" Markdown = "1" MarkdownLiteral = "0.1" -Pluto = "0.19.36" +Pluto = "0.19.38" PlutoHooks = "0.0.5" PlutoLinks = "0.1.6" PlutoSliderServer = "0.3.28" +PlutoUI = "0.7.55" ProgressLogging = "0.1" +RelocatableFolders = "1" SHA = "0.7" ThreadsX = "0.1" Unicode = "1" diff --git a/README.md b/README.md index 7c8d650..1c21bee 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,33 @@ This repository will contain "PlutoPages", the site generation system that power Currently this code is in https://github.com/mitmath/computational-thinking/blob/Fall23/PlutoPages.jl Contact https://github.com/LucaFerranti for more info! + + +## During development +Use `PlutoPages.develop` to start developing your website. It will launch two browser tabs: one with the PlutoPages development dashboard, and one with a preview of your website. + +When you make edits to the website source files, they should get detected automatically, and the site is regenerated. If changes are not detected, go to the PlutoPages dashboard and click "Read input files again". + +```julia +import PlutoPages + +# replace this with the path of your own website +my_site_source = PlutoPages.create_test_basic_site() + +PlutoPages.develop(my_site_source) +``` + + +## Generating the site +Use `PlutoPages.generate` if you want to generate your website once, without a development server. + + + +```julia +import PlutoPages + +# replace this with the path of your own website +my_site_source = PlutoPages.create_test_basic_site() + +output_dir = PlutoPages.generate(my_site_source) +``` diff --git a/src/PlutoPages.jl b/src/PlutoPages.jl index 98ad19d..4bfee0f 100644 --- a/src/PlutoPages.jl +++ b/src/PlutoPages.jl @@ -1,5 +1,146 @@ module PlutoPages -include("plutopages.jl") +import Pluto +using RelocatableFolders +import LiveServer +include("./pluto control.jl") +include("./open in browser.jl") + + +# """ +# Generate +# """ +# function generate() + + + +# end + +const PlutoPages_notebook_path = @path joinpath(dirname(@__DIR__), "src", "notebook.jl") + + +function run_plutopages_notebook(; + input_dir::String, + output_dir::String, + cache_dir::String, + kwargs... +) + mkpath(output_dir) + mkpath(cache_dir) + run_with_replacements( + PlutoPages_notebook_path, + Dict( + :input_dir => input_dir, + :output_dir => output_dir, + :cache_dir => cache_dir, + ); + kwargs... + ) +end + + + +function create_subdirs(root_dir::String) + @assert(isdir(root_dir)) + @assert(isdir(joinpath(root_dir, "src"))) + + (; + input_dir = joinpath(root_dir, "src"), + output_dir = joinpath(root_dir, "_site"), + cache_dir = joinpath(root_dir, "_cache"), + ) +end + +function develop(root_dir::String) + develop(;create_subdirs(root_dir)...) +end + + + +const isolated_cell_ids = ( + "cf27b3d3-1689-4b3a-a8fe-3ad639eb2f82", + "7f7f1981-978d-4861-b840-71ab611faf74", + "7d9cb939-da6b-4961-9584-a905ad453b5d", + "4e88cf07-8d85-4327-b310-6c71ba951bba", + "079a6399-50eb-4dee-a36d-b3dcb81c8456", + "b0006e61-b037-41ed-a3e4-9962d15584c4", + "06edb2d7-325f-4f80-8c55-dc01c7783054", + "e0a25f24-a7de-4eac-9f88-cb7632de09eb", +) +const isolated_cell_query = join("&isolated_cell_id=$(i)" for i in isolated_cell_ids) + + +function develop(; + input_dir::String, + output_dir::String, + cache_dir::String, +) + app = run_plutopages_notebook(; input_dir, output_dir, cache_dir, run_server=true) + + notebook = fetch(app.notebook_task) + + ccall(:jl_exit_on_sigint, Cvoid, (Cint,), 0) + @info "PlutoPages: Press Ctrl+C multiple times to stop the server." + file_server_port = rand(8100:8900) + + file_server_task = Threads.@spawn LiveServer.serve(port=file_server_port, dir=output_dir) + + sleep(2) + + dev_server_url = "http://localhost:$(file_server_port)/" + pluto_server_url = "http://localhost:$(app.pluto_server_port)/edit?secret=$(app.session.secret)&id=$(notebook.notebook_id)$(isolated_cell_query)" + + + @info """ + + ✅✅✅ + + Ready! To see the website, visit: + ➡️ $(dev_server_url) + + To inspect the generation process, go to: + ➡️ $(pluto_server_url) + + ✅✅✅ + + """ + + open_in_default_browser(dev_server_url) + open_in_default_browser(pluto_server_url) + + wait(file_server_task) + wait(app.pluto_server_instance) + app +end + + + + +function generate(; + input_dir::String, + output_dir::String, + cache_dir::String, +) + app = run_plutopages_notebook(; input_dir, output_dir, cache_dir, run_server=false) + fetch(app.notebook_task) + @info "PlutoPages: cleaning up..." + shutdown(app) + + return output_dir +end + +generate(root_dir::String) = generate(;create_subdirs(root_dir)...) + + +function create_test_basic_site() + original = joinpath(dirname(@__DIR__), "test", "basic_site") + new_dir = mktempdir() + cp(original, new_dir; force=true) + new_dir end + + + + +end \ No newline at end of file diff --git a/src/notebook.jl b/src/notebook.jl index a317c23..f8469a9 100644 --- a/src/notebook.jl +++ b/src/notebook.jl @@ -390,7 +390,7 @@ md""" """ # ╔═╡ c52c9786-a25f-11ec-1fdc-9b13922d7ccb -const dir = joinpath(@__DIR__, "src") +const input_dir = joinpath(@__DIR__, "src") # ╔═╡ cf27b3d3-1689-4b3a-a8fe-3ad639eb2f82 md""" @@ -409,7 +409,7 @@ const this_file = split(@__FILE__, "#==#")[1] # ╔═╡ d38dc2aa-d5ba-4cf7-9f9e-c4e4611a57ac function ignore(abs_path; allow_special_dirs::Bool=false) - p = relpath(abs_path, dir) + p = relpath(abs_path, input_dir) # (_cache, _site, _andmore) any(x -> ignored_dirname(x; allow_special_dirs), splitpath(p)) || @@ -424,8 +424,8 @@ dir_changed_time = let @info "Starting watch task" - @use_task([dir]) do - BetterFileWatching.watch_folder(dir) do e + @use_task([input_dir]) do + BetterFileWatching.watch_folder(input_dir) do e @debug "File event" e try is_caused_by_me = all(x -> ignore(x; allow_special_dirs=true), e.paths) @@ -444,12 +444,12 @@ dir_changed_time = let end # ╔═╡ 7d9cb939-da6b-4961-9584-a905ad453b5d -allfiles = filter(PlutoSliderServer.list_files_recursive(dir)) do p +allfiles = filter(PlutoSliderServer.list_files_recursive(input_dir)) do p # reference to retrigger when files change dir_changed_time manual_update_trigger - !ignore(joinpath(dir, p)) + !ignore(joinpath(input_dir, p)) end # ╔═╡ d314ab46-b866-44c6-bfca-9a413bc06514 @@ -737,7 +737,7 @@ template_results = let # let's go! running all the template handlers progressmap_async(allfiles; ntasks=NUM_PARALLEL_WORKERS) do f - absolute_path = joinpath(dir, f) + absolute_path = joinpath(input_dir, f) input = TemplateInput(; contents=read(absolute_path), @@ -803,7 +803,7 @@ function process_layouts(page::Page)::Page layoutname = output.frontmatter["layout"] @assert layoutname isa String - layout_file = joinpath(dir, "_includes", layoutname) + layout_file = joinpath(input_dir, "_includes", layoutname) @assert isfile(layout_file) "$layout_file is not a valid layout path" @@ -816,7 +816,7 @@ function process_layouts(page::Page)::Page input = TemplateInput(; contents=read(layout_file), absolute_path=layout_file, - relative_path=relpath(layout_file, dir), + relative_path=relpath(layout_file, input_dir), frontmatter=merge(output.frontmatter, FrontMatter( "content" => content, diff --git a/src/open in browser.jl b/src/open in browser.jl new file mode 100644 index 0000000..e010a8c --- /dev/null +++ b/src/open in browser.jl @@ -0,0 +1,25 @@ +# copy paste from pluto source code +function detectwsl() + Sys.islinux() && + isfile("/proc/sys/kernel/osrelease") && + occursin(r"Microsoft|WSL"i, read("/proc/sys/kernel/osrelease", String)) +end + +function open_in_default_browser(url::AbstractString)::Bool + try + if Sys.isapple() + Base.run(`open $url`) + true + elseif Sys.iswindows() || detectwsl() + Base.run(`powershell.exe Start "'$url'"`) + true + elseif Sys.islinux() + Base.run(`xdg-open $url`) + true + else + false + end + catch ex + false + end +end \ No newline at end of file diff --git a/src/pluto control.jl b/src/pluto control.jl new file mode 100644 index 0000000..c8ab90e --- /dev/null +++ b/src/pluto control.jl @@ -0,0 +1,104 @@ +import Pluto: Pluto, PlutoDependencyExplorer + +function run_with_replacements(notebook_path::AbstractString, inputs::Dict{Symbol,<:Any}; + run_server::Bool=false, +) + port_channel = Channel{UInt16}(1) + + function on_event(e::Pluto.ServerStartEvent) + put!(port_channel, e.port) + end + function on_event(e) end + + options=Pluto.Configuration.from_flat_kwargs(; + workspace_use_distributed=true, + disable_writing_notebook_files=true, + launch_browser=false, + show_file_system=false, + dismiss_update_notification=true, + on_event, + port_hint=6872, + ) + session = Pluto.ServerSession(;options) + + @info "PlutoPages: Starting Pluto server..." + notebook_task = Threads.@spawn try + notebook = Pluto.SessionActions.open( + session, notebook_path; + run_async=false, + execution_allowed=false, # start in "Safe mode", to allow us to inject some code before running the notebook :) + ) + + notebook.topology = Pluto.static_resolve_topology(Pluto.updated_topology(notebook.topology, notebook, notebook.cells)) + + for (name, value) in pairs(inputs) + # Find the cell that currently defines a variable with this name. + cs = PlutoDependencyExplorer.where_assigned(notebook.topology, Set([name])) + if length(cs) != 1 + error("The variable $(name) is not defined in this notebook: it cannot be used as input to the app.") + else + c = only(cs)::Pluto.Cell + + c.code = "const $(name) = $(repr(value))" + c.code_folded = true + end + end + + log_progress = Ref(true) + let + last_progress = (0,0) + Threads.@spawn while log_progress[] + progress = ( + notebook.process_status == Pluto.ProcessStatus.ready ? + length(notebook.cells) - count(c -> c.running || c.queued, notebook.cells) : + 0, + length(notebook.cells) + ) + if progress != last_progress + @info "Notebook: $(progress[1])/$(progress[2]) done..." + last_progress = progress + end + sleep(.5) + end + end + + # disable "Safe preview" mode + notebook.process_status = Pluto.ProcessStatus.starting + # run all cells + Pluto.update_save_run!(session, notebook, notebook.cells; run_async=false) + @info "Pluto app: notebook finished!" + log_progress[] = false + + notebook + catch e + @error "Error while running notebook" exception=(e,catch_backtrace()) + end + + pluto_server_instance = if run_server + Pluto.run!(session) + end + + pluto_server_port = run_server ? take!(port_channel) : nothing + @info "Pluto app: waiting for notebook to finish..." + + return (; + session, + notebook_task, + pluto_server_instance, + pluto_server_port, + ) +end + + + +function shutdown(app) + # uhhh this will wait for it to finish but whatever + notebook = fetch(app.notebook_task) + + Pluto.SessionActions.shutdown(app.session, notebook) + + if app.pluto_server_instance !== nothing + Base.close(app.pluto_server_instance) + end +end + diff --git a/test/basic_site/src/_includes/layout.jlhtml b/test/basic_site/src/_includes/layout.jlhtml new file mode 100644 index 0000000..e6a2851 --- /dev/null +++ b/test/basic_site/src/_includes/layout.jlhtml @@ -0,0 +1,90 @@ +$(begin + import Pluto + "The contents of `
` from a Pluto HTML export." + const pluto_head = let + default = Pluto.generate_html(; + pluto_cdn_root=Pluto.PLUTO_VERSION < v"0.19" ? "https://cdn.jsdelivr.net/gh/fonsp/Pluto.jl@9ca70c36/frontend/" : nothing) + m = match(r"