aboutsummaryrefslogtreecommitdiff
path: root/src/tour.gleam
diff options
context:
space:
mode:
authorLouis Pilfold <louis@lpil.uk>2024-01-18 17:47:12 +0000
committerLouis Pilfold <louis@lpil.uk>2024-01-18 17:47:12 +0000
commit3bfbe688f5a62e29835c7d3c4f282e7fff57949d (patch)
tree038cd1bf7a72949f7d68358ffd8af59d073ef70a /src/tour.gleam
parentf92661980deac22b54e79cd44c25caba17910c95 (diff)
downloadtour-3bfbe688f5a62e29835c7d3c4f282e7fff57949d.tar.gz
tour-3bfbe688f5a62e29835c7d3c4f282e7fff57949d.zip
Rename
Diffstat (limited to 'src/tour.gleam')
-rw-r--r--src/tour.gleam554
1 files changed, 554 insertions, 0 deletions
diff --git a/src/tour.gleam b/src/tour.gleam
new file mode 100644
index 0000000..5738bcf
--- /dev/null
+++ b/src/tour.gleam
@@ -0,0 +1,554 @@
+import gleam/io
+import gleam/list
+import htmb.{h, text}
+import gleam/string_builder
+import gleam/option.{type Option, None, Some}
+import gleam/string
+import gleam/result
+import simplifile
+import snag
+
+const static = "static"
+
+const public = "public"
+
+const public_precompiled = "public/precompiled"
+
+const prelude = "build/dev/javascript/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 = "../gleam/compiler-wasm/pkg"
+
+const content_path = "src/content"
+
+const hello_joe = "import gleam/io
+
+pub fn main() {
+ io.println(\"Hello, Joe!\")
+}
+"
+
+const hello_mike = "import gleam/io
+import gleam/list
+
+pub fn main() {
+ list.each(erlang_the_movie, io.println)
+}
+
+const erlang_the_movie = [
+ \"📞\", \"Hello, Mike!\", \"Hello, Joe!\", \"System working?\", \"Seems to be.\",
+ \"OK, fine.\", \"OK.\", \"💫\",
+]
+"
+
+const home_html = "
+<h2>Welcome the Gleam language tour! 💫</h2>
+<p>
+ This tour 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 bottom section, 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>
+"
+
+const what_next_html = "
+<h2>What next? 💫</h2>
+<p>
+ Congratulations on completing the tour! Here's some ideas for what to do next:
+</p>
+
+<p>
+ Read the <a href=\"https://gleam.run/getting-started/\">Gleam getting started
+ documentation</a> to learn more about the language and its tooling.
+</p>
+<p>
+ Join the <a href=\"https://discord.gg/Fm8Pwmy\">the Gleam Discord server</a>
+ and meet the community. They're friendly and helpful!
+</p>
+<p>
+ Enroll in the <a href=\"https://exercism.io/tracks/gleam\">Exercism
+ Gleam track</a> practice your Gleam skills through a series of exercises,
+ and get feedback from experienced Gleam developers.
+</p>
+"
+
+const path_home = "/"
+
+const path_index = "/index"
+
+const path_what_next = "/what-next"
+
+// 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_content())
+ use _ <- result.try(write_content(p))
+ Ok(Nil)
+ }
+
+ case result {
+ Ok(_) -> {
+ io.println("Done")
+ }
+ Error(snag) -> {
+ panic as snag.pretty_print(snag)
+ }
+ }
+}
+
+type Chapter {
+ Chapter(name: String, path: String, lessons: List(Lesson))
+}
+
+type Lesson {
+ Lesson(
+ name: String,
+ text: String,
+ code: String,
+ path: String,
+ previous: Option(String),
+ next: Option(String),
+ )
+}
+
+type FileNames {
+ FileNames(path: String, name: String, slug: String)
+}
+
+fn load_directory_names(path: String) -> snag.Result(List(FileNames)) {
+ use files <- result.map(
+ simplifile.read_directory(path)
+ |> file_error("Failed to read directory " <> path),
+ )
+ files
+ |> list.sort(by: string.compare)
+ |> list.filter(fn(file) { !string.starts_with(file, ".") })
+ |> list.map(fn(file) {
+ let path = path <> "/" <> file
+ let slug =
+ file
+ |> string.split("_")
+ |> list.drop(1)
+ |> string.join("-")
+ let name =
+ slug
+ |> string.replace("-", " ")
+ |> string.capitalise
+ FileNames(path: path, name: name, slug: slug)
+ })
+}
+
+fn load_chapter(names: FileNames) -> snag.Result(Chapter) {
+ let path = "/" <> names.slug
+ use lessons <- result.try(load_directory_names(names.path))
+ use lessons <- result.try(list.try_map(lessons, load_lesson(path, _)))
+ Ok(Chapter(name: names.name, path: path, lessons: lessons))
+}
+
+fn read_file(path: String) -> snag.Result(String) {
+ simplifile.read(path)
+ |> file_error("Failed to read file " <> path)
+}
+
+fn load_lesson(chapter_path: String, names: FileNames) -> snag.Result(Lesson) {
+ use code <- result.try(read_file(names.path <> "/code.gleam"))
+ use text <- result.try(read_file(names.path <> "/text.html"))
+
+ Ok(Lesson(
+ name: names.name,
+ text: text,
+ code: code,
+ path: chapter_path <> "/" <> names.slug,
+ previous: None,
+ next: None,
+ ))
+}
+
+fn load_content() -> snag.Result(List(Chapter)) {
+ use chapters <- result.try(load_directory_names(content_path))
+ use chapters <- result.try(list.try_map(chapters, load_chapter))
+ Ok(add_prev_next(chapters, [], path_home))
+}
+
+fn write_content(chapters: List(Chapter)) -> snag.Result(Nil) {
+ let lessons = list.flat_map(chapters, fn(c) { c.lessons })
+ use _ <- result.try(list.try_map(lessons, write_lesson))
+
+ let assert Ok(first) = list.first(lessons)
+ let assert Ok(last) = list.last(lessons)
+
+ // Home page
+ use _ <- result.try(
+ write_lesson(Lesson(
+ name: "Hello, world!",
+ text: home_html,
+ code: hello_joe,
+ path: path_home,
+ previous: None,
+ next: Some(first.path),
+ )),
+ )
+
+ // "What next" final page
+ use _ <- result.try(
+ write_lesson(Lesson(
+ name: "What next?",
+ text: what_next_html,
+ code: hello_mike,
+ path: path_what_next,
+ previous: Some(last.path),
+ next: None,
+ )),
+ )
+
+ // Lesson index page
+ use _ <- result.try(
+ write_lesson(Lesson(
+ name: "Index",
+ text: string.join(list.map(chapters, index_chapter_html), "\n"),
+ code: hello_joe,
+ path: path_index,
+ previous: None,
+ next: None,
+ )),
+ )
+
+ Ok(Nil)
+}
+
+fn index_chapter_html(chapter: Chapter) -> String {
+ string.concat([
+ render_html(h("h3", [#("class", "mb-0")], [text(chapter.name)])),
+ render_html(h(
+ "ul",
+ [],
+ list.map(chapter.lessons, fn(lesson) {
+ h("li", [], [
+ h("a", [#("href", lesson.path)], [
+ lesson.name
+ |> string.replace("-", " ")
+ |> string.capitalise
+ |> text,
+ ]),
+ ])
+ }),
+ )),
+ ])
+}
+
+fn render_html(html: htmb.Html) -> String {
+ html
+ |> htmb.render
+ |> string_builder.to_string
+}
+
+fn write_lesson(lesson: Lesson) -> snag.Result(Nil) {
+ let path = public <> lesson.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: lesson_html(lesson))
+ |> file_error("Failed to write page " <> path)
+}
+
+fn add_prev_next(
+ rest: List(Chapter),
+ acc: List(Chapter),
+ previous: String,
+) -> List(Chapter) {
+ case rest {
+ [chapter1, Chapter(lessons: [next, ..], ..) as chapter2, ..rest] -> {
+ let lessons = chapter1.lessons
+ let #(lessons, previous) =
+ add_prev_next_for_chapter(lessons, [], previous, next.path)
+ let chapter1 = Chapter(..chapter1, lessons: lessons)
+ add_prev_next([chapter2, ..rest], [chapter1, ..acc], previous)
+ }
+
+ [chapter, ..rest] -> {
+ let lessons = chapter.lessons
+ let #(lessons, previous) =
+ add_prev_next_for_chapter(lessons, [], previous, path_what_next)
+ let chapter = Chapter(..chapter, lessons: lessons)
+ add_prev_next(rest, [chapter, ..acc], previous)
+ }
+
+ [] -> list.reverse(acc)
+ }
+}
+
+fn add_prev_next_for_chapter(
+ rest: List(Lesson),
+ acc: List(Lesson),
+ previous: String,
+ last: String,
+) -> #(List(Lesson), String) {
+ case rest {
+ [lesson1, lesson2, ..rest] -> {
+ let next = lesson2.path
+ let lesson = Lesson(..lesson1, previous: Some(previous), next: Some(next))
+ let rest = [lesson2, ..rest]
+ add_prev_next_for_chapter(rest, [lesson, ..acc], lesson.path, last)
+ }
+ [lesson, ..rest] -> {
+ let lesson = Lesson(..lesson, previous: Some(previous), next: Some(last))
+ add_prev_next_for_chapter(rest, [lesson, ..acc], lesson.path, last)
+ }
+ [] -> #(list.reverse(acc), previous)
+ }
+}
+
+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 _ <- result.try(
+ simplifile.create_directory_all(public)
+ |> file_error("Failed to delete public directory"),
+ )
+
+ 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 lesson_html(page: Lesson) -> 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(page.name <> " - The Gleam Language Tour")]),
+ h("link", [#("rel", "stylesheet"), #("href", "/style.css")], []),
+ ]),
+ h("body", [], [
+ h("nav", [#("class", "navbar")], [
+ h("a", [#("href", "/")], [text("Try Gleam")]),
+ ]),
+ h("article", [#("id", "playground")], [
+ h("section", [#("id", "left")], [
+ htmb.dangerous_unescaped_fragment(string_builder.from_string(
+ page.text,
+ )),
+ h("nav", [#("class", "prev-next")], [
+ navlink("Back", page.previous),
+ text(" — "),
+ h("a", [#("href", path_index)], [text("Index")]),
+ text(" — "),
+ navlink("Next", page.next),
+ ]),
+ ]),
+ h("section", [#("id", "right")], [
+ h("section", [#("id", "editor")], [
+ h("div", [#("id", "editor-target")], []),
+ ]),
+ h("aside", [#("id", "output")], []),
+ ]),
+ ]),
+ h("script", [#("type", "gleam"), #("id", "code")], [
+ htmb.dangerous_unescaped_fragment(string_builder.from_string(page.code)),
+ ]),
+ h("script", [#("type", "module"), #("src", "/index.js")], []),
+ ]),
+ ])
+ |> htmb.render_page("html")
+ |> string_builder.to_string
+}