aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/test.yml23
-rw-r--r--.gitignore5
-rw-r--r--README.md22
-rw-r--r--gleam.toml19
-rw-r--r--lessons/README.md22
-rw-r--r--lessons/gleam.toml9
-rw-r--r--lessons/manifest.toml11
-rw-r--r--lessons/src/lesson000_hello_world/code.gleam5
-rw-r--r--lessons/src/lesson000_hello_world/text.html26
-rw-r--r--lessons/src/lesson001_basics/code.gleam5
-rw-r--r--lessons/src/lesson001_basics/text.html3
-rw-r--r--lessons/src/lesson002_another/code.gleam5
-rw-r--r--lessons/src/lesson002_another/text.html8
-rw-r--r--lessons/test/lessons_test.gleam12
-rw-r--r--manifest.toml17
-rw-r--r--src/try_gleam.gleam390
-rw-r--r--static/compiler.js90
-rw-r--r--static/index.js124
-rw-r--r--static/style.css174
-rw-r--r--test/playground_test.gleam12
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
+
+[![Package Version](https://img.shields.io/hexpm/v/playground)](https://hex.pm/packages/playground)
+[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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
+
+[![Package Version](https://img.shields.io/hexpm/v/lessons)](https://hex.pm/packages/lessons)
+[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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)
+}