diff options
author | Louis Pilfold <louis@lpil.uk> | 2023-12-01 22:17:09 +0000 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2023-12-06 16:20:50 +0000 |
commit | e32d68d064c4663818dc5445541f2cd67d2e04dc (patch) | |
tree | 5270ebce540312e8d7ea76a78e656726245920e0 /static | |
download | tour-e32d68d064c4663818dc5445541f2cd67d2e04dc.tar.gz tour-e32d68d064c4663818dc5445541f2cd67d2e04dc.zip |
Rename
Diffstat (limited to 'static')
-rw-r--r-- | static/compiler.js | 90 | ||||
-rw-r--r-- | static/index.js | 124 | ||||
-rw-r--r-- | static/style.css | 174 |
3 files changed, 388 insertions, 0 deletions
diff --git a/static/compiler.js b/static/compiler.js new file mode 100644 index 0000000..021992c --- /dev/null +++ b/static/compiler.js @@ -0,0 +1,90 @@ +let compiler; + +export default async function initGleamCompiler() { + const wasm = await import("/compiler/gleam_wasm.js"); + await wasm.default(); + wasm.initialise_panic_hook(); + if (!compiler) { + compiler = new Compiler(wasm); + } + return compiler; +} + +class Compiler { + #wasm; + #nextId = 0; + #projects = new Map(); + + constructor(wasm) { + this.#wasm = wasm; + } + + get wasm() { + return this.#wasm; + } + + newProject() { + const id = this.#nextId++; + const project = new Project(id); + this.#projects.set(id, new WeakRef(project)); + return project; + } + + garbageCollectProjects() { + const gone = []; + for (const [id, project] of this.#projects) { + if (!project.deref()) gone.push(id); + } + for (const id of gone) { + this.#projects.delete(id); + this.#wasm.delete_project(id); + } + } +} + +class Project { + #id; + + constructor(id) { + this.#id = id; + } + + get projectId() { + return this.#id; + } + + writeModule(moduleName, code) { + compiler.wasm.write_module(this.#id, moduleName, code); + } + + compilePackage(target) { + compiler.garbageCollectProjects(); + compiler.wasm.reset_warnings(this.#id); + compiler.wasm.compile_package(this.#id, target); + } + + readCompiledJavaScript(moduleName) { + return compiler.wasm.read_compiled_javascript(this.#id, moduleName); + } + + readCompiledErlang(moduleName) { + return compiler.wasm.read_compiled_erlang(this.#id, moduleName); + } + + resetFilesystem() { + compiler.wasm.reset_filesystem(this.#id); + } + + delete() { + compiler.wasm.delete_project(this.#id); + } + + takeWarnings() { + const warnings = []; + while (true) { + const warning = compiler.wasm.pop_warning(this.#id); + if (!warning) return warnings; + warnings.push(warning.trimStart()); + } + } +} diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..d460c61 --- /dev/null +++ b/static/index.js @@ -0,0 +1,124 @@ +import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; +import initGleamCompiler from "./compiler.js"; +import stdlib from "./stdlib.js"; + +const output = document.querySelector("#output"); +const initialCode = document.querySelector("#code").textContent; + +const prismGrammar = { + comment: { + pattern: /\/\/.*/, + greedy: true, + }, + function: /([a-z_][a-z0-9_]+)(?=\()/, + keyword: + /\b(use|case|if|external|fn|import|let|assert|try|pub|type|opaque|const|todo|as)\b/, + symbol: { + pattern: /([A-Z][A-Za-z0-9_]+)/, + greedy: true, + }, + operator: { + pattern: + /(<<|>>|<-|->|\|>|<>|\.\.|<=\.?|>=\.?|==\.?|!=\.?|<\.?|>\.?|&&|\|\||\+\.?|-\.?|\/\.?|\*\.?|%\.?|=)/, + greedy: true, + }, + string: { + pattern: /"(?:\\(?:\r\n|[\s\S])|(?!")[^\\\r\n])*"/, + greedy: true, + }, + module: { + pattern: /([a-z][a-z0-9_]*)\./, + inside: { + punctuation: /\./, + }, + alias: "keyword", + }, + punctuation: /[.\\:,{}()]/, + number: + /\b(?:0b[0-1]+|0o[0-7]+|[[:digit:]][[:digit:]_]*(\\.[[:digit:]]*)?|0x[[:xdigit:]]+)\b/, +}; + +// Monkey patch console.log to keep a copy of the output +let logged = ""; +const log = console.log; +console.log = (...args) => { + log(...args); + logged += args.map((e) => `${e}`).join(" ") + "\n"; +}; + +function resetLogCapture() { + logged = ""; +} + +async function compileEval(project, code) { + try { + project.writeModule("main", code); + project.compilePackage("javascript"); + const js = project.readCompiledJavaScript("main"); + const main = await loadProgram(js); + resetLogCapture(); + if (main) main(); + replaceOutput(logged, "log"); + } catch (error) { + replaceOutput(error.toString(), "error"); + } + for (const warning of project.takeWarnings()) { + appendOutput(warning, "warning"); + } +} + +async function loadProgram(js) { + const url = new URL(import.meta.url); + url.pathname = ""; + url.hash = ""; + url.search = ""; + const href = url.toString(); + const js1 = js.replaceAll( + /from\s+"\.\/(.+)"/g, + `from "${href}precompiled/$1"` + ); + const js2 = btoa(unescape(encodeURIComponent(js1))); + const module = await import("data:text/javascript;base64," + js2); + return module.main; +} + +function clearOutput() { + while (output.firstChild) { + output.removeChild(output.firstChild); + } +} + +function replaceOutput(content, className) { + clearOutput(); + appendOutput(content, className); +} + +function appendOutput(content, className) { + const element = document.createElement("pre"); + element.textContent = content; + element.className = className; + output.appendChild(element); +} + +const editor = new CodeFlask("#editor-target", { + language: "gleam", +}); +editor.addLanguage("gleam", prismGrammar); +editor.updateCode(initialCode); + +const compiler = await initGleamCompiler(); +const project = compiler.newProject(); +for (const [name, code] of Object.entries(stdlib)) { + project.writeModule(name, code); +} + +function debounce(fn, delay) { + let timer = null; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +} + +editor.onUpdate(debounce((code) => compileEval(project, code), 200)); +compileEval(project, initialCode); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b18edfd --- /dev/null +++ b/static/style.css @@ -0,0 +1,174 @@ +@font-face { + font-family: "Lexend"; + font-display: swap; + font-weight: 400; + src: url("https://gleam.run/fonts/Lexend.woff2") format("woff2"); +} + +@font-face { + font-family: "Lexend"; + font-display: swap; + font-weight: 700; + src: url("https://gleam.run/fonts/Lexend-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Outfit"; + font-display: swap; + src: url("https://gleam.run/fonts/Outfit.woff") format("woff"); +} + +:root { + --font-family-normal: "Outfit", sans-serif; + --font-family-title: "Lexend", sans-serif; + + --color-pink: #ffaff3; + --color-red: #ff6262; + --color-orange: #ffd596; + --color-green: #c8ffa7; + --gap: 12px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + display: flex; + flex-direction: column; + font-family: var(--font-family-normal); + letter-spacing: 0.01em; + line-height: 1.3; +} + +codeflask__textarea, +pre, +code { + font-weight: normal; + letter-spacing: unset; +} + +p code { + padding: 1px 2px; + background-color: #f5f5f5; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-family-title); + font-weight: normal; +} + +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--gap); + background-color: var(--color-pink); +} + +.navbar a:visited, +.navbar a { + text-decoration: none; + color: #000; +} + +.playground { + display: flex; + flex-direction: column; + flex-wrap: wrap; + flex: auto; + max-width: 100vw; +} + +#text { + flex: 1; +} + +#text > :first-child { + margin-top: 0; +} + +#editor { + flex: 1; + position: relative; +} + +#output { + flex: 1; +} + +@media (min-width: 600px) { + #text, + #output, + #editor { + width: 50vw; + } + #text { + flex-basis: 100%; + border-right: 2px solid var(--color-pink); + } + #editor { + flex: 1.62; + } + #output, + #editor { + max-height: 50vh; + } +} + +#text, +#output > *, +#editor .codeflask__flatten { + padding: var(--gap); +} + +#text, +#output, +#editor { + min-width: 50%; + max-width: 100%; + overflow-y: auto; + border-bottom: 2px solid var(--color-pink); + margin: 0; + overflow-wrap: break-word; +} + +#output > * { + margin: 0; + white-space: pre-wrap; +} + +.error, +.warning { + border: var(--gap) solid var(--border-color); + border-top: 0; + border-bottom: 0; +} + +.error { + --border-color: var(--color-red); +} + +.warning { + --border-color: var(--color-orange); +} + +.prev-next { + display: flex; + justify-content: center; + align-items: center; + padding: 0 var(--gap); + gap: 0.5em; +} + +.prev-next span { + opacity: 0.5; +} |