diff options
-rw-r--r-- | .github/workflows/test.yml | 23 | ||||
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | gleam.toml | 19 | ||||
-rw-r--r-- | lessons/README.md | 22 | ||||
-rw-r--r-- | lessons/gleam.toml | 9 | ||||
-rw-r--r-- | lessons/manifest.toml | 11 | ||||
-rw-r--r-- | lessons/src/lesson000_hello_world/code.gleam | 5 | ||||
-rw-r--r-- | lessons/src/lesson000_hello_world/text.html | 26 | ||||
-rw-r--r-- | lessons/src/lesson001_basics/code.gleam | 5 | ||||
-rw-r--r-- | lessons/src/lesson001_basics/text.html | 3 | ||||
-rw-r--r-- | lessons/src/lesson002_another/code.gleam | 5 | ||||
-rw-r--r-- | lessons/src/lesson002_another/text.html | 8 | ||||
-rw-r--r-- | lessons/test/lessons_test.gleam | 12 | ||||
-rw-r--r-- | manifest.toml | 17 | ||||
-rw-r--r-- | src/try_gleam.gleam | 390 | ||||
-rw-r--r-- | static/compiler.js | 90 | ||||
-rw-r--r-- | static/index.js | 124 | ||||
-rw-r--r-- | static/style.css | 174 | ||||
-rw-r--r-- | test/playground_test.gleam | 12 |
20 files changed, 982 insertions, 0 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cf2096e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "0.32.4" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e472833 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.beam +*.ez +build +erl_crash.dump +public diff --git a/README.md b/README.md new file mode 100644 index 0000000..5516621 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# playground + +[](https://hex.pm/packages/playground) +[](https://hexdocs.pm/playground/) + +## Quick start + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` + +## Installation + +If available on Hex this package can be added to your Gleam project: + +```sh +gleam add playground +``` + +and its documentation can be found at <https://hexdocs.pm/playground>. diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..6c6cf61 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,19 @@ +name = "try_gleam" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "username", repo = "project" } +# links = [{ title = "Website", href = "https://gleam.run" }] + +[dependencies] +gleam_stdlib = "~> 0.32" +simplifile = "~> 1.0" +snag = "~> 0.2" +htmb = "~> 1.1" + +[dev-dependencies] +gleeunit = "~> 1.0" diff --git a/lessons/README.md b/lessons/README.md new file mode 100644 index 0000000..01cefea --- /dev/null +++ b/lessons/README.md @@ -0,0 +1,22 @@ +# lessons + +[](https://hex.pm/packages/lessons) +[](https://hexdocs.pm/lessons/) + +## Quick start + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` + +## Installation + +If available on Hex this package can be added to your Gleam project: + +```sh +gleam add lessons +``` + +and its documentation can be found at <https://hexdocs.pm/lessons>. diff --git a/lessons/gleam.toml b/lessons/gleam.toml new file mode 100644 index 0000000..6eb67c6 --- /dev/null +++ b/lessons/gleam.toml @@ -0,0 +1,9 @@ +name = "lessons" +version = "1.0.0" +target = "javascript" + +[dependencies] +gleam_stdlib = "~> 0.32" + +[dev-dependencies] +gleeunit = "~> 1.0" diff --git a/lessons/manifest.toml b/lessons/manifest.toml new file mode 100644 index 0000000..9491fa4 --- /dev/null +++ b/lessons/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.33.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "539E37A2AA5EBE8E75F4B74755E4CC604BD957C3000AC8D705A2024886A2738B" }, + { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, +] + +[requirements] +gleam_stdlib = { version = "~> 0.32" } +gleeunit = { version = "~> 1.0" } diff --git a/lessons/src/lesson000_hello_world/code.gleam b/lessons/src/lesson000_hello_world/code.gleam new file mode 100644 index 0000000..30530b2 --- /dev/null +++ b/lessons/src/lesson000_hello_world/code.gleam @@ -0,0 +1,5 @@ +import gleam/io + +pub fn main() { + io.println("Hello, Joe!") +} diff --git a/lessons/src/lesson000_hello_world/text.html b/lessons/src/lesson000_hello_world/text.html new file mode 100644 index 0000000..46e3fc2 --- /dev/null +++ b/lessons/src/lesson000_hello_world/text.html @@ -0,0 +1,26 @@ +<h2>Hello, friend 💫</h2> +<p> + Welcome to Try Gleam! An interactive tour of the Gleam programming language. +</p> +<p> + It covers all aspects of the Gleam language, and assuming you have some + prior programming experience should teach you everything you need to write + real programs in Gleam. +</p> +<p> + The tour is interactive! The code shown is editable and will be compiled and + evaluated as you type. Anything you print using <code>io.println</code> or + <code>io.debug</code> will be shown in the lower right, along with any compile + errors and warnings. To evaluate Gleam code the tour compiles Gleam to + JavaScript and runs it, all entirely within your browser window. +</p> +<p> + If at any point you get stuck or have a question do not hesitate to ask in + <a href="https://discord.gg/Fm8Pwmy">the Gleam Discord server</a>. We're here + to help, and if you find something confusing then it's likely others will too, + and we want to know about it so we can improve the tour. +</p> +<p> + OK, let's go. Click "Next" to get started, or click "Index" to jump to a + specific topic. +</p> diff --git a/lessons/src/lesson001_basics/code.gleam b/lessons/src/lesson001_basics/code.gleam new file mode 100644 index 0000000..440dc98 --- /dev/null +++ b/lessons/src/lesson001_basics/code.gleam @@ -0,0 +1,5 @@ +import gleam/io + +pub fn main() { + io.println("Hello, Mike!") +} diff --git a/lessons/src/lesson001_basics/text.html b/lessons/src/lesson001_basics/text.html new file mode 100644 index 0000000..53bd3d3 --- /dev/null +++ b/lessons/src/lesson001_basics/text.html @@ -0,0 +1,3 @@ +<p> + Hey look, cool stuff! +</p> diff --git a/lessons/src/lesson002_another/code.gleam b/lessons/src/lesson002_another/code.gleam new file mode 100644 index 0000000..440dc98 --- /dev/null +++ b/lessons/src/lesson002_another/code.gleam @@ -0,0 +1,5 @@ +import gleam/io + +pub fn main() { + io.println("Hello, Mike!") +} diff --git a/lessons/src/lesson002_another/text.html b/lessons/src/lesson002_another/text.html new file mode 100644 index 0000000..850d151 --- /dev/null +++ b/lessons/src/lesson002_another/text.html @@ -0,0 +1,8 @@ +<p> + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Magnam ab fuga, + placeat hic possimus harum tempore voluptatem, id nulla nihil laboriosam + suscipit quis obcaecati beatae blanditiis sint. Suscipit, voluptas officia. +</p> +<p> + Very exciting. +</p> diff --git a/lessons/test/lessons_test.gleam b/lessons/test/lessons_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/lessons/test/lessons_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..c4da273 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,17 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.33.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "539E37A2AA5EBE8E75F4B74755E4CC604BD957C3000AC8D705A2024886A2738B" }, + { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, + { name = "htmb", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "htmb", source = "hex", outer_checksum = "30D448F0E15DFCF7283AAAC2F351D77B9D54E318219C9FDDB1877572B67C27B7" }, + { name = "simplifile", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0BD6F0E7DA1A7E11D18B8AD48453225CAFCA4C8CFB4513D217B372D2866C501C" }, + { name = "snag", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "8FD70D8FB3728E08AC425283BB509BB0F012BE1AE218424A597CDE001B0EE589" }, +] + +[requirements] +gleam_stdlib = { version = "~> 0.32" } +gleeunit = { version = "~> 1.0" } +htmb = { version = "~> 1.1" } +simplifile = { version = "~> 1.0" } +snag = { version = "~> 0.2" } diff --git a/src/try_gleam.gleam b/src/try_gleam.gleam new file mode 100644 index 0000000..f34f4a7 --- /dev/null +++ b/src/try_gleam.gleam @@ -0,0 +1,390 @@ +import gleam/io +import gleam/list +import htmb.{h, text} +import gleam/string_builder +import gleam/option.{type Option, None, Some} +import gleam/pair +import gleam/string +import gleam/result +import simplifile +import snag + +const static = "static" + +const public = "public" + +const public_precompiled = "public/precompiled" + +const prelude = "../compiler-core/templates/prelude.mjs" + +const stdlib_compiled = "build/dev/javascript/gleam_stdlib/gleam" + +const stdlib_sources = "build/packages/gleam_stdlib/src/gleam" + +const stdlib_external = "build/packages/gleam_stdlib/src" + +const compiler_wasm = "../compiler-wasm/pkg" + +const lessons_src = "lessons/src" + +const hello_joe = "import gleam/io + +pub fn main() { + io.println(\"Hello, Joe!\") +} +" + +// Don't include deprecated stdlib modules +const skipped_stdlib_modules = [ + "bit_string.gleam", "bit_builder.gleam", "map.gleam", +] + +pub fn main() { + let result = { + use _ <- result.try(reset_output()) + use _ <- result.try(make_prelude_available()) + use _ <- result.try(make_stdlib_available()) + use _ <- result.try(copy_wasm_compiler()) + use p <- result.try(load_pages()) + use _ <- result.try(write_pages(p)) + Ok(Nil) + } + + case result { + Ok(_) -> Nil + Error(snag) -> { + io.println(snag.pretty_print(snag)) + panic + } + } +} + +type Page { + Page( + name: String, + text: String, + code: String, + path: String, + previous: Option(String), + next: Option(String), + ) +} + +fn load_pages() -> snag.Result(List(Page)) { + use lessons <- result.try( + simplifile.read_directory(lessons_src) + |> file_error("Failed to read lessons directory"), + ) + + let lessons = + lessons + |> list.sort(by: string.compare) + |> list.index_map(pair.new) + + use pages <- result.try(list.try_map(lessons, fn(pair) { + let #(index, lesson) = pair + let path = lessons_src <> "/" <> lesson + let name = + lesson + |> string.split("_") + |> list.drop(1) + |> string.join("-") + + use code <- result.try( + simplifile.read(path <> "/code.gleam") + |> file_error("Failed to read code.gleam"), + ) + + use text <- result.try( + simplifile.read(path <> "/text.html") + |> file_error("Failed to read text.html"), + ) + + let path = case index { + 0 -> "/" + _ -> "/" <> name + } + + Ok( + Page( + name: name, + text: text, + code: code, + path: path, + previous: None, + next: None, + ), + ) + })) + + Ok(add_previous_next(pages, [], None)) +} + +fn write_pages(pages: List(Page)) -> snag.Result(Nil) { + use _ <- result.try(list.try_each(pages, write_page)) + + let render = fn(h) { string_builder.to_string(htmb.render(h)) } + let html = + string.concat([ + render(h("h2", [], [text("Table of contents")])), + render(h("ul", [], list.map(pages, fn(page) { + h("li", [], [ + h("a", [#("href", page.path)], [ + page.name + |> string.replace("-", " ") + |> string.capitalise + |> text, + ]), + ]) + }))), + ]) + + let page = + Page( + name: "Index", + text: html, + code: hello_joe, + path: "/index", + previous: None, + next: None, + ) + write_page(page) +} + +fn write_page(page: Page) -> snag.Result(Nil) { + let path = public <> page.path + use _ <- result.try( + simplifile.create_directory_all(path) + |> file_error("Failed to make " <> path), + ) + + let path = path <> "/index.html" + simplifile.write(to: path, contents: page_html(page)) + |> file_error("Failed to write page " <> path) +} + +fn add_previous_next( + rest: List(Page), + acc: List(Page), + previous: Option(String), +) -> List(Page) { + case rest { + [] -> list.reverse(acc) + [page, next, ..rest] -> { + let page = Page(..page, previous: previous, next: Some(next.path)) + add_previous_next([next, ..rest], [page, ..acc], Some(page.path)) + } + [page, ..rest] -> { + let page = Page(..page, previous: previous, next: None) + add_previous_next(rest, [page, ..acc], Some(page.path)) + } + } +} + +fn copy_wasm_compiler() -> snag.Result(Nil) { + use <- require( + simplifile.is_directory(compiler_wasm), + "compiler-wasm/pkg must have been compiled", + ) + + simplifile.copy_directory(compiler_wasm, public <> "/compiler") + |> file_error("Failed to copy compiler-wasm") +} + +fn make_prelude_available() -> snag.Result(Nil) { + use _ <- result.try( + simplifile.create_directory_all(public_precompiled) + |> file_error("Failed to make " <> public_precompiled), + ) + + simplifile.copy_file(prelude, public_precompiled <> "/gleam.mjs") + |> file_error("Failed to copy prelude.mjs") +} + +fn make_stdlib_available() -> snag.Result(Nil) { + use files <- result.try( + simplifile.read_directory(stdlib_sources) + |> file_error("Failed to read stdlib directory"), + ) + + let modules = + files + |> list.filter(fn(file) { string.ends_with(file, ".gleam") }) + |> list.filter(fn(file) { !list.contains(skipped_stdlib_modules, file) }) + |> list.map(string.replace(_, ".gleam", "")) + + use _ <- result.try( + generate_stdlib_bundle(modules) + |> snag.context("Failed to generate stdlib.js bundle"), + ) + + use _ <- result.try( + copy_compiled_stdlib(modules) + |> snag.context("Failed to copy precompiled stdlib modules"), + ) + + use _ <- result.try( + copy_stdlib_externals() + |> snag.context("Failed to copy stdlib external files"), + ) + + Ok(Nil) +} + +fn copy_stdlib_externals() -> snag.Result(Nil) { + use files <- result.try( + simplifile.read_directory(stdlib_external) + |> file_error("Failed to read stdlib external directory"), + ) + let files = list.filter(files, string.ends_with(_, ".mjs")) + + list.try_each(files, fn(file) { + let from = stdlib_external <> "/" <> file + let to = public_precompiled <> "/" <> file + simplifile.copy_file(from, to) + |> file_error("Failed to copy stdlib external file " <> from) + }) +} + +fn copy_compiled_stdlib(modules: List(String)) -> snag.Result(Nil) { + use <- require( + simplifile.is_directory(stdlib_compiled), + "Project must have been compiled for JavaScript", + ) + + let dest = public_precompiled <> "/gleam" + use _ <- result.try( + simplifile.create_directory_all(dest) + |> file_error("Failed to make " <> dest), + ) + + use _ <- result.try(list.try_each(modules, fn(name) { + let from = stdlib_compiled <> "/" <> name <> ".mjs" + let to = dest <> "/" <> name <> ".mjs" + simplifile.copy_file(from, to) + |> file_error("Failed to copy stdlib module " <> from) + })) + + Ok(Nil) +} + +fn generate_stdlib_bundle(modules: List(String)) -> snag.Result(Nil) { + use entries <- result.try(list.try_map(modules, fn(name) { + let path = stdlib_sources <> "/" <> name <> ".gleam" + use code <- result.try( + simplifile.read(path) + |> file_error("Failed to read stdlib module " <> path), + ) + let name = string.replace(name, ".gleam", "") + let code = + code + |> string.replace("\\", "\\\\") + |> string.replace("`", "\\`") + |> string.split("\n") + |> list.filter(fn(line) { !string.starts_with(string.trim(line), "//") }) + |> list.filter(fn(line) { !string.starts_with(line, "@external(erlang") }) + |> list.filter(fn(line) { line != "" }) + |> string.join("\n") + + Ok(" \"gleam/" <> name <> "\": `" <> code <> "`") + })) + + entries + |> string.join(",\n") + |> string.append("export default {\n", _) + |> string.append("\n}\n") + |> simplifile.write(public <> "/stdlib.js", _) + |> file_error("Failed to write stdlib.js") +} + +fn reset_output() -> snag.Result(Nil) { + use files <- result.try( + simplifile.read_directory(public) + |> file_error("Failed to read public directory"), + ) + + use _ <- result.try( + files + |> list.map(string.append(public <> "/", _)) + |> simplifile.delete_all + |> file_error("Failed to delete public directory"), + ) + + simplifile.copy_directory(static, public) + |> file_error("Failed to copy static directory") +} + +fn require( + that condition: Bool, + because reason: String, + then next: fn() -> snag.Result(t), +) -> snag.Result(t) { + case condition { + True -> next() + False -> Error(snag.new(reason)) + } +} + +fn file_error( + result: Result(t, simplifile.FileError), + context: String, +) -> snag.Result(t) { + case result { + Ok(value) -> Ok(value) + Error(error) -> + snag.error("File error: " <> string.inspect(error)) + |> snag.context(context) + } +} + +fn page_html(page: Page) -> String { + let navlink = fn(name, link) { + case link { + None -> h("span", [], [text(name)]) + Some(path) -> h("a", [#("href", path)], [text(name)]) + } + } + + h("html", [#("lang", "en-gb")], [ + h("head", [], [ + h("meta", [#("charset", "utf-8")], []), + h( + "meta", + [ + #("name", "viewport"), + #("content", "width=device-width, initial-scale=1"), + ], + [], + ), + h("title", [], [text("Try Gleam")]), + h("link", [#("rel", "stylesheet"), #("href", "/style.css")], []), + ]), + h("body", [], [ + h("nav", [#("class", "navbar")], [ + h("a", [#("href", "/")], [text("Try Gleam")]), + ]), + h("article", [#("class", "playground")], [ + h("section", [#("id", "text")], [ + htmb.dangerous_unescaped_fragment( + string_builder.from_string(page.text), + ), + h("nav", [#("class", "prev-next")], [ + navlink("Back", page.previous), + text(" — "), + h("a", [#("href", "/index")], [text("Index")]), + text(" — "), + navlink("Next", page.next), + ]), + ]), + h("section", [#("id", "editor")], [ + h("div", [#("id", "editor-target")], []), + ]), + h("aside", [#("id", "output")], []), + ]), + h("script", [#("type", "gleam"), #("id", "code")], [text(page.code)]), + h("script", [#("type", "module"), #("src", "/index.js")], []), + ]), + ]) + |> htmb.render_page("html") + |> string_builder.to_string +} 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; +} diff --git a/test/playground_test.gleam b/test/playground_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/test/playground_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} |