├── .github └── workflows │ ├── bundle.yaml │ ├── tests.yaml │ └── www.yaml ├── .gitignore ├── LICENSE ├── README.md ├── ci └── all_tests.sh ├── docs.sh ├── examples ├── csv-movies.roc ├── letters.roc ├── markdown.roc ├── numbers.roc └── xml-svg.roc ├── flake.lock ├── flake.nix ├── package ├── CSV.roc ├── HTTP.roc ├── Markdown.roc ├── Parser.roc ├── String.roc ├── Xml.roc └── main.roc └── www ├── 0.10.0 ├── CSV │ └── index.html ├── HTTP │ └── index.html ├── Markdown │ └── index.html ├── Parser │ └── index.html ├── String │ └── index.html ├── Xml │ └── index.html ├── index.html ├── llms.txt ├── search.js └── styles.css └── index.html /.github/workflows/bundle.yaml: -------------------------------------------------------------------------------- 1 | name: Bundle 2 | 3 | on: 4 | # Run when a release is published 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | bundle: 11 | name: Bundle 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | steps: 16 | - name: Check out the repository 17 | uses: actions/checkout@v3 18 | - name: Install Roc 19 | uses: hasnep/setup-roc@main 20 | with: 21 | roc-version: nightly 22 | - name: Bundle and release the library 23 | uses: hasnep/bundle-roc-library@v0.1.0 24 | with: 25 | library: package/main.roc 26 | token: ${{ github.token }} -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | workflow_dispatch: 4 | 5 | # this cancels workflows currently in progress if you start a new one 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | 12 | test-examples: 13 | runs-on: [ubuntu-20.04] 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - id: try_fetching_testing_release 18 | continue-on-error: true 19 | run: | 20 | curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-TESTING.tar.gz 21 | 22 | - name: There are no TESTING releases, checking regular releases instead 23 | if: steps.try_fetching_testing_release.outcome == 'failure' 24 | run: | 25 | curl -fOL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_x86_64-latest.tar.gz 26 | 27 | - name: rename nightly tar 28 | run: mv $(ls | grep "roc_nightly.*tar\.gz") roc_nightly.tar.gz 29 | 30 | - name: decompress the tar 31 | run: tar -xzf roc_nightly.tar.gz 32 | 33 | - run: rm roc_nightly.tar.gz 34 | 35 | - name: simplify nightly folder name 36 | run: mv roc_nightly* roc_nightly 37 | 38 | # Check roc cli available 39 | - run: ./roc_nightly/roc version 40 | 41 | # Run all tests 42 | - run: ROC=./roc_nightly/roc ./ci/all_tests.sh 43 | -------------------------------------------------------------------------------- /.github/workflows/www.yaml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy www/ to GH Pages 3 | 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 15 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: false 19 | 20 | jobs: 21 | deploy: 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Pages 30 | uses: actions/configure-pages@v5 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v3 33 | with: 34 | # Upload docs/ folder 35 | path: "./www" 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/letters 2 | examples/numbers 3 | examples/csv-movies 4 | examples/markdown 5 | examples/xml-svg 6 | 7 | generated-docs/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Luke Boswell and subsequent authors 2 | 3 | The Universal Permissive License (UPL), Version 1.0 4 | 5 | Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the "Software"), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both 6 | 7 | (a) the Software, and 8 | 9 | (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a “Larger Work” to which the Software is contributed by such licensors), 10 | 11 | without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. 12 | 13 | This license is subject to the following condition: 14 | 15 | The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Parser for Roc 2 | 3 | A simple [Parser Combinator](https://en.wikipedia.org/wiki/Parser_combinator) package for Roc. 4 | 5 | ```roc 6 | color : Parser Utf8 [Red, Green, Blue] 7 | color = 8 | one_of( 9 | [ 10 | const(Red) |> skip(string("red")), 11 | const(Green) |> skip(string("green")), 12 | const(Blue) |> skip(string("blue")), 13 | ], 14 | ) 15 | 16 | expect parse_str(color, "green") == Ok(Green) 17 | ``` 18 | 19 | Includes modules to parse the following (with various levels of maturity); 20 | - Utf-8 Strings 21 | - CSV 22 | - XML 23 | - Markdown 24 | - HTTP 25 | 26 | ## Documentation 27 | 28 | See [lukewilliamboswell.github.io/roc-parser/](https://lukewilliamboswell.github.io/roc-parser/) 29 | 30 | Locally generate docs using `roc docs package/main.roc` 31 | 32 | ## Contributing 33 | 34 | If you see anything that could be improved please create an Issue or Pull Request. 35 | 36 | ## Tests 37 | 38 | Run tests locally with `roc test package/main.roc` 39 | 40 | ## Packaging 41 | 42 | Bundle package into a URL for distribution using `roc build --bundle .tar.br package/main.roc` 43 | -------------------------------------------------------------------------------- /ci/all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 4 | set -euxo pipefail 5 | 6 | if [ -z "${ROC:-}" ]; then 7 | echo "INFO: The ROC environment variable is not set, using the default roc command." 8 | export ROC=$(which roc) 9 | fi 10 | 11 | EXAMPLES_DIR='./examples' 12 | PACKAGE_DIR='./package' 13 | 14 | echo "test the package" 15 | $ROC test package/Parser.roc 16 | $ROC test package/CSV.roc 17 | $ROC test package/HTTP.roc 18 | $ROC test package/Markdown.roc 19 | $ROC test package/String.roc 20 | $ROC test package/Xml.roc 21 | 22 | echo "roc check the examples" 23 | for ROC_FILE in $EXAMPLES_DIR/*.roc; do 24 | $ROC check $ROC_FILE 25 | done 26 | 27 | echo "roc build the examples" 28 | for ROC_FILE in $EXAMPLES_DIR/*.roc; do 29 | $ROC build $ROC_FILE --linker=legacy 30 | done 31 | 32 | echo "test building docs website" 33 | $ROC docs $PACKAGE_DIR/main.roc 34 | -------------------------------------------------------------------------------- /docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ 4 | set -euxo pipefail 5 | 6 | # Function to validate version number format (x.y.z) 7 | validate_version() { 8 | if [[ ! $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 9 | echo "Error: Version number must be in format x.y.z (e.g., 0.12.0)" 10 | exit 1 11 | fi 12 | } 13 | 14 | # Check if version argument is provided 15 | if [ $# -ne 1 ]; then 16 | echo "Usage: $0 " 17 | echo "Example: $0 0.12.0" 18 | exit 1 19 | fi 20 | 21 | VERSION=$1 22 | 23 | # Validate version number 24 | validate_version "$VERSION" 25 | 26 | # Run roc docs with validated version 27 | roc docs --root-dir "/roc-parser/$VERSION/" package/main.roc 28 | 29 | # Create new version directory in www/ 30 | mkdir www/$VERSION 31 | 32 | # Move generated docs to version directory 33 | mv generated-docs/* www/$VERSION 34 | -------------------------------------------------------------------------------- /examples/csv-movies.roc: -------------------------------------------------------------------------------- 1 | app [main!] { 2 | cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", 3 | parser: "../package/main.roc", 4 | } 5 | 6 | import parser.Parser as P 7 | import parser.CSV 8 | import parser.String 9 | import cli.Stdout 10 | import cli.Stderr 11 | 12 | MovieInfo := { title : Str, release_year : U64, actors : List Str } 13 | 14 | input : Str 15 | input = 16 | """ 17 | Airplane!,1980,\"Robert Hays,Julie Hagerty\" 18 | Caddyshack,1980,\"Chevy Chase,Rodney Dangerfield,Ted Knight,Michael O'Keefe,Bill Murray\" 19 | """ 20 | 21 | main! = |_args| 22 | when CSV.parse_str(movie_info_parser, input) is 23 | Ok(movies) -> 24 | movies_string = 25 | movies 26 | |> List.map(movie_info_explanation) 27 | |> Str.join_with("\n") 28 | 29 | n_movies = List.len(movies) |> Num.to_str 30 | 31 | Stdout.line!("${n_movies} movies were found:\n\n${movies_string}\n\nParse success!\n") 32 | 33 | Err(problem) -> 34 | when problem is 35 | ParsingFailure(failure) -> 36 | Stderr.line!("Parsing failure: ${failure}\n") 37 | 38 | ParsingIncomplete(leftover) -> 39 | leftover_str = leftover |> List.map(String.str_from_utf8) |> List.map(|val| "\"${val}\"") |> Str.join_with(", ") 40 | 41 | Stderr.line!("Parsing incomplete. Following leftover fields while parsing a record: ${leftover_str}\n") 42 | 43 | SyntaxError(error) -> 44 | Stderr.line!("Parsing failure. Syntax error in the CSV: ${error}") 45 | 46 | movie_info_parser = 47 | CSV.record(|title| |release_year| |actors| @MovieInfo({ title, release_year, actors })) 48 | |> P.keep(CSV.field(CSV.string)) 49 | |> P.keep(CSV.field(CSV.u64)) 50 | |> P.keep(CSV.field(actors_parser)) 51 | 52 | actors_parser = CSV.string |> P.map(|val| Str.split_on(val, ",")) 53 | 54 | movie_info_explanation = |@MovieInfo({ title, release_year, actors })| 55 | enumerated_actors = enumerate(actors) 56 | release_year_str = Num.to_str(release_year) 57 | 58 | "The movie '${title}' was released in ${release_year_str} and stars ${enumerated_actors}" 59 | 60 | enumerate : List Str -> Str 61 | enumerate = |elements| 62 | { before: inits, others: last } = List.split_at(elements, (List.len(elements) - 1)) 63 | 64 | last 65 | |> List.prepend((inits |> Str.join_with(", "))) 66 | |> Str.join_with(" and ") 67 | -------------------------------------------------------------------------------- /examples/letters.roc: -------------------------------------------------------------------------------- 1 | app [main!] { 2 | cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", 3 | parser: "../package/main.roc", 4 | } 5 | 6 | import cli.Stdout 7 | import cli.Stderr 8 | import parser.Parser 9 | import parser.String 10 | 11 | main! = |_args| 12 | 13 | result : Result (List Letter) [ParsingFailure Str, ParsingIncomplete Str] 14 | result = String.parse_str(Parser.many(letter_parser), "AAAiBByAABBwBtCCCiAyArBBx") 15 | 16 | when result |> Result.map_ok(count_letter_as) is 17 | Ok(count) -> Stdout.line!("I counted ${Num.to_str(count)} letter A's!") 18 | Err(_) -> Stderr.line!("Failed while parsing input") 19 | 20 | Letter : [A, B, C, Other] 21 | 22 | # Helper to check if a letter is an A tag 23 | is_a = |l| 24 | l == A 25 | 26 | # Count the number of Letter A's 27 | count_letter_as : List Letter -> U64 28 | count_letter_as = |letters| 29 | letters 30 | |> List.keep_if(is_a) 31 | |> List.map(|_| 1) 32 | |> List.sum 33 | 34 | # Build a custom parser to convert utf8 input into Letter tags 35 | letter_parser : Parser.Parser (List U8) Letter 36 | letter_parser = Parser.build_primitive_parser( 37 | |input| 38 | val_result = 39 | when input is 40 | [] -> Err(ParsingFailure("Nothing to parse")) 41 | ['A', ..] -> Ok(A) 42 | ['B', ..] -> Ok(B) 43 | ['C', ..] -> Ok(C) 44 | _ -> Ok(Other) 45 | 46 | val_result 47 | |> Result.map_ok(|val| { val, input: List.drop_first(input, 1) }), 48 | ) 49 | 50 | # Test we can parse a single B letter 51 | expect 52 | input = "B" 53 | parser = letter_parser 54 | result = String.parse_str(parser, input) 55 | result == Ok(B) 56 | 57 | # Test we can parse a number of different letters 58 | expect 59 | input = "BCXA" 60 | parser = Parser.many(letter_parser) 61 | result = String.parse_str(parser, input) 62 | result == Ok([B, C, Other, A]) 63 | -------------------------------------------------------------------------------- /examples/markdown.roc: -------------------------------------------------------------------------------- 1 | app [main!] { 2 | cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", 3 | parser: "../package/main.roc", 4 | } 5 | 6 | import cli.Stdout 7 | import parser.String 8 | import parser.Markdown 9 | 10 | content : Str 11 | content = 12 | """ 13 | # Title 14 | 15 | This is some text 16 | 17 | [roc website](https://roc-lang.org) 18 | 19 | ## Sub-title 20 | 21 | ```roc 22 | # some code 23 | foo = bar 24 | ``` 25 | """ 26 | 27 | main! = |_args| 28 | String.parse_str(Markdown.all, content) 29 | |> Result.map_ok(|nodes| render_content(nodes, "")) 30 | |> Result.with_default("PARSING ERROR") 31 | |> Stdout.line! 32 | 33 | render_content : List Markdown.Markdown, Str -> Str 34 | render_content = |nodes, buf| 35 | when nodes is 36 | [] -> 37 | buf # base case 38 | 39 | [Heading(level, str), .. as rest] -> 40 | render_content(rest, Str.concat(buf, "HEADING: ${Inspect.to_str(level)} ${str}\n")) 41 | 42 | [Link({ alt, href }), .. as rest] -> 43 | render_content(rest, Str.concat(buf, "LINK: ${Inspect.to_str({ alt, href })}\n")) 44 | 45 | [Image({ alt, href }), .. as rest] -> 46 | render_content(rest, Str.concat(buf, "IMAGE: ${Inspect.to_str({ alt, href })}\n")) 47 | 48 | [Code({ ext, pre }), .. as rest] -> 49 | render_content(rest, Str.concat(buf, "CODE: ${Inspect.to_str({ ext, pre })}\n")) 50 | 51 | [TODO(line), .. as rest] -> 52 | render_content(rest, Str.concat(buf, "TODO: ${line}\n")) 53 | -------------------------------------------------------------------------------- /examples/numbers.roc: -------------------------------------------------------------------------------- 1 | app [main!] { 2 | cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", 3 | parser: "../package/main.roc", 4 | } 5 | 6 | import cli.Stdout 7 | import cli.Stderr 8 | import parser.Parser 9 | import parser.String 10 | 11 | main! = |_args| 12 | 13 | result : Result (List (List U64)) [ParsingFailure Str, ParsingIncomplete Str] 14 | result = String.parse_str(Parser.many(multiple_numbers), "1000\n2000\n3000\n\n4000\n\n5000\n6000\n\n") 15 | 16 | when result |> Result.map_ok(largest) is 17 | Ok(count) -> Stdout.line!("The lagest sum is ${Num.to_str(count)}") 18 | Err(_) -> Stderr.line!("Failed while parsing input") 19 | 20 | # Parse a number followed by a newline 21 | single_number : Parser.Parser (List U8) U64 22 | single_number = 23 | Parser.const(|n| n) 24 | |> Parser.keep(String.digits) 25 | |> Parser.skip(String.string("\n")) 26 | 27 | expect 28 | actual = String.parse_str(single_number, "1000\n") 29 | actual == Ok(1000) 30 | 31 | # Parse a series of numbers followed by a newline 32 | multiple_numbers : Parser.Parser (List U8) (List U64) 33 | multiple_numbers = 34 | Parser.const(|ns| ns) 35 | |> Parser.keep(Parser.many(single_number)) 36 | |> Parser.skip(String.string("\n")) 37 | 38 | expect 39 | actual = String.parse_str(multiple_numbers, "1000\n2000\n3000\n\n") 40 | actual == Ok([1000, 2000, 3000]) 41 | 42 | # Sum up the lists and return the largest sum 43 | largest : List (List U64) -> U64 44 | largest = |numbers| 45 | numbers 46 | |> List.map(List.sum) 47 | |> List.sort_desc 48 | |> List.first 49 | |> Result.with_default(0) 50 | 51 | expect largest([[1000, 2000, 3000], [4000], [5000, 6000]]) == 11_000 52 | -------------------------------------------------------------------------------- /examples/xml-svg.roc: -------------------------------------------------------------------------------- 1 | app [main!] { 2 | cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", 3 | # TODO replace with release URL after https://github.com/Hasnep/roc-html/pull/20 is merged 4 | # and a new release is published 5 | html: "https://github.com/lukewilliamboswell/roc-html/releases/download/testing0/cFRUcxD_hiFxkjG21muA4gIrPd9wePNkHY2FQoElXW4.tar.br", 6 | parser: "../package/main.roc", 7 | } 8 | 9 | import cli.Stdout 10 | import parser.String 11 | import parser.Xml 12 | import html.Html 13 | import html.Attribute 14 | 15 | svg_input = 16 | """ 17 | 18 | """ 19 | 20 | expected_html = 21 | """ 22 | svg [ 23 | xmlns "http://www.w3.org/2000/svg", 24 | width "16", 25 | height "16", 26 | fill "currentColor", 27 | class "bi bi-sort-up", 28 | viewBox "0 0 16 16" 29 | ] [ 30 | path [ 31 | d "M3.5 12.5a.5.5 0 0 1-1 0V3.707L1.354 4.854a.5.5 0 1 1-.708-.708l2-1.999.007-.007a.5.5 0 0 1 .7.006l2 2a.5.5 0 1 1-.707.708L3.5 3.707zm3.5-9a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z" 32 | ] [] 33 | ] 34 | """ 35 | 36 | main! = |_args| 37 | 38 | svg_converted_to_html = 39 | Result.map_ok( 40 | String.parse_str(Xml.xml_parser, svg_input), 41 | |xml| html_to_roc_dsl(svg_to_html(xml.root), "", 0), 42 | )? 43 | 44 | if svg_converted_to_html == expected_html then 45 | Stdout.line!("Successfully converted SVG into HTML DSL") 46 | else 47 | Stdout.line!("Did not match expected HTML DSL") 48 | 49 | svg_to_html : Xml.Node -> Html.Node 50 | svg_to_html = |xml| 51 | when xml is 52 | Element(name, attrs, children) -> 53 | (Html.element(name))( 54 | List.map(attrs, xml_to_html_attribute), 55 | List.map(children, svg_to_html), 56 | ) 57 | 58 | Text(text) -> Html.text(text) 59 | 60 | xml_to_html_attribute : { name : Str, value : Str } -> Attribute.Attribute 61 | xml_to_html_attribute = |{ name, value }| (Attribute.attribute(name))(value) 62 | 63 | html_to_roc_dsl : Html.Node, Str, U8 -> Str 64 | html_to_roc_dsl = |html, buf, depth| 65 | 66 | map_child = |child| html_to_roc_dsl(child, " ${depth_to_ident(depth)}", (depth + 1)) 67 | map_attr = |Attribute(name, value)| " ${depth_to_ident(depth)}${name} \"${value}\"" 68 | 69 | when html is 70 | Element(name, _, attrs, children) -> 71 | formatted_attrs = 72 | if List.is_empty(attrs) then "[]" else "[\n${List.map(attrs, map_attr) |> Str.join_with(",\n")}\n${depth_to_ident(depth)}]" 73 | 74 | formatted_children = 75 | if List.is_empty(children) then "[]" else "[\n${List.map(children, map_child) |> Str.join_with(",\n")}\n${depth_to_ident(depth)}]" 76 | 77 | "${buf}${name} ${formatted_attrs} ${formatted_children}" 78 | 79 | Text(text) -> "${buf}text \"${text}\"" 80 | UnescapedHtml(_raw) -> crash("UnescapedHtml not supported") 81 | 82 | expect 83 | a = html_to_roc_dsl(Html.text("foo"), "", 0) 84 | a == "text \"foo\"" 85 | 86 | expect 87 | a = html_to_roc_dsl(Html.h1([Attribute.class("green"), Attribute.width("1rem")], [Html.text("foo")]), "", 0) 88 | a 89 | == 90 | """ 91 | h1 [ 92 | class \"green\", 93 | width \"1rem\" 94 | ] [ 95 | text \"foo\" 96 | ] 97 | """ 98 | 99 | expect 100 | a = html_to_roc_dsl(Html.h1([], [Html.text("foo"), Html.text("bar"), Html.text("baz")]), "", 0) 101 | a 102 | == 103 | """ 104 | h1 [] [ 105 | text \"foo\", 106 | text \"bar\", 107 | text \"baz\" 108 | ] 109 | """ 110 | 111 | expect 112 | a = html_to_roc_dsl(Html.h1([], [Html.h2([Attribute.class("green")], [Html.text("foo")]), Html.text("bar")]), "", 0) 113 | a 114 | == 115 | """ 116 | h1 [] [ 117 | h2 [ 118 | class \"green\" 119 | ] [ 120 | text \"foo\" 121 | ], 122 | text \"bar\" 123 | ] 124 | """ 125 | 126 | depth_to_ident = |depth| 127 | List.range({ start: At(0), end: Before(depth) }) 128 | |> List.map(|_| " ") 129 | |> Str.join_with("") 130 | 131 | expect depth_to_ident(0) == "" 132 | expect depth_to_ident(1) == " " 133 | expect depth_to_ident(2) == " " 134 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1733328505, 7 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils_2": { 38 | "inputs": { 39 | "systems": "systems_2" 40 | }, 41 | "locked": { 42 | "lastModified": 1731533236, 43 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "nixgl": { 56 | "inputs": { 57 | "flake-utils": [ 58 | "roc", 59 | "flake-utils" 60 | ], 61 | "nixpkgs": [ 62 | "roc", 63 | "nixpkgs" 64 | ] 65 | }, 66 | "locked": { 67 | "lastModified": 1713543440, 68 | "narHash": "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=", 69 | "owner": "guibou", 70 | "repo": "nixGL", 71 | "rev": "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "owner": "guibou", 76 | "repo": "nixGL", 77 | "type": "github" 78 | } 79 | }, 80 | "nixpkgs": { 81 | "locked": { 82 | "lastModified": 1722403750, 83 | "narHash": "sha256-tRmn6UiFAPX0m9G1AVcEPjWEOc9BtGsxGcs7Bz3MpsM=", 84 | "owner": "nixos", 85 | "repo": "nixpkgs", 86 | "rev": "184957277e885c06a505db112b35dfbec7c60494", 87 | "type": "github" 88 | }, 89 | "original": { 90 | "owner": "nixos", 91 | "repo": "nixpkgs", 92 | "rev": "184957277e885c06a505db112b35dfbec7c60494", 93 | "type": "github" 94 | } 95 | }, 96 | "roc": { 97 | "inputs": { 98 | "flake-compat": "flake-compat", 99 | "flake-utils": "flake-utils_2", 100 | "nixgl": "nixgl", 101 | "nixpkgs": "nixpkgs", 102 | "rust-overlay": "rust-overlay" 103 | }, 104 | "locked": { 105 | "lastModified": 1738058275, 106 | "narHash": "sha256-k3iYD2UNGPkbGePM45mWMGwp1N4uLuPWw0P/tDThLO4=", 107 | "owner": "roc-lang", 108 | "repo": "roc", 109 | "rev": "0e35e33f85342e2c2dda3f8e6e5414719293d759", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "roc-lang", 114 | "repo": "roc", 115 | "type": "github" 116 | } 117 | }, 118 | "root": { 119 | "inputs": { 120 | "flake-utils": "flake-utils", 121 | "nixpkgs": [ 122 | "roc", 123 | "nixpkgs" 124 | ], 125 | "roc": "roc" 126 | } 127 | }, 128 | "rust-overlay": { 129 | "inputs": { 130 | "nixpkgs": [ 131 | "roc", 132 | "nixpkgs" 133 | ] 134 | }, 135 | "locked": { 136 | "lastModified": 1736303309, 137 | "narHash": "sha256-IKrk7RL+Q/2NC6+Ql6dwwCNZI6T6JH2grTdJaVWHF0A=", 138 | "owner": "oxalica", 139 | "repo": "rust-overlay", 140 | "rev": "a0b81d4fa349d9af1765b0f0b4a899c13776f706", 141 | "type": "github" 142 | }, 143 | "original": { 144 | "owner": "oxalica", 145 | "repo": "rust-overlay", 146 | "type": "github" 147 | } 148 | }, 149 | "systems": { 150 | "locked": { 151 | "lastModified": 1681028828, 152 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 153 | "owner": "nix-systems", 154 | "repo": "default", 155 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 156 | "type": "github" 157 | }, 158 | "original": { 159 | "owner": "nix-systems", 160 | "repo": "default", 161 | "type": "github" 162 | } 163 | }, 164 | "systems_2": { 165 | "locked": { 166 | "lastModified": 1681028828, 167 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 168 | "owner": "nix-systems", 169 | "repo": "default", 170 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 171 | "type": "github" 172 | }, 173 | "original": { 174 | "owner": "nix-systems", 175 | "repo": "default", 176 | "type": "github" 177 | } 178 | } 179 | }, 180 | "root": "root", 181 | "version": 7 182 | } 183 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "roc-parser devShell flake"; 3 | 4 | inputs = { 5 | roc.url = "github:roc-lang/roc"; 6 | nixpkgs.follows = "roc/nixpkgs"; 7 | 8 | # to easily make configs for multiple architectures 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = { self, nixpkgs, flake-utils, roc }: 13 | let supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux" ]; 14 | in flake-utils.lib.eachSystem supportedSystems (system: 15 | let 16 | pkgs = import nixpkgs { inherit system; }; 17 | rocPkgs = roc.packages.${system}; 18 | 19 | linuxInputs = with pkgs; 20 | lib.optionals stdenv.isLinux [ 21 | ]; 22 | 23 | darwinInputs = with pkgs; 24 | lib.optionals stdenv.isDarwin 25 | (with pkgs.darwin.apple_sdk.frameworks; [ 26 | ]); 27 | 28 | sharedInputs = (with pkgs; [ 29 | expect 30 | rocPkgs.cli 31 | ]); 32 | in { 33 | 34 | devShell = pkgs.mkShell { 35 | buildInputs = sharedInputs ++ darwinInputs ++ linuxInputs; 36 | }; 37 | 38 | formatter = pkgs.nixpkgs-fmt; 39 | }); 40 | } -------------------------------------------------------------------------------- /package/CSV.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | CSV, 3 | CSVRecord, 4 | file, 5 | record, 6 | parse_str, 7 | parse_csv, 8 | parse_str_to_csv_record, 9 | field, 10 | string, 11 | u64, 12 | f64, 13 | ] 14 | 15 | import Parser exposing [Parser] 16 | import String 17 | 18 | ## This is a CSV parser which follows RFC4180 19 | ## 20 | ## For simplicity's sake, the following things are not yet supported: 21 | ## - CSV files with headings 22 | ## 23 | ## The following however *is* supported 24 | ## - A simple LF ("\n") instead of CRLF ("\r\n") to separate records. 25 | CSV : List CSVRecord 26 | CSVRecord : List CSVField 27 | CSVField : String.Utf8 28 | 29 | ## Attempts to Parser.parse an `a` from a `Str` that is encoded in CSV format. 30 | parse_str : Parser CSVRecord a, Str -> Result (List a) [ParsingFailure Str, SyntaxError Str, ParsingIncomplete CSVRecord] 31 | parse_str = |csv_parser, input| 32 | when parse_str_to_csv(input) is 33 | Err(ParsingIncomplete(rest)) -> 34 | rest_str = String.str_from_utf8(rest) 35 | 36 | Err(SyntaxError(rest_str)) 37 | 38 | Err(ParsingFailure(str)) -> 39 | Err(ParsingFailure(str)) 40 | 41 | Ok(csv_data) -> 42 | when parse_csv(csv_parser, csv_data) is 43 | Err(ParsingFailure(str)) -> 44 | Err(ParsingFailure(str)) 45 | 46 | Err(ParsingIncomplete(problem)) -> 47 | Err(ParsingIncomplete(problem)) 48 | 49 | Ok(vals) -> 50 | Ok(vals) 51 | 52 | ## Attempts to Parser.parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields). 53 | parse_csv : Parser CSVRecord a, CSV -> Result (List a) [ParsingFailure Str, ParsingIncomplete CSVRecord] 54 | parse_csv = |csv_parser, csv_data| 55 | csv_data 56 | |> List.map_with_index(|record_fields_list, index| { record: record_fields_list, index: index }) 57 | |> List.walk_until( 58 | Ok([]), 59 | |state, { record: record_fields_list, index: index }| 60 | when parse_csv_record(csv_parser, record_fields_list) is 61 | Err(ParsingFailure(problem)) -> 62 | index_str = Num.to_str((index + 1)) 63 | record_str = record_fields_list |> List.map(String.str_from_utf8) |> List.map(|val| "\"${val}\"") |> Str.join_with(", ") 64 | problem_str = "${problem}\nWhile parsing record no. ${index_str}: `${record_str}`" 65 | 66 | Break(Err(ParsingFailure(problem_str))) 67 | 68 | Err(ParsingIncomplete(problem)) -> 69 | Break(Err(ParsingIncomplete(problem))) 70 | 71 | Ok(val) -> 72 | state 73 | |> Result.map_ok(|vals| List.append(vals, val)) 74 | |> Continue, 75 | ) 76 | 77 | ## Attempts to Parser.parse an `a` from a `CSVRecord` datastructure (a list of bytestring-fields) 78 | ## 79 | ## This parser succeeds when all fields of the CSVRecord are consumed by the parser. 80 | parse_csv_record : Parser CSVRecord a, CSVRecord -> Result a [ParsingFailure Str, ParsingIncomplete CSVRecord] 81 | parse_csv_record = |csv_parser, record_fields_list| 82 | Parser.parse(csv_parser, record_fields_list, |leftover| leftover == []) 83 | 84 | ## Wrapper function to combine a set of fields into your desired `a` 85 | ## 86 | ## ```roc 87 | ## record(|first_name| |last_name| |age| User({ first_name, last_name, age })) 88 | ## |> field(string) 89 | ## |> field(string) 90 | ## |> field(u64) 91 | ## ``` 92 | record : a -> Parser CSVRecord a 93 | record = |f| 94 | Parser.const(f) 95 | 96 | ## Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`. 97 | field : Parser String.Utf8 a -> Parser CSVRecord a 98 | field = |field_parser| 99 | Parser.build_primitive_parser( 100 | |fields_list| 101 | when List.get(fields_list, 0) is 102 | Err(OutOfBounds) -> 103 | Err(ParsingFailure("expected another CSV field but there are no more fields in this record")) 104 | 105 | Ok(raw_str) -> 106 | when String.parse_utf8(field_parser, raw_str) is 107 | Ok(val) -> 108 | Ok({ val: val, input: List.drop_first(fields_list, 1) }) 109 | 110 | Err(ParsingFailure(reason)) -> 111 | field_str = raw_str |> String.str_from_utf8 112 | 113 | Err(ParsingFailure("Field `${field_str}` could not be parsed. ${reason}")) 114 | 115 | Err(ParsingIncomplete(reason)) -> 116 | reason_str = String.str_from_utf8(reason) 117 | fields_str = fields_list |> List.map(String.str_from_utf8) |> Str.join_with(", ") 118 | 119 | Err(ParsingFailure("The field parser was unable to read the whole field: `${reason_str}` while parsing the first field of leftover ${fields_str})")), 120 | ) 121 | 122 | ## Parser for a field containing a UTF8-encoded string 123 | string : Parser CSVField Str 124 | string = String.any_string 125 | 126 | ## Parse a number from a CSV field 127 | u64 : Parser CSVField U64 128 | u64 = 129 | string 130 | |> Parser.map( 131 | |val| 132 | when Str.to_u64(val) is 133 | Ok(num) -> Ok(num) 134 | Err(_) -> Err("${val} is not a U64."), 135 | ) 136 | |> Parser.flatten 137 | 138 | ## Parse a 64-bit float from a CSV field 139 | f64 : Parser CSVField F64 140 | f64 = 141 | string 142 | |> Parser.map( 143 | |val| 144 | when Str.to_f64(val) is 145 | Ok(num) -> Ok(num) 146 | Err(_) -> Err("${val} is not a F64."), 147 | ) 148 | |> Parser.flatten 149 | 150 | ## Attempts to Parser.parse a Str into the internal `CSV` datastructure (A list of lists of bytestring-fields). 151 | parse_str_to_csv : Str -> Result CSV [ParsingFailure Str, ParsingIncomplete String.Utf8] 152 | parse_str_to_csv = |input| 153 | Parser.parse(file, Str.to_utf8(input), |leftover| leftover == []) 154 | 155 | ## Attempts to Parser.parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields). 156 | parse_str_to_csv_record : Str -> Result CSVRecord [ParsingFailure Str, ParsingIncomplete String.Utf8] 157 | parse_str_to_csv_record = |input| 158 | Parser.parse(csv_record, Str.to_utf8(input), |leftover| leftover == []) 159 | 160 | # The following are parsers to turn strings into CSV structures 161 | file : Parser String.Utf8 CSV 162 | file = Parser.sep_by(csv_record, end_of_line) 163 | 164 | csv_record : Parser String.Utf8 CSVRecord 165 | csv_record = Parser.sep_by1(csv_field, comma) 166 | 167 | csv_field : Parser String.Utf8 CSVField 168 | csv_field = Parser.alt(escaped_csv_field, nonescaped_csv_field) 169 | 170 | escaped_csv_field : Parser String.Utf8 CSVField 171 | escaped_csv_field = Parser.between(escaped_contents, dquote, dquote) 172 | 173 | escaped_contents : Parser String.Utf8 (List U8) 174 | escaped_contents = 175 | String.one_of( 176 | [ 177 | twodquotes |> Parser.map(|_| '"'), 178 | comma, 179 | cr, 180 | lf, 181 | textdata, 182 | ], 183 | ) 184 | |> Parser.many 185 | 186 | twodquotes : Parser String.Utf8 Str 187 | twodquotes = String.string("\"\"") 188 | 189 | nonescaped_csv_field : Parser String.Utf8 CSVField 190 | nonescaped_csv_field = Parser.many(textdata) 191 | 192 | comma = String.codeunit(',') 193 | dquote = String.codeunit('"') 194 | end_of_line = Parser.alt(Parser.ignore(crlf), Parser.ignore(lf)) 195 | cr = String.codeunit('\r') 196 | lf = String.codeunit('\n') 197 | crlf = String.string("\r\n") 198 | textdata = String.codeunit_satisfies(|x| (x >= 32 and x <= 33) or (x >= 35 and x <= 43) or (x >= 45 and x <= 126)) # Any printable char except " (34) and , (44) 199 | -------------------------------------------------------------------------------- /package/HTTP.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | Request, 3 | Response, 4 | request, 5 | response, 6 | ] 7 | 8 | import Parser exposing [Parser] 9 | import String 10 | 11 | # https://www.ietf.org/rfc/rfc2616.txt 12 | Method : [Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch] 13 | 14 | HttpVersion : { major : U8, minor : U8 } 15 | 16 | Request : { 17 | method : Method, 18 | uri : Str, 19 | http_version : HttpVersion, 20 | headers : List [Header Str Str], 21 | body : List U8, 22 | } 23 | 24 | Response : { 25 | http_version : HttpVersion, 26 | status_code : U16, 27 | status : Str, 28 | headers : List [Header Str Str], 29 | body : List U8, 30 | } 31 | 32 | method : Parser String.Utf8 Method 33 | method = 34 | String.one_of( 35 | [ 36 | String.string("OPTIONS") |> Parser.map(|_| Options), 37 | String.string("GET") |> Parser.map(|_| Get), 38 | String.string("POST") |> Parser.map(|_| Post), 39 | String.string("PUT") |> Parser.map(|_| Put), 40 | String.string("DELETE") |> Parser.map(|_| Delete), 41 | String.string("HEAD") |> Parser.map(|_| Head), 42 | String.string("TRACE") |> Parser.map(|_| Trace), 43 | String.string("CONNECT") |> Parser.map(|_| Connect), 44 | String.string("PATCH") |> Parser.map(|_| Patch), 45 | ], 46 | ) 47 | 48 | expect String.parse_str(method, "GET") == Ok(Get) 49 | expect String.parse_str(method, "DELETE") == Ok(Delete) 50 | 51 | # TODO: do we want more structure in the URI, or is Str actually what programs want anyway? 52 | # This is not a full URL! 53 | # Request-URI = "*" | absoluteURI | abs_path | authority 54 | RequestUri : Str 55 | 56 | request_uri : Parser String.Utf8 RequestUri 57 | request_uri = 58 | String.codeunit_satisfies(|c| c != ' ') 59 | |> Parser.one_or_more 60 | |> Parser.map(String.str_from_utf8) 61 | 62 | sp = String.codeunit(' ') 63 | crlf = String.string("\r\n") 64 | 65 | http_version : Parser String.Utf8 HttpVersion 66 | http_version = 67 | Parser.const(|major| |minor| { major, minor }) 68 | |> Parser.skip(String.string("HTTP/")) 69 | |> Parser.keep((String.digits |> Parser.map(Num.to_u8))) 70 | |> Parser.skip(String.codeunit('.')) 71 | |> Parser.keep((String.digits |> Parser.map(Num.to_u8))) 72 | 73 | expect 74 | actual = String.parse_str(http_version, "HTTP/1.1") 75 | expected = Ok({ major: 1, minor: 1 }) 76 | actual == expected 77 | 78 | Header : [Header Str Str] 79 | 80 | string_without_colon : Parser String.Utf8 Str 81 | string_without_colon = 82 | String.codeunit_satisfies(|c| c != ':') 83 | |> Parser.one_or_more 84 | |> Parser.map(String.str_from_utf8) 85 | 86 | string_without_cr : Parser String.Utf8 Str 87 | string_without_cr = 88 | String.codeunit_satisfies(|c| c != '\r') 89 | |> Parser.one_or_more 90 | |> Parser.map(String.str_from_utf8) 91 | 92 | header : Parser String.Utf8 Header 93 | header = 94 | Parser.const(|k| |v| Header(k, v)) 95 | |> Parser.keep(string_without_colon) 96 | |> Parser.skip(String.string(": ")) 97 | |> Parser.keep(string_without_cr) 98 | |> Parser.skip(crlf) 99 | 100 | expect 101 | actual = String.parse_str(header, "Accept-Encoding: gzip, deflate\r\n") 102 | expected = Ok(Header("Accept-Encoding", "gzip, deflate")) 103 | actual == expected 104 | 105 | request : Parser String.Utf8 Request 106 | request = 107 | Parser.const(|m| |u| |hv| |hs| |b| { method: m, uri: u, http_version: hv, headers: hs, body: b }) 108 | |> Parser.keep(method) 109 | |> Parser.skip(sp) 110 | |> Parser.keep(request_uri) 111 | |> Parser.skip(sp) 112 | |> Parser.keep(http_version) 113 | |> Parser.skip(crlf) 114 | |> Parser.keep(Parser.many(header)) 115 | |> Parser.skip(crlf) 116 | |> Parser.keep(String.any_thing) 117 | 118 | expect 119 | request_text = 120 | """ 121 | GET /things?id=1 HTTP/1.1\r 122 | Host: bar.example\r 123 | Accept-Encoding: gzip, deflate\r 124 | \r 125 | Hello, world! 126 | """ 127 | actual = 128 | String.parse_str(request, request_text) 129 | 130 | expected : Result Request [ParsingFailure Str, ParsingIncomplete Str] 131 | expected = Ok( 132 | { 133 | method: Get, 134 | uri: "/things?id=1", 135 | http_version: { major: 1, minor: 1 }, 136 | headers: [ 137 | Header("Host", "bar.example"), 138 | Header("Accept-Encoding", "gzip, deflate"), 139 | ], 140 | body: "Hello, world!" |> Str.to_utf8, 141 | }, 142 | ) 143 | actual == expected 144 | 145 | expect 146 | request_text = 147 | """ 148 | OPTIONS /resources/post-here/ HTTP/1.1\r 149 | Host: bar.example\r 150 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r 151 | Accept-Language: en-us,en;q=0.5\r 152 | Accept-Encoding: gzip,deflate\r 153 | Connection: Parser.keep-alive\r 154 | Origin: https://foo.example\r 155 | Access-Control-Request-Method: POST\r 156 | Access-Control-Request-Headers: X-PINGOTHER, Content-Type\r 157 | \r\n 158 | """ 159 | actual = 160 | String.parse_str(request, request_text) 161 | expected = Ok( 162 | { 163 | method: Options, 164 | uri: "/resources/post-here/", 165 | http_version: { major: 1, minor: 1 }, 166 | headers: [ 167 | Header("Host", "bar.example"), 168 | Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), 169 | Header("Accept-Language", "en-us,en;q=0.5"), 170 | Header("Accept-Encoding", "gzip,deflate"), 171 | Header("Connection", "Parser.keep-alive"), 172 | Header("Origin", "https://foo.example"), 173 | Header("Access-Control-Request-Method", "POST"), 174 | Header("Access-Control-Request-Headers", "X-PINGOTHER, Content-Type"), 175 | ], 176 | body: [], 177 | }, 178 | ) 179 | actual == expected 180 | 181 | response : Parser String.Utf8 Response 182 | response = 183 | Parser.const(|hv| |sc| |s| |hs| |b| { http_version: hv, status_code: sc, status: s, headers: hs, body: b }) 184 | |> Parser.keep(http_version) 185 | |> Parser.skip(sp) 186 | |> Parser.keep((String.digits |> Parser.map(Num.to_u16))) 187 | |> Parser.skip(sp) 188 | |> Parser.keep(string_without_cr) 189 | |> Parser.skip(crlf) 190 | |> Parser.keep(Parser.many(header)) 191 | |> Parser.skip(crlf) 192 | |> Parser.keep(String.any_thing) 193 | 194 | expect 195 | body = 196 | """ 197 | \r 198 | \r 199 | \r 200 | \r 201 | A simple webpage\r 202 | \r 203 | \r 204 |

Simple HTML webpage

\r 205 |

Hello, world!

\r 206 | \r 207 | \r\n 208 | """ 209 | response_text = 210 | """ 211 | HTTP/1.1 200 OK\r 212 | Content-Type: text/html; charset=utf-8\r 213 | Content-Length: 55743\r 214 | Connection: Parser.keep-alive\r 215 | Cache-Control: s-maxage=300, public, max-age=0\r 216 | Content-Language: en-US\r 217 | Date: Thu, 06 Dec 2018 17:37:18 GMT\r 218 | ETag: "2e77ad1dc6ab0b53a2996dfd4653c1c3"\r 219 | Server: meinheld/0.6.1\r 220 | Strict-Transport-Security: max-age=63072000\r 221 | X-Content-Type-Options: nosniff\r 222 | X-Frame-Options: DENY\r 223 | X-XSS-Protection: 1; mode=block\r 224 | Vary: Accept-Encoding,Cookie\r 225 | Age: 7\r 226 | \r 227 | ${body} 228 | """ 229 | actual = 230 | String.parse_str(response, response_text) 231 | expected = 232 | Ok( 233 | { 234 | http_version: { major: 1, minor: 1 }, 235 | status_code: 200, 236 | status: "OK", 237 | headers: [ 238 | Header("Content-Type", "text/html; charset=utf-8"), 239 | Header("Content-Length", "55743"), 240 | Header("Connection", "Parser.keep-alive"), 241 | Header("Cache-Control", "s-maxage=300, public, max-age=0"), 242 | Header("Content-Language", "en-US"), 243 | Header("Date", "Thu, 06 Dec 2018 17:37:18 GMT"), 244 | Header("ETag", "\"2e77ad1dc6ab0b53a2996dfd4653c1c3\""), 245 | Header("Server", "meinheld/0.6.1"), 246 | Header("Strict-Transport-Security", "max-age=63072000"), 247 | Header("X-Content-Type-Options", "nosniff"), 248 | Header("X-Frame-Options", "DENY"), 249 | Header("X-XSS-Protection", "1; mode=block"), 250 | Header("Vary", "Accept-Encoding,Cookie"), 251 | Header("Age", "7"), 252 | ], 253 | body: Str.to_utf8(body), 254 | }, 255 | ) 256 | actual == expected 257 | -------------------------------------------------------------------------------- /package/Markdown.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | Markdown, 3 | all, 4 | heading, 5 | link, 6 | image, 7 | code, 8 | ] 9 | 10 | import Parser exposing [Parser] 11 | import String 12 | 13 | Level : [One, Two, Three, Four, Five, Six] 14 | 15 | ## Content values 16 | Markdown : [ 17 | Heading Level Str, 18 | Link { alt : Str, href : Str }, 19 | Image { alt : Str, href : Str }, 20 | Code { ext : Str, pre : Str }, 21 | TODO Str, 22 | ] 23 | 24 | all : Parser String.Utf8 (List Markdown) 25 | all = 26 | [ 27 | heading, 28 | link, 29 | image, 30 | code, 31 | todo, 32 | ] 33 | |> Parser.one_of 34 | |> Parser.sep_by(end_of_line) 35 | 36 | ## temporyary parser for anything that is not yet supported 37 | ## just parse into a TODO tag for now 38 | todo : Parser String.Utf8 Markdown 39 | todo = 40 | Parser.const(TODO) |> Parser.keep((Parser.chomp_while(not_end_of_line) |> Parser.map(String.str_from_utf8))) 41 | 42 | expect 43 | a = String.parse_str(todo, "Foo Bar") 44 | a == Ok(TODO("Foo Bar")) 45 | 46 | expect 47 | a = String.parse_str(all, "Foo Bar\n\nBaz") 48 | a == Ok([TODO("Foo Bar"), TODO(""), TODO("Baz")]) 49 | 50 | end_of_line = Parser.one_of([String.string("\n"), String.string("\r\n")]) 51 | not_end_of_line = |b| b != '\n' and b != '\r' 52 | 53 | ## Headings 54 | ## 55 | ## ``` 56 | ## expect String.parse_str(heading, "# Foo Bar") == Ok(Heading One "Foo Bar") 57 | ## expect String.parse_str(heading, "Foo Bar\n---") == Ok(Heading Two "Foo Bar") 58 | ## ``` 59 | heading : Parser String.Utf8 Markdown 60 | heading = 61 | Parser.one_of( 62 | [ 63 | inline_heading, 64 | two_line_heading_level_one, 65 | two_line_heading_level_two, 66 | ], 67 | ) 68 | 69 | expect String.parse_str(heading, "# Foo Bar") == Ok(Heading(One, "Foo Bar")) 70 | expect String.parse_str(heading, "Foo Bar\n---") == Ok(Heading(Two, "Foo Bar")) 71 | 72 | inline_heading = 73 | Parser.const(|level| |str| Heading(level, str)) 74 | |> Parser.keep( 75 | Parser.one_of( 76 | [ 77 | Parser.const(One) |> Parser.skip(String.string("# ")), 78 | Parser.const(Two) |> Parser.skip(String.string("## ")), 79 | Parser.const(Three) |> Parser.skip(String.string("### ")), 80 | Parser.const(Four) |> Parser.skip(String.string("#### ")), 81 | Parser.const(Five) |> Parser.skip(String.string("##### ")), 82 | Parser.const(Six) |> Parser.skip(String.string("###### ")), 83 | ], 84 | ), 85 | ) 86 | |> Parser.keep((Parser.chomp_while(not_end_of_line) |> Parser.map(String.str_from_utf8))) 87 | 88 | expect 89 | a = String.parse_str(inline_heading, "# Foo Bar") 90 | a == Ok(Heading(One, "Foo Bar")) 91 | 92 | expect 93 | a = String.parse_str_partial(inline_heading, "### Foo Bar\nBaz") 94 | a == Ok({ val: Heading(Three, "Foo Bar"), input: "\nBaz" }) 95 | 96 | two_line_heading_level_one = 97 | Parser.const(|str| Heading(One, str)) 98 | |> Parser.keep((Parser.chomp_while(not_end_of_line) |> Parser.map(String.str_from_utf8))) 99 | |> Parser.skip(end_of_line) 100 | |> Parser.skip(String.string("==")) 101 | |> Parser.skip(Parser.chomp_while(|b| not_end_of_line(b) and b == '=')) 102 | 103 | expect 104 | a = String.parse_str(two_line_heading_level_one, "Foo Bar\n==") 105 | a == Ok(Heading(One, "Foo Bar")) 106 | 107 | expect 108 | a = String.parse_str_partial(two_line_heading_level_one, "Foo Bar\n=============\n") 109 | a == Ok({ val: Heading(One, "Foo Bar"), input: "\n" }) 110 | 111 | two_line_heading_level_two = 112 | Parser.const(|str| Heading(Two, str)) 113 | |> Parser.keep((Parser.chomp_while(not_end_of_line) |> Parser.map(String.str_from_utf8))) 114 | |> Parser.skip(end_of_line) 115 | |> Parser.skip(String.string("--")) 116 | |> Parser.skip(Parser.chomp_while(|b| not_end_of_line(b) and b == '-')) 117 | 118 | expect 119 | a = String.parse_str(two_line_heading_level_two, "Foo Bar\n---") 120 | a == Ok(Heading(Two, "Foo Bar")) 121 | 122 | expect 123 | a = String.parse_str_partial(two_line_heading_level_two, "Foo Bar\n-----\nApples") 124 | a == Ok({ val: Heading(Two, "Foo Bar"), input: "\nApples" }) 125 | 126 | ## Links 127 | ## 128 | ## ```roc 129 | ## expect String.parse_str(link, "[roc](https://roc-lang.org)") == Ok(Link("roc", "https://roc-lang.org")) 130 | ## ``` 131 | link : Parser String.Utf8 Markdown 132 | link = 133 | Parser.const(|alt| |href| Link({ alt, href })) 134 | |> Parser.skip(String.string("[")) 135 | |> Parser.keep((Parser.chomp_while(|b| b != ']') |> Parser.map(String.str_from_utf8))) 136 | |> Parser.skip(String.string("](")) 137 | |> Parser.keep((Parser.chomp_while(|b| b != ')') |> Parser.map(String.str_from_utf8))) 138 | |> Parser.skip(String.codeunit(')')) 139 | 140 | expect String.parse_str(link, "[roc](https://roc-lang.org)") == Ok(Link({ alt: "roc", href: "https://roc-lang.org" })) 141 | 142 | expect 143 | a = String.parse_str_partial(link, "[roc](https://roc-lang.org)\nApples") 144 | a == Ok({ val: Link({ alt: "roc", href: "https://roc-lang.org" }), input: "\nApples" }) 145 | 146 | ## Images 147 | ## 148 | ## ```roc 149 | ## expect String.parse_str(image, "![alt text](/images/logo.png)") == Ok(Image("alt text", "/images/logo.png")) 150 | ## ``` 151 | image : Parser String.Utf8 Markdown 152 | image = 153 | Parser.const(|alt| |href| Image({ alt, href })) 154 | |> Parser.skip(String.string("![")) 155 | |> Parser.keep((Parser.chomp_while(|b| b != ']') |> Parser.map(String.str_from_utf8))) 156 | |> Parser.skip(String.string("](")) 157 | |> Parser.keep((Parser.chomp_while(|b| b != ')') |> Parser.map(String.str_from_utf8))) 158 | |> Parser.skip(String.codeunit(')')) 159 | 160 | expect String.parse_str(image, "![alt text](/images/logo.png)") == Ok(Image({ alt: "alt text", href: "/images/logo.png" })) 161 | 162 | expect 163 | a = String.parse_str_partial(image, "![alt text](/images/logo.png)\nApples") 164 | a == Ok({ val: Image({ alt: "alt text", href: "/images/logo.png" }), input: "\nApples" }) 165 | 166 | ## Parse code blocks using triple backticks 167 | ## supports block extension e.g. ```roc 168 | ## 169 | ## ```roc 170 | ## expect 171 | ## text = 172 | ## """ 173 | ## ```roc 174 | ## # some code 175 | ## foo = bar 176 | ## ``` 177 | ## """ 178 | ## 179 | ## a = String.parse_str(code, text) 180 | ## a == Ok(Code({ ext: "roc", pre: "# some code\nfoo = bar\n" })) 181 | ## ``` 182 | code : Parser String.Utf8 Markdown 183 | code = 184 | 185 | Parser.const(|ext| |pre| Code({ ext, pre })) 186 | |> Parser.keep( 187 | Parser.one_of( 188 | [ 189 | # parse backticks with ext e.g. ```roc 190 | Parser.const(|i| i) 191 | |> Parser.skip(String.string("```")) 192 | |> Parser.keep((Parser.chomp_while(not_end_of_line) |> Parser.map(String.str_from_utf8))) 193 | |> Parser.skip(end_of_line), 194 | 195 | # parse just backticks e.g. ``` 196 | Parser.const("") |> Parser.skip(String.string("```")), 197 | ], 198 | ), 199 | ) 200 | |> Parser.keep(chomp_until_code_block_end) 201 | 202 | expect 203 | text = 204 | """ 205 | ```roc 206 | # some code 207 | foo = bar 208 | ``` 209 | """ 210 | 211 | a = String.parse_str(code, text) 212 | a == Ok(Code({ ext: "roc", pre: "# some code\nfoo = bar\n" })) 213 | 214 | chomp_until_code_block_end : Parser String.Utf8 Str 215 | chomp_until_code_block_end = 216 | Parser.build_primitive_parser(|input| chomp_to_code_block_end_help({ val: List.with_capacity(1000), input })) 217 | |> Parser.map(String.str_from_utf8) 218 | 219 | chomp_to_code_block_end_help : { val : String.Utf8, input : String.Utf8 } -> Parser.ParseResult String.Utf8 String.Utf8 220 | chomp_to_code_block_end_help = |{ val, input }| 221 | when input is 222 | [] -> Err(ParsingFailure("expected ```, ran out of input")) 223 | ['`', '`', '`', .. as rest] -> Ok({ val, input: rest }) 224 | [first, .. as rest] -> chomp_to_code_block_end_help({ val: List.append(val, first), input: rest }) 225 | 226 | expect 227 | val = "" |> Str.to_utf8 228 | input = "some code\n```" |> Str.to_utf8 229 | expected = "some code\n" |> Str.to_utf8 230 | a = chomp_to_code_block_end_help({ val, input }) 231 | a == Ok({ val: expected, input: [] }) 232 | -------------------------------------------------------------------------------- /package/Parser.roc: -------------------------------------------------------------------------------- 1 | ## # Parser 2 | ## 3 | ## This package implements a basic [Parser Combinator](https://en.wikipedia.org/wiki/Parser_combinator) 4 | ## for Roc which is useful for transforming input into a more useful structure. 5 | ## 6 | ## ## Example 7 | ## For example, say we wanted to parse the following string from `in` to `out`: 8 | ## ```roc 9 | ## in = "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green" 10 | ## out = 11 | ## { 12 | ## id: 1, 13 | ## requirements: [ 14 | ## [Blue 3, Red 4], 15 | ## [Red 1, Green 2, Blue 6], 16 | ## [Green 2], 17 | ## ] 18 | ## } 19 | ## ``` 20 | ## We could do this using the following: 21 | ## ```roc 22 | ## Requirement : [Green U64, Red U64, Blue U64] 23 | ## RequirementSet : List Requirement 24 | ## Game : { id : U64, requirements : List RequirementSet } 25 | ## 26 | ## parse_game : Str -> Result Game [ParsingError] 27 | ## parse_game = |s| 28 | ## green = const(Green) |> keep(digits) |> skip(string(" green")) 29 | ## red = const(Red) |> keep(digits) |> skip(string(" red")) 30 | ## blue = const(Blue) |> keep(digits) |> skip(string(" blue")) 31 | ## 32 | ## requirement_set : Parser _ RequirementSet 33 | ## requirement_set = one_of([green, red, blue]) |> sep_by(string(", ")) 34 | ## 35 | ## requirements : Parser _ (List RequirementSet) 36 | ## requirements = requirement_set |> sep_by(string("; ")) 37 | ## 38 | ## game : Parser _ Game 39 | ## game = 40 | ## const(|id| |r| { id, requirements: r }) 41 | ## |> skip(string("Game ")) 42 | ## |> keep(digits) 43 | ## |> skip(string(": ")) 44 | ## |> keep(requirements) 45 | ## 46 | ## when parse_str(game, s) is 47 | ## Ok(g) -> Ok(g) 48 | ## Err(ParsingFailure(_)) | Err(ParsingIncomplete(_)) -> Err(ParsingError) 49 | ## ``` 50 | module [ 51 | Parser, 52 | ParseResult, 53 | parse, 54 | parse_partial, 55 | fail, 56 | const, 57 | alt, 58 | apply, 59 | one_of, 60 | map, 61 | map2, 62 | map3, 63 | lazy, 64 | maybe, 65 | one_or_more, 66 | many, 67 | between, 68 | sep_by, 69 | sep_by1, 70 | ignore, 71 | build_primitive_parser, 72 | flatten, 73 | keep, 74 | skip, 75 | chomp_until, 76 | chomp_while, 77 | ] 78 | 79 | ## Opaque type for a parser that will try to parse an `a` from an `input`. 80 | ## 81 | ## As such, a parser can be considered a recipe for a function of the type 82 | ## ```roc 83 | ## input -> Result {val: a, input: input} [ParsingFailure Str] 84 | ## ``` 85 | ## 86 | ## How a parser is _actually_ implemented internally is not important 87 | ## and this might change between versions; 88 | ## for instance to improve efficiency or error messages on parsing failures. 89 | Parser input a := input -> ParseResult input a 90 | 91 | ## ```roc 92 | ## ParseResult input a : Result { val : a, input : input } [ParsingFailure Str] 93 | ## ``` 94 | ParseResult input a : Result { val : a, input : input } [ParsingFailure Str] 95 | 96 | ## Write a custom parser without using provided combintors. 97 | build_primitive_parser : (input -> ParseResult input a) -> Parser input a 98 | build_primitive_parser = |fun| @Parser(fun) 99 | 100 | ## Most general way of running a parser. 101 | ## 102 | ## Can be thought of as turning the recipe of a parser into its actual parsing function 103 | ## and running this function on the given input. 104 | ## 105 | ## Moat parsers consume part of `input` when they succeed. This allows you to string parsers 106 | ## together that run one after the other. The part of the input that the first 107 | ## parser did not consume, is used by the next parser. 108 | ## This is why a parser returns on success both the resulting value and the leftover part of the input. 109 | ## 110 | ## This is mostly useful when creating your own internal parsing building blocks. 111 | parse_partial : Parser input a, input -> ParseResult input a 112 | parse_partial = |@Parser(parser), input| 113 | parser(input) 114 | 115 | ## Runs a parser on the given input, expecting it to fully consume the input 116 | ## 117 | ## The `input -> Bool` parameter is used to check whether parsing has 'completed', 118 | ## i.e. how to determine if all of the input has been consumed. 119 | ## 120 | ## For most input types, a parsing run that leaves some unparsed input behind 121 | ## should be considered an error. 122 | parse : Parser input a, input, (input -> Bool) -> Result a [ParsingFailure Str, ParsingIncomplete input] 123 | parse = |parser, input, is_parsing_completed| 124 | when parse_partial(parser, input) is 125 | Ok({ val: val, input: leftover }) -> 126 | if is_parsing_completed(leftover) then 127 | Ok(val) 128 | else 129 | Err(ParsingIncomplete(leftover)) 130 | 131 | Err(ParsingFailure(msg)) -> 132 | Err(ParsingFailure(msg)) 133 | 134 | ## Parser that can never succeed, regardless of the given input. 135 | ## It will always fail with the given error message. 136 | ## 137 | ## This is mostly useful as a 'base case' if all other parsers 138 | ## in a `oneOf` or `alt` have failed, to provide some more descriptive error message. 139 | fail : Str -> Parser * * 140 | fail = |msg| 141 | build_primitive_parser(|_input| Err(ParsingFailure(msg))) 142 | 143 | ## Parser that will always produce the given `a`, without looking at the actual input. 144 | ## This is useful as a basic building block, especially in combination with 145 | ## `map` and `apply`. 146 | ## ```roc 147 | ## parse_u32 : Parser (List U8) U32 148 | ## parse_u32 = 149 | ## const(Num.to_u32) 150 | ## |> keep(digits) 151 | ## 152 | ## expect parse_str(parse_u32, "123") == Ok(123u32) 153 | ## ``` 154 | const : a -> Parser * a 155 | const = |val| 156 | build_primitive_parser( 157 | |input| 158 | Ok({ val: val, input: input }), 159 | ) 160 | 161 | ## Try the `first` parser and (only) if it fails, try the `second` parser as fallback. 162 | alt : Parser input a, Parser input a -> Parser input a 163 | alt = |first, second| 164 | build_primitive_parser( 165 | |input| 166 | when parse_partial(first, input) is 167 | Ok({ val: val, input: rest }) -> Ok({ val: val, input: rest }) 168 | Err(ParsingFailure(first_err)) -> 169 | when parse_partial(second, input) is 170 | Ok({ val: val, input: rest }) -> Ok({ val: val, input: rest }) 171 | Err(ParsingFailure(second_err)) -> 172 | Err(ParsingFailure("${first_err} or ${second_err}")), 173 | ) 174 | 175 | ## Runs a parser building a function, then a parser building a value, 176 | ## and finally returns the result of calling the function with the value. 177 | ## 178 | ## This is useful if you are building up a structure that requires more parameters 179 | ## than there are variants of `map`, `map2`, `map3` etc. for. 180 | ## 181 | ## For instance, the following two are the same: 182 | ## ```roc 183 | ## const(|x| |y| |z| Triple(x, y, z)) 184 | ## |> map3(String.digits, String.digits, String.digits) 185 | ## 186 | ## const(|x| |y| |z| Triple(x, y, z)) 187 | ## |> apply(String.digits) 188 | ## |> apply(String.digits) 189 | ## |> apply(String.digits) 190 | ## ``` 191 | ## Indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood. 192 | ## 193 | ## # Currying 194 | ## Be aware that when using `apply`, you need to explicitly 'curry' the parameters to the construction function. 195 | ## This means that instead of writing `|x, y, z| ...` 196 | ## you'll need to write `|x| |y| |z| ...`. 197 | ## This is because the parameters of the function will be applied one by one as parsing continues. 198 | apply : Parser input (a -> b), Parser input a -> Parser input b 199 | apply = |fun_parser, val_parser| 200 | combined = |input| 201 | { val: fun_val, input: rest } = parse_partial(fun_parser, input)? 202 | parse_partial(val_parser, rest) 203 | |> Result.map_ok( 204 | |{ val: val, input: rest2 }| 205 | { val: fun_val(val), input: rest2 }, 206 | ) 207 | 208 | build_primitive_parser(combined) 209 | 210 | # Internal utility function. Not exposed to users, since usage is discouraged! 211 | # 212 | # Runs `first_parser` and (only) if it succeeds, 213 | # runs the function `buildNextParser` on its result value. 214 | # This function returns a new parser, which is finally run. 215 | # 216 | # `and_then` is usually more flexible than necessary, and less efficient 217 | # than using `const` with `map` and/or `apply`. 218 | # Consider using those functions first. 219 | and_then : Parser input a, (a -> Parser input b) -> Parser input b 220 | and_then = |first_parser, build_next_parser| 221 | fun = |input| 222 | { val: first_val, input: rest } = parse_partial(first_parser, input)? 223 | next_parser = build_next_parser(first_val) 224 | 225 | parse_partial(next_parser, rest) 226 | 227 | build_primitive_parser(fun) 228 | 229 | ## Try a list of parsers in turn, until one of them succeeds. 230 | ## ```roc 231 | ## color : Parser Utf8 [Red, Green, Blue] 232 | ## color = 233 | ## one_of( 234 | ## [ 235 | ## const(Red) |> skip(string("red")), 236 | ## const(Green) |> skip(string("green")), 237 | ## const(Blue) |> skip(string("blue")), 238 | ## ], 239 | ## ) 240 | ## 241 | ## expect parse_str(color, "green") == Ok(Green) 242 | ## ``` 243 | one_of : List (Parser input a) -> Parser input a 244 | one_of = |parsers| 245 | List.walk_backwards(parsers, fail("oneOf: The list of parsers was empty"), |later_parser, earlier_parser| alt(earlier_parser, later_parser)) 246 | 247 | ## Transforms the result of parsing into something else, 248 | ## using the given transformation function. 249 | map : Parser input a, (a -> b) -> Parser input b 250 | map = |simple_parser, transform| 251 | const(transform) 252 | |> apply(simple_parser) 253 | 254 | ## Transforms the result of parsing into something else, 255 | ## using the given two-parameter transformation function. 256 | map2 : Parser input a, Parser input b, (a, b -> c) -> Parser input c 257 | map2 = |parser_a, parser_b, transform| 258 | const(|a| |b| transform(a, b)) 259 | |> apply(parser_a) 260 | |> apply(parser_b) 261 | 262 | ## Transforms the result of parsing into something else, 263 | ## using the given three-parameter transformation function. 264 | ## 265 | ## If you need transformations with more inputs, 266 | ## take a look at `apply`. 267 | map3 : Parser input a, Parser input b, Parser input c, (a, b, c -> d) -> Parser input d 268 | map3 = |parser_a, parser_b, parser_c, transform| 269 | const(|a| |b| |c| transform(a, b, c)) 270 | |> apply(parser_a) 271 | |> apply(parser_b) 272 | |> apply(parser_c) 273 | 274 | ## Removes a layer of `Result` from running the parser. 275 | ## 276 | ## Use this to map functions that return a result over the parser, 277 | ## where errors are turned into `ParsingFailure`s. 278 | ## 279 | ## ```roc 280 | ## # Parse a number from a List U8 281 | ## u64 : Parser Utf8 U64 282 | ## u64 = 283 | ## string 284 | ## |> map( 285 | ## |val| 286 | ## when Str.to_u64(val) is 287 | ## Ok(num) -> Ok(num) 288 | ## Err(_) -> Err("${val} is not a U64."), 289 | ## ) 290 | ## |> flatten 291 | ## ``` 292 | flatten : Parser input (Result a Str) -> Parser input a 293 | flatten = |parser| 294 | build_primitive_parser( 295 | |input| 296 | result = parse_partial(parser, input) 297 | 298 | when result is 299 | Err(problem) -> Err(problem) 300 | Ok({ val: Ok(val), input: input_rest }) -> Ok({ val: val, input: input_rest }) 301 | Ok({ val: Err(problem), input: _inputRest }) -> Err(ParsingFailure(problem)), 302 | ) 303 | 304 | ## Runs a parser lazily 305 | ## 306 | ## This is (only) useful when dealing with a recursive structure. 307 | ## For instance, consider a type `Comment : { message: String, responses: List Comment }`. 308 | ## Without `lazy`, you would ask the compiler to build an infinitely deep parser. 309 | ## (Resulting in a compiler error.) 310 | ## 311 | lazy : ({} -> Parser input a) -> Parser input a 312 | lazy = |thunk| 313 | const({}) 314 | |> and_then(thunk) 315 | 316 | maybe : Parser input a -> Parser input (Result a [Nothing]) 317 | maybe = |parser| 318 | alt((parser |> map(|val| Ok(val))), const(Err(Nothing))) 319 | 320 | many_impl : Parser input a, List a, input -> ParseResult input (List a) 321 | many_impl = |parser, vals, input| 322 | result = parse_partial(parser, input) 323 | 324 | when result is 325 | Err(_) -> 326 | Ok({ val: vals, input: input }) 327 | 328 | Ok({ val: val, input: input_rest }) -> 329 | many_impl(parser, List.append(vals, val), input_rest) 330 | 331 | ## A parser which runs the element parser *zero* or more times on the input, 332 | ## returning a list containing all the parsed elements. 333 | many : Parser input a -> Parser input (List a) 334 | many = |parser| 335 | build_primitive_parser( 336 | |input| 337 | many_impl(parser, [], input), 338 | ) 339 | 340 | ## A parser which runs the element parser *one* or more times on the input, 341 | ## returning a list containing all the parsed elements. 342 | ## 343 | ## Also see [Parser.many]. 344 | one_or_more : Parser input a -> Parser input (List a) 345 | one_or_more = |parser| 346 | const(|val| |vals| List.prepend(vals, val)) 347 | |> apply(parser) 348 | |> apply(many(parser)) 349 | 350 | ## Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter, 351 | ## and only returns the result of your main parser. 352 | ## 353 | ## Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.) 354 | ## 355 | ## ```roc 356 | ## between_braces = |parser| parser |> between(scalar('['), scalar(']')) 357 | ## ``` 358 | between : Parser input a, Parser input open, Parser input close -> Parser input a 359 | between = |parser, open, close| 360 | const(|_| |val| |_| val) 361 | |> apply(open) 362 | |> apply(parser) 363 | |> apply(close) 364 | 365 | sep_by1 : Parser input a, Parser input sep -> Parser input (List a) 366 | sep_by1 = |parser, separator| 367 | parser_followed_by_sep = 368 | const(|_| |val| val) 369 | |> apply(separator) 370 | |> apply(parser) 371 | 372 | const(|val| |vals| List.prepend(vals, val)) 373 | |> apply(parser) 374 | |> apply(many(parser_followed_by_sep)) 375 | 376 | ## ```roc 377 | ## parse_numbers : Parser (List U8) (List U64) 378 | ## parse_numbers = 379 | ## digits |> sep_by(codeunit(',')) 380 | ## 381 | ## expect parse_str(parse_numbers, "1,2,3") == Ok([1, 2, 3]) 382 | ## ``` 383 | sep_by : Parser input a, Parser input sep -> Parser input (List a) 384 | sep_by = |parser, separator| 385 | alt(sep_by1(parser, separator), const([])) 386 | 387 | ignore : Parser input a -> Parser input {} 388 | ignore = |parser| 389 | map(parser, |_| {}) 390 | 391 | keep : Parser input (a -> b), Parser input a -> Parser input b 392 | keep = |fun_parser, val_parser| 393 | build_primitive_parser( 394 | |input| 395 | when parse_partial(fun_parser, input) is 396 | Err(msg) -> Err(msg) 397 | Ok({ val: fun_val, input: rest }) -> 398 | when parse_partial(val_parser, rest) is 399 | Err(msg2) -> Err(msg2) 400 | Ok({ val: val, input: rest2 }) -> 401 | Ok({ val: fun_val(val), input: rest2 }), 402 | ) 403 | 404 | skip : Parser input a, Parser input * -> Parser input a 405 | skip = |fun_parser, skip_parser| 406 | build_primitive_parser( 407 | |input| 408 | when parse_partial(fun_parser, input) is 409 | Err(msg) -> Err(msg) 410 | Ok({ val: fun_val, input: rest }) -> 411 | when parse_partial(skip_parser, rest) is 412 | Err(msg2) -> Err(msg2) 413 | Ok({ val: _, input: rest2 }) -> Ok({ val: fun_val, input: rest2 }), 414 | ) 415 | 416 | ## Match zero or more codeunits until the it reaches the given codeunit. 417 | ## The given codeunit is not included in the match. 418 | ## 419 | ## This can be used with [Parser.skip] to ignore text. 420 | ## 421 | ## ```roc 422 | ## ignore_text : Parser (List U8) U64 423 | ## ignore_text = 424 | ## const(|d| d) 425 | ## |> skip(chomp_until(':')) 426 | ## |> skip(codeunit(':')) 427 | ## |> keep(digits) 428 | ## 429 | ## expect parse_str(ignore_text, "ignore preceding text:123") == Ok(123) 430 | ## ``` 431 | ## 432 | ## This can be used with [Parser.keep] to capture a list of `U8` codeunits. 433 | ## 434 | ## ```roc 435 | ## capture_text : Parser (List U8) (List U8) 436 | ## capture_text = 437 | ## const(|codeunits| codeunits) 438 | ## |> keep(chomp_until(':')) 439 | ## |> skip(codeunit(':')) 440 | ## 441 | ## expect parse_str(capture_text, "Roc:") == Ok(['R', 'o', 'c']) 442 | ## ``` 443 | ## 444 | ## Use [String.str_from_utf8] to turn the results into a `Str`. 445 | ## 446 | ## Also see [Parser.chomp_while]. 447 | chomp_until : a -> Parser (List a) (List a) where a implements Eq 448 | chomp_until = |char| 449 | build_primitive_parser( 450 | |input| 451 | when List.find_first_index(input, |x| Bool.is_eq(x, char)) is 452 | Ok(index) -> 453 | val = List.sublist(input, { start: 0, len: index }) 454 | Ok({ val, input: List.drop_first(input, index) }) 455 | 456 | Err(_) -> Err(ParsingFailure("character not found")), 457 | ) 458 | 459 | expect 460 | input = "# H\nR" |> Str.to_utf8 461 | result = parse_partial(chomp_until('\n'), input) 462 | result == Ok({ val: ['#', ' ', 'H'], input: ['\n', 'R'] }) 463 | 464 | expect 465 | when parse_partial(chomp_until('\n'), []) is 466 | Ok(_) -> Bool.false 467 | Err(ParsingFailure(_)) -> Bool.true 468 | 469 | ## Match zero or more codeunits until the check returns false. 470 | ## The codeunit that returned false is not included in the match. 471 | ## Note: a chompWhile parser always succeeds! 472 | ## 473 | ## This can be used with [Parser.skip] to ignore text. 474 | ## This is useful for chomping whitespace or variable names. 475 | ## 476 | ## ``` 477 | ## ignore_numbers : Parser (List U8) Str 478 | ## ignore_numbers = 479 | ## const(|str| str) 480 | ## |> skip(chomp_while(|b| b >= '0' && b <= '9')) 481 | ## |> keep(string("TEXT")) 482 | ## 483 | ## expect parse_str(ignore_numbers, "0123456789876543210TEXT") == Ok("TEXT") 484 | ## ``` 485 | ## 486 | ## This can be used with [Parser.keep] to capture a list of `U8` codeunits. 487 | ## 488 | ## ``` 489 | ## capture_numbers : Parser (List U8) (List U8) 490 | ## capture_numbers = 491 | ## const(|codeunits| codeunits) 492 | ## |> keep(chomp_while(|b| b >= '0' && b <= '9')) 493 | ## |> skip(string("TEXT")) 494 | ## 495 | ## expect parse_str(capture_numbers, "123TEXT") == Ok(['1', '2', '3']) 496 | ## ``` 497 | ## 498 | ## Use [String.str_from_utf8] to turn the results into a `Str`. 499 | ## 500 | ## Also see [Parser.chomp_until]. 501 | chomp_while : (a -> Bool) -> Parser (List a) (List a) where a implements Eq 502 | chomp_while = |check| 503 | build_primitive_parser( 504 | |input| 505 | index = List.walk_until( 506 | input, 507 | 0, 508 | |i, elem| 509 | if check(elem) then 510 | Continue((i + 1)) 511 | else 512 | Break(i), 513 | ) 514 | 515 | if index == 0 then 516 | Ok({ val: [], input: input }) 517 | else 518 | Ok( 519 | { 520 | val: List.sublist(input, { start: 0, len: index }), 521 | input: List.drop_first(input, index), 522 | }, 523 | ), 524 | ) 525 | 526 | expect 527 | input = [97u8, 's', '\n', 'd', 'f'] 528 | not_eol = |x| Bool.is_not_eq(x, '\n') 529 | result = parse_partial(chomp_while(not_eol), input) 530 | result == Ok({ val: [97u8, 's'], input: ['\n', 'd', 'f'] }) 531 | -------------------------------------------------------------------------------- /package/String.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | Utf8, 3 | parse_str, 4 | parse_str_partial, 5 | parse_utf8, 6 | parse_utf8_partial, 7 | string, 8 | utf8, 9 | codeunit, 10 | codeunit_satisfies, 11 | any_string, 12 | any_thing, 13 | any_codeunit, 14 | one_of, 15 | digit, 16 | digits, 17 | str_from_utf8, 18 | str_from_ascii, 19 | ] 20 | 21 | import Parser exposing [Parser] 22 | 23 | ## ``` 24 | ## Utf8 : List U8 25 | ## ``` 26 | Utf8 : List U8 27 | 28 | ## Parse a [Str] using a [Parser] 29 | ## ```roc 30 | ## color : Parser Utf8 [Red, Green, Blue] 31 | ## color = 32 | ## one_of( 33 | ## [ 34 | ## Parser.const(Red) |> Parser.skip(string("red")), 35 | ## Parser.const(Green) |> Parser.skip(string("green")), 36 | ## Parser.const(Blue) |> Parser.skip(string("blue")), 37 | ## ], 38 | ## ) 39 | ## 40 | ## expect parse_str(color, "green") == Ok(Green) 41 | ## ``` 42 | parse_str : Parser Utf8 a, Str -> Result a [ParsingFailure Str, ParsingIncomplete Str] 43 | parse_str = |parser, input| 44 | parser 45 | |> parse_utf8(str_to_raw(input)) 46 | |> Result.map_err( 47 | |problem| 48 | when problem is 49 | ParsingFailure(msg) -> ParsingFailure(msg) 50 | ParsingIncomplete(leftover_raw) -> ParsingIncomplete(str_from_utf8(leftover_raw)), 51 | ) 52 | 53 | ## Runs a parser against the start of a string, allowing the parser to consume it only partially. 54 | ## 55 | ## - If the parser succeeds, returns the resulting value as well as the leftover input. 56 | ## - If the parser fails, returns `Err (ParsingFailure msg)` 57 | ## 58 | ## ```roc 59 | ## at_sign : Parser Utf8 [AtSign] 60 | ## at_sign = Parser.const(AtSign) |> Parser.skip(codeunit('@')) 61 | ## 62 | ## expect parse_str(at_sign, "@") == Ok(AtSign) 63 | ## expect parse_str_partial(at_sign, "@") |> Result.map_ok(.val) == Ok(AtSign) 64 | ## expect parse_str_partial(at_sign, "$") |> Result.is_err 65 | ## ``` 66 | parse_str_partial : Parser Utf8 a, Str -> Parser.ParseResult Str a 67 | parse_str_partial = |parser, input| 68 | parser 69 | |> parse_utf8_partial(str_to_raw(input)) 70 | |> Result.map_ok( 71 | |{ val: val, input: rest_raw }| 72 | { val: val, input: str_from_utf8(rest_raw) }, 73 | ) 74 | 75 | ## Runs a parser against a string, requiring the parser to consume it fully. 76 | ## 77 | ## - If the parser succeeds, returns `Ok a` 78 | ## - If the parser fails, returns `Err (ParsingFailure Str)` 79 | ## - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete (List U8))` 80 | ## 81 | parse_utf8 : Parser Utf8 a, Utf8 -> Result a [ParsingFailure Str, ParsingIncomplete Utf8] 82 | parse_utf8 = |parser, input| 83 | Parser.parse(parser, input, |leftover| List.len(leftover) == 0) 84 | 85 | ## Runs a parser against the start of a list of scalars, allowing the parser to consume it only partially. 86 | parse_utf8_partial : Parser Utf8 a, Utf8 -> Parser.ParseResult Utf8 a 87 | parse_utf8_partial = |parser, input| 88 | Parser.parse_partial(parser, input) 89 | 90 | ## ```roc 91 | ## is_digit : U8 -> Bool 92 | ## is_digit = |b| b >= '0' && b <= '9' 93 | ## 94 | ## expect parse_str(codeunit_satisfies(is_digit), "0") == Ok('0') 95 | ## expect parse_str(codeunit_satisfies(is_digit), "*") |> Result.is_err 96 | ## ``` 97 | codeunit_satisfies : (U8 -> Bool) -> Parser Utf8 U8 98 | codeunit_satisfies = |check| 99 | Parser.build_primitive_parser( 100 | |input| 101 | { before: start, others: input_rest } = List.split_at(input, 1) 102 | 103 | when List.get(start, 0) is 104 | Err(OutOfBounds) -> 105 | Err(ParsingFailure("expected a codeunit satisfying a condition, but input was empty.")) 106 | 107 | Ok(start_codeunit) -> 108 | if check(start_codeunit) then 109 | Ok({ val: start_codeunit, input: input_rest }) 110 | else 111 | other_char = str_from_codeunit(start_codeunit) 112 | input_str = str_from_utf8(input) 113 | 114 | Err(ParsingFailure("expected a codeunit satisfying a condition but found `${other_char}`.\n While reading: `${input_str}`")), 115 | ) 116 | 117 | ## ```roc 118 | ## at_sign : Parser Utf8 [AtSign] 119 | ## at_sign = Parser.const(AtSign) |> Parser.skip(codeunit('@')) 120 | ## 121 | ## expect parse_str(at_sign, "@") == Ok(AtSign) 122 | ## expect Result.is_err(parse_str_partial(at_sign, "$")) 123 | ## ``` 124 | codeunit : U8 -> Parser Utf8 U8 125 | codeunit = |expected_code_unit| 126 | Parser.build_primitive_parser( 127 | |input| 128 | when input is 129 | [] -> 130 | Err(ParsingFailure("expected char `${str_from_codeunit(expected_code_unit)}` but input was empty.")) 131 | 132 | [first, .. as rest] if first == expected_code_unit -> 133 | Ok({ val: expected_code_unit, input: rest }) 134 | 135 | [first, ..] -> 136 | Err(ParsingFailure("expected char `${str_from_codeunit(expected_code_unit)}` but found `${str_from_codeunit(first)}`.\n While reading: `${str_from_utf8(input)}`")), 137 | ) 138 | 139 | ## Parse an extact sequence of utf8 140 | utf8 : List U8 -> Parser Utf8 (List U8) 141 | utf8 = |expected_string| 142 | # Implemented manually instead of a sequence of codeunits 143 | # because of efficiency and better error messages 144 | Parser.build_primitive_parser( 145 | |input| 146 | { before: start, others: input_rest } = List.split_at(input, List.len(expected_string)) 147 | 148 | if start == expected_string then 149 | Ok({ val: expected_string, input: input_rest }) 150 | else 151 | error_string = str_from_utf8(expected_string) 152 | other_string = str_from_utf8(start) 153 | input_string = str_from_utf8(input) 154 | 155 | Err(ParsingFailure("expected string `${error_string}` but found `${other_string}`.\nWhile reading: ${input_string}")), 156 | ) 157 | 158 | ## Parse the given [Str] 159 | ## ```roc 160 | ## expect parse_str(string("Foo"), "Foo") == Ok("Foo") 161 | ## expect Result.is_err(parse_str(string("Foo"), "Bar")) 162 | ## ``` 163 | string : Str -> Parser Utf8 Str 164 | string = |expected_string| 165 | str_to_raw(expected_string) 166 | |> utf8 167 | |> Parser.map(|_val| expected_string) 168 | 169 | ## Matches any [U8] codeunit 170 | ## ```roc 171 | ## expect parse_str(any_codeunit, "a") == Ok('a') 172 | ## expect parse_str(any_codeunit, "$") == Ok('$') 173 | ## ``` 174 | any_codeunit : Parser Utf8 U8 175 | any_codeunit = codeunit_satisfies(|_| Bool.true) 176 | 177 | expect parse_str(any_codeunit, "a") == Ok('a') 178 | expect parse_str(any_codeunit, "\$") == Ok(36) 179 | 180 | ## Matches any [Utf8] and consumes all the input without fail. 181 | ## ```roc 182 | ## expect 183 | ## bytes = Str.to_utf8("consumes all the input") 184 | ## Parser.parse(any_thing, bytes, List.is_empty) == Ok(bytes) 185 | ## ``` 186 | any_thing : Parser Utf8 Utf8 187 | any_thing = Parser.build_primitive_parser(|input| Ok({ val: input, input: [] })) 188 | 189 | expect 190 | bytes = Str.to_utf8("consumes all the input") 191 | Parser.parse(any_thing, bytes, List.is_empty) == Ok(bytes) 192 | 193 | # Matches any string 194 | # as long as it is valid UTF8. 195 | any_string : Parser Utf8 Str 196 | any_string = Parser.build_primitive_parser( 197 | |field_utf8ing| 198 | when Str.from_utf8(field_utf8ing) is 199 | Ok(string_val) -> 200 | Ok({ val: string_val, input: [] }) 201 | 202 | Err(BadUtf8(_)) -> 203 | Err(ParsingFailure("Expected a string field, but its contents cannot be parsed as UTF8.")), 204 | ) 205 | 206 | ## ```roc 207 | ## expect parse_str(digit, "0") == Ok(0) 208 | ## expect Result.is_err(parse_str(digit, "not a digit")) 209 | ## ``` 210 | digit : Parser Utf8 U64 211 | digit = 212 | Parser.build_primitive_parser( 213 | |input| 214 | when input is 215 | [] -> 216 | Err(ParsingFailure("Expected a digit from 0-9 but input was empty.")) 217 | 218 | [first, .. as rest] if first >= '0' and first <= '9' -> 219 | Ok({ val: Num.to_u64((first - '0')), input: rest }) 220 | 221 | _ -> 222 | Err(ParsingFailure("Not a digit")), 223 | ) 224 | 225 | ## Parse a sequence of digits into a [U64], accepting leading zeroes 226 | ## ```roc 227 | ## expect parse_str(digits, "0123") == Ok(123) 228 | ## expect Result.is_err(parse_str(digits, "not a digit")) 229 | ## ``` 230 | digits : Parser Utf8 U64 231 | digits = 232 | Parser.one_or_more(digit) 233 | |> Parser.map(|ds| List.walk(ds, 0, |sum, d| sum * 10 + d)) 234 | 235 | ## Try a bunch of different parsers. 236 | ## 237 | ## The first parser which is tried is the one at the front of the list, 238 | ## and the next one is tried until one succeeds or the end of the list was reached. 239 | ## ```roc 240 | ## bool_parser : Parser Utf8 Bool 241 | ## bool_parser = 242 | ## one_of([string("true"), string("false")]) 243 | ## |> Parser.map(|x| if x == "true" then Bool.true else Bool.false) 244 | ## 245 | ## expect parse_str(bool_parser, "true") == Ok(Bool.true) 246 | ## expect parse_str(bool_parser, "false") == Ok(Bool.false) 247 | ## expect Result.is_err(parse_str(bool_parser, "not a bool")) 248 | ## ``` 249 | one_of : List (Parser Utf8 a) -> Parser Utf8 a 250 | one_of = |parsers| 251 | Parser.build_primitive_parser( 252 | |input| 253 | List.walk_until( 254 | parsers, 255 | Err(ParsingFailure("(no possibilities)")), 256 | |_, parser| 257 | when parse_utf8_partial(parser, input) is 258 | Ok(val) -> 259 | Break(Ok(val)) 260 | 261 | Err(problem) -> 262 | Continue(Err(problem)), 263 | ), 264 | ) 265 | 266 | str_from_utf8 : Utf8 -> Str 267 | str_from_utf8 = |raw_str| 268 | raw_str 269 | |> Str.from_utf8 270 | |> Result.with_default("Unexpected problem while turning a List U8 (that was originally a Str) back into a Str. This should never happen!") 271 | 272 | str_to_raw : Str -> Utf8 273 | str_to_raw = |str| 274 | str |> Str.to_utf8 275 | 276 | str_from_codeunit : U8 -> Str 277 | str_from_codeunit = |cu| 278 | str_from_utf8([cu]) 279 | 280 | str_from_ascii : U8 -> Str 281 | str_from_ascii = |ascii_num| 282 | when Str.from_utf8([ascii_num]) is 283 | Ok(answer) -> answer 284 | Err(_) -> crash("The number ${Num.to_str(ascii_num)} is not a valid ASCII constant!") 285 | 286 | # -------------------- example snippets used in docs -------------------- 287 | 288 | parse_u32 : Parser Utf8 U32 289 | parse_u32 = 290 | Parser.const(Num.to_u32) 291 | |> Parser.keep(digits) 292 | 293 | expect parse_str(parse_u32, "123") == Ok(123u32) 294 | 295 | color : Parser Utf8 [Red, Green, Blue] 296 | color = 297 | one_of( 298 | [ 299 | Parser.const(Red) |> Parser.skip(string("red")), 300 | Parser.const(Green) |> Parser.skip(string("green")), 301 | Parser.const(Blue) |> Parser.skip(string("blue")), 302 | ], 303 | ) 304 | 305 | expect parse_str(color, "green") == Ok(Green) 306 | 307 | parse_numbers : Parser Utf8 (List U64) 308 | parse_numbers = 309 | digits |> Parser.sep_by(codeunit(',')) 310 | 311 | expect parse_str(parse_numbers, "1,2,3") == Ok([1, 2, 3]) 312 | 313 | expect parse_str(string("Foo"), "Foo") == Ok("Foo") 314 | expect parse_str(string("Foo"), "Bar") |> Result.is_err 315 | 316 | ignore_text : Parser Utf8 U64 317 | ignore_text = 318 | Parser.const(|d| d) 319 | |> Parser.skip(Parser.chomp_until(':')) 320 | |> Parser.skip(codeunit(':')) 321 | |> Parser.keep(digits) 322 | 323 | expect parse_str(ignore_text, "ignore preceding text:123") == Ok(123) 324 | 325 | ignore_numbers : Parser Utf8 Str 326 | ignore_numbers = 327 | Parser.const(|str| str) 328 | |> Parser.skip(Parser.chomp_while(|b| b >= '0' and b <= '9')) 329 | |> Parser.keep(string("TEXT")) 330 | 331 | expect parse_str(ignore_numbers, "0123456789876543210TEXT") == Ok("TEXT") 332 | 333 | is_digit : U8 -> Bool 334 | is_digit = |b| b >= '0' and b <= '9' 335 | 336 | expect parse_str(codeunit_satisfies(is_digit), "0") == Ok('0') 337 | expect parse_str(codeunit_satisfies(is_digit), "*") |> Result.is_err 338 | 339 | at_sign : Parser Utf8 [AtSign] 340 | at_sign = Parser.const(AtSign) |> Parser.skip(codeunit('@')) 341 | 342 | expect parse_str(at_sign, "@") == Ok(AtSign) 343 | expect parse_str_partial(at_sign, "@") |> Result.map_ok(.val) == Ok(AtSign) 344 | expect parse_str_partial(at_sign, "\$") |> Result.is_err 345 | 346 | Requirement : [Green U64, Red U64, Blue U64] 347 | RequirementSet : List Requirement 348 | Game : { id : U64, requirements : List RequirementSet } 349 | 350 | parse_game : Str -> Result Game [ParsingError] 351 | parse_game = |s| 352 | green = Parser.const(Green) |> Parser.keep(digits) |> Parser.skip(string(" green")) 353 | red = Parser.const(Red) |> Parser.keep(digits) |> Parser.skip(string(" red")) 354 | blue = Parser.const(Blue) |> Parser.keep(digits) |> Parser.skip(string(" blue")) 355 | 356 | requirement_set : Parser _ RequirementSet 357 | requirement_set = (one_of([green, red, blue])) |> Parser.sep_by(string(", ")) 358 | 359 | requirements : Parser _ (List RequirementSet) 360 | requirements = requirement_set |> Parser.sep_by(string("; ")) 361 | 362 | game : Parser _ Game 363 | game = 364 | Parser.const(|id| |r| { id, requirements: r }) 365 | |> Parser.skip(string("Game ")) 366 | |> Parser.keep(digits) 367 | |> Parser.skip(string(": ")) 368 | |> Parser.keep(requirements) 369 | 370 | when parse_str(game, s) is 371 | Ok(g) -> Ok(g) 372 | Err(ParsingFailure(_)) | Err(ParsingIncomplete(_)) -> Err(ParsingError) 373 | 374 | expect 375 | parse_game("Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green") 376 | == Ok( 377 | { 378 | id: 1, 379 | requirements: [ 380 | [Blue(3), Red(4)], 381 | [Red(1), Green(2), Blue(6)], 382 | [Green(2)], 383 | ], 384 | }, 385 | ) 386 | 387 | expect parse_str(digit, "0") == Ok(0) 388 | expect parse_str(digit, "not a digit") |> Result.is_err 389 | 390 | expect parse_str(digits, "0123") == Ok(123) 391 | expect parse_str(digits, "not a digit") |> Result.is_err 392 | 393 | bool_parser : Parser Utf8 Bool 394 | bool_parser = 395 | one_of([string("true"), string("false")]) 396 | |> Parser.map(|x| if x == "true" then Bool.true else Bool.false) 397 | 398 | expect parse_str(bool_parser, "true") == Ok(Bool.true) 399 | expect parse_str(bool_parser, "false") == Ok(Bool.false) 400 | expect parse_str(bool_parser, "not a bool") |> Result.is_err 401 | -------------------------------------------------------------------------------- /package/Xml.roc: -------------------------------------------------------------------------------- 1 | ## # XML Parser 2 | ## Original author: [Johannes Maas](https://github.com/j-maas) 3 | ## 4 | ## Following the specification from https://www.w3.org/TR/2008/REC-xml-20081126/ 5 | module [ 6 | Xml, 7 | XmlDeclaration, 8 | XmlVersion, 9 | Node, 10 | Attribute, 11 | xml_parser, 12 | ] 13 | 14 | import Parser exposing [Parser, const, map, skip, keep, one_or_more, one_of, many, between, alt, chomp_while, flatten, lazy, chomp_until] 15 | import String exposing [parse_str, string, Utf8, digits, codeunit_satisfies] 16 | 17 | Xml : { 18 | xml_declaration : [Given XmlDeclaration, Missing], 19 | root : Node, 20 | } 21 | 22 | XmlDeclaration : { 23 | version : XmlVersion, 24 | encoding : [Given XmlEncoding, Missing], 25 | } 26 | 27 | XmlVersion := { 28 | after_dot : U8, 29 | } 30 | implements [Eq] 31 | 32 | v1_dot0 : XmlVersion 33 | v1_dot0 = @XmlVersion( 34 | { 35 | after_dot: 0, 36 | }, 37 | ) 38 | 39 | XmlEncoding : [ 40 | Utf8Encoding, 41 | OtherEncoding Str, 42 | ] 43 | 44 | Node : [ 45 | Element Str (List Attribute) (List Node), 46 | Text Str, 47 | ] 48 | 49 | Attribute : { name : Str, value : Str } 50 | 51 | expect 52 | # xml to be parsed 53 | result = parse_str(xml_parser, test_xml) 54 | 55 | result 56 | == Ok( 57 | { 58 | xml_declaration: Given( 59 | { 60 | version: v1_dot0, 61 | encoding: Given(Utf8Encoding), 62 | }, 63 | ), 64 | root: Element( 65 | "root", 66 | [], 67 | [ 68 | Text("\n "), 69 | Element( 70 | "element", 71 | [{ name: "arg", value: "value" }], 72 | [], 73 | ), 74 | Text("\n"), 75 | ], 76 | ), 77 | }, 78 | ) 79 | 80 | expect 81 | # XML with empty prolog to be parsed 82 | result = parse_str(xml_parser, "") 83 | 84 | result 85 | == Ok( 86 | { 87 | xml_declaration: Missing, 88 | root: Element("element", [], []), 89 | }, 90 | ) 91 | 92 | xml_parser : Parser Utf8 Xml 93 | xml_parser = 94 | const( 95 | |xml_declaration| 96 | |root| { 97 | xml_declaration, 98 | root, 99 | }, 100 | ) 101 | |> keep(p_prolog) 102 | |> keep(p_element) 103 | |> skip(many(p_whitespace)) 104 | 105 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-prolog 106 | p_prolog : Parser Utf8 [Given XmlDeclaration, Missing] 107 | p_prolog = 108 | const(|xml_declaration| |_misc| xml_declaration) 109 | |> keep((p_xml_declaration |> map(Given) |> maybe_with_default(Missing))) 110 | |> keep(p_many_misc) 111 | 112 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-XMLDecl 113 | p_xml_declaration : Parser Utf8 XmlDeclaration 114 | p_xml_declaration = 115 | ( 116 | const( 117 | |version| 118 | |encoding| { 119 | version, 120 | encoding, 121 | }, 122 | ) 123 | ) 124 | |> skip(string(" skip(one_or_more(p_whitespace)) 126 | |> keep(p_version) 127 | |> keep( 128 | ( 129 | ( 130 | const(|encoding| encoding) 131 | |> skip(one_or_more(p_whitespace)) 132 | |> keep(p_encoding_declaration) 133 | |> map(Given) 134 | ) 135 | |> maybe_with_default(Missing) 136 | ), 137 | ) 138 | |> skip(many(p_whitespace)) 139 | |> skip(string("?>")) 140 | 141 | expect 142 | # XML declaration to be parsed 143 | result = 144 | parse_str( 145 | p_xml_declaration, 146 | """ 147 | 148 | """, 149 | ) 150 | 151 | result 152 | == Ok( 153 | { 154 | version: v1_dot0, 155 | encoding: Given(Utf8Encoding), 156 | }, 157 | ) 158 | 159 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-VersionInfo 160 | p_version : Parser Utf8 XmlVersion 161 | p_version = 162 | between_quotes(p_version_number) 163 | |> p_attribute("version") 164 | 165 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-VersionNum 166 | p_version_number : Parser Utf8 XmlVersion 167 | p_version_number = 168 | const( 169 | |after_dot| 170 | @XmlVersion( 171 | { 172 | after_dot: after_dot |> Num.to_u8, 173 | }, 174 | ), 175 | ) 176 | |> skip(string("1.")) 177 | |> keep(digits) 178 | 179 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-EncodingDecl 180 | p_encoding_declaration : Parser Utf8 XmlEncoding 181 | p_encoding_declaration = 182 | between_quotes(p_encoding_name) 183 | |> p_attribute("encoding") 184 | 185 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-EncName 186 | p_encoding_name : Parser Utf8 XmlEncoding 187 | p_encoding_name = 188 | const( 189 | |first_char| 190 | |rest| 191 | combine_to_str(first_char, rest) 192 | |> Result.map_ok( 193 | |encoding_name| 194 | 195 | when encoding_name is 196 | "utf-8" -> Utf8Encoding 197 | other -> OtherEncoding(other), 198 | ), 199 | ) 200 | |> keep(codeunit_satisfies(is_alphabetical)) 201 | |> keep( 202 | chomp_while( 203 | |c| 204 | is_alphabetical(c) 205 | or is_digit(c) 206 | or (c == '-') 207 | or (c == '.') 208 | or (c == '_'), 209 | ), 210 | ) 211 | |> flatten 212 | 213 | expect 214 | # encoding name to be parsed 215 | result = parse_str(p_encoding_name, "utf-8") 216 | 217 | result == Ok(Utf8Encoding) 218 | 219 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-element 220 | p_element : Parser Utf8 Node 221 | p_element = 222 | const(|name| |arguments| |contents| Element(name, arguments, contents)) 223 | |> skip(string("<")) 224 | |> keep(p_name) 225 | |> keep( 226 | many( 227 | ( 228 | const(|attribute| attribute) 229 | |> skip(many(p_whitespace)) 230 | |> keep(p_element_attribute) 231 | ), 232 | ), 233 | ) 234 | |> skip(many(p_whitespace)) 235 | |> keep( 236 | ( 237 | empty_tag = 238 | string("/>") |> map(|_| []) 239 | tag_with_content = 240 | const(|contents| contents) 241 | |> skip(string(">")) 242 | |> keep(lazy(|_| p_element_contents)) 243 | |> skip(p_end_tag) 244 | # Due to https://github.com/lukewilliamboswell/roc-parser/issues/13 we cannot use `oneOf`, since we are using oneOf in `pElementContents`. 245 | alt( 246 | tag_with_content, 247 | empty_tag, 248 | ) 249 | ), 250 | ) 251 | 252 | expect 253 | # empty element tag without arguments to be parsed 254 | result = parse_str(p_element, "") 255 | 256 | result == Ok(Element("element", [], [])) 257 | 258 | expect 259 | # empty element tag without arguments and without whitespace to be parsed 260 | result = parse_str(p_element, "") 261 | 262 | result == Ok(Element("element", [], [])) 263 | 264 | expect 265 | # empty element tag with argument to be parsed 266 | result = parse_str( 267 | p_element, 268 | """ 269 | 270 | """, 271 | ) 272 | 273 | result == Ok(Element("element", [{ name: "arg", value: "value" }], [])) 274 | 275 | expect 276 | # empty element without arguments to be parsed 277 | result = parse_str(p_element, "") 278 | 279 | result == Ok(Element("element", [], [])) 280 | 281 | # TODO: reject mismatched tags for better debugging 282 | # expect 283 | # # mismatched end tag is rejected 284 | # result = parseStr pElement "" 285 | 286 | # when result is 287 | # Err (ParsingFailure _) -> Bool.true 288 | # _ -> Bool.false 289 | 290 | expect 291 | # element with multiple arguments and text content to be parsed 292 | result = parse_str( 293 | p_element, 294 | """ 295 | text content 296 | """, 297 | ) 298 | 299 | result 300 | == Ok( 301 | Element( 302 | "element", 303 | [ 304 | { name: "firstArg", value: "one" }, 305 | { name: "secondArg", value: "two" }, 306 | ], 307 | [Text("text content")], 308 | ), 309 | ) 310 | 311 | expect 312 | # content with CDATA sections to be parsed 313 | result = parse_str( 314 | p_element, 315 | "]]>", 316 | ) 317 | 318 | result 319 | == Ok( 320 | Element( 321 | "element", 322 | [], 323 | [Text("")], 324 | ), 325 | ) 326 | 327 | expect 328 | # CDATA section with partial CDATA section end tag to be parsed 329 | result = parse_str( 330 | p_element, 331 | " the end]]>", 332 | ) 333 | 334 | result 335 | == Ok( 336 | Element( 337 | "element", 338 | [], 339 | [Text("this is ]] not ]> the end")], 340 | ), 341 | ) 342 | 343 | expect 344 | # nested elements to be parsed 345 | result = parse_str( 346 | p_element, 347 | "", 348 | ) 349 | 350 | result == Ok(Element("parent", [], [Element("child", [], [])])) 351 | 352 | expect 353 | # nested element with arguments to be parsed 354 | result = parse_str( 355 | p_element, 356 | """ 357 | 358 | """, 359 | ) 360 | 361 | result 362 | == Ok( 363 | Element( 364 | "parent", 365 | [ 366 | { name: "argParent", value: "outer" }, 367 | ], 368 | [ 369 | Element( 370 | "child", 371 | [ 372 | { name: "argChild", value: "inner" }, 373 | ], 374 | [], 375 | ), 376 | ], 377 | ), 378 | ) 379 | 380 | expect 381 | # nested elements with whitespace to be parsed 382 | result = parse_str( 383 | p_element, 384 | """ 385 | 386 | 387 | 388 | """, 389 | ) 390 | 391 | result 392 | == Ok( 393 | Element( 394 | "parent", 395 | [], 396 | [ 397 | Text("\n "), 398 | Element("child", [], []), 399 | Text("\n"), 400 | ], 401 | ), 402 | ) 403 | 404 | expect 405 | # element with diverse children to be parsed 406 | result = parse_str( 407 | p_element, 408 | """ 409 | 410 | Atom Feed 411 | 412 | 2024-02-23T20:38:24Z 413 | 414 | """, 415 | ) 416 | 417 | result 418 | == Ok( 419 | Element( 420 | "feed", 421 | [{ name: "xmlns", value: "http://www.w3.org/2005/Atom" }], 422 | [ 423 | Text("\n "), 424 | Element("title", [], [Text("Atom Feed")]), 425 | Text("\n "), 426 | Element( 427 | "link", 428 | [ 429 | { name: "rel", value: "self" }, 430 | { name: "type", value: "application/atom+xml" }, 431 | { name: "href", value: "http://example.org" }, 432 | ], 433 | [], 434 | ), 435 | Text("\n "), 436 | Element( 437 | "updated", 438 | [], 439 | [ 440 | Text("2024-02-23T20:38:24Z"), 441 | ], 442 | ), 443 | Text("\n"), 444 | ], 445 | ), 446 | ) 447 | 448 | p_element_attribute : Parser Utf8 Attribute 449 | p_element_attribute = 450 | const( 451 | |name| 452 | |value| { 453 | name, 454 | value, 455 | }, 456 | ) 457 | |> keep(p_name) 458 | |> skip(p_equal) 459 | |> keep( 460 | one_of( 461 | [ 462 | p_attribute_value('"') |> between(string("\""), string("\"")), 463 | p_attribute_value('\'') |> between(string("'"), string("'")), 464 | ], 465 | ), 466 | ) 467 | 468 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue 469 | p_attribute_value : U8 -> Parser Utf8 Str 470 | p_attribute_value = |quote| 471 | chomp_while(|c| c != quote) 472 | |> map(|chomped| str_from_utf8(chomped)) 473 | |> flatten 474 | # TODO: Implement reference values 475 | 476 | p_element_contents : Parser Utf8 (List Node) 477 | p_element_contents = 478 | many( 479 | one_of( 480 | [ 481 | p_character_data, 482 | p_element, 483 | p_cdata_section, 484 | ], 485 | ), 486 | ) 487 | 488 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-ETag 489 | p_end_tag : Parser Utf8 Str 490 | p_end_tag = 491 | const(|name| name) 492 | |> skip(string(" keep(p_name) 494 | |> skip(many(p_whitespace)) 495 | |> skip(string(">")) 496 | 497 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-CharData 498 | p_character_data : Parser Utf8 Node 499 | p_character_data = 500 | const(|first| |chars| combine_to_str(first, chars)) 501 | |> keep(codeunit_satisfies(is_character_data)) 502 | |> keep(chomp_while(is_character_data)) 503 | |> flatten 504 | |> map(Text) 505 | # TODO: Reject CDATA section close delimiter 506 | 507 | is_character_data : U8 -> Bool 508 | is_character_data = |c| 509 | (c != '<') 510 | and (c != '&') 511 | 512 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-CDSect 513 | p_cdata_section : Parser Utf8 Node 514 | p_cdata_section = 515 | ( 516 | const(|text| text) 517 | |> skip(string(" keep(p_cdata_section_content) 519 | ) 520 | |> map(Text) 521 | 522 | p_cdata_section_content : Parser Utf8 Str 523 | p_cdata_section_content = 524 | const(|first| |rest| Str.concat(first, rest)) 525 | |> keep((chomp_until(']') |> map(str_from_utf8) |> flatten)) 526 | |> skip(string("]")) 527 | |> keep( 528 | one_of( 529 | [ 530 | string("]>") |> map(|_| ""), 531 | lazy(|_| p_cdata_section_content |> map(|rest| Str.concat("]", rest))), 532 | ], 533 | ), 534 | ) 535 | 536 | p_name : Parser Utf8 Str 537 | p_name = 538 | const( 539 | |first_char| 540 | |rest| 541 | combine_to_str(first_char, rest), 542 | ) 543 | |> keep(codeunit_satisfies(is_name_start_char)) 544 | |> keep(chomp_while(is_name_char)) 545 | |> flatten 546 | 547 | is_name_start_char : U8 -> Bool 548 | is_name_start_char = |c| 549 | is_alphabetical(c) 550 | or (c == ':') 551 | or (c == '_') 552 | # TODO: Implement missing character groups 553 | 554 | is_name_char : U8 -> Bool 555 | is_name_char = |c| 556 | is_name_start_char(c) 557 | or (c == '-') 558 | or (c == '.') 559 | 560 | combine_to_str : U8, List U8 -> Result Str Str 561 | combine_to_str = |first, rest| 562 | rest 563 | |> List.prepend(first) 564 | |> str_from_utf8 565 | 566 | str_from_utf8 : List U8 -> Result Str Str 567 | str_from_utf8 = |chars| 568 | Str.from_utf8(chars) 569 | |> Result.map_err(|_| "Error decoding UTF8") 570 | 571 | XmlMisc : List [Comment, ProcessingInstruction] 572 | 573 | p_many_misc : Parser Utf8 XmlMisc 574 | p_many_misc = 575 | # TODO: Implement comment and processing instructions 576 | many(p_whitespace) 577 | |> map(|_| []) 578 | 579 | p_attribute : Parser Utf8 output, Str -> Parser Utf8 output 580 | p_attribute = |parser, attribute_name| 581 | const(|result| result) 582 | |> skip(string(attribute_name)) 583 | |> skip(p_equal) 584 | |> keep(parser) 585 | 586 | # See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Eq 587 | p_equal : Parser Utf8 Str 588 | p_equal = 589 | many(p_whitespace) 590 | |> skip(string("=")) 591 | |> skip(many(p_whitespace)) 592 | |> map(|strings| strings |> Str.join_with("")) 593 | 594 | between_quotes : Parser Utf8 a -> Parser Utf8 a 595 | between_quotes = |parser| 596 | one_of( 597 | [ 598 | parser |> between(string("\""), string("\"")), 599 | parser |> between(string("'"), string("'")), 600 | ], 601 | ) 602 | 603 | maybe_with_default : Parser input output, output -> Parser input output 604 | maybe_with_default = |parser, default| 605 | alt(parser, const(default)) 606 | 607 | p_whitespace : Parser Utf8 Str 608 | p_whitespace = 609 | one_of( 610 | [ 611 | string("\u(20)"), 612 | string("\u(9)"), 613 | string("\u(D)"), 614 | string("\u(A)"), 615 | ], 616 | ) 617 | 618 | is_alphabetical : U8 -> Bool 619 | is_alphabetical = |c| 620 | (c >= 'A' and c <= 'Z') 621 | or (c >= 'a' and c <= 'z') 622 | 623 | is_digit : U8 -> Bool 624 | is_digit = |c| 625 | c >= '0' and c <= '9' 626 | 627 | test_xml = 628 | """ 629 | 630 | 631 | 632 | 633 | """ 634 | 635 | trailing_whitespace_xml = 636 | """ 637 | 638 | 639 | 640 | """ 641 | 642 | expect 643 | # ignore trailing newline 644 | result : Result Xml _ 645 | result = parse_str(xml_parser, trailing_whitespace_xml) 646 | 647 | expected : Xml 648 | expected = { 649 | xml_declaration: Given( 650 | { 651 | version: v1_dot0, 652 | encoding: Given(OtherEncoding("UTF-8")), 653 | }, 654 | ), 655 | root: Element( 656 | "root", 657 | [], 658 | [ 659 | Element("Example", [], []), 660 | ], 661 | ), 662 | } 663 | 664 | result == Ok(expected) 665 | -------------------------------------------------------------------------------- /package/main.roc: -------------------------------------------------------------------------------- 1 | package [ 2 | Parser, 3 | String, 4 | CSV, 5 | HTTP, 6 | Markdown, 7 | Xml, 8 | ] {} 9 | -------------------------------------------------------------------------------- /www/0.10.0/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | - Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 65 |
66 |
67 | 88 | 91 | 93 | 94 | 97 | 98 | 106 | 107 | 108 | 172 |
173 |

Exposed Modules

LLM docs
174 |
175 |

Made by people who like to make nice things.

176 |
177 |
178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /www/0.10.0/llms.txt: -------------------------------------------------------------------------------- 1 | # LLM Prompt for Documentation 2 | 3 | ## Documentation 4 | 5 | ### Parser 6 | 7 | #### Parser 8 | 9 | **Type Annotation** 10 | 11 | **Description** 12 | 13 | Opaque type for a parser that will try to parse an `a` from an `input`. 14 | 15 | As such, a parser can be considered a recipe for a function of the type 16 | ```roc 17 | input -> Result {val: a, input: input} [ParsingFailure Str] 18 | ``` 19 | 20 | How a parser is _actually_ implemented internally is not important 21 | and this might change between versions; 22 | for instance to improve efficiency or error messages on parsing failures. 23 | 24 | #### ParseResult 25 | 26 | **Type Annotation** 27 | 28 | **Description** 29 | 30 | ```roc 31 | ParseResult input a : Result { val : a, input : input } [ParsingFailure Str] 32 | ``` 33 | 34 | #### build_primitive_parser 35 | 36 | **Type Annotation** 37 | 38 | ```roc 39 | (input -> ParseResult input a) -> Parser input a 40 | ``` 41 | 42 | **Description** 43 | 44 | Write a custom parser without using provided combintors. 45 | 46 | #### parse_partial 47 | 48 | **Type Annotation** 49 | 50 | ```roc 51 | Parser input a, input -> ParseResult input a 52 | ``` 53 | 54 | **Description** 55 | 56 | Most general way of running a parser. 57 | 58 | Can be thought of as turning the recipe of a parser into its actual parsing function 59 | and running this function on the given input. 60 | 61 | Moat parsers consume part of `input` when they succeed. This allows you to string parsers 62 | together that run one after the other. The part of the input that the first 63 | parser did not consume, is used by the next parser. 64 | This is why a parser returns on success both the resulting value and the leftover part of the input. 65 | 66 | This is mostly useful when creating your own internal parsing building blocks. 67 | 68 | #### parse 69 | 70 | **Type Annotation** 71 | 72 | ```roc 73 | 74 | Parser input a, 75 | input, 76 | (input -> Bool) 77 | -> Result a 78 | [ 79 | ParsingFailure Str, 80 | ParsingIncomplete input 81 | ] 82 | ``` 83 | 84 | **Description** 85 | 86 | Runs a parser on the given input, expecting it to fully consume the input 87 | 88 | The `input -> Bool` parameter is used to check whether parsing has 'completed', 89 | i.e. how to determine if all of the input has been consumed. 90 | 91 | For most input types, a parsing run that leaves some unparsed input behind 92 | should be considered an error. 93 | 94 | #### fail 95 | 96 | **Type Annotation** 97 | 98 | ```roc 99 | Str -> Parser * * 100 | ``` 101 | 102 | **Description** 103 | 104 | Parser that can never succeed, regardless of the given input. 105 | It will always fail with the given error message. 106 | 107 | This is mostly useful as a 'base case' if all other parsers 108 | in a `oneOf` or `alt` have failed, to provide some more descriptive error message. 109 | 110 | #### const 111 | 112 | **Type Annotation** 113 | 114 | ```roc 115 | a -> Parser * a 116 | ``` 117 | 118 | **Description** 119 | 120 | Parser that will always produce the given `a`, without looking at the actual input. 121 | This is useful as a basic building block, especially in combination with 122 | `map` and `apply`. 123 | ```roc 124 | parse_u32 : Parser (List U8) U32 125 | parse_u32 = 126 | const(Num.to_u32) 127 | |> keep(digits) 128 | 129 | expect parse_str(parse_u32, "123") == Ok(123u32) 130 | ``` 131 | 132 | #### alt 133 | 134 | **Type Annotation** 135 | 136 | ```roc 137 | Parser input a, Parser input a -> Parser input a 138 | ``` 139 | 140 | **Description** 141 | 142 | Try the `first` parser and (only) if it fails, try the `second` parser as fallback. 143 | 144 | #### apply 145 | 146 | **Type Annotation** 147 | 148 | ```roc 149 | Parser input (a -> b), Parser input a -> Parser input b 150 | ``` 151 | 152 | **Description** 153 | 154 | Runs a parser building a function, then a parser building a value, 155 | and finally returns the result of calling the function with the value. 156 | 157 | This is useful if you are building up a structure that requires more parameters 158 | than there are variants of `map`, `map2`, `map3` etc. for. 159 | 160 | For instance, the following two are the same: 161 | ```roc 162 | const(\x, y, z -> Triple(x, y, z)) 163 | |> map3(String.digits, String.digits, String.digits) 164 | 165 | const(\x -> \y -> \z -> Triple(x, y, z)) 166 | |> apply(String.digits) 167 | |> apply(String.digits) 168 | |> apply(String.digits) 169 | ``` 170 | Indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood. 171 | 172 | # Currying 173 | Be aware that when using `apply`, you need to explicitly 'curry' the parameters to the construction function. 174 | This means that instead of writing `\x, y, z -> ...` 175 | you'll need to write `\x -> \y -> \z -> ...`. 176 | This is because the parameters of the function will be applied one by one as parsing continues. 177 | 178 | #### one_of 179 | 180 | **Type Annotation** 181 | 182 | ```roc 183 | List (Parser input a) -> Parser input a 184 | ``` 185 | 186 | **Description** 187 | 188 | Try a list of parsers in turn, until one of them succeeds. 189 | ```roc 190 | color : Parser Utf8 [Red, Green, Blue] 191 | color = 192 | one_of( 193 | [ 194 | const(Red) |> skip(string("red")), 195 | const(Green) |> skip(string("green")), 196 | const(Blue) |> skip(string("blue")), 197 | ], 198 | ) 199 | 200 | expect parse_str(color, "green") == Ok(Green) 201 | ``` 202 | 203 | #### map 204 | 205 | **Type Annotation** 206 | 207 | ```roc 208 | Parser input a, (a -> b) -> Parser input b 209 | ``` 210 | 211 | **Description** 212 | 213 | Transforms the result of parsing into something else, 214 | using the given transformation function. 215 | 216 | #### map2 217 | 218 | **Type Annotation** 219 | 220 | ```roc 221 | 222 | Parser input a, 223 | Parser input b, 224 | (a, b -> c) 225 | -> Parser input c 226 | ``` 227 | 228 | **Description** 229 | 230 | Transforms the result of parsing into something else, 231 | using the given two-parameter transformation function. 232 | 233 | #### map3 234 | 235 | **Type Annotation** 236 | 237 | ```roc 238 | 239 | Parser input a, 240 | Parser input b, 241 | Parser input c, 242 | (a, 243 | b, 244 | c 245 | -> d) 246 | -> Parser input d 247 | ``` 248 | 249 | **Description** 250 | 251 | Transforms the result of parsing into something else, 252 | using the given three-parameter transformation function. 253 | 254 | If you need transformations with more inputs, 255 | take a look at `apply`. 256 | 257 | #### flatten 258 | 259 | **Type Annotation** 260 | 261 | ```roc 262 | Parser input (Result a Str) -> Parser input a 263 | ``` 264 | 265 | **Description** 266 | 267 | Removes a layer of `Result` from running the parser. 268 | 269 | Use this to map functions that return a result over the parser, 270 | where errors are turned into `ParsingFailure`s. 271 | 272 | ```roc 273 | # Parse a number from a List U8 274 | u64 : Parser Utf8 U64 275 | u64 = 276 | string 277 | |> map( 278 | \val -> 279 | when Str.to_u64(val) is 280 | Ok(num) -> Ok(num) 281 | Err(_) -> Err("${val} is not a U64."), 282 | ) 283 | |> flatten 284 | ``` 285 | 286 | #### lazy 287 | 288 | **Type Annotation** 289 | 290 | ```roc 291 | ({} -> Parser input a) -> Parser input a 292 | ``` 293 | 294 | **Description** 295 | 296 | Runs a parser lazily 297 | 298 | This is (only) useful when dealing with a recursive structure. 299 | For instance, consider a type `Comment : { message: String, responses: List Comment }`. 300 | Without `lazy`, you would ask the compiler to build an infinitely deep parser. 301 | (Resulting in a compiler error.) 302 | 303 | 304 | #### maybe 305 | 306 | **Type Annotation** 307 | 308 | ```roc 309 | Parser input a -> Parser input (Result a [Nothing]) 310 | ``` 311 | 312 | #### many 313 | 314 | **Type Annotation** 315 | 316 | ```roc 317 | Parser input a -> Parser input (List a) 318 | ``` 319 | 320 | **Description** 321 | 322 | A parser which runs the element parser *zero* or more times on the input, 323 | returning a list containing all the parsed elements. 324 | 325 | #### one_or_more 326 | 327 | **Type Annotation** 328 | 329 | ```roc 330 | Parser input a -> Parser input (List a) 331 | ``` 332 | 333 | **Description** 334 | 335 | A parser which runs the element parser *one* or more times on the input, 336 | returning a list containing all the parsed elements. 337 | 338 | Also see [Parser.many]. 339 | 340 | #### between 341 | 342 | **Type Annotation** 343 | 344 | ```roc 345 | 346 | Parser input a, 347 | Parser input open, 348 | Parser input close 349 | -> Parser input a 350 | ``` 351 | 352 | **Description** 353 | 354 | Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter, 355 | and only returns the result of your main parser. 356 | 357 | Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.) 358 | 359 | ```roc 360 | between_braces = \parser -> parser |> between(scalar('['), scalar(']')) 361 | ``` 362 | 363 | #### sep_by1 364 | 365 | **Type Annotation** 366 | 367 | ```roc 368 | Parser input a, Parser input sep -> Parser input (List a) 369 | ``` 370 | 371 | #### sep_by 372 | 373 | **Type Annotation** 374 | 375 | ```roc 376 | Parser input a, Parser input sep -> Parser input (List a) 377 | ``` 378 | 379 | **Description** 380 | 381 | ```roc 382 | parse_numbers : Parser (List U8) (List U64) 383 | parse_numbers = 384 | digits |> sep_by(codeunit(',')) 385 | 386 | expect parse_str(parse_numbers, "1,2,3") == Ok([1, 2, 3]) 387 | ``` 388 | 389 | #### ignore 390 | 391 | **Type Annotation** 392 | 393 | ```roc 394 | Parser input a -> Parser input {} 395 | ``` 396 | 397 | #### keep 398 | 399 | **Type Annotation** 400 | 401 | ```roc 402 | Parser input (a -> b), Parser input a -> Parser input b 403 | ``` 404 | 405 | #### skip 406 | 407 | **Type Annotation** 408 | 409 | ```roc 410 | Parser input a, Parser input * -> Parser input a 411 | ``` 412 | 413 | #### chomp_until 414 | 415 | **Type Annotation** 416 | 417 | ```roc 418 | a -> Parser (List a) (List a) 419 | where a implements Eq 420 | ``` 421 | 422 | **Description** 423 | 424 | Match zero or more codeunits until the it reaches the given codeunit. 425 | The given codeunit is not included in the match. 426 | 427 | This can be used with [Parser.skip] to ignore text. 428 | 429 | ```roc 430 | ignore_text : Parser (List U8) U64 431 | ignore_text = 432 | const(\d -> d) 433 | |> skip(chomp_until(':')) 434 | |> skip(codeunit(':')) 435 | |> keep(digits) 436 | 437 | expect parse_str(ignore_text, "ignore preceding text:123") == Ok(123) 438 | ``` 439 | 440 | This can be used with [Parser.keep] to capture a list of `U8` codeunits. 441 | 442 | ```roc 443 | capture_text : Parser (List U8) (List U8) 444 | capture_text = 445 | const(\codeunits -> codeunits) 446 | |> keep(chomp_until(':')) 447 | |> skip(codeunit(':')) 448 | 449 | expect parse_str(capture_text, "Roc:") == Ok(['R', 'o', 'c']) 450 | ``` 451 | 452 | Use [String.str_from_utf8] to turn the results into a `Str`. 453 | 454 | Also see [Parser.chomp_while]. 455 | 456 | #### chomp_while 457 | 458 | **Type Annotation** 459 | 460 | ```roc 461 | (a -> Bool) -> Parser (List a) (List a) 462 | where a implements Eq 463 | ``` 464 | 465 | **Description** 466 | 467 | Match zero or more codeunits until the check returns false. 468 | The codeunit that returned false is not included in the match. 469 | Note: a chompWhile parser always succeeds! 470 | 471 | This can be used with [Parser.skip] to ignore text. 472 | This is useful for chomping whitespace or variable names. 473 | 474 | ``` 475 | ignore_numbers : Parser (List U8) Str 476 | ignore_numbers = 477 | const(\str -> str) 478 | |> skip(chomp_while(\b -> b >= '0' && b <= '9')) 479 | |> keep(string("TEXT")) 480 | 481 | expect parse_str(ignore_numbers, "0123456789876543210TEXT") == Ok("TEXT") 482 | ``` 483 | 484 | This can be used with [Parser.keep] to capture a list of `U8` codeunits. 485 | 486 | ``` 487 | capture_numbers : Parser (List U8) (List U8) 488 | capture_numbers = 489 | const(\codeunits -> codeunits) 490 | |> keep(chomp_while(\b -> b >= '0' && b <= '9')) 491 | |> skip(string("TEXT")) 492 | 493 | expect parse_str(capture_numbers, "123TEXT") == Ok(['1', '2', '3']) 494 | ``` 495 | 496 | Use [String.str_from_utf8] to turn the results into a `Str`. 497 | 498 | Also see [Parser.chomp_until]. 499 | 500 | ### String 501 | 502 | #### Utf8 503 | 504 | **Type Annotation** 505 | 506 | **Description** 507 | 508 | ``` 509 | Utf8 : List U8 510 | ``` 511 | 512 | #### parse_str 513 | 514 | **Type Annotation** 515 | 516 | ```roc 517 | 518 | Parser Utf8 a, 519 | Str 520 | -> Result a 521 | [ 522 | ParsingFailure Str, 523 | ParsingIncomplete Str 524 | ] 525 | ``` 526 | 527 | **Description** 528 | 529 | Parse a [Str] using a [Parser] 530 | ```roc 531 | color : Parser Utf8 [Red, Green, Blue] 532 | color = 533 | one_of( 534 | [ 535 | Parser.const(Red) |> Parser.skip(string("red")), 536 | Parser.const(Green) |> Parser.skip(string("green")), 537 | Parser.const(Blue) |> Parser.skip(string("blue")), 538 | ], 539 | ) 540 | 541 | expect parse_str(color, "green") == Ok(Green) 542 | ``` 543 | 544 | #### parse_str_partial 545 | 546 | **Type Annotation** 547 | 548 | ```roc 549 | Parser Utf8 a, Str -> Parser.ParseResult Str a 550 | ``` 551 | 552 | **Description** 553 | 554 | Runs a parser against the start of a string, allowing the parser to consume it only partially. 555 | 556 | - If the parser succeeds, returns the resulting value as well as the leftover input. 557 | - If the parser fails, returns `Err (ParsingFailure msg)` 558 | 559 | ```roc 560 | at_sign : Parser Utf8 [AtSign] 561 | at_sign = Parser.const(AtSign) |> Parser.skip(codeunit('@')) 562 | 563 | expect parse_str(at_sign, "@") == Ok(AtSign) 564 | expect parse_str_partial(at_sign, "@") |> Result.map_ok(.val) == Ok(AtSign) 565 | expect parse_str_partial(at_sign, "$") |> Result.is_err 566 | ``` 567 | 568 | #### parse_utf8 569 | 570 | **Type Annotation** 571 | 572 | ```roc 573 | 574 | Parser Utf8 a, 575 | Utf8 576 | -> Result a 577 | [ 578 | ParsingFailure Str, 579 | ParsingIncomplete Utf8 580 | ] 581 | ``` 582 | 583 | **Description** 584 | 585 | Runs a parser against a string, requiring the parser to consume it fully. 586 | 587 | - If the parser succeeds, returns `Ok a` 588 | - If the parser fails, returns `Err (ParsingFailure Str)` 589 | - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete (List U8))` 590 | 591 | 592 | #### parse_utf8_partial 593 | 594 | **Type Annotation** 595 | 596 | ```roc 597 | Parser Utf8 a, Utf8 -> Parser.ParseResult Utf8 a 598 | ``` 599 | 600 | **Description** 601 | 602 | Runs a parser against the start of a list of scalars, allowing the parser to consume it only partially. 603 | 604 | #### codeunit_satisfies 605 | 606 | **Type Annotation** 607 | 608 | ```roc 609 | (U8 -> Bool) -> Parser Utf8 U8 610 | ``` 611 | 612 | **Description** 613 | 614 | ```roc 615 | is_digit : U8 -> Bool 616 | is_digit = \b -> b >= '0' && b <= '9' 617 | 618 | expect parse_str(codeunit_satisfies(is_digit), "0") == Ok('0') 619 | expect parse_str(codeunit_satisfies(is_digit), "*") |> Result.is_err 620 | ``` 621 | 622 | #### codeunit 623 | 624 | **Type Annotation** 625 | 626 | ```roc 627 | U8 -> Parser Utf8 U8 628 | ``` 629 | 630 | **Description** 631 | 632 | ```roc 633 | at_sign : Parser Utf8 [AtSign] 634 | at_sign = Parser.const(AtSign) |> Parser.skip(codeunit('@')) 635 | 636 | expect parse_str(at_sign, "@") == Ok(AtSign) 637 | expect Result.is_err(parse_str_partial(at_sign, "$")) 638 | ``` 639 | 640 | #### utf8 641 | 642 | **Type Annotation** 643 | 644 | ```roc 645 | List U8 -> Parser Utf8 (List U8) 646 | ``` 647 | 648 | **Description** 649 | 650 | Parse an extact sequence of utf8 651 | 652 | #### string 653 | 654 | **Type Annotation** 655 | 656 | ```roc 657 | Str -> Parser Utf8 Str 658 | ``` 659 | 660 | **Description** 661 | 662 | Parse the given [Str] 663 | ```roc 664 | expect parse_str(string("Foo"), "Foo") == Ok("Foo") 665 | expect Result.is_err(parse_str(string("Foo"), "Bar")) 666 | ``` 667 | 668 | #### any_codeunit 669 | 670 | **Type Annotation** 671 | 672 | ```roc 673 | Parser Utf8 U8 674 | ``` 675 | 676 | **Description** 677 | 678 | Matches any [U8] codeunit 679 | ```roc 680 | expect parse_str(any_codeunit, "a") == Ok('a') 681 | expect parse_str(any_codeunit, "$") == Ok('$') 682 | ``` 683 | 684 | #### any_thing 685 | 686 | **Type Annotation** 687 | 688 | ```roc 689 | Parser Utf8 Utf8 690 | ``` 691 | 692 | **Description** 693 | 694 | Matches any [Utf8] and consumes all the input without fail. 695 | ```roc 696 | expect 697 | bytes = Str.to_utf8("consumes all the input") 698 | Parser.parse(any_thing, bytes, List.is_empty) == Ok(bytes) 699 | ``` 700 | 701 | #### any_string 702 | 703 | **Type Annotation** 704 | 705 | ```roc 706 | Parser Utf8 Str 707 | ``` 708 | 709 | #### digit 710 | 711 | **Type Annotation** 712 | 713 | ```roc 714 | Parser Utf8 U64 715 | ``` 716 | 717 | **Description** 718 | 719 | ```roc 720 | expect parse_str(digit, "0") == Ok(0) 721 | expect Result.is_err(parse_str(digit, "not a digit")) 722 | ``` 723 | 724 | #### digits 725 | 726 | **Type Annotation** 727 | 728 | ```roc 729 | Parser Utf8 U64 730 | ``` 731 | 732 | **Description** 733 | 734 | Parse a sequence of digits into a [U64], accepting leading zeroes 735 | ```roc 736 | expect parse_str(digits, "0123") == Ok(123) 737 | expect Result.is_err(parse_str(digits, "not a digit")) 738 | ``` 739 | 740 | #### one_of 741 | 742 | **Type Annotation** 743 | 744 | ```roc 745 | List (Parser Utf8 a) -> Parser Utf8 a 746 | ``` 747 | 748 | **Description** 749 | 750 | Try a bunch of different parsers. 751 | 752 | The first parser which is tried is the one at the front of the list, 753 | and the next one is tried until one succeeds or the end of the list was reached. 754 | ```roc 755 | bool_parser : Parser Utf8 Bool 756 | bool_parser = 757 | one_of([string("true"), string("false")]) 758 | |> Parser.map(\x -> if x == "true" then Bool.true else Bool.false) 759 | 760 | expect parse_str(bool_parser, "true") == Ok(Bool.true) 761 | expect parse_str(bool_parser, "false") == Ok(Bool.false) 762 | expect Result.is_err(parse_str(bool_parser, "not a bool")) 763 | ``` 764 | 765 | #### str_from_utf8 766 | 767 | **Type Annotation** 768 | 769 | ```roc 770 | Utf8 -> Str 771 | ``` 772 | 773 | #### str_from_ascii 774 | 775 | **Type Annotation** 776 | 777 | ```roc 778 | U8 -> Str 779 | ``` 780 | 781 | ### CSV 782 | 783 | #### CSV 784 | 785 | **Type Annotation** 786 | 787 | **Description** 788 | 789 | This is a CSV parser which follows RFC4180 790 | 791 | For simplicity's sake, the following things are not yet supported: 792 | - CSV files with headings 793 | 794 | The following however *is* supported 795 | - A simple LF ("\n") instead of CRLF ("\r\n") to separate records. 796 | 797 | #### CSVRecord 798 | 799 | **Type Annotation** 800 | 801 | #### parse_str 802 | 803 | **Type Annotation** 804 | 805 | ```roc 806 | 807 | Parser CSVRecord a, 808 | Str 809 | -> Result (List a) 810 | [ 811 | ParsingFailure Str, 812 | SyntaxError Str, 813 | ParsingIncomplete CSVRecord 814 | ] 815 | ``` 816 | 817 | **Description** 818 | 819 | Attempts to Parser.parse an `a` from a `Str` that is encoded in CSV format. 820 | 821 | #### parse_csv 822 | 823 | **Type Annotation** 824 | 825 | ```roc 826 | 827 | Parser CSVRecord a, 828 | CSV 829 | -> Result (List a) 830 | [ 831 | ParsingFailure Str, 832 | ParsingIncomplete CSVRecord 833 | ] 834 | ``` 835 | 836 | **Description** 837 | 838 | Attempts to Parser.parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields). 839 | 840 | #### record 841 | 842 | **Type Annotation** 843 | 844 | ```roc 845 | a -> Parser CSVRecord a 846 | ``` 847 | 848 | **Description** 849 | 850 | Wrapper function to combine a set of fields into your desired `a` 851 | 852 | ```roc 853 | record(\first_name -> \last_name -> \age -> User({ first_name, last_name, age })) 854 | |> field(string) 855 | |> field(string) 856 | |> field(u64) 857 | ``` 858 | 859 | #### field 860 | 861 | **Type Annotation** 862 | 863 | ```roc 864 | Parser String.Utf8 a -> Parser CSVRecord a 865 | ``` 866 | 867 | **Description** 868 | 869 | Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`. 870 | 871 | #### string 872 | 873 | **Type Annotation** 874 | 875 | ```roc 876 | Parser CSVField Str 877 | ``` 878 | 879 | **Description** 880 | 881 | Parser for a field containing a UTF8-encoded string 882 | 883 | #### u64 884 | 885 | **Type Annotation** 886 | 887 | ```roc 888 | Parser CSVField U64 889 | ``` 890 | 891 | **Description** 892 | 893 | Parse a number from a CSV field 894 | 895 | #### f64 896 | 897 | **Type Annotation** 898 | 899 | ```roc 900 | Parser CSVField F64 901 | ``` 902 | 903 | **Description** 904 | 905 | Parse a 64-bit float from a CSV field 906 | 907 | #### parse_str_to_csv_record 908 | 909 | **Type Annotation** 910 | 911 | ```roc 912 | 913 | Str 914 | -> Result CSVRecord 915 | [ 916 | ParsingFailure Str, 917 | ParsingIncomplete String.Utf8 918 | ] 919 | ``` 920 | 921 | **Description** 922 | 923 | Attempts to Parser.parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields). 924 | 925 | #### file 926 | 927 | **Type Annotation** 928 | 929 | ```roc 930 | Parser String.Utf8 CSV 931 | ``` 932 | 933 | ### HTTP 934 | 935 | #### Request 936 | 937 | **Type Annotation** 938 | 939 | #### Response 940 | 941 | **Type Annotation** 942 | 943 | #### request 944 | 945 | **Type Annotation** 946 | 947 | ```roc 948 | Parser String.Utf8 Request 949 | ``` 950 | 951 | #### response 952 | 953 | **Type Annotation** 954 | 955 | ```roc 956 | Parser String.Utf8 Response 957 | ``` 958 | 959 | ### Markdown 960 | 961 | #### Markdown 962 | 963 | **Type Annotation** 964 | 965 | **Description** 966 | 967 | Content values 968 | 969 | #### all 970 | 971 | **Type Annotation** 972 | 973 | ```roc 974 | Parser String.Utf8 (List Markdown) 975 | ``` 976 | 977 | #### heading 978 | 979 | **Type Annotation** 980 | 981 | ```roc 982 | Parser String.Utf8 Markdown 983 | ``` 984 | 985 | **Description** 986 | 987 | Headings 988 | 989 | ``` 990 | expect String.parse_str(heading, "# Foo Bar") == Ok(Heading One "Foo Bar") 991 | expect String.parse_str(heading, "Foo Bar\n---") == Ok(Heading Two "Foo Bar") 992 | ``` 993 | 994 | #### link 995 | 996 | **Type Annotation** 997 | 998 | ```roc 999 | Parser String.Utf8 Markdown 1000 | ``` 1001 | 1002 | **Description** 1003 | 1004 | Links 1005 | 1006 | ```roc 1007 | expect String.parse_str(link, "[roc](https://roc-lang.org)") == Ok(Link("roc", "https://roc-lang.org")) 1008 | ``` 1009 | 1010 | #### image 1011 | 1012 | **Type Annotation** 1013 | 1014 | ```roc 1015 | Parser String.Utf8 Markdown 1016 | ``` 1017 | 1018 | **Description** 1019 | 1020 | Images 1021 | 1022 | ```roc 1023 | expect String.parse_str(image, "![alt text](/images/logo.png)") == Ok(Image("alt text", "/images/logo.png")) 1024 | ``` 1025 | 1026 | #### code 1027 | 1028 | **Type Annotation** 1029 | 1030 | ```roc 1031 | Parser String.Utf8 Markdown 1032 | ``` 1033 | 1034 | **Description** 1035 | 1036 | Parse code blocks using triple backticks 1037 | supports block extension e.g. ```roc 1038 | 1039 | ```roc 1040 | expect 1041 | text = 1042 | """ 1043 | ```roc 1044 | # some code 1045 | foo = bar 1046 | ``` 1047 | """ 1048 | 1049 | a = String.parse_str(code, text) 1050 | a == Ok(Code({ ext: "roc", pre: "# some code\nfoo = bar\n" })) 1051 | ``` 1052 | 1053 | ### Xml 1054 | 1055 | #### Xml 1056 | 1057 | **Type Annotation** 1058 | 1059 | #### XmlDeclaration 1060 | 1061 | **Type Annotation** 1062 | 1063 | #### XmlVersion 1064 | 1065 | **Type Annotation** 1066 | 1067 | #### Node 1068 | 1069 | **Type Annotation** 1070 | 1071 | #### Attribute 1072 | 1073 | **Type Annotation** 1074 | 1075 | #### xml_parser 1076 | 1077 | **Type Annotation** 1078 | 1079 | ```roc 1080 | Parser Utf8 Xml 1081 | ``` 1082 | 1083 | -------------------------------------------------------------------------------- /www/0.10.0/search.js: -------------------------------------------------------------------------------- 1 | const toggleSidebarEntryActive = (moduleName) => { 2 | let sidebar = document.getElementById("sidebar-nav"); 3 | 4 | if (sidebar != null) { 5 | // Un-hide everything 6 | sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => { 7 | let entryName = entry.querySelector(".sidebar-module-link").dataset 8 | .moduleName; 9 | if (moduleName === entryName) { 10 | entry.firstChild.classList.toggle("active"); 11 | } 12 | }); 13 | } 14 | }; 15 | 16 | const setupSidebarNav = () => { 17 | // Re-hide all the sub-entries except for those of the current module 18 | let currentModuleName = document.querySelector(".module-name").textContent; 19 | toggleSidebarEntryActive(currentModuleName); 20 | 21 | document.querySelectorAll(".entry-toggle").forEach((el) => { 22 | el.addEventListener("click", (e) => { 23 | e.preventDefault(); 24 | e.stopImmediatePropagation(); 25 | const moduleName = e.target.parentElement.dataset.moduleName; 26 | toggleSidebarEntryActive(moduleName); 27 | }); 28 | }); 29 | }; 30 | 31 | const setupSearch = () => { 32 | let searchTypeAhead = document.getElementById("search-type-ahead"); 33 | let searchBox = document.getElementById("module-search"); 34 | let searchForm = document.getElementById("module-search-form"); 35 | let topSearchResultListItem = undefined; 36 | 37 | // Hide the results whenever anyone clicks outside the search results, 38 | // or on a specific search result. 39 | window.addEventListener("click", function (event) { 40 | if (!searchForm?.contains(event.target) || event.target.closest("#search-type-ahead a")) { 41 | searchTypeAhead.classList.add("hidden"); 42 | } 43 | }); 44 | 45 | if (searchBox != null) { 46 | function searchKeyDown(event) { 47 | switch (event.key) { 48 | case "ArrowDown": { 49 | event.preventDefault(); 50 | 51 | const focused = document.querySelector( 52 | "#search-type-ahead > li:not([class*='hidden']) > a:focus", 53 | ); 54 | 55 | // Find the next element to focus. 56 | let nextToFocus = focused?.parentElement?.nextElementSibling; 57 | 58 | while ( 59 | nextToFocus != null && 60 | nextToFocus.classList.contains("hidden") 61 | ) { 62 | nextToFocus = nextToFocus.nextElementSibling; 63 | } 64 | 65 | if (nextToFocus == null) { 66 | // If none of the links were focused, focus the first one. 67 | // Also if we've reached the last one in the list, wrap around to the first. 68 | document 69 | .querySelector( 70 | "#search-type-ahead > li:not([class*='hidden']) > a", 71 | ) 72 | ?.focus(); 73 | } else { 74 | nextToFocus.querySelector("a").focus(); 75 | } 76 | 77 | break; 78 | } 79 | case "ArrowUp": { 80 | event.preventDefault(); 81 | 82 | const focused = document.querySelector( 83 | "#search-type-ahead > li:not([class*='hidden']) > a:focus", 84 | ); 85 | 86 | // Find the next element to focus. 87 | let nextToFocus = focused?.parentElement?.previousElementSibling; 88 | while ( 89 | nextToFocus != null && 90 | nextToFocus.classList.contains("hidden") 91 | ) { 92 | nextToFocus = nextToFocus.previousElementSibling; 93 | } 94 | 95 | if (nextToFocus == null) { 96 | // If none of the links were focused, or we're at the first one, focus the search box again. 97 | searchBox?.focus(); 98 | } else { 99 | // If one of the links was focused, focus the previous one 100 | nextToFocus.querySelector("a").focus(); 101 | } 102 | 103 | break; 104 | } 105 | case "Enter": { 106 | // In case this is just an anchor link (which will move the scroll bar but not 107 | // reload the page), hide the search bar. 108 | searchTypeAhead.classList.add("hidden"); 109 | break; 110 | } 111 | } 112 | } 113 | 114 | searchForm.addEventListener("keydown", searchKeyDown); 115 | 116 | function search() { 117 | topSearchResultListItem = undefined; 118 | let text = searchBox.value.toLowerCase(); // Search is case-insensitive. 119 | 120 | if (text === "") { 121 | searchTypeAhead.classList.add("hidden"); 122 | } else { 123 | let totalResults = 0; 124 | // Firsttype-ahead-signature", show/hide all the sub-entries within each module (top-level functions etc.) 125 | searchTypeAhead.querySelectorAll("li").forEach((entry) => { 126 | const entryModule = entry 127 | .querySelector(".type-ahead-module-name") 128 | .textContent.toLowerCase(); 129 | const entryName = entry 130 | .querySelector(".type-ahead-def-name") 131 | .textContent.toLowerCase(); 132 | const entrySignature = entry 133 | .querySelector(".type-ahead-signature") 134 | ?.textContent?.toLowerCase() 135 | ?.replace(/\s+/g, ""); 136 | 137 | const qualifiedEntryName = `${entryModule}.${entryName}`; 138 | 139 | if ( 140 | qualifiedEntryName.includes(text) || 141 | entrySignature?.includes(text.replace(/\s+/g, "")) 142 | ) { 143 | totalResults++; 144 | entry.classList.remove("hidden"); 145 | if (topSearchResultListItem === undefined) { 146 | topSearchResultListItem = entry; 147 | } 148 | } else { 149 | entry.classList.add("hidden"); 150 | } 151 | }); 152 | if (totalResults < 1) { 153 | searchTypeAhead.classList.add("hidden"); 154 | } else { 155 | searchTypeAhead.classList.remove("hidden"); 156 | } 157 | } 158 | } 159 | 160 | searchBox.addEventListener("input", search); 161 | 162 | search(); 163 | 164 | function searchSubmit(e) { 165 | // pick the top result if the user submits search form 166 | e.preventDefault(); 167 | if (topSearchResultListItem !== undefined) { 168 | let topSearchResultListItemAnchor = 169 | topSearchResultListItem.querySelector("a"); 170 | if (topSearchResultListItemAnchor !== null) { 171 | topSearchResultListItemAnchor.click(); 172 | } 173 | } 174 | } 175 | searchForm.addEventListener("submit", searchSubmit); 176 | 177 | // Capture '/' keypress for quick search 178 | window.addEventListener("keyup", (e) => { 179 | if (e.key === "s" && document.activeElement !== searchBox) { 180 | e.preventDefault(); 181 | searchBox.focus(); 182 | searchBox.value = ""; 183 | } 184 | 185 | if (e.key === "Escape") { 186 | if (document.activeElement === searchBox) { 187 | // De-focus and clear input box 188 | searchBox.value = ""; 189 | searchBox.blur(); 190 | } else { 191 | // Hide the search results 192 | searchTypeAhead.classList.add("hidden"); 193 | 194 | if (searchTypeAhead.contains(document.activeElement)) { 195 | searchBox.focus(); 196 | } 197 | } 198 | } 199 | }); 200 | } 201 | }; 202 | 203 | const isTouchSupported = () => { 204 | try { 205 | document.createEvent("TouchEvent"); 206 | return true; 207 | } catch (e) { 208 | return false; 209 | } 210 | }; 211 | 212 | const setupCodeBlocks = () => { 213 | // Select all elements that are children of
 elements
214 |   const codeBlocks = document.querySelectorAll("pre > samp");
215 | 
216 |   // Iterate over each code block
217 |   codeBlocks.forEach((codeBlock) => {
218 |     // Create a "Copy" button
219 |     const copyButton = document.createElement("button");
220 |     copyButton.classList.add("copy-button");
221 |     copyButton.textContent = "Copy";
222 | 
223 |     // Add event listener to copy button
224 |     copyButton.addEventListener("click", () => {
225 |       const codeText = codeBlock.innerText;
226 |       navigator.clipboard.writeText(codeText);
227 |       copyButton.textContent = "Copied!";
228 |       copyButton.classList.add("copy-button-copied");
229 |       copyButton.addEventListener("mouseleave", () => {
230 |         copyButton.textContent = "Copy";
231 |         copyButton.classList.remove("copy-button-copied");
232 |       });
233 |     });
234 | 
235 |     // Create a container for the copy button and append it to the document
236 |     const buttonContainer = document.createElement("div");
237 |     buttonContainer.classList.add("button-container");
238 |     buttonContainer.appendChild(copyButton);
239 |     codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
240 | 
241 |     // Hide the button container by default
242 |     buttonContainer.style.display = "none";
243 | 
244 |     if (isTouchSupported()) {
245 |       // Show the button container on click for touch support (e.g. mobile)
246 |       document.addEventListener("click", (event) => {
247 |         if (event.target.closest("pre > samp") !== codeBlock) {
248 |           buttonContainer.style.display = "none";
249 |         } else {
250 |           buttonContainer.style.display = "block";
251 |         }
252 |       });
253 |     } else {
254 |       // Show the button container on hover for non-touch support (e.g. desktop)
255 |       codeBlock.parentNode.addEventListener("mouseenter", () => {
256 |         buttonContainer.style.display = "block";
257 |       });
258 | 
259 |       codeBlock.parentNode.addEventListener("mouseleave", () => {
260 |         buttonContainer.style.display = "none";
261 |       });
262 |     }
263 |   });
264 | };
265 | 
266 | const setupSidebarToggle = () => {
267 |   let body = document.body;
268 |   const sidebarOpen = "sidebar-open";
269 |   const removeOpenClass = () => {
270 |     body.classList.remove(sidebarOpen);
271 |     document.body
272 |       .querySelector("main")
273 |       .removeEventListener("click", removeOpenClass);
274 |   };
275 |   Array.from(document.body.querySelectorAll(".menu-toggle")).forEach(
276 |     (menuToggle) => {
277 |       menuToggle.addEventListener("click", (e) => {
278 |         body.classList.toggle(sidebarOpen);
279 |         e.stopPropagation();
280 |         if (body.classList.contains(sidebarOpen)) {
281 |           document.body.addEventListener("click", removeOpenClass);
282 |         }
283 |       });
284 |     },
285 |   );
286 | };
287 | 
288 | setupSidebarNav();
289 | setupSearch();
290 | setupCodeBlocks();
291 | setupSidebarToggle();
292 | 


--------------------------------------------------------------------------------
/www/0.10.0/styles.css:
--------------------------------------------------------------------------------
   1 | :root {
   2 |   /* WCAG AAA Compliant colors - important that luminence (the l in hsl) is 18% for font colors against the bg's luminence of 96-97% when the font-size is at least 14pt */
   3 |   --code-bg: hsl(262 33% 96% / 1);
   4 |   --gray: hsl(0 0% 18% / 1);
   5 |   --orange: hsl(25 100% 18% / 1);
   6 |   --green: hsl(115 100% 18% / 1);
   7 |   --cyan: hsl(190 100% 18% / 1);
   8 |   --blue: #05006d;
   9 |   --violet: #7c38f5;
  10 |   --violet-bg: hsl(262.22deg 87.1% 96%);
  11 |   --magenta: #a20031;
  12 |   --link-hover-color: #333;
  13 |   --link-color: var(--violet);
  14 |   --code-link-color: var(--violet);
  15 |   --text-color: #000;
  16 |   --text-hover-color: var(--violet);
  17 |   --body-bg-color: #ffffff;
  18 |   --border-color: #717171;
  19 |   --faded-color: #4c4c4c;
  20 |   --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
  21 |   --font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
  22 |   --top-header-height: 67px;
  23 |   --sidebar-width: clamp(280px, 25dvw, 500px);
  24 |   --module-search-height: 56px;
  25 |   --module-search-padding-height: 16px;
  26 |   --module-search-form-padding-width: 20px;
  27 |   --sidebar-bg-color: hsl(from var(--violet-bg) h calc(s * 1.05) calc(l * 0.95));
  28 | }
  29 | 
  30 | 
  31 | a {
  32 |   color: var(--violet);
  33 | }
  34 | 
  35 | table tr th {
  36 |   border: 1px solid var(--gray);
  37 | }
  38 | 
  39 | table tr th,
  40 | table tr td {
  41 |   padding: 6px 13px;
  42 | }
  43 | 
  44 | .logo svg {
  45 |   height: 48px;
  46 |   width: 48px;
  47 |   fill: var(--violet);
  48 | }
  49 | 
  50 | .logo:hover {
  51 |   text-decoration: none;
  52 | }
  53 | 
  54 | .logo svg:hover {
  55 |   fill: var(--link-hover-color);
  56 | }
  57 | 
  58 | .pkg-full-name {
  59 |   display: flex;
  60 |   align-items: center;
  61 |   font-size: 24px;
  62 |   margin: 0 8px;
  63 |   font-weight: normal;
  64 |   white-space: nowrap;
  65 |   overflow: hidden;
  66 |   text-overflow: ellipsis;
  67 | }
  68 | 
  69 | .entry-name {
  70 |   white-space: pre-wrap;
  71 |   font-family: var(--font-mono);
  72 |   font-size: 16px;
  73 |   font-weight: normal;
  74 |   background-color: var(--violet-bg);
  75 |   color: var(--text-color);
  76 |   width: auto;
  77 |   margin-top: 0;
  78 |   margin-bottom: 24px;
  79 |   padding: 12px 16px;
  80 |   border-left: 4px solid var(--violet);
  81 |   display: flex;
  82 | }
  83 | 
  84 | .entry-name strong {
  85 |     color: var(--text-color);
  86 | }
  87 | 
  88 | .entry-name code {
  89 |     background: none;
  90 | }
  91 | 
  92 | .entry-name:target {
  93 |   background-color: var(--violet-bg);
  94 | }
  95 | 
  96 | .entry-name a {
  97 |   visibility: hidden;
  98 |   display: inline-block;
  99 |   width: 18px;
 100 |   height: 14px;
 101 |   margin-left: -8px;
 102 |   margin-right: 4px;
 103 |   user-select: none;
 104 |   color: var(--violet);
 105 | }
 106 | 
 107 | .entry-name:hover a {
 108 |   visibility: visible;
 109 |   text-decoration: none;
 110 | }
 111 | 
 112 | .entry-name:not(:hover) a {
 113 |   visibility: hidden;
 114 |   transition: visibility 2s;
 115 | }
 116 | 
 117 | .pkg-full-name a {
 118 |   padding-top: 12px;
 119 |   padding-bottom: 16px;
 120 |   overflow: hidden;
 121 |   text-overflow: ellipsis;
 122 |   white-space: nowrap;
 123 | }
 124 | 
 125 | a {
 126 |   text-decoration: none;
 127 | }
 128 | 
 129 | a:hover,
 130 | a:hover code {
 131 |   text-decoration: underline;
 132 | }
 133 | 
 134 | .pkg-and-logo {
 135 |   min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */
 136 |   display: flex;
 137 |   align-items: center;
 138 |   justify-content: flex-start;
 139 |   gap: 8px;
 140 |   background-color: var(--violet-bg);
 141 |   padding: 16px;
 142 | }
 143 | 
 144 | .pkg-and-logo a,
 145 | .pkg-and-logo a:visited {
 146 |   color: var(--violet);
 147 | }
 148 | 
 149 | .pkg-and-logo a:hover {
 150 |   color: var(--link-hover-color);
 151 |   text-decoration: none;
 152 | }
 153 | 
 154 | .search-button {
 155 |   flex-shrink: 0; /* always shrink the package name before these; they have a relatively constrained length */
 156 |   padding: 12px 18px;
 157 |   margin-right: 42px;
 158 |   display: none; /* only show this in the mobile view */
 159 | }
 160 | 
 161 | .version {
 162 |   padding: 18px 10px;
 163 |   min-width: 48px;
 164 |   margin-right: 8px;
 165 | }
 166 | 
 167 | body {
 168 |   display: grid;
 169 |   grid-template-columns:
 170 |       [sidebar] var(--sidebar-width)
 171 |       [main-content] 1fr
 172 |       [content-end];
 173 |   grid-template-rows: 1fr;
 174 |   height: 100dvh;
 175 |   box-sizing: border-box;
 176 |   margin: 0;
 177 |   padding: 0;
 178 |   font-family: var(--font-sans);
 179 |   color: var(--text-color);
 180 |   background-color: var(--body-bg-color);
 181 |   overflow: hidden;
 182 | }
 183 | 
 184 | main {
 185 |   grid-column-start: main-content;
 186 |   grid-column-end: content-end;
 187 |   box-sizing: border-box;
 188 |   position: relative;
 189 |   font-size: 14pt; /* This keeps links AAA compliant while still making the links distinctly colored. */
 190 |   line-height: 1.85em;
 191 |   margin-top: 2px;
 192 |   padding: 16px;
 193 |   padding-top: 0px;
 194 |   /* necessary for text-overflow: ellipsis to work in descendants */
 195 |   min-width: 0;
 196 |   overflow-x: auto;
 197 |   /* fixes issues with horizonatal scroll in cases where word is too long,
 198 |   like in one of the examples at https://www.roc-lang.org/builtins/Num#Dec */
 199 |   overflow-wrap: break-word;
 200 |   overflow-y: auto;
 201 |   display: grid;
 202 |   --main-content-width: clamp(100px, calc(100% - 32px), 60ch); 
 203 |   grid-template-columns: [main-start] minmax(16px,1fr) [main-content-start] var(--main-content-width) [main-content-end] minmax(16px,1fr) [main-end];
 204 |   grid-template-rows: auto;
 205 |   flex-direction: column;
 206 |   scrollbar-color: var(--violet) var(--body-bg-color);
 207 |   scrollbar-gutter: stable both-edges;
 208 |   scroll-padding-top: calc(16px + 16px + 1lh + 16px + 16px);
 209 | }
 210 | 
 211 | main > * {
 212 |     grid-column-start: main-content-start;
 213 |     grid-column-end: main-content-end;
 214 | }
 215 | 
 216 | /* Module links on the package index page (/index.html) */
 217 | .index-module-links {
 218 |     margin: 0;
 219 |     padding: 0;
 220 |     list-style-type: none;
 221 | }
 222 | 
 223 | section {
 224 |   padding: 0px 0px 16px 0px;
 225 |   margin: 36px 0px;
 226 | }
 227 | 
 228 | section blockquote {
 229 |   font-style: italic;
 230 |   position: relative;
 231 |   margin-left: 0;
 232 |   margin-right: 0;
 233 | }
 234 | 
 235 | section blockquote:before {
 236 |   content: "";
 237 |   position: absolute;
 238 |   top: 0;
 239 |   right: 0;
 240 |   width: 2px;
 241 |   height: 100%;
 242 |   background-color: var(--gray);
 243 | }
 244 | 
 245 | 
 246 | section > *:last-child {
 247 |   margin-bottom: 0;
 248 | }
 249 | 
 250 | section h1,
 251 | section h2,
 252 | section h3,
 253 | section h4,
 254 | section p {
 255 | padding: 0px 16px;
 256 | }
 257 | 
 258 | #sidebar-nav {
 259 |     grid-column-start: sidebar;
 260 |     grid-column-end: main-content;
 261 |     position: relative;
 262 |     display: grid;
 263 |     grid-template-rows: min-content 1fr;
 264 |     box-sizing: border-box;
 265 |     width: 100%;
 266 |     background-color: var(--sidebar-bg-color);
 267 |     transition: all 1s linear;
 268 | }
 269 | 
 270 | #sidebar-nav .module-links-container {
 271 |     position: relative;
 272 | }
 273 | 
 274 | 
 275 | #sidebar-nav .module-links {
 276 |     position: absolute;
 277 |     inset: 0;
 278 |     overflow-y: auto;
 279 |     overflow-x: hidden;
 280 |     scrollbar-color: var(--violet) var(--sidebar-bg-color);
 281 |     scrollbar-gutter: stable;
 282 |     padding: 16px 8px;
 283 |     transition: all .2s linear;
 284 | }
 285 | 
 286 | .top-header {
 287 |   grid-column-start: sidebar;
 288 |   grid-column-end: end;
 289 |   grid-row-start: top-header;
 290 |   grid-row-end: top-header;
 291 |   display: flex;
 292 |   flex-direction: row;
 293 |   align-items: center;
 294 |   flex-wrap: nowrap;
 295 |   box-sizing: border-box;
 296 |   font-family: var(--font-sans);
 297 |   font-size: 20px;
 298 |   height: 100%;
 299 |   background-color: var(--violet-bg);
 300 |   /* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants, but we want this anyway. */
 301 |   min-width: 1024px;
 302 | }
 303 | 
 304 | p {
 305 |   overflow-wrap: break-word;
 306 |   margin: 24px 0;
 307 | }
 308 | 
 309 | footer {
 310 |   max-width: var(--main-content-max-width);
 311 |   font-size: 14px;
 312 |   box-sizing: border-box;
 313 |   padding: 16px;
 314 | }
 315 | 
 316 | footer p {
 317 |   display: inline-block;
 318 |   margin-top: 0;
 319 |   margin-bottom: 8px;
 320 | }
 321 | 
 322 | .content {
 323 |   box-sizing: border-box;
 324 |   display: flex;
 325 |   flex-direction: row;
 326 |   justify-content: space-between;
 327 | }
 328 | 
 329 | .sidebar-entry ul {
 330 |   list-style-type: none;
 331 |   margin: 0;
 332 | }
 333 | 
 334 | .sidebar-entry a {
 335 |   box-sizing: border-box;
 336 |   min-height: 40px;
 337 |   min-width: 48px;
 338 |   padding: 8px 16px;
 339 |   font-family: var(--font-mono);
 340 | }
 341 | 
 342 | .sidebar-entry a,
 343 | .sidebar-entry a:visited {
 344 |   color: var(--text-color);
 345 | }
 346 | 
 347 | .sidebar-sub-entries {
 348 |     font-size: 12pt;
 349 |     display: none;
 350 | }
 351 | 
 352 | .active + .sidebar-sub-entries {
 353 |     display: block;
 354 | }
 355 | 
 356 | .sidebar-sub-entries a {
 357 |   display: block;
 358 |   line-height: 24px;
 359 |   width: 100%;
 360 |   overflow: hidden;
 361 |   text-overflow: ellipsis;
 362 |   margin-left: 20px;
 363 |   padding-left: 27px;
 364 |   border-left: 2px solid rgb(from var(--gray) r g b / .30);
 365 |   white-space: nowrap;
 366 |   display: flex;
 367 |   align-items: center;
 368 | }
 369 | 
 370 | .sidebar-sub-entries a:first-child {
 371 |     margin-top: 8px;
 372 |     padding-top: 0;
 373 | }
 374 | 
 375 | .sidebar-sub-entries a:last-child {
 376 |     margin-bottom: 8px;
 377 |     padding-bottom: 0;
 378 | }
 379 | 
 380 | .sidebar-sub-entries a:hover {
 381 |     border-left-color: rgb(from var(--violet) r g b / .60);
 382 |     color: var(--violet);
 383 |     text-decoration: none;
 384 | }
 385 | 
 386 | .module-name {
 387 |   font-size: 40pt;
 388 |   line-height: 1em;
 389 |   font-family: var(--font-mono);
 390 |   font-weight: bold;
 391 |   margin-top: 36px;
 392 |   margin-bottom: 16px;
 393 |   color: var(--violet);
 394 | }
 395 | 
 396 | main h2 {
 397 |     font-size: 28pt;
 398 | }
 399 | main h3 {
 400 |     font-size: 24pt;
 401 | }
 402 | main h4 {
 403 |     font-size: 20pt;
 404 | }
 405 | 
 406 | .module-name a,
 407 | .module-name a:visited {
 408 | color: inherit;
 409 | }
 410 | 
 411 | .module-name a:hover {
 412 |   color: var(--link-hover-color);
 413 | }
 414 | 
 415 | a.sidebar-module-link {
 416 |   box-sizing: border-box;
 417 |   font-size: 14pt;
 418 |   line-height: 24px;
 419 |   font-family: var(--font-mono);
 420 |   display: flex;
 421 |   flex-direction: row-reverse;
 422 |   justify-content: space-between;
 423 |   align-items: center;
 424 |   width: 100%;
 425 |   padding: 0;
 426 |   white-space: nowrap;
 427 |   overflow: hidden;
 428 |   text-overflow: ellipsis;
 429 | }
 430 | 
 431 | .sidebar-module-link:hover {
 432 |     text-decoration: none;
 433 | 
 434 |     span:hover {
 435 |         color: var(--violet);
 436 |     }
 437 | }
 438 | 
 439 | .sidebar-module-link span {
 440 |     display: inline-block;
 441 |     flex-grow: 1;
 442 | }
 443 | 
 444 | .sidebar-module-link.active {
 445 |   font-weight: bold;
 446 | }
 447 | 
 448 | .sidebar-module-link > .entry-toggle {
 449 |     background-color: transparent;
 450 |     appearance: none;
 451 |     border: none;
 452 |     color: transparent;
 453 |     color: rgba(from var(--text-color) r g b / .6);
 454 |     transition: all 80ms linear;
 455 |     text-decoration: none;
 456 |     font-size: 0.6rem;
 457 |     cursor: pointer;
 458 |     padding: 8px 16px;
 459 |     
 460 |     &:hover {
 461 |         color: var(--violet);
 462 |     }
 463 | }
 464 | 
 465 | :hover >  .entry-toggle {
 466 |     color: var(--text-color);
 467 | }
 468 | 
 469 | .active .entry-toggle {
 470 |     rotate: 90deg;
 471 | }
 472 | 
 473 | a,
 474 | a:visited {
 475 |   color: var(--link-color);
 476 | }
 477 | 
 478 | h3 {
 479 |   font-size: 32px;
 480 |   margin: 48px 0 24px 0;
 481 | }
 482 | 
 483 | h4 {
 484 |   font-size: 24px;
 485 | }
 486 | 
 487 | .type-def {
 488 |   font-size: 24px;
 489 |   color: var(--link-color);
 490 | }
 491 | 
 492 | .code-snippet {
 493 |   padding: 8px 16px;
 494 |   display: block;
 495 |   box-sizing: border-box;
 496 |   font-family: var(--font-mono);
 497 |   background-color: var(--code-bg);
 498 | }
 499 | 
 500 | code {
 501 |   font-family: var(--font-mono);
 502 |   color: var(--code-color);
 503 |   background-color: var(--code-bg);
 504 |   display: inline-block;
 505 | }
 506 | 
 507 | p code {
 508 |   padding: 0 8px;
 509 | }
 510 | 
 511 | code a,
 512 | a code {
 513 |   text-decoration: none;
 514 |   color: var(--code-link-color);
 515 |   background: none;
 516 |   padding: 0;
 517 |   font-weight: bold; /* Important for AAA compliance while keeping color distinct */
 518 | }
 519 | 
 520 | code a:visited,
 521 | a:visited code {
 522 |   color: var(--code-link-color);
 523 | }
 524 | 
 525 | pre {
 526 |   margin: 36px 0;
 527 |   padding: 8px 16px;
 528 |   box-sizing: border-box;
 529 |   background-color: var(--code-bg);
 530 |   position: relative;
 531 |   word-wrap: normal;
 532 | }
 533 | 
 534 | pre>samp {
 535 |     overflow-x: auto;
 536 |     display: block;
 537 |     scrollbar-color: var(--violet) var(--code-bg);
 538 |     scrollbar-width: thin;
 539 |     scrollbar-gutter: stable;
 540 | }
 541 | 
 542 | .hidden {
 543 |   /* Use !important to win all specificity fights. */
 544 |   display: none !important;
 545 | }
 546 | 
 547 | #module-search-form {
 548 |   display: flex;
 549 |   align-items: center;
 550 |   align-content: center;
 551 |   height: 100%;
 552 |   position: sticky;
 553 |   flex-grow: 1;
 554 |   box-sizing: border-box;
 555 |   padding-block: 16px;
 556 |   background-color: var(--body-bg-color);
 557 |   top: 0;
 558 |   z-index: 1;
 559 | }
 560 | 
 561 | .menu-toggle {
 562 |     display: none;
 563 |     margin-right: 8px;
 564 |     appearance: none;
 565 |     background-color: transparent;
 566 |     outline: none;
 567 |     border: none;
 568 |     color: var(--violet);
 569 |     padding: 0;
 570 |     cursor: pointer;
 571 | }
 572 | 
 573 | .menu-toggle svg {
 574 |     height: 48px;
 575 |     width: 48px;
 576 | }
 577 | 
 578 | 
 579 | #module-search,
 580 | #module-search:focus {
 581 |   opacity: 1;
 582 |   padding: 12px 16px;
 583 |   height: var(--module-search-height);
 584 | }
 585 | 
 586 | #module-search {
 587 |   border-radius: 8px;
 588 |   display: block;
 589 |   position: relative;
 590 |   box-sizing: border-box;
 591 |   width: 100%;
 592 |   box-sizing: border-box;
 593 |   font-size: 16px;
 594 |   line-height: 18px;
 595 |   border: none;
 596 |   color: var(--faded-color);
 597 |   background-color: var(--body-bg-color);
 598 |   font-family: var(--font-serif);
 599 |   border: 2px solid var(--violet-bg);
 600 | }
 601 | 
 602 | @media (prefers-color-scheme: light) {
 603 |     #module-search {
 604 |         outline: 1px solid var(--gray);
 605 |     }
 606 | }
 607 | 
 608 | #module-search::placeholder {
 609 |   color: var(--faded-color);
 610 |   opacity: 1;
 611 | }
 612 | 
 613 | #module-search:focus, #module-search:hover {
 614 |   outline: 2px solid var(--violet);
 615 | }
 616 | 
 617 | #search-type-ahead {
 618 |   font-family: var(--font-mono);
 619 |   display: flex;
 620 |   gap: 0px;
 621 |   flex-direction: column;
 622 |   position: absolute;
 623 |   top: calc(var(--module-search-padding-height) + var(--module-search-height));
 624 |   left: var(--module-search-form-padding-width);
 625 |   width: calc(100% - 2 * var(--module-search-form-padding-width));
 626 |   box-sizing: border-box;
 627 |   z-index: 100;
 628 |   background-color: var(--body-bg-color);
 629 |   border-width: 1px;
 630 |   border-style: solid;
 631 |   border-color: var(--border-color);
 632 |   list-style-type: none;
 633 |   margin: 0;
 634 |   padding: 0;
 635 | }
 636 | 
 637 | .search-icon {
 638 |     fill: var(--faded-color);
 639 |     pointer-events: none;
 640 |     opacity: 0.6;
 641 |     position: absolute;
 642 |     right: 32px;
 643 | }
 644 | 
 645 | #search-type-ahead .type-ahead-link {
 646 |   font-size: 1rem;
 647 |   color: var(--text-color);
 648 |   line-height: 2em;
 649 |   position: relative;
 650 |   box-sizing: border-box;
 651 |   width: 100%;
 652 |   height: 100%;
 653 |   padding: 4px 8px;
 654 | 
 655 |   max-height: 6em;
 656 |   text-overflow: ellipsis;
 657 |   overflow: hidden;
 658 |   display: -webkit-box;
 659 |   -webkit-line-clamp: 3;
 660 |   -webkit-box-orient: vertical;
 661 | 
 662 |   /* if it wraps, indent after the first line */
 663 |   padding-left: calc(2em + 8px);
 664 |   text-indent: -2em;
 665 | 
 666 | 
 667 |   span {
 668 |     margin: 0px;
 669 |   }
 670 | 
 671 |   .type-ahead-module-name, .type-ahead-def-name {
 672 |     color: var(--violet);
 673 |     font-size: 1rem;
 674 |   }
 675 | }
 676 | 
 677 | #search-type-ahead li {
 678 |   box-sizing: border-box;
 679 |   position: relative;
 680 | }
 681 | 
 682 | #search-type-ahead a:focus {
 683 |   outline: none;
 684 |   background: var(--violet-bg);
 685 | }
 686 | 
 687 | #module-search-form:focus-within #search-label, #module-search-form:focus-within .search-icon {
 688 |   display: none;
 689 | }
 690 | 
 691 | #search-label {
 692 |   color: var(--faded-color);
 693 |   box-sizing: border-box;
 694 |   align-items: center;
 695 |   font-size: 18px;
 696 |   pointer-events: none;
 697 |   position: absolute;
 698 |   right: 72px;
 699 | }
 700 | 
 701 | #search-shortcut-key {
 702 |   font-family: var(--font-mono);
 703 |   border: 1px solid #666;
 704 |   border-radius: 5px;
 705 |   padding: 1px 3px 3px;
 706 |   font-style: normal;
 707 |   line-height: 15px;
 708 |   pointer-events: none;
 709 | }
 710 | 
 711 | .builtins-tip {
 712 |   padding: 1em;
 713 |   font-style: italic;
 714 |   line-height: 1.3em;
 715 | }
 716 | 
 717 | .module-header-container {
 718 |   display: flex;
 719 |   justify-content: space-between;
 720 |   align-items: flex-end;
 721 |   margin-bottom: 48px;
 722 | }
 723 | 
 724 | .llm-prompt-link {
 725 |   flex-shrink: 0;
 726 | }
 727 | 
 728 | .module-name {
 729 |     flex-grow: 1;
 730 | }
 731 | 
 732 | @media (prefers-color-scheme: dark) {
 733 |   :root {
 734 |       /* WCAG AAA Compliant colors */
 735 |     --code-bg: hsl(228.95deg 37.25% 15%);
 736 |     --gray: hsl(0 0% 70% / 1);
 737 |     --orange: hsl(25 98% 70% / 1);
 738 |     --green: hsl(115 40% 70% / 1);
 739 |     --cyan: hsl(176 84% 70% / 1);
 740 |     --blue: hsl(243 43% 80% / 1);
 741 |     --violet: #caadfb;
 742 |     --violet-bg: hsl(262 25% 15% / 1);
 743 |     --magenta: hsl(348 79% 80% / 1);
 744 |       --link-hover-color: #fff;
 745 | 
 746 |       --link-color: var(--violet);
 747 |       --code-link-color: var(--violet);
 748 |       --text-color: #eaeaea;
 749 |       --body-bg-color: hsl(from var(--violet-bg) h s calc(l * .5));
 750 |       --border-color: var(--gray);
 751 |       --code-color: #eeeeee;
 752 |       --logo-solid: #8f8f8f;
 753 |       --faded-color: #bbbbbb;
 754 |       --sidebar-bg-color: hsl(from var(--violet-bg) h calc(s * 1.1) calc(l * 0.75));
 755 |   }
 756 | 
 757 |   html {
 758 |       scrollbar-color: #8f8f8f #2f2f2f;
 759 |   }
 760 | }
 761 | 
 762 | @media only screen and (max-width: 768px) {
 763 |     :root {
 764 |         --sidebar-width: clamp(280px, 50dvw, 385px);
 765 |     }
 766 |     body {
 767 |         display: block;
 768 |         overflow-y: auto;
 769 |         overflow-x: hidden;
 770 |     }
 771 | 
 772 |     #sidebar-nav {
 773 |         left: calc(-1 * var(--sidebar-width));
 774 |         top: 0;
 775 |         bottom: 0;
 776 |         position: fixed;
 777 |         z-index: 2;
 778 |         transition: all .2s linear;
 779 |     }
 780 | 
 781 |     .entry-toggle {
 782 |         height: 48px;
 783 |         width: 48px;
 784 |     }
 785 | 
 786 |     body.sidebar-open #sidebar-nav {
 787 |         left: 0;
 788 |     }
 789 | 
 790 |     main {
 791 |         display: block;
 792 |         margin: 0 16px;
 793 |         --main-content-width: minmax(calc(100% - 32px), 60ch);
 794 |     }
 795 | 
 796 |     :root {
 797 |         --top-header-height: 160px;
 798 |     }
 799 | 
 800 |     #search-shortcut-key, .header-start-extension, .header-end-extension, #search-label {
 801 |         display: none;
 802 |     }
 803 | 
 804 |     #module-search-form {
 805 |         padding: 16px 16px;
 806 |         height: auto;
 807 |         margin-bottom: 16px;
 808 |         grid-column-start: main-content-start;
 809 |         grid-column-end: main-content-end;
 810 |         position: fixed;
 811 |         left: 0;
 812 |         right: 0;
 813 |     }
 814 | 
 815 |     .menu-toggle {
 816 |         display: inline-block;
 817 |         margin-right: 8px;
 818 |         appearance: none;
 819 |         background-color: transparent;
 820 |         outline: none;
 821 |         border: none;
 822 |         color: var(--text-color);
 823 |         padding: 0;
 824 |     }
 825 | 
 826 |     .menu-toggle svg {
 827 |         height: 48px;
 828 |         width: 48px;
 829 |     }
 830 | 
 831 |     /* Hide the Copy Link button on mobile. */
 832 |     .entry-name a:first-of-type {
 833 |         display: none;
 834 |     }
 835 | 
 836 |     .search-icon {
 837 |         display: block; /* This is only visible in mobile. */
 838 |         top: calc(1lh / 2 + 16px);
 839 |     }
 840 | 
 841 |     .top-header {
 842 |         flex-direction: column;
 843 |         height: auto;
 844 |         justify-content: space-between;
 845 |         /* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants. */
 846 |         min-width: 0;
 847 |     }
 848 | 
 849 |     .pkg-full-name {
 850 |         font-size: 20px;
 851 |         padding-bottom: 14px;
 852 |     }
 853 | 
 854 |     .pkg-full-name a {
 855 |         vertical-align: middle;
 856 |         padding: 18px 0;
 857 |     }
 858 | 
 859 |     .logo {
 860 |         width: 50px;
 861 |         height: 54px;
 862 |     }
 863 | 
 864 |     .version {
 865 |         margin: 0;
 866 |         font-weight: normal;
 867 |         font-size: 18px;
 868 |         padding-bottom: 16px;
 869 |     }
 870 | 
 871 |     .module-name {
 872 |         font-size: 24px;
 873 |         margin-top: 8px;
 874 |         margin-bottom: 8px;
 875 |         overflow: hidden;
 876 |         text-overflow: ellipsis;
 877 |     }
 878 | 
 879 |     main {
 880 |         padding: 18px;
 881 |         font-size: 16px;
 882 |         padding-top: calc(16px + 16px + 1lh + 16px);
 883 |     }
 884 | 
 885 |     #sidebar-nav {
 886 |         margin-top: 0;
 887 |         padding-left: 0;
 888 |         width: var(--sidebar-width);
 889 |     }
 890 | 
 891 |     #sidebar-heading {
 892 |         font-size: 24px;
 893 |         margin: 16px;
 894 |     }
 895 | 
 896 |     h3 {
 897 |         font-size: 18px;
 898 |         margin: 0;
 899 |         padding: 0;
 900 |     }
 901 | 
 902 |     h4 {
 903 |         font-size: 16px;
 904 |     }
 905 | 
 906 |     body {
 907 |         margin: 0;
 908 |         min-width: 320px;
 909 |         max-width: 100dvw;
 910 |     }
 911 | 
 912 |     .top-header-triangle {
 913 |         display: none;
 914 |     }
 915 | 
 916 |     .pkg-and-logo {
 917 |         padding-block: 4px;
 918 |     }
 919 | 
 920 |     .pkg-full-name {
 921 |         flex-grow: 1;
 922 |     }
 923 | 
 924 |     .pkg-full-name a {
 925 |         padding-top: 24px;
 926 |         padding-bottom: 12px;
 927 |     }
 928 | }
 929 | 
 930 | /* Comments `#` and Documentation comments `##` */
 931 | samp .comment,
 932 | code .comment {
 933 |   color: var(--green);
 934 | }
 935 | 
 936 | /* Number, String, Tag literals */
 937 | samp .storage.type,
 938 | code .storage.type,
 939 | samp .string,
 940 | code .string,
 941 | samp .string.begin,
 942 | code .string.begin,
 943 | samp .string.end,
 944 | code .string.end,
 945 | samp .constant,
 946 | code .constant,
 947 | samp .literal,
 948 | code .literal {
 949 |   color: var(--cyan);
 950 | }
 951 | 
 952 | /* Keywords and punctuation */
 953 | samp .keyword,
 954 | code .keyword,
 955 | samp .punctuation.section,
 956 | code .punctuation.section,
 957 | samp .punctuation.separator,
 958 | code .punctuation.separator,
 959 | samp .punctuation.terminator,
 960 | code .punctuation.terminator,
 961 | samp .kw,
 962 | code .kw {
 963 |     color: var(--magenta);
 964 | }
 965 | 
 966 | /* Operators */
 967 | samp .op,
 968 | code .op,
 969 | samp .keyword.operator,
 970 | code .keyword.operator {
 971 |   color: var(--orange);
 972 | }
 973 | 
 974 | /* Delimieters */
 975 | samp .delimeter,
 976 | code .delimeter {
 977 |   color: var(--gray);
 978 | }
 979 | 
 980 | /* Variables modules and field names */
 981 | samp .function,
 982 | code .function,
 983 | samp .meta.group,
 984 | code .meta.group,
 985 | samp .meta.block,
 986 | code .meta.block,
 987 | samp .lowerident,
 988 | code .lowerident {
 989 |   color: var(--blue);
 990 | }
 991 | 
 992 | /* Types, Tags, and Modules */
 993 | samp .type,
 994 | code .type,
 995 | samp .meta.path,
 996 | code .meta.path,
 997 | samp .upperident,
 998 | code .upperident {
 999 |   color: var(--green);
1000 | }
1001 | 
1002 | samp .dim,
1003 | code .dim {
1004 |   opacity: 0.55;
1005 | }
1006 | 
1007 | .button-container {
1008 |   position: absolute;
1009 |   top: 0;
1010 |   right: 0;
1011 | }
1012 | 
1013 | .copy-button {
1014 |   background: var(--code-bg);
1015 |   border: 1px solid var(--magenta);
1016 |   color: var(--magenta);
1017 |   display: inline-block;
1018 |   cursor: pointer;
1019 |   margin: 8px;
1020 | }
1021 | 
1022 | .copy-button:hover {
1023 |   border-color: var(--link-hover-color);
1024 |   color: var(--link-hover-color);
1025 | }
1026 | 


--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |         
 5 |         
 6 |         Redirecting...
 7 |         
10 |     
11 |     
12 |         
18 |     
19 | 
20 | 


--------------------------------------------------------------------------------