aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorLouis Pilfold <louis@lpil.uk>2023-12-01 22:17:09 +0000
committerLouis Pilfold <louis@lpil.uk>2023-12-06 16:20:50 +0000
commite32d68d064c4663818dc5445541f2cd67d2e04dc (patch)
tree5270ebce540312e8d7ea76a78e656726245920e0 /static
downloadtour-e32d68d064c4663818dc5445541f2cd67d2e04dc.tar.gz
tour-e32d68d064c4663818dc5445541f2cd67d2e04dc.zip
Rename
Diffstat (limited to 'static')
-rw-r--r--static/compiler.js90
-rw-r--r--static/index.js124
-rw-r--r--static/style.css174
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;
+}