From eb3e97fd955404d2de148e33d184b63b57df85da Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Thu, 19 Oct 2023 14:51:46 +1100 Subject: [PATCH] Update scripts to WIP site for REPL workaround for copy static files add REPL to WIP home page WIP site download REPL nightly scripts build WIP Netlify and dev --- www/build.sh | 10 +- www/wip_new_website/build-dev-local.sh | 15 ++ www/wip_new_website/build.roc | 54 ------ www/wip_new_website/content/index.md | 27 +-- www/wip_new_website/static/repl.css | 173 ++++++++++++++++++ www/wip_new_website/static/repl.js | 238 +++++++++++++++++++++++++ 6 files changed, 445 insertions(+), 72 deletions(-) create mode 100644 www/wip_new_website/build-dev-local.sh delete mode 100755 www/wip_new_website/build.roc create mode 100644 www/wip_new_website/static/repl.css create mode 100644 www/wip_new_website/static/repl.js diff --git a/www/build.sh b/www/build.sh index ff1b92f33d..afcf882f89 100755 --- a/www/build.sh +++ b/www/build.sh @@ -17,6 +17,7 @@ cd $SCRIPT_RELATIVE_DIR rm -rf build/ cp -r public/ build/ +mkdir build/wip # for WIP site # download fonts just-in-time so we don't have to bloat the repo with them. DESIGN_ASSETS_COMMIT="4d949642ebc56ca455cf270b288382788bce5873" @@ -36,13 +37,14 @@ curl -fLJO https://github.com/roc-lang/roc/archive/www.tar.gz REPL_TARFILE="roc_repl_wasm.tar.gz" curl -fLJO https://github.com/roc-lang/roc/releases/download/nightly/$REPL_TARFILE tar -xzf $REPL_TARFILE -C repl +tar -xzf $REPL_TARFILE -C wip # note we also need this for WIP repl rm $REPL_TARFILE ls -lh repl +ls -lh wip popd pushd .. -echo 'Generating builtin docs...' cargo --version # We set ROC_DOCS_ROOT_DIR=builtins so that links will be generated relative to @@ -92,10 +94,8 @@ $roc run www/generate_tutorial/src/tutorial.roc -- www/generate_tutorial/src/inp mv www/build/tutorial/tutorial.html www/build/tutorial/index.html # for new wip site -mkdir www/build/wip -$roc run www/wip_new_website/main.roc -- www/wip_new_website/content/ www/build/wip -cp -r www/wip_new_website/static/site.css www/build/wip -cp -r www/build/fonts www/build/wip/fonts +$roc run www/wip_new_website/main.roc -- www/wip_new_website/content/ www/build/wip/ +cp -r www/wip_new_website/static/* www/build/wip/ # cleanup rm -rf roc_nightly roc_releases.json diff --git a/www/wip_new_website/build-dev-local.sh b/www/wip_new_website/build-dev-local.sh new file mode 100644 index 0000000000..887f2cea2a --- /dev/null +++ b/www/wip_new_website/build-dev-local.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Use this script to for testing the WIP site locally without downloading assets every time. + +# NOTE run `bash www/build.sh` to cache local copy of fonts, and repl assets etc + +rm -rf dist/ +mkdir dist +mkdir dist/wip +cp -r ../build/wip/* dist/wip/ +roc run main.roc -- content/ dist/wip/ +cp -r static/* dist/wip/ +cp -r ../build/fonts/ dist/fonts/ + +simple-http-server -p 8080 --nocache --index -- dist/ diff --git a/www/wip_new_website/build.roc b/www/wip_new_website/build.roc deleted file mode 100755 index f78a1268b9..0000000000 --- a/www/wip_new_website/build.roc +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env roc -app "website-builder" - packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } - imports [ - pf.Task.{ Task }, - pf.Cmd, - ] - provides [main] to pf - -main = - # TODO take dist folder name and main.roc path as args once https://github.com/roc-lang/basic-cli/issues/82 is fixed - # TODO add function to remove boilerplate - # Remove dist folder - {} <- - Cmd.new "rm" - |> Cmd.args ["-rf", "dist/"] - |> Cmd.status - |> Task.onErr \_ -> crash "Failed to remove dist folder" - |> Task.await - - # Build site - {} <- - Cmd.new "roc" - |> Cmd.args ["run", "main.roc", "--", "content/", "dist/wip/"] - |> Cmd.status - |> Task.onErr \_ -> crash "Failed to build site" - |> Task.await - - # Copy static files - {} <- - Cmd.new "cp" - |> Cmd.args ["-r", "static/site.css", "dist/wip/"] - |> Cmd.status - |> Task.onErr \_ -> crash "Failed to copy static files" - |> Task.await - - # Copy font files - assume that www/build.sh has been run previously and the - # fonts are available locally in ../build/fonts - {} <- - Cmd.new "cp" - |> Cmd.args ["-r", "../build/fonts/", "dist/fonts/"] - |> Cmd.status - |> Task.onErr \_ -> crash "Failed to copy static files" - |> Task.await - - # Start file server - {} <- - Cmd.new "simple-http-server" - |> Cmd.args ["-p", "8080", "--nocache", "--index", "--", "dist/"] - |> Cmd.status - |> Task.onErr \_ -> crash "Failed to run file server; consider intalling with `cargo install simple-http-server`" - |> Task.await - - Task.ok {} diff --git a/www/wip_new_website/content/index.md b/www/wip_new_website/content/index.md index daf3d7d9dc..8c7e64ba99 100644 --- a/www/wip_new_website/content/index.md +++ b/www/wip_new_website/content/index.md @@ -18,7 +18,6 @@ A work-in-progress programming language that aims to be fast, friendly, and func

Friendly

-

Friendly

Roc aims to be a user-friendly language with a friendly community of users. This involves the set of tools Roc includes, and also the spirit of the community of Roc programmers. What does friendly mean here?

@@ -28,19 +27,21 @@ A work-in-progress programming language that aims to be fast, friendly, and func ## Try Roc - + +
+ +
+
Loading REPL WebAssembly module…please wait!
+
+
+ +
+
+ +
-The code below shows a Roc application which prints `Hello World!` to the terminal. It does this using the [roc-lang/basic-cli](https://github.com/roc-lang/basic-cli) platform. - -```roc -app "hello-world" - packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" } - imports [pf.Stdout] - provides [main] to pf - -main = - Stdout.line "Hello, World!" -``` +## Examples We have developed a number of smaller code [examples](https://github.com/roc-lang/examples) which demonstrate how to use Roc. These cover a range of topics from basic syntax to more advanced features such as random number generation and using the popular `Task` feature. diff --git a/www/wip_new_website/static/repl.css b/www/wip_new_website/static/repl.css new file mode 100644 index 0000000000..d6587d1a5c --- /dev/null +++ b/www/wip_new_website/static/repl.css @@ -0,0 +1,173 @@ +#repl { + display: flex; + flex-direction: column; +} + +/* h1 { + font-family: "Merriweather"; + font-size: 48px; + line-height: 48px; + padding: 48px 0; + margin: 0; + color: var(--header-link-color); +} */ + +#source-input-wrapper { + position: relative; + width: 100%; + box-sizing: border-box; +} + +#source-input-wrapper::before { + content: "» "; + position: absolute; + left: 15px; + top: 18px; + line-height: 16px; + height: 16px; + z-index: 2; + font-family: var(--font-mono); + color: var(--cyan); + /* Let clicks pass through to the textarea */ + pointer-events: none; +} + +#source-input { + width: 100%; + font-family: var(--font-mono); + color: var(--code-color); + background-color: var(--code-bg); + display: inline-block; + height: 76px; + padding: 16px; + padding-left: 36px; + border: 1px solid transparent; + margin: 0; + margin-bottom: 2em; + box-sizing: border-box; +} + +#source-input:focus { + border: 1px solid var(--cyan); + box-sizing: border-box; + outline: none; +} + +/* li { + margin: 8px; +} */ + +.history { + padding: 1em; + padding-bottom: 0; +} + +#help-text, +#history-text { + white-space: pre-wrap; + +} + +#history-text { + margin-top: 16px; +} + +#loading-message { + text-align: center; + /* approximately match height after loading and printing help message */ + height: 96px; +} + +.history-item { + margin-bottom: 24px; +} + +.history-item .input { + margin: 0; + margin-bottom: 8px; +} + +.history-item .output { + margin: 0; +} + +.panic { + color: red; +} + +.input-line-prefix { + color: var(--cyan); +} + +.color-red { + color: red; +} + +.color-green { + color: var(--green); +} + +.color-yellow { + color: var(--orange); +} + +.color-blue { + color: var(--cyan); +} + +.color-magenta { + color: var(--magenta); +} + +.color-cyan { + color: var(--cyan); +} + +.color-white { + /* Really this isn't white so much as "default text color." For the repl, this should be black + in a light color scheme, and only white in dark mode. The name could be better! */ + color: black; +} + +@media (prefers-color-scheme: dark) { + .color-white { + color: white; + } +} + +.bold { + font-weight: bold; +} + +.underline { + text-decoration: underline; +} + + +/* Mobile-friendly screen width */ +@media only screen and (max-width: 767px) { + h1 { + font-size: 24px !important; + margin: 0; + padding: 16px 0; + text-align: center; + } + + #repl { + margin: 0; + padding: 0; + min-height: calc(100vh - var(--top-bar-height)); + } + + code.history { + flex-grow: 1; + } + + #source-input { + margin: 0 + } + + #loading-message { + margin: 0; + } +} diff --git a/www/wip_new_website/static/repl.js b/www/wip_new_website/static/repl.js new file mode 100644 index 0000000000..1c62a87050 --- /dev/null +++ b/www/wip_new_website/static/repl.js @@ -0,0 +1,238 @@ +// The only way we can provide values to wasm_bindgen's generated code is to set globals +window.js_create_app = js_create_app; +window.js_run_app = js_run_app; +window.js_get_result_and_memory = js_get_result_and_memory; + +// The only place we use console.error is in wasm_bindgen, where it gets a single string argument. +console.error = function displayErrorInHistoryPanel(string) { + const html = `
${string}
`; + updateHistoryEntry(repl.inputHistoryIndex, false, html); +}; + +import * as roc_repl_wasm from "/wip/roc_repl_wasm.js"; + +// ---------------------------------------------------------------------------- +// REPL state +// ---------------------------------------------------------------------------- + +const repl = { + elemHistory: document.getElementById("history-text"), + elemSourceInput: document.getElementById("source-input"), + + inputQueue: [], + inputHistory: [], + inputHistoryIndex: 0, + inputStash: "", // stash the user input while we're toggling through history with up/down arrows + + textDecoder: new TextDecoder(), + textEncoder: new TextEncoder(), + + compiler: null, + app: null, + + // Temporary storage for the address of the result of running the user's code. + // Used while control flow returns to Rust to allocate space to copy the app's memory buffer. + result_addr: 0, +}; + +// Initialise +repl.elemSourceInput.addEventListener("input", onInput); +repl.elemSourceInput.addEventListener("keydown", onInputKeydown); +repl.elemSourceInput.addEventListener("keyup", onInputKeyup); +roc_repl_wasm.default("/wip/roc_repl_wasm_bg.wasm").then(async (instance) => { + repl.elemHistory.querySelector("#loading-message").remove(); + repl.elemSourceInput.disabled = false; + repl.elemSourceInput.placeholder = "Type some Roc code and press Enter."; + repl.elemSourceInput.focus(); + repl.compiler = instance; + + // Get help text from the compiler, and display it at top of the history panel + try { + const helpText = await roc_repl_wasm.entrypoint_from_js(":help"); + const helpElem = document.getElementById("help-text"); + helpElem.innerHTML = helpText.trim(); + } catch (e) { + // Print error for Roc devs. Don't use console.error, we overrode that above to display on the page! + console.warn(e); + } +}); + +// ---------------------------------------------------------------------------- +// Handle inputs +// ---------------------------------------------------------------------------- + +function onInput(event) { + // Have the textarea grow with the input + event.target.style.height = event.target.scrollHeight + 2 + "px"; // +2 for the border +} + +function onInputKeydown(event) { + const ENTER = 13; + + const { keyCode } = event; + + if (keyCode === ENTER) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey) { + // Don't advance the caret to the next line + event.preventDefault(); + + const inputText = repl.elemSourceInput.value.trim(); + + repl.elemSourceInput.value = ""; + repl.elemSourceInput.style.height = ""; + + repl.inputQueue.push(inputText); + if (repl.inputQueue.length === 1) { + processInputQueue(); + } + } + } +} + +function onInputKeyup(event) { + const UP = 38; + const DOWN = 40; + + const { keyCode } = event; + + const el = repl.elemSourceInput; + + switch (keyCode) { + case UP: + if (repl.inputHistory.length === 0) { + return; + } + if (repl.inputHistoryIndex == repl.inputHistory.length - 1) { + repl.inputStash = el.value; + } + setInput(repl.inputHistory[repl.inputHistoryIndex]); + + if (repl.inputHistoryIndex > 0) { + repl.inputHistoryIndex--; + } + break; + + case DOWN: + if (repl.inputHistory.length === 0) { + return; + } + if (repl.inputHistoryIndex === repl.inputHistory.length - 1) { + setInput(repl.inputStash); + } else { + repl.inputHistoryIndex++; + setInput(repl.inputHistory[repl.inputHistoryIndex]); + } + break; + + default: + break; + } +} + +function setInput(value) { + const el = repl.elemSourceInput; + el.value = value; + el.selectionStart = value.length; + el.selectionEnd = value.length; +} + +// Use a queue just in case we somehow get inputs very fast +// We want the REPL to only process one at a time, since we're using some global state. +// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste? +async function processInputQueue() { + while (repl.inputQueue.length) { + const inputText = repl.inputQueue[0]; + repl.inputHistoryIndex = createHistoryEntry(inputText); + repl.inputStash = ""; + + let outputText = ""; + let ok = true; + if (inputText) { + try { + outputText = await roc_repl_wasm.entrypoint_from_js(inputText); + } catch (e) { + outputText = `${e}`; + ok = false; + } + } + + updateHistoryEntry(repl.inputHistoryIndex, ok, outputText); + repl.inputQueue.shift(); + } +} + +// ---------------------------------------------------------------------------- +// Callbacks to JS from Rust +// ---------------------------------------------------------------------------- + +// Load Wasm code into the browser's virtual machine, so we can run it later. +// This operation is async, so we call it before entering any code shared +// with the command-line REPL, which is sync. +async function js_create_app(wasm_module_bytes) { + const { instance } = await WebAssembly.instantiate(wasm_module_bytes); + // Keep the instance alive so we can run it later from shared REPL code + repl.app = instance; +} + +// Call the `main` function of the user app, via the `wrapper` function. +function js_run_app() { + const { wrapper, memory } = repl.app.exports; + + // Run the user code, and remember the result address + // We'll pass it to Rust in the next callback + repl.result_addr = wrapper(); + + // Tell Rust how much space to reserve for its copy of the app's memory buffer. + // We couldn't know that size until we actually ran the app. + return memory.buffer.byteLength; +} + +// After Rust has allocated space for the app's memory buffer, +// we copy it, and return the result address too +function js_get_result_and_memory(buffer_alloc_addr) { + const appMemory = new Uint8Array(repl.app.exports.memory.buffer); + const compilerMemory = new Uint8Array(repl.compiler.memory.buffer); + compilerMemory.set(appMemory, buffer_alloc_addr); + return repl.result_addr; +} + +// ---------------------------------------------------------------------------- +// Rendering +// ---------------------------------------------------------------------------- + +function createHistoryEntry(inputText) { + const historyIndex = repl.inputHistory.length; + repl.inputHistory.push(inputText); + + const firstLinePrefix = '» '; + const otherLinePrefix = '
'; + const inputLines = inputText.split("\n"); + if (inputLines[inputLines.length - 1] === "") { + inputLines.pop(); + } + const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix); + + const inputElem = document.createElement("div"); + inputElem.innerHTML = inputWithPrefixes; + inputElem.classList.add("input"); + + const historyItem = document.createElement("div"); + historyItem.appendChild(inputElem); + historyItem.classList.add("history-item"); + + repl.elemHistory.appendChild(historyItem); + + return historyIndex; +} + +function updateHistoryEntry(index, ok, outputText) { + const outputElem = document.createElement("div"); + outputElem.innerHTML = outputText; + outputElem.classList.add("output", ok ? "output-ok" : "output-error"); + + const historyItem = repl.elemHistory.children[index]; + historyItem.appendChild(outputElem); + + // Scroll the page to the bottom so you can see the most recent output. + // window.scrollTo(0, document.body.scrollHeight); +}