From 6f27f1a096bf193c42ee5703aefc7acb3919ea6f Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Mon, 6 May 2024 11:56:52 +0100 Subject: [PATCH 01/18] copy tour --- README.md | 26 +- bin/download-compiler | 12 + gleam.toml | 5 + manifest.toml | 19 + .../lesson01_hello_world/code.gleam | 5 + .../lesson01_hello_world/en.html | 14 + .../lesson02_modules/code.gleam | 10 + .../chapter0_basics/lesson02_modules/en.html | 37 + .../lesson03_unqualified_imports/code.gleam | 10 + .../lesson03_unqualified_imports/en.html | 17 + .../lesson04_type_checking/code.gleam | 7 + .../lesson04_type_checking/en.html | 28 + .../chapter0_basics/lesson05_ints/code.gleam | 25 + .../chapter0_basics/lesson05_ints/en.html | 17 + .../lesson06_floats/code.gleam | 27 + .../chapter0_basics/lesson06_floats/en.html | 32 + .../lesson07_number_formats/code.gleam | 16 + .../lesson07_number_formats/en.html | 13 + .../lesson08_equality/code.gleam | 6 + .../chapter0_basics/lesson08_equality/en.html | 13 + .../lesson09_strings/code.gleam | 23 + .../chapter0_basics/lesson09_strings/en.html | 26 + .../chapter0_basics/lesson10_bools/code.gleam | 14 + .../chapter0_basics/lesson10_bools/en.html | 20 + .../lesson11_assignments/code.gleam | 17 + .../lesson11_assignments/en.html | 9 + .../lesson12_discard_patterns/code.gleam | 4 + .../lesson12_discard_patterns/en.html | 10 + .../lesson13_type_annotations/code.gleam | 7 + .../lesson13_type_annotations/en.html | 15 + .../lesson14_type_imports/code.gleam | 10 + .../lesson14_type_imports/en.html | 22 + .../lesson15_type_aliases/code.gleam | 12 + .../lesson15_type_aliases/en.html | 12 + .../lesson16_blocks/code.gleam | 13 + .../chapter0_basics/lesson16_blocks/en.html | 26 + .../chapter0_basics/lesson17_lists/code.gleam | 16 + .../chapter0_basics/lesson17_lists/en.html | 21 + .../lesson18_constants/code.gleam | 13 + .../lesson18_constants/en.html | 18 + .../lesson00_functions/code.gleam | 13 + .../lesson00_functions/en.html | 14 + .../code.gleam | 18 + .../lesson03_higher_order_functions/en.html | 12 + .../lesson04_anonymous_functions/code.gleam | 19 + .../lesson04_anonymous_functions/en.html | 9 + .../lesson05_function_captures/code.gleam | 14 + .../lesson05_function_captures/en.html | 12 + .../lesson06_generic_functions/code.gleam | 20 + .../lesson06_generic_functions/en.html | 26 + .../lesson07_pipelines/code.gleam | 19 + .../lesson07_pipelines/en.html | 25 + .../lesson08_labelled_arguments/code.gleam | 16 + .../lesson08_labelled_arguments/en.html | 23 + .../code.gleam | 19 + .../lesson09_documentation_comments/en.html | 16 + .../lesson10_deprecations/code.gleam | 13 + .../lesson10_deprecations/en.html | 14 + .../lesson01_case_expressions/code.gleam | 17 + .../lesson01_case_expressions/en.html | 18 + .../lesson02_variable_patterns/code.gleam | 14 + .../lesson02_variable_patterns/en.html | 7 + .../lesson03_string_patterns/code.gleam | 14 + .../lesson03_string_patterns/en.html | 9 + .../lesson04_list_patterns/code.gleam | 17 + .../lesson04_list_patterns/en.html | 15 + .../lesson05_recursion/code.gleam | 18 + .../lesson05_recursion/en.html | 20 + .../lesson06_tail_calls/code.gleam | 22 + .../lesson06_tail_calls/en.html | 23 + .../lesson07_list_recursion/code.gleam | 13 + .../lesson07_list_recursion/en.html | 19 + .../lesson08_multiple_subjects/code.gleam | 17 + .../lesson08_multiple_subjects/en.html | 13 + .../lesson09_alternative_patterns/code.gleam | 14 + .../lesson09_alternative_patterns/en.html | 12 + .../lesson10_pattern_aliases/code.gleam | 15 + .../lesson10_pattern_aliases/en.html | 7 + .../lesson11_guards/code.gleam | 15 + .../lesson11_guards/en.html | 9 + .../lesson00_tuples/code.gleam | 10 + .../lesson00_tuples/en.html | 20 + .../lesson01_custom_types/code.gleam | 22 + .../lesson01_custom_types/en.html | 11 + .../lesson02_records/code.gleam | 17 + .../lesson02_records/en.html | 13 + .../lesson03_record_accessors/code.gleam | 15 + .../lesson03_record_accessors/en.html | 18 + .../lesson04_record_updates/code.gleam | 15 + .../lesson04_record_updates/en.html | 8 + .../lesson05_generic_custom_types/code.gleam | 10 + .../lesson05_generic_custom_types/en.html | 12 + .../lesson06_nil/code.gleam | 11 + .../chapter3_data_types/lesson06_nil/en.html | 15 + .../lesson07_results/code.gleam | 25 + .../lesson07_results/en.html | 39 + .../lesson08_bit_arrays/code.gleam | 13 + .../lesson08_bit_arrays/en.html | 44 + .../code.gleam | 6 + .../lesson00_standard_library_package/en.html | 17 + .../lesson01_list_module/code.gleam | 19 + .../lesson01_list_module/en.html | 42 + .../lesson02_result_module/code.gleam | 24 + .../lesson02_result_module/en.html | 40 + .../lesson03_dict_module/code.gleam | 14 + .../lesson03_dict_module/en.html | 40 + .../lesson04_option_module/code.gleam | 14 + .../lesson04_option_module/en.html | 19 + .../lesson00_opaque_types/code.gleam | 26 + .../lesson00_opaque_types/en.html | 17 + .../lesson01_use/code.gleam | 38 + .../lesson01_use/en.html | 30 + .../lesson02_use_sugar/code.gleam | 30 + .../lesson02_use_sugar/en.html | 30 + .../lesson03_todo/code.gleam | 7 + .../lesson03_todo/en.html | 14 + .../lesson04_panic/code.gleam | 15 + .../lesson04_panic/en.html | 11 + .../lesson05_let_assert/code.gleam | 16 + .../lesson05_let_assert/en.html | 15 + .../lesson06_externals/code.gleam | 17 + .../lesson06_externals/en.html | 25 + .../code.gleam | 11 + .../lesson07_multi_target_externals/en.html | 22 + .../code.gleam | 18 + .../lesson08_external_gleam_fallbacks/en.html | 13 + src/playground.gleam | 1060 ++++++++++++++++- src/playground/widgets.gleam | 212 ++++ static/common.css | 41 + static/compiler.js | 90 ++ static/css/code/color-schemes/atom-one.css | 76 ++ static/css/code/syntax-highlight.css | 196 +++ static/css/fonts.css | 28 + static/css/layout.css | 11 + static/css/pages/everything.css | 354 ++++++ static/css/pages/lesson.css | 132 ++ static/css/root.css | 168 +++ static/css/theme.css | 55 + static/index.js | 104 ++ static/js/highlight/highlight-gleam.js | 296 +++++ static/js/highlight/highlight.core.min.js | 307 +++++ static/js/highlight/regexes.js | 20 + static/precompiled/my_package_ffi.mjs | 3 + static/worker.js | 67 ++ test/tour_test.gleam | 11 + 145 files changed, 5396 insertions(+), 20 deletions(-) create mode 100755 bin/download-compiler create mode 100644 manifest.toml create mode 100644 src/content/chapter0_basics/lesson01_hello_world/code.gleam create mode 100644 src/content/chapter0_basics/lesson01_hello_world/en.html create mode 100644 src/content/chapter0_basics/lesson02_modules/code.gleam create mode 100644 src/content/chapter0_basics/lesson02_modules/en.html create mode 100644 src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam create mode 100644 src/content/chapter0_basics/lesson03_unqualified_imports/en.html create mode 100644 src/content/chapter0_basics/lesson04_type_checking/code.gleam create mode 100644 src/content/chapter0_basics/lesson04_type_checking/en.html create mode 100644 src/content/chapter0_basics/lesson05_ints/code.gleam create mode 100644 src/content/chapter0_basics/lesson05_ints/en.html create mode 100644 src/content/chapter0_basics/lesson06_floats/code.gleam create mode 100644 src/content/chapter0_basics/lesson06_floats/en.html create mode 100644 src/content/chapter0_basics/lesson07_number_formats/code.gleam create mode 100644 src/content/chapter0_basics/lesson07_number_formats/en.html create mode 100644 src/content/chapter0_basics/lesson08_equality/code.gleam create mode 100644 src/content/chapter0_basics/lesson08_equality/en.html create mode 100644 src/content/chapter0_basics/lesson09_strings/code.gleam create mode 100644 src/content/chapter0_basics/lesson09_strings/en.html create mode 100644 src/content/chapter0_basics/lesson10_bools/code.gleam create mode 100644 src/content/chapter0_basics/lesson10_bools/en.html create mode 100644 src/content/chapter0_basics/lesson11_assignments/code.gleam create mode 100644 src/content/chapter0_basics/lesson11_assignments/en.html create mode 100644 src/content/chapter0_basics/lesson12_discard_patterns/code.gleam create mode 100644 src/content/chapter0_basics/lesson12_discard_patterns/en.html create mode 100644 src/content/chapter0_basics/lesson13_type_annotations/code.gleam create mode 100644 src/content/chapter0_basics/lesson13_type_annotations/en.html create mode 100644 src/content/chapter0_basics/lesson14_type_imports/code.gleam create mode 100644 src/content/chapter0_basics/lesson14_type_imports/en.html create mode 100644 src/content/chapter0_basics/lesson15_type_aliases/code.gleam create mode 100644 src/content/chapter0_basics/lesson15_type_aliases/en.html create mode 100644 src/content/chapter0_basics/lesson16_blocks/code.gleam create mode 100644 src/content/chapter0_basics/lesson16_blocks/en.html create mode 100644 src/content/chapter0_basics/lesson17_lists/code.gleam create mode 100644 src/content/chapter0_basics/lesson17_lists/en.html create mode 100644 src/content/chapter0_basics/lesson18_constants/code.gleam create mode 100644 src/content/chapter0_basics/lesson18_constants/en.html create mode 100644 src/content/chapter1_functions/lesson00_functions/code.gleam create mode 100644 src/content/chapter1_functions/lesson00_functions/en.html create mode 100644 src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam create mode 100644 src/content/chapter1_functions/lesson03_higher_order_functions/en.html create mode 100644 src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam create mode 100644 src/content/chapter1_functions/lesson04_anonymous_functions/en.html create mode 100644 src/content/chapter1_functions/lesson05_function_captures/code.gleam create mode 100644 src/content/chapter1_functions/lesson05_function_captures/en.html create mode 100644 src/content/chapter1_functions/lesson06_generic_functions/code.gleam create mode 100644 src/content/chapter1_functions/lesson06_generic_functions/en.html create mode 100644 src/content/chapter1_functions/lesson07_pipelines/code.gleam create mode 100644 src/content/chapter1_functions/lesson07_pipelines/en.html create mode 100644 src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam create mode 100644 src/content/chapter1_functions/lesson08_labelled_arguments/en.html create mode 100644 src/content/chapter1_functions/lesson09_documentation_comments/code.gleam create mode 100644 src/content/chapter1_functions/lesson09_documentation_comments/en.html create mode 100644 src/content/chapter1_functions/lesson10_deprecations/code.gleam create mode 100644 src/content/chapter1_functions/lesson10_deprecations/en.html create mode 100644 src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson01_case_expressions/en.html create mode 100644 src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson02_variable_patterns/en.html create mode 100644 src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson03_string_patterns/en.html create mode 100644 src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson04_list_patterns/en.html create mode 100644 src/content/chapter2_flow_control/lesson05_recursion/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson05_recursion/en.html create mode 100644 src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson06_tail_calls/en.html create mode 100644 src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson07_list_recursion/en.html create mode 100644 src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html create mode 100644 src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html create mode 100644 src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html create mode 100644 src/content/chapter2_flow_control/lesson11_guards/code.gleam create mode 100644 src/content/chapter2_flow_control/lesson11_guards/en.html create mode 100644 src/content/chapter3_data_types/lesson00_tuples/code.gleam create mode 100644 src/content/chapter3_data_types/lesson00_tuples/en.html create mode 100644 src/content/chapter3_data_types/lesson01_custom_types/code.gleam create mode 100644 src/content/chapter3_data_types/lesson01_custom_types/en.html create mode 100644 src/content/chapter3_data_types/lesson02_records/code.gleam create mode 100644 src/content/chapter3_data_types/lesson02_records/en.html create mode 100644 src/content/chapter3_data_types/lesson03_record_accessors/code.gleam create mode 100644 src/content/chapter3_data_types/lesson03_record_accessors/en.html create mode 100644 src/content/chapter3_data_types/lesson04_record_updates/code.gleam create mode 100644 src/content/chapter3_data_types/lesson04_record_updates/en.html create mode 100644 src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam create mode 100644 src/content/chapter3_data_types/lesson05_generic_custom_types/en.html create mode 100644 src/content/chapter3_data_types/lesson06_nil/code.gleam create mode 100644 src/content/chapter3_data_types/lesson06_nil/en.html create mode 100644 src/content/chapter3_data_types/lesson07_results/code.gleam create mode 100644 src/content/chapter3_data_types/lesson07_results/en.html create mode 100644 src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam create mode 100644 src/content/chapter3_data_types/lesson08_bit_arrays/en.html create mode 100644 src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam create mode 100644 src/content/chapter4_standard_library/lesson00_standard_library_package/en.html create mode 100644 src/content/chapter4_standard_library/lesson01_list_module/code.gleam create mode 100644 src/content/chapter4_standard_library/lesson01_list_module/en.html create mode 100644 src/content/chapter4_standard_library/lesson02_result_module/code.gleam create mode 100644 src/content/chapter4_standard_library/lesson02_result_module/en.html create mode 100644 src/content/chapter4_standard_library/lesson03_dict_module/code.gleam create mode 100644 src/content/chapter4_standard_library/lesson03_dict_module/en.html create mode 100644 src/content/chapter4_standard_library/lesson04_option_module/code.gleam create mode 100644 src/content/chapter4_standard_library/lesson04_option_module/en.html create mode 100644 src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson00_opaque_types/en.html create mode 100644 src/content/chapter5_advanced_features/lesson01_use/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson01_use/en.html create mode 100644 src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson02_use_sugar/en.html create mode 100644 src/content/chapter5_advanced_features/lesson03_todo/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson03_todo/en.html create mode 100644 src/content/chapter5_advanced_features/lesson04_panic/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson04_panic/en.html create mode 100644 src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson05_let_assert/en.html create mode 100644 src/content/chapter5_advanced_features/lesson06_externals/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson06_externals/en.html create mode 100644 src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html create mode 100644 src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam create mode 100644 src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html create mode 100644 src/playground/widgets.gleam create mode 100644 static/common.css create mode 100644 static/compiler.js create mode 100644 static/css/code/color-schemes/atom-one.css create mode 100644 static/css/code/syntax-highlight.css create mode 100644 static/css/fonts.css create mode 100644 static/css/layout.css create mode 100644 static/css/pages/everything.css create mode 100644 static/css/pages/lesson.css create mode 100644 static/css/root.css create mode 100644 static/css/theme.css create mode 100644 static/index.js create mode 100644 static/js/highlight/highlight-gleam.js create mode 100644 static/js/highlight/highlight.core.min.js create mode 100644 static/js/highlight/regexes.js create mode 100644 static/precompiled/my_package_ffi.mjs create mode 100644 static/worker.js create mode 100644 test/tour_test.gleam diff --git a/README.md b/README.md index dd80e54..45b5b93 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,13 @@ -# playground +# The Gleam Language Tour -[![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/) +An interactive tour of the Gleam programming language. ```sh -gleam add playground -``` -```gleam -import playground - -pub fn main() { - // TODO: An example of the project in use -} -``` +# Download a wasm version of the Gleam compiler +./bin/download-compiler -Further documentation can be found at . +# Build the site +gleam run -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell +# It's now all the in `public/` directory ``` diff --git a/bin/download-compiler b/bin/download-compiler new file mode 100755 index 0000000..552cf44 --- /dev/null +++ b/bin/download-compiler @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eu + +# Ensure you update the CI Gleam version to match this +VERSION="v1.1.0" + +rm -fr wasm-compiler +mkdir wasm-compiler +cd wasm-compiler +curl -L "https://github.com/gleam-lang/gleam/releases/download/$VERSION/gleam-$VERSION-browser.tar.gz" | tar xz +cd .. diff --git a/gleam.toml b/gleam.toml index 59d76e2..f75db55 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,6 @@ name = "playground" version = "1.0.0" +target = "javascript" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -14,6 +15,10 @@ version = "1.0.0" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +simplifile = "~> 1.0" +snag = "~> 0.2" +htmb = "~> 1.1" +filepath = "~> 0.1" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..23d8eb3 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,19 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "filepath", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "FC1B1B29438A5BA6C990F8047A011430BEC0C5BA638BFAA62718C4EAEFE00435" }, + { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, + { name = "htmb", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "htmb", source = "hex", outer_checksum = "30D448F0E15DFCF7283AAAC2F351D77B9D54E318219C9FDDB1877572B67C27B7" }, + { name = "simplifile", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C44DB387524F90DC42142699C78C850003289D32C7C99C7D32873792A299CDF7" }, + { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, +] + +[requirements] +filepath = { version = "~> 0.1" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +htmb = { version = "~> 1.1" } +simplifile = { version = "~> 1.0" } +snag = { version = "~> 0.2" } diff --git a/src/content/chapter0_basics/lesson01_hello_world/code.gleam b/src/content/chapter0_basics/lesson01_hello_world/code.gleam new file mode 100644 index 0000000..30530b2 --- /dev/null +++ b/src/content/chapter0_basics/lesson01_hello_world/code.gleam @@ -0,0 +1,5 @@ +import gleam/io + +pub fn main() { + io.println("Hello, Joe!") +} diff --git a/src/content/chapter0_basics/lesson01_hello_world/en.html b/src/content/chapter0_basics/lesson01_hello_world/en.html new file mode 100644 index 0000000..b555160 --- /dev/null +++ b/src/content/chapter0_basics/lesson01_hello_world/en.html @@ -0,0 +1,14 @@ +

+ Here is a tiny program that prints out the text "Hello, Joe!". We'll explain + how it works shortly. +

+

+ In a normal Gleam project this program would be run using the command + gleam run on the command line, but here in this tour the program + is compiled and run inside your web browser, allowing you to try Gleam without + installing anything on your computer. +

+

+ Try changing the text being printed to Hello, Mike! and see what + happens. +

diff --git a/src/content/chapter0_basics/lesson02_modules/code.gleam b/src/content/chapter0_basics/lesson02_modules/code.gleam new file mode 100644 index 0000000..e258c33 --- /dev/null +++ b/src/content/chapter0_basics/lesson02_modules/code.gleam @@ -0,0 +1,10 @@ +import gleam/io +import gleam/string as text + +pub fn main() { + // Use a function from the `gleam/io` module + io.println("Hello, Mike!") + + // Use a function from the `gleam/string` module + io.println(text.reverse("Hello, Joe!")) +} diff --git a/src/content/chapter0_basics/lesson02_modules/en.html b/src/content/chapter0_basics/lesson02_modules/en.html new file mode 100644 index 0000000..7a2d0ca --- /dev/null +++ b/src/content/chapter0_basics/lesson02_modules/en.html @@ -0,0 +1,37 @@ +

+ Gleam code is organized into units called modules. A module is a + bunch of definitions (of types, functions, etc.) that seem to belong together. + For example, the + + gleam/io + + module contains a variety of functions for printing, like + + println + . +

+

+ All gleam code is in some module or other, whose name comes from the + name of the file it's in. For example, + + gleam/io + + is in a file called io.gleam in a directory called gleam. +

+

+ For code in one module to access code in another module, we import it using + the import keyword, and the name used to refer to it is the last + part of the module name. For example, the + + gleam/io + + module is referred to as io once imported. +

+

+ The as keyword can be used to refer to a module by a different + name. See how the + + gleam/string + + module is referred to as text here. +

diff --git a/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam b/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam new file mode 100644 index 0000000..2708f25 --- /dev/null +++ b/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam @@ -0,0 +1,10 @@ +// Import the module and one of its functions +import gleam/io.{println} + +pub fn main() { + // Use the function in a qualified fashion + io.println("This is qualified") + + // Or an unqualified fashion + println("This is unqualified") +} diff --git a/src/content/chapter0_basics/lesson03_unqualified_imports/en.html b/src/content/chapter0_basics/lesson03_unqualified_imports/en.html new file mode 100644 index 0000000..35ea10f --- /dev/null +++ b/src/content/chapter0_basics/lesson03_unqualified_imports/en.html @@ -0,0 +1,17 @@ +

+ Normally functions from other modules are used in a + qualified fashion, meaning the name used to refer the module goes + before function name with a dot between them. For example, + + io.println("Hello!") + . +

+

+ It is also possible to specify a list of functions to import from a module in + an unqualified fashion, meaning the function name can be used without + the module qualifier (the name and the dot) before it. +

+

+ Generally it is best to use qualified imports, as this makes it clear where + the function is defined, making the code easier to read. +

diff --git a/src/content/chapter0_basics/lesson04_type_checking/code.gleam b/src/content/chapter0_basics/lesson04_type_checking/code.gleam new file mode 100644 index 0000000..e068f31 --- /dev/null +++ b/src/content/chapter0_basics/lesson04_type_checking/code.gleam @@ -0,0 +1,7 @@ +import gleam/io + +pub fn main() { + io.println("My lucky number is:") + // io.println(4) + // ๐Ÿ‘†๏ธ Uncomment this line +} diff --git a/src/content/chapter0_basics/lesson04_type_checking/en.html b/src/content/chapter0_basics/lesson04_type_checking/en.html new file mode 100644 index 0000000..0cffcbe --- /dev/null +++ b/src/content/chapter0_basics/lesson04_type_checking/en.html @@ -0,0 +1,28 @@ +

+ Gleam has a robust static type system that helps you as you write and edit + code, catching mistakes and showing you where to make changes. +

+

+ Uncomment the line + + io.println(4) + + and see how a compile time error + is reported as the + + io.println + + function only works with strings, not ints. +

+

+ To fix the code change the code to call the + + io.debug + + function instead, as it will print a value of any type. +

+

+ Gleam has no null, no implicit conversions, no exceptions, and + always performs full type checking. If the code compiles you can be reasonably + confident it does not have any inconsistencies that may cause bugs or crashes. +

diff --git a/src/content/chapter0_basics/lesson05_ints/code.gleam b/src/content/chapter0_basics/lesson05_ints/code.gleam new file mode 100644 index 0000000..120b342 --- /dev/null +++ b/src/content/chapter0_basics/lesson05_ints/code.gleam @@ -0,0 +1,25 @@ +import gleam/int +import gleam/io + +pub fn main() { + // Int arithmetic + io.debug(1 + 1) + io.debug(5 - 1) + io.debug(5 / 2) + io.debug(3 * 3) + io.debug(5 % 2) + + // Int comparisons + io.debug(2 > 1) + io.debug(2 < 1) + io.debug(2 >= 1) + io.debug(2 <= 1) + + // Equality works for any type + io.debug(1 == 1) + io.debug(2 == 1) + + // Standard library int functions + io.debug(int.max(42, 77)) + io.debug(int.clamp(5, 10, 20)) +} diff --git a/src/content/chapter0_basics/lesson05_ints/en.html b/src/content/chapter0_basics/lesson05_ints/en.html new file mode 100644 index 0000000..41793d5 --- /dev/null +++ b/src/content/chapter0_basics/lesson05_ints/en.html @@ -0,0 +1,17 @@ +

Gleam's Int type represents whole numbers.

+

+ There are arithmetic and comparison operators for ints, as well as the + equality operator which works on all types. +

+

+ When running on the Erlang virtual machine ints have no maximum and minimum + size. When running on JavaScript runtimes ints are represented using + JavaScript's 64 bit floating point numbers, +

+

+ The + + gleam/int + + standard library module contains functions for working with ints. +

diff --git a/src/content/chapter0_basics/lesson06_floats/code.gleam b/src/content/chapter0_basics/lesson06_floats/code.gleam new file mode 100644 index 0000000..b56eb3a --- /dev/null +++ b/src/content/chapter0_basics/lesson06_floats/code.gleam @@ -0,0 +1,27 @@ +import gleam/float +import gleam/io + +pub fn main() { + // Float arithmetic + io.debug(1.0 +. 1.5) + io.debug(5.0 -. 1.5) + io.debug(5.0 /. 2.5) + io.debug(3.0 *. 3.5) + + // Float comparisons + io.debug(2.2 >. 1.3) + io.debug(2.2 <. 1.3) + io.debug(2.2 >=. 1.3) + io.debug(2.2 <=. 1.3) + + // Equality works for any type + io.debug(1.1 == 1.1) + io.debug(2.1 == 1.2) + + // Division by zero is not an error + io.debug(3.14 /. 0.0) + + // Standard library float functions + io.debug(float.max(2.0, 9.5)) + io.debug(float.ceiling(5.4)) +} diff --git a/src/content/chapter0_basics/lesson06_floats/en.html b/src/content/chapter0_basics/lesson06_floats/en.html new file mode 100644 index 0000000..2b2c5b7 --- /dev/null +++ b/src/content/chapter0_basics/lesson06_floats/en.html @@ -0,0 +1,32 @@ +

Gleam's Float type represents numbers that are not integers.

+

+ Gleam's numerical operators are not overloaded, so there are dedicated + operators for working with floats. +

+

+ Floats are represented as 64 bit floating point numbers on both the Erlang and + JavaScript runtimes. The floating point behaviour is native to their + respective runtimes, so their exact behaviour will be slightly different + on the two runtimes. +

+

+ Under the JavaScript runtime, exceeding the maximum (or minimum) representable + value for a floating point value will result in Infinity (or + -Infinity). Should you try to divide two infinities you will + get NaN as a result. +

+

+ When running on the BEAM any overflow will raise an error. So there is + no NaN or Infinity float value in the Erlang + runtime. +

+

+ Division by zero will not overflow, but is instead defined to be zero. +

+

+ The + + gleam/float + + standard library module contains functions for working with floats. +

diff --git a/src/content/chapter0_basics/lesson07_number_formats/code.gleam b/src/content/chapter0_basics/lesson07_number_formats/code.gleam new file mode 100644 index 0000000..7307185 --- /dev/null +++ b/src/content/chapter0_basics/lesson07_number_formats/code.gleam @@ -0,0 +1,16 @@ +import gleam/io + +pub fn main() { + // Underscores + io.debug(1_000_000) + io.debug(10_000.01) + + // Binary, octal, and hex Int literals + io.debug(0b00001111) + io.debug(0o17) + io.debug(0xF) + + // Scientific notation Float literals + io.debug(7.0e7) + io.debug(3.0e-4) +} diff --git a/src/content/chapter0_basics/lesson07_number_formats/en.html b/src/content/chapter0_basics/lesson07_number_formats/en.html new file mode 100644 index 0000000..308219a --- /dev/null +++ b/src/content/chapter0_basics/lesson07_number_formats/en.html @@ -0,0 +1,13 @@ +

+ Underscores can be added to numbers for clarity. For example, + 1000000 can be tricky to read quickly, while + 1_000_000 can be easier. +

+

+ Ints can be written in binary, octal, or hexadecimal formats using the + 0b, 0o, and 0x prefixes respectively. +

+

+ Floats can be written in a scientific notation. +

+ diff --git a/src/content/chapter0_basics/lesson08_equality/code.gleam b/src/content/chapter0_basics/lesson08_equality/code.gleam new file mode 100644 index 0000000..70a2b89 --- /dev/null +++ b/src/content/chapter0_basics/lesson08_equality/code.gleam @@ -0,0 +1,6 @@ +import gleam/io + +pub fn main() { + io.debug(100 == 100) + io.debug(1.5 != 0.1) +} diff --git a/src/content/chapter0_basics/lesson08_equality/en.html b/src/content/chapter0_basics/lesson08_equality/en.html new file mode 100644 index 0000000..e8c2169 --- /dev/null +++ b/src/content/chapter0_basics/lesson08_equality/en.html @@ -0,0 +1,13 @@ +

+ Gleam has the == and != operators for checking + equality. +

+

+ The operators can be used with values of any type, but both sides of the + operator must be of the same type. +

+

+ Equality is checked structurally, meaning that two values are equal + if they have the same structure rather than if they are at the same memory + location. +

diff --git a/src/content/chapter0_basics/lesson09_strings/code.gleam b/src/content/chapter0_basics/lesson09_strings/code.gleam new file mode 100644 index 0000000..ea47e0f --- /dev/null +++ b/src/content/chapter0_basics/lesson09_strings/code.gleam @@ -0,0 +1,23 @@ +import gleam/io +import gleam/string + +pub fn main() { + // String literals + io.debug("๐Ÿ‘ฉโ€๐Ÿ’ป ใ“ใ‚“ใซใกใฏ Gleam ๐Ÿณ๏ธโ€๐ŸŒˆ") + io.debug( + "multi + line + string", + ) + io.debug("\u{1F600}") + + // Double quote can be escaped + io.println("\"X\" marks the spot") + + // String concatenation + io.debug("One " <> "Two") + + // String functions + io.debug(string.reverse("1 2 3 4 5")) + io.debug(string.append("abc", "def")) +} diff --git a/src/content/chapter0_basics/lesson09_strings/en.html b/src/content/chapter0_basics/lesson09_strings/en.html new file mode 100644 index 0000000..8e4b6f7 --- /dev/null +++ b/src/content/chapter0_basics/lesson09_strings/en.html @@ -0,0 +1,26 @@ +

+ In Gleam strings are written as text surrounded by double quotes, and + can span multiple lines and contain unicode characters. +

+

+ The <> operator can be used to concatenate strings. +

+

+ Several escape sequences are supported: +

+ +

+ The + + gleam/string + + standard library module contains functions for working with strings. +

diff --git a/src/content/chapter0_basics/lesson10_bools/code.gleam b/src/content/chapter0_basics/lesson10_bools/code.gleam new file mode 100644 index 0000000..aa25ba2 --- /dev/null +++ b/src/content/chapter0_basics/lesson10_bools/code.gleam @@ -0,0 +1,14 @@ +import gleam/bool +import gleam/io + +pub fn main() { + // Bool operators + io.debug(True && False) + io.debug(True && True) + io.debug(False || False) + io.debug(False || True) + + // Bool functions + io.debug(bool.to_string(True)) + io.debug(bool.to_int(False)) +} diff --git a/src/content/chapter0_basics/lesson10_bools/en.html b/src/content/chapter0_basics/lesson10_bools/en.html new file mode 100644 index 0000000..3cbf0d4 --- /dev/null +++ b/src/content/chapter0_basics/lesson10_bools/en.html @@ -0,0 +1,20 @@ +

+ A Bool is either True or False. +

+

+ The ||, &&, and ! operators can be used + to manipulate bools. +

+

+ The || and && operators are short-circuiting, + meaning that if the left hand side of the operator is True for + || or False for && then the right hand + side of the operator will not be evaluated. +

+

+ The + + gleam/bool + + standard library module contains functions for working with bools. +

diff --git a/src/content/chapter0_basics/lesson11_assignments/code.gleam b/src/content/chapter0_basics/lesson11_assignments/code.gleam new file mode 100644 index 0000000..a030e43 --- /dev/null +++ b/src/content/chapter0_basics/lesson11_assignments/code.gleam @@ -0,0 +1,17 @@ +import gleam/io + +pub fn main() { + let x = "Original" + io.debug(x) + + // Assign `y` to the value of `x` + let y = x + io.debug(y) + + // Assign `x` to a new value + let x = "New" + io.debug(x) + + // The `y` still refers to the original value + io.debug(y) +} diff --git a/src/content/chapter0_basics/lesson11_assignments/en.html b/src/content/chapter0_basics/lesson11_assignments/en.html new file mode 100644 index 0000000..d106fd9 --- /dev/null +++ b/src/content/chapter0_basics/lesson11_assignments/en.html @@ -0,0 +1,9 @@ +

A value can be assigned to a variable using let.

+

+ Variable names can be reused by later let bindings, but the values they + reference are immutable, so the values themselves are not changed or mutated + in any way. +

+

+ In Gleam variable and function names are written in snake_case. +

diff --git a/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam b/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam new file mode 100644 index 0000000..fa2c0e3 --- /dev/null +++ b/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam @@ -0,0 +1,4 @@ +pub fn main() { + // This variable is never used + let _score = 1000 +} diff --git a/src/content/chapter0_basics/lesson12_discard_patterns/en.html b/src/content/chapter0_basics/lesson12_discard_patterns/en.html new file mode 100644 index 0000000..91cfb42 --- /dev/null +++ b/src/content/chapter0_basics/lesson12_discard_patterns/en.html @@ -0,0 +1,10 @@ +

+ If a variable is assigned but not used then Gleam will emit a warning. +

+

+ If a variable is intended to not be used, then the name can be prefixed with an + underscore, silencing the warning. +

+

+ Try changing the variable name to score to see the warning. +

diff --git a/src/content/chapter0_basics/lesson13_type_annotations/code.gleam b/src/content/chapter0_basics/lesson13_type_annotations/code.gleam new file mode 100644 index 0000000..1299c2f --- /dev/null +++ b/src/content/chapter0_basics/lesson13_type_annotations/code.gleam @@ -0,0 +1,7 @@ +pub fn main() { + let _name: String = "Gleam" + + let _is_cool: Bool = True + + let _version: Int = 1 +} diff --git a/src/content/chapter0_basics/lesson13_type_annotations/en.html b/src/content/chapter0_basics/lesson13_type_annotations/en.html new file mode 100644 index 0000000..8738a15 --- /dev/null +++ b/src/content/chapter0_basics/lesson13_type_annotations/en.html @@ -0,0 +1,15 @@ +

+ Let assignments can be written with a type annotation after the name. +

+

+ Type annotations may be useful for documentation purposes, but they do not + change how Gleam type checks the code beyond ensuring that the annotation is + correct. +

+

+ Typically Gleam code will not have type annotations for assignments. +

+

+ Try changing a type annotation to something incorrect to see the compile + error. +

diff --git a/src/content/chapter0_basics/lesson14_type_imports/code.gleam b/src/content/chapter0_basics/lesson14_type_imports/code.gleam new file mode 100644 index 0000000..61f552a --- /dev/null +++ b/src/content/chapter0_basics/lesson14_type_imports/code.gleam @@ -0,0 +1,10 @@ +import gleam/bytes_builder +import gleam/string_builder.{type StringBuilder} + +pub fn main() { + // Referring to a type in a qualified way + let _bytes: bytes_builder.BytesBuilder = bytes_builder.new() + + // Refering to a type in an unqualified way + let _text: StringBuilder = string_builder.new() +} diff --git a/src/content/chapter0_basics/lesson14_type_imports/en.html b/src/content/chapter0_basics/lesson14_type_imports/en.html new file mode 100644 index 0000000..0f362e5 --- /dev/null +++ b/src/content/chapter0_basics/lesson14_type_imports/en.html @@ -0,0 +1,22 @@ +

+ Other modules may also define types that we wish to refer to. In this case we + need to import them. +

+

+ Like functions, types can be referred to in a qualified way by + putting the imported module name and a dot before the type name. For example, + + bytes_builder.BytesBuilder + +

+

+ Types can also be imported in an unqualified way by listing them in + the import statement with the word type before the type name. +

+

+ It is more common in Gleam code for types to be imported in an unqualified way + than it is for functions to be imported in an unqualified way. +

diff --git a/src/content/chapter0_basics/lesson15_type_aliases/code.gleam b/src/content/chapter0_basics/lesson15_type_aliases/code.gleam new file mode 100644 index 0000000..6125ffe --- /dev/null +++ b/src/content/chapter0_basics/lesson15_type_aliases/code.gleam @@ -0,0 +1,12 @@ +import gleam/io + +pub type UserId = + Int + +pub fn main() { + let one: UserId = 1 + let two: Int = 2 + + // UserId and Int are the same type + io.debug(one == two) +} diff --git a/src/content/chapter0_basics/lesson15_type_aliases/en.html b/src/content/chapter0_basics/lesson15_type_aliases/en.html new file mode 100644 index 0000000..9fddd70 --- /dev/null +++ b/src/content/chapter0_basics/lesson15_type_aliases/en.html @@ -0,0 +1,12 @@ +

+ A type alias can be used to refer to a type by a different name. Giving a type + an alias doesn't make a new type, it is still the same type. +

+

+ A type's name always starts with a capital letter, contrasting to variables + and functions, which start with a lowercase letter. +

+

+ When the pub keyword is used the type alias is public and can be + referred to by other modules. +

diff --git a/src/content/chapter0_basics/lesson16_blocks/code.gleam b/src/content/chapter0_basics/lesson16_blocks/code.gleam new file mode 100644 index 0000000..31e4729 --- /dev/null +++ b/src/content/chapter0_basics/lesson16_blocks/code.gleam @@ -0,0 +1,13 @@ +import gleam/io + +pub fn main() { + let fahrenheit = { + let degrees = 64 + degrees + } + // io.debug(degrees) // <- This will not compile + + // Changing order of evaluation + let celsius = { fahrenheit - 32 } * 5 / 9 + io.debug(celsius) +} diff --git a/src/content/chapter0_basics/lesson16_blocks/en.html b/src/content/chapter0_basics/lesson16_blocks/en.html new file mode 100644 index 0000000..b19c586 --- /dev/null +++ b/src/content/chapter0_basics/lesson16_blocks/en.html @@ -0,0 +1,26 @@ +

+ Blocks are one or more expressions grouped together with curly braces. Each + expression is evaluated in order and the value of the last expression is + returned. +

+

+ Any variables assigned within the block can only be used within the block. +

+

+ Try uncommenting + + io.debug(degrees) + + to see the compile error from trying to use a variable that is not in scope. +

+

+ Blocks can also be used to change the order of evaluation of binary operators + expressions. +

+

+ * binds more tightly than + so the expression + 1 + 2 * 3 evaluates to 7. If the 1 + 2 should be + evaluated first to make the expression evaluate to 9 then the expression can be + wrapped in a block: { 1 + 2 } * 3. This is similar to grouping + with parentheses in some other languages. +

diff --git a/src/content/chapter0_basics/lesson17_lists/code.gleam b/src/content/chapter0_basics/lesson17_lists/code.gleam new file mode 100644 index 0000000..646ad6e --- /dev/null +++ b/src/content/chapter0_basics/lesson17_lists/code.gleam @@ -0,0 +1,16 @@ +import gleam/io + +pub fn main() { + let ints = [1, 2, 3] + + io.debug(ints) + + // Immutably prepend + io.debug([-1, 0, ..ints]) + + // Uncomment this to see the error + // io.debug(["zero", ..ints]) + + // The original lists are unchanged + io.debug(ints) +} diff --git a/src/content/chapter0_basics/lesson17_lists/en.html b/src/content/chapter0_basics/lesson17_lists/en.html new file mode 100644 index 0000000..84f88f3 --- /dev/null +++ b/src/content/chapter0_basics/lesson17_lists/en.html @@ -0,0 +1,21 @@ +

+ Lists are ordered collections of values. +

+

+ + List + + is a generic type, having a type parameter for the type of values it contains. + A list of ints has the type List(Int), and a list of strings has the type + List(String). +

+

+ Lists are immutable single-linked lists, meaning they are very efficient to + add and remove elements from the front of the list. +

+

+ Counting the length of a list or getting elements from other positions in the + list is expensive and rarely done. It is rare to write algorithms that index + into sequences in Gleam, but when they are written a list is not the right + choice of data structure. +

diff --git a/src/content/chapter0_basics/lesson18_constants/code.gleam b/src/content/chapter0_basics/lesson18_constants/code.gleam new file mode 100644 index 0000000..aed6fb0 --- /dev/null +++ b/src/content/chapter0_basics/lesson18_constants/code.gleam @@ -0,0 +1,13 @@ +import gleam/io + +const ints: List(Int) = [1, 2, 3] + +const floats = [1.0, 2.0, 3.0] + +pub fn main() { + io.debug(ints) + io.debug(ints == [1, 2, 3]) + + io.debug(floats) + io.debug(floats == [1.0, 2.0, 3.0]) +} diff --git a/src/content/chapter0_basics/lesson18_constants/en.html b/src/content/chapter0_basics/lesson18_constants/en.html new file mode 100644 index 0000000..f610dcd --- /dev/null +++ b/src/content/chapter0_basics/lesson18_constants/en.html @@ -0,0 +1,18 @@ +

+ As well as let assignments Gleam also has constants, which are defined at the + top level of a module. +

+

+ Constants must be literal values, functions cannot be used in their + definitions. +

+

+ Constants may be useful for values that are used throughout your program, + permitting them to be named and to ensure there are no differences in the + definition between each use. +

+

+ Using a constant may be more efficient than creating the same value in + multiple functions, though the exact performance characteristics will depend + on the runtime and whether compiling to Erlang or JavaScript. +

diff --git a/src/content/chapter1_functions/lesson00_functions/code.gleam b/src/content/chapter1_functions/lesson00_functions/code.gleam new file mode 100644 index 0000000..220e58d --- /dev/null +++ b/src/content/chapter1_functions/lesson00_functions/code.gleam @@ -0,0 +1,13 @@ +import gleam/io + +pub fn main() { + io.debug(double(10)) +} + +fn double(a: Int) -> Int { + multiply(a, 2) +} + +fn multiply(a: Int, b: Int) -> Int { + a * b +} diff --git a/src/content/chapter1_functions/lesson00_functions/en.html b/src/content/chapter1_functions/lesson00_functions/en.html new file mode 100644 index 0000000..32d5bed --- /dev/null +++ b/src/content/chapter1_functions/lesson00_functions/en.html @@ -0,0 +1,14 @@ +

+ The fn keyword is used to define new functions. +

+

+ The double and multiply functions are defined + without the pub keyword. This makes them private + functions, they can only be used within this module. If another module + attempted to use them it would result in a compiler error. +

+

+ Like with assignments, type annotations are optional for function arguments + and return values. It is considered good practice to use type annotations for + functions, for clarity and to encourage intentional and thoughtful design. +

diff --git a/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam b/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam new file mode 100644 index 0000000..43b6ca4 --- /dev/null +++ b/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam @@ -0,0 +1,18 @@ +import gleam/io + +pub fn main() { + // Call a function with another function + io.debug(twice(1, add_one)) + + // Functions can be assigned to variables + let my_function = add_one + io.debug(my_function(100)) +} + +fn twice(argument: Int, passed_function: fn(Int) -> Int) -> Int { + passed_function(passed_function(argument)) +} + +fn add_one(argument: Int) -> Int { + argument + 1 +} diff --git a/src/content/chapter1_functions/lesson03_higher_order_functions/en.html b/src/content/chapter1_functions/lesson03_higher_order_functions/en.html new file mode 100644 index 0000000..3343e4d --- /dev/null +++ b/src/content/chapter1_functions/lesson03_higher_order_functions/en.html @@ -0,0 +1,12 @@ +

+ In Gleam functions are values. They can be assigned to variables, passed to + other functions, and anything else you can do with values. +

+

+ Here the function add_one is being passed as an argument to the + twice function. +

+

+ Notice the fn keyword is also used to describe the type of the + function that twice takes as its second argument. +

diff --git a/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam b/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam new file mode 100644 index 0000000..df7a6f9 --- /dev/null +++ b/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam @@ -0,0 +1,19 @@ +import gleam/io + +pub fn main() { + // Assign an anonymous function to a variable + let add_one = fn(a) { a + 1 } + io.debug(twice(1, add_one)) + + // Pass an anonymous function as an argument + io.debug(twice(1, fn(a) { a * 2 })) + + let secret_number = 42 + // This anonymous function always returns 42 + let secret = fn() { secret_number } + io.debug(secret()) +} + +fn twice(argument: Int, my_function: fn(Int) -> Int) -> Int { + my_function(my_function(argument)) +} diff --git a/src/content/chapter1_functions/lesson04_anonymous_functions/en.html b/src/content/chapter1_functions/lesson04_anonymous_functions/en.html new file mode 100644 index 0000000..93e11a9 --- /dev/null +++ b/src/content/chapter1_functions/lesson04_anonymous_functions/en.html @@ -0,0 +1,9 @@ +

+ As well as module-level named functions, Gleam has anonymous function + literals, written with the fn() { ... } syntax. +

+

Anonymous functions can be used interchangeably with named functions.

+

+ Anonymous functions can reference variables that were in scope when they were + defined, making them closures. +

diff --git a/src/content/chapter1_functions/lesson05_function_captures/code.gleam b/src/content/chapter1_functions/lesson05_function_captures/code.gleam new file mode 100644 index 0000000..35f3412 --- /dev/null +++ b/src/content/chapter1_functions/lesson05_function_captures/code.gleam @@ -0,0 +1,14 @@ +import gleam/io + +pub fn main() { + // These two statements are equivalent + let add_one_v1 = fn(x) { add(1, x) } + let add_one_v2 = add(1, _) + + io.debug(add_one_v1(10)) + io.debug(add_one_v2(10)) +} + +fn add(a: Int, b: Int) -> Int { + a + b +} diff --git a/src/content/chapter1_functions/lesson05_function_captures/en.html b/src/content/chapter1_functions/lesson05_function_captures/en.html new file mode 100644 index 0000000..4c86e27 --- /dev/null +++ b/src/content/chapter1_functions/lesson05_function_captures/en.html @@ -0,0 +1,12 @@ +

+ Gleam has a shorthand syntax for creating anonymous functions that take one + argument and immediately call another function with that argument: the + function capture syntax. +

+

+ The anonymous function fn(a) { some_function(..., a, ...) } can + be written as some_function(..., _, ...), with any number of + other arguments passed directly to the inner function. The underscore + _ is a placeholder for the argument, equivalent to + a in fn(a) { some_function(..., a, ...) }. +

diff --git a/src/content/chapter1_functions/lesson06_generic_functions/code.gleam b/src/content/chapter1_functions/lesson06_generic_functions/code.gleam new file mode 100644 index 0000000..db53efe --- /dev/null +++ b/src/content/chapter1_functions/lesson06_generic_functions/code.gleam @@ -0,0 +1,20 @@ +import gleam/io + +pub fn main() { + let add_one = fn(x) { x + 1 } + let exclaim = fn(x) { x <> "!" } + + // Invalid, Int and String are not the same type + // twice(10, exclaim) + + // Here the type variable is replaced by the type Int + io.debug(twice(10, add_one)) + + // Here the type variable is replaced by the type String + io.debug(twice("Hello", exclaim)) +} + +// The name `value` refers to the same type multiple times +fn twice(argument: value, my_function: fn(value) -> value) -> value { + my_function(my_function(argument)) +} diff --git a/src/content/chapter1_functions/lesson06_generic_functions/en.html b/src/content/chapter1_functions/lesson06_generic_functions/en.html new file mode 100644 index 0000000..d5abb55 --- /dev/null +++ b/src/content/chapter1_functions/lesson06_generic_functions/en.html @@ -0,0 +1,26 @@ +

+ Up until now each function has accepted precisely one type for each of its + arguments. +

+

+ The twice function in the previous lesson on + higher order functions only worked with functions that would take and + return ints. This is overly restrictive, it should be possible to use this + function with any type, so long as the function and the initial value are + compatible. +

+

+ To enable this, Gleam supports generics, also known as + parametric polymorphism. +

+

+ This works by using a type variable instead of specifying a concrete type. It + stands in for whatever specific type is being used when the function is + called. These type variables are written with a lowercase name. +

+

+ Type variables are not like an any type, they get replaced with a + specific type each time the function is called. Try uncommenting + twice(10, exclaim) to see the compiler error from trying to use a + type variable as an int and a string at the same time. +

diff --git a/src/content/chapter1_functions/lesson07_pipelines/code.gleam b/src/content/chapter1_functions/lesson07_pipelines/code.gleam new file mode 100644 index 0000000..ec9b805 --- /dev/null +++ b/src/content/chapter1_functions/lesson07_pipelines/code.gleam @@ -0,0 +1,19 @@ +import gleam/io +import gleam/string + +pub fn main() { + // Without the pipe operator + io.debug(string.drop_left(string.drop_right("Hello, Joe!", 1), 7)) + + // With the pipe operator + "Hello, Mike!" + |> string.drop_right(1) + |> string.drop_left(7) + |> io.debug + + // Changing order with function capturing + "1" + |> string.append("2") + |> string.append("3", _) + |> io.debug +} diff --git a/src/content/chapter1_functions/lesson07_pipelines/en.html b/src/content/chapter1_functions/lesson07_pipelines/en.html new file mode 100644 index 0000000..783ade9 --- /dev/null +++ b/src/content/chapter1_functions/lesson07_pipelines/en.html @@ -0,0 +1,25 @@ +

+ It's common to want to call a series of functions, passing the result of one + to the next. With the regular function call syntax this can be a little + difficult to read as you have to read the code from the inside out. +

+

+ Gleam's pipe operator |> helps with this problem by allowing you + to write code top-to-bottom. +

+

+ The pipe operator takes the result of the expression on its left and passes it + as an argument to the function on its right. +

+

+ It will first check to see if the left-hand value could be used as the first + argument to the call. For example, a |> b(1, 2) would become + b(a, 1, 2). If not, it falls back to calling the result of the + right-hand side as a function, e.g., b(1, 2)(a) +

+

+ Gleam code is typically written with the "subject" of the function as the + first argument, to make it easier to pipe. If you wish to pipe to a different + position then a function capture can be used to insert the argument to the + desired position. +

diff --git a/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam b/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam new file mode 100644 index 0000000..25bb8c1 --- /dev/null +++ b/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam @@ -0,0 +1,16 @@ +import gleam/io + +pub fn main() { + // Without using labels + io.debug(calculate(1, 2, 3)) + + // Using the labels + io.debug(calculate(1, add: 2, multiply: 3)) + + // Using the labels in a different order + io.debug(calculate(1, multiply: 3, add: 2)) +} + +fn calculate(value: Int, add addend: Int, multiply multiplier: Int) { + value * multiplier + addend +} diff --git a/src/content/chapter1_functions/lesson08_labelled_arguments/en.html b/src/content/chapter1_functions/lesson08_labelled_arguments/en.html new file mode 100644 index 0000000..b1d771c --- /dev/null +++ b/src/content/chapter1_functions/lesson08_labelled_arguments/en.html @@ -0,0 +1,23 @@ +

+ When functions take several arguments it can be difficult to remember what the + arguments are, and what order they are expected in. +

+

+ To help with this Gleam supports labelled arguments, where function arguments + are given an external label in addition to their internal name. These labels + are written before the argument name in the function definition. +

+

+ When labelled arguments are used the order of the arguments does not matter, + but all unlabelled arguments must come before labelled arguments. +

+

+ There is no performance cost to using labelled arguments, it does not allocate + a dictionary or perform any other runtime work. +

+

+ Labels are optional when calling a function, it is up to the programmer to + decide what is clearest in their code. +

+ + diff --git a/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam b/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam new file mode 100644 index 0000000..a29875f --- /dev/null +++ b/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam @@ -0,0 +1,19 @@ +//// A module containing some unusual functions and types. + +/// A type where the value can never be constructed. +/// Can you work out why? +pub type Never { + Never(Never) +} + +/// Call a function twice with an initial value. +/// +pub fn twice(argument: value, my_function: fn(value) -> value) -> value { + my_function(my_function(argument)) +} + +/// Call a function three times with an initial value. +/// +pub fn thrice(argument: value, my_function: fn(value) -> value) -> value { + my_function(my_function(my_function(argument))) +} diff --git a/src/content/chapter1_functions/lesson09_documentation_comments/en.html b/src/content/chapter1_functions/lesson09_documentation_comments/en.html new file mode 100644 index 0000000..c27bac6 --- /dev/null +++ b/src/content/chapter1_functions/lesson09_documentation_comments/en.html @@ -0,0 +1,16 @@ +

+ Documentation and comments are important tools for making your code easier to + work with and understand. +

+

+ As well as regular // comments Gleam has /// and + //// comments which are used for attaching documentation to code. +

+

+ /// is used for documenting types and functions, and should be + placed immediately before the type or function it is documenting. +

+

+ //// is used for documenting modules, and should be placed + at the top of the module. +

diff --git a/src/content/chapter1_functions/lesson10_deprecations/code.gleam b/src/content/chapter1_functions/lesson10_deprecations/code.gleam new file mode 100644 index 0000000..26a8f0b --- /dev/null +++ b/src/content/chapter1_functions/lesson10_deprecations/code.gleam @@ -0,0 +1,13 @@ +pub fn main() { + old_function() + new_function() +} + +@deprecated("Use new_function instead") +fn old_function() { + Nil +} + +fn new_function() { + Nil +} diff --git a/src/content/chapter1_functions/lesson10_deprecations/en.html b/src/content/chapter1_functions/lesson10_deprecations/en.html new file mode 100644 index 0000000..b181597 --- /dev/null +++ b/src/content/chapter1_functions/lesson10_deprecations/en.html @@ -0,0 +1,14 @@ +

+ Functions and other definitions can be marked as deprecated using the + @deprecated + attribute. +

+

+ If a deprecated function is referenced the compiler will emit a warning, + letting the programmer know they ought to update their code. +

+

+ The deprecation attribute takes a message and this will be displayed to the + user in the warning. In the message explain to the user the new approach or + replacement function, or direct them to documentation on how to upgrade. +

diff --git a/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam b/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam new file mode 100644 index 0000000..d198832 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam @@ -0,0 +1,17 @@ +import gleam/int +import gleam/io + +pub fn main() { + let x = int.random(5) + io.debug(x) + + let result = case x { + // Match specific values + 0 -> "Zero" + 1 -> "One" + + // Match any other value + _ -> "Other" + } + io.debug(result) +} diff --git a/src/content/chapter2_flow_control/lesson01_case_expressions/en.html b/src/content/chapter2_flow_control/lesson01_case_expressions/en.html new file mode 100644 index 0000000..07ff421 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson01_case_expressions/en.html @@ -0,0 +1,18 @@ +

+ The case expression is the most common kind of flow control in Gleam code. It + is similar to switch in some other languages, but more powerful + than most. +

+

+ It allows the programmer to say "if the data has this shape then run this + code", a process called pattern matching. +

+

+ Gleam performs exhaustiveness checking to ensure that the patterns in + a case expression cover all possible values. With this you can have confidence + that your logic is up-to-date for the design of the data you are working with. +

+

+ Try commenting out patterns or adding new redundant ones, and see what + problems the compiler reports. +

diff --git a/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam b/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam new file mode 100644 index 0000000..5bce937 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam @@ -0,0 +1,14 @@ +import gleam/int +import gleam/io + +pub fn main() { + let result = case int.random(5) { + // Match specific values + 0 -> "Zero" + 1 -> "One" + + // Match any other value and assign it to a variable + other -> "It is " <> int.to_string(other) + } + io.debug(result) +} diff --git a/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html b/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html new file mode 100644 index 0000000..7e9ac11 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html @@ -0,0 +1,7 @@ +

+ Patterns in case expressions can also assign variables. +

+

+ When a variable name is used in a pattern the value that is matched against is + assigned to that name, and can be used in the body of that clause. +

diff --git a/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam b/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam new file mode 100644 index 0000000..d1441a0 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam @@ -0,0 +1,14 @@ +import gleam/io + +pub fn main() { + io.debug(get_name("Hello, Joe")) + io.debug(get_name("Hello, Mike")) + io.debug(get_name("System still working?")) +} + +fn get_name(x: String) -> String { + case x { + "Hello, " <> name -> name + _ -> "Unknown" + } +} diff --git a/src/content/chapter2_flow_control/lesson03_string_patterns/en.html b/src/content/chapter2_flow_control/lesson03_string_patterns/en.html new file mode 100644 index 0000000..11e4c35 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson03_string_patterns/en.html @@ -0,0 +1,9 @@ +

+ When pattern matching on strings the <> operator can be + used to match on strings with a specific prefix. +

+

+ The pattern "hello " <> name matches any string that starts with + "hello " and assigns the rest of the string to the variable + name. +

diff --git a/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam b/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam new file mode 100644 index 0000000..1b71feb --- /dev/null +++ b/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam @@ -0,0 +1,17 @@ +import gleam/int +import gleam/io +import gleam/list + +pub fn main() { + let x = list.repeat(int.random(5), times: int.random(3)) + io.debug(x) + + let result = case x { + [] -> "Empty list" + [1] -> "List of just 1" + [4, ..] -> "List starting with 4" + [_, _] -> "List of 2 elements" + _ -> "Some other list" + } + io.debug(result) +} diff --git a/src/content/chapter2_flow_control/lesson04_list_patterns/en.html b/src/content/chapter2_flow_control/lesson04_list_patterns/en.html new file mode 100644 index 0000000..de55eef --- /dev/null +++ b/src/content/chapter2_flow_control/lesson04_list_patterns/en.html @@ -0,0 +1,15 @@ +

+ Lists and the values they contain can be pattern matched on in case + expressions. +

+

+ List patterns match on specific lengths of lists. The pattern [] + matches an empty list, and the pattern [_] matches a list with + one element. They will not match on lists with other lengths. +

+

+ The spread pattern .. can be used to match the rest of the list. + The pattern [1, ..] matches any list that starts with + 1. The pattern [_, _, ..] matches any list that has + at least two elements. +

diff --git a/src/content/chapter2_flow_control/lesson05_recursion/code.gleam b/src/content/chapter2_flow_control/lesson05_recursion/code.gleam new file mode 100644 index 0000000..10d45ab --- /dev/null +++ b/src/content/chapter2_flow_control/lesson05_recursion/code.gleam @@ -0,0 +1,18 @@ +import gleam/io + +pub fn main() { + io.debug(factorial(5)) + io.debug(factorial(7)) +} + +// A recursive functions that calculates factorial +pub fn factorial(x: Int) -> Int { + case x { + // Base case + 0 -> 1 + 1 -> 1 + + // Recursive case + _ -> x * factorial(x - 1) + } +} diff --git a/src/content/chapter2_flow_control/lesson05_recursion/en.html b/src/content/chapter2_flow_control/lesson05_recursion/en.html new file mode 100644 index 0000000..f1585bb --- /dev/null +++ b/src/content/chapter2_flow_control/lesson05_recursion/en.html @@ -0,0 +1,20 @@ +

+ Gleam doesn't have loops, instead iteration is done through recursion, that is + through top-level functions calling themselves with different arguments. +

+

+ A recursive function needs to have at least one base case and at + least one recursive case. A base case returns a value without calling + the function again. A recursive case calls the function again with different + inputs, looping again. +

+

+ The Gleam standard library has functions for various common looping patterns, + some of which will be introduced in later lessons, however for more complex + loops manual recursion is often the clearest way to write it. +

+

+ Recursion can seem daunting or unclear at first if you are more familiar with + languages that have special looping features, but stick with it! With time + it'll become just as familiar and comfortable as any other way of iterating. +

diff --git a/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam b/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam new file mode 100644 index 0000000..b0a416c --- /dev/null +++ b/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam @@ -0,0 +1,22 @@ +import gleam/io + +pub fn main() { + io.debug(factorial(5)) + io.debug(factorial(7)) +} + +pub fn factorial(x: Int) -> Int { + // The public function calls the private tail recursive function + factorial_loop(x, 1) +} + +fn factorial_loop(x: Int, accumulator: Int) -> Int { + case x { + 0 -> accumulator + 1 -> accumulator + + // The last thing this function does is call itself + // In the previous lesson the last thing it did was multiply two ints + _ -> factorial_loop(x - 1, accumulator * x) + } +} diff --git a/src/content/chapter2_flow_control/lesson06_tail_calls/en.html b/src/content/chapter2_flow_control/lesson06_tail_calls/en.html new file mode 100644 index 0000000..ec39cda --- /dev/null +++ b/src/content/chapter2_flow_control/lesson06_tail_calls/en.html @@ -0,0 +1,23 @@ +

+ When a function is called a new stack frame is created in memory to store the + arguments and local variables of the function. If lots of these frames are + created during recursion then the program would use a large amount of memory, + or even crash the program if some limit is hit. +

+

+ To avoid this problem Gleam supports tail call optimisation, which + allows the compiler to reuse the stack frame for the current function if a + function call is the last thing the function does, removing the memory cost. +

+ +

+ Unoptimised recursive functions can often be rewritten into tail call + optimised functions by using an accumulator. An accumulator is a variable that + is passed along in addition to the data, similar to a mutable variable in a + language with while loops. +

+

+ Accumulators should be hidden away from the users of your code, they are + internal implementation details. To do this write a public function that calls + a recursive private function with the initial accumulator value. +

diff --git a/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam b/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam new file mode 100644 index 0000000..370675a --- /dev/null +++ b/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam @@ -0,0 +1,13 @@ +import gleam/io + +pub fn main() { + let sum = sum_list([18, 56, 35, 85, 91], 0) + io.debug(sum) +} + +fn sum_list(list: List(Int), total: Int) -> Int { + case list { + [first, ..rest] -> sum_list(rest, total + first) + [] -> total + } +} diff --git a/src/content/chapter2_flow_control/lesson07_list_recursion/en.html b/src/content/chapter2_flow_control/lesson07_list_recursion/en.html new file mode 100644 index 0000000..8a3a7d3 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson07_list_recursion/en.html @@ -0,0 +1,19 @@ +

+ While it is more common to use functions in the + + gleam/list + + module to iterate across a list, at times you may prefer to work + with the list directly. +

+

+ The [first, ..rest] pattern matches on a list with at least one + element, assigning the first element to the variable first and + the rest of the list to the variable rest. By using this pattern + and a pattern for the empty list [] a function can run code on + each element of a list until the end is reached. +

+

+ This code sums a list by recursing over the list and adding each int to a + total argument, returning it when the end is reached. +

diff --git a/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam b/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam new file mode 100644 index 0000000..38885b6 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam @@ -0,0 +1,17 @@ +import gleam/int +import gleam/io + +pub fn main() { + let x = int.random(2) + let y = int.random(2) + io.debug(x) + io.debug(y) + + let result = case x, y { + 0, 0 -> "Both are zero" + 0, _ -> "First is zero" + _, 0 -> "Second is zero" + _, _ -> "Neither are zero" + } + io.debug(result) +} diff --git a/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html b/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html new file mode 100644 index 0000000..1784e99 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html @@ -0,0 +1,13 @@ +

+ Sometimes it is useful to pattern match on multiple values at the same time in + one case expression. +

+

+ To do this, you can give multiple subjects and multiple patterns, separated by + commas. +

+

+ When matching on multiple subjects there must be the same number of patterns + as there are subjects. Try removing one of the _, sub-patterns to + see the compile time error that is returned. +

diff --git a/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam b/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam new file mode 100644 index 0000000..e916a03 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam @@ -0,0 +1,14 @@ +import gleam/int +import gleam/io + +pub fn main() { + let number = int.random(10) + io.debug(number) + + let result = case number { + 2 | 4 | 6 | 8 -> "This is an even number" + 1 | 3 | 5 | 7 -> "This is an odd number" + _ -> "I'm not sure" + } + io.debug(result) +} diff --git a/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html b/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html new file mode 100644 index 0000000..25421f4 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html @@ -0,0 +1,12 @@ +

+ Alternative patterns can be given for a case clause using the + | operator. If any of the patterns match then the clause matches. +

+

+ If a pattern defines a variable then all of the alternative patterns for that + clause must also define a variable with the same name and same type. +

+

+ Currently it is not possible to have nested alternative patterns, so the + pattern [1 | 2 | 3] is not valid. +

diff --git a/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam b/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam new file mode 100644 index 0000000..ee40a26 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam @@ -0,0 +1,15 @@ +import gleam/io + +pub fn main() { + io.debug(get_first_non_empty([[], [1, 2, 3], [4, 5]])) + io.debug(get_first_non_empty([[1, 2], [3, 4, 5], []])) + io.debug(get_first_non_empty([[], [], []])) +} + +fn get_first_non_empty(lists: List(List(t))) -> List(t) { + case lists { + [[_, ..] as first, ..] -> first + [_, ..rest] -> get_first_non_empty(rest) + [] -> [] + } +} diff --git a/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html b/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html new file mode 100644 index 0000000..37a1276 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html @@ -0,0 +1,7 @@ +

+ The as operator can be used to assign sub patterns to variables. +

+

+ The pattern [_, ..] as first will match any non-empty list and + assign that list to the variable first. +

diff --git a/src/content/chapter2_flow_control/lesson11_guards/code.gleam b/src/content/chapter2_flow_control/lesson11_guards/code.gleam new file mode 100644 index 0000000..3744228 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson11_guards/code.gleam @@ -0,0 +1,15 @@ +import gleam/io + +pub fn main() { + let numbers = [1, 2, 3, 4, 5] + io.debug(get_first_larger(numbers, 3)) + io.debug(get_first_larger(numbers, 5)) +} + +fn get_first_larger(lists: List(Int), limit: Int) -> Int { + case lists { + [first, ..] if first > limit -> first + [_, ..rest] -> get_first_larger(rest, limit) + [] -> 0 + } +} diff --git a/src/content/chapter2_flow_control/lesson11_guards/en.html b/src/content/chapter2_flow_control/lesson11_guards/en.html new file mode 100644 index 0000000..3ea9cc5 --- /dev/null +++ b/src/content/chapter2_flow_control/lesson11_guards/en.html @@ -0,0 +1,9 @@ +

+ The if keyword can be used with case expressions to add a + guard to a pattern. A guard is an expression that must evaluate to + True for the pattern to match. +

+

+ Only a limited set of operators can be used in guards, and functions cannot be + called at all. +

diff --git a/src/content/chapter3_data_types/lesson00_tuples/code.gleam b/src/content/chapter3_data_types/lesson00_tuples/code.gleam new file mode 100644 index 0000000..d5c6313 --- /dev/null +++ b/src/content/chapter3_data_types/lesson00_tuples/code.gleam @@ -0,0 +1,10 @@ +import gleam/io + +pub fn main() { + let triple = #(1, 2.2, "three") + io.debug(triple) + + let #(a, _, _) = triple + io.debug(a) + io.debug(triple.1) +} diff --git a/src/content/chapter3_data_types/lesson00_tuples/en.html b/src/content/chapter3_data_types/lesson00_tuples/en.html new file mode 100644 index 0000000..359e93a --- /dev/null +++ b/src/content/chapter3_data_types/lesson00_tuples/en.html @@ -0,0 +1,20 @@ +

+ Lists are good for when we want a collection of one type, but sometimes we + want to combine multiple values of different types. In this case tuples are a + quick and convenient option. +

+

+ The tuple access syntax can be used to get elements from a tuple without + pattern matching. some_tuple.0 gets the first element, + some_tuple.1 gets the second element, etc. +

+

+ Tuples are generic types, they have type parameters for the types they + contain. #(1, "Hi!") has the type #(Int, String), + and #(1.4, 10, 48) has the type #(Float, Int, Int). +

+

+ Tuples are most commonly used to return 2 or 3 values from a function. Often + it is clearer to use a custom type where a tuple could be used, We + will cover custom types next. +

diff --git a/src/content/chapter3_data_types/lesson01_custom_types/code.gleam b/src/content/chapter3_data_types/lesson01_custom_types/code.gleam new file mode 100644 index 0000000..35629bf --- /dev/null +++ b/src/content/chapter3_data_types/lesson01_custom_types/code.gleam @@ -0,0 +1,22 @@ +import gleam/io + +pub type Season { + Spring + Summer + Autumn + Winter +} + +pub fn main() { + io.debug(weather(Spring)) + io.debug(weather(Autumn)) +} + +fn weather(season: Season) -> String { + case season { + Spring -> "Mild" + Summer -> "Hot" + Autumn -> "Windy" + Winter -> "Cold" + } +} diff --git a/src/content/chapter3_data_types/lesson01_custom_types/en.html b/src/content/chapter3_data_types/lesson01_custom_types/en.html new file mode 100644 index 0000000..3ca1f66 --- /dev/null +++ b/src/content/chapter3_data_types/lesson01_custom_types/en.html @@ -0,0 +1,11 @@ +

+ Gleam has a few built in types such as Int and + String, but custom types allow the creation of entirely new + types. +

+

+ A custom type is defined with the type keyword followed by the + name of the type and a constructor for each variant of the type. Both + the type name and the names of the constructors start with uppercase letters. +

+

Custom type variants can be pattern matched on using a case expression.

diff --git a/src/content/chapter3_data_types/lesson02_records/code.gleam b/src/content/chapter3_data_types/lesson02_records/code.gleam new file mode 100644 index 0000000..bd6da3c --- /dev/null +++ b/src/content/chapter3_data_types/lesson02_records/code.gleam @@ -0,0 +1,17 @@ +import gleam/io + +pub type SchoolPerson { + Teacher(name: String, subject: String) + Student(String) +} + +pub fn main() { + let teacher1 = Teacher("Mr Schofield", "Physics") + let teacher2 = Teacher(name: "Miss Percy", subject: "Physics") + let student1 = Student("Koushiar") + let student2 = Student("Naomi") + let student3 = Student("Shaheer") + + let school = [teacher1, teacher2, student1, student2, student3] + io.debug(school) +} diff --git a/src/content/chapter3_data_types/lesson02_records/en.html b/src/content/chapter3_data_types/lesson02_records/en.html new file mode 100644 index 0000000..ea46e4d --- /dev/null +++ b/src/content/chapter3_data_types/lesson02_records/en.html @@ -0,0 +1,13 @@ +

+ A variant of a custom type can hold other data within it. In this case + the variant is called a record. +

+

+ The fields of a record can be given labels, and like function argument labels they can + be optionally used when calling the record constructor. Typically labels will + be used for variants that define them. +

+

+ It is common to have a custom type with one variant that holds data, this is + the Gleam equivalent of a struct or object in other languages. +

diff --git a/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam b/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam new file mode 100644 index 0000000..63ca721 --- /dev/null +++ b/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam @@ -0,0 +1,15 @@ +import gleam/io + +pub type SchoolPerson { + Teacher(name: String, subject: String) + Student(name: String) +} + +pub fn main() { + let teacher = Teacher("Mr Schofield", "Physics") + let student = Student("Koushiar") + + io.debug(teacher.name) + io.debug(student.name) + // io.debug(teacher.subject) +} diff --git a/src/content/chapter3_data_types/lesson03_record_accessors/en.html b/src/content/chapter3_data_types/lesson03_record_accessors/en.html new file mode 100644 index 0000000..bb2473b --- /dev/null +++ b/src/content/chapter3_data_types/lesson03_record_accessors/en.html @@ -0,0 +1,18 @@ +

+ The record accessor syntax record.field_label can be used to get + contained values from a custom type record. +

+

+ The accessor syntax can only be used for fields with the same name that are in + the same position and have the same type for all variants of the custom type. +

+

+ The name field is in the first position and has type + String for all variants, so it can be accessed. +

+

+ The subject field is absent on the Student variant, + so it cannot be used on any variant of type SchoolPerson. + Uncomment the teacher.subject line to see the compile error from + trying to use this accessor. +

diff --git a/src/content/chapter3_data_types/lesson04_record_updates/code.gleam b/src/content/chapter3_data_types/lesson04_record_updates/code.gleam new file mode 100644 index 0000000..ed7b45b --- /dev/null +++ b/src/content/chapter3_data_types/lesson04_record_updates/code.gleam @@ -0,0 +1,15 @@ +import gleam/io + +pub type SchoolPerson { + Teacher(name: String, subject: String, floor: Int, room: Int) +} + +pub fn main() { + let teacher1 = Teacher(name: "Mr Dodd", subject: "ICT", floor: 2, room: 2) + + // Use the update syntax + let teacher2 = Teacher(..teacher1, subject: "PE", room: 6) + + io.debug(teacher1) + io.debug(teacher2) +} diff --git a/src/content/chapter3_data_types/lesson04_record_updates/en.html b/src/content/chapter3_data_types/lesson04_record_updates/en.html new file mode 100644 index 0000000..4a5cd3a --- /dev/null +++ b/src/content/chapter3_data_types/lesson04_record_updates/en.html @@ -0,0 +1,8 @@ +

+ The record update syntax can be used to create a new record from an existing + one of the same type, but with some fields changed. +

+

+ Gleam is an immutable language, so using the record update syntax does not + mutate or otherwise change the original record. +

diff --git a/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam b/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam new file mode 100644 index 0000000..7e34e99 --- /dev/null +++ b/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam @@ -0,0 +1,10 @@ +pub type Option(inner) { + Some(inner) + None +} + +// An option of string +pub const name: Option(String) = Some("Annah") + +// An option of int +pub const level: Option(Int) = Some(10) diff --git a/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html b/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html new file mode 100644 index 0000000..3d003d7 --- /dev/null +++ b/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html @@ -0,0 +1,12 @@ +

+ Like functions, custom types can also be generic, taking contained types as + parameters. +

+

+ Here a generic Option type is defined, which is used to represent + a value that is either present or absent. This type is quite useful! The + + gleam/option + + module defines it so you can use it in your Gleam projects. +

diff --git a/src/content/chapter3_data_types/lesson06_nil/code.gleam b/src/content/chapter3_data_types/lesson06_nil/code.gleam new file mode 100644 index 0000000..c28080b --- /dev/null +++ b/src/content/chapter3_data_types/lesson06_nil/code.gleam @@ -0,0 +1,11 @@ +import gleam/io + +pub fn main() { + let x = Nil + io.debug(x) + + // let y: List(String) = Nil + + let result = io.println("Hello!") + io.debug(result == Nil) +} diff --git a/src/content/chapter3_data_types/lesson06_nil/en.html b/src/content/chapter3_data_types/lesson06_nil/en.html new file mode 100644 index 0000000..9749180 --- /dev/null +++ b/src/content/chapter3_data_types/lesson06_nil/en.html @@ -0,0 +1,15 @@ +

+ Nil is Gleam's unit type. It is a value that is returned by + functions that have nothing else to return, as all functions must return + something. +

+

+ Nil is not a valid value of any other types. Therefore, values in + Gleam are not nullable. If the type of a value is Nil then it is + the value Nil. If it is some other type then the value is not + Nil. +

+

+ Uncomment the line that assigns Nil to a variable with an + incompatible type annotation to see the compile time error it produces. +

diff --git a/src/content/chapter3_data_types/lesson07_results/code.gleam b/src/content/chapter3_data_types/lesson07_results/code.gleam new file mode 100644 index 0000000..b613566 --- /dev/null +++ b/src/content/chapter3_data_types/lesson07_results/code.gleam @@ -0,0 +1,25 @@ +import gleam/int +import gleam/io + +pub fn main() { + io.debug(buy_pastry(10)) + io.debug(buy_pastry(8)) + io.debug(buy_pastry(5)) + io.debug(buy_pastry(3)) +} + +pub type PurchaseError { + NotEnoughMoney(required: Int) + NotLuckyEnough +} + +fn buy_pastry(money: Int) -> Result(Int, PurchaseError) { + case money >= 5 { + True -> + case int.random(4) == 0 { + True -> Error(NotLuckyEnough) + False -> Ok(money - 5) + } + False -> Error(NotEnoughMoney(required: 5)) + } +} diff --git a/src/content/chapter3_data_types/lesson07_results/en.html b/src/content/chapter3_data_types/lesson07_results/en.html new file mode 100644 index 0000000..c43a0b7 --- /dev/null +++ b/src/content/chapter3_data_types/lesson07_results/en.html @@ -0,0 +1,39 @@ +

+ Gleam doesn't use exceptions, instead computations that can either succeed or + fail return a value of the built-in Result(value, error) type. It + has two variants: +

+ +

+ The type is generic with two type parameters, one for the success value and + one for the error. With these the result can hold any type for success and + failure. +

+

+ Commonly a Gleam program or library will define a custom type with a variant + for each possible problem that can arise, along with any error information + that would be useful to the programmer. +

+

+ This is advantageous over exceptions as you can immediately see what if any + errors a function can return, and the compiler will ensure they are handled. + No nasty surprises with unexpected exceptions! +

+

+ A result value can be handled by pattern matching with a + case expression, but given how frequently results are returned + this can become unwieldy. Gleam code commonly uses the + + gleam/result + + standard library module and use expressions when working with results, + both of which will be covered in later chapters. +

diff --git a/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam b/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam new file mode 100644 index 0000000..dc772ca --- /dev/null +++ b/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam @@ -0,0 +1,13 @@ +import gleam/io + +pub fn main() { + // 8 bit int. In binary: 00000011 + io.debug(<<3>>) + io.debug(<<3>> == <<3:size(8)>>) + + // 16 bit int. In binary: 0001100000000011 + io.debug(<<6147:size(16)>>) + + // A bit array of UTF8 data + io.debug(<<"Hello, Joe!":utf8>>) +} diff --git a/src/content/chapter3_data_types/lesson08_bit_arrays/en.html b/src/content/chapter3_data_types/lesson08_bit_arrays/en.html new file mode 100644 index 0000000..8b92335 --- /dev/null +++ b/src/content/chapter3_data_types/lesson08_bit_arrays/en.html @@ -0,0 +1,44 @@ +

+ Bit arrays represent a sequence of 1s and 0s, and are a convenient syntax for + constructing and manipulating binary data. +

+

+ Each segment of a bit array can be given options to specify the representation + used for that segment. +

+ +

+ Multiple options can be given to a segment by separating each with a dash: + x:unsigned-little-size(2). +

+

+ Bit arrays have limited support when compiling to JavaScript, not all options + can be used. Full bit array support will be implemented in the future. +

+

+ For more information on bit arrays see the + Erlang bit syntax documentation. +

diff --git a/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam b/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam new file mode 100644 index 0000000..a842430 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam @@ -0,0 +1,6 @@ +import gleam/io + +pub fn main() { + io.println("Hello, Joe!") + io.println("Hello, Mike!") +} diff --git a/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html b/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html new file mode 100644 index 0000000..4014136 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html @@ -0,0 +1,17 @@ +

+ The Gleam standard library is a regular Gleam package that has been published + to the Hex package repository. You could opt to + not use it if you wish, though almost all Gleam projects depend on it. +

+

+ All of the modules imported so far in this guide, such as + + gleam/io + , + are from the standard library. +

+

+ All of the documentation for the standard library is available on + HexDocs. We will go over some + of the most commonly used modules now. +

diff --git a/src/content/chapter4_standard_library/lesson01_list_module/code.gleam b/src/content/chapter4_standard_library/lesson01_list_module/code.gleam new file mode 100644 index 0000000..29f2448 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson01_list_module/code.gleam @@ -0,0 +1,19 @@ +import gleam/io +import gleam/list + +pub fn main() { + let ints = [0, 1, 2, 3, 4, 5] + + io.println("=== map ===") + io.debug(list.map(ints, fn(x) { x * 2 })) + + io.println("=== filter ===") + io.debug(list.filter(ints, fn(x) { x % 2 == 0 })) + + io.println("=== fold ===") + io.debug(list.fold(ints, 0, fn(count, e) { count + e })) + + io.println("=== find ===") + io.debug(list.find(ints, fn(x) { x > 3 })) + io.debug(list.find(ints, fn(x) { x > 13 })) +} diff --git a/src/content/chapter4_standard_library/lesson01_list_module/en.html b/src/content/chapter4_standard_library/lesson01_list_module/en.html new file mode 100644 index 0000000..9c6b953 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson01_list_module/en.html @@ -0,0 +1,42 @@ +

+ The + + gleam/list + + standard library module contains functions for working with lists. A Gleam + program will likely make heavy use of this module, the various functions + serving as different types of loops over lists. +

+ +

+ + map + + makes a new list by running a function on each element in a list. +

+

+ + filter + + makes a new list containing only the elements for which a function returns + true. +

+

+ + fold + + combines all the elements in a list into a single value by running a function + left-to-right on each element, passing the result of the previous call to the + next call. +

+

+ + find + + returns the first element in a list for which a function returns + True. +

+

+ It's worth getting familiar with all the functions in this module when writing + Gleam code, you'll be using them a lot! +

diff --git a/src/content/chapter4_standard_library/lesson02_result_module/code.gleam b/src/content/chapter4_standard_library/lesson02_result_module/code.gleam new file mode 100644 index 0000000..ec7039b --- /dev/null +++ b/src/content/chapter4_standard_library/lesson02_result_module/code.gleam @@ -0,0 +1,24 @@ +import gleam/int +import gleam/io +import gleam/result + +pub fn main() { + io.println("=== map ===") + io.debug(result.map(Ok(1), fn(x) { x * 2 })) + io.debug(result.map(Error(1), fn(x) { x * 2 })) + + io.println("=== try ===") + io.debug(result.try(Ok("1"), int.parse)) + io.debug(result.try(Ok("no"), int.parse)) + io.debug(result.try(Error(Nil), int.parse)) + + io.println("=== unwrap ===") + io.debug(result.unwrap(Ok("1234"), "default")) + io.debug(result.unwrap(Error(Nil), "default")) + + io.println("=== pipeline ===") + int.parse("-1234") + |> result.map(int.absolute_value) + |> result.try(int.remainder(_, 42)) + |> io.debug +} diff --git a/src/content/chapter4_standard_library/lesson02_result_module/en.html b/src/content/chapter4_standard_library/lesson02_result_module/en.html new file mode 100644 index 0000000..0760145 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson02_result_module/en.html @@ -0,0 +1,40 @@ +

+ The + + gleam/result + + standard library module contains functions for working with results. Gleam + programs will make heavy use of this module to avoid excessive nested case + expressions when calling multiple functions that can fail. +

+ +

+ + map + + updates a value held within the Ok of a result by calling a given function on + it. If the result is an error then the function is not called. +

+ +

+ + try + + runs a result returning function on the value held within an Ok of a result. + If the result is an error then the function is not called. This is useful for + chaining together multiple function calls that can fail, one after the other, + stopping at the first error. +

+ +

+ + unwrap + + extracts the success value from a result, or returning a default value if the + result is an error. +

+ +

+ Result functions are often used with pipelines to chain together multiple + calls to result returning functions. +

diff --git a/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam b/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam new file mode 100644 index 0000000..e7b3a59 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam @@ -0,0 +1,14 @@ +import gleam/dict +import gleam/io + +pub fn main() { + let scores = dict.from_list([#("Lucy", 13), #("Drew", 15)]) + io.debug(scores) + + let scores = + scores + |> dict.insert("Bushra", 16) + |> dict.insert("Darius", 14) + |> dict.delete("Drew") + io.debug(scores) +} diff --git a/src/content/chapter4_standard_library/lesson03_dict_module/en.html b/src/content/chapter4_standard_library/lesson03_dict_module/en.html new file mode 100644 index 0000000..4037085 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson03_dict_module/en.html @@ -0,0 +1,40 @@ +

+ The + gleam/dict + standard library module defines Gleam's Dict type and functions + for working with it. A dict is a collection of keys and values which other + languages may call a hashmap or table. +

+ +

+ + new + + and + + from_list + + can be used to create new dicts. +

+ +

+ + insert + + and + + delete + + are used to add and remove items from a dict. +

+

+ Like lists, dicts are immutable. Inserting or deleting an item from a dict + will return a new dict with the item added or removed. +

+

+ Dicts are unordered! If it appears that the items in a dict are in a certain + order this is incidental and should not be relied upon. Any ordering may + change without warning in future versions or on different runtimes. +

diff --git a/src/content/chapter4_standard_library/lesson04_option_module/code.gleam b/src/content/chapter4_standard_library/lesson04_option_module/code.gleam new file mode 100644 index 0000000..eb60001 --- /dev/null +++ b/src/content/chapter4_standard_library/lesson04_option_module/code.gleam @@ -0,0 +1,14 @@ +import gleam/io +import gleam/option.{type Option, None, Some} + +pub type Person { + Person(name: String, pet: Option(String)) +} + +pub fn main() { + let person_with_pet = Person("Al", Some("Nubi")) + let person_without_pet = Person("Maria", None) + + io.debug(person_with_pet) + io.debug(person_without_pet) +} diff --git a/src/content/chapter4_standard_library/lesson04_option_module/en.html b/src/content/chapter4_standard_library/lesson04_option_module/en.html new file mode 100644 index 0000000..699b5bd --- /dev/null +++ b/src/content/chapter4_standard_library/lesson04_option_module/en.html @@ -0,0 +1,19 @@ +

+ Values in Gleam are not nullable, so the + + gleam/option + + standard library module defines Gleam's + + Option + + type, which can be used to represent a value that is either present or absent. +

+ +

+ The option type is very similar to the result type, but it does not have an + error value. Some languages have functions return an option when there is no + extra error detail to give, but Gleam always uses result. This makes all + fallible functions consistent and removes any boilerplate that would be + required when mixing functions that use each type. +

diff --git a/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam b/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam new file mode 100644 index 0000000..d116b42 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam @@ -0,0 +1,26 @@ +import gleam/io + +pub fn main() { + let positive = new(1) + let zero = new(0) + let negative = new(-1) + + io.debug(to_int(positive)) + io.debug(to_int(zero)) + io.debug(to_int(negative)) +} + +pub opaque type PositiveInt { + PositiveInt(inner: Int) +} + +pub fn new(i: Int) -> PositiveInt { + case i >= 0 { + True -> PositiveInt(i) + False -> PositiveInt(0) + } +} + +pub fn to_int(i: PositiveInt) -> Int { + i.inner +} diff --git a/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html b/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html new file mode 100644 index 0000000..f0956fb --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html @@ -0,0 +1,17 @@ +

+ Opaque types are types where a custom type itself is public and can + be used by other modules, but the constructors for the type are private and + can only be used by the module that defines the type. This prevents other + modules from constructing or pattern matching on the type. +

+

+ This is useful for creating types with smart constructors. A smart + constructor is a function that constructs a value of a type, but is more + restrictive than if the programmer were to use one of the type's constructors + directly. This can be useful for ensuring that the type is used correctly. +

+

+ For example, this PositiveInt custom type is opaque. If other + modules want to construct one they have to use the new function, + which ensures that the integer is positive. +

diff --git a/src/content/chapter5_advanced_features/lesson01_use/code.gleam b/src/content/chapter5_advanced_features/lesson01_use/code.gleam new file mode 100644 index 0000000..e7b435b --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson01_use/code.gleam @@ -0,0 +1,38 @@ +import gleam/io +import gleam/result + +pub fn main() { + io.debug(without_use()) + io.debug(with_use()) +} + +pub fn without_use() { + result.try(get_username(), fn(username) { + result.try(get_password(), fn(password) { + result.map(log_in(username, password), fn(greeting) { + greeting <> ", " <> username + }) + }) + }) +} + +pub fn with_use() { + use username <- result.try(get_username()) + use password <- result.try(get_password()) + use greeting <- result.map(log_in(username, password)) + greeting <> ", " <> username +} + +// Here are some pretend functions for this example: + +fn get_username() { + Ok("alice") +} + +fn get_password() { + Ok("hunter2") +} + +fn log_in(_username: String, _password: String) { + Ok("Welcome") +} diff --git a/src/content/chapter5_advanced_features/lesson01_use/en.html b/src/content/chapter5_advanced_features/lesson01_use/en.html new file mode 100644 index 0000000..d879aec --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson01_use/en.html @@ -0,0 +1,30 @@ +

+ Gleam lacks exceptions, macros, type classes, early returns, and a variety of + other features, instead going all-in with just first-class-functions and + pattern matching. This makes Gleam code easier to understand, but it can + sometimes result in excessive indentation. +

+

+ Gleam's use expression helps out here by enabling us to write code that uses + callbacks in an unindented style, as shown in the code window. +

+ +

+ The higher order function being called goes on the right hand side of the + <- operator. It must take a callback function as its final + argument. +

+

+ The argument names for the callback function go on the left hand side of the + <- operator. The function can take any number of arguments, + including zero. +

+

+ All the remaining code in the enclosing {} block + becomes the body of the callback function. +

+

+ This is a very capable and useful feature, but excessive application of + use may result in unclear code, especially to beginners. Usually + the regular function call syntax results in more approachable code! +

diff --git a/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam b/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam new file mode 100644 index 0000000..6c5ccaa --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam @@ -0,0 +1,30 @@ +import gleam/io +import gleam/result + +pub fn main() { + let x = { + use username <- result.try(get_username()) + use password <- result.try(get_password()) + use greeting <- result.map(log_in(username, password)) + greeting <> ", " <> username + } + + case x { + Ok(greeting) -> io.println(greeting) + Error(error) -> io.println("ERROR:" <> error) + } +} + +// Here are some pretend functions for this example: + +fn get_username() { + Ok("alice") +} + +fn get_password() { + Ok("hunter2") +} + +fn log_in(_username: String, _password: String) { + Ok("Welcome") +} diff --git a/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html b/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html new file mode 100644 index 0000000..e28c843 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html @@ -0,0 +1,30 @@ +

+ The use expression is syntactic sugar for a regular function call + and an anonymous function. +

+ +

This code:

+
+use a, b <- my_function
+next(a)
+next(b)
+
+ +

Expands into this code:

+
+my_function(fn(a, b) {
+  next(a)
+  next(b)
+})
+
+ +

+ To ensure that your use code works and is as understandable as + possible, the right-hand-side ideally should be a function call rather than a + pipeline or other expression, which is typically more difficult to read. +

+ +

+ use is an expression like everything else in Gleam, so it can be + placed within blocks. +

diff --git a/src/content/chapter5_advanced_features/lesson03_todo/code.gleam b/src/content/chapter5_advanced_features/lesson03_todo/code.gleam new file mode 100644 index 0000000..d5abe8f --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson03_todo/code.gleam @@ -0,0 +1,7 @@ +pub fn main() { + todo as "I haven't written this code yet!" +} + +pub fn todo_without_reason() { + todo +} diff --git a/src/content/chapter5_advanced_features/lesson03_todo/en.html b/src/content/chapter5_advanced_features/lesson03_todo/en.html new file mode 100644 index 0000000..4a2c433 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson03_todo/en.html @@ -0,0 +1,14 @@ +

+ The todo keyword is used to specify that some code is not yet + implemented. +

+

+ The as "some string" is optional, though you may wish to include + the message if you have more than one code block marked as + todo in your code. +

+

+ When used the Gleam compiler will print a warning to remind you the code is + unfinished, and if the code is run then the program will crash with the given + message. +

diff --git a/src/content/chapter5_advanced_features/lesson04_panic/code.gleam b/src/content/chapter5_advanced_features/lesson04_panic/code.gleam new file mode 100644 index 0000000..fce9d66 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson04_panic/code.gleam @@ -0,0 +1,15 @@ +import gleam/io + +pub fn main() { + print_score(10) + print_score(100_000) + print_score(-1) +} + +pub fn print_score(score: Int) { + case score { + score if score > 1000 -> io.println("High score!") + score if score > 0 -> io.println("Still working on it") + _ -> panic as "Scores should never be negative!" + } +} diff --git a/src/content/chapter5_advanced_features/lesson04_panic/en.html b/src/content/chapter5_advanced_features/lesson04_panic/en.html new file mode 100644 index 0000000..c54c217 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson04_panic/en.html @@ -0,0 +1,11 @@ +

+ The panic keyword is similar to the todo keyword, + but it is used to crash the program when the program has reached a point that + should never be reached. +

+

+ This keyword should almost never be used! It may be useful in initial + prototypes and scripts, but its use in a library or production application is + a sign that the design could be improved. With well designed types the type + system can typically be used to make these invalid states unrepresentable. +

diff --git a/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam b/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam new file mode 100644 index 0000000..2ba907a --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam @@ -0,0 +1,16 @@ +import gleam/io + +pub fn main() { + let a = unsafely_get_first_element([123]) + io.debug(a) + + let b = unsafely_get_first_element([]) + io.debug(b) +} + +pub fn unsafely_get_first_element(items: List(a)) -> a { + // This will panic if the list is empty. + // A regular `let` would not permit this partial pattern + let assert [first, ..] = items + first +} diff --git a/src/content/chapter5_advanced_features/lesson05_let_assert/en.html b/src/content/chapter5_advanced_features/lesson05_let_assert/en.html new file mode 100644 index 0000000..29fd06c --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson05_let_assert/en.html @@ -0,0 +1,15 @@ +

+ let assert is the final way to intentionally crash your Gleam + program. It is similar to the panic keyword in that it crashes + when the program has reached a point that should never be reached. +

+

+ let assert is similar to let in that it is a way to + assign values to variables, but it is different in that the pattern can be + partial. The pattern does not need to match every possible value of the + type being assigned. +

+

+ Like panic this feature should be used sparingly, and likely not + at all in libraries. +

diff --git a/src/content/chapter5_advanced_features/lesson06_externals/code.gleam b/src/content/chapter5_advanced_features/lesson06_externals/code.gleam new file mode 100644 index 0000000..1101b82 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson06_externals/code.gleam @@ -0,0 +1,17 @@ +import gleam/io + +// A type with no Gleam constructors +pub type DateTime + +// An external function that creates an instance of the type +@external(javascript, "./my_package_ffi.mjs", "now") +pub fn now() -> DateTime + +// The `now` function in `./my_package_ffi.mjs` looks like this: +// export function now() { +// return new Date(); +// } + +pub fn main() { + io.debug(now()) +} diff --git a/src/content/chapter5_advanced_features/lesson06_externals/en.html b/src/content/chapter5_advanced_features/lesson06_externals/en.html new file mode 100644 index 0000000..8815fa7 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson06_externals/en.html @@ -0,0 +1,25 @@ +

+ Sometimes in our projects we want to use code written in other languages, most + commonly Erlang and JavaScript, depending on which runtime is being used. + Gleam's external functions and external types allow us to + import and use this non-Gleam code. +

+

+ An external type is one that has no constructors. Gleam doesn't know what + shape it has or how to create one, it only knows that it exists. +

+

+ An external function is one that has the @external attribute on + it, directing the compiler to use the specified module function as the + implementation, instead of Gleam code. +

+

+ The compiler can't tell the types of functions written in other languages, so + when the external attribute is given type annotations must be provided. Gleam + trusts that the type given is correct so an inaccurate type annotation can + result in unexpected behaviour and crashes at runtime. Be careful! +

+

+ External functions are useful but should be used sparingly. Prefer to write + Gleam code where possible. +

diff --git a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam new file mode 100644 index 0000000..b62a735 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam @@ -0,0 +1,11 @@ +import gleam/io + +pub type DateTime + +@external(erlang, "calendar", "local_time") +@external(javascript, "./my_package_ffi.mjs", "now") +pub fn now() -> DateTime + +pub fn main() { + io.debug(now()) +} diff --git a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html new file mode 100644 index 0000000..6e02d36 --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html @@ -0,0 +1,22 @@ +

+ Multiple external implementations can be specified for the same function, + enabling the function to work on both Erlang and JavaScript. +

+

+ If a function doesn't have an implementation for the currently compiled-for + target then the compiler will return an error. +

+

+ You should try to implement functions for all targets, but this isn't always + possible due to incompatibilities in how IO and concurreny works in Erlang and + JavaScript. With Erlang concurrent IO is handled transparently by the runtime, + while in JavaScript concurrent IO requires the use of promises or callbacks. + If your code uses the Erlang style it is typically not possible to implement + in JavaScript, while if callbacks are used then it won't be compatible with + most Gleam and Erlang code as it forces any code that calls the function to + also use callbacks. +

+

+ Libraries that make use of concurrent IO will typically have to decide whether + they support Erlang or JavaScript, and document this in their README. +

diff --git a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam new file mode 100644 index 0000000..a97b8fc --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam @@ -0,0 +1,18 @@ +import gleam/io + +@external(erlang, "lists", "reverse") +pub fn reverse_list(items: List(e)) -> List(e) { + tail_recursive_reverse(items, []) +} + +fn tail_recursive_reverse(items: List(e), reversed: List(e)) -> List(e) { + case items { + [] -> reversed + [first, ..rest] -> tail_recursive_reverse(rest, [first, ..reversed]) + } +} + +pub fn main() { + io.debug(reverse_list([1, 2, 3, 4, 5])) + io.debug(reverse_list(["a", "b", "c", "d", "e"])) +} diff --git a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html new file mode 100644 index 0000000..243c7ea --- /dev/null +++ b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html @@ -0,0 +1,13 @@ +

+ It's possible for a function to have both a Gleam implementation and an + external implementation. If there exists an external implementation for the + currently compiled-for target then it will be used, otherwise the Gleam + implementation is used. +

+

+ This may be useful if you have a function that can be implemented in Gleam, + but there is an optimised implementation that can be used for one target. For + example, the Erlang virtual machine has a built-in list reverse function that + is implemented in native code. The code here uses this implementation when + running on Erlang, as it is then available. +

diff --git a/src/playground.gleam b/src/playground.gleam index d632e4d..2ffd43a 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -1,5 +1,1063 @@ +import filepath import gleam/io +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result +import gleam/string +import gleam/string_builder +import htmb.{type Html, h, text} +import simplifile +import snag +import playground/widgets.{Link} + +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 = "./wasm-compiler" + +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() { - io.println("Hello from playground!") + 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 = " +

+ 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. +

+

+ The tour is interactive! The code shown is editable and will be compiled and + evaluated as you type. Anything you print using + + io.println + + or + + io.debug + + 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. +

+

+ If at any point you get stuck or have a question do not hesitate to ask in + the Gleam Discord server. 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. +

+

+ OK, let's go. Click \"Next\" to get started, click \"Contents\" to jump to a + specific topic, or go here to read everything in + one page. +

+" + +const what_next_html = " +

+ Congratulations on completing the tour! Here's some ideas for what to do next: +

+ +

+ Read the Writing Gleam + guide to learn how to create and develop a Gleam project. +

+

+ Join the the Gleam Discord server + and meet the community. They're friendly and helpful! +

+

+ Enroll in the Exercism + Gleam track to practice your Gleam skills through a series of exercises + and optionally get feedback from experienced Gleam developers. +

+

+ Happy hacking! +

+" + +// page paths + +const path_home = "/" + +const path_table_of_contents = "/table-of-contents" + +const path_what_next = "/what-next" + +const path_everything = "/everything" + +// 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("Site compiled to ./public ๐ŸŽ‰") + } + Error(snag) -> { + panic as snag.pretty_print(snag) + } + } +} + +pub type Chapter { + Chapter(name: String, path: String, lessons: List(Lesson)) +} + +pub 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 <> "/en.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: "Welcome to the Gleam language tour! ๐Ÿ’ซ", + 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 contents page + use _ <- result.try( + write_lesson(Lesson( + name: "Table of Contents", + text: contents_list_html(chapters), + code: hello_joe, + path: path_table_of_contents, + previous: None, + next: None, + )), + ) + + // Everything page + use _ <- result.try(write_everything_page(chapters)) + + Ok(Nil) +} + +fn contents_list_html(chapters: List(Chapter)) -> String { + let chapters = + list.flat_map(chapters, fn(chapter) { + [ + h("h3", [#("class", "mb-0")], [text(chapter.name)]), + h( + "ul", + [], + list.map(chapter.lessons, fn(lesson) { + h("li", [], [ + h("a", [#("href", lesson.path)], [ + lesson.name + |> string.replace("-", " ") + |> string.capitalise + |> text, + ]), + ]) + }), + ), + ] + }) + + [ + h("p", [], [ + text("Looking for all the content on one page? "), + h("a", [#("href", path_everything)], [text("Find it here")]), + text("!"), + ]), + ..chapters + ] + |> list.append([ + h("p", [], [h("a", [#("href", path_what_next)], [text("What's nextโ€ฆ?")])]), + ]) + |> list.map(render_html) + |> string.join("\n") +} + +fn render_html(html: Html) -> String { + html + |> htmb.render + |> string_builder.to_string +} + +fn ensure_directory(path: String) -> snag.Result(Nil) { + simplifile.create_directory_all(path) + |> file_error("Failed to create directory " <> path) +} + +fn write_text(path: String, text: String) -> snag.Result(Nil) { + simplifile.write(path, text) + |> file_error("Failed to write " <> path) +} + +fn write_everything_page(chapters: List(Chapter)) -> snag.Result(Nil) { + let path = public <> "/everything" + use _ <- result.try(ensure_directory(path)) + let path = filepath.join(path, "/index.html") + write_text(path, everything_page_render(chapters)) +} + +fn write_lesson(lesson: Lesson) -> snag.Result(Nil) { + let path = public <> lesson.path + use _ <- result.try(ensure_directory(path)) + let path = filepath.join(path, "/index.html") + write_text(path, lesson_page_render(lesson)) +} + +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 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) + } +} + +// Shared stylesheets paths + +const css__gleam_common = "/common.css" + +/// Loads fonts and defines font sizes +const css_fonts = "/css/fonts.css" + +/// Derives app colors for both dark & light themes from common.css variables +const css_theme = "/css/theme.css" + +/// Defines layout unit variables +const css_layout = "/css/layout.css" + +/// Sensitive defaults for any page +const css_defaults_page = [css_fonts, css_theme, css__gleam_common, css_layout] + +// Page stylesheet paths + +/// Common stylesheet for all tour pages +const css_root = "/css/root.css" + +// Path to the css specific to the everything page +const css_everything_page = "/css/pages/everything.css" + +// Path to the css speciic to to lesson & main pages +const css_lesson_page = "/css/pages/lesson.css" + +// Defines code syntax highlighting for highlightJS & CodeFlash +// based on dark / light mode and the currenly loaded color scheme +const css_syntax_highlight = "/css/code/syntax-highlight.css" + +// Color schemes +// TODO: add more color schemes + +/// Atom One Dark & Atom One Light colors +const css_scheme_atom_one = "/css/code/color-schemes/atom-one.css" + +/// Sensitive defaults for any page needing to display Gleam code +/// To be used alonside defaults_page +const css_defaults_code = [css_syntax_highlight, css_scheme_atom_one] + +// Common page HTML elements renders + +/// Renders the navbar with common links +fn render_navbar() -> Html { + widgets.navbar(titled: "Gleam Language Tour", links: [ + Link(label: "gleam.run", to: "http://gleam.run"), + ]) +} + +/// Renders the script that that contains the code +/// needed for the light/dark theme picker to work +pub fn theme_picker_script() -> Html { + html_dangerous_inline_script( + widgets.theme_picker_js, + ScriptOptions(module: True, defer: False), + [], + ) +} + +pub fn arrow_keys_navigation_script( + next: Option(String), + prev: Option(String), +) -> Html { + let to_handler = fn(maybe_link) { + case maybe_link { + None -> "null" + Some(link) -> "() => { window.location.href = '" <> link <> "' }" + } + } + + html_dangerous_inline_script(" + const keyHandlers = { + 'ArrowLeft': " <> to_handler(prev) <> ", + 'ArrowRight': " <> to_handler(next) <> ", + } + + document.addEventListener('keydown', function(event) { + // Don't hijack arrow keys when focus is on the code textarea. + if (document.querySelector('textarea.codeflask__textarea') === document.activeElement) { + return; + } + + const handler = keyHandlers[event.key]; + if (handler !== undefined && handler !== null) { + handler(); + } + }) + ", ScriptOptions(module: True, defer: False), []) +} + +// Page Renders + +/// Renders a Lesson's page +/// Complete with title, lesson, editor and output +fn lesson_page_render(lesson: Lesson) -> String { + let navlink = fn(name, link) { + case link { + None -> h("span", [], [text(name)]) + Some(path) -> h("a", [#("href", path)], [text(name)]) + } + } + + render_page(PageConfig( + path: lesson.path, + title: lesson.name, + stylesheets: list.flatten([ + css_defaults_page, + css_defaults_code, + [css_root, css_lesson_page], + ]), + static_content: [render_navbar()], + content: [ + h("article", [#("id", "playground")], [ + h("section", [#("id", "left"), #("class", "content-nav")], [ + h("div", [], [ + h("h2", [], [text(lesson.name <> " lesson")]), + htmb.dangerous_unescaped_fragment(string_builder.from_string( + lesson.text, + )), + ]), + h("nav", [#("class", "prev-next")], [ + navlink("Back", lesson.previous), + text(" โ€” "), + h("a", [#("href", path_table_of_contents)], [text("Contents")]), + text(" โ€” "), + navlink("Next", lesson.next), + ]), + ]), + h("section", [#("id", "right")], [ + h("section", [#("id", "editor")], [ + h("div", [#("id", "editor-target")], []), + ]), + h("aside", [#("id", "output")], []), + ]), + ]), + ], + scripts: ScriptConfig( + body: [ + theme_picker_script(), + arrow_keys_navigation_script(lesson.next, lesson.previous), + h("script", [#("type", "gleam"), #("id", "code")], [ + htmb.dangerous_unescaped_fragment(string_builder.from_string( + lesson.code, + )), + ]), + html_script("/index.js", ScriptOptions(module: True, defer: False), []), + ], + head: [], + ), + )) +} + +/// Transform a path into a slug +fn slugify_path(path: String) -> String { + string.replace(path, "/", "-") + |> string.drop_left(up_to: 1) +} + +/// Renders a lesson item in the everyting page's list +fn everything_page_lesson_html(lesson: Lesson, index: Int, end_index: Int) { + let snippet_link_title = "Experiment with " <> lesson.name <> " in browser" + + let lesson_content = + h("article", [#("class", "lesson"), #("id", slugify_path(lesson.path))], [ + h("a", [#("href", "#" <> slugify_path(lesson.path)), #("class", "link")], [ + h("h2", [#("class", "lesson-title")], [text(lesson.name)]), + ]), + htmb.dangerous_unescaped_fragment(string_builder.from_string(lesson.text)), + h("pre", [#("class", "lesson-snippet hljs gleam language-gleam")], [ + h("code", [], [text(lesson.code)]), + h( + "a", + [ + #("class", "lesson-snippet-link"), + #("href", lesson.path), + #("title", snippet_link_title), + #("aria-label", snippet_link_title), + ], + [ + h("i", [#("class", "snippet-link-icon")], [text("")]), + text("Run code snippet"), + ], + ), + ]), + ]) + + case index { + i if i == end_index -> [lesson_content] + _ -> [lesson_content, widgets.separator("lesson")] + } +} + +/// Renders a list containing all chapters and their lessons +fn everything_page_chapters_html(chapters: List(Chapter)) -> List(Html) { + use #(chapter, index) <- list.flat_map( + list.index_map(chapters, fn(chap, i) { #(chap, i) }), + ) + + let lessons = + list.index_map(chapter.lessons, fn(lesson, index) { + everything_page_lesson_html( + lesson, + index, + list.length(chapter.lessons) - 1, + ) + }) + let chapter_title = + h("h3", [#("id", slugify_path(chapter.path)), #("class", "chapter-title")], [ + text(chapter.name), + ]) + + let chapter_header = case index { + 0 -> [chapter_title, widgets.separator("chapter")] + _ -> [ + widgets.separator("chapter-between"), + chapter_title, + widgets.separator("chapter"), + ] + } + + list.concat([chapter_header, ..lessons]) +} + +/// Renders a link to a lesson in the table of contents +fn everything_page_toc_link(lesson: Lesson) -> Html { + h("li", [], [ + widgets.text_link( + Link(label: lesson.name, to: "#" <> slugify_path(lesson.path)), + [#("class", "link padded")], + ), + ]) +} + +/// Renders the everything pages's table of contents +fn everything_page_toc_html(chapters: List(Chapter)) -> List(Html) { + use chapter <- list.map(chapters) + let links = list.map(chapter.lessons, everything_page_toc_link) + + h("article", [#("class", "chapter"), #("id", "chapter-" <> chapter.name)], [ + h("h3", [], [text(chapter.name)]), + h("ul", [], links), + ]) +} + +/// Renders the /everything's page body content +fn everything_page_html(chapters: List(Chapter)) -> Html { + let chapter_lessons = everything_page_chapters_html(chapters) + let table_of_contents = everything_page_toc_html(chapters) + + h("main", [#("id", "everything")], [ + h( + "aside", + [#("id", "everything-contents"), #("class", "dim-bg")], + table_of_contents, + ), + h("section", [#("id", "everything-lessons")], chapter_lessons), + ]) +} + +/// Renders the /everything page to a string +pub fn everything_page_render(chapters: List(Chapter)) -> String { + render_page(PageConfig( + path: path_everything, + title: "Everything!", + stylesheets: list.flatten([ + css_defaults_page, + css_defaults_code, + [css_root, css_everything_page], + ]), + static_content: [render_navbar()], + content: [everything_page_html(chapters)], + scripts: ScriptConfig( + head: [ + html_script( + "/js/highlight/highlight.core.min.js", + ScriptOptions(module: True, defer: False), + [], + ), + html_script( + "/js/highlight/regexes.js", + ScriptOptions(module: True, defer: True), + [], + ), + ], + body: [ + theme_picker_script(), + html_script( + "/js/highlight/highlight-gleam.js", + ScriptOptions(module: True, defer: True), + [], + ), + ], + ), + )) +} + +/// Generic HTML rendering utils +pub type HtmlAttribute = + #(String, String) + +pub type ScriptOptions { + ScriptOptions(module: Bool, defer: Bool) +} + +/// Formats js script options into usage html attributes +fn html_script_common_attributes( + attributes: ScriptOptions, +) -> List(HtmlAttribute) { + let type_attr = #("type", case attributes.module { + True -> "module" + _ -> "text/javascript" + }) + let defer_attr = #("defer", "") + + case attributes.defer { + True -> [defer_attr, type_attr] + _ -> [type_attr] + } +} + +/// Renders an HTML script tag +pub fn html_script( + src source: String, + options attributes: ScriptOptions, + attributes additional_attributes: List(HtmlAttribute), +) -> Html { + let attrs = { + let src_attr = #("src", source) + let base_attrs = [src_attr, ..html_script_common_attributes(attributes)] + list.flatten([base_attrs, additional_attributes]) + } + h("script", attrs, []) +} + +/// Renders an inline HTML script tag +pub fn html_dangerous_inline_script( + script content: String, + options attributes: ScriptOptions, + attributes additional_attributes: List(HtmlAttribute), +) -> Html { + let attrs = { + list.flatten([ + html_script_common_attributes(attributes), + additional_attributes, + ]) + } + h("script", attrs, [ + htmb.dangerous_unescaped_fragment(string_builder.from_string(content)), + ]) +} + +/// Renders an HTML meta tag +pub fn html_meta(data attributes: List(HtmlAttribute)) -> Html { + h("meta", attributes, []) +} + +/// Renders an HTML meta property tag +pub fn html_meta_prop(property: String, content: String) -> Html { + html_meta([#("property", property), #("content", content)]) +} + +/// Renders an HTML link tag +pub fn html_link(rel: String, href: String) -> Html { + h("link", [#("rel", rel), #("href", href)], []) +} + +/// Renders a stylesheet link tag +pub fn html_stylesheet(src: String) -> Html { + html_link("stylesheet", src) +} + +/// Renders an HTML title tag +pub fn html_title(title: String) -> Html { + h("title", [], [text(title)]) +} + +pub type HeadConfig { + HeadConfig( + path: String, + title: String, + description: String, + url: String, + image: String, + meta: List(Html), + stylesheets: List(String), + scripts: List(Html), + ) +} + +/// Renders the page head as HTML +fn head(with config: HeadConfig) -> htmb.Html { + let meta_tags = [ + html_meta_prop("og:type", "website"), + html_meta_prop("og:title", config.title), + html_meta_prop("og:description", config.description), + html_meta_prop("og:url", config.url), + html_meta_prop("og:image", config.image), + html_meta_prop("twitter:card", "summary_large_image"), + html_meta_prop("twitter:url", config.url), + html_meta_prop("twitter:title", config.title), + html_meta_prop("twitter:description", config.description), + html_meta_prop("twitter:image", config.image), + ..config.meta + ] + + let head_meta = [ + html_meta([#("charset", "utf-8")]), + html_meta([ + #("name", "viewport"), + #("content", "width=device-width, initial-scale=1"), + ]), + html_title(config.title), + html_meta([#("name", "description"), #("content", config.description)]), + ..meta_tags + ] + + let head_links = [ + html_link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"), + ..list.map(config.stylesheets, html_stylesheet) + ] + + let head_content = list.concat([head_meta, head_links, config.scripts]) + + h("head", [], head_content) +} + +pub type BodyConfig { + BodyConfig( + content: List(Html), + static_content: List(Html), + scripts: List(Html), + attributes: List(HtmlAttribute), + ) +} + +/// Renders an Html body tag +fn html_body(with config: BodyConfig) -> Html { + let content = + list.flatten([config.static_content, config.content, config.scripts]) + + h("body", config.attributes, content) +} + +pub type HtmlConfig { + HtmlConfig( + attributes: List(HtmlAttribute), + lang: String, + head: HeadConfig, + body: BodyConfig, + ) +} + +/// Renders an HTML tag and its children +fn html(with config: HtmlConfig) -> Html { + let attributes = [#("lang", config.lang), ..config.attributes] + + h("html", attributes, [head(config.head), html_body(config.body)]) +} + +pub type ScriptConfig { + ScriptConfig(head: List(Html), body: List(Html)) +} + +pub type PageConfig { + PageConfig( + path: String, + title: String, + content: List(Html), + static_content: List(Html), + stylesheets: List(String), + scripts: ScriptConfig, + ) +} + +/// Renders a page in the language tour +pub fn render_page_html(page config: PageConfig) -> Html { + // add path-specific class to body to make styling easier + let body_class = #("id", "page" <> string.replace(config.path, "/", "-")) + + // render html + html(HtmlConfig( + head: HeadConfig( + description: "An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!", + image: "https://gleam.run/images/og-image.png", + title: config.title <> " - The Gleam Language Tour", + url: "https://tour.gleam.run/" <> config.path, + path: config.path, + meta: [], + stylesheets: config.stylesheets, + scripts: [ + html_script( + "https://plausible.io/js/script.js", + ScriptOptions(defer: True, module: False), + [#("data-domain", "tour.gleam.run")], + ), + ..config.scripts.head + ], + ), + lang: "en-GB", + attributes: [#("class", "theme-light")], + body: BodyConfig( + attributes: [body_class], + scripts: config.scripts.body, + static_content: config.static_content, + content: config.content, + ), + )) +} + +/// Renders an HTML document in String form from a PageConfig +pub fn render_page(page config: PageConfig) -> String { + config + |> render_page_html + |> htmb.render_page("html") + |> string_builder.to_string } diff --git a/src/playground/widgets.gleam b/src/playground/widgets.gleam new file mode 100644 index 0000000..8ac9f08 --- /dev/null +++ b/src/playground/widgets.gleam @@ -0,0 +1,212 @@ +import gleam/list +import htmb.{type Html, h, text} + +pub fn icon_moon() -> Html { + h("svg", [#("id", "icon-moon"), #("viewBox", "0 0 24 24")], [ + h( + "path", + [ + #( + "d", + "M21.996 12.882c0.022-0.233-0.038-0.476-0.188-0.681-0.325-0.446-0.951-0.544-1.397-0.219-0.95 0.693-2.060 1.086-3.188 1.162-1.368 0.092-2.765-0.283-3.95-1.158-1.333-0.985-2.139-2.415-2.367-3.935s0.124-3.124 1.109-4.456c0.142-0.191 0.216-0.435 0.191-0.691-0.053-0.55-0.542-0.952-1.092-0.898-2.258 0.22-4.314 1.18-5.895 2.651-1.736 1.615-2.902 3.847-3.137 6.386-0.254 2.749 0.631 5.343 2.266 7.311s4.022 3.313 6.772 3.567 5.343-0.631 7.311-2.266 3.313-4.022 3.567-6.772zM19.567 14.674c-0.49 1.363-1.335 2.543-2.416 3.441-1.576 1.309-3.648 2.016-5.848 1.813s-4.108-1.278-5.417-2.854-2.016-3.648-1.813-5.848c0.187-2.032 1.117-3.814 2.507-5.106 0.782-0.728 1.71-1.3 2.731-1.672-0.456 1.264-0.577 2.606-0.384 3.899 0.303 2.023 1.38 3.934 3.156 5.247 1.578 1.167 3.448 1.668 5.272 1.545 0.752-0.050 1.496-0.207 2.21-0.465z", + ), + ], + [], + ), + ]) +} + +pub fn icon_sun() -> Html { + h("svg", [#("id", "icon-sun"), #("viewBox", "0 0 24 24")], [ + h( + "path", + [ + #( + "d", + "M18 12c0-1.657-0.673-3.158-1.757-4.243s-2.586-1.757-4.243-1.757-3.158 0.673-4.243 1.757-1.757 2.586-1.757 4.243 0.673 3.158 1.757 4.243 2.586 1.757 4.243 1.757 3.158-0.673 4.243-1.757 1.757-2.586 1.757-4.243zM16 12c0 1.105-0.447 2.103-1.172 2.828s-1.723 1.172-2.828 1.172-2.103-0.447-2.828-1.172-1.172-1.723-1.172-2.828 0.447-2.103 1.172-2.828 1.723-1.172 2.828-1.172 2.103 0.447 2.828 1.172 1.172 1.723 1.172 2.828zM11 1v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM11 21v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM3.513 4.927l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM17.653 19.067l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM1 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM21 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4.927 20.487l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM19.067 6.347l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z", + ), + ], + [], + ), + ]) +} + +pub fn icon_toggle_left() -> Html { + h("svg", [#("id", "icon-toggle-left"), #("viewBox", "0 0 24 24")], [ + h( + "path", + [ + #( + "d", + "M8 4c-2.209 0-4.21 0.897-5.657 2.343s-2.343 3.448-2.343 5.657 0.897 4.21 2.343 5.657 3.448 2.343 5.657 2.343h8c2.209 0 4.21-0.897 5.657-2.343s2.343-3.448 2.343-5.657-0.897-4.21-2.343-5.657-3.448-2.343-5.657-2.343zM8 6h8c1.657 0 3.156 0.67 4.243 1.757s1.757 2.586 1.757 4.243-0.67 3.156-1.757 4.243-2.586 1.757-4.243 1.757h-8c-1.657 0-3.156-0.67-4.243-1.757s-1.757-2.586-1.757-4.243 0.67-3.156 1.757-4.243 2.586-1.757 4.243-1.757zM12 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM10 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z", + ), + ], + [], + ), + ]) +} + +pub fn icon_toggle_right() -> Html { + h("svg", [#("id", "icon-toggle-right"), #("viewBox", "0 0 24 24")], [ + h( + "path", + [ + #( + "d", + "M8 4c-2.209 0-4.21 0.897-5.657 2.343s-2.343 3.448-2.343 5.657 0.897 4.21 2.343 5.657 3.448 2.343 5.657 2.343h8c2.209 0 4.21-0.897 5.657-2.343s2.343-3.448 2.343-5.657-0.897-4.21-2.343-5.657-3.448-2.343-5.657-2.343zM8 6h8c1.657 0 3.156 0.67 4.243 1.757s1.757 2.586 1.757 4.243-0.67 3.156-1.757 4.243-2.586 1.757-4.243 1.757h-8c-1.657 0-3.156-0.67-4.243-1.757s-1.757-2.586-1.757-4.243 0.67-3.156 1.757-4.243 2.586-1.757 4.243-1.757zM20 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM18 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z", + ), + ], + [], + ), + ]) +} + +pub fn theme_picker() -> Html { + h("div", [#("class", "theme-picker")], [ + h( + "button", + [ + #("type", "button"), + #("alt", "Switch to light mode"), + #("title", "Switch to light mode"), + #("class", "theme-button -light"), + #("data-light-theme-toggle", ""), + ], + [icon_moon(), icon_toggle_left()], + ), + h( + "button", + [ + #("type", "button"), + #("alt", "Switch to dark mode"), + #("title", "Switch to dark mode"), + #("class", "theme-button -dark"), + #("data-dark-theme-toggle", ""), + ], + [icon_sun(), icon_toggle_right()], + ), + ]) +} + +// This script is inlined in the response to avoid FOUC when applying the theme +pub const theme_picker_js = " +const mediaPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)'); +const themeStorageKey = 'theme'; + +function getPreferredTheme() { + return mediaPrefersDarkTheme.matches ? 'dark' : 'light'; +} + +function getAppliedTheme() { + return document.documentElement.classList.contains('theme-dark') + ? 'dark' + : 'light'; +} + +function getStoredTheme() { + return localStorage.getItem(themeStorageKey); +} + +function storeTheme(selectedTheme) { + localStorage.setItem(themeStorageKey, selectedTheme); +} + +function syncStoredTheme(theme) { + if (theme === getPreferredTheme()) { + // Selected theme is the same as the device's preferred theme, so we can forget this setting. + localStorage.removeItem(themeStorageKey); + } else { + // Remember the selected theme to apply it on the next visit + storeTheme(theme); + } +} + +function applyTheme(theme, initial = false) { + // abort if theme is already applied + if (theme === getAppliedTheme()) return; + // apply theme css class + document.documentElement.classList.toggle('theme-dark', theme === 'dark'); + document.documentElement.classList.toggle('theme-light', theme !== 'dark'); +} + +function setTheme(theme) { + syncStoredTheme(theme); + applyTheme(theme); +} + +function toggleTheme() { + setTheme(getAppliedTheme() === 'dark' ? 'light' : 'dark'); +} + +function initThemeEvents() { + // Watch the device's preferred theme and update theme if user did not select a theme + mediaPrefersDarkTheme.addEventListener('change', () => { + // abort if the user already selected a theme + if (!!getStoredTheme()) return; + // update applied theme accordingly + applyTheme(getPreferredTheme()); + }); + // Add handlers for theme selection button + document + .querySelector('.theme-picker') + ?.addEventListener('click', toggleTheme); +} + +function initTheme() { + // apply stored or preferred theme + applyTheme(getStoredTheme() ?? getPreferredTheme()); + initThemeEvents(); +} + +initTheme(); +" + +/// Renders an HTML anhor tag +pub fn anchor( + to href: String, + attrs attributes: List(#(String, String)), + with content: List(Html), +) -> Html { + h("a", [#("href", href), ..attributes], content) +} + +pub type Link { + Link(label: String, to: String) +} + +/// Renders a styled text link +pub fn text_link( + for link: Link, + attributes attributes: List(#(String, String)), +) -> Html { + let link_attributes = [#("class", "link"), ..attributes] + + anchor(link.to, link_attributes, [text(link.label)]) +} + +/// Renders the tour's navbar as html +pub fn navbar(titled title: String, links links: List(Link)) -> Html { + let links = list.map(links, fn(l) { text_link(l, []) }) + + let nav_right_items = list.flatten([links, [theme_picker()]]) + + h("nav", [#("class", "navbar")], [ + anchor("/", [#("class", "logo")], [ + h( + "img", + [ + #("src", "https://gleam.run/images/lucy/lucy.svg"), + #("alt", "Lucy the star, Gleam's mascot"), + ], + [], + ), + text(title), + ]), + h("div", [#("class", "nav-right")], nav_right_items), + ]) +} + +/// Renders a horizontal separator +pub fn separator(class: String) -> Html { + h("hr", [#("class", class <> "-separator")], []) +} diff --git a/static/common.css b/static/common.css new file mode 100644 index 0000000..2c9a188 --- /dev/null +++ b/static/common.css @@ -0,0 +1,41 @@ +/* This file contains design tokens used in core Gleam projects */ +:root { + /* Branding */ + --faff-pink: #ffaff3; + --white: #fefefc; + --unnamed-blue: #a6f0fc; + --aged-plastic-yellow: #fffbe8; + --unexpected-aubergine: #584355; + --underwater-blue: #292d3e; + --charcoal: #2f2f2f; + --black: #1e1e1e; + --blacker: #151515; + + /* Other greys */ + --off-white: #f5f5f5; + + /* Other colors */ + --menthol: #c8ffa7; + --caramel: #ffd596; + --deep-saffron: #ff9d35; + --tomato: #ff6262; + + /* Semantic colors */ + --brand-success: var(--menthol); + --brand-warning: var(--caramel); + --brand-error: var(--tomato); + + /* Light theme */ + --light-theme-background: var(--white); + --light-theme-background-dim: var(--off-white); + --light-theme-text: var(--black); + --light-theme-text-secondary: var(--charcoal); + --light-theme-code: var(--black); + + /* Dark theme */ + --dark-theme-background: var(--underwater-blue); + --dark-theme-background-dim: var(--black); + --dark-theme-text: var(--white); + --dark-theme-text-secondary: var(--aged-plastic-yellow); + --dark-theme-code: var(--deep-saffron); +} 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/css/code/color-schemes/atom-one.css b/static/css/code/color-schemes/atom-one.css new file mode 100644 index 0000000..f2a5467 --- /dev/null +++ b/static/css/code/color-schemes/atom-one.css @@ -0,0 +1,76 @@ +/* + +Atom One Dark & Light by Daniel Gamage +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +Tweaked for compat with light / dark themes + +*/ + +:root { + /* + Atom One Dark + + base: #282c34 + mono-1: #abb2bf + mono-2: #818896 + mono-3: #5c6370 + hue-1: #56b6c2 + hue-2: #61aeee + hue-3: #c678dd + hue-4: #98c379 + hue-5: #e06c75 + hue-5-2: #be5046 + hue-6: #d19a66 + hue-6-2: #e6c07b + */ + + --code-background-dark: #282c34; /* base */ + --code-token-base-dark: #abb2bf; /* mono-1 */ + --code-token-punctuation-dark: #818896; /* mono-2 */ + --code-token-operator-dark: #c678dd; /* hue-3 */ + --code-token-keyword-dark: #c678dd; /* hue-3 */ + --code-token-string-dark: #98c379; /* hue-4 */ + --code-token-comment-dark: #5c6370; /* mono-3 */ + --code-token-attribute-dark: #818896; /* mono-2 */ + --code-token-function-dark: #61aeee; /* hue-2 */ + --code-token-function-name-dark: #61aeee; /* hue-2 */ + --code-token-function-param-dark: #abb2bf; /* mono-1 */ + --code-token-boolean-dark: #d19a66; /* hue-6 */ + --code-token-number-dark: #d19a66; /* hue-6 */ + --code-token-selector-dark: #e6c07b; /* hue-6-2 */ + --code-token-type-dark: #56b6c2; /* hue-1 */ + + /* + Atom One Light + + base: #fafafa + mono-1: #383a42 + mono-2: #686b77 + mono-3: #a0a1a7 + hue-1: #0184bb + hue-2: #4078f2 + hue-3: #a626a4 + hue-4: #50a14f + hue-5: #e45649 + hue-5-2: #c91243 + hue-6: #986801 + hue-6-2: #c18401 + */ + + --code-background-light: #fafafa; /* base */ + --code-token-base-light: #383a42; /* mono-1 */ + --code-token-punctuation-light: #686b77; /* mono-2 */ + --code-token-operator-light: #a626a4; /* hue-3 */ + --code-token-keyword-light: #a626a4; /* hue-3 */ + --code-token-string-light: #50a14f; /* hue-4 */ + --code-token-comment-light: #a0a1a7; /* mono-3 */ + --code-token-attribute-light: #686b77; /* mono-2 */ + --code-token-function-light: #4078f2; /* hue-2 */ + --code-token-function-name-light: #4078f2; /* hue-2 */ + --code-token-function-param-light: #383a42; /* mono-1 */ + --code-token-boolean-light: #986801; /* hue-6 */ + --code-token-number-light: #986801; /* hue-6 */ + --code-token-selector-light: #c18401; /* hue-6-2 */ + --code-token-type-light: #0184bb; /* hue-1 */ +} \ No newline at end of file diff --git a/static/css/code/syntax-highlight.css b/static/css/code/syntax-highlight.css new file mode 100644 index 0000000..af9df72 --- /dev/null +++ b/static/css/code/syntax-highlight.css @@ -0,0 +1,196 @@ +/* + +Swiches between code colors based on theme +and binds color scheme values to highlightJS & CodeFlask +defaults to light theme. + +*/ + +:root { + --code-background: var(--code-background-light); + --code-token-base: var(--code-token-base-light); + --code-token-punctuation: var(--code-token-punctuation-light); + --code-token-operator: var(--code-token-operator-light); + --code-token-keyword: var(--code-token-keyword-light); + --code-token-boolean: var(--code-token-boolean-light); + --code-token-number: var(--code-token-number-light); + --code-token-type: var(--code-token-type-light); + --code-token-function-name: var(--code-token-function-name-light); + --code-token-function-param: var(--code-token-function-param-light); + --code-token-attribute: var(--code-token-attribute-light); + --code-token-string: var(--code-token-string-light); + --code-token-function: var(--code-token-function-light); + --code-token-comment: var(--code-token-comment-light); +} + +html.theme-light { + --code-background: var(--code-background-light); + --code-token-base: var(--code-token-base-light); + --code-token-punctuation: var(--code-token-punctuation-light); + --code-token-operator: var(--code-token-operator-light); + --code-token-keyword: var(--code-token-keyword-light); + --code-token-boolean: var(--code-token-boolean-light); + --code-token-number: var(--code-token-number-light); + --code-token-type: var(--code-token-type-light); + --code-token-function-name: var(--code-token-function-name-light); + --code-token-function-param: var(--code-token-function-param-light); + --code-token-attribute: var(--code-token-attribute-light); + --code-token-string: var(--code-token-string-light); + --code-token-function: var(--code-token-function-light); + --code-token-comment: var(--code-token-comment-light); +} + +html.theme-dark { + --code-background: var(--code-background-dark); + --code-token-base: var(--code-token-base-dark); + --code-token-punctuation: var(--code-token-punctuation-dark); + --code-token-operator: var(--code-token-operator-dark); + --code-token-keyword: var(--code-token-keyword-dark); + --code-token-boolean: var(--code-token-boolean-dark); + --code-token-number: var(--code-token-number-dark); + --code-token-type: var(--code-token-type-dark); + --code-token-function-name: var(--code-token-function-name-dark); + --code-token-function-param: var(--code-token-function-param-dark); + --code-token-attribute: var(--code-token-attribute-dark); + --code-token-string: var(--code-token-string-dark); + --code-token-function: var(--code-token-function-dark); + --code-token-comment: var(--code-token-comment-dark); +} + +/* + +highlightJS mappings + +*/ + +pre.hljs { + background: var(--code-background); +} +.hljs { + color: var(--color-token-base); +} +.hljs-punctuation { + /* and operators */ + color: var(--code-token-punctuation); +} +.hljs-variable, +.hljs-name { + color: var(--code-token-base); +} +.hljs-function-param { + font-weight: bold; + font-style: italic; + color: var(--color-token-base); +} +.hljs-operator { + color: var(--code-token-operator); +} +.hljs-keyword { + color: var(--code-token-keyword); +} +.hljs-boolean { + color: var(--code-token-boolean); +} +.hljs-number { + color: var(--code-token-number); +} +.hljs-type { + color: var(--code-token-type); +} +.hljs-function.function-name { + color: var(--code-token-function-name); +} +.hljs-function.function-call { + font-style: italic; +} +.hljs-function.function-params { + color: var(--code-token-function); +} +.hljs-attribute { + color: var(--code-token-attribute); + font-style: italic; +} +.hljs-string { + color: var(--code-token-string); +} +.hljs-comment { + color: var(--code-token-comment); + font-style: italic; +} +/* +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id, +.hljs-title { + color: var(--code-token-function); +} +.hljs-built_in, +.hljs-title.class_, +.hljs-class .hljs-title { + color: var(--code-token-function); +} */ +.hljs-emphasis { + font-style: italic; +} +.hljs-strong { + font-weight: bold; +} +.hljs-link { + text-decoration: underline; +} + +/* + +CodeFlask mappings + +*/ + +.codeflask .codeflask__textarea { + color: var(--code-background); /* Prevents rendering artifacts in dark mode */ + caret-color: var( + --code-token-base + ); /* Makes the text input cursor visible in dark mode */ +} + +.codeflask { + background: var(--code-background); + color: var(--code-token-base); +} +.codeflask .token.punctuation { + color: var(--code-token-punctuation); +} +.codeflask .token.keyword { + color: var(--code-token-keyword); +} +.codeflask .token.operator { + color: var(--code-token-operator); +} +.codeflask .token.string { + color: var(--code-token-string); +} +.codeflask .token.comment { + color: var(--code-token-comment); +} +.codeflask .token.function { + color: var(--code-token-function); +} +.codeflask .token.boolean { + color: var(--code-token-boolean); +} +.codeflask .token.number { + color: var(--code-token-number); +} +.codeflask .token.selector { + color: var(--code-token-selector); +} +.codeflask .token.property { + color: var(--code-token-property); +} +.codeflask .token.tag { + color: var(--code-token-tag); +} +.codeflask .token.attr-value { + color: var(--code-token-attr-value); +} diff --git a/static/css/fonts.css b/static/css/fonts.css new file mode 100644 index 0000000..da3a3f6 --- /dev/null +++ b/static/css/fonts.css @@ -0,0 +1,28 @@ +@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; + + --font-size-normal: calc(var(--gap) * 1.5); + --font-size-small: calc(var(--gap) * 1.2); + --font-size-extra-small: calc(var(--gap)); +} diff --git a/static/css/layout.css b/static/css/layout.css new file mode 100644 index 0000000..3de815f --- /dev/null +++ b/static/css/layout.css @@ -0,0 +1,11 @@ +:root { + --gap: 0.75rem; + --gap-double: calc(2 * var(--gap)); + --gap-triple: calc(3 * var(--gap)); + --gap-quad: calc(4 * var(--gap)); + --gap-half: calc(0.5 * var(--gap)); + --gap-quarter: calc(0.25 * var(--gap)); + + --navbar-height: calc(var(--gap-double) + 20px); + --border-radius: .25rem; +} diff --git a/static/css/pages/everything.css b/static/css/pages/everything.css new file mode 100644 index 0000000..1885078 --- /dev/null +++ b/static/css/pages/everything.css @@ -0,0 +1,354 @@ +/* + * /everything page + * inline chapters & lessons + */ + +body { + /* max-height: 100%; */ + overflow: hidden; + min-height: 100vh; + /* mobile viewport bug fix */ + min-height: -webkit-fill-available; +} + +html { + min-height: -webkit-fill-available; +} + +/* set navbar to position: fixed */ +.navbar { + z-index: 100; + position: fixed; + inset: 0; + bottom: 0; +} + +/* everything wrapper, 2x2 responsive grid layout */ +main#everything { + display: grid; + max-height: 100%; + min-height: -webkit-fill-available; + /* height: -webkit-fill-available; */ + grid-template-columns: minmax(0, max-content) 1fr; + overflow: hidden; + padding-top: var(--navbar-height); +} + +p, +a { + margin: var(--gap) 0; + font-size: var(--font-size-normal); + line-height: var(--gap-double); + font-weight: 400; +} + +code { + font-size: var(--font-size-small); +} + +h1, +h2, +h3 { + margin: 0; +} + +#everything-contents, +#everything-lessons { + grid-column-end: span 1; + grid-row: 1 / span 1; + max-height: 100%; + padding: var(--gap); + padding-bottom: var(--gap-double); + overflow-y: auto; + display: flex; + background: var(--color-background); + flex-direction: column; + position: relative; +} + +/* table of contents on the left, scrollable */ +#everything-contents { + grid-column-start: 1; + padding-bottom: var(--gap-quad); + gap: var(--gap-double); +} + +#everything-contents::before { + position: fixed; +} + +#everything-contents * { + margin: 0; +} + +#everything-contents .chapter { + display: flex; + flex-direction: column; + gap: var(--gap); +} + +#everything-contents .chapter h3 { + white-space: nowrap; +} + +#everything-contents .chapter ul, +#everything-contents .chapter li { + list-style: none; + padding: 0; + color: var(--color-text-secondary); +} + +#everything-contents .chapter ul { + display: flex; + flex-direction: column; + gap: var(--gap-half); +} + +#everything-lessons { + grid-column-start: 2; + container-type: inline-size; + container-name: lessons-list; + padding-top: 0; + scroll-behavior: smooth; + scroll-snap-type: y proximity; + scroll-padding-block-start: calc(2 * var(--navbar-height)); +} + +#everything-lessons .chapter-title { + margin: 0; + padding-top: var(--gap); + color: var(--color-text-accent); + position: sticky; + height: var(--gap-triple); + top: 0; + background: var(--color-background); + z-index: 3; +} + +#everything-lessons .chapter-title:first-child { + margin-top: 0; +} + +#everything-lessons .lesson { + margin: var(--gap) 0; + padding: var(--gap) 0; + padding-bottom: var(--gap-triple); + scroll-snap-align: center top; +} + +#everything-lessons .lesson-title { + position: sticky; + top: var(--gap-triple); + background: var(--color-background); + color: var(--color-link); + z-index: 2; + padding: var(--gap) 0; + margin-bottom: var(--gap-double); +} + +#everything-lessons .lesson:target { + animation: highlight-block 900ms ease-in-out 300ms 1; +} + +#everything-lessons .lesson:target .lesson-title { + animation: highlight-text 900ms ease-in-out 300ms 1; +} + +#everything-lessons .lesson-snippet { + padding: var(--gap); + margin-right: var(--gap); + margin-top: var(--gap-double); + position: relative; + background: var(--code-background); + box-shadow: var(--drop-shadow); +} + +#everything-lessons .lesson-snippet code { + overflow-x: auto; + display: inline-block; + width: 100%; + padding: var(--gap); + padding-bottom: var(--gap-double); +} + +#everything-lessons .lesson-snippet-link { + background: var(--color-accent-muted); + margin: 0; + position: absolute; + bottom: 0; + right: 0; + padding: var(--gap-half) var(--gap); + display: flex; + align-items: center; + justify-content: center; + gap: var(--gap); + font-size: var(--font-size-small); + color: var(--color-link); + border-radius: none; + text-decoration: none; + outline: 1px solid transparent; + outline-offset: -1px; + cursor: pointer; +} + +#everything-lessons hr { + width: 100%; + height: 1px; + border: 0; + padding: 0; + margin: 0; + display: block; +} + +#everything-lessons .lesson-separator { + border-top: 1px solid var(--color-accent-muted); +} + +#everything-lessons .chapter-separator { + border-top: 1px solid var(--color-accent); + position: sticky; + top: calc(var(--gap-triple) - 1px); + z-index: 3; +} + +@media only screen and (min-width: 1100px) { + #everything { + grid-template-columns: minmax(min-content, max-content) 1fr; + } + + #everything-contents { + padding: var(--gap) var(--gap-double) var(--gap-quad) var(--gap); + } + + #everything-contents .chapter ul { + padding-left: var(--gap); + } +} + +/* transform contents as to a side menu */ +@media only screen and (max-width: 768px) { + #everything-contents { + display: none; + } + + .theme-light #everything-contents::after { + filter: contrast(1); + } + + #everything-lessons { + grid-column: 1 / span 2; + padding-bottom: var(--gap-quad); + padding-left: 0; + padding-right: 0; + } + + #everything-lessons .lesson, + #everything-lessons .chapter-title, + #everything-lessons .chapter-separator { + padding-left: var(--gap); + padding-right: var(--gap); + } + + #everything-lessons .lesson-snippet code { + padding: var(--gap); + } +} + +@container lessons-list (min-width: 900px) { + #everything-lessons .lesson, + #everything-lessons .chapter-title { + padding-right: var(--gap-quad); + padding-left: var(--gap-quad); + } + + #everything-lessons .chapter-separator { + margin-left: var(--gap-quad); + margin-right: var(--gap-quad); + width: calc(100% - var(--gap-quad) * 2); + } +} + +@keyframes highlight-text { + 0% { + text-decoration: underline; + text-decoration-color: transparent; + } + + 50% { + text-decoration: underline; + text-decoration-color: var(--color-accent); + color: var(--color-accent-text); + background: var(--color-accent-muted); + } + + 100% { + text-decoration: underline; + text-decoration-color: transparent; + } +} + +@keyframes highlight-block { + 0% { + } + + 50% { + background: var(--color-accent-muted); + } + + 100% { + } +} + +@keyframes reveal { + 0% { + overflow: unset; + } + + 99% { + overflow: unset; + } + + 100% { + overflow-y: auto; + } +} + +@media print { + body#page-everything { + overflow: visible; + float: none; + display: inline; + } + + aside#everything-contents { + display: none; + } + + nav.navbar { + position: unset; + } + + main#everything { + max-height: unset; + overflow: visible; + display: inline; + } + + section#everything-lessons { + max-height: unset; + overflow: visible; + display: inline; + } + + h3.chapter-title:not(:first-child) { + break-before: always; + } + + article.lesson:not(hr.chapter-separator + article.lesson) { + break-inside: avoid; + } + + #everything-lessons .lesson-snippet-link { + display: none; + } +} diff --git a/static/css/pages/lesson.css b/static/css/pages/lesson.css new file mode 100644 index 0000000..b2478de --- /dev/null +++ b/static/css/pages/lesson.css @@ -0,0 +1,132 @@ +#playground { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +#left, +#output>*, +#editor .codeflask__flatten { + padding: var(--gap); +} + +#right { + display: flex; + flex-direction: column; + border: var(--color-divider); + background: var(--code-background); + padding-top: .15rem; + overflow: hidden; + flex-grow: 1; + min-height: fit-content; +} + +#output, +#editor { + border-top: 1px solid var(--color-accent-muted); +} + +#editor { + position: relative; + overflow: clip; + flex-grow: 1; +} + +#output { + min-height: 1rem; + background: var(--color-background-dim); +} + +#output>* { + margin: 0; + white-space: pre-wrap; +} + +/* Larger then mobile */ +@media (min-width: 768px) { + #playground { + min-height: calc(100dvh - var(--navbar-height)); + flex-direction: row; + } + + #left { + height: 100%; + width: 50%; + overflow-y: auto; + + & h2:first-of-type { + margin-top: 0; + } + } + + #right { + border-left: 1px solid var(--color-accent-muted); + width: 50%; + } + + #editor { + border: none; + } + + #output { + height: 33%; + overflow: auto; + border-top: 1px solid var(--color-accent-muted); + } +} + +/* Larger than medium screen and has enough to height to not worry about losing vertical space */ +@media (min-width: 1200px) and (min-height: 700px) { + #left { + /* Lift the navigation bar up a bit so it's not sitting right on the bottom*/ + padding: calc(var(--gap) * 2); + } + + #right { + border-left: unset; + border-radius: var(--border-radius); + padding: 2px 1px; + box-shadow: var(--drop-shadow); + /* Use calc here to add additional padding dynamically to allow for the drop shadow */ + margin-top: calc(var(--gap) * 2); + margin-right: calc(var(--gap) * 3); + margin-bottom: calc(var(--gap) * 3); + margin-left: calc(var(--gap) * 2); + } +} + +.error, +.warning { + border-style: solid; + height: 100%; +} + +.error { + border-color: var(--brand-error); +} + +.warning { + border-color: var(--brand-warning); +} + +.prev-next { + display: flex; + justify-content: center; + align-items: center; + padding: 0 var(--gap); + gap: 0.5em; +} + +.prev-next span { + opacity: 0.5; +} + +.mb-0 { + margin-bottom: 0; +} + +.content-nav { + display: flex; + flex-direction: column; + justify-content: space-between; +} \ No newline at end of file diff --git a/static/css/root.css b/static/css/root.css new file mode 100644 index 0000000..b65b5a8 --- /dev/null +++ b/static/css/root.css @@ -0,0 +1,168 @@ +/* + * Common page styles + * + * used by all pages to ensure consistent styling of common elements + */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + display: flex; + flex-direction: column; + background-color: var(--color-background); + font-family: var(--font-family-normal); + letter-spacing: 0.01em; + color: var(--color-text); +} + +.codeflask__textarea, +pre, +code { + font-weight: normal; + letter-spacing: initial; +} + +p code { + padding: 1px 2px; + color: var(--color-code); + background-color: var(--color-background-dim); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-family-title); + font-weight: normal; +} + +a { + color: var(--color-link); + text-decoration-color: var(--color-link-decoration); +} + +/* + * Nav bar & Nav links + */ + +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + height: var(--navbar-height); + min-height: var(--navbar-height); + padding: var(--gap); + background: var(--color-navbar-background); + color: var(--color-navbar-text); + box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.1); +} + +.navbar .logo { + display: flex; + align-items: center; +} + +.navbar .logo img { + display: inline-block; + height: 2em; + transform: rotate(-10deg); + margin-right: 0.5em; +} + +.navbar a:visited, +.navbar a { + text-decoration: none; + color: var(--color-navbar-link); +} + +.navbar .nav-right { + display: flex; + align-items: center; + gap: var(--gap-double); +} + +/* + * Theme toggle button + */ + +html.theme-dark .theme-button.-dark { + display: none; +} + +html.theme-light .theme-button.-light { + display: none; +} + +.theme-button { + appearance: none; + margin: 0; + border: 0; + padding: 0; + background: none; + color: inherit; + display: flex; + gap: 0.25em; + font-size: inherit; + color: inherit; + cursor: pointer; +} + +.theme-button svg { + display: inline-block; + fill: currentColor; + height: 1em; + width: 1em; +} + +/* + * utility classes + */ + +/* + * dims the background of any element it its applied to + */ + +.dim-bg { + position: relative; +} + +.dim-bg * { + z-index: 1; + position: relative; +} + +.dim-bg::before { + content: ""; + position: absolute; + inset: 0; + background: inherit; + filter: brightness(0.4) saturate(1.3); + z-index: 0; + opacity: 0.3; +} + +.theme-light .dim-bg::before { + filter: brightness(0.8) saturate(1.3); +} + +/* + * highlights an element (usually a link) on hover + */ + +.link { + color: var(--color-text-accent); + text-decoration: underline; + text-decoration-color: var(--color-accent-muted); +} + +.link.padded, +.navbar .link { + padding: calc(var(--gap-quarter) * 0.5) var(--gap-quarter) 0; +} diff --git a/static/css/theme.css b/static/css/theme.css new file mode 100644 index 0000000..1ca496a --- /dev/null +++ b/static/css/theme.css @@ -0,0 +1,55 @@ +/* + +Derives app colors for both dark & light themes from common.css variables + +*/ + +:root { + --hot-pink: #d900b8; + --light-pink: #fedcfb; + --gray-light: #dfdfdf; + + --drop-shadow: 0 0 var(--gap-quarter) var(--color-background), + var(--gap) var(--gap) 0 var(--color-drop-shadow), + inset 0 0 0 1px var(--color-accent-muted); + + --color-navbar-background: var(--faff-pink); + --color-navbar-text: var(--light-theme-text); + --color-navbar-link: var(--light-theme-text); + + --color-accent: var(--faff-pink); + --color-accent-light: var(--light-pink); + --color-accent-hot: var(--hot-pink); + --color-accent-dark: var(--unexpected-aubergine); +} + +html.theme-light { + --color-background: var(--light-theme-background); + --color-background-dim: var(--light-theme-background-dim); + --color-text: var(--light-theme-text); + --color-text-secondary: var(--light-theme-text-secondary); + --color-link: var(--light-theme-text); + --color-link-decoration: var(--faff-pink); + --color-code: var(--light-theme-code); + --color-divider: var(--faff-pink); + --color-drop-shadow: var(--gray-light); + --color-accent-muted: var(--color-accent-light); + --color-text-accent: var(--color-accent-dark); + color-scheme: light; +} + +html.theme-dark { + --color-background: var(--dark-theme-background); + --color-background-dim: var(--dark-theme-background-dim); + --color-text: var(--dark-theme-text); + --color-text-secondary: var(--dark-theme-text-secondary); + --color-link: var(--dark-theme-text); + --color-link-decoration: var(--faff-pink); + --color-code: var(--dark-theme-code); + --color-divider: var(--unexpected-aubergine); + --color-drop-shadow: var(--color-background-dim); + --color-accent-muted: var(--color-accent-dark); + --color-text-accent: var(--color-accent-light); + color-scheme: dark; +} + diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..c09faea --- /dev/null +++ b/static/index.js @@ -0,0 +1,104 @@ +import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; + +console.log(CodeFlask); +globalThis.CodeFlask = CodeFlask; + +const output = document.querySelector("#output"); +const initialCode = document.querySelector("#code").innerHTML; + +const prismGrammar = { + comment: { + pattern: /\/\/.*/, + greedy: true, + }, + function: /([a-z_][a-z0-9_]+)(?=\()/, + keyword: + /\b(use|case|if|@external|@deprecated|fn|import|let|assert|try|pub|type|opaque|const|panic|todo|as)\b/, + symbol: { + pattern: /([A-Z][A-Za-z0-9_]+)/, + greedy: true, + }, + operator: { + pattern: + /(<<|>>|<-|->|\|>|<>|\.\.|<=\.?|>=\.?|==\.?|!=\.?|<\.?|>\.?|&&|\|\||\+\.?|-\.?|\/\.?|\*\.?|%\.?|=)/, + greedy: true, + }, + string: { + pattern: /"((?:[^"\\]|\\.)*)"/, + 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/, +}; + +function clearOutput() { + while (output.firstChild) { + output.removeChild(output.firstChild); + } +} + +function appendOutput(content, className) { + if (!content) return; + const element = document.createElement("pre"); + element.textContent = content; + element.className = className; + output.appendChild(element); +} + +const editor = new CodeFlask("#editor-target", { + language: "gleam", + defaultTheme: false, +}); +editor.addLanguage("gleam", prismGrammar); +editor.updateCode(initialCode); + +function debounce(fn, delay) { + let timer = null; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +} + +// Whether the worker is currently working or not, used to avoid sending +// multiple messages to the worker at once. +// This will be true when the worker is compiling and executing the code, but +// this first time it is as the worker is initialising. +let workerWorking = true; +let queuedWork = undefined; +const worker = new Worker("/worker.js", { type: "module" }); + +function sendToWorker(code) { + if (workerWorking) { + queuedWork = code; + return; + } + workerWorking = true; + worker.postMessage(code); +} + +worker.onmessage = (event) => { + // Handle the result of the compilation and execution + const result = event.data; + clearOutput(); + if (result.log) appendOutput(result.log, "log"); + if (result.error) appendOutput(result.error, "error"); + for (const warning of result.warnings || []) { + appendOutput(warning, "warning"); + } + + // Deal with any queued work + workerWorking = false; + if (queuedWork) sendToWorker(queuedWork); + queuedWork = undefined; +}; + +editor.onUpdate(debounce((code) => sendToWorker(code), 200)); diff --git a/static/js/highlight/highlight-gleam.js b/static/js/highlight/highlight-gleam.js new file mode 100644 index 0000000..cc03f40 --- /dev/null +++ b/static/js/highlight/highlight-gleam.js @@ -0,0 +1,296 @@ +/** + * Registers gleam as a language + * + * Based off https://github.com/gleam-lang/website/blob/main/javascript/highlightjs-gleam.js + * Edited to work with minified hightlightjs core v11 (module) & match more of the syntax + */ + +import hljs from "./highlight.core.min.js"; +import * as regexes from "./regexes.js"; + +/** + * Copies an object to prevent prototype pollution + * @template {object} TObject - the object's structure + * @param {TObject} obj - The source object to copy + * @returns {TObject} - A shallow copy of the source object + */ +const cp = (obj) => ({ ...obj }); + +// Define operators and keywords to highlight gleam code without spawning an editor +const GLEAM_OPERATORS = [ + "<<", + ">>", + "<-", + "->", + "|>", + "<>", + "..", + "<=", + "<=.", + ">=", + ">=.", + "==", + "==.", + "%", + "%.", + "!=", + "!=.", + "<", + "<.", + ">", + ">.", + "&&", + "||", + "+", + "+.", + "-", + "-.", + "/", + "/.", + "*", + "*.", + "=", +]; +const GLEAM_KEYWORDS = [ + "as", + "assert", + "auto", + "case", + "const", + "delegate", + "derive", + "echo", + "else", + "fn", + "if", + "implement", + "import", + "let", + "macro", + "opaque", + "panic", + "pub", + "test", + "todo", + "type", + "use", +]; + +/** + * HLJS modes + * Glorified regular expressions used to target & highlight code snippets + * + * Ordered by `relevance` -> more or less translates to parsing order / priority + * https://highlightjs.readthedocs.io/en/stable/language-guide.html#relevance + * + * Their `scope` maps to 1 or more css class + * https://highlightjs.readthedocs.io/en/stable/css-classes-reference.html + */ + +// Relevance 0 + +const PUNCTUATION = { + name: "punctuation", + scope: "punctuation", + match: regexes.punctuation, + relevance: 0, +}; + +const VARIABLES = { + scope: "variable", + match: regexes.snakeCase, + relevance: 0, +}; + +/** + * TODO: fix regex to not break other selectors + */ +const FUNCTION_PARAM = { + scope: "function-param", + match: regexes.functionParam, + relevance: 0, +}; + +const DISCARD_NAMES = { + scope: "attribute", + begin: regexes.discardName, + relevance: 0, +}; + +const MODULES = { + scope: "module", + match: regexes.importModule, + relevance: 0, +}; + +// Relevance 1 + +const OPERATORS = { + scope: "operator", + begin: regexes.operator, + keywords: { + operator: GLEAM_OPERATORS.join(" "), + $pattern: /\b\S+\b/g, + }, + relevance: 1, +}; + +const KEYWORDS = { + name: "Gleam keywords", + scope: "keyword", + keywords: { + keyword: GLEAM_KEYWORDS.join(" "), + operator: GLEAM_OPERATORS.join(" "), + }, + relevance: 1, +}; + +// Relevance 2 + +const LITERALS = { + name: "Booleans or Nil", + scope: "literal", + match: regexes.literal, + relevance: 2, +}; + +const NUMBERS = { + name: "Number", + scope: "number", + variants: [ + { + begin: regexes.number.binary, + }, + { + begin: regexes.number.octal, + }, + { + begin: regexes.number.hex, + }, + { + begin: regexes.number.decOrFloat, + }, + { + match: regexes.number.scientific, + }, + ], + relevance: 2, +}; + +// Relevance 3 + +const TYPES = { + name: "Types & Aliases", + scope: "type", + match: regexes.type, + relevance: 3, +}; + +// Relevance 4 + +const FUNCTION_CALL = { + name: "Function calls", + scope: "function function-name function-call", + match: regexes.functionCall, + relevance: 4, +}; + +const FUNCTION_DECLARATION = { + name: "function declaration", + scope: "function function-name", + beginKeywords: "fn", + end: regexes.endParenthesis, + returnEnd: true, + relevance: 4, +}; + +// Relevance 5 + +// Relevance 6 + +const ATTRIBUTES = { + name: "Attributes", + scope: "attribute", + match: regexes.attribute, + relevance: 6, +}; + +// Relevance 7 + +const STRINGS = { + name: "Strings", + scope: "string", + variants: [{ begin: /"/, end: /"/ }], + contains: [hljs.BACKSLASH_ESCAPE], + relevance: 7, +}; + +// Relevance 8 + +const BIT_ARRAYS = { + // bit array + begin: "<<", + end: ">>", + scope: "operator", + contains: [ + { + scope: "keyword", + beginKeywords: + "binary bits bytes int float bit_string bit_array bits utf8 utf16 " + + "utf32 utf8_codepoint utf16_codepoint utf32_codepoint signed " + + "unsigned big little native unit size", + }, + cp(KEYWORDS), + cp(STRINGS), + cp(VARIABLES), + cp(DISCARD_NAMES), + cp(NUMBERS), + cp(PUNCTUATION), + ], + relevance: 8, +}; + +// Relevance 10 + +const COMMENTS = { + name: "Comments", + scope: "comment", + match: regexes.comment, + relevance: 10, +}; + +/** + * Register the Gleam lang to HLJS global exported from `./highlight.core.min.js` + */ +hljs.registerLanguage("gleam", function (hljs) { + return { + name: "Gleam", + aliases: ["gleam"], + keywords: { + keyword: KEYWORDS.keywords.keyword, + operator: OPERATORS.keywords.operator, + }, + contains: [ + hljs.C_LINE_COMMENT_MODE, + cp(PUNCTUATION), + cp(MODULES), + cp(DISCARD_NAMES), + cp(OPERATORS), + cp(LITERALS), + cp(NUMBERS), + cp(TYPES), + cp(FUNCTION_DECLARATION), + cp(FUNCTION_CALL), + cp(ATTRIBUTES), + cp(STRINGS), + cp(COMMENTS), + ], + }; +}); + +/** + * Wait until other scripts & css are loaded before highlighting + */ +addEventListener("DOMContentLoaded", () => { + hljs.highlightAll(); +}); diff --git a/static/js/highlight/highlight.core.min.js b/static/js/highlight/highlight.core.min.js new file mode 100644 index 0000000..d4dd62e --- /dev/null +++ b/static/js/highlight/highlight.core.min.js @@ -0,0 +1,307 @@ +/*! + Highlight.js v11.9.0 (git: b7ec4bfafc) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ + function e(t){return t instanceof Map?t.clear=t.delete=t.set=()=>{ + throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ + throw Error("set is read-only") + }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ + const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) + })),t}class t{constructor(e){ + void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} + ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ + return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") + }function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] + ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope + ;class r{constructor(e,t){ + this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ + this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ + if(e.startsWith("language:"))return e.replace("language:","language-") + ;if(e.includes(".")){const n=e.split(".") + ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") + }return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} + closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ + this.buffer+=``}}const o=(e={})=>{const t={children:[]} + ;return Object.assign(t,e),t};class a{constructor(){ + this.rootNode=o(),this.stack=[this.rootNode]}get top(){ + return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ + this.top.children.push(e)}openNode(e){const t=o({scope:e}) + ;this.add(t),this.stack.push(t)}closeNode(){ + if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ + for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} + walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ + return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), + t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ + "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ + a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} + addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ + this.closeNode()}__addSublanguage(e,t){const n=e.root + ;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ + return new r(this,this.options).value()}finalize(){ + return this.closeAllNodes(),!0}}function l(e){ + return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} + function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} + function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ + const t=e[e.length-1] + ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} + })(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} + function p(e){return RegExp(e.toString()+"|").exec("").length-1} + const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ + ;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n + ;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} + s+=i.substring(0,e.index), + i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], + "("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} + const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={ + begin:"\\\\[\\s\\S]",relevance:0},k={scope:"string",begin:"'",end:"'", + illegal:"\\n",contains:[O]},v={scope:"string",begin:'"',end:'"',illegal:"\\n", + contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, + contains:[]},n);s.contains.push({scope:"doctag", + begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) + ;const r=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) + ;return s.contains.push({begin:h(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s + },S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var A=Object.freeze({ + __proto__:null,APOS_STRING_MODE:k,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ + scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N, + C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", + begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{ + "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ + t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, + MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, + NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w, + PHRASAL_WORDS_MODE:{ + begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + },QUOTE_STRING_MODE:v,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, + end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, + RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", + SHEBANG:(e={})=>{const t=/^#![ ]*\// + ;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, + end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, + TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, + UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function j(e,t){ + "."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ + void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ + t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", + e.__beforeBegin=j,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, + void 0===e.relevance&&(e.relevance=0))}function L(e,t){ + Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ + if(e.match){ + if(e.begin||e.end)throw Error("begin & end are not supported with match") + ;e.begin=e.match,delete e.match}}function P(e,t){ + void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return + ;if(e.starts)throw Error("beforeMatch cannot be used with starts") + ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] + })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ + relevance:0,contains:[Object.assign(n,{endsParent:!0})] + },e.relevance=0,delete n.beforeMatch + },H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword" + ;function $(e,t,n=C){const i=Object.create(null) + ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ + Object.assign(i,$(e[n],t,n))})),i;function s(e,n){ + t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") + ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ + return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{ + console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{ + z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) + },K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],r={},o={} + ;for(let e=1;e<=t.length;e++)o[e+i]=s[e],r[e+i]=!0,i+=p(t[e-1]) + ;e[n]=o,e[n]._emit=r,e[n]._multi=!0}function Z(e){(e=>{ + e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, + delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ + _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope + }),(e=>{if(Array.isArray(e.begin)){ + if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), + K + ;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"), + K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ + if(Array.isArray(e.end)){ + if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"), + K + ;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"), + K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){ + function t(t,n){ + return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) + }class n{constructor(){ + this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} + addRule(e,t){ + t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), + this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) + ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" + }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex + ;const t=this.matcherRe.exec(e);if(!t)return null + ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] + ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ + this.rules=[],this.multiRegexes=[], + this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ + if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n + ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), + t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ + return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ + this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ + const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex + ;let n=t.exec(e) + ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ + const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} + return n&&(this.regexIndex+=n.position+1, + this.regexIndex===this.count&&this.considerAll()),n}} + if(e.compilerExtensions||(e.compilerExtensions=[]), + e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") + ;return e.classNameAliases=i(e.classNameAliases||{}),function n(r,o){const a=r + ;if(r.isCompiled)return a + ;[I,B,Z,D].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))), + r.__beforeBegin=null,[T,L,P].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null + ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), + c=r.keywords.$pattern, + delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=$(r.keywords,e.case_insensitive)), + a.keywordPatternRe=t(c,!0), + o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), + r.end&&(a.endRe=t(a.end)), + a.terminatorEnd=l(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)), + r.illegal&&(a.illegalRe=t(r.illegal)), + r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ + variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{ + starts:e.starts?i(e.starts):null + }):Object.isFrozen(e)?i(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a) + })),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new s + ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" + }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" + }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ + return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ + constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} + const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{ + const i=Object.create(null),s=Object.create(null),r=[];let o=!0 + ;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ + disableAutodetect:!0,name:"Plain text",contains:[]};let p={ + ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, + languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", + cssSelector:"pre code",languages:null,__emitter:c};function b(e){ + return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" + ;"object"==typeof t?(i=e, + n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."), + G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), + s=e,i=t),void 0===n&&(n=!0);const r={code:i,language:s};N("before:highlight",r) + ;const o=r.result?r.result:E(r.language,r.code,n) + ;return o.code=r.code,N("after:highlight",o),o}function E(e,n,s,r){ + const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) + ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" + ;for(;t;){n+=R.substring(e,t.index) + ;const s=_.case_insensitive?t[0].toLowerCase():t[0],r=(i=s,N.keywords[i]);if(r){ + const[e,i]=r + ;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(A+=i),e.startsWith("_"))n+=t[0];else{ + const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] + ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i + ;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ + if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ + if(!i[N.subLanguage])return void M.addText(R) + ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top + }else e=x(R,N.subLanguage.length?N.subLanguage:null) + ;N.relevance>0&&(A+=e.relevance),M.__addSublanguage(e._emitter,e.language) + })():l(),R=""}function u(e,t){ + ""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 + ;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} + const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} + function h(e,t){ + return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope), + e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), + R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ + value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) + ;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) + ;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ + for(;e.endsParent&&e.parent;)e=e.parent;return e}} + if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ + return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ + const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const r=N + ;N.endScope&&N.endScope._wrap?(g(), + u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), + d(N.endScope,e)):r.skip?R+=t:(r.returnEnd||r.excludeEnd||(R+=t), + g(),r.excludeEnd&&(R=t));do{ + N.scope&&M.closeNode(),N.skip||N.subLanguage||(A+=N.relevance),N=N.parent + }while(N!==s.parent);return s.starts&&h(s.starts,e),r.returnEnd?0:t.length} + let w={};function y(i,r){const a=r&&r[0];if(R+=i,null==a)return g(),0 + ;if("begin"===w.type&&"end"===r.type&&w.index===r.index&&""===a){ + if(R+=n.slice(r.index,r.index+1),!o){const t=Error(`0 width match regex (${e})`) + ;throw t.languageName=e,t.badRule=w.rule,t}return 1} + if(w=r,"begin"===r.type)return(e=>{ + const n=e[0],i=e.rule,s=new t(i),r=[i.__beforeBegin,i["on:begin"]] + ;for(const t of r)if(t&&(t(e,s),s.isMatchIgnored))return b(n) + ;return i.skip?R+=n:(i.excludeBegin&&(R+=n), + g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(r) + ;if("illegal"===r.type&&!s){ + const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') + ;throw e.mode=N,e}if("end"===r.type){const e=m(r);if(e!==ee)return e} + if("illegal"===r.type&&""===a)return 1 + ;if(I>1e5&&I>3*r.index)throw Error("potential infinite loop, way more iterations than matches") + ;return R+=a,a.length}const _=O(e) + ;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"') + ;const k=V(_);let v="",N=r||k;const S={},M=new p.__emitter(p);(()=>{const e=[] + ;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) + ;e.forEach((e=>M.openNode(e)))})();let R="",A=0,j=0,I=0,T=!1;try{ + if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ + I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=j + ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(j,e.index),e) + ;j=e.index+t}y(n.substring(j))}return M.finalize(),v=M.toHTML(),{language:e, + value:v,relevance:A,illegal:!1,_emitter:M,_top:N}}catch(t){ + if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), + illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j, + context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:v},_emitter:M};if(o)return{ + language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} + ;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ + const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} + ;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(v).map((t=>E(t,e,!1))) + ;s.unshift(n);const r=s.sort(((e,t)=>{ + if(e.relevance!==t.relevance)return t.relevance-e.relevance + ;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 + ;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=r,c=o + ;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{ + let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" + ;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) + ;return t||(X(a.replace("{}",n[1])), + X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} + return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return + ;if(N("before:highlightElement",{el:e,language:n + }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) + ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), + console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), + console.warn("The element with unescaped HTML:"), + console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) + ;t=e;const i=t.textContent,r=n?m(i,{language:n,ignoreIllegals:!0}):x(i) + ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n + ;e.classList.add("hljs"),e.classList.add("language-"+i) + })(e,n,r.language),e.result={language:r.language,re:r.relevance, + relevance:r.relevance},r.secondBest&&(e.secondBest={ + language:r.secondBest.language,relevance:r.secondBest.relevance + }),N("after:highlightElement",{el:e,result:r,text:i})}let y=!1;function _(){ + "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0 + }function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} + function k(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ + s[e.toLowerCase()]=t}))}function v(e){const t=O(e) + ;return t&&!t.disableAutodetect}function N(e,t){const n=e;r.forEach((e=>{ + e[n]&&e[n](t)}))} + "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ + y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_, + highlightElement:w, + highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"), + G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)}, + initHighlighting:()=>{ + _(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, + initHighlightingOnLoad:()=>{ + _(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") + },registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ + if(W("Language definition for '{}' could not be registered.".replace("{}",e)), + !o)throw t;W(t),s=l} + s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&k(s.aliases,{ + languageName:e})},unregisterLanguage:e=>{delete i[e] + ;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, + listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:k, + autoDetection:v,inherit:Q,addPlugin:e=>{(e=>{ + e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ + e["before:highlightBlock"](Object.assign({block:t.el},t)) + }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ + e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),r.push(e)}, + removePlugin:e=>{const t=r.indexOf(e);-1!==t&&r.splice(t,1)}}),n.debugMode=()=>{ + o=!1},n.safeMode=()=>{o=!0},n.versionString="11.9.0",n.regex={concat:h, + lookahead:g,either:f,optional:d,anyNumberOfTimes:u} + ;for(const t in A)"object"==typeof A[t]&&e(A[t]);return Object.assign(n,A),n + },ne=te({});ne.newInstance=()=>te({});export{ne as default}; \ No newline at end of file diff --git a/static/js/highlight/regexes.js b/static/js/highlight/regexes.js new file mode 100644 index 0000000..4477a98 --- /dev/null +++ b/static/js/highlight/regexes.js @@ -0,0 +1,20 @@ + +export const snakeCase = /\b(?:_)*[a-z_]+[a-z\d_]+\b/g; +export const punctuation = /[\\.,\(\):\[\]{}#]/g; +export const endParenthesis = /\(/g; +export const importModule = /(?<=import )\b(?:_)*[a-z_]+[a-z\d_]+\b/g +export const functionCall = /\b(?:_)*[a-z_]+[a-z\d_]+\b(?=\()/g; +export const functionParam = /(?<=\b(?:_)*[a-z_]+[a-z\d_]+\b\()(.|\s|\n)*(?=\))/g; +export const operator = /(<<|>>|<-|->|\|>|<>|\.\.|<=\.?|>=\.?|==\.?|!=\.?|<\.?|>\.?|&&|\|\||\+\.?|-\.?|\/\.?|\*\.?|%\.?|=)/g; +export const type = /\b[A-Z]{1}(?:[a-z]+[A-Z]{0,1})*\b/g; +export const literal = /\b(True|False|Nil)\b/g; +export const comment = /\/\/.*/g; +export const attribute = /@[a-zA-Z0-9]+\b(?=\()/g; +export const discardName = /\b_[a-z][a-z0-9_]*\b/g; +export const number = { + binary: "\\b0[bB](?:_?[01]+)+", + octal: "\\b0[oO](?:_?[0-7]+)+", + hex: "\\b0[xX](?:_?[0-9a-fA-F]+)+", + decOrFloat: /\b\d(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?/g, + scientific: /(?:(?:-\d)|\d)(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?e(?:(?:-\d)|\d)(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?/g +} \ No newline at end of file diff --git a/static/precompiled/my_package_ffi.mjs b/static/precompiled/my_package_ffi.mjs new file mode 100644 index 0000000..3c8e2f8 --- /dev/null +++ b/static/precompiled/my_package_ffi.mjs @@ -0,0 +1,3 @@ +export function now() { + return new Date(); +} diff --git a/static/worker.js b/static/worker.js new file mode 100644 index 0000000..7a3db05 --- /dev/null +++ b/static/worker.js @@ -0,0 +1,67 @@ +import initGleamCompiler from "./compiler.js"; +import stdlib from "./stdlib.js"; + +const compiler = await initGleamCompiler(); +const project = compiler.newProject(); + +for (const [name, code] of Object.entries(stdlib)) { + project.writeModule(name, code); +} + +// 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"; +}; + +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; +} + +async function compileEval(code) { + logged = ""; + const result = { + log: null, + error: null, + warnings: [], + }; + + try { + project.writeModule("main", code); + project.compilePackage("javascript"); + const js = project.readCompiledJavaScript("main"); + const main = await loadProgram(js); + if (main) main(); + } catch (error) { + console.error(error); + result.error = error.toString(); + } + for (const warning of project.takeWarnings()) { + result.warnings.push(warning); + } + result.log = logged; + + return result; +} + +self.onmessage = async (event) => { + const result = compileEval(event.data); + postMessage(await result); +}; + +// Send an initial message to the main thread to indicate that the worker is +// ready to receive messages. +postMessage({}); diff --git a/test/tour_test.gleam b/test/tour_test.gleam new file mode 100644 index 0000000..67c2bd5 --- /dev/null +++ b/test/tour_test.gleam @@ -0,0 +1,11 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +pub fn hello_world_test() { + 1 + |> should.equal(1) +} From 45f1c769dc1fd6572064e99333bcf085404d7bfc Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Mon, 6 May 2024 15:06:20 +0100 Subject: [PATCH 02/18] playground: get SSG working --- .../lesson01_hello_world/code.gleam | 5 - .../lesson01_hello_world/en.html | 14 - .../lesson02_modules/code.gleam | 10 - .../chapter0_basics/lesson02_modules/en.html | 37 - .../lesson03_unqualified_imports/code.gleam | 10 - .../lesson03_unqualified_imports/en.html | 17 - .../lesson04_type_checking/code.gleam | 7 - .../lesson04_type_checking/en.html | 28 - .../chapter0_basics/lesson05_ints/code.gleam | 25 - .../chapter0_basics/lesson05_ints/en.html | 17 - .../lesson06_floats/code.gleam | 27 - .../chapter0_basics/lesson06_floats/en.html | 32 - .../lesson07_number_formats/code.gleam | 16 - .../lesson07_number_formats/en.html | 13 - .../lesson08_equality/code.gleam | 6 - .../chapter0_basics/lesson08_equality/en.html | 13 - .../lesson09_strings/code.gleam | 23 - .../chapter0_basics/lesson09_strings/en.html | 26 - .../chapter0_basics/lesson10_bools/code.gleam | 14 - .../chapter0_basics/lesson10_bools/en.html | 20 - .../lesson11_assignments/code.gleam | 17 - .../lesson11_assignments/en.html | 9 - .../lesson12_discard_patterns/code.gleam | 4 - .../lesson12_discard_patterns/en.html | 10 - .../lesson13_type_annotations/code.gleam | 7 - .../lesson13_type_annotations/en.html | 15 - .../lesson14_type_imports/code.gleam | 10 - .../lesson14_type_imports/en.html | 22 - .../lesson15_type_aliases/code.gleam | 12 - .../lesson15_type_aliases/en.html | 12 - .../lesson16_blocks/code.gleam | 13 - .../chapter0_basics/lesson16_blocks/en.html | 26 - .../chapter0_basics/lesson17_lists/code.gleam | 16 - .../chapter0_basics/lesson17_lists/en.html | 21 - .../lesson18_constants/code.gleam | 13 - .../lesson18_constants/en.html | 18 - .../lesson00_functions/code.gleam | 13 - .../lesson00_functions/en.html | 14 - .../code.gleam | 18 - .../lesson03_higher_order_functions/en.html | 12 - .../lesson04_anonymous_functions/code.gleam | 19 - .../lesson04_anonymous_functions/en.html | 9 - .../lesson05_function_captures/code.gleam | 14 - .../lesson05_function_captures/en.html | 12 - .../lesson06_generic_functions/code.gleam | 20 - .../lesson06_generic_functions/en.html | 26 - .../lesson07_pipelines/code.gleam | 19 - .../lesson07_pipelines/en.html | 25 - .../lesson08_labelled_arguments/code.gleam | 16 - .../lesson08_labelled_arguments/en.html | 23 - .../code.gleam | 19 - .../lesson09_documentation_comments/en.html | 16 - .../lesson10_deprecations/code.gleam | 13 - .../lesson10_deprecations/en.html | 14 - .../lesson01_case_expressions/code.gleam | 17 - .../lesson01_case_expressions/en.html | 18 - .../lesson02_variable_patterns/code.gleam | 14 - .../lesson02_variable_patterns/en.html | 7 - .../lesson03_string_patterns/code.gleam | 14 - .../lesson03_string_patterns/en.html | 9 - .../lesson04_list_patterns/code.gleam | 17 - .../lesson04_list_patterns/en.html | 15 - .../lesson05_recursion/code.gleam | 18 - .../lesson05_recursion/en.html | 20 - .../lesson06_tail_calls/code.gleam | 22 - .../lesson06_tail_calls/en.html | 23 - .../lesson07_list_recursion/code.gleam | 13 - .../lesson07_list_recursion/en.html | 19 - .../lesson08_multiple_subjects/code.gleam | 17 - .../lesson08_multiple_subjects/en.html | 13 - .../lesson09_alternative_patterns/code.gleam | 14 - .../lesson09_alternative_patterns/en.html | 12 - .../lesson10_pattern_aliases/code.gleam | 15 - .../lesson10_pattern_aliases/en.html | 7 - .../lesson11_guards/code.gleam | 15 - .../lesson11_guards/en.html | 9 - .../lesson00_tuples/code.gleam | 10 - .../lesson00_tuples/en.html | 20 - .../lesson01_custom_types/code.gleam | 22 - .../lesson01_custom_types/en.html | 11 - .../lesson02_records/code.gleam | 17 - .../lesson02_records/en.html | 13 - .../lesson03_record_accessors/code.gleam | 15 - .../lesson03_record_accessors/en.html | 18 - .../lesson04_record_updates/code.gleam | 15 - .../lesson04_record_updates/en.html | 8 - .../lesson05_generic_custom_types/code.gleam | 10 - .../lesson05_generic_custom_types/en.html | 12 - .../lesson06_nil/code.gleam | 11 - .../chapter3_data_types/lesson06_nil/en.html | 15 - .../lesson07_results/code.gleam | 25 - .../lesson07_results/en.html | 39 - .../lesson08_bit_arrays/code.gleam | 13 - .../lesson08_bit_arrays/en.html | 44 - .../code.gleam | 6 - .../lesson00_standard_library_package/en.html | 17 - .../lesson01_list_module/code.gleam | 19 - .../lesson01_list_module/en.html | 42 - .../lesson02_result_module/code.gleam | 24 - .../lesson02_result_module/en.html | 40 - .../lesson03_dict_module/code.gleam | 14 - .../lesson03_dict_module/en.html | 40 - .../lesson04_option_module/code.gleam | 14 - .../lesson04_option_module/en.html | 19 - .../lesson00_opaque_types/code.gleam | 26 - .../lesson00_opaque_types/en.html | 17 - .../lesson01_use/code.gleam | 38 - .../lesson01_use/en.html | 30 - .../lesson02_use_sugar/code.gleam | 30 - .../lesson02_use_sugar/en.html | 30 - .../lesson03_todo/code.gleam | 7 - .../lesson03_todo/en.html | 14 - .../lesson04_panic/code.gleam | 15 - .../lesson04_panic/en.html | 11 - .../lesson05_let_assert/code.gleam | 16 - .../lesson05_let_assert/en.html | 15 - .../lesson06_externals/code.gleam | 17 - .../lesson06_externals/en.html | 25 - .../code.gleam | 11 - .../lesson07_multi_target_externals/en.html | 22 - .../code.gleam | 18 - .../lesson08_external_gleam_fallbacks/en.html | 13 - src/pages/about/en.html | 0 src/pages/en.html | 8 + src/playground.gleam | 806 ++---------------- src/playground/html.gleam | 225 +++++ static/css/pages/everything.css | 354 -------- 127 files changed, 322 insertions(+), 3194 deletions(-) delete mode 100644 src/content/chapter0_basics/lesson01_hello_world/code.gleam delete mode 100644 src/content/chapter0_basics/lesson01_hello_world/en.html delete mode 100644 src/content/chapter0_basics/lesson02_modules/code.gleam delete mode 100644 src/content/chapter0_basics/lesson02_modules/en.html delete mode 100644 src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam delete mode 100644 src/content/chapter0_basics/lesson03_unqualified_imports/en.html delete mode 100644 src/content/chapter0_basics/lesson04_type_checking/code.gleam delete mode 100644 src/content/chapter0_basics/lesson04_type_checking/en.html delete mode 100644 src/content/chapter0_basics/lesson05_ints/code.gleam delete mode 100644 src/content/chapter0_basics/lesson05_ints/en.html delete mode 100644 src/content/chapter0_basics/lesson06_floats/code.gleam delete mode 100644 src/content/chapter0_basics/lesson06_floats/en.html delete mode 100644 src/content/chapter0_basics/lesson07_number_formats/code.gleam delete mode 100644 src/content/chapter0_basics/lesson07_number_formats/en.html delete mode 100644 src/content/chapter0_basics/lesson08_equality/code.gleam delete mode 100644 src/content/chapter0_basics/lesson08_equality/en.html delete mode 100644 src/content/chapter0_basics/lesson09_strings/code.gleam delete mode 100644 src/content/chapter0_basics/lesson09_strings/en.html delete mode 100644 src/content/chapter0_basics/lesson10_bools/code.gleam delete mode 100644 src/content/chapter0_basics/lesson10_bools/en.html delete mode 100644 src/content/chapter0_basics/lesson11_assignments/code.gleam delete mode 100644 src/content/chapter0_basics/lesson11_assignments/en.html delete mode 100644 src/content/chapter0_basics/lesson12_discard_patterns/code.gleam delete mode 100644 src/content/chapter0_basics/lesson12_discard_patterns/en.html delete mode 100644 src/content/chapter0_basics/lesson13_type_annotations/code.gleam delete mode 100644 src/content/chapter0_basics/lesson13_type_annotations/en.html delete mode 100644 src/content/chapter0_basics/lesson14_type_imports/code.gleam delete mode 100644 src/content/chapter0_basics/lesson14_type_imports/en.html delete mode 100644 src/content/chapter0_basics/lesson15_type_aliases/code.gleam delete mode 100644 src/content/chapter0_basics/lesson15_type_aliases/en.html delete mode 100644 src/content/chapter0_basics/lesson16_blocks/code.gleam delete mode 100644 src/content/chapter0_basics/lesson16_blocks/en.html delete mode 100644 src/content/chapter0_basics/lesson17_lists/code.gleam delete mode 100644 src/content/chapter0_basics/lesson17_lists/en.html delete mode 100644 src/content/chapter0_basics/lesson18_constants/code.gleam delete mode 100644 src/content/chapter0_basics/lesson18_constants/en.html delete mode 100644 src/content/chapter1_functions/lesson00_functions/code.gleam delete mode 100644 src/content/chapter1_functions/lesson00_functions/en.html delete mode 100644 src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam delete mode 100644 src/content/chapter1_functions/lesson03_higher_order_functions/en.html delete mode 100644 src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam delete mode 100644 src/content/chapter1_functions/lesson04_anonymous_functions/en.html delete mode 100644 src/content/chapter1_functions/lesson05_function_captures/code.gleam delete mode 100644 src/content/chapter1_functions/lesson05_function_captures/en.html delete mode 100644 src/content/chapter1_functions/lesson06_generic_functions/code.gleam delete mode 100644 src/content/chapter1_functions/lesson06_generic_functions/en.html delete mode 100644 src/content/chapter1_functions/lesson07_pipelines/code.gleam delete mode 100644 src/content/chapter1_functions/lesson07_pipelines/en.html delete mode 100644 src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam delete mode 100644 src/content/chapter1_functions/lesson08_labelled_arguments/en.html delete mode 100644 src/content/chapter1_functions/lesson09_documentation_comments/code.gleam delete mode 100644 src/content/chapter1_functions/lesson09_documentation_comments/en.html delete mode 100644 src/content/chapter1_functions/lesson10_deprecations/code.gleam delete mode 100644 src/content/chapter1_functions/lesson10_deprecations/en.html delete mode 100644 src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson01_case_expressions/en.html delete mode 100644 src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson02_variable_patterns/en.html delete mode 100644 src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson03_string_patterns/en.html delete mode 100644 src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson04_list_patterns/en.html delete mode 100644 src/content/chapter2_flow_control/lesson05_recursion/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson05_recursion/en.html delete mode 100644 src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson06_tail_calls/en.html delete mode 100644 src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson07_list_recursion/en.html delete mode 100644 src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html delete mode 100644 src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html delete mode 100644 src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html delete mode 100644 src/content/chapter2_flow_control/lesson11_guards/code.gleam delete mode 100644 src/content/chapter2_flow_control/lesson11_guards/en.html delete mode 100644 src/content/chapter3_data_types/lesson00_tuples/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson00_tuples/en.html delete mode 100644 src/content/chapter3_data_types/lesson01_custom_types/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson01_custom_types/en.html delete mode 100644 src/content/chapter3_data_types/lesson02_records/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson02_records/en.html delete mode 100644 src/content/chapter3_data_types/lesson03_record_accessors/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson03_record_accessors/en.html delete mode 100644 src/content/chapter3_data_types/lesson04_record_updates/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson04_record_updates/en.html delete mode 100644 src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson05_generic_custom_types/en.html delete mode 100644 src/content/chapter3_data_types/lesson06_nil/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson06_nil/en.html delete mode 100644 src/content/chapter3_data_types/lesson07_results/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson07_results/en.html delete mode 100644 src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam delete mode 100644 src/content/chapter3_data_types/lesson08_bit_arrays/en.html delete mode 100644 src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam delete mode 100644 src/content/chapter4_standard_library/lesson00_standard_library_package/en.html delete mode 100644 src/content/chapter4_standard_library/lesson01_list_module/code.gleam delete mode 100644 src/content/chapter4_standard_library/lesson01_list_module/en.html delete mode 100644 src/content/chapter4_standard_library/lesson02_result_module/code.gleam delete mode 100644 src/content/chapter4_standard_library/lesson02_result_module/en.html delete mode 100644 src/content/chapter4_standard_library/lesson03_dict_module/code.gleam delete mode 100644 src/content/chapter4_standard_library/lesson03_dict_module/en.html delete mode 100644 src/content/chapter4_standard_library/lesson04_option_module/code.gleam delete mode 100644 src/content/chapter4_standard_library/lesson04_option_module/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson00_opaque_types/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson01_use/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson01_use/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson02_use_sugar/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson03_todo/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson03_todo/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson04_panic/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson04_panic/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson05_let_assert/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson06_externals/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson06_externals/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html delete mode 100644 src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam delete mode 100644 src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html create mode 100644 src/pages/about/en.html create mode 100644 src/pages/en.html create mode 100644 src/playground/html.gleam delete mode 100644 static/css/pages/everything.css diff --git a/src/content/chapter0_basics/lesson01_hello_world/code.gleam b/src/content/chapter0_basics/lesson01_hello_world/code.gleam deleted file mode 100644 index 30530b2..0000000 --- a/src/content/chapter0_basics/lesson01_hello_world/code.gleam +++ /dev/null @@ -1,5 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("Hello, Joe!") -} diff --git a/src/content/chapter0_basics/lesson01_hello_world/en.html b/src/content/chapter0_basics/lesson01_hello_world/en.html deleted file mode 100644 index b555160..0000000 --- a/src/content/chapter0_basics/lesson01_hello_world/en.html +++ /dev/null @@ -1,14 +0,0 @@ -

- Here is a tiny program that prints out the text "Hello, Joe!". We'll explain - how it works shortly. -

-

- In a normal Gleam project this program would be run using the command - gleam run on the command line, but here in this tour the program - is compiled and run inside your web browser, allowing you to try Gleam without - installing anything on your computer. -

-

- Try changing the text being printed to Hello, Mike! and see what - happens. -

diff --git a/src/content/chapter0_basics/lesson02_modules/code.gleam b/src/content/chapter0_basics/lesson02_modules/code.gleam deleted file mode 100644 index e258c33..0000000 --- a/src/content/chapter0_basics/lesson02_modules/code.gleam +++ /dev/null @@ -1,10 +0,0 @@ -import gleam/io -import gleam/string as text - -pub fn main() { - // Use a function from the `gleam/io` module - io.println("Hello, Mike!") - - // Use a function from the `gleam/string` module - io.println(text.reverse("Hello, Joe!")) -} diff --git a/src/content/chapter0_basics/lesson02_modules/en.html b/src/content/chapter0_basics/lesson02_modules/en.html deleted file mode 100644 index 7a2d0ca..0000000 --- a/src/content/chapter0_basics/lesson02_modules/en.html +++ /dev/null @@ -1,37 +0,0 @@ -

- Gleam code is organized into units called modules. A module is a - bunch of definitions (of types, functions, etc.) that seem to belong together. - For example, the - - gleam/io - - module contains a variety of functions for printing, like - - println - . -

-

- All gleam code is in some module or other, whose name comes from the - name of the file it's in. For example, - - gleam/io - - is in a file called io.gleam in a directory called gleam. -

-

- For code in one module to access code in another module, we import it using - the import keyword, and the name used to refer to it is the last - part of the module name. For example, the - - gleam/io - - module is referred to as io once imported. -

-

- The as keyword can be used to refer to a module by a different - name. See how the - - gleam/string - - module is referred to as text here. -

diff --git a/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam b/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam deleted file mode 100644 index 2708f25..0000000 --- a/src/content/chapter0_basics/lesson03_unqualified_imports/code.gleam +++ /dev/null @@ -1,10 +0,0 @@ -// Import the module and one of its functions -import gleam/io.{println} - -pub fn main() { - // Use the function in a qualified fashion - io.println("This is qualified") - - // Or an unqualified fashion - println("This is unqualified") -} diff --git a/src/content/chapter0_basics/lesson03_unqualified_imports/en.html b/src/content/chapter0_basics/lesson03_unqualified_imports/en.html deleted file mode 100644 index 35ea10f..0000000 --- a/src/content/chapter0_basics/lesson03_unqualified_imports/en.html +++ /dev/null @@ -1,17 +0,0 @@ -

- Normally functions from other modules are used in a - qualified fashion, meaning the name used to refer the module goes - before function name with a dot between them. For example, - - io.println("Hello!") - . -

-

- It is also possible to specify a list of functions to import from a module in - an unqualified fashion, meaning the function name can be used without - the module qualifier (the name and the dot) before it. -

-

- Generally it is best to use qualified imports, as this makes it clear where - the function is defined, making the code easier to read. -

diff --git a/src/content/chapter0_basics/lesson04_type_checking/code.gleam b/src/content/chapter0_basics/lesson04_type_checking/code.gleam deleted file mode 100644 index e068f31..0000000 --- a/src/content/chapter0_basics/lesson04_type_checking/code.gleam +++ /dev/null @@ -1,7 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("My lucky number is:") - // io.println(4) - // ๐Ÿ‘†๏ธ Uncomment this line -} diff --git a/src/content/chapter0_basics/lesson04_type_checking/en.html b/src/content/chapter0_basics/lesson04_type_checking/en.html deleted file mode 100644 index 0cffcbe..0000000 --- a/src/content/chapter0_basics/lesson04_type_checking/en.html +++ /dev/null @@ -1,28 +0,0 @@ -

- Gleam has a robust static type system that helps you as you write and edit - code, catching mistakes and showing you where to make changes. -

-

- Uncomment the line - - io.println(4) - - and see how a compile time error - is reported as the - - io.println - - function only works with strings, not ints. -

-

- To fix the code change the code to call the - - io.debug - - function instead, as it will print a value of any type. -

-

- Gleam has no null, no implicit conversions, no exceptions, and - always performs full type checking. If the code compiles you can be reasonably - confident it does not have any inconsistencies that may cause bugs or crashes. -

diff --git a/src/content/chapter0_basics/lesson05_ints/code.gleam b/src/content/chapter0_basics/lesson05_ints/code.gleam deleted file mode 100644 index 120b342..0000000 --- a/src/content/chapter0_basics/lesson05_ints/code.gleam +++ /dev/null @@ -1,25 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - // Int arithmetic - io.debug(1 + 1) - io.debug(5 - 1) - io.debug(5 / 2) - io.debug(3 * 3) - io.debug(5 % 2) - - // Int comparisons - io.debug(2 > 1) - io.debug(2 < 1) - io.debug(2 >= 1) - io.debug(2 <= 1) - - // Equality works for any type - io.debug(1 == 1) - io.debug(2 == 1) - - // Standard library int functions - io.debug(int.max(42, 77)) - io.debug(int.clamp(5, 10, 20)) -} diff --git a/src/content/chapter0_basics/lesson05_ints/en.html b/src/content/chapter0_basics/lesson05_ints/en.html deleted file mode 100644 index 41793d5..0000000 --- a/src/content/chapter0_basics/lesson05_ints/en.html +++ /dev/null @@ -1,17 +0,0 @@ -

Gleam's Int type represents whole numbers.

-

- There are arithmetic and comparison operators for ints, as well as the - equality operator which works on all types. -

-

- When running on the Erlang virtual machine ints have no maximum and minimum - size. When running on JavaScript runtimes ints are represented using - JavaScript's 64 bit floating point numbers, -

-

- The - - gleam/int - - standard library module contains functions for working with ints. -

diff --git a/src/content/chapter0_basics/lesson06_floats/code.gleam b/src/content/chapter0_basics/lesson06_floats/code.gleam deleted file mode 100644 index b56eb3a..0000000 --- a/src/content/chapter0_basics/lesson06_floats/code.gleam +++ /dev/null @@ -1,27 +0,0 @@ -import gleam/float -import gleam/io - -pub fn main() { - // Float arithmetic - io.debug(1.0 +. 1.5) - io.debug(5.0 -. 1.5) - io.debug(5.0 /. 2.5) - io.debug(3.0 *. 3.5) - - // Float comparisons - io.debug(2.2 >. 1.3) - io.debug(2.2 <. 1.3) - io.debug(2.2 >=. 1.3) - io.debug(2.2 <=. 1.3) - - // Equality works for any type - io.debug(1.1 == 1.1) - io.debug(2.1 == 1.2) - - // Division by zero is not an error - io.debug(3.14 /. 0.0) - - // Standard library float functions - io.debug(float.max(2.0, 9.5)) - io.debug(float.ceiling(5.4)) -} diff --git a/src/content/chapter0_basics/lesson06_floats/en.html b/src/content/chapter0_basics/lesson06_floats/en.html deleted file mode 100644 index 2b2c5b7..0000000 --- a/src/content/chapter0_basics/lesson06_floats/en.html +++ /dev/null @@ -1,32 +0,0 @@ -

Gleam's Float type represents numbers that are not integers.

-

- Gleam's numerical operators are not overloaded, so there are dedicated - operators for working with floats. -

-

- Floats are represented as 64 bit floating point numbers on both the Erlang and - JavaScript runtimes. The floating point behaviour is native to their - respective runtimes, so their exact behaviour will be slightly different - on the two runtimes. -

-

- Under the JavaScript runtime, exceeding the maximum (or minimum) representable - value for a floating point value will result in Infinity (or - -Infinity). Should you try to divide two infinities you will - get NaN as a result. -

-

- When running on the BEAM any overflow will raise an error. So there is - no NaN or Infinity float value in the Erlang - runtime. -

-

- Division by zero will not overflow, but is instead defined to be zero. -

-

- The - - gleam/float - - standard library module contains functions for working with floats. -

diff --git a/src/content/chapter0_basics/lesson07_number_formats/code.gleam b/src/content/chapter0_basics/lesson07_number_formats/code.gleam deleted file mode 100644 index 7307185..0000000 --- a/src/content/chapter0_basics/lesson07_number_formats/code.gleam +++ /dev/null @@ -1,16 +0,0 @@ -import gleam/io - -pub fn main() { - // Underscores - io.debug(1_000_000) - io.debug(10_000.01) - - // Binary, octal, and hex Int literals - io.debug(0b00001111) - io.debug(0o17) - io.debug(0xF) - - // Scientific notation Float literals - io.debug(7.0e7) - io.debug(3.0e-4) -} diff --git a/src/content/chapter0_basics/lesson07_number_formats/en.html b/src/content/chapter0_basics/lesson07_number_formats/en.html deleted file mode 100644 index 308219a..0000000 --- a/src/content/chapter0_basics/lesson07_number_formats/en.html +++ /dev/null @@ -1,13 +0,0 @@ -

- Underscores can be added to numbers for clarity. For example, - 1000000 can be tricky to read quickly, while - 1_000_000 can be easier. -

-

- Ints can be written in binary, octal, or hexadecimal formats using the - 0b, 0o, and 0x prefixes respectively. -

-

- Floats can be written in a scientific notation. -

- diff --git a/src/content/chapter0_basics/lesson08_equality/code.gleam b/src/content/chapter0_basics/lesson08_equality/code.gleam deleted file mode 100644 index 70a2b89..0000000 --- a/src/content/chapter0_basics/lesson08_equality/code.gleam +++ /dev/null @@ -1,6 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(100 == 100) - io.debug(1.5 != 0.1) -} diff --git a/src/content/chapter0_basics/lesson08_equality/en.html b/src/content/chapter0_basics/lesson08_equality/en.html deleted file mode 100644 index e8c2169..0000000 --- a/src/content/chapter0_basics/lesson08_equality/en.html +++ /dev/null @@ -1,13 +0,0 @@ -

- Gleam has the == and != operators for checking - equality. -

-

- The operators can be used with values of any type, but both sides of the - operator must be of the same type. -

-

- Equality is checked structurally, meaning that two values are equal - if they have the same structure rather than if they are at the same memory - location. -

diff --git a/src/content/chapter0_basics/lesson09_strings/code.gleam b/src/content/chapter0_basics/lesson09_strings/code.gleam deleted file mode 100644 index ea47e0f..0000000 --- a/src/content/chapter0_basics/lesson09_strings/code.gleam +++ /dev/null @@ -1,23 +0,0 @@ -import gleam/io -import gleam/string - -pub fn main() { - // String literals - io.debug("๐Ÿ‘ฉโ€๐Ÿ’ป ใ“ใ‚“ใซใกใฏ Gleam ๐Ÿณ๏ธโ€๐ŸŒˆ") - io.debug( - "multi - line - string", - ) - io.debug("\u{1F600}") - - // Double quote can be escaped - io.println("\"X\" marks the spot") - - // String concatenation - io.debug("One " <> "Two") - - // String functions - io.debug(string.reverse("1 2 3 4 5")) - io.debug(string.append("abc", "def")) -} diff --git a/src/content/chapter0_basics/lesson09_strings/en.html b/src/content/chapter0_basics/lesson09_strings/en.html deleted file mode 100644 index 8e4b6f7..0000000 --- a/src/content/chapter0_basics/lesson09_strings/en.html +++ /dev/null @@ -1,26 +0,0 @@ -

- In Gleam strings are written as text surrounded by double quotes, and - can span multiple lines and contain unicode characters. -

-

- The <> operator can be used to concatenate strings. -

-

- Several escape sequences are supported: -

-
    -
  • \" - double quote
  • -
  • \\ - backslash
  • -
  • \f - form feed
  • -
  • \n - newline
  • -
  • \r - carriage return
  • -
  • \t - tab
  • -
  • \u{xxxxxx} - unicode codepoint
  • -
-

- The - - gleam/string - - standard library module contains functions for working with strings. -

diff --git a/src/content/chapter0_basics/lesson10_bools/code.gleam b/src/content/chapter0_basics/lesson10_bools/code.gleam deleted file mode 100644 index aa25ba2..0000000 --- a/src/content/chapter0_basics/lesson10_bools/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/bool -import gleam/io - -pub fn main() { - // Bool operators - io.debug(True && False) - io.debug(True && True) - io.debug(False || False) - io.debug(False || True) - - // Bool functions - io.debug(bool.to_string(True)) - io.debug(bool.to_int(False)) -} diff --git a/src/content/chapter0_basics/lesson10_bools/en.html b/src/content/chapter0_basics/lesson10_bools/en.html deleted file mode 100644 index 3cbf0d4..0000000 --- a/src/content/chapter0_basics/lesson10_bools/en.html +++ /dev/null @@ -1,20 +0,0 @@ -

- A Bool is either True or False. -

-

- The ||, &&, and ! operators can be used - to manipulate bools. -

-

- The || and && operators are short-circuiting, - meaning that if the left hand side of the operator is True for - || or False for && then the right hand - side of the operator will not be evaluated. -

-

- The - - gleam/bool - - standard library module contains functions for working with bools. -

diff --git a/src/content/chapter0_basics/lesson11_assignments/code.gleam b/src/content/chapter0_basics/lesson11_assignments/code.gleam deleted file mode 100644 index a030e43..0000000 --- a/src/content/chapter0_basics/lesson11_assignments/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/io - -pub fn main() { - let x = "Original" - io.debug(x) - - // Assign `y` to the value of `x` - let y = x - io.debug(y) - - // Assign `x` to a new value - let x = "New" - io.debug(x) - - // The `y` still refers to the original value - io.debug(y) -} diff --git a/src/content/chapter0_basics/lesson11_assignments/en.html b/src/content/chapter0_basics/lesson11_assignments/en.html deleted file mode 100644 index d106fd9..0000000 --- a/src/content/chapter0_basics/lesson11_assignments/en.html +++ /dev/null @@ -1,9 +0,0 @@ -

A value can be assigned to a variable using let.

-

- Variable names can be reused by later let bindings, but the values they - reference are immutable, so the values themselves are not changed or mutated - in any way. -

-

- In Gleam variable and function names are written in snake_case. -

diff --git a/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam b/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam deleted file mode 100644 index fa2c0e3..0000000 --- a/src/content/chapter0_basics/lesson12_discard_patterns/code.gleam +++ /dev/null @@ -1,4 +0,0 @@ -pub fn main() { - // This variable is never used - let _score = 1000 -} diff --git a/src/content/chapter0_basics/lesson12_discard_patterns/en.html b/src/content/chapter0_basics/lesson12_discard_patterns/en.html deleted file mode 100644 index 91cfb42..0000000 --- a/src/content/chapter0_basics/lesson12_discard_patterns/en.html +++ /dev/null @@ -1,10 +0,0 @@ -

- If a variable is assigned but not used then Gleam will emit a warning. -

-

- If a variable is intended to not be used, then the name can be prefixed with an - underscore, silencing the warning. -

-

- Try changing the variable name to score to see the warning. -

diff --git a/src/content/chapter0_basics/lesson13_type_annotations/code.gleam b/src/content/chapter0_basics/lesson13_type_annotations/code.gleam deleted file mode 100644 index 1299c2f..0000000 --- a/src/content/chapter0_basics/lesson13_type_annotations/code.gleam +++ /dev/null @@ -1,7 +0,0 @@ -pub fn main() { - let _name: String = "Gleam" - - let _is_cool: Bool = True - - let _version: Int = 1 -} diff --git a/src/content/chapter0_basics/lesson13_type_annotations/en.html b/src/content/chapter0_basics/lesson13_type_annotations/en.html deleted file mode 100644 index 8738a15..0000000 --- a/src/content/chapter0_basics/lesson13_type_annotations/en.html +++ /dev/null @@ -1,15 +0,0 @@ -

- Let assignments can be written with a type annotation after the name. -

-

- Type annotations may be useful for documentation purposes, but they do not - change how Gleam type checks the code beyond ensuring that the annotation is - correct. -

-

- Typically Gleam code will not have type annotations for assignments. -

-

- Try changing a type annotation to something incorrect to see the compile - error. -

diff --git a/src/content/chapter0_basics/lesson14_type_imports/code.gleam b/src/content/chapter0_basics/lesson14_type_imports/code.gleam deleted file mode 100644 index 61f552a..0000000 --- a/src/content/chapter0_basics/lesson14_type_imports/code.gleam +++ /dev/null @@ -1,10 +0,0 @@ -import gleam/bytes_builder -import gleam/string_builder.{type StringBuilder} - -pub fn main() { - // Referring to a type in a qualified way - let _bytes: bytes_builder.BytesBuilder = bytes_builder.new() - - // Refering to a type in an unqualified way - let _text: StringBuilder = string_builder.new() -} diff --git a/src/content/chapter0_basics/lesson14_type_imports/en.html b/src/content/chapter0_basics/lesson14_type_imports/en.html deleted file mode 100644 index 0f362e5..0000000 --- a/src/content/chapter0_basics/lesson14_type_imports/en.html +++ /dev/null @@ -1,22 +0,0 @@ -

- Other modules may also define types that we wish to refer to. In this case we - need to import them. -

-

- Like functions, types can be referred to in a qualified way by - putting the imported module name and a dot before the type name. For example, - - bytes_builder.BytesBuilder - -

-

- Types can also be imported in an unqualified way by listing them in - the import statement with the word type before the type name. -

-

- It is more common in Gleam code for types to be imported in an unqualified way - than it is for functions to be imported in an unqualified way. -

diff --git a/src/content/chapter0_basics/lesson15_type_aliases/code.gleam b/src/content/chapter0_basics/lesson15_type_aliases/code.gleam deleted file mode 100644 index 6125ffe..0000000 --- a/src/content/chapter0_basics/lesson15_type_aliases/code.gleam +++ /dev/null @@ -1,12 +0,0 @@ -import gleam/io - -pub type UserId = - Int - -pub fn main() { - let one: UserId = 1 - let two: Int = 2 - - // UserId and Int are the same type - io.debug(one == two) -} diff --git a/src/content/chapter0_basics/lesson15_type_aliases/en.html b/src/content/chapter0_basics/lesson15_type_aliases/en.html deleted file mode 100644 index 9fddd70..0000000 --- a/src/content/chapter0_basics/lesson15_type_aliases/en.html +++ /dev/null @@ -1,12 +0,0 @@ -

- A type alias can be used to refer to a type by a different name. Giving a type - an alias doesn't make a new type, it is still the same type. -

-

- A type's name always starts with a capital letter, contrasting to variables - and functions, which start with a lowercase letter. -

-

- When the pub keyword is used the type alias is public and can be - referred to by other modules. -

diff --git a/src/content/chapter0_basics/lesson16_blocks/code.gleam b/src/content/chapter0_basics/lesson16_blocks/code.gleam deleted file mode 100644 index 31e4729..0000000 --- a/src/content/chapter0_basics/lesson16_blocks/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/io - -pub fn main() { - let fahrenheit = { - let degrees = 64 - degrees - } - // io.debug(degrees) // <- This will not compile - - // Changing order of evaluation - let celsius = { fahrenheit - 32 } * 5 / 9 - io.debug(celsius) -} diff --git a/src/content/chapter0_basics/lesson16_blocks/en.html b/src/content/chapter0_basics/lesson16_blocks/en.html deleted file mode 100644 index b19c586..0000000 --- a/src/content/chapter0_basics/lesson16_blocks/en.html +++ /dev/null @@ -1,26 +0,0 @@ -

- Blocks are one or more expressions grouped together with curly braces. Each - expression is evaluated in order and the value of the last expression is - returned. -

-

- Any variables assigned within the block can only be used within the block. -

-

- Try uncommenting - - io.debug(degrees) - - to see the compile error from trying to use a variable that is not in scope. -

-

- Blocks can also be used to change the order of evaluation of binary operators - expressions. -

-

- * binds more tightly than + so the expression - 1 + 2 * 3 evaluates to 7. If the 1 + 2 should be - evaluated first to make the expression evaluate to 9 then the expression can be - wrapped in a block: { 1 + 2 } * 3. This is similar to grouping - with parentheses in some other languages. -

diff --git a/src/content/chapter0_basics/lesson17_lists/code.gleam b/src/content/chapter0_basics/lesson17_lists/code.gleam deleted file mode 100644 index 646ad6e..0000000 --- a/src/content/chapter0_basics/lesson17_lists/code.gleam +++ /dev/null @@ -1,16 +0,0 @@ -import gleam/io - -pub fn main() { - let ints = [1, 2, 3] - - io.debug(ints) - - // Immutably prepend - io.debug([-1, 0, ..ints]) - - // Uncomment this to see the error - // io.debug(["zero", ..ints]) - - // The original lists are unchanged - io.debug(ints) -} diff --git a/src/content/chapter0_basics/lesson17_lists/en.html b/src/content/chapter0_basics/lesson17_lists/en.html deleted file mode 100644 index 84f88f3..0000000 --- a/src/content/chapter0_basics/lesson17_lists/en.html +++ /dev/null @@ -1,21 +0,0 @@ -

- Lists are ordered collections of values. -

-

- - List - - is a generic type, having a type parameter for the type of values it contains. - A list of ints has the type List(Int), and a list of strings has the type - List(String). -

-

- Lists are immutable single-linked lists, meaning they are very efficient to - add and remove elements from the front of the list. -

-

- Counting the length of a list or getting elements from other positions in the - list is expensive and rarely done. It is rare to write algorithms that index - into sequences in Gleam, but when they are written a list is not the right - choice of data structure. -

diff --git a/src/content/chapter0_basics/lesson18_constants/code.gleam b/src/content/chapter0_basics/lesson18_constants/code.gleam deleted file mode 100644 index aed6fb0..0000000 --- a/src/content/chapter0_basics/lesson18_constants/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/io - -const ints: List(Int) = [1, 2, 3] - -const floats = [1.0, 2.0, 3.0] - -pub fn main() { - io.debug(ints) - io.debug(ints == [1, 2, 3]) - - io.debug(floats) - io.debug(floats == [1.0, 2.0, 3.0]) -} diff --git a/src/content/chapter0_basics/lesson18_constants/en.html b/src/content/chapter0_basics/lesson18_constants/en.html deleted file mode 100644 index f610dcd..0000000 --- a/src/content/chapter0_basics/lesson18_constants/en.html +++ /dev/null @@ -1,18 +0,0 @@ -

- As well as let assignments Gleam also has constants, which are defined at the - top level of a module. -

-

- Constants must be literal values, functions cannot be used in their - definitions. -

-

- Constants may be useful for values that are used throughout your program, - permitting them to be named and to ensure there are no differences in the - definition between each use. -

-

- Using a constant may be more efficient than creating the same value in - multiple functions, though the exact performance characteristics will depend - on the runtime and whether compiling to Erlang or JavaScript. -

diff --git a/src/content/chapter1_functions/lesson00_functions/code.gleam b/src/content/chapter1_functions/lesson00_functions/code.gleam deleted file mode 100644 index 220e58d..0000000 --- a/src/content/chapter1_functions/lesson00_functions/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(double(10)) -} - -fn double(a: Int) -> Int { - multiply(a, 2) -} - -fn multiply(a: Int, b: Int) -> Int { - a * b -} diff --git a/src/content/chapter1_functions/lesson00_functions/en.html b/src/content/chapter1_functions/lesson00_functions/en.html deleted file mode 100644 index 32d5bed..0000000 --- a/src/content/chapter1_functions/lesson00_functions/en.html +++ /dev/null @@ -1,14 +0,0 @@ -

- The fn keyword is used to define new functions. -

-

- The double and multiply functions are defined - without the pub keyword. This makes them private - functions, they can only be used within this module. If another module - attempted to use them it would result in a compiler error. -

-

- Like with assignments, type annotations are optional for function arguments - and return values. It is considered good practice to use type annotations for - functions, for clarity and to encourage intentional and thoughtful design. -

diff --git a/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam b/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam deleted file mode 100644 index 43b6ca4..0000000 --- a/src/content/chapter1_functions/lesson03_higher_order_functions/code.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/io - -pub fn main() { - // Call a function with another function - io.debug(twice(1, add_one)) - - // Functions can be assigned to variables - let my_function = add_one - io.debug(my_function(100)) -} - -fn twice(argument: Int, passed_function: fn(Int) -> Int) -> Int { - passed_function(passed_function(argument)) -} - -fn add_one(argument: Int) -> Int { - argument + 1 -} diff --git a/src/content/chapter1_functions/lesson03_higher_order_functions/en.html b/src/content/chapter1_functions/lesson03_higher_order_functions/en.html deleted file mode 100644 index 3343e4d..0000000 --- a/src/content/chapter1_functions/lesson03_higher_order_functions/en.html +++ /dev/null @@ -1,12 +0,0 @@ -

- In Gleam functions are values. They can be assigned to variables, passed to - other functions, and anything else you can do with values. -

-

- Here the function add_one is being passed as an argument to the - twice function. -

-

- Notice the fn keyword is also used to describe the type of the - function that twice takes as its second argument. -

diff --git a/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam b/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam deleted file mode 100644 index df7a6f9..0000000 --- a/src/content/chapter1_functions/lesson04_anonymous_functions/code.gleam +++ /dev/null @@ -1,19 +0,0 @@ -import gleam/io - -pub fn main() { - // Assign an anonymous function to a variable - let add_one = fn(a) { a + 1 } - io.debug(twice(1, add_one)) - - // Pass an anonymous function as an argument - io.debug(twice(1, fn(a) { a * 2 })) - - let secret_number = 42 - // This anonymous function always returns 42 - let secret = fn() { secret_number } - io.debug(secret()) -} - -fn twice(argument: Int, my_function: fn(Int) -> Int) -> Int { - my_function(my_function(argument)) -} diff --git a/src/content/chapter1_functions/lesson04_anonymous_functions/en.html b/src/content/chapter1_functions/lesson04_anonymous_functions/en.html deleted file mode 100644 index 93e11a9..0000000 --- a/src/content/chapter1_functions/lesson04_anonymous_functions/en.html +++ /dev/null @@ -1,9 +0,0 @@ -

- As well as module-level named functions, Gleam has anonymous function - literals, written with the fn() { ... } syntax. -

-

Anonymous functions can be used interchangeably with named functions.

-

- Anonymous functions can reference variables that were in scope when they were - defined, making them closures. -

diff --git a/src/content/chapter1_functions/lesson05_function_captures/code.gleam b/src/content/chapter1_functions/lesson05_function_captures/code.gleam deleted file mode 100644 index 35f3412..0000000 --- a/src/content/chapter1_functions/lesson05_function_captures/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/io - -pub fn main() { - // These two statements are equivalent - let add_one_v1 = fn(x) { add(1, x) } - let add_one_v2 = add(1, _) - - io.debug(add_one_v1(10)) - io.debug(add_one_v2(10)) -} - -fn add(a: Int, b: Int) -> Int { - a + b -} diff --git a/src/content/chapter1_functions/lesson05_function_captures/en.html b/src/content/chapter1_functions/lesson05_function_captures/en.html deleted file mode 100644 index 4c86e27..0000000 --- a/src/content/chapter1_functions/lesson05_function_captures/en.html +++ /dev/null @@ -1,12 +0,0 @@ -

- Gleam has a shorthand syntax for creating anonymous functions that take one - argument and immediately call another function with that argument: the - function capture syntax. -

-

- The anonymous function fn(a) { some_function(..., a, ...) } can - be written as some_function(..., _, ...), with any number of - other arguments passed directly to the inner function. The underscore - _ is a placeholder for the argument, equivalent to - a in fn(a) { some_function(..., a, ...) }. -

diff --git a/src/content/chapter1_functions/lesson06_generic_functions/code.gleam b/src/content/chapter1_functions/lesson06_generic_functions/code.gleam deleted file mode 100644 index db53efe..0000000 --- a/src/content/chapter1_functions/lesson06_generic_functions/code.gleam +++ /dev/null @@ -1,20 +0,0 @@ -import gleam/io - -pub fn main() { - let add_one = fn(x) { x + 1 } - let exclaim = fn(x) { x <> "!" } - - // Invalid, Int and String are not the same type - // twice(10, exclaim) - - // Here the type variable is replaced by the type Int - io.debug(twice(10, add_one)) - - // Here the type variable is replaced by the type String - io.debug(twice("Hello", exclaim)) -} - -// The name `value` refers to the same type multiple times -fn twice(argument: value, my_function: fn(value) -> value) -> value { - my_function(my_function(argument)) -} diff --git a/src/content/chapter1_functions/lesson06_generic_functions/en.html b/src/content/chapter1_functions/lesson06_generic_functions/en.html deleted file mode 100644 index d5abb55..0000000 --- a/src/content/chapter1_functions/lesson06_generic_functions/en.html +++ /dev/null @@ -1,26 +0,0 @@ -

- Up until now each function has accepted precisely one type for each of its - arguments. -

-

- The twice function in the previous lesson on - higher order functions only worked with functions that would take and - return ints. This is overly restrictive, it should be possible to use this - function with any type, so long as the function and the initial value are - compatible. -

-

- To enable this, Gleam supports generics, also known as - parametric polymorphism. -

-

- This works by using a type variable instead of specifying a concrete type. It - stands in for whatever specific type is being used when the function is - called. These type variables are written with a lowercase name. -

-

- Type variables are not like an any type, they get replaced with a - specific type each time the function is called. Try uncommenting - twice(10, exclaim) to see the compiler error from trying to use a - type variable as an int and a string at the same time. -

diff --git a/src/content/chapter1_functions/lesson07_pipelines/code.gleam b/src/content/chapter1_functions/lesson07_pipelines/code.gleam deleted file mode 100644 index ec9b805..0000000 --- a/src/content/chapter1_functions/lesson07_pipelines/code.gleam +++ /dev/null @@ -1,19 +0,0 @@ -import gleam/io -import gleam/string - -pub fn main() { - // Without the pipe operator - io.debug(string.drop_left(string.drop_right("Hello, Joe!", 1), 7)) - - // With the pipe operator - "Hello, Mike!" - |> string.drop_right(1) - |> string.drop_left(7) - |> io.debug - - // Changing order with function capturing - "1" - |> string.append("2") - |> string.append("3", _) - |> io.debug -} diff --git a/src/content/chapter1_functions/lesson07_pipelines/en.html b/src/content/chapter1_functions/lesson07_pipelines/en.html deleted file mode 100644 index 783ade9..0000000 --- a/src/content/chapter1_functions/lesson07_pipelines/en.html +++ /dev/null @@ -1,25 +0,0 @@ -

- It's common to want to call a series of functions, passing the result of one - to the next. With the regular function call syntax this can be a little - difficult to read as you have to read the code from the inside out. -

-

- Gleam's pipe operator |> helps with this problem by allowing you - to write code top-to-bottom. -

-

- The pipe operator takes the result of the expression on its left and passes it - as an argument to the function on its right. -

-

- It will first check to see if the left-hand value could be used as the first - argument to the call. For example, a |> b(1, 2) would become - b(a, 1, 2). If not, it falls back to calling the result of the - right-hand side as a function, e.g., b(1, 2)(a) -

-

- Gleam code is typically written with the "subject" of the function as the - first argument, to make it easier to pipe. If you wish to pipe to a different - position then a function capture can be used to insert the argument to the - desired position. -

diff --git a/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam b/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam deleted file mode 100644 index 25bb8c1..0000000 --- a/src/content/chapter1_functions/lesson08_labelled_arguments/code.gleam +++ /dev/null @@ -1,16 +0,0 @@ -import gleam/io - -pub fn main() { - // Without using labels - io.debug(calculate(1, 2, 3)) - - // Using the labels - io.debug(calculate(1, add: 2, multiply: 3)) - - // Using the labels in a different order - io.debug(calculate(1, multiply: 3, add: 2)) -} - -fn calculate(value: Int, add addend: Int, multiply multiplier: Int) { - value * multiplier + addend -} diff --git a/src/content/chapter1_functions/lesson08_labelled_arguments/en.html b/src/content/chapter1_functions/lesson08_labelled_arguments/en.html deleted file mode 100644 index b1d771c..0000000 --- a/src/content/chapter1_functions/lesson08_labelled_arguments/en.html +++ /dev/null @@ -1,23 +0,0 @@ -

- When functions take several arguments it can be difficult to remember what the - arguments are, and what order they are expected in. -

-

- To help with this Gleam supports labelled arguments, where function arguments - are given an external label in addition to their internal name. These labels - are written before the argument name in the function definition. -

-

- When labelled arguments are used the order of the arguments does not matter, - but all unlabelled arguments must come before labelled arguments. -

-

- There is no performance cost to using labelled arguments, it does not allocate - a dictionary or perform any other runtime work. -

-

- Labels are optional when calling a function, it is up to the programmer to - decide what is clearest in their code. -

- - diff --git a/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam b/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam deleted file mode 100644 index a29875f..0000000 --- a/src/content/chapter1_functions/lesson09_documentation_comments/code.gleam +++ /dev/null @@ -1,19 +0,0 @@ -//// A module containing some unusual functions and types. - -/// A type where the value can never be constructed. -/// Can you work out why? -pub type Never { - Never(Never) -} - -/// Call a function twice with an initial value. -/// -pub fn twice(argument: value, my_function: fn(value) -> value) -> value { - my_function(my_function(argument)) -} - -/// Call a function three times with an initial value. -/// -pub fn thrice(argument: value, my_function: fn(value) -> value) -> value { - my_function(my_function(my_function(argument))) -} diff --git a/src/content/chapter1_functions/lesson09_documentation_comments/en.html b/src/content/chapter1_functions/lesson09_documentation_comments/en.html deleted file mode 100644 index c27bac6..0000000 --- a/src/content/chapter1_functions/lesson09_documentation_comments/en.html +++ /dev/null @@ -1,16 +0,0 @@ -

- Documentation and comments are important tools for making your code easier to - work with and understand. -

-

- As well as regular // comments Gleam has /// and - //// comments which are used for attaching documentation to code. -

-

- /// is used for documenting types and functions, and should be - placed immediately before the type or function it is documenting. -

-

- //// is used for documenting modules, and should be placed - at the top of the module. -

diff --git a/src/content/chapter1_functions/lesson10_deprecations/code.gleam b/src/content/chapter1_functions/lesson10_deprecations/code.gleam deleted file mode 100644 index 26a8f0b..0000000 --- a/src/content/chapter1_functions/lesson10_deprecations/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -pub fn main() { - old_function() - new_function() -} - -@deprecated("Use new_function instead") -fn old_function() { - Nil -} - -fn new_function() { - Nil -} diff --git a/src/content/chapter1_functions/lesson10_deprecations/en.html b/src/content/chapter1_functions/lesson10_deprecations/en.html deleted file mode 100644 index b181597..0000000 --- a/src/content/chapter1_functions/lesson10_deprecations/en.html +++ /dev/null @@ -1,14 +0,0 @@ -

- Functions and other definitions can be marked as deprecated using the - @deprecated - attribute. -

-

- If a deprecated function is referenced the compiler will emit a warning, - letting the programmer know they ought to update their code. -

-

- The deprecation attribute takes a message and this will be displayed to the - user in the warning. In the message explain to the user the new approach or - replacement function, or direct them to documentation on how to upgrade. -

diff --git a/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam b/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam deleted file mode 100644 index d198832..0000000 --- a/src/content/chapter2_flow_control/lesson01_case_expressions/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - let x = int.random(5) - io.debug(x) - - let result = case x { - // Match specific values - 0 -> "Zero" - 1 -> "One" - - // Match any other value - _ -> "Other" - } - io.debug(result) -} diff --git a/src/content/chapter2_flow_control/lesson01_case_expressions/en.html b/src/content/chapter2_flow_control/lesson01_case_expressions/en.html deleted file mode 100644 index 07ff421..0000000 --- a/src/content/chapter2_flow_control/lesson01_case_expressions/en.html +++ /dev/null @@ -1,18 +0,0 @@ -

- The case expression is the most common kind of flow control in Gleam code. It - is similar to switch in some other languages, but more powerful - than most. -

-

- It allows the programmer to say "if the data has this shape then run this - code", a process called pattern matching. -

-

- Gleam performs exhaustiveness checking to ensure that the patterns in - a case expression cover all possible values. With this you can have confidence - that your logic is up-to-date for the design of the data you are working with. -

-

- Try commenting out patterns or adding new redundant ones, and see what - problems the compiler reports. -

diff --git a/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam b/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam deleted file mode 100644 index 5bce937..0000000 --- a/src/content/chapter2_flow_control/lesson02_variable_patterns/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - let result = case int.random(5) { - // Match specific values - 0 -> "Zero" - 1 -> "One" - - // Match any other value and assign it to a variable - other -> "It is " <> int.to_string(other) - } - io.debug(result) -} diff --git a/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html b/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html deleted file mode 100644 index 7e9ac11..0000000 --- a/src/content/chapter2_flow_control/lesson02_variable_patterns/en.html +++ /dev/null @@ -1,7 +0,0 @@ -

- Patterns in case expressions can also assign variables. -

-

- When a variable name is used in a pattern the value that is matched against is - assigned to that name, and can be used in the body of that clause. -

diff --git a/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam b/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam deleted file mode 100644 index d1441a0..0000000 --- a/src/content/chapter2_flow_control/lesson03_string_patterns/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(get_name("Hello, Joe")) - io.debug(get_name("Hello, Mike")) - io.debug(get_name("System still working?")) -} - -fn get_name(x: String) -> String { - case x { - "Hello, " <> name -> name - _ -> "Unknown" - } -} diff --git a/src/content/chapter2_flow_control/lesson03_string_patterns/en.html b/src/content/chapter2_flow_control/lesson03_string_patterns/en.html deleted file mode 100644 index 11e4c35..0000000 --- a/src/content/chapter2_flow_control/lesson03_string_patterns/en.html +++ /dev/null @@ -1,9 +0,0 @@ -

- When pattern matching on strings the <> operator can be - used to match on strings with a specific prefix. -

-

- The pattern "hello " <> name matches any string that starts with - "hello " and assigns the rest of the string to the variable - name. -

diff --git a/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam b/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam deleted file mode 100644 index 1b71feb..0000000 --- a/src/content/chapter2_flow_control/lesson04_list_patterns/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/int -import gleam/io -import gleam/list - -pub fn main() { - let x = list.repeat(int.random(5), times: int.random(3)) - io.debug(x) - - let result = case x { - [] -> "Empty list" - [1] -> "List of just 1" - [4, ..] -> "List starting with 4" - [_, _] -> "List of 2 elements" - _ -> "Some other list" - } - io.debug(result) -} diff --git a/src/content/chapter2_flow_control/lesson04_list_patterns/en.html b/src/content/chapter2_flow_control/lesson04_list_patterns/en.html deleted file mode 100644 index de55eef..0000000 --- a/src/content/chapter2_flow_control/lesson04_list_patterns/en.html +++ /dev/null @@ -1,15 +0,0 @@ -

- Lists and the values they contain can be pattern matched on in case - expressions. -

-

- List patterns match on specific lengths of lists. The pattern [] - matches an empty list, and the pattern [_] matches a list with - one element. They will not match on lists with other lengths. -

-

- The spread pattern .. can be used to match the rest of the list. - The pattern [1, ..] matches any list that starts with - 1. The pattern [_, _, ..] matches any list that has - at least two elements. -

diff --git a/src/content/chapter2_flow_control/lesson05_recursion/code.gleam b/src/content/chapter2_flow_control/lesson05_recursion/code.gleam deleted file mode 100644 index 10d45ab..0000000 --- a/src/content/chapter2_flow_control/lesson05_recursion/code.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(factorial(5)) - io.debug(factorial(7)) -} - -// A recursive functions that calculates factorial -pub fn factorial(x: Int) -> Int { - case x { - // Base case - 0 -> 1 - 1 -> 1 - - // Recursive case - _ -> x * factorial(x - 1) - } -} diff --git a/src/content/chapter2_flow_control/lesson05_recursion/en.html b/src/content/chapter2_flow_control/lesson05_recursion/en.html deleted file mode 100644 index f1585bb..0000000 --- a/src/content/chapter2_flow_control/lesson05_recursion/en.html +++ /dev/null @@ -1,20 +0,0 @@ -

- Gleam doesn't have loops, instead iteration is done through recursion, that is - through top-level functions calling themselves with different arguments. -

-

- A recursive function needs to have at least one base case and at - least one recursive case. A base case returns a value without calling - the function again. A recursive case calls the function again with different - inputs, looping again. -

-

- The Gleam standard library has functions for various common looping patterns, - some of which will be introduced in later lessons, however for more complex - loops manual recursion is often the clearest way to write it. -

-

- Recursion can seem daunting or unclear at first if you are more familiar with - languages that have special looping features, but stick with it! With time - it'll become just as familiar and comfortable as any other way of iterating. -

diff --git a/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam b/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam deleted file mode 100644 index b0a416c..0000000 --- a/src/content/chapter2_flow_control/lesson06_tail_calls/code.gleam +++ /dev/null @@ -1,22 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(factorial(5)) - io.debug(factorial(7)) -} - -pub fn factorial(x: Int) -> Int { - // The public function calls the private tail recursive function - factorial_loop(x, 1) -} - -fn factorial_loop(x: Int, accumulator: Int) -> Int { - case x { - 0 -> accumulator - 1 -> accumulator - - // The last thing this function does is call itself - // In the previous lesson the last thing it did was multiply two ints - _ -> factorial_loop(x - 1, accumulator * x) - } -} diff --git a/src/content/chapter2_flow_control/lesson06_tail_calls/en.html b/src/content/chapter2_flow_control/lesson06_tail_calls/en.html deleted file mode 100644 index ec39cda..0000000 --- a/src/content/chapter2_flow_control/lesson06_tail_calls/en.html +++ /dev/null @@ -1,23 +0,0 @@ -

- When a function is called a new stack frame is created in memory to store the - arguments and local variables of the function. If lots of these frames are - created during recursion then the program would use a large amount of memory, - or even crash the program if some limit is hit. -

-

- To avoid this problem Gleam supports tail call optimisation, which - allows the compiler to reuse the stack frame for the current function if a - function call is the last thing the function does, removing the memory cost. -

- -

- Unoptimised recursive functions can often be rewritten into tail call - optimised functions by using an accumulator. An accumulator is a variable that - is passed along in addition to the data, similar to a mutable variable in a - language with while loops. -

-

- Accumulators should be hidden away from the users of your code, they are - internal implementation details. To do this write a public function that calls - a recursive private function with the initial accumulator value. -

diff --git a/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam b/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam deleted file mode 100644 index 370675a..0000000 --- a/src/content/chapter2_flow_control/lesson07_list_recursion/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/io - -pub fn main() { - let sum = sum_list([18, 56, 35, 85, 91], 0) - io.debug(sum) -} - -fn sum_list(list: List(Int), total: Int) -> Int { - case list { - [first, ..rest] -> sum_list(rest, total + first) - [] -> total - } -} diff --git a/src/content/chapter2_flow_control/lesson07_list_recursion/en.html b/src/content/chapter2_flow_control/lesson07_list_recursion/en.html deleted file mode 100644 index 8a3a7d3..0000000 --- a/src/content/chapter2_flow_control/lesson07_list_recursion/en.html +++ /dev/null @@ -1,19 +0,0 @@ -

- While it is more common to use functions in the - - gleam/list - - module to iterate across a list, at times you may prefer to work - with the list directly. -

-

- The [first, ..rest] pattern matches on a list with at least one - element, assigning the first element to the variable first and - the rest of the list to the variable rest. By using this pattern - and a pattern for the empty list [] a function can run code on - each element of a list until the end is reached. -

-

- This code sums a list by recursing over the list and adding each int to a - total argument, returning it when the end is reached. -

diff --git a/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam b/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam deleted file mode 100644 index 38885b6..0000000 --- a/src/content/chapter2_flow_control/lesson08_multiple_subjects/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - let x = int.random(2) - let y = int.random(2) - io.debug(x) - io.debug(y) - - let result = case x, y { - 0, 0 -> "Both are zero" - 0, _ -> "First is zero" - _, 0 -> "Second is zero" - _, _ -> "Neither are zero" - } - io.debug(result) -} diff --git a/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html b/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html deleted file mode 100644 index 1784e99..0000000 --- a/src/content/chapter2_flow_control/lesson08_multiple_subjects/en.html +++ /dev/null @@ -1,13 +0,0 @@ -

- Sometimes it is useful to pattern match on multiple values at the same time in - one case expression. -

-

- To do this, you can give multiple subjects and multiple patterns, separated by - commas. -

-

- When matching on multiple subjects there must be the same number of patterns - as there are subjects. Try removing one of the _, sub-patterns to - see the compile time error that is returned. -

diff --git a/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam b/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam deleted file mode 100644 index e916a03..0000000 --- a/src/content/chapter2_flow_control/lesson09_alternative_patterns/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - let number = int.random(10) - io.debug(number) - - let result = case number { - 2 | 4 | 6 | 8 -> "This is an even number" - 1 | 3 | 5 | 7 -> "This is an odd number" - _ -> "I'm not sure" - } - io.debug(result) -} diff --git a/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html b/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html deleted file mode 100644 index 25421f4..0000000 --- a/src/content/chapter2_flow_control/lesson09_alternative_patterns/en.html +++ /dev/null @@ -1,12 +0,0 @@ -

- Alternative patterns can be given for a case clause using the - | operator. If any of the patterns match then the clause matches. -

-

- If a pattern defines a variable then all of the alternative patterns for that - clause must also define a variable with the same name and same type. -

-

- Currently it is not possible to have nested alternative patterns, so the - pattern [1 | 2 | 3] is not valid. -

diff --git a/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam b/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam deleted file mode 100644 index ee40a26..0000000 --- a/src/content/chapter2_flow_control/lesson10_pattern_aliases/code.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import gleam/io - -pub fn main() { - io.debug(get_first_non_empty([[], [1, 2, 3], [4, 5]])) - io.debug(get_first_non_empty([[1, 2], [3, 4, 5], []])) - io.debug(get_first_non_empty([[], [], []])) -} - -fn get_first_non_empty(lists: List(List(t))) -> List(t) { - case lists { - [[_, ..] as first, ..] -> first - [_, ..rest] -> get_first_non_empty(rest) - [] -> [] - } -} diff --git a/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html b/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html deleted file mode 100644 index 37a1276..0000000 --- a/src/content/chapter2_flow_control/lesson10_pattern_aliases/en.html +++ /dev/null @@ -1,7 +0,0 @@ -

- The as operator can be used to assign sub patterns to variables. -

-

- The pattern [_, ..] as first will match any non-empty list and - assign that list to the variable first. -

diff --git a/src/content/chapter2_flow_control/lesson11_guards/code.gleam b/src/content/chapter2_flow_control/lesson11_guards/code.gleam deleted file mode 100644 index 3744228..0000000 --- a/src/content/chapter2_flow_control/lesson11_guards/code.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import gleam/io - -pub fn main() { - let numbers = [1, 2, 3, 4, 5] - io.debug(get_first_larger(numbers, 3)) - io.debug(get_first_larger(numbers, 5)) -} - -fn get_first_larger(lists: List(Int), limit: Int) -> Int { - case lists { - [first, ..] if first > limit -> first - [_, ..rest] -> get_first_larger(rest, limit) - [] -> 0 - } -} diff --git a/src/content/chapter2_flow_control/lesson11_guards/en.html b/src/content/chapter2_flow_control/lesson11_guards/en.html deleted file mode 100644 index 3ea9cc5..0000000 --- a/src/content/chapter2_flow_control/lesson11_guards/en.html +++ /dev/null @@ -1,9 +0,0 @@ -

- The if keyword can be used with case expressions to add a - guard to a pattern. A guard is an expression that must evaluate to - True for the pattern to match. -

-

- Only a limited set of operators can be used in guards, and functions cannot be - called at all. -

diff --git a/src/content/chapter3_data_types/lesson00_tuples/code.gleam b/src/content/chapter3_data_types/lesson00_tuples/code.gleam deleted file mode 100644 index d5c6313..0000000 --- a/src/content/chapter3_data_types/lesson00_tuples/code.gleam +++ /dev/null @@ -1,10 +0,0 @@ -import gleam/io - -pub fn main() { - let triple = #(1, 2.2, "three") - io.debug(triple) - - let #(a, _, _) = triple - io.debug(a) - io.debug(triple.1) -} diff --git a/src/content/chapter3_data_types/lesson00_tuples/en.html b/src/content/chapter3_data_types/lesson00_tuples/en.html deleted file mode 100644 index 359e93a..0000000 --- a/src/content/chapter3_data_types/lesson00_tuples/en.html +++ /dev/null @@ -1,20 +0,0 @@ -

- Lists are good for when we want a collection of one type, but sometimes we - want to combine multiple values of different types. In this case tuples are a - quick and convenient option. -

-

- The tuple access syntax can be used to get elements from a tuple without - pattern matching. some_tuple.0 gets the first element, - some_tuple.1 gets the second element, etc. -

-

- Tuples are generic types, they have type parameters for the types they - contain. #(1, "Hi!") has the type #(Int, String), - and #(1.4, 10, 48) has the type #(Float, Int, Int). -

-

- Tuples are most commonly used to return 2 or 3 values from a function. Often - it is clearer to use a custom type where a tuple could be used, We - will cover custom types next. -

diff --git a/src/content/chapter3_data_types/lesson01_custom_types/code.gleam b/src/content/chapter3_data_types/lesson01_custom_types/code.gleam deleted file mode 100644 index 35629bf..0000000 --- a/src/content/chapter3_data_types/lesson01_custom_types/code.gleam +++ /dev/null @@ -1,22 +0,0 @@ -import gleam/io - -pub type Season { - Spring - Summer - Autumn - Winter -} - -pub fn main() { - io.debug(weather(Spring)) - io.debug(weather(Autumn)) -} - -fn weather(season: Season) -> String { - case season { - Spring -> "Mild" - Summer -> "Hot" - Autumn -> "Windy" - Winter -> "Cold" - } -} diff --git a/src/content/chapter3_data_types/lesson01_custom_types/en.html b/src/content/chapter3_data_types/lesson01_custom_types/en.html deleted file mode 100644 index 3ca1f66..0000000 --- a/src/content/chapter3_data_types/lesson01_custom_types/en.html +++ /dev/null @@ -1,11 +0,0 @@ -

- Gleam has a few built in types such as Int and - String, but custom types allow the creation of entirely new - types. -

-

- A custom type is defined with the type keyword followed by the - name of the type and a constructor for each variant of the type. Both - the type name and the names of the constructors start with uppercase letters. -

-

Custom type variants can be pattern matched on using a case expression.

diff --git a/src/content/chapter3_data_types/lesson02_records/code.gleam b/src/content/chapter3_data_types/lesson02_records/code.gleam deleted file mode 100644 index bd6da3c..0000000 --- a/src/content/chapter3_data_types/lesson02_records/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/io - -pub type SchoolPerson { - Teacher(name: String, subject: String) - Student(String) -} - -pub fn main() { - let teacher1 = Teacher("Mr Schofield", "Physics") - let teacher2 = Teacher(name: "Miss Percy", subject: "Physics") - let student1 = Student("Koushiar") - let student2 = Student("Naomi") - let student3 = Student("Shaheer") - - let school = [teacher1, teacher2, student1, student2, student3] - io.debug(school) -} diff --git a/src/content/chapter3_data_types/lesson02_records/en.html b/src/content/chapter3_data_types/lesson02_records/en.html deleted file mode 100644 index ea46e4d..0000000 --- a/src/content/chapter3_data_types/lesson02_records/en.html +++ /dev/null @@ -1,13 +0,0 @@ -

- A variant of a custom type can hold other data within it. In this case - the variant is called a record. -

-

- The fields of a record can be given labels, and like function argument labels they can - be optionally used when calling the record constructor. Typically labels will - be used for variants that define them. -

-

- It is common to have a custom type with one variant that holds data, this is - the Gleam equivalent of a struct or object in other languages. -

diff --git a/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam b/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam deleted file mode 100644 index 63ca721..0000000 --- a/src/content/chapter3_data_types/lesson03_record_accessors/code.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import gleam/io - -pub type SchoolPerson { - Teacher(name: String, subject: String) - Student(name: String) -} - -pub fn main() { - let teacher = Teacher("Mr Schofield", "Physics") - let student = Student("Koushiar") - - io.debug(teacher.name) - io.debug(student.name) - // io.debug(teacher.subject) -} diff --git a/src/content/chapter3_data_types/lesson03_record_accessors/en.html b/src/content/chapter3_data_types/lesson03_record_accessors/en.html deleted file mode 100644 index bb2473b..0000000 --- a/src/content/chapter3_data_types/lesson03_record_accessors/en.html +++ /dev/null @@ -1,18 +0,0 @@ -

- The record accessor syntax record.field_label can be used to get - contained values from a custom type record. -

-

- The accessor syntax can only be used for fields with the same name that are in - the same position and have the same type for all variants of the custom type. -

-

- The name field is in the first position and has type - String for all variants, so it can be accessed. -

-

- The subject field is absent on the Student variant, - so it cannot be used on any variant of type SchoolPerson. - Uncomment the teacher.subject line to see the compile error from - trying to use this accessor. -

diff --git a/src/content/chapter3_data_types/lesson04_record_updates/code.gleam b/src/content/chapter3_data_types/lesson04_record_updates/code.gleam deleted file mode 100644 index ed7b45b..0000000 --- a/src/content/chapter3_data_types/lesson04_record_updates/code.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import gleam/io - -pub type SchoolPerson { - Teacher(name: String, subject: String, floor: Int, room: Int) -} - -pub fn main() { - let teacher1 = Teacher(name: "Mr Dodd", subject: "ICT", floor: 2, room: 2) - - // Use the update syntax - let teacher2 = Teacher(..teacher1, subject: "PE", room: 6) - - io.debug(teacher1) - io.debug(teacher2) -} diff --git a/src/content/chapter3_data_types/lesson04_record_updates/en.html b/src/content/chapter3_data_types/lesson04_record_updates/en.html deleted file mode 100644 index 4a5cd3a..0000000 --- a/src/content/chapter3_data_types/lesson04_record_updates/en.html +++ /dev/null @@ -1,8 +0,0 @@ -

- The record update syntax can be used to create a new record from an existing - one of the same type, but with some fields changed. -

-

- Gleam is an immutable language, so using the record update syntax does not - mutate or otherwise change the original record. -

diff --git a/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam b/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam deleted file mode 100644 index 7e34e99..0000000 --- a/src/content/chapter3_data_types/lesson05_generic_custom_types/code.gleam +++ /dev/null @@ -1,10 +0,0 @@ -pub type Option(inner) { - Some(inner) - None -} - -// An option of string -pub const name: Option(String) = Some("Annah") - -// An option of int -pub const level: Option(Int) = Some(10) diff --git a/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html b/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html deleted file mode 100644 index 3d003d7..0000000 --- a/src/content/chapter3_data_types/lesson05_generic_custom_types/en.html +++ /dev/null @@ -1,12 +0,0 @@ -

- Like functions, custom types can also be generic, taking contained types as - parameters. -

-

- Here a generic Option type is defined, which is used to represent - a value that is either present or absent. This type is quite useful! The - - gleam/option - - module defines it so you can use it in your Gleam projects. -

diff --git a/src/content/chapter3_data_types/lesson06_nil/code.gleam b/src/content/chapter3_data_types/lesson06_nil/code.gleam deleted file mode 100644 index c28080b..0000000 --- a/src/content/chapter3_data_types/lesson06_nil/code.gleam +++ /dev/null @@ -1,11 +0,0 @@ -import gleam/io - -pub fn main() { - let x = Nil - io.debug(x) - - // let y: List(String) = Nil - - let result = io.println("Hello!") - io.debug(result == Nil) -} diff --git a/src/content/chapter3_data_types/lesson06_nil/en.html b/src/content/chapter3_data_types/lesson06_nil/en.html deleted file mode 100644 index 9749180..0000000 --- a/src/content/chapter3_data_types/lesson06_nil/en.html +++ /dev/null @@ -1,15 +0,0 @@ -

- Nil is Gleam's unit type. It is a value that is returned by - functions that have nothing else to return, as all functions must return - something. -

-

- Nil is not a valid value of any other types. Therefore, values in - Gleam are not nullable. If the type of a value is Nil then it is - the value Nil. If it is some other type then the value is not - Nil. -

-

- Uncomment the line that assigns Nil to a variable with an - incompatible type annotation to see the compile time error it produces. -

diff --git a/src/content/chapter3_data_types/lesson07_results/code.gleam b/src/content/chapter3_data_types/lesson07_results/code.gleam deleted file mode 100644 index b613566..0000000 --- a/src/content/chapter3_data_types/lesson07_results/code.gleam +++ /dev/null @@ -1,25 +0,0 @@ -import gleam/int -import gleam/io - -pub fn main() { - io.debug(buy_pastry(10)) - io.debug(buy_pastry(8)) - io.debug(buy_pastry(5)) - io.debug(buy_pastry(3)) -} - -pub type PurchaseError { - NotEnoughMoney(required: Int) - NotLuckyEnough -} - -fn buy_pastry(money: Int) -> Result(Int, PurchaseError) { - case money >= 5 { - True -> - case int.random(4) == 0 { - True -> Error(NotLuckyEnough) - False -> Ok(money - 5) - } - False -> Error(NotEnoughMoney(required: 5)) - } -} diff --git a/src/content/chapter3_data_types/lesson07_results/en.html b/src/content/chapter3_data_types/lesson07_results/en.html deleted file mode 100644 index c43a0b7..0000000 --- a/src/content/chapter3_data_types/lesson07_results/en.html +++ /dev/null @@ -1,39 +0,0 @@ -

- Gleam doesn't use exceptions, instead computations that can either succeed or - fail return a value of the built-in Result(value, error) type. It - has two variants: -

-
    -
  • - Ok, which contains the return value of a successful - computation. -
  • -
  • - Error, which contains the reason for a failed computation. -
  • -
-

- The type is generic with two type parameters, one for the success value and - one for the error. With these the result can hold any type for success and - failure. -

-

- Commonly a Gleam program or library will define a custom type with a variant - for each possible problem that can arise, along with any error information - that would be useful to the programmer. -

-

- This is advantageous over exceptions as you can immediately see what if any - errors a function can return, and the compiler will ensure they are handled. - No nasty surprises with unexpected exceptions! -

-

- A result value can be handled by pattern matching with a - case expression, but given how frequently results are returned - this can become unwieldy. Gleam code commonly uses the - - gleam/result - - standard library module and use expressions when working with results, - both of which will be covered in later chapters. -

diff --git a/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam b/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam deleted file mode 100644 index dc772ca..0000000 --- a/src/content/chapter3_data_types/lesson08_bit_arrays/code.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/io - -pub fn main() { - // 8 bit int. In binary: 00000011 - io.debug(<<3>>) - io.debug(<<3>> == <<3:size(8)>>) - - // 16 bit int. In binary: 0001100000000011 - io.debug(<<6147:size(16)>>) - - // A bit array of UTF8 data - io.debug(<<"Hello, Joe!":utf8>>) -} diff --git a/src/content/chapter3_data_types/lesson08_bit_arrays/en.html b/src/content/chapter3_data_types/lesson08_bit_arrays/en.html deleted file mode 100644 index 8b92335..0000000 --- a/src/content/chapter3_data_types/lesson08_bit_arrays/en.html +++ /dev/null @@ -1,44 +0,0 @@ -

- Bit arrays represent a sequence of 1s and 0s, and are a convenient syntax for - constructing and manipulating binary data. -

-

- Each segment of a bit array can be given options to specify the representation - used for that segment. -

-
    -
  • size: the size of the segment in bits.
  • -
  • - unit: the number of bits that the size value is a - multiple of. -
  • -
  • bits: a nested bit array of any size.
  • -
  • bytes: a nested byte-aligned bit array.
  • -
  • float: a 64 bits floating point number.
  • -
  • int: an int with a default size of 8 bits.
  • -
  • big: big endian.
  • -
  • little: little endian.
  • -
  • native: the endianness of the processor.
  • -
  • utf8: utf8 encoded text.
  • -
  • utf16: utf16 encoded text.
  • -
  • utf32: utf32 encoded text.
  • -
  • utf8_codepoint: a utf8 codepoint.
  • -
  • utf16_codepoint: a utf16 codepoint.
  • -
  • utf32_codepoint: a utf32 codepoint.
  • -
  • signed: a signed number.
  • -
  • unsigned: an unsigned number.
  • -
-

- Multiple options can be given to a segment by separating each with a dash: - x:unsigned-little-size(2). -

-

- Bit arrays have limited support when compiling to JavaScript, not all options - can be used. Full bit array support will be implemented in the future. -

-

- For more information on bit arrays see the - Erlang bit syntax documentation. -

diff --git a/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam b/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam deleted file mode 100644 index a842430..0000000 --- a/src/content/chapter4_standard_library/lesson00_standard_library_package/code.gleam +++ /dev/null @@ -1,6 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("Hello, Joe!") - io.println("Hello, Mike!") -} diff --git a/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html b/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html deleted file mode 100644 index 4014136..0000000 --- a/src/content/chapter4_standard_library/lesson00_standard_library_package/en.html +++ /dev/null @@ -1,17 +0,0 @@ -

- The Gleam standard library is a regular Gleam package that has been published - to the Hex package repository. You could opt to - not use it if you wish, though almost all Gleam projects depend on it. -

-

- All of the modules imported so far in this guide, such as - - gleam/io - , - are from the standard library. -

-

- All of the documentation for the standard library is available on - HexDocs. We will go over some - of the most commonly used modules now. -

diff --git a/src/content/chapter4_standard_library/lesson01_list_module/code.gleam b/src/content/chapter4_standard_library/lesson01_list_module/code.gleam deleted file mode 100644 index 29f2448..0000000 --- a/src/content/chapter4_standard_library/lesson01_list_module/code.gleam +++ /dev/null @@ -1,19 +0,0 @@ -import gleam/io -import gleam/list - -pub fn main() { - let ints = [0, 1, 2, 3, 4, 5] - - io.println("=== map ===") - io.debug(list.map(ints, fn(x) { x * 2 })) - - io.println("=== filter ===") - io.debug(list.filter(ints, fn(x) { x % 2 == 0 })) - - io.println("=== fold ===") - io.debug(list.fold(ints, 0, fn(count, e) { count + e })) - - io.println("=== find ===") - io.debug(list.find(ints, fn(x) { x > 3 })) - io.debug(list.find(ints, fn(x) { x > 13 })) -} diff --git a/src/content/chapter4_standard_library/lesson01_list_module/en.html b/src/content/chapter4_standard_library/lesson01_list_module/en.html deleted file mode 100644 index 9c6b953..0000000 --- a/src/content/chapter4_standard_library/lesson01_list_module/en.html +++ /dev/null @@ -1,42 +0,0 @@ -

- The - - gleam/list - - standard library module contains functions for working with lists. A Gleam - program will likely make heavy use of this module, the various functions - serving as different types of loops over lists. -

- -

- - map - - makes a new list by running a function on each element in a list. -

-

- - filter - - makes a new list containing only the elements for which a function returns - true. -

-

- - fold - - combines all the elements in a list into a single value by running a function - left-to-right on each element, passing the result of the previous call to the - next call. -

-

- - find - - returns the first element in a list for which a function returns - True. -

-

- It's worth getting familiar with all the functions in this module when writing - Gleam code, you'll be using them a lot! -

diff --git a/src/content/chapter4_standard_library/lesson02_result_module/code.gleam b/src/content/chapter4_standard_library/lesson02_result_module/code.gleam deleted file mode 100644 index ec7039b..0000000 --- a/src/content/chapter4_standard_library/lesson02_result_module/code.gleam +++ /dev/null @@ -1,24 +0,0 @@ -import gleam/int -import gleam/io -import gleam/result - -pub fn main() { - io.println("=== map ===") - io.debug(result.map(Ok(1), fn(x) { x * 2 })) - io.debug(result.map(Error(1), fn(x) { x * 2 })) - - io.println("=== try ===") - io.debug(result.try(Ok("1"), int.parse)) - io.debug(result.try(Ok("no"), int.parse)) - io.debug(result.try(Error(Nil), int.parse)) - - io.println("=== unwrap ===") - io.debug(result.unwrap(Ok("1234"), "default")) - io.debug(result.unwrap(Error(Nil), "default")) - - io.println("=== pipeline ===") - int.parse("-1234") - |> result.map(int.absolute_value) - |> result.try(int.remainder(_, 42)) - |> io.debug -} diff --git a/src/content/chapter4_standard_library/lesson02_result_module/en.html b/src/content/chapter4_standard_library/lesson02_result_module/en.html deleted file mode 100644 index 0760145..0000000 --- a/src/content/chapter4_standard_library/lesson02_result_module/en.html +++ /dev/null @@ -1,40 +0,0 @@ -

- The - - gleam/result - - standard library module contains functions for working with results. Gleam - programs will make heavy use of this module to avoid excessive nested case - expressions when calling multiple functions that can fail. -

- -

- - map - - updates a value held within the Ok of a result by calling a given function on - it. If the result is an error then the function is not called. -

- -

- - try - - runs a result returning function on the value held within an Ok of a result. - If the result is an error then the function is not called. This is useful for - chaining together multiple function calls that can fail, one after the other, - stopping at the first error. -

- -

- - unwrap - - extracts the success value from a result, or returning a default value if the - result is an error. -

- -

- Result functions are often used with pipelines to chain together multiple - calls to result returning functions. -

diff --git a/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam b/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam deleted file mode 100644 index e7b3a59..0000000 --- a/src/content/chapter4_standard_library/lesson03_dict_module/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/dict -import gleam/io - -pub fn main() { - let scores = dict.from_list([#("Lucy", 13), #("Drew", 15)]) - io.debug(scores) - - let scores = - scores - |> dict.insert("Bushra", 16) - |> dict.insert("Darius", 14) - |> dict.delete("Drew") - io.debug(scores) -} diff --git a/src/content/chapter4_standard_library/lesson03_dict_module/en.html b/src/content/chapter4_standard_library/lesson03_dict_module/en.html deleted file mode 100644 index 4037085..0000000 --- a/src/content/chapter4_standard_library/lesson03_dict_module/en.html +++ /dev/null @@ -1,40 +0,0 @@ -

- The - gleam/dict - standard library module defines Gleam's Dict type and functions - for working with it. A dict is a collection of keys and values which other - languages may call a hashmap or table. -

- -

- - new - - and - - from_list - - can be used to create new dicts. -

- -

- - insert - - and - - delete - - are used to add and remove items from a dict. -

-

- Like lists, dicts are immutable. Inserting or deleting an item from a dict - will return a new dict with the item added or removed. -

-

- Dicts are unordered! If it appears that the items in a dict are in a certain - order this is incidental and should not be relied upon. Any ordering may - change without warning in future versions or on different runtimes. -

diff --git a/src/content/chapter4_standard_library/lesson04_option_module/code.gleam b/src/content/chapter4_standard_library/lesson04_option_module/code.gleam deleted file mode 100644 index eb60001..0000000 --- a/src/content/chapter4_standard_library/lesson04_option_module/code.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import gleam/io -import gleam/option.{type Option, None, Some} - -pub type Person { - Person(name: String, pet: Option(String)) -} - -pub fn main() { - let person_with_pet = Person("Al", Some("Nubi")) - let person_without_pet = Person("Maria", None) - - io.debug(person_with_pet) - io.debug(person_without_pet) -} diff --git a/src/content/chapter4_standard_library/lesson04_option_module/en.html b/src/content/chapter4_standard_library/lesson04_option_module/en.html deleted file mode 100644 index 699b5bd..0000000 --- a/src/content/chapter4_standard_library/lesson04_option_module/en.html +++ /dev/null @@ -1,19 +0,0 @@ -

- Values in Gleam are not nullable, so the - - gleam/option - - standard library module defines Gleam's - - Option - - type, which can be used to represent a value that is either present or absent. -

- -

- The option type is very similar to the result type, but it does not have an - error value. Some languages have functions return an option when there is no - extra error detail to give, but Gleam always uses result. This makes all - fallible functions consistent and removes any boilerplate that would be - required when mixing functions that use each type. -

diff --git a/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam b/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam deleted file mode 100644 index d116b42..0000000 --- a/src/content/chapter5_advanced_features/lesson00_opaque_types/code.gleam +++ /dev/null @@ -1,26 +0,0 @@ -import gleam/io - -pub fn main() { - let positive = new(1) - let zero = new(0) - let negative = new(-1) - - io.debug(to_int(positive)) - io.debug(to_int(zero)) - io.debug(to_int(negative)) -} - -pub opaque type PositiveInt { - PositiveInt(inner: Int) -} - -pub fn new(i: Int) -> PositiveInt { - case i >= 0 { - True -> PositiveInt(i) - False -> PositiveInt(0) - } -} - -pub fn to_int(i: PositiveInt) -> Int { - i.inner -} diff --git a/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html b/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html deleted file mode 100644 index f0956fb..0000000 --- a/src/content/chapter5_advanced_features/lesson00_opaque_types/en.html +++ /dev/null @@ -1,17 +0,0 @@ -

- Opaque types are types where a custom type itself is public and can - be used by other modules, but the constructors for the type are private and - can only be used by the module that defines the type. This prevents other - modules from constructing or pattern matching on the type. -

-

- This is useful for creating types with smart constructors. A smart - constructor is a function that constructs a value of a type, but is more - restrictive than if the programmer were to use one of the type's constructors - directly. This can be useful for ensuring that the type is used correctly. -

-

- For example, this PositiveInt custom type is opaque. If other - modules want to construct one they have to use the new function, - which ensures that the integer is positive. -

diff --git a/src/content/chapter5_advanced_features/lesson01_use/code.gleam b/src/content/chapter5_advanced_features/lesson01_use/code.gleam deleted file mode 100644 index e7b435b..0000000 --- a/src/content/chapter5_advanced_features/lesson01_use/code.gleam +++ /dev/null @@ -1,38 +0,0 @@ -import gleam/io -import gleam/result - -pub fn main() { - io.debug(without_use()) - io.debug(with_use()) -} - -pub fn without_use() { - result.try(get_username(), fn(username) { - result.try(get_password(), fn(password) { - result.map(log_in(username, password), fn(greeting) { - greeting <> ", " <> username - }) - }) - }) -} - -pub fn with_use() { - use username <- result.try(get_username()) - use password <- result.try(get_password()) - use greeting <- result.map(log_in(username, password)) - greeting <> ", " <> username -} - -// Here are some pretend functions for this example: - -fn get_username() { - Ok("alice") -} - -fn get_password() { - Ok("hunter2") -} - -fn log_in(_username: String, _password: String) { - Ok("Welcome") -} diff --git a/src/content/chapter5_advanced_features/lesson01_use/en.html b/src/content/chapter5_advanced_features/lesson01_use/en.html deleted file mode 100644 index d879aec..0000000 --- a/src/content/chapter5_advanced_features/lesson01_use/en.html +++ /dev/null @@ -1,30 +0,0 @@ -

- Gleam lacks exceptions, macros, type classes, early returns, and a variety of - other features, instead going all-in with just first-class-functions and - pattern matching. This makes Gleam code easier to understand, but it can - sometimes result in excessive indentation. -

-

- Gleam's use expression helps out here by enabling us to write code that uses - callbacks in an unindented style, as shown in the code window. -

- -

- The higher order function being called goes on the right hand side of the - <- operator. It must take a callback function as its final - argument. -

-

- The argument names for the callback function go on the left hand side of the - <- operator. The function can take any number of arguments, - including zero. -

-

- All the remaining code in the enclosing {} block - becomes the body of the callback function. -

-

- This is a very capable and useful feature, but excessive application of - use may result in unclear code, especially to beginners. Usually - the regular function call syntax results in more approachable code! -

diff --git a/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam b/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam deleted file mode 100644 index 6c5ccaa..0000000 --- a/src/content/chapter5_advanced_features/lesson02_use_sugar/code.gleam +++ /dev/null @@ -1,30 +0,0 @@ -import gleam/io -import gleam/result - -pub fn main() { - let x = { - use username <- result.try(get_username()) - use password <- result.try(get_password()) - use greeting <- result.map(log_in(username, password)) - greeting <> ", " <> username - } - - case x { - Ok(greeting) -> io.println(greeting) - Error(error) -> io.println("ERROR:" <> error) - } -} - -// Here are some pretend functions for this example: - -fn get_username() { - Ok("alice") -} - -fn get_password() { - Ok("hunter2") -} - -fn log_in(_username: String, _password: String) { - Ok("Welcome") -} diff --git a/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html b/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html deleted file mode 100644 index e28c843..0000000 --- a/src/content/chapter5_advanced_features/lesson02_use_sugar/en.html +++ /dev/null @@ -1,30 +0,0 @@ -

- The use expression is syntactic sugar for a regular function call - and an anonymous function. -

- -

This code:

-
-use a, b <- my_function
-next(a)
-next(b)
-
- -

Expands into this code:

-
-my_function(fn(a, b) {
-  next(a)
-  next(b)
-})
-
- -

- To ensure that your use code works and is as understandable as - possible, the right-hand-side ideally should be a function call rather than a - pipeline or other expression, which is typically more difficult to read. -

- -

- use is an expression like everything else in Gleam, so it can be - placed within blocks. -

diff --git a/src/content/chapter5_advanced_features/lesson03_todo/code.gleam b/src/content/chapter5_advanced_features/lesson03_todo/code.gleam deleted file mode 100644 index d5abe8f..0000000 --- a/src/content/chapter5_advanced_features/lesson03_todo/code.gleam +++ /dev/null @@ -1,7 +0,0 @@ -pub fn main() { - todo as "I haven't written this code yet!" -} - -pub fn todo_without_reason() { - todo -} diff --git a/src/content/chapter5_advanced_features/lesson03_todo/en.html b/src/content/chapter5_advanced_features/lesson03_todo/en.html deleted file mode 100644 index 4a2c433..0000000 --- a/src/content/chapter5_advanced_features/lesson03_todo/en.html +++ /dev/null @@ -1,14 +0,0 @@ -

- The todo keyword is used to specify that some code is not yet - implemented. -

-

- The as "some string" is optional, though you may wish to include - the message if you have more than one code block marked as - todo in your code. -

-

- When used the Gleam compiler will print a warning to remind you the code is - unfinished, and if the code is run then the program will crash with the given - message. -

diff --git a/src/content/chapter5_advanced_features/lesson04_panic/code.gleam b/src/content/chapter5_advanced_features/lesson04_panic/code.gleam deleted file mode 100644 index fce9d66..0000000 --- a/src/content/chapter5_advanced_features/lesson04_panic/code.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import gleam/io - -pub fn main() { - print_score(10) - print_score(100_000) - print_score(-1) -} - -pub fn print_score(score: Int) { - case score { - score if score > 1000 -> io.println("High score!") - score if score > 0 -> io.println("Still working on it") - _ -> panic as "Scores should never be negative!" - } -} diff --git a/src/content/chapter5_advanced_features/lesson04_panic/en.html b/src/content/chapter5_advanced_features/lesson04_panic/en.html deleted file mode 100644 index c54c217..0000000 --- a/src/content/chapter5_advanced_features/lesson04_panic/en.html +++ /dev/null @@ -1,11 +0,0 @@ -

- The panic keyword is similar to the todo keyword, - but it is used to crash the program when the program has reached a point that - should never be reached. -

-

- This keyword should almost never be used! It may be useful in initial - prototypes and scripts, but its use in a library or production application is - a sign that the design could be improved. With well designed types the type - system can typically be used to make these invalid states unrepresentable. -

diff --git a/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam b/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam deleted file mode 100644 index 2ba907a..0000000 --- a/src/content/chapter5_advanced_features/lesson05_let_assert/code.gleam +++ /dev/null @@ -1,16 +0,0 @@ -import gleam/io - -pub fn main() { - let a = unsafely_get_first_element([123]) - io.debug(a) - - let b = unsafely_get_first_element([]) - io.debug(b) -} - -pub fn unsafely_get_first_element(items: List(a)) -> a { - // This will panic if the list is empty. - // A regular `let` would not permit this partial pattern - let assert [first, ..] = items - first -} diff --git a/src/content/chapter5_advanced_features/lesson05_let_assert/en.html b/src/content/chapter5_advanced_features/lesson05_let_assert/en.html deleted file mode 100644 index 29fd06c..0000000 --- a/src/content/chapter5_advanced_features/lesson05_let_assert/en.html +++ /dev/null @@ -1,15 +0,0 @@ -

- let assert is the final way to intentionally crash your Gleam - program. It is similar to the panic keyword in that it crashes - when the program has reached a point that should never be reached. -

-

- let assert is similar to let in that it is a way to - assign values to variables, but it is different in that the pattern can be - partial. The pattern does not need to match every possible value of the - type being assigned. -

-

- Like panic this feature should be used sparingly, and likely not - at all in libraries. -

diff --git a/src/content/chapter5_advanced_features/lesson06_externals/code.gleam b/src/content/chapter5_advanced_features/lesson06_externals/code.gleam deleted file mode 100644 index 1101b82..0000000 --- a/src/content/chapter5_advanced_features/lesson06_externals/code.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/io - -// A type with no Gleam constructors -pub type DateTime - -// An external function that creates an instance of the type -@external(javascript, "./my_package_ffi.mjs", "now") -pub fn now() -> DateTime - -// The `now` function in `./my_package_ffi.mjs` looks like this: -// export function now() { -// return new Date(); -// } - -pub fn main() { - io.debug(now()) -} diff --git a/src/content/chapter5_advanced_features/lesson06_externals/en.html b/src/content/chapter5_advanced_features/lesson06_externals/en.html deleted file mode 100644 index 8815fa7..0000000 --- a/src/content/chapter5_advanced_features/lesson06_externals/en.html +++ /dev/null @@ -1,25 +0,0 @@ -

- Sometimes in our projects we want to use code written in other languages, most - commonly Erlang and JavaScript, depending on which runtime is being used. - Gleam's external functions and external types allow us to - import and use this non-Gleam code. -

-

- An external type is one that has no constructors. Gleam doesn't know what - shape it has or how to create one, it only knows that it exists. -

-

- An external function is one that has the @external attribute on - it, directing the compiler to use the specified module function as the - implementation, instead of Gleam code. -

-

- The compiler can't tell the types of functions written in other languages, so - when the external attribute is given type annotations must be provided. Gleam - trusts that the type given is correct so an inaccurate type annotation can - result in unexpected behaviour and crashes at runtime. Be careful! -

-

- External functions are useful but should be used sparingly. Prefer to write - Gleam code where possible. -

diff --git a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam deleted file mode 100644 index b62a735..0000000 --- a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/code.gleam +++ /dev/null @@ -1,11 +0,0 @@ -import gleam/io - -pub type DateTime - -@external(erlang, "calendar", "local_time") -@external(javascript, "./my_package_ffi.mjs", "now") -pub fn now() -> DateTime - -pub fn main() { - io.debug(now()) -} diff --git a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html b/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html deleted file mode 100644 index 6e02d36..0000000 --- a/src/content/chapter5_advanced_features/lesson07_multi_target_externals/en.html +++ /dev/null @@ -1,22 +0,0 @@ -

- Multiple external implementations can be specified for the same function, - enabling the function to work on both Erlang and JavaScript. -

-

- If a function doesn't have an implementation for the currently compiled-for - target then the compiler will return an error. -

-

- You should try to implement functions for all targets, but this isn't always - possible due to incompatibilities in how IO and concurreny works in Erlang and - JavaScript. With Erlang concurrent IO is handled transparently by the runtime, - while in JavaScript concurrent IO requires the use of promises or callbacks. - If your code uses the Erlang style it is typically not possible to implement - in JavaScript, while if callbacks are used then it won't be compatible with - most Gleam and Erlang code as it forces any code that calls the function to - also use callbacks. -

-

- Libraries that make use of concurrent IO will typically have to decide whether - they support Erlang or JavaScript, and document this in their README. -

diff --git a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam deleted file mode 100644 index a97b8fc..0000000 --- a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/code.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/io - -@external(erlang, "lists", "reverse") -pub fn reverse_list(items: List(e)) -> List(e) { - tail_recursive_reverse(items, []) -} - -fn tail_recursive_reverse(items: List(e), reversed: List(e)) -> List(e) { - case items { - [] -> reversed - [first, ..rest] -> tail_recursive_reverse(rest, [first, ..reversed]) - } -} - -pub fn main() { - io.debug(reverse_list([1, 2, 3, 4, 5])) - io.debug(reverse_list(["a", "b", "c", "d", "e"])) -} diff --git a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html b/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html deleted file mode 100644 index 243c7ea..0000000 --- a/src/content/chapter5_advanced_features/lesson08_external_gleam_fallbacks/en.html +++ /dev/null @@ -1,13 +0,0 @@ -

- It's possible for a function to have both a Gleam implementation and an - external implementation. If there exists an external implementation for the - currently compiled-for target then it will be used, otherwise the Gleam - implementation is used. -

-

- This may be useful if you have a function that can be implemented in Gleam, - but there is an optimised implementation that can be used for one target. For - example, the Erlang virtual machine has a built-in list reverse function that - is implemented in native code. The code here uses this implementation when - running on Erlang, as it is then available. -

diff --git a/src/pages/about/en.html b/src/pages/about/en.html new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/en.html b/src/pages/en.html new file mode 100644 index 0000000..689bba3 --- /dev/null +++ b/src/pages/en.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/src/playground.gleam b/src/playground.gleam index 2ffd43a..9fcf4a5 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -1,14 +1,17 @@ import filepath import gleam/io import gleam/list -import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import gleam/string_builder -import htmb.{type Html, h, text} +import htmb.{type Html, h} import simplifile import snag import playground/widgets.{Link} +import playground/html.{ + PageConfig, ScriptConfig, ScriptOptions, html_dangerous_inline_script, + html_script, render_page, +} const static = "static" @@ -26,7 +29,9 @@ const stdlib_external = "build/packages/gleam_stdlib/src" const compiler_wasm = "./wasm-compiler" -const content_path = "src/content" +const home_title = "Gleam Playground" + +const pages_path = "src/pages" const hello_joe = "import gleam/io @@ -35,98 +40,33 @@ pub fn main() { } " -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 = " -

- 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. -

-

- The tour is interactive! The code shown is editable and will be compiled and - evaluated as you type. Anything you print using - - io.println - - or - - io.debug - - 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. -

-

- If at any point you get stuck or have a question do not hesitate to ask in - the Gleam Discord server. 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. -

-

- OK, let's go. Click \"Next\" to get started, click \"Contents\" to jump to a - specific topic, or go here to read everything in - one page. -

-" - -const what_next_html = " -

- Congratulations on completing the tour! Here's some ideas for what to do next: -

- -

- Read the Writing Gleam - guide to learn how to create and develop a Gleam project. -

-

- Join the the Gleam Discord server - and meet the community. They're friendly and helpful! -

-

- Enroll in the Exercism - Gleam track to practice your Gleam skills through a series of exercises - and optionally get feedback from experienced Gleam developers. -

-

- Happy hacking! -

-" - // page paths const path_home = "/" -const path_table_of_contents = "/table-of-contents" - -const path_what_next = "/what-next" - -const path_everything = "/everything" - // Don't include deprecated stdlib modules const skipped_stdlib_modules = [ "bit_string.gleam", "bit_builder.gleam", "map.gleam", ] pub fn main() { + let _ = { + use f <- result.try(load_file_names(pages_path, [])) + io.debug(read_pages(f)) + } + 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)) + use filenames <- result.try(load_file_names(pages_path, [])) + use pages <- result.try(read_pages(filenames)) + use _ <- result.try(write_pages(pages)) + + io.debug("Done rendering pages") + // use p <- result.try(load_content()) + // use _ <- result.try(write_content(p)) Ok(Nil) } @@ -140,170 +80,76 @@ pub fn main() { } } -pub type Chapter { - Chapter(name: String, path: String, lessons: List(Lesson)) -} - -pub 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( +/// Recursively list files in a directory +fn load_file_names( + path: String, + filenames: List(FileNames), +) -> snag.Result(List(FileNames)) { + use files <- result.try( 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) + |> list.fold(Ok(filenames), fn(filenames, file_or_dir_path) { + use filenames <- result.try(filenames) + let full_file_path = path <> "/" <> file_or_dir_path + case simplifile.is_directory(full_file_path) { + True -> load_file_names(full_file_path, filenames) + False -> { + let slug = + full_file_path + |> string.replace(pages_path <> "/", "") + |> string.split("/") + |> list.reverse + |> list.drop(1) + |> list.reverse + |> string.join("/") + |> fn(s) { path_home <> s } + + let name = + slug + |> string.replace("/", " ") + |> string.replace("-", " ") + |> string.capitalise + |> string_coalesce(home_title) + + let file = FileNames(path: full_file_path, name: name, slug: slug) + Ok([file, ..filenames]) + } + } }) } -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 <> "/en.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 string_coalesce(coalesce value: String, with replacement: String) -> String { + case value { + "" -> replacement + _ -> value + } } -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: "Welcome to the Gleam language tour! ๐Ÿ’ซ", - 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 contents page - use _ <- result.try( - write_lesson(Lesson( - name: "Table of Contents", - text: contents_list_html(chapters), - code: hello_joe, - path: path_table_of_contents, - previous: None, - next: None, - )), - ) - - // Everything page - use _ <- result.try(write_everything_page(chapters)) - - Ok(Nil) +type Page { + Page(filenames: FileNames, content: String) } -fn contents_list_html(chapters: List(Chapter)) -> String { - let chapters = - list.flat_map(chapters, fn(chapter) { - [ - h("h3", [#("class", "mb-0")], [text(chapter.name)]), - h( - "ul", - [], - list.map(chapter.lessons, fn(lesson) { - h("li", [], [ - h("a", [#("href", lesson.path)], [ - lesson.name - |> string.replace("-", " ") - |> string.capitalise - |> text, - ]), - ]) - }), - ), - ] - }) - - [ - h("p", [], [ - text("Looking for all the content on one page? "), - h("a", [#("href", path_everything)], [text("Find it here")]), - text("!"), - ]), - ..chapters - ] - |> list.append([ - h("p", [], [h("a", [#("href", path_what_next)], [text("What's nextโ€ฆ?")])]), - ]) - |> list.map(render_html) - |> string.join("\n") +fn read_pages(filenames: List(FileNames)) -> snag.Result(List(Page)) { + filenames + |> list.fold(Ok([]), fn(pages, filename) { + use pages <- result.try(pages) + use content <- result.try(read_file(filename.path)) + let page = Page(filenames: filename, content: content) + Ok([page, ..pages]) + }) } -fn render_html(html: Html) -> String { - html - |> htmb.render - |> string_builder.to_string +fn read_file(path: String) -> snag.Result(String) { + simplifile.read(path) + |> file_error("Failed to read file " <> path) } fn ensure_directory(path: String) -> snag.Result(Nil) { @@ -316,67 +162,6 @@ fn write_text(path: String, text: String) -> snag.Result(Nil) { |> file_error("Failed to write " <> path) } -fn write_everything_page(chapters: List(Chapter)) -> snag.Result(Nil) { - let path = public <> "/everything" - use _ <- result.try(ensure_directory(path)) - let path = filepath.join(path, "/index.html") - write_text(path, everything_page_render(chapters)) -} - -fn write_lesson(lesson: Lesson) -> snag.Result(Nil) { - let path = public <> lesson.path - use _ <- result.try(ensure_directory(path)) - let path = filepath.join(path, "/index.html") - write_text(path, lesson_page_render(lesson)) -} - -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), @@ -565,9 +350,6 @@ const css_defaults_page = [css_fonts, css_theme, css__gleam_common, css_layout] /// Common stylesheet for all tour pages const css_root = "/css/root.css" -// Path to the css specific to the everything page -const css_everything_page = "/css/pages/everything.css" - // Path to the css speciic to to lesson & main pages const css_lesson_page = "/css/pages/lesson.css" @@ -604,52 +386,12 @@ pub fn theme_picker_script() -> Html { ) } -pub fn arrow_keys_navigation_script( - next: Option(String), - prev: Option(String), -) -> Html { - let to_handler = fn(maybe_link) { - case maybe_link { - None -> "null" - Some(link) -> "() => { window.location.href = '" <> link <> "' }" - } - } - - html_dangerous_inline_script(" - const keyHandlers = { - 'ArrowLeft': " <> to_handler(prev) <> ", - 'ArrowRight': " <> to_handler(next) <> ", - } - - document.addEventListener('keydown', function(event) { - // Don't hijack arrow keys when focus is on the code textarea. - if (document.querySelector('textarea.codeflask__textarea') === document.activeElement) { - return; - } - - const handler = keyHandlers[event.key]; - if (handler !== undefined && handler !== null) { - handler(); - } - }) - ", ScriptOptions(module: True, defer: False), []) -} - // Page Renders -/// Renders a Lesson's page -/// Complete with title, lesson, editor and output -fn lesson_page_render(lesson: Lesson) -> String { - let navlink = fn(name, link) { - case link { - None -> h("span", [], [text(name)]) - Some(path) -> h("a", [#("href", path)], [text(name)]) - } - } - +fn render_page_object(page: Page) -> String { render_page(PageConfig( - path: lesson.path, - title: lesson.name, + path: page.filenames.path, + title: page.filenames.name, stylesheets: list.flatten([ css_defaults_page, css_defaults_code, @@ -657,37 +399,14 @@ fn lesson_page_render(lesson: Lesson) -> String { ]), static_content: [render_navbar()], content: [ - h("article", [#("id", "playground")], [ - h("section", [#("id", "left"), #("class", "content-nav")], [ - h("div", [], [ - h("h2", [], [text(lesson.name <> " lesson")]), - htmb.dangerous_unescaped_fragment(string_builder.from_string( - lesson.text, - )), - ]), - h("nav", [#("class", "prev-next")], [ - navlink("Back", lesson.previous), - text(" โ€” "), - h("a", [#("href", path_table_of_contents)], [text("Contents")]), - text(" โ€” "), - navlink("Next", lesson.next), - ]), - ]), - h("section", [#("id", "right")], [ - h("section", [#("id", "editor")], [ - h("div", [#("id", "editor-target")], []), - ]), - h("aside", [#("id", "output")], []), - ]), - ]), + htmb.dangerous_unescaped_fragment(string_builder.from_string(page.content)), ], scripts: ScriptConfig( body: [ theme_picker_script(), - arrow_keys_navigation_script(lesson.next, lesson.previous), h("script", [#("type", "gleam"), #("id", "code")], [ htmb.dangerous_unescaped_fragment(string_builder.from_string( - lesson.code, + hello_joe, )), ]), html_script("/index.js", ScriptOptions(module: True, defer: False), []), @@ -697,367 +416,20 @@ fn lesson_page_render(lesson: Lesson) -> String { )) } +fn write_pages(pages: List(Page)) -> snag.Result(Nil) { + list.try_each(pages, fn(page) { + let dir = filepath.join(public, slugify_path(page.filenames.slug)) + use _ <- result.try(ensure_directory(dir)) + let path = filepath.join(dir, "index.html") + let html = render_page_object(page) + write_text(path, html) + }) +} + +/// Renders a Lesson's page +/// Complete with title, lesson, editor and output /// Transform a path into a slug fn slugify_path(path: String) -> String { string.replace(path, "/", "-") |> string.drop_left(up_to: 1) } - -/// Renders a lesson item in the everyting page's list -fn everything_page_lesson_html(lesson: Lesson, index: Int, end_index: Int) { - let snippet_link_title = "Experiment with " <> lesson.name <> " in browser" - - let lesson_content = - h("article", [#("class", "lesson"), #("id", slugify_path(lesson.path))], [ - h("a", [#("href", "#" <> slugify_path(lesson.path)), #("class", "link")], [ - h("h2", [#("class", "lesson-title")], [text(lesson.name)]), - ]), - htmb.dangerous_unescaped_fragment(string_builder.from_string(lesson.text)), - h("pre", [#("class", "lesson-snippet hljs gleam language-gleam")], [ - h("code", [], [text(lesson.code)]), - h( - "a", - [ - #("class", "lesson-snippet-link"), - #("href", lesson.path), - #("title", snippet_link_title), - #("aria-label", snippet_link_title), - ], - [ - h("i", [#("class", "snippet-link-icon")], [text("")]), - text("Run code snippet"), - ], - ), - ]), - ]) - - case index { - i if i == end_index -> [lesson_content] - _ -> [lesson_content, widgets.separator("lesson")] - } -} - -/// Renders a list containing all chapters and their lessons -fn everything_page_chapters_html(chapters: List(Chapter)) -> List(Html) { - use #(chapter, index) <- list.flat_map( - list.index_map(chapters, fn(chap, i) { #(chap, i) }), - ) - - let lessons = - list.index_map(chapter.lessons, fn(lesson, index) { - everything_page_lesson_html( - lesson, - index, - list.length(chapter.lessons) - 1, - ) - }) - let chapter_title = - h("h3", [#("id", slugify_path(chapter.path)), #("class", "chapter-title")], [ - text(chapter.name), - ]) - - let chapter_header = case index { - 0 -> [chapter_title, widgets.separator("chapter")] - _ -> [ - widgets.separator("chapter-between"), - chapter_title, - widgets.separator("chapter"), - ] - } - - list.concat([chapter_header, ..lessons]) -} - -/// Renders a link to a lesson in the table of contents -fn everything_page_toc_link(lesson: Lesson) -> Html { - h("li", [], [ - widgets.text_link( - Link(label: lesson.name, to: "#" <> slugify_path(lesson.path)), - [#("class", "link padded")], - ), - ]) -} - -/// Renders the everything pages's table of contents -fn everything_page_toc_html(chapters: List(Chapter)) -> List(Html) { - use chapter <- list.map(chapters) - let links = list.map(chapter.lessons, everything_page_toc_link) - - h("article", [#("class", "chapter"), #("id", "chapter-" <> chapter.name)], [ - h("h3", [], [text(chapter.name)]), - h("ul", [], links), - ]) -} - -/// Renders the /everything's page body content -fn everything_page_html(chapters: List(Chapter)) -> Html { - let chapter_lessons = everything_page_chapters_html(chapters) - let table_of_contents = everything_page_toc_html(chapters) - - h("main", [#("id", "everything")], [ - h( - "aside", - [#("id", "everything-contents"), #("class", "dim-bg")], - table_of_contents, - ), - h("section", [#("id", "everything-lessons")], chapter_lessons), - ]) -} - -/// Renders the /everything page to a string -pub fn everything_page_render(chapters: List(Chapter)) -> String { - render_page(PageConfig( - path: path_everything, - title: "Everything!", - stylesheets: list.flatten([ - css_defaults_page, - css_defaults_code, - [css_root, css_everything_page], - ]), - static_content: [render_navbar()], - content: [everything_page_html(chapters)], - scripts: ScriptConfig( - head: [ - html_script( - "/js/highlight/highlight.core.min.js", - ScriptOptions(module: True, defer: False), - [], - ), - html_script( - "/js/highlight/regexes.js", - ScriptOptions(module: True, defer: True), - [], - ), - ], - body: [ - theme_picker_script(), - html_script( - "/js/highlight/highlight-gleam.js", - ScriptOptions(module: True, defer: True), - [], - ), - ], - ), - )) -} - -/// Generic HTML rendering utils -pub type HtmlAttribute = - #(String, String) - -pub type ScriptOptions { - ScriptOptions(module: Bool, defer: Bool) -} - -/// Formats js script options into usage html attributes -fn html_script_common_attributes( - attributes: ScriptOptions, -) -> List(HtmlAttribute) { - let type_attr = #("type", case attributes.module { - True -> "module" - _ -> "text/javascript" - }) - let defer_attr = #("defer", "") - - case attributes.defer { - True -> [defer_attr, type_attr] - _ -> [type_attr] - } -} - -/// Renders an HTML script tag -pub fn html_script( - src source: String, - options attributes: ScriptOptions, - attributes additional_attributes: List(HtmlAttribute), -) -> Html { - let attrs = { - let src_attr = #("src", source) - let base_attrs = [src_attr, ..html_script_common_attributes(attributes)] - list.flatten([base_attrs, additional_attributes]) - } - h("script", attrs, []) -} - -/// Renders an inline HTML script tag -pub fn html_dangerous_inline_script( - script content: String, - options attributes: ScriptOptions, - attributes additional_attributes: List(HtmlAttribute), -) -> Html { - let attrs = { - list.flatten([ - html_script_common_attributes(attributes), - additional_attributes, - ]) - } - h("script", attrs, [ - htmb.dangerous_unescaped_fragment(string_builder.from_string(content)), - ]) -} - -/// Renders an HTML meta tag -pub fn html_meta(data attributes: List(HtmlAttribute)) -> Html { - h("meta", attributes, []) -} - -/// Renders an HTML meta property tag -pub fn html_meta_prop(property: String, content: String) -> Html { - html_meta([#("property", property), #("content", content)]) -} - -/// Renders an HTML link tag -pub fn html_link(rel: String, href: String) -> Html { - h("link", [#("rel", rel), #("href", href)], []) -} - -/// Renders a stylesheet link tag -pub fn html_stylesheet(src: String) -> Html { - html_link("stylesheet", src) -} - -/// Renders an HTML title tag -pub fn html_title(title: String) -> Html { - h("title", [], [text(title)]) -} - -pub type HeadConfig { - HeadConfig( - path: String, - title: String, - description: String, - url: String, - image: String, - meta: List(Html), - stylesheets: List(String), - scripts: List(Html), - ) -} - -/// Renders the page head as HTML -fn head(with config: HeadConfig) -> htmb.Html { - let meta_tags = [ - html_meta_prop("og:type", "website"), - html_meta_prop("og:title", config.title), - html_meta_prop("og:description", config.description), - html_meta_prop("og:url", config.url), - html_meta_prop("og:image", config.image), - html_meta_prop("twitter:card", "summary_large_image"), - html_meta_prop("twitter:url", config.url), - html_meta_prop("twitter:title", config.title), - html_meta_prop("twitter:description", config.description), - html_meta_prop("twitter:image", config.image), - ..config.meta - ] - - let head_meta = [ - html_meta([#("charset", "utf-8")]), - html_meta([ - #("name", "viewport"), - #("content", "width=device-width, initial-scale=1"), - ]), - html_title(config.title), - html_meta([#("name", "description"), #("content", config.description)]), - ..meta_tags - ] - - let head_links = [ - html_link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"), - ..list.map(config.stylesheets, html_stylesheet) - ] - - let head_content = list.concat([head_meta, head_links, config.scripts]) - - h("head", [], head_content) -} - -pub type BodyConfig { - BodyConfig( - content: List(Html), - static_content: List(Html), - scripts: List(Html), - attributes: List(HtmlAttribute), - ) -} - -/// Renders an Html body tag -fn html_body(with config: BodyConfig) -> Html { - let content = - list.flatten([config.static_content, config.content, config.scripts]) - - h("body", config.attributes, content) -} - -pub type HtmlConfig { - HtmlConfig( - attributes: List(HtmlAttribute), - lang: String, - head: HeadConfig, - body: BodyConfig, - ) -} - -/// Renders an HTML tag and its children -fn html(with config: HtmlConfig) -> Html { - let attributes = [#("lang", config.lang), ..config.attributes] - - h("html", attributes, [head(config.head), html_body(config.body)]) -} - -pub type ScriptConfig { - ScriptConfig(head: List(Html), body: List(Html)) -} - -pub type PageConfig { - PageConfig( - path: String, - title: String, - content: List(Html), - static_content: List(Html), - stylesheets: List(String), - scripts: ScriptConfig, - ) -} - -/// Renders a page in the language tour -pub fn render_page_html(page config: PageConfig) -> Html { - // add path-specific class to body to make styling easier - let body_class = #("id", "page" <> string.replace(config.path, "/", "-")) - - // render html - html(HtmlConfig( - head: HeadConfig( - description: "An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!", - image: "https://gleam.run/images/og-image.png", - title: config.title <> " - The Gleam Language Tour", - url: "https://tour.gleam.run/" <> config.path, - path: config.path, - meta: [], - stylesheets: config.stylesheets, - scripts: [ - html_script( - "https://plausible.io/js/script.js", - ScriptOptions(defer: True, module: False), - [#("data-domain", "tour.gleam.run")], - ), - ..config.scripts.head - ], - ), - lang: "en-GB", - attributes: [#("class", "theme-light")], - body: BodyConfig( - attributes: [body_class], - scripts: config.scripts.body, - static_content: config.static_content, - content: config.content, - ), - )) -} - -/// Renders an HTML document in String form from a PageConfig -pub fn render_page(page config: PageConfig) -> String { - config - |> render_page_html - |> htmb.render_page("html") - |> string_builder.to_string -} diff --git a/src/playground/html.gleam b/src/playground/html.gleam new file mode 100644 index 0000000..e752072 --- /dev/null +++ b/src/playground/html.gleam @@ -0,0 +1,225 @@ +import gleam/list +import gleam/string +import gleam/string_builder +import htmb.{type Html, h, text} + +/// Generic HTML rendering utils +pub type HtmlAttribute = + #(String, String) + +pub type ScriptOptions { + ScriptOptions(module: Bool, defer: Bool) +} + +/// Formats js script options into usage html attributes +fn html_script_common_attributes( + attributes: ScriptOptions, +) -> List(HtmlAttribute) { + let type_attr = #("type", case attributes.module { + True -> "module" + _ -> "text/javascript" + }) + let defer_attr = #("defer", "") + + case attributes.defer { + True -> [defer_attr, type_attr] + _ -> [type_attr] + } +} + +/// Renders an HTML script tag +pub fn html_script( + src source: String, + options attributes: ScriptOptions, + attributes additional_attributes: List(HtmlAttribute), +) -> Html { + let attrs = { + let src_attr = #("src", source) + let base_attrs = [src_attr, ..html_script_common_attributes(attributes)] + list.flatten([base_attrs, additional_attributes]) + } + h("script", attrs, []) +} + +/// Renders an inline HTML script tag +pub fn html_dangerous_inline_script( + script content: String, + options attributes: ScriptOptions, + attributes additional_attributes: List(HtmlAttribute), +) -> Html { + let attrs = { + list.flatten([ + html_script_common_attributes(attributes), + additional_attributes, + ]) + } + h("script", attrs, [ + htmb.dangerous_unescaped_fragment(string_builder.from_string(content)), + ]) +} + +/// Renders an HTML meta tag +pub fn html_meta(data attributes: List(HtmlAttribute)) -> Html { + h("meta", attributes, []) +} + +/// Renders an HTML meta property tag +pub fn html_meta_prop(property: String, content: String) -> Html { + html_meta([#("property", property), #("content", content)]) +} + +/// Renders an HTML link tag +pub fn html_link(rel: String, href: String) -> Html { + h("link", [#("rel", rel), #("href", href)], []) +} + +/// Renders a stylesheet link tag +pub fn html_stylesheet(src: String) -> Html { + html_link("stylesheet", src) +} + +/// Renders an HTML title tag +pub fn html_title(title: String) -> Html { + h("title", [], [text(title)]) +} + +pub type HeadConfig { + HeadConfig( + path: String, + title: String, + description: String, + url: String, + image: String, + meta: List(Html), + stylesheets: List(String), + scripts: List(Html), + ) +} + +/// Renders the page head as HTML +fn head(with config: HeadConfig) -> htmb.Html { + let meta_tags = [ + html_meta_prop("og:type", "website"), + html_meta_prop("og:title", config.title), + html_meta_prop("og:description", config.description), + html_meta_prop("og:url", config.url), + html_meta_prop("og:image", config.image), + html_meta_prop("twitter:card", "summary_large_image"), + html_meta_prop("twitter:url", config.url), + html_meta_prop("twitter:title", config.title), + html_meta_prop("twitter:description", config.description), + html_meta_prop("twitter:image", config.image), + ..config.meta + ] + + let head_meta = [ + html_meta([#("charset", "utf-8")]), + html_meta([ + #("name", "viewport"), + #("content", "width=device-width, initial-scale=1"), + ]), + html_title(config.title), + html_meta([#("name", "description"), #("content", config.description)]), + ..meta_tags + ] + + let head_links = [ + html_link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"), + ..list.map(config.stylesheets, html_stylesheet) + ] + + let head_content = list.concat([head_meta, head_links, config.scripts]) + + h("head", [], head_content) +} + +pub type BodyConfig { + BodyConfig( + content: List(Html), + static_content: List(Html), + scripts: List(Html), + attributes: List(HtmlAttribute), + ) +} + +/// Renders an Html body tag +fn html_body(with config: BodyConfig) -> Html { + let content = + list.flatten([config.static_content, config.content, config.scripts]) + + h("body", config.attributes, content) +} + +pub type HtmlConfig { + HtmlConfig( + attributes: List(HtmlAttribute), + lang: String, + head: HeadConfig, + body: BodyConfig, + ) +} + +/// Renders an HTML tag and its children +fn html(with config: HtmlConfig) -> Html { + let attributes = [#("lang", config.lang), ..config.attributes] + + h("html", attributes, [head(config.head), html_body(config.body)]) +} + +pub type ScriptConfig { + ScriptConfig(head: List(Html), body: List(Html)) +} + +pub type PageConfig { + PageConfig( + path: String, + title: String, + content: List(Html), + static_content: List(Html), + stylesheets: List(String), + scripts: ScriptConfig, + ) +} + +/// Renders a page in the language tour +pub fn render_page_html(page config: PageConfig) -> Html { + // add path-specific class to body to make styling easier + let body_class = #("id", "page" <> string.replace(config.path, "/", "-")) + + // render html + html(HtmlConfig( + head: HeadConfig( + description: "An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!", + image: "https://gleam.run/images/og-image.png", + title: config.title <> " - The Gleam Language Tour", + url: "https://tour.gleam.run/" <> config.path, + path: config.path, + meta: [], + stylesheets: config.stylesheets, + scripts: [ + html_script( + "https://plausible.io/js/script.js", + ScriptOptions(defer: True, module: False), + [#("data-domain", "tour.gleam.run")], + ), + ..config.scripts.head + ], + ), + lang: "en-GB", + attributes: [#("class", "theme-light")], + body: BodyConfig( + attributes: [body_class], + scripts: config.scripts.body, + static_content: config.static_content, + content: config.content, + ), + )) +} + +/// Renders an HTML document in String form from a PageConfig +pub fn render_page(page config: PageConfig) -> String { + config + |> render_page_html + |> htmb.render_page("html") + |> string_builder.to_string +} diff --git a/static/css/pages/everything.css b/static/css/pages/everything.css deleted file mode 100644 index 1885078..0000000 --- a/static/css/pages/everything.css +++ /dev/null @@ -1,354 +0,0 @@ -/* - * /everything page - * inline chapters & lessons - */ - -body { - /* max-height: 100%; */ - overflow: hidden; - min-height: 100vh; - /* mobile viewport bug fix */ - min-height: -webkit-fill-available; -} - -html { - min-height: -webkit-fill-available; -} - -/* set navbar to position: fixed */ -.navbar { - z-index: 100; - position: fixed; - inset: 0; - bottom: 0; -} - -/* everything wrapper, 2x2 responsive grid layout */ -main#everything { - display: grid; - max-height: 100%; - min-height: -webkit-fill-available; - /* height: -webkit-fill-available; */ - grid-template-columns: minmax(0, max-content) 1fr; - overflow: hidden; - padding-top: var(--navbar-height); -} - -p, -a { - margin: var(--gap) 0; - font-size: var(--font-size-normal); - line-height: var(--gap-double); - font-weight: 400; -} - -code { - font-size: var(--font-size-small); -} - -h1, -h2, -h3 { - margin: 0; -} - -#everything-contents, -#everything-lessons { - grid-column-end: span 1; - grid-row: 1 / span 1; - max-height: 100%; - padding: var(--gap); - padding-bottom: var(--gap-double); - overflow-y: auto; - display: flex; - background: var(--color-background); - flex-direction: column; - position: relative; -} - -/* table of contents on the left, scrollable */ -#everything-contents { - grid-column-start: 1; - padding-bottom: var(--gap-quad); - gap: var(--gap-double); -} - -#everything-contents::before { - position: fixed; -} - -#everything-contents * { - margin: 0; -} - -#everything-contents .chapter { - display: flex; - flex-direction: column; - gap: var(--gap); -} - -#everything-contents .chapter h3 { - white-space: nowrap; -} - -#everything-contents .chapter ul, -#everything-contents .chapter li { - list-style: none; - padding: 0; - color: var(--color-text-secondary); -} - -#everything-contents .chapter ul { - display: flex; - flex-direction: column; - gap: var(--gap-half); -} - -#everything-lessons { - grid-column-start: 2; - container-type: inline-size; - container-name: lessons-list; - padding-top: 0; - scroll-behavior: smooth; - scroll-snap-type: y proximity; - scroll-padding-block-start: calc(2 * var(--navbar-height)); -} - -#everything-lessons .chapter-title { - margin: 0; - padding-top: var(--gap); - color: var(--color-text-accent); - position: sticky; - height: var(--gap-triple); - top: 0; - background: var(--color-background); - z-index: 3; -} - -#everything-lessons .chapter-title:first-child { - margin-top: 0; -} - -#everything-lessons .lesson { - margin: var(--gap) 0; - padding: var(--gap) 0; - padding-bottom: var(--gap-triple); - scroll-snap-align: center top; -} - -#everything-lessons .lesson-title { - position: sticky; - top: var(--gap-triple); - background: var(--color-background); - color: var(--color-link); - z-index: 2; - padding: var(--gap) 0; - margin-bottom: var(--gap-double); -} - -#everything-lessons .lesson:target { - animation: highlight-block 900ms ease-in-out 300ms 1; -} - -#everything-lessons .lesson:target .lesson-title { - animation: highlight-text 900ms ease-in-out 300ms 1; -} - -#everything-lessons .lesson-snippet { - padding: var(--gap); - margin-right: var(--gap); - margin-top: var(--gap-double); - position: relative; - background: var(--code-background); - box-shadow: var(--drop-shadow); -} - -#everything-lessons .lesson-snippet code { - overflow-x: auto; - display: inline-block; - width: 100%; - padding: var(--gap); - padding-bottom: var(--gap-double); -} - -#everything-lessons .lesson-snippet-link { - background: var(--color-accent-muted); - margin: 0; - position: absolute; - bottom: 0; - right: 0; - padding: var(--gap-half) var(--gap); - display: flex; - align-items: center; - justify-content: center; - gap: var(--gap); - font-size: var(--font-size-small); - color: var(--color-link); - border-radius: none; - text-decoration: none; - outline: 1px solid transparent; - outline-offset: -1px; - cursor: pointer; -} - -#everything-lessons hr { - width: 100%; - height: 1px; - border: 0; - padding: 0; - margin: 0; - display: block; -} - -#everything-lessons .lesson-separator { - border-top: 1px solid var(--color-accent-muted); -} - -#everything-lessons .chapter-separator { - border-top: 1px solid var(--color-accent); - position: sticky; - top: calc(var(--gap-triple) - 1px); - z-index: 3; -} - -@media only screen and (min-width: 1100px) { - #everything { - grid-template-columns: minmax(min-content, max-content) 1fr; - } - - #everything-contents { - padding: var(--gap) var(--gap-double) var(--gap-quad) var(--gap); - } - - #everything-contents .chapter ul { - padding-left: var(--gap); - } -} - -/* transform contents as to a side menu */ -@media only screen and (max-width: 768px) { - #everything-contents { - display: none; - } - - .theme-light #everything-contents::after { - filter: contrast(1); - } - - #everything-lessons { - grid-column: 1 / span 2; - padding-bottom: var(--gap-quad); - padding-left: 0; - padding-right: 0; - } - - #everything-lessons .lesson, - #everything-lessons .chapter-title, - #everything-lessons .chapter-separator { - padding-left: var(--gap); - padding-right: var(--gap); - } - - #everything-lessons .lesson-snippet code { - padding: var(--gap); - } -} - -@container lessons-list (min-width: 900px) { - #everything-lessons .lesson, - #everything-lessons .chapter-title { - padding-right: var(--gap-quad); - padding-left: var(--gap-quad); - } - - #everything-lessons .chapter-separator { - margin-left: var(--gap-quad); - margin-right: var(--gap-quad); - width: calc(100% - var(--gap-quad) * 2); - } -} - -@keyframes highlight-text { - 0% { - text-decoration: underline; - text-decoration-color: transparent; - } - - 50% { - text-decoration: underline; - text-decoration-color: var(--color-accent); - color: var(--color-accent-text); - background: var(--color-accent-muted); - } - - 100% { - text-decoration: underline; - text-decoration-color: transparent; - } -} - -@keyframes highlight-block { - 0% { - } - - 50% { - background: var(--color-accent-muted); - } - - 100% { - } -} - -@keyframes reveal { - 0% { - overflow: unset; - } - - 99% { - overflow: unset; - } - - 100% { - overflow-y: auto; - } -} - -@media print { - body#page-everything { - overflow: visible; - float: none; - display: inline; - } - - aside#everything-contents { - display: none; - } - - nav.navbar { - position: unset; - } - - main#everything { - max-height: unset; - overflow: visible; - display: inline; - } - - section#everything-lessons { - max-height: unset; - overflow: visible; - display: inline; - } - - h3.chapter-title:not(:first-child) { - break-before: always; - } - - article.lesson:not(hr.chapter-separator + article.lesson) { - break-inside: avoid; - } - - #everything-lessons .lesson-snippet-link { - display: none; - } -} From e00ea0759043594a953625d3db30e65b10ada251 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Mon, 6 May 2024 23:42:50 +0100 Subject: [PATCH 03/18] remove public --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 599be4e..a336ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.ez /build erl_crash.dump +public/ From da853ad265342868bf9005e1d59319e86c21f5a8 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Mon, 13 May 2024 21:56:10 +0100 Subject: [PATCH 04/18] playground: set up sharing, compilation target button html --- src/pages/en.html | 37 ++++++++-- static/css/pages/lesson.css | 140 ++++++++++++++++++++++++------------ 2 files changed, 125 insertions(+), 52 deletions(-) diff --git a/src/pages/en.html b/src/pages/en.html index 689bba3..c0486b6 100644 --- a/src/pages/en.html +++ b/src/pages/en.html @@ -1,8 +1,33 @@ -
- diff --git a/src/playground.gleam b/src/playground.gleam index 9fcf4a5..23ca322 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -5,13 +5,13 @@ import gleam/result import gleam/string import gleam/string_builder import htmb.{type Html, h} -import simplifile -import snag -import playground/widgets.{Link} import playground/html.{ PageConfig, ScriptConfig, ScriptOptions, html_dangerous_inline_script, html_script, render_page, } +import playground/widgets.{Link} +import simplifile +import snag const static = "static" diff --git a/static/css/pages/lesson.css b/static/css/pages/lesson.css index bcc9875..6764e83 100644 --- a/static/css/pages/lesson.css +++ b/static/css/pages/lesson.css @@ -96,6 +96,10 @@ white-space: pre-wrap; } +.output>pre { + background: none !important; +} + #output-container:has(#output-radio:checked)>#output { display: block; } diff --git a/static/index.js b/static/index.js index b92560c..3f33ac9 100644 --- a/static/index.js +++ b/static/index.js @@ -1,7 +1,14 @@ import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; +import hljs from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js"; +import js from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/javascript.min.js"; +// import erlang from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/erlang.min.js"; console.log(CodeFlask); globalThis.CodeFlask = CodeFlask; +globalThis.hljs = hljs; + +hljs.registerLanguage("javascript", js); +// hljs.registerLanguage("erlang", erlang); const output = document.querySelector("#output"); const compiledJavascript = document.querySelector("#compiled-javascript"); @@ -50,11 +57,21 @@ function clearElement(target) { function appendCode(target, content, className) { if (!content) return; const element = document.createElement("pre"); - element.textContent = content; + const code = document.createElement("code"); + code.textContent = content; + // code.className = className; + element.appendChild(code); + // element.textContent = content; element.className = className; target.appendChild(element); } +function highlightOutput(target, childClassName) { + target.querySelectorAll(`.${childClassName}`).forEach((element) => { + hljs.highlightElement(element); + }) +} + const editor = new CodeFlask("#editor-target", { language: "gleam", defaultTheme: false, @@ -95,6 +112,8 @@ worker.onmessage = (event) => { if (result.log) appendCode(output, result.log, "log"); if (result.error) appendCode(output, result.error, "error"); if (result.js) appendCode(compiledJavascript, result.js, "javascript"); + highlightOutput(compiledJavascript, "javascript"); + // highlightOutput(compiledErlang, "erlang"); for (const warning of result.warnings || []) { appendCode(warning, "warning"); } diff --git a/static/js/highlight/highlight-gleam.js b/static/js/highlight/highlight-gleam.js deleted file mode 100644 index cc03f40..0000000 --- a/static/js/highlight/highlight-gleam.js +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Registers gleam as a language - * - * Based off https://github.com/gleam-lang/website/blob/main/javascript/highlightjs-gleam.js - * Edited to work with minified hightlightjs core v11 (module) & match more of the syntax - */ - -import hljs from "./highlight.core.min.js"; -import * as regexes from "./regexes.js"; - -/** - * Copies an object to prevent prototype pollution - * @template {object} TObject - the object's structure - * @param {TObject} obj - The source object to copy - * @returns {TObject} - A shallow copy of the source object - */ -const cp = (obj) => ({ ...obj }); - -// Define operators and keywords to highlight gleam code without spawning an editor -const GLEAM_OPERATORS = [ - "<<", - ">>", - "<-", - "->", - "|>", - "<>", - "..", - "<=", - "<=.", - ">=", - ">=.", - "==", - "==.", - "%", - "%.", - "!=", - "!=.", - "<", - "<.", - ">", - ">.", - "&&", - "||", - "+", - "+.", - "-", - "-.", - "/", - "/.", - "*", - "*.", - "=", -]; -const GLEAM_KEYWORDS = [ - "as", - "assert", - "auto", - "case", - "const", - "delegate", - "derive", - "echo", - "else", - "fn", - "if", - "implement", - "import", - "let", - "macro", - "opaque", - "panic", - "pub", - "test", - "todo", - "type", - "use", -]; - -/** - * HLJS modes - * Glorified regular expressions used to target & highlight code snippets - * - * Ordered by `relevance` -> more or less translates to parsing order / priority - * https://highlightjs.readthedocs.io/en/stable/language-guide.html#relevance - * - * Their `scope` maps to 1 or more css class - * https://highlightjs.readthedocs.io/en/stable/css-classes-reference.html - */ - -// Relevance 0 - -const PUNCTUATION = { - name: "punctuation", - scope: "punctuation", - match: regexes.punctuation, - relevance: 0, -}; - -const VARIABLES = { - scope: "variable", - match: regexes.snakeCase, - relevance: 0, -}; - -/** - * TODO: fix regex to not break other selectors - */ -const FUNCTION_PARAM = { - scope: "function-param", - match: regexes.functionParam, - relevance: 0, -}; - -const DISCARD_NAMES = { - scope: "attribute", - begin: regexes.discardName, - relevance: 0, -}; - -const MODULES = { - scope: "module", - match: regexes.importModule, - relevance: 0, -}; - -// Relevance 1 - -const OPERATORS = { - scope: "operator", - begin: regexes.operator, - keywords: { - operator: GLEAM_OPERATORS.join(" "), - $pattern: /\b\S+\b/g, - }, - relevance: 1, -}; - -const KEYWORDS = { - name: "Gleam keywords", - scope: "keyword", - keywords: { - keyword: GLEAM_KEYWORDS.join(" "), - operator: GLEAM_OPERATORS.join(" "), - }, - relevance: 1, -}; - -// Relevance 2 - -const LITERALS = { - name: "Booleans or Nil", - scope: "literal", - match: regexes.literal, - relevance: 2, -}; - -const NUMBERS = { - name: "Number", - scope: "number", - variants: [ - { - begin: regexes.number.binary, - }, - { - begin: regexes.number.octal, - }, - { - begin: regexes.number.hex, - }, - { - begin: regexes.number.decOrFloat, - }, - { - match: regexes.number.scientific, - }, - ], - relevance: 2, -}; - -// Relevance 3 - -const TYPES = { - name: "Types & Aliases", - scope: "type", - match: regexes.type, - relevance: 3, -}; - -// Relevance 4 - -const FUNCTION_CALL = { - name: "Function calls", - scope: "function function-name function-call", - match: regexes.functionCall, - relevance: 4, -}; - -const FUNCTION_DECLARATION = { - name: "function declaration", - scope: "function function-name", - beginKeywords: "fn", - end: regexes.endParenthesis, - returnEnd: true, - relevance: 4, -}; - -// Relevance 5 - -// Relevance 6 - -const ATTRIBUTES = { - name: "Attributes", - scope: "attribute", - match: regexes.attribute, - relevance: 6, -}; - -// Relevance 7 - -const STRINGS = { - name: "Strings", - scope: "string", - variants: [{ begin: /"/, end: /"/ }], - contains: [hljs.BACKSLASH_ESCAPE], - relevance: 7, -}; - -// Relevance 8 - -const BIT_ARRAYS = { - // bit array - begin: "<<", - end: ">>", - scope: "operator", - contains: [ - { - scope: "keyword", - beginKeywords: - "binary bits bytes int float bit_string bit_array bits utf8 utf16 " + - "utf32 utf8_codepoint utf16_codepoint utf32_codepoint signed " + - "unsigned big little native unit size", - }, - cp(KEYWORDS), - cp(STRINGS), - cp(VARIABLES), - cp(DISCARD_NAMES), - cp(NUMBERS), - cp(PUNCTUATION), - ], - relevance: 8, -}; - -// Relevance 10 - -const COMMENTS = { - name: "Comments", - scope: "comment", - match: regexes.comment, - relevance: 10, -}; - -/** - * Register the Gleam lang to HLJS global exported from `./highlight.core.min.js` - */ -hljs.registerLanguage("gleam", function (hljs) { - return { - name: "Gleam", - aliases: ["gleam"], - keywords: { - keyword: KEYWORDS.keywords.keyword, - operator: OPERATORS.keywords.operator, - }, - contains: [ - hljs.C_LINE_COMMENT_MODE, - cp(PUNCTUATION), - cp(MODULES), - cp(DISCARD_NAMES), - cp(OPERATORS), - cp(LITERALS), - cp(NUMBERS), - cp(TYPES), - cp(FUNCTION_DECLARATION), - cp(FUNCTION_CALL), - cp(ATTRIBUTES), - cp(STRINGS), - cp(COMMENTS), - ], - }; -}); - -/** - * Wait until other scripts & css are loaded before highlighting - */ -addEventListener("DOMContentLoaded", () => { - hljs.highlightAll(); -}); diff --git a/static/js/highlight/highlight.core.min.js b/static/js/highlight/highlight.core.min.js deleted file mode 100644 index d4dd62e..0000000 --- a/static/js/highlight/highlight.core.min.js +++ /dev/null @@ -1,307 +0,0 @@ -/*! - Highlight.js v11.9.0 (git: b7ec4bfafc) - (c) 2006-2023 undefined and other contributors - License: BSD-3-Clause - */ - function e(t){return t instanceof Map?t.clear=t.delete=t.set=()=>{ - throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ - throw Error("set is read-only") - }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ - const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) - })),t}class t{constructor(e){ - void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} - ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ - return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") - }function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] - ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope - ;class r{constructor(e,t){ - this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ - this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ - if(e.startsWith("language:"))return e.replace("language:","language-") - ;if(e.includes(".")){const n=e.split(".") - ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") - }return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} - closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ - this.buffer+=``}}const o=(e={})=>{const t={children:[]} - ;return Object.assign(t,e),t};class a{constructor(){ - this.rootNode=o(),this.stack=[this.rootNode]}get top(){ - return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ - this.top.children.push(e)}openNode(e){const t=o({scope:e}) - ;this.add(t),this.stack.push(t)}closeNode(){ - if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ - for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} - walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ - return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), - t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ - "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ - a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} - addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ - this.closeNode()}__addSublanguage(e,t){const n=e.root - ;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ - return new r(this,this.options).value()}finalize(){ - return this.closeAllNodes(),!0}}function l(e){ - return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} - function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} - function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ - const t=e[e.length-1] - ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} - })(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} - function p(e){return RegExp(e.toString()+"|").exec("").length-1} - const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ - ;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n - ;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} - s+=i.substring(0,e.index), - i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], - "("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} - const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={ - begin:"\\\\[\\s\\S]",relevance:0},k={scope:"string",begin:"'",end:"'", - illegal:"\\n",contains:[O]},v={scope:"string",begin:'"',end:'"',illegal:"\\n", - contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, - contains:[]},n);s.contains.push({scope:"doctag", - begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", - end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) - ;const r=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) - ;return s.contains.push({begin:h(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s - },S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var A=Object.freeze({ - __proto__:null,APOS_STRING_MODE:k,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ - scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N, - C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", - begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{ - "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ - t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, - MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, - NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w, - PHRASAL_WORDS_MODE:{ - begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ - },QUOTE_STRING_MODE:v,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, - end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, - RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", - SHEBANG:(e={})=>{const t=/^#![ ]*\// - ;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, - end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, - TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, - UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function j(e,t){ - "."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ - void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ - t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", - e.__beforeBegin=j,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, - void 0===e.relevance&&(e.relevance=0))}function L(e,t){ - Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ - if(e.match){ - if(e.begin||e.end)throw Error("begin & end are not supported with match") - ;e.begin=e.match,delete e.match}}function P(e,t){ - void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return - ;if(e.starts)throw Error("beforeMatch cannot be used with starts") - ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] - })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ - relevance:0,contains:[Object.assign(n,{endsParent:!0})] - },e.relevance=0,delete n.beforeMatch - },H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword" - ;function $(e,t,n=C){const i=Object.create(null) - ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ - Object.assign(i,$(e[n],t,n))})),i;function s(e,n){ - t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") - ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ - return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{ - console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{ - z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) - },K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],r={},o={} - ;for(let e=1;e<=t.length;e++)o[e+i]=s[e],r[e+i]=!0,i+=p(t[e-1]) - ;e[n]=o,e[n]._emit=r,e[n]._multi=!0}function Z(e){(e=>{ - e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, - delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ - _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope - }),(e=>{if(Array.isArray(e.begin)){ - if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), - K - ;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"), - K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ - if(Array.isArray(e.end)){ - if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"), - K - ;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"), - K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){ - function t(t,n){ - return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) - }class n{constructor(){ - this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} - addRule(e,t){ - t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), - this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) - ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" - }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex - ;const t=this.matcherRe.exec(e);if(!t)return null - ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] - ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ - this.rules=[],this.multiRegexes=[], - this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ - if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n - ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), - t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ - return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ - this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ - const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex - ;let n=t.exec(e) - ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ - const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} - return n&&(this.regexIndex+=n.position+1, - this.regexIndex===this.count&&this.considerAll()),n}} - if(e.compilerExtensions||(e.compilerExtensions=[]), - e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") - ;return e.classNameAliases=i(e.classNameAliases||{}),function n(r,o){const a=r - ;if(r.isCompiled)return a - ;[I,B,Z,D].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))), - r.__beforeBegin=null,[T,L,P].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null - ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), - c=r.keywords.$pattern, - delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=$(r.keywords,e.case_insensitive)), - a.keywordPatternRe=t(c,!0), - o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), - r.end&&(a.endRe=t(a.end)), - a.terminatorEnd=l(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)), - r.illegal&&(a.illegalRe=t(r.illegal)), - r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ - variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{ - starts:e.starts?i(e.starts):null - }):Object.isFrozen(e)?i(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a) - })),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new s - ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" - }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" - }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ - return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ - constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} - const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{ - const i=Object.create(null),s=Object.create(null),r=[];let o=!0 - ;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ - disableAutodetect:!0,name:"Plain text",contains:[]};let p={ - ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, - languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", - cssSelector:"pre code",languages:null,__emitter:c};function b(e){ - return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" - ;"object"==typeof t?(i=e, - n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."), - G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), - s=e,i=t),void 0===n&&(n=!0);const r={code:i,language:s};N("before:highlight",r) - ;const o=r.result?r.result:E(r.language,r.code,n) - ;return o.code=r.code,N("after:highlight",o),o}function E(e,n,s,r){ - const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) - ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" - ;for(;t;){n+=R.substring(e,t.index) - ;const s=_.case_insensitive?t[0].toLowerCase():t[0],r=(i=s,N.keywords[i]);if(r){ - const[e,i]=r - ;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(A+=i),e.startsWith("_"))n+=t[0];else{ - const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] - ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i - ;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ - if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ - if(!i[N.subLanguage])return void M.addText(R) - ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top - }else e=x(R,N.subLanguage.length?N.subLanguage:null) - ;N.relevance>0&&(A+=e.relevance),M.__addSublanguage(e._emitter,e.language) - })():l(),R=""}function u(e,t){ - ""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 - ;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} - const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} - function h(e,t){ - return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope), - e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), - R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ - value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) - ;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) - ;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ - for(;e.endsParent&&e.parent;)e=e.parent;return e}} - if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ - return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ - const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const r=N - ;N.endScope&&N.endScope._wrap?(g(), - u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), - d(N.endScope,e)):r.skip?R+=t:(r.returnEnd||r.excludeEnd||(R+=t), - g(),r.excludeEnd&&(R=t));do{ - N.scope&&M.closeNode(),N.skip||N.subLanguage||(A+=N.relevance),N=N.parent - }while(N!==s.parent);return s.starts&&h(s.starts,e),r.returnEnd?0:t.length} - let w={};function y(i,r){const a=r&&r[0];if(R+=i,null==a)return g(),0 - ;if("begin"===w.type&&"end"===r.type&&w.index===r.index&&""===a){ - if(R+=n.slice(r.index,r.index+1),!o){const t=Error(`0 width match regex (${e})`) - ;throw t.languageName=e,t.badRule=w.rule,t}return 1} - if(w=r,"begin"===r.type)return(e=>{ - const n=e[0],i=e.rule,s=new t(i),r=[i.__beforeBegin,i["on:begin"]] - ;for(const t of r)if(t&&(t(e,s),s.isMatchIgnored))return b(n) - ;return i.skip?R+=n:(i.excludeBegin&&(R+=n), - g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(r) - ;if("illegal"===r.type&&!s){ - const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') - ;throw e.mode=N,e}if("end"===r.type){const e=m(r);if(e!==ee)return e} - if("illegal"===r.type&&""===a)return 1 - ;if(I>1e5&&I>3*r.index)throw Error("potential infinite loop, way more iterations than matches") - ;return R+=a,a.length}const _=O(e) - ;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"') - ;const k=V(_);let v="",N=r||k;const S={},M=new p.__emitter(p);(()=>{const e=[] - ;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) - ;e.forEach((e=>M.openNode(e)))})();let R="",A=0,j=0,I=0,T=!1;try{ - if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ - I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=j - ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(j,e.index),e) - ;j=e.index+t}y(n.substring(j))}return M.finalize(),v=M.toHTML(),{language:e, - value:v,relevance:A,illegal:!1,_emitter:M,_top:N}}catch(t){ - if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), - illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j, - context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:v},_emitter:M};if(o)return{ - language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} - ;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ - const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} - ;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(v).map((t=>E(t,e,!1))) - ;s.unshift(n);const r=s.sort(((e,t)=>{ - if(e.relevance!==t.relevance)return t.relevance-e.relevance - ;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 - ;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=r,c=o - ;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{ - let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" - ;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) - ;return t||(X(a.replace("{}",n[1])), - X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} - return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return - ;if(N("before:highlightElement",{el:e,language:n - }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) - ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), - console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), - console.warn("The element with unescaped HTML:"), - console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) - ;t=e;const i=t.textContent,r=n?m(i,{language:n,ignoreIllegals:!0}):x(i) - ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n - ;e.classList.add("hljs"),e.classList.add("language-"+i) - })(e,n,r.language),e.result={language:r.language,re:r.relevance, - relevance:r.relevance},r.secondBest&&(e.secondBest={ - language:r.secondBest.language,relevance:r.secondBest.relevance - }),N("after:highlightElement",{el:e,result:r,text:i})}let y=!1;function _(){ - "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0 - }function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} - function k(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ - s[e.toLowerCase()]=t}))}function v(e){const t=O(e) - ;return t&&!t.disableAutodetect}function N(e,t){const n=e;r.forEach((e=>{ - e[n]&&e[n](t)}))} - "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ - y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_, - highlightElement:w, - highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"), - G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)}, - initHighlighting:()=>{ - _(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, - initHighlightingOnLoad:()=>{ - _(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") - },registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ - if(W("Language definition for '{}' could not be registered.".replace("{}",e)), - !o)throw t;W(t),s=l} - s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&k(s.aliases,{ - languageName:e})},unregisterLanguage:e=>{delete i[e] - ;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, - listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:k, - autoDetection:v,inherit:Q,addPlugin:e=>{(e=>{ - e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ - e["before:highlightBlock"](Object.assign({block:t.el},t)) - }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ - e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),r.push(e)}, - removePlugin:e=>{const t=r.indexOf(e);-1!==t&&r.splice(t,1)}}),n.debugMode=()=>{ - o=!1},n.safeMode=()=>{o=!0},n.versionString="11.9.0",n.regex={concat:h, - lookahead:g,either:f,optional:d,anyNumberOfTimes:u} - ;for(const t in A)"object"==typeof A[t]&&e(A[t]);return Object.assign(n,A),n - },ne=te({});ne.newInstance=()=>te({});export{ne as default}; \ No newline at end of file diff --git a/static/js/highlight/regexes.js b/static/js/highlight/regexes.js deleted file mode 100644 index 4477a98..0000000 --- a/static/js/highlight/regexes.js +++ /dev/null @@ -1,20 +0,0 @@ - -export const snakeCase = /\b(?:_)*[a-z_]+[a-z\d_]+\b/g; -export const punctuation = /[\\.,\(\):\[\]{}#]/g; -export const endParenthesis = /\(/g; -export const importModule = /(?<=import )\b(?:_)*[a-z_]+[a-z\d_]+\b/g -export const functionCall = /\b(?:_)*[a-z_]+[a-z\d_]+\b(?=\()/g; -export const functionParam = /(?<=\b(?:_)*[a-z_]+[a-z\d_]+\b\()(.|\s|\n)*(?=\))/g; -export const operator = /(<<|>>|<-|->|\|>|<>|\.\.|<=\.?|>=\.?|==\.?|!=\.?|<\.?|>\.?|&&|\|\||\+\.?|-\.?|\/\.?|\*\.?|%\.?|=)/g; -export const type = /\b[A-Z]{1}(?:[a-z]+[A-Z]{0,1})*\b/g; -export const literal = /\b(True|False|Nil)\b/g; -export const comment = /\/\/.*/g; -export const attribute = /@[a-zA-Z0-9]+\b(?=\()/g; -export const discardName = /\b_[a-z][a-z0-9_]*\b/g; -export const number = { - binary: "\\b0[bB](?:_?[01]+)+", - octal: "\\b0[oO](?:_?[0-7]+)+", - hex: "\\b0[xX](?:_?[0-9a-fA-F]+)+", - decOrFloat: /\b\d(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?/g, - scientific: /(?:(?:-\d)|\d)(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?e(?:(?:-\d)|\d)(?:_?\d+)*(?:\.(?:\d(?:_?\d+)*)*)?/g -} \ No newline at end of file From 4b490fb7a74dda42d6272de4de85ab1de0955525 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:00:32 +0100 Subject: [PATCH 07/18] playground: add sharing functionality --- static/index.js | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/static/index.js b/static/index.js index 3f33ac9..9a7723a 100644 --- a/static/index.js +++ b/static/index.js @@ -2,8 +2,8 @@ import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; import hljs from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js"; import js from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/javascript.min.js"; // import erlang from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/erlang.min.js"; +import lz from "https://cdn.jsdelivr.net/npm/lz-string@1.5.0/+esm"; -console.log(CodeFlask); globalThis.CodeFlask = CodeFlask; globalThis.hljs = hljs; @@ -59,17 +59,19 @@ function appendCode(target, content, className) { const element = document.createElement("pre"); const code = document.createElement("code"); code.textContent = content; - // code.className = className; element.appendChild(code); - // element.textContent = content; element.className = className; target.appendChild(element); } function highlightOutput(target, childClassName) { + // Disable annoying warnings from hljs + const warn = console.warn; + console.warn = () => { }; target.querySelectorAll(`.${childClassName}`).forEach((element) => { hljs.highlightElement(element); }) + console.warn = warn; } const editor = new CodeFlask("#editor-target", { @@ -125,3 +127,32 @@ worker.onmessage = (event) => { }; editor.onUpdate(debounce((code) => sendToWorker(code), 200)); + +// Title and hash +const titleInput = document.querySelector("#title-input"); + +// Get the title from the query string if it exists, +// otherwise use the title input value (so we can set the default in the HTML) +titleInput.value = new URLSearchParams(window.location.search).get("title") || titleInput.value; + +if (window.location.hash) { + const hash = window.location.hash.slice(1); + const code = lz.decompressFromBase64(hash); + if (code) { + editor.updateCode(code); + } +} + +const shareButton = document.querySelector("#share-button"); + +function share() { + const code = editor.getCode(); + const compressed = lz.compressToBase64(code); + const url = `${window.location.origin}${window.location.pathname}?title=${titleInput.value}#${compressed}`; + navigator.clipboard.writeText(url); + shareButton.textContent = "Copied!"; + setTimeout(() => { + shareButton.textContent = "Share"; + }, 1000); +} +shareButton.addEventListener("click", share); From a96ba5d5a22d9192d0516d602a78a13dbe125be6 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:21:09 +0100 Subject: [PATCH 08/18] playground: fix mobile css bug --- static/css/pages/lesson.css | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/static/css/pages/lesson.css b/static/css/pages/lesson.css index 6764e83..b342c4c 100644 --- a/static/css/pages/lesson.css +++ b/static/css/pages/lesson.css @@ -3,6 +3,10 @@ padding: var(--gap); } +#playground-container { + height: calc(100dvh - var(--navbar-height)); +} + #playground { display: flex; flex-direction: column; @@ -82,13 +86,15 @@ } #output-container { - min-height: 1rem; + height: 30dvh; background: var(--color-background-dim); } .output { /* Only display if radio is checked */ display: none; + max-height: calc(100% - 4 * var(--gap)); + overflow: auto; } .output>* { @@ -124,10 +130,6 @@ /* Larger than mobile */ @media (min-width: 768px) { - #playground-container { - height: calc(100dvh - var(--navbar-height)); - } - #playground-content { border-left: 1px solid var(--color-accent-muted); flex-direction: row; @@ -138,6 +140,7 @@ } #output-container { + height: unset; width: 50%; overflow: auto; border: none; From 5cfe9f020bca8e625597cfa9ff0856942209fbc3 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:22:23 +0100 Subject: [PATCH 09/18] playground: rename css file --- src/playground.gleam | 4 ++-- static/css/pages/{lesson.css => playground.css} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename static/css/pages/{lesson.css => playground.css} (100%) diff --git a/src/playground.gleam b/src/playground.gleam index 23ca322..4e656f3 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -351,7 +351,7 @@ const css_defaults_page = [css_fonts, css_theme, css__gleam_common, css_layout] const css_root = "/css/root.css" // Path to the css speciic to to lesson & main pages -const css_lesson_page = "/css/pages/lesson.css" +const css_playground_page = "/css/pages/playground.css" // Defines code syntax highlighting for highlightJS & CodeFlash // based on dark / light mode and the currenly loaded color scheme @@ -395,7 +395,7 @@ fn render_page_object(page: Page) -> String { stylesheets: list.flatten([ css_defaults_page, css_defaults_code, - [css_root, css_lesson_page], + [css_root, css_playground_page], ]), static_content: [render_navbar()], content: [ diff --git a/static/css/pages/lesson.css b/static/css/pages/playground.css similarity index 100% rename from static/css/pages/lesson.css rename to static/css/pages/playground.css From 2daa5d8b5088b703b50597ea211b157b91ee463c Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:23:06 +0100 Subject: [PATCH 10/18] playground: remove about page --- src/pages/about/en.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/pages/about/en.html diff --git a/src/pages/about/en.html b/src/pages/about/en.html deleted file mode 100644 index e69de29..0000000 From 667e47737d8558b306d016926d12971651dec39d Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:28:41 +0100 Subject: [PATCH 11/18] playground: make hash versioned --- static/index.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/static/index.js b/static/index.js index 9a7723a..b08c4ec 100644 --- a/static/index.js +++ b/static/index.js @@ -131,13 +131,52 @@ editor.onUpdate(debounce((code) => sendToWorker(code), 200)); // Title and hash const titleInput = document.querySelector("#title-input"); +/** + * Hashed object format: + * { + * version: 1, + * content: "code" + * } + */ +function makeV1Hash(code) { + return lz.compressToBase64(JSON.stringify({ + version: 1, + content: code, + })); +} + +function parseV1Hash(obj) { + if (obj.version !== 1) { + throw new Error("Unsupported version"); + } + return obj.content; +} + +function parseHash(hash) { + let obj; + try { + obj = JSON.parse(lz.decompressFromBase64(hash)); + } catch (e) { + return null; + } + if (!obj) { + return null; + } + switch (obj.version) { + case 1: + return parseV1Hash(obj); + } + return null +} + + // Get the title from the query string if it exists, // otherwise use the title input value (so we can set the default in the HTML) titleInput.value = new URLSearchParams(window.location.search).get("title") || titleInput.value; if (window.location.hash) { const hash = window.location.hash.slice(1); - const code = lz.decompressFromBase64(hash); + const code = parseHash(hash); if (code) { editor.updateCode(code); } @@ -147,7 +186,7 @@ const shareButton = document.querySelector("#share-button"); function share() { const code = editor.getCode(); - const compressed = lz.compressToBase64(code); + const compressed = makeV1Hash(code); const url = `${window.location.origin}${window.location.pathname}?title=${titleInput.value}#${compressed}`; navigator.clipboard.writeText(url); shareButton.textContent = "Copied!"; From da61c84f48adc62788d772b0722ad9c1fcc19412 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:33:38 +0100 Subject: [PATCH 12/18] playground: code cleanup and titles --- README.md | 4 ++-- src/playground.gleam | 4 +--- src/playground/html.gleam | 4 ++-- static/index.js | 3 +++ 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 45b5b93..015afbf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# The Gleam Language Tour +# The Gleam Playground -An interactive tour of the Gleam programming language. +An interactive playground for the Gleam language. ```sh # Download a wasm version of the Gleam compiler diff --git a/src/playground.gleam b/src/playground.gleam index 4e656f3..c90a153 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -65,8 +65,6 @@ pub fn main() { use _ <- result.try(write_pages(pages)) io.debug("Done rendering pages") - // use p <- result.try(load_content()) - // use _ <- result.try(write_content(p)) Ok(Nil) } @@ -371,7 +369,7 @@ const css_defaults_code = [css_syntax_highlight, css_scheme_atom_one] /// Renders the navbar with common links fn render_navbar() -> Html { - widgets.navbar(titled: "Gleam Language Tour", links: [ + widgets.navbar(titled: "Gleam Playground", links: [ Link(label: "gleam.run", to: "http://gleam.run"), ]) } diff --git a/src/playground/html.gleam b/src/playground/html.gleam index e752072..1f2a028 100644 --- a/src/playground/html.gleam +++ b/src/playground/html.gleam @@ -181,7 +181,7 @@ pub type PageConfig { ) } -/// Renders a page in the language tour +/// Renders a page pub fn render_page_html(page config: PageConfig) -> Html { // add path-specific class to body to make styling easier let body_class = #("id", "page" <> string.replace(config.path, "/", "-")) @@ -191,7 +191,7 @@ pub fn render_page_html(page config: PageConfig) -> Html { head: HeadConfig( description: "An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!", image: "https://gleam.run/images/og-image.png", - title: config.title <> " - The Gleam Language Tour", + title: "The Gleam Playground", url: "https://tour.gleam.run/" <> config.path, path: config.path, meta: [], diff --git a/static/index.js b/static/index.js index b08c4ec..efd9116 100644 --- a/static/index.js +++ b/static/index.js @@ -1,6 +1,8 @@ import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; import hljs from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js"; import js from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/javascript.min.js"; +// TODO: add Erlang support once we have the precompiled stdlib +// in the browser // import erlang from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/erlang.min.js"; import lz from "https://cdn.jsdelivr.net/npm/lz-string@1.5.0/+esm"; @@ -173,6 +175,7 @@ function parseHash(hash) { // Get the title from the query string if it exists, // otherwise use the title input value (so we can set the default in the HTML) titleInput.value = new URLSearchParams(window.location.search).get("title") || titleInput.value; +document.title = `${titleInput.value} - The Gleam Playground`; if (window.location.hash) { const hash = window.location.hash.slice(1); From f394876578655c0deaffe6b80a773f4af0f7e513 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 15 May 2024 22:41:16 +0100 Subject: [PATCH 13/18] ci: yoink ci from language tour --- .github/workflows/deploy.yml | 46 ++++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 8 ++++--- 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..caa0f11 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: deploy +on: + push: + branches: ["main"] + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.1.0" + rebar3-version: "3" + + - name: Download WASM version of Gleam compiler + run: ./bin/download-compiler + - name: Build site + run: gleam run + + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'public' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12b9772..f754191 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,15 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: "26.0.2" + # Ensure you update the bin/download-compiler Gleam version to match this gleam-version: "1.1.0" rebar3-version: "3" - # elixir-version: "1.15.4" + - run: ./bin/download-compiler - run: gleam deps download - - run: gleam test - run: gleam format --check src test + - run: gleam test + - run: gleam run From e4438b4ec835e3709c614b050e06b0b5e5c6ffe0 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sun, 19 May 2024 11:21:33 +0100 Subject: [PATCH 14/18] playground: address comments --- src/pages/en.html | 53 ------------------ src/playground.gleam | 104 ++++------------------------------- src/playground/html.gleam | 6 +- src/playground/pages.gleam | 75 +++++++++++++++++++++++++ src/playground/widgets.gleam | 7 +-- 5 files changed, 90 insertions(+), 155 deletions(-) delete mode 100644 src/pages/en.html create mode 100644 src/playground/pages.gleam diff --git a/src/pages/en.html b/src/pages/en.html deleted file mode 100644 index a6bc4dd..0000000 --- a/src/pages/en.html +++ /dev/null @@ -1,53 +0,0 @@ -
-
-
- - -
- -
-
-
-
- -
-
- - - - - - - -
- - - -
-
-
-
diff --git a/src/playground.gleam b/src/playground.gleam index c90a153..b8a65c8 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -9,6 +9,7 @@ import playground/html.{ PageConfig, ScriptConfig, ScriptOptions, html_dangerous_inline_script, html_script, render_page, } +import playground/pages.{home_page} import playground/widgets.{Link} import simplifile import snag @@ -31,8 +32,6 @@ const compiler_wasm = "./wasm-compiler" const home_title = "Gleam Playground" -const pages_path = "src/pages" - const hello_joe = "import gleam/io pub fn main() { @@ -44,27 +43,15 @@ pub fn main() { const path_home = "/" -// Don't include deprecated stdlib modules -const skipped_stdlib_modules = [ - "bit_string.gleam", "bit_builder.gleam", "map.gleam", -] - pub fn main() { - let _ = { - use f <- result.try(load_file_names(pages_path, [])) - io.debug(read_pages(f)) - } - 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 filenames <- result.try(load_file_names(pages_path, [])) - use pages <- result.try(read_pages(filenames)) + use pages <- result.try(get_pages()) use _ <- result.try(write_pages(pages)) - io.debug("Done rendering pages") Ok(Nil) } @@ -82,72 +69,14 @@ type FileNames { FileNames(path: String, name: String, slug: String) } -/// Recursively list files in a directory -fn load_file_names( - path: String, - filenames: List(FileNames), -) -> snag.Result(List(FileNames)) { - use files <- result.try( - simplifile.read_directory(path) - |> file_error("Failed to read directory " <> path), - ) - - files - |> list.filter(fn(file) { !string.starts_with(file, ".") }) - |> list.fold(Ok(filenames), fn(filenames, file_or_dir_path) { - use filenames <- result.try(filenames) - let full_file_path = path <> "/" <> file_or_dir_path - case simplifile.is_directory(full_file_path) { - True -> load_file_names(full_file_path, filenames) - False -> { - let slug = - full_file_path - |> string.replace(pages_path <> "/", "") - |> string.split("/") - |> list.reverse - |> list.drop(1) - |> list.reverse - |> string.join("/") - |> fn(s) { path_home <> s } - - let name = - slug - |> string.replace("/", " ") - |> string.replace("-", " ") - |> string.capitalise - |> string_coalesce(home_title) - - let file = FileNames(path: full_file_path, name: name, slug: slug) - Ok([file, ..filenames]) - } - } - }) -} - -fn string_coalesce(coalesce value: String, with replacement: String) -> String { - case value { - "" -> replacement - _ -> value - } -} - type Page { - Page(filenames: FileNames, content: String) -} - -fn read_pages(filenames: List(FileNames)) -> snag.Result(List(Page)) { - filenames - |> list.fold(Ok([]), fn(pages, filename) { - use pages <- result.try(pages) - use content <- result.try(read_file(filename.path)) - let page = Page(filenames: filename, content: content) - Ok([page, ..pages]) - }) + Page(filenames: FileNames, content: Html) } -fn read_file(path: String) -> snag.Result(String) { - simplifile.read(path) - |> file_error("Failed to read file " <> path) +fn get_pages() -> snag.Result(List(Page)) { + Ok([ + Page(FileNames(path: path_home, name: home_title, slug: "/"), home_page()), + ]) } fn ensure_directory(path: String) -> snag.Result(Nil) { @@ -189,7 +118,6 @@ fn make_stdlib_available() -> snag.Result(Nil) { 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( @@ -345,7 +273,7 @@ const css_defaults_page = [css_fonts, css_theme, css__gleam_common, css_layout] // Page stylesheet paths -/// Common stylesheet for all tour pages +/// Common stylesheet for all playground pages const css_root = "/css/root.css" // Path to the css speciic to to lesson & main pages @@ -388,7 +316,7 @@ pub fn theme_picker_script() -> Html { fn render_page_object(page: Page) -> String { render_page(PageConfig( - path: page.filenames.path, + path: page.filenames.slug, title: page.filenames.name, stylesheets: list.flatten([ css_defaults_page, @@ -396,9 +324,7 @@ fn render_page_object(page: Page) -> String { [css_root, css_playground_page], ]), static_content: [render_navbar()], - content: [ - htmb.dangerous_unescaped_fragment(string_builder.from_string(page.content)), - ], + content: [page.content], scripts: ScriptConfig( body: [ theme_picker_script(), @@ -416,18 +342,10 @@ fn render_page_object(page: Page) -> String { fn write_pages(pages: List(Page)) -> snag.Result(Nil) { list.try_each(pages, fn(page) { - let dir = filepath.join(public, slugify_path(page.filenames.slug)) + let dir = filepath.join(public, page.filenames.slug) use _ <- result.try(ensure_directory(dir)) let path = filepath.join(dir, "index.html") let html = render_page_object(page) write_text(path, html) }) } - -/// Renders a Lesson's page -/// Complete with title, lesson, editor and output -/// Transform a path into a slug -fn slugify_path(path: String) -> String { - string.replace(path, "/", "-") - |> string.drop_left(up_to: 1) -} diff --git a/src/playground/html.gleam b/src/playground/html.gleam index 1f2a028..68e1fb8 100644 --- a/src/playground/html.gleam +++ b/src/playground/html.gleam @@ -189,10 +189,10 @@ pub fn render_page_html(page config: PageConfig) -> Html { // render html html(HtmlConfig( head: HeadConfig( - description: "An interactive introduction and reference to the Gleam programming language. Learn Gleam in your browser!", + description: "A playground for the Gleam programming language. Write, run, and share Gleam code in your browser.", image: "https://gleam.run/images/og-image.png", title: "The Gleam Playground", - url: "https://tour.gleam.run/" <> config.path, + url: "https://play.gleam.run" <> config.path, path: config.path, meta: [], stylesheets: config.stylesheets, @@ -200,7 +200,7 @@ pub fn render_page_html(page config: PageConfig) -> Html { html_script( "https://plausible.io/js/script.js", ScriptOptions(defer: True, module: False), - [#("data-domain", "tour.gleam.run")], + [#("data-domain", "play.gleam.run")], ), ..config.scripts.head ], diff --git a/src/playground/pages.gleam b/src/playground/pages.gleam new file mode 100644 index 0000000..b5936af --- /dev/null +++ b/src/playground/pages.gleam @@ -0,0 +1,75 @@ +import htmb.{type Html, h, text} + +fn output_tab(label: String, id: String, value: String, checked: Bool) -> Html { + let checked = case checked { + True -> "true" + False -> "false" + } + h("label", [#("class", "tab")], [ + h("p", [], [text(label)]), + h( + "input", + [ + #("type", "radio"), + #("id", id), + #("name", "output-display"), + #("value", value), + #("hidden", "true"), + #("checked", checked), + ], + [], + ), + ]) +} + +pub fn home_page() -> Html { + h("article", [#("id", "playground-container")], [ + h("section", [#("id", "playground")], [ + h("div", [#("id", "playground-header")], [ + h( + "input", + [ + #("id", "title-input"), + #("type", "text"), + #("name", "title"), + #("value", "A Gleam Playground project"), + #("placeholder", "Project title"), + ], + [], + ), + h("button", [#("id", "share-button")], [text("Share")]), + ]), + h("div", [#("id", "playground-content")], [ + h("section", [#("id", "editor")], [ + h("div", [#("id", "editor-target")], []), + ]), + h("div", [#("id", "output-container")], [ + h("div", [#("id", "tabs")], [ + output_tab("Output", "output-radio", "output", True), + // output_tab( + // "Compiled Erlang", + // "compiled-erlang-radio", + // "erlang", + // False, + // ), + output_tab( + "Compiled JavaScript", + "compiled-javascript-radio", + "javascript", + False, + ), + ]), + h("aside", [#("id", "output"), #("class", "output")], []), + h( + "aside", + [ + #("id", "compiled-javascript"), + #("class", "output language-javascript"), + ], + [], + ), + ]), + ]), + ]), + ]) +} diff --git a/src/playground/widgets.gleam b/src/playground/widgets.gleam index 8ac9f08..15c71b7 100644 --- a/src/playground/widgets.gleam +++ b/src/playground/widgets.gleam @@ -184,7 +184,7 @@ pub fn text_link( anchor(link.to, link_attributes, [text(link.label)]) } -/// Renders the tour's navbar as html +/// Renders the playground's navbar as html pub fn navbar(titled title: String, links links: List(Link)) -> Html { let links = list.map(links, fn(l) { text_link(l, []) }) @@ -205,8 +205,3 @@ pub fn navbar(titled title: String, links links: List(Link)) -> Html { h("div", [#("class", "nav-right")], nav_right_items), ]) } - -/// Renders a horizontal separator -pub fn separator(class: String) -> Html { - h("hr", [#("class", class <> "-separator")], []) -} From 71fd63624b2ee217fd05b3f0a7cf0f5287426039 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sun, 19 May 2024 11:33:53 +0100 Subject: [PATCH 15/18] playground: fix warning bugs --- static/index.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/static/index.js b/static/index.js index efd9116..421e8d2 100644 --- a/static/index.js +++ b/static/index.js @@ -12,9 +12,9 @@ globalThis.hljs = hljs; hljs.registerLanguage("javascript", js); // hljs.registerLanguage("erlang", erlang); -const output = document.querySelector("#output"); -const compiledJavascript = document.querySelector("#compiled-javascript"); -// const compiledErlang = document.querySelector("#compiled-erlang"); +const outputEl = document.querySelector("#output"); +const compiledJavascriptEl = document.querySelector("#compiled-javascript"); +// const compiledErlangEl = document.querySelector("#compiled-erlang"); const initialCode = document.querySelector("#code").innerHTML; const prismGrammar = { @@ -111,17 +111,25 @@ function sendToWorker(code) { worker.onmessage = (event) => { // Handle the result of the compilation and execution const result = event.data; - clearElement(output); - clearElement(compiledJavascript); - if (result.log) appendCode(output, result.log, "log"); - if (result.error) appendCode(output, result.error, "error"); - if (result.js) appendCode(compiledJavascript, result.js, "javascript"); - highlightOutput(compiledJavascript, "javascript"); - // highlightOutput(compiledErlang, "erlang"); + clearElement(outputEl); + clearElement(compiledJavascriptEl); + // clearElement(compiledErlangEl); + if (result.log) { + appendCode(outputEl, result.log, "log"); + } + if (result.error) { + appendCode(outputEl, result.error, "error"); + } + if (result.js) { + appendCode(compiledJavascriptEl, result.js, "javascript"); + } for (const warning of result.warnings || []) { - appendCode(warning, "warning"); + appendCode(outputEl, warning, "warning"); } + highlightOutput(compiledJavascriptEl, "javascript"); + // highlightOutput(compiledErlangEl, "erlang"); + // Deal with any queued work workerWorking = false; if (queuedWork) sendToWorker(queuedWork); From a5c1d6c164c32ff75be143c989fddb87862cb12f Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sun, 19 May 2024 14:20:42 +0100 Subject: [PATCH 16/18] playground: add compiled erlang --- src/playground.gleam | 3 --- src/playground/pages.gleam | 28 +++++++++++++--------------- static/index.js | 15 ++++++++------- static/worker.js | 10 ++++------ 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/playground.gleam b/src/playground.gleam index b8a65c8..78b81b9 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -192,9 +192,6 @@ fn generate_stdlib_bundle(modules: List(String)) -> snag.Result(Nil) { |> 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") diff --git a/src/playground/pages.gleam b/src/playground/pages.gleam index b5936af..59001d6 100644 --- a/src/playground/pages.gleam +++ b/src/playground/pages.gleam @@ -22,6 +22,10 @@ fn output_tab(label: String, id: String, value: String, checked: Bool) -> Html { ]) } +fn output_container(id: String, class: String) -> Html { + h("aside", [#("id", id), #("class", class)], []) +} + pub fn home_page() -> Html { h("article", [#("id", "playground-container")], [ h("section", [#("id", "playground")], [ @@ -46,12 +50,12 @@ pub fn home_page() -> Html { h("div", [#("id", "output-container")], [ h("div", [#("id", "tabs")], [ output_tab("Output", "output-radio", "output", True), - // output_tab( - // "Compiled Erlang", - // "compiled-erlang-radio", - // "erlang", - // False, - // ), + output_tab( + "Compiled Erlang", + "compiled-erlang-radio", + "erlang", + False, + ), output_tab( "Compiled JavaScript", "compiled-javascript-radio", @@ -59,15 +63,9 @@ pub fn home_page() -> Html { False, ), ]), - h("aside", [#("id", "output"), #("class", "output")], []), - h( - "aside", - [ - #("id", "compiled-javascript"), - #("class", "output language-javascript"), - ], - [], - ), + output_container("output", "output"), + output_container("compiled-erlang", "output language-erlang"), + output_container("compiled-javascript", "output language-javascript"), ]), ]), ]), diff --git a/static/index.js b/static/index.js index 421e8d2..626d371 100644 --- a/static/index.js +++ b/static/index.js @@ -1,20 +1,18 @@ import CodeFlask from "https://cdn.jsdelivr.net/npm/codeflask@1.4.1/+esm"; import hljs from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js"; import js from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/javascript.min.js"; -// TODO: add Erlang support once we have the precompiled stdlib -// in the browser -// import erlang from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/erlang.min.js"; +import erlang from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/erlang.min.js"; import lz from "https://cdn.jsdelivr.net/npm/lz-string@1.5.0/+esm"; globalThis.CodeFlask = CodeFlask; globalThis.hljs = hljs; hljs.registerLanguage("javascript", js); -// hljs.registerLanguage("erlang", erlang); +hljs.registerLanguage("erlang", erlang); const outputEl = document.querySelector("#output"); const compiledJavascriptEl = document.querySelector("#compiled-javascript"); -// const compiledErlangEl = document.querySelector("#compiled-erlang"); +const compiledErlangEl = document.querySelector("#compiled-erlang"); const initialCode = document.querySelector("#code").innerHTML; const prismGrammar = { @@ -113,7 +111,7 @@ worker.onmessage = (event) => { const result = event.data; clearElement(outputEl); clearElement(compiledJavascriptEl); - // clearElement(compiledErlangEl); + clearElement(compiledErlangEl); if (result.log) { appendCode(outputEl, result.log, "log"); } @@ -123,12 +121,15 @@ worker.onmessage = (event) => { if (result.js) { appendCode(compiledJavascriptEl, result.js, "javascript"); } + if (result.erlang) { + appendCode(compiledErlangEl, result.erlang, "erlang"); + } for (const warning of result.warnings || []) { appendCode(outputEl, warning, "warning"); } highlightOutput(compiledJavascriptEl, "javascript"); - // highlightOutput(compiledErlangEl, "erlang"); + highlightOutput(compiledErlangEl, "erlang"); // Deal with any queued work workerWorking = false; diff --git a/static/worker.js b/static/worker.js index 0204b99..08c8527 100644 --- a/static/worker.js +++ b/static/worker.js @@ -36,9 +36,7 @@ async function compileEval(code) { const result = { log: null, js: null, - // TODO: add Erlang support once we have the precompiled stdlib - // in the browser - // erl: null, + erlang: null, error: null, warnings: [], }; @@ -47,13 +45,13 @@ async function compileEval(code) { project.writeModule("main", code); project.compilePackage("javascript"); const js = project.readCompiledJavaScript("main"); - // project.compilePackage("erlang"); - // const erl = project.readCompiledErlang("main"); + project.compilePackage("erlang"); + const erlang = project.readCompiledErlang("main"); const main = await loadProgram(js); if (main) main(); result.js = js; - // result.erl = erl; + result.erlang = erlang; } catch (error) { console.error(error); result.error = error.toString(); From b40e0557914cc9929e044b42156b0e7bfff197b6 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Fri, 7 Jun 2024 20:28:38 +0100 Subject: [PATCH 17/18] encode title uri component --- static/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.js b/static/index.js index 626d371..2bcc50a 100644 --- a/static/index.js +++ b/static/index.js @@ -199,7 +199,7 @@ const shareButton = document.querySelector("#share-button"); function share() { const code = editor.getCode(); const compressed = makeV1Hash(code); - const url = `${window.location.origin}${window.location.pathname}?title=${titleInput.value}#${compressed}`; + const url = `${window.location.origin}${window.location.pathname}?title=${encodeURIComponent(titleInput.value)}#${compressed}`; navigator.clipboard.writeText(url); shareButton.textContent = "Copied!"; setTimeout(() => { From 1d63b23f68366de274e825a3476ce9a881a3c520 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Thu, 26 Sep 2024 14:07:59 +0100 Subject: [PATCH 18/18] simplify code --- gleam.toml | 4 +- manifest.toml | 8 +- src/playground.gleam | 189 ++++++++++++++++++++------------ src/playground/html.gleam | 145 +----------------------- src/playground/pages.gleam | 73 ------------ src/playground/widgets.gleam | 47 ++++++-- static/css/pages/playground.css | 50 +++------ static/index.js | 30 ++--- 8 files changed, 194 insertions(+), 352 deletions(-) delete mode 100644 src/playground/pages.gleam diff --git a/gleam.toml b/gleam.toml index f75db55..2144261 100644 --- a/gleam.toml +++ b/gleam.toml @@ -15,10 +15,10 @@ target = "javascript" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" -simplifile = "~> 1.0" +simplifile = ">= 2.2.0 and < 3.0.0" snag = "~> 0.2" htmb = "~> 1.1" -filepath = "~> 0.1" +filepath = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 23d8eb3..52d1718 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,18 +2,18 @@ # You typically do not need to edit this file packages = [ - { name = "filepath", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "FC1B1B29438A5BA6C990F8047A011430BEC0C5BA638BFAA62718C4EAEFE00435" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, { name = "htmb", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "htmb", source = "hex", outer_checksum = "30D448F0E15DFCF7283AAAC2F351D77B9D54E318219C9FDDB1877572B67C27B7" }, - { name = "simplifile", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C44DB387524F90DC42142699C78C850003289D32C7C99C7D32873792A299CDF7" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, ] [requirements] -filepath = { version = "~> 0.1" } +filepath = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } htmb = { version = "~> 1.1" } -simplifile = { version = "~> 1.0" } +simplifile = { version = ">= 2.2.0 and < 3.0.0" } snag = { version = "~> 0.2" } diff --git a/src/playground.gleam b/src/playground.gleam index 78b81b9..91fd8d6 100644 --- a/src/playground.gleam +++ b/src/playground.gleam @@ -6,14 +6,25 @@ import gleam/string import gleam/string_builder import htmb.{type Html, h} import playground/html.{ - PageConfig, ScriptConfig, ScriptOptions, html_dangerous_inline_script, - html_script, render_page, + ScriptOptions, html_dangerous_inline_script, html_link, html_meta, + html_meta_prop, html_script, html_stylesheet, html_title, } -import playground/pages.{home_page} -import playground/widgets.{Link} +import playground/widgets.{output_container, output_tab} import simplifile import snag +// Meta bits + +const meta_title = "The Gleam Playground" + +const meta_description = "A playground for the Gleam programming language. Write, run, and share Gleam code in your browser." + +const meta_image = "https://gleam.run/images/og-image.png" + +const meta_url = "https://play.gleam.run" + +// Paths + const static = "static" const public = "public" @@ -30,8 +41,6 @@ const stdlib_external = "build/packages/gleam_stdlib/src" const compiler_wasm = "./wasm-compiler" -const home_title = "Gleam Playground" - const hello_joe = "import gleam/io pub fn main() { @@ -41,16 +50,22 @@ pub fn main() { // page paths -const path_home = "/" - 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 pages <- result.try(get_pages()) - use _ <- result.try(write_pages(pages)) + + let page_html = + home_page() + |> htmb.render_page("html") + |> string_builder.to_string + + use _ <- result.try(ensure_directory(public)) + let path = filepath.join(public, "index.html") + + use _ <- result.try(write_text(path, page_html)) Ok(Nil) } @@ -65,20 +80,6 @@ pub fn main() { } } -type FileNames { - FileNames(path: String, name: String, slug: String) -} - -type Page { - Page(filenames: FileNames, content: Html) -} - -fn get_pages() -> snag.Result(List(Page)) { - Ok([ - Page(FileNames(path: path_home, name: home_title, slug: "/"), home_page()), - ]) -} - fn ensure_directory(path: String) -> snag.Result(Nil) { simplifile.create_directory_all(path) |> file_error("Failed to create directory " <> path) @@ -90,10 +91,11 @@ fn write_text(path: String, text: String) -> snag.Result(Nil) { } fn copy_wasm_compiler() -> snag.Result(Nil) { - use <- require( - simplifile.is_directory(compiler_wasm), - "compiler-wasm must have been compiled", + use compiler_wasm_exists <- result.try( + simplifile.is_directory(compiler_wasm) + |> file_error("Failed to check compiler-wasm directory"), ) + use <- require(compiler_wasm_exists, "compiler-wasm must have been compiled") simplifile.copy_directory(compiler_wasm, public <> "/compiler") |> file_error("Failed to copy compiler-wasm") @@ -154,8 +156,12 @@ fn copy_stdlib_externals() -> snag.Result(Nil) { } fn copy_compiled_stdlib(modules: List(String)) -> snag.Result(Nil) { + use stdlib_dir_exists <- result.try( + simplifile.is_directory(stdlib_compiled) + |> file_error("Failed to check stdlib directory"), + ) use <- require( - simplifile.is_directory(stdlib_compiled), + stdlib_dir_exists, "Project must have been compiled for JavaScript", ) @@ -290,15 +296,6 @@ const css_scheme_atom_one = "/css/code/color-schemes/atom-one.css" /// To be used alonside defaults_page const css_defaults_code = [css_syntax_highlight, css_scheme_atom_one] -// Common page HTML elements renders - -/// Renders the navbar with common links -fn render_navbar() -> Html { - widgets.navbar(titled: "Gleam Playground", links: [ - Link(label: "gleam.run", to: "http://gleam.run"), - ]) -} - /// Renders the script that that contains the code /// needed for the light/dark theme picker to work pub fn theme_picker_script() -> Html { @@ -311,38 +308,94 @@ pub fn theme_picker_script() -> Html { // Page Renders -fn render_page_object(page: Page) -> String { - render_page(PageConfig( - path: page.filenames.slug, - title: page.filenames.name, - stylesheets: list.flatten([ - css_defaults_page, - css_defaults_code, - [css_root, css_playground_page], +fn home_page() -> Html { + let head_content = [ + // Meta property tags + html_meta_prop("og:type", "website"), + html_meta_prop("og:title", meta_title), + html_meta_prop("og:description", meta_description), + html_meta_prop("og:url", meta_url), + html_meta_prop("og:image", meta_image), + html_meta_prop("twitter:card", "summary_large_image"), + html_meta_prop("twitter:url", meta_url), + html_meta_prop("twitter:title", meta_title), + html_meta_prop("twitter:description", meta_description), + html_meta_prop("twitter:image", meta_image), + // Page meta + html_meta([#("charset", "utf-8")]), + html_meta([ + #("name", "viewport"), + #("content", "width=device-width, initial-scale=1"), ]), - static_content: [render_navbar()], - content: [page.content], - scripts: ScriptConfig( - body: [ - theme_picker_script(), - h("script", [#("type", "gleam"), #("id", "code")], [ - htmb.dangerous_unescaped_fragment(string_builder.from_string( - hello_joe, - )), - ]), - html_script("/index.js", ScriptOptions(module: True, defer: False), []), - ], - head: [], + html_title(meta_title), + html_meta([#("name", "description"), #("content", meta_description)]), + // Links + html_link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"), + // Scripts + html_script( + "https://plausible.io/js/script.js", + ScriptOptions(defer: True, module: False), + [#("data-domain", "play.gleam.run")], ), - )) -} + // Stylesheets + ..{ + list.flatten([ + css_defaults_page, + css_defaults_code, + [css_root, css_playground_page], + ]) + |> list.map(html_stylesheet) + } + ] -fn write_pages(pages: List(Page)) -> snag.Result(Nil) { - list.try_each(pages, fn(page) { - let dir = filepath.join(public, page.filenames.slug) - use _ <- result.try(ensure_directory(dir)) - let path = filepath.join(dir, "index.html") - let html = render_page_object(page) - write_text(path, html) - }) + let body_scripts = [ + theme_picker_script(), + h("script", [#("type", "gleam"), #("id", "code")], [ + htmb.dangerous_unescaped_fragment(string_builder.from_string(hello_joe)), + ]), + html_script("/index.js", ScriptOptions(module: True, defer: False), []), + ] + + let body_content = [ + widgets.navbar(), + h("article", [#("id", "playground-container")], [ + h("section", [#("id", "playground")], [ + h("div", [#("id", "playground-content")], [ + h("section", [#("id", "editor")], [ + h("div", [#("id", "editor-target")], []), + ]), + h("div", [#("id", "output-container")], [ + h("div", [#("id", "tabs")], [ + output_tab("Output", "output-radio", "output", True), + output_tab( + "Compiled Erlang", + "compiled-erlang-radio", + "erlang", + False, + ), + output_tab( + "Compiled JavaScript", + "compiled-javascript-radio", + "javascript", + False, + ), + h("button", [#("id", "share-button")], [htmb.text("Share code")]), + ]), + output_container("output", "output"), + output_container("compiled-erlang", "output language-erlang"), + output_container( + "compiled-javascript", + "output language-javascript", + ), + ]), + ]), + ]), + ]), + ..body_scripts + ] + + h("html", [#("class", "theme-light"), #("lang", "en-GB")], [ + h("head", [], head_content), + h("body", [], body_content), + ]) } diff --git a/src/playground/html.gleam b/src/playground/html.gleam index 68e1fb8..204924f 100644 --- a/src/playground/html.gleam +++ b/src/playground/html.gleam @@ -1,9 +1,9 @@ +//// Generic HTML rendering utils + import gleam/list -import gleam/string import gleam/string_builder import htmb.{type Html, h, text} -/// Generic HTML rendering utils pub type HtmlAttribute = #(String, String) @@ -82,144 +82,3 @@ pub fn html_stylesheet(src: String) -> Html { pub fn html_title(title: String) -> Html { h("title", [], [text(title)]) } - -pub type HeadConfig { - HeadConfig( - path: String, - title: String, - description: String, - url: String, - image: String, - meta: List(Html), - stylesheets: List(String), - scripts: List(Html), - ) -} - -/// Renders the page head as HTML -fn head(with config: HeadConfig) -> htmb.Html { - let meta_tags = [ - html_meta_prop("og:type", "website"), - html_meta_prop("og:title", config.title), - html_meta_prop("og:description", config.description), - html_meta_prop("og:url", config.url), - html_meta_prop("og:image", config.image), - html_meta_prop("twitter:card", "summary_large_image"), - html_meta_prop("twitter:url", config.url), - html_meta_prop("twitter:title", config.title), - html_meta_prop("twitter:description", config.description), - html_meta_prop("twitter:image", config.image), - ..config.meta - ] - - let head_meta = [ - html_meta([#("charset", "utf-8")]), - html_meta([ - #("name", "viewport"), - #("content", "width=device-width, initial-scale=1"), - ]), - html_title(config.title), - html_meta([#("name", "description"), #("content", config.description)]), - ..meta_tags - ] - - let head_links = [ - html_link("shortcut icon", "https://gleam.run/images/lucy/lucy.svg"), - ..list.map(config.stylesheets, html_stylesheet) - ] - - let head_content = list.concat([head_meta, head_links, config.scripts]) - - h("head", [], head_content) -} - -pub type BodyConfig { - BodyConfig( - content: List(Html), - static_content: List(Html), - scripts: List(Html), - attributes: List(HtmlAttribute), - ) -} - -/// Renders an Html body tag -fn html_body(with config: BodyConfig) -> Html { - let content = - list.flatten([config.static_content, config.content, config.scripts]) - - h("body", config.attributes, content) -} - -pub type HtmlConfig { - HtmlConfig( - attributes: List(HtmlAttribute), - lang: String, - head: HeadConfig, - body: BodyConfig, - ) -} - -/// Renders an HTML tag and its children -fn html(with config: HtmlConfig) -> Html { - let attributes = [#("lang", config.lang), ..config.attributes] - - h("html", attributes, [head(config.head), html_body(config.body)]) -} - -pub type ScriptConfig { - ScriptConfig(head: List(Html), body: List(Html)) -} - -pub type PageConfig { - PageConfig( - path: String, - title: String, - content: List(Html), - static_content: List(Html), - stylesheets: List(String), - scripts: ScriptConfig, - ) -} - -/// Renders a page -pub fn render_page_html(page config: PageConfig) -> Html { - // add path-specific class to body to make styling easier - let body_class = #("id", "page" <> string.replace(config.path, "/", "-")) - - // render html - html(HtmlConfig( - head: HeadConfig( - description: "A playground for the Gleam programming language. Write, run, and share Gleam code in your browser.", - image: "https://gleam.run/images/og-image.png", - title: "The Gleam Playground", - url: "https://play.gleam.run" <> config.path, - path: config.path, - meta: [], - stylesheets: config.stylesheets, - scripts: [ - html_script( - "https://plausible.io/js/script.js", - ScriptOptions(defer: True, module: False), - [#("data-domain", "play.gleam.run")], - ), - ..config.scripts.head - ], - ), - lang: "en-GB", - attributes: [#("class", "theme-light")], - body: BodyConfig( - attributes: [body_class], - scripts: config.scripts.body, - static_content: config.static_content, - content: config.content, - ), - )) -} - -/// Renders an HTML document in String form from a PageConfig -pub fn render_page(page config: PageConfig) -> String { - config - |> render_page_html - |> htmb.render_page("html") - |> string_builder.to_string -} diff --git a/src/playground/pages.gleam b/src/playground/pages.gleam deleted file mode 100644 index 59001d6..0000000 --- a/src/playground/pages.gleam +++ /dev/null @@ -1,73 +0,0 @@ -import htmb.{type Html, h, text} - -fn output_tab(label: String, id: String, value: String, checked: Bool) -> Html { - let checked = case checked { - True -> "true" - False -> "false" - } - h("label", [#("class", "tab")], [ - h("p", [], [text(label)]), - h( - "input", - [ - #("type", "radio"), - #("id", id), - #("name", "output-display"), - #("value", value), - #("hidden", "true"), - #("checked", checked), - ], - [], - ), - ]) -} - -fn output_container(id: String, class: String) -> Html { - h("aside", [#("id", id), #("class", class)], []) -} - -pub fn home_page() -> Html { - h("article", [#("id", "playground-container")], [ - h("section", [#("id", "playground")], [ - h("div", [#("id", "playground-header")], [ - h( - "input", - [ - #("id", "title-input"), - #("type", "text"), - #("name", "title"), - #("value", "A Gleam Playground project"), - #("placeholder", "Project title"), - ], - [], - ), - h("button", [#("id", "share-button")], [text("Share")]), - ]), - h("div", [#("id", "playground-content")], [ - h("section", [#("id", "editor")], [ - h("div", [#("id", "editor-target")], []), - ]), - h("div", [#("id", "output-container")], [ - h("div", [#("id", "tabs")], [ - output_tab("Output", "output-radio", "output", True), - output_tab( - "Compiled Erlang", - "compiled-erlang-radio", - "erlang", - False, - ), - output_tab( - "Compiled JavaScript", - "compiled-javascript-radio", - "javascript", - False, - ), - ]), - output_container("output", "output"), - output_container("compiled-erlang", "output language-erlang"), - output_container("compiled-javascript", "output language-javascript"), - ]), - ]), - ]), - ]) -} diff --git a/src/playground/widgets.gleam b/src/playground/widgets.gleam index 15c71b7..030340d 100644 --- a/src/playground/widgets.gleam +++ b/src/playground/widgets.gleam @@ -1,4 +1,3 @@ -import gleam/list import htmb.{type Html, h, text} pub fn icon_moon() -> Html { @@ -185,11 +184,7 @@ pub fn text_link( } /// Renders the playground's navbar as html -pub fn navbar(titled title: String, links links: List(Link)) -> Html { - let links = list.map(links, fn(l) { text_link(l, []) }) - - let nav_right_items = list.flatten([links, [theme_picker()]]) - +pub fn navbar() -> Html { h("nav", [#("class", "navbar")], [ anchor("/", [#("class", "logo")], [ h( @@ -200,8 +195,44 @@ pub fn navbar(titled title: String, links links: List(Link)) -> Html { ], [], ), - text(title), + text("Gleam Playground"), + ]), + h("div", [#("class", "nav-right")], [ + anchor("https://gleam.run", [#("class", "link")], [text("gleam.run")]), + theme_picker(), ]), - h("div", [#("class", "nav-right")], nav_right_items), ]) } + +/// Renders a tab for the output display +pub fn output_tab( + label: String, + id: String, + value: String, + checked: Bool, +) -> Html { + let checked = case checked { + True -> "true" + False -> "false" + } + h("label", [#("class", "tab")], [ + h("p", [], [text(label)]), + h( + "input", + [ + #("type", "radio"), + #("id", id), + #("name", "output-display"), + #("value", value), + #("hidden", "true"), + #("checked", checked), + ], + [], + ), + ]) +} + +/// Renders a container for containing some output +pub fn output_container(id: String, class: String) -> Html { + h("aside", [#("id", id), #("class", class)], []) +} diff --git a/static/css/pages/playground.css b/static/css/pages/playground.css index b342c4c..a2a4d47 100644 --- a/static/css/pages/playground.css +++ b/static/css/pages/playground.css @@ -1,4 +1,4 @@ -.output>*, +.output > *, #editor .codeflask__flatten { padding: var(--gap); } @@ -24,32 +24,6 @@ min-height: fit-content; } -#playground-header { - display: flex; - justify-content: space-between; - gap: var(--gap); - align-items: center; - padding: var(--gap); - border-bottom: 1px solid var(--color-divider); -} - -#title-input { - font-size: 1.5rem; - font-weight: bold; - border: none; - border-bottom: 1px solid transparent; - background: none; - color: var(--color-text); - width: 100%; -} - -#title-input:focus, -#title-input:active, -#title-input:hover { - outline: none; - border-bottom: 1px solid var(--color-divider); -} - #tabs { display: flex; gap: var(--gap); @@ -70,7 +44,7 @@ border-bottom: 2px solid var(--color-accent-muted); } -.tab>p { +.tab > p { margin: 0; } @@ -97,35 +71,39 @@ overflow: auto; } -.output>* { +.output > * { margin: 0; white-space: pre-wrap; } -.output>pre { +.output > pre { background: none !important; } -#output-container:has(#output-radio:checked)>#output { +#output-container:has(#output-radio:checked) > #output { display: block; } -#output-container:has(#compiled-javascript-radio:checked)>#compiled-javascript { +#output-container:has(#compiled-javascript-radio:checked) + > #compiled-javascript { display: block; } -#output-container:has(#compiled-erlang-radio:checked)>#compiled-erlang { +#output-container:has(#compiled-erlang-radio:checked) > #compiled-erlang { display: block; } #share-button { - padding: var(--gap); + padding: var(--gap-half) var(--gap); border: none; - color: var(--color-text-muted); - background-color: var(--color-background-dim); + color: var(--color-text); + background-color: var(--color-background); border-radius: 0.5rem; font-family: var(--font-family-normal); cursor: pointer; + align-self: center; + margin-left: auto; + margin-right: var(--gap); } /* Larger than mobile */ diff --git a/static/index.js b/static/index.js index 2bcc50a..43c58aa 100644 --- a/static/index.js +++ b/static/index.js @@ -70,7 +70,7 @@ function highlightOutput(target, childClassName) { console.warn = () => { }; target.querySelectorAll(`.${childClassName}`).forEach((element) => { hljs.highlightElement(element); - }) + }); console.warn = warn; } @@ -139,9 +139,6 @@ worker.onmessage = (event) => { editor.onUpdate(debounce((code) => sendToWorker(code), 200)); -// Title and hash -const titleInput = document.querySelector("#title-input"); - /** * Hashed object format: * { @@ -150,10 +147,12 @@ const titleInput = document.querySelector("#title-input"); * } */ function makeV1Hash(code) { - return lz.compressToBase64(JSON.stringify({ - version: 1, - content: code, - })); + return lz.compressToBase64( + JSON.stringify({ + version: 1, + content: code, + }), + ); } function parseV1Hash(obj) { @@ -177,15 +176,9 @@ function parseHash(hash) { case 1: return parseV1Hash(obj); } - return null + return null; } - -// Get the title from the query string if it exists, -// otherwise use the title input value (so we can set the default in the HTML) -titleInput.value = new URLSearchParams(window.location.search).get("title") || titleInput.value; -document.title = `${titleInput.value} - The Gleam Playground`; - if (window.location.hash) { const hash = window.location.hash.slice(1); const code = parseHash(hash); @@ -199,11 +192,12 @@ const shareButton = document.querySelector("#share-button"); function share() { const code = editor.getCode(); const compressed = makeV1Hash(code); - const url = `${window.location.origin}${window.location.pathname}?title=${encodeURIComponent(titleInput.value)}#${compressed}`; + const url = `${window.location.origin}${window.location.pathname}#${compressed}`; navigator.clipboard.writeText(url); - shareButton.textContent = "Copied!"; + const before = shareButton.textContent; + shareButton.textContent = "Link copied!"; setTimeout(() => { - shareButton.textContent = "Share"; + shareButton.textContent = before; }, 1000); } shareButton.addEventListener("click", share);