├── .gitignore ├── src ├── main.roc ├── SafeStr.roc ├── Attribute.roc └── Html.roc ├── examples ├── .gitignore └── example.roc ├── .github ├── dependabot.yaml ├── release-drafter.yaml └── workflows │ ├── test.yaml │ ├── bundle.yaml │ ├── update-draft-release.yaml │ ├── check-pr-labels.yaml │ ├── generate-docs.yaml │ └── run-pre-commit.yaml ├── Justfile ├── README.md ├── flake.nix ├── LICENCE ├── .pre-commit-config.yaml └── flake.lock /.gitignore: -------------------------------------------------------------------------------- 1 | generated-docs/ 2 | -------------------------------------------------------------------------------- /src/main.roc: -------------------------------------------------------------------------------- 1 | package [Html, Attribute] {} 2 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !*.roc 4 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | # Look for workflow files in .github/workflows/ 6 | directory: / 7 | # Check for updates once a month 8 | schedule: 9 | interval: monthly 10 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | default: format check test examples docs 2 | 3 | format: 4 | roc format src/ 5 | roc format examples/ 6 | 7 | check: 8 | roc check src/main.roc 9 | fd --extension roc . examples/ --exec roc check 10 | 11 | test: 12 | roc test src/main.roc 13 | 14 | examples: 15 | fd --extension roc . examples/ --exec roc run 16 | 17 | docs: 18 | roc docs src/main.roc 19 | 20 | ratchet: 21 | ratchet upgrade .github/workflows/*.yaml 22 | -------------------------------------------------------------------------------- /.github/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | # The name of the release 2 | name-template: v$RESOLVED_VERSION 3 | # The tag of the release 4 | tag-template: v$RESOLVED_VERSION 5 | # Template for category headings 6 | category-template: "### $TITLE" 7 | # Template for each individual change 8 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 9 | change-title-escapes: \<*_& 10 | categories: 11 | - title: Features 12 | label: feature 13 | - title: Bug fixes 14 | labels: fix 15 | - title: Maintenance 16 | label: chore 17 | version-resolver: 18 | major: 19 | labels: 20 | - major 21 | minor: 22 | labels: 23 | - minor 24 | patch: 25 | labels: 26 | - patch 27 | default: patch # Default increment if no PRs are found 28 | template: | 29 | ## Changelog 30 | 31 | $CHANGES 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | # Run on all PRs 5 | pull_request: 6 | paths-ignore: 7 | - "**.md" 8 | # Run when a PR is merged into main 9 | push: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - "**.md" 14 | 15 | jobs: 16 | test: 17 | name: Test 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Check out the repository 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5 24 | - name: Install Roc 25 | uses: hasnep/setup-roc@9866c6fdc971ee9f4b3eeba03d825dc32a5efa7f # ratchet:hasnep/setup-roc@v0.5.0 26 | with: 27 | roc-version: 0.0.0-alpha2-rolling 28 | - name: Test the library 29 | run: roc test src/main.roc 30 | -------------------------------------------------------------------------------- /.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5 18 | - name: Install Roc 19 | uses: hasnep/setup-roc@9866c6fdc971ee9f4b3eeba03d825dc32a5efa7f # ratchet:hasnep/setup-roc@v0.5.0 20 | with: 21 | roc-version: 0.0.0-alpha2-rolling 22 | - name: Bundle and release the library 23 | uses: hasnep/bundle-roc-library@4364d15b4ae83c99e0bc0caab8a254a5d0a9369f # ratchet:hasnep/bundle-roc-library@v0.1.0 24 | with: 25 | library: src/main.roc 26 | token: ${{ github.token }} 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roc HTML 2 | 3 | A library to create HTML in Roc. 4 | 5 | ## Example 6 | 7 | This Roc code: 8 | 9 | ```roc 10 | Html.html [] [ 11 | Html.body [] [ 12 | Html.h1 [] [Html.text "Roc"], 13 | Html.p [] [ 14 | Html.text "You should really check out ", 15 | Html.a [Attribute.href "https://roc-lang.org/"] [Html.text "Roc"], 16 | Html.text "!", 17 | ] 18 | ] 19 | ] |> Html.render 20 | ``` 21 | 22 | Returns this HTML (give or take a little formatting): 23 | 24 | ```html 25 | 26 | 27 | 28 |

Roc

29 |

You should really check out Roc!

30 | 31 | 32 | ``` 33 | 34 | ## Licence 35 | 36 | This repository is released under the [UPL licence](./LICENCE) and was mostly written by [Brian Carroll](https://github.com/brian-carroll/). Thanks, Brian! 37 | -------------------------------------------------------------------------------- /.github/workflows/update-draft-release.yaml: -------------------------------------------------------------------------------- 1 | name: Update draft release 2 | 3 | on: 4 | # Run when a PR is merged into main 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update-draft-release: 11 | name: Update draft release 12 | permissions: 13 | # Permissions required for creating a GitHub release 14 | contents: write 15 | pull-requests: read 16 | runs-on: ubuntu-latest 17 | outputs: 18 | tag_name: ${{ steps.update-draft-release.outputs.tag_name }} 19 | steps: 20 | - name: Update the draft release 21 | id: update-draft-release 22 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # ratchet:release-drafter/release-drafter@v6 23 | with: 24 | config-name: release-drafter.yaml 25 | disable-autolabeler: true 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /examples/example.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 | html: "../src/main.roc", 4 | } 5 | 6 | import cli.Stdout 7 | import html.Html 8 | import html.Attribute 9 | 10 | main! = |_args| 11 | page = Html.html( 12 | [], 13 | [ 14 | Html.body( 15 | [], 16 | [ 17 | Html.h1([], [Html.text("Roc")]), 18 | Html.p( 19 | [], 20 | [ 21 | Html.text("My favourite language is "), 22 | Html.a([Attribute.href("https://roc-lang.org/")], [Html.text("Roc")]), 23 | Html.text("!"), 24 | ], 25 | ), 26 | ], 27 | ), 28 | ], 29 | ) 30 | rendered_html = Html.render(page) 31 | Stdout.line!(rendered_html) 32 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-labels.yaml: -------------------------------------------------------------------------------- 1 | name: Check PR labels 2 | 3 | on: 4 | # Run on all PRs 5 | pull_request: 6 | types: 7 | - labeled 8 | - opened 9 | - reopened 10 | - synchronize 11 | 12 | jobs: 13 | check-pr-labels: 14 | name: Check PR labels 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check for PR category labels 18 | uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # ratchet:yogevbd/enforce-label-action@2.2.2 19 | with: 20 | REQUIRED_LABELS_ANY: feature,fix,chore 21 | REQUIRED_LABELS_ANY_DESCRIPTION: Please tag your PR with one of `feature`, `fix`, or `chore`. 22 | - name: Check for PR version labels 23 | uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # ratchet:yogevbd/enforce-label-action@2.2.2 24 | with: 25 | REQUIRED_LABELS_ANY: major,minor,patch 26 | REQUIRED_LABELS_ANY_DESCRIPTION: Please tag your PR with one of `major`, `minor`, `patch`. 27 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Generate docs 2 | 3 | on: 4 | # Run when a release is published 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | generate-docs: 11 | name: Generate docs 12 | runs-on: ubuntu-latest 13 | permissions: 14 | pages: write 15 | id-token: write 16 | steps: 17 | - name: Check out the repository 18 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5 19 | - name: Install Roc 20 | uses: hasnep/setup-roc@9866c6fdc971ee9f4b3eeba03d825dc32a5efa7f # ratchet:hasnep/setup-roc@v0.5.0 21 | with: 22 | roc-version: 0.0.0-alpha2-rolling 23 | - name: Generate docs 24 | run: roc docs src/main.roc 25 | - name: Fix absolute paths 26 | run: | 27 | find generated-docs/ -type f -name '*.html' -exec sed -i "s/\(href\|src\)=\"\//\1=\"\/${{ github.event.repository.name }}\//g" {} + 28 | - name: Upload docs artifact 29 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # ratchet:actions/upload-pages-artifact@v4 30 | with: 31 | path: generated-docs 32 | - name: Deploy docs 33 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # ratchet:actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | roc.url = "github:roc-lang/roc/alpha4-rolling"; 6 | }; 7 | 8 | nixConfig = { 9 | extra-trusted-public-keys = "roc-lang.cachix.org-1:6lZeqLP9SadjmUbskJAvcdGR2T5ViR57pDVkxJQb8R4="; 10 | extra-trusted-substituters = "https://roc-lang.cachix.org"; 11 | }; 12 | 13 | outputs = 14 | inputs@{ flake-parts, ... }: 15 | flake-parts.lib.mkFlake { inherit inputs; } { 16 | systems = [ 17 | "aarch64-darwin" 18 | "aarch64-linux" 19 | "x86_64-darwin" 20 | "x86_64-linux" 21 | ]; 22 | perSystem = 23 | { inputs', pkgs, ... }: 24 | { 25 | devShells.default = pkgs.mkShell { 26 | name = "roc-html"; 27 | packages = [ 28 | inputs'.roc.packages.cli 29 | ] 30 | # Pre-commit 31 | ++ [ 32 | pkgs.actionlint 33 | pkgs.check-jsonschema 34 | pkgs.fd 35 | pkgs.just 36 | pkgs.nixfmt 37 | pkgs.pre-commit 38 | pkgs.prettier 39 | pkgs.python3Packages.pre-commit-hooks 40 | pkgs.ratchet 41 | ]; 42 | shellHook = "pre-commit install --overwrite"; 43 | }; 44 | formatter = pkgs.nixfmt-tree; 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/run-pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Run Pre-commit 2 | 3 | on: 4 | # Run on all PRs 5 | pull_request: 6 | # Run when a PR is merged into main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-pre-commit: 13 | name: Run pre-commit 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out the repository 17 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5 18 | - name: Install Nix 19 | uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # ratchet:nixbuild/nix-quick-install-action@v34 20 | - name: Cache Nix environment 21 | uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # ratchet:nix-community/cache-nix-action@v6 22 | with: 23 | primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} 24 | restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} 25 | - name: Use Nix dev shell for subsequent steps 26 | uses: rrbutani/use-nix-shell-action@59a52b2b9bbfe3cc0e7deb8f9059abe37a439edf # ratchet:rrbutani/use-nix-shell-action@v1 27 | with: 28 | extraNixOptions: --accept-flake-config 29 | - name: Run Pre-commit 30 | run: pre-commit run --all-files 31 | env: 32 | # Prevent this action failing when running on the main branch 33 | SKIP: no-commit-to-branch 34 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Brian Carroll, Luke Boswell, Hannes Smit and Roc contributors 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 6 | person obtaining a copy of this software, associated documentation and/or data 7 | (collectively the "Software"), free of charge and under any and all copyright 8 | rights in the Software, and any and all patent rights owned or freely 9 | licensable by each licensor hereunder covering either (i) the unmodified 10 | Software as contributed to or provided by such licensor, or (ii) the Larger 11 | Works (as defined below), to deal in both 12 | 13 | (a) the Software, and 14 | (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if 15 | one is included with the Software (each a “Larger Work” to which the Software 16 | is contributed by such licensors), 17 | 18 | without restriction, including without limitation the rights to copy, create 19 | derivative works of, display, perform, and distribute the Software and make, 20 | use, sell, offer for sale, import, export, have made, and have sold the 21 | Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | either these or other terms. 23 | 24 | This license is subject to the following condition: 25 | The above copyright notice and either this complete permission notice or at 26 | a minimum a reference to the UPL must be included in all copies or 27 | substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # General checks 3 | - repo: local 4 | hooks: 5 | - name: Prevent committing to main 6 | id: no-commit-to-branch 7 | language: system 8 | entry: no-commit-to-branch 9 | args: [--branch, main] 10 | pass_filenames: false 11 | - name: Make sure files end with a newline character 12 | id: end-of-file-fixer 13 | language: system 14 | entry: end-of-file-fixer 15 | types: [text] 16 | - name: Remove trailing whitespace 17 | id: trailing-whitespace-fixer 18 | language: system 19 | entry: trailing-whitespace-fixer 20 | types: [text] 21 | - name: Check for files that would conflict on case-insensitive filesystem 22 | id: check-case-conflict 23 | language: system 24 | entry: check-case-conflict 25 | - name: Check for merge conflicts 26 | id: check-merge-conflict 27 | language: system 28 | entry: check-merge-conflict 29 | - name: Check executable files have a shebang 30 | id: check-executables-have-shebangs 31 | language: system 32 | entry: check-executables-have-shebangs 33 | types: [executable] 34 | - name: Check scripts with a shebang are executable 35 | id: check-shebang-scripts-are-executable 36 | language: system 37 | entry: check-shebang-scripts-are-executable 38 | - name: Don't allow adding large files 39 | id: check-added-large-files 40 | language: system 41 | entry: check-added-large-files 42 | 43 | # Roc 44 | - repo: https://github.com/hasnep/pre-commit-roc 45 | rev: v0.1.0 46 | hooks: 47 | - name: Lint Roc files 48 | id: check 49 | args: [src/main.roc] 50 | - name: Format Roc files 51 | id: format 52 | 53 | # YAML 54 | - repo: local 55 | hooks: 56 | - name: Format YAML files 57 | id: yaml-format 58 | language: system 59 | entry: prettier --write 60 | types: [yaml] 61 | 62 | # Markdown 63 | - repo: local 64 | hooks: 65 | - name: Format markdown files 66 | id: markdown-format 67 | language: system 68 | entry: prettier --write 69 | types: [markdown] 70 | 71 | # GitHub Actions 72 | - repo: local 73 | hooks: 74 | - name: Validate GitHub Actions workflow files 75 | id: github-workflows-check 76 | language: system 77 | entry: actionlint 78 | types: [yaml] 79 | files: \.github/workflows/.*\.ya?ml$ 80 | - name: Check GitHub Actions are pinned 81 | id: github-workflows-check-pinned 82 | language: system 83 | entry: ratchet check 84 | types: [yaml] 85 | files: \.github/workflows/.*\.ya?ml$ 86 | 87 | # Nix 88 | - repo: local 89 | hooks: 90 | - name: Format Nix files 91 | id: nix-format 92 | language: system 93 | entry: nixfmt 94 | types: [nix] 95 | -------------------------------------------------------------------------------- /src/SafeStr.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | SafeStr, 3 | to_str, 4 | escape, 5 | dangerously_mark_safe, 6 | with_capacity, 7 | concat, 8 | reserve, 9 | ] 10 | 11 | ## An opaque type used to keep track of the fact that we know a string is HTML-safe. 12 | SafeStr := Str 13 | 14 | ## Escape a string so that it can safely be used in HTML text nodes or attributes. 15 | ## This is the function that should usually be used for converting a Str to a SafeStr. 16 | escape : Str -> SafeStr 17 | escape = |str| 18 | (encoded_bytes, is_original_string_fine) = 19 | # We allocate more bytes than the original string had because we'll need extra bytes 20 | # if there are any characters we need to escape. My choice of the proportion 3/2 21 | # was arbitrary. 22 | capacity = Str.count_utf8_bytes(str) |> Num.div_ceil(2) |> Num.mul(3) 23 | bytes = List.with_capacity(capacity) 24 | # Look at each byte in the string. It's important to look at each codepoint, not the 25 | # whole graheme. (See the test below.) Because we're only looking for `"`, `&`, `'`, 26 | # `<`, and `>`, each of which are one byte, it's fine to just iterate over the bytes. 27 | # In UTF-8, the only bytes that ever start with a 0 are the single-byte codepoints, so 28 | # these bytes will never appear in the middle of a codepoint. 29 | Str.walk_utf8( 30 | str, 31 | (bytes, Bool.true), 32 | |(bytes_so_far, is_original_string_fine_so_far), byte| 33 | when byte is 34 | 34 -> (List.concat(bytes_so_far, Str.to_utf8(""")), Bool.false) # " must be escaped 35 | 38 -> (List.concat(bytes_so_far, Str.to_utf8("&")), Bool.false) # & must be escaped 36 | 39 -> (List.concat(bytes_so_far, Str.to_utf8("'")), Bool.false) # ' must be escaped 37 | 60 -> (List.concat(bytes_so_far, Str.to_utf8("<")), Bool.false) # < must be escaped 38 | 62 -> (List.concat(bytes_so_far, Str.to_utf8(">")), Bool.false) # > must be escaped 39 | _ -> (List.append(bytes_so_far, byte), is_original_string_fine_so_far), 40 | ) # All other bytes are fine! 41 | if is_original_string_fine then 42 | # If we didn't do any replacements, we might as well use the original string 43 | # so that Roc can free encodedBytes and avoid re-validating the UTF-8. 44 | @SafeStr(str) 45 | else 46 | when Str.from_utf8(encoded_bytes) is 47 | Ok(s) -> @SafeStr(s) 48 | Err(BadUtf8(_)) -> 49 | crash("SafeStr.escape: bad utf8. This should not be possible; please report this bug to roc-html.") 50 | 51 | ## Convert a SafeStr to a regular Str. 52 | to_str : SafeStr -> Str 53 | to_str = |@SafeStr(str)| str 54 | 55 | expect to_str(escape("

abc

")) == "<h1>abc</h1>" 56 | expect to_str(escape("abc")) == "abc" 57 | expect to_str(escape("折り紙🕊")) == "折り紙🕊" 58 | expect to_str(escape("é é")) == "é é" 59 | expect to_str(escape("﷽ᄀᄀᄀ각ᆨᆨ🇺🇸각नीநி")) == "﷽ᄀᄀᄀ각ᆨᆨ🇺🇸각नीநி" 60 | expect to_str(escape("﷽&ᄀᄀᄀ각ᆨᆨ🇺🇸각नीநி")) == "﷽&ᄀᄀᄀ각ᆨᆨ🇺🇸각नीநி" 61 | expect to_str(escape("'&\"<>")) == "'&"<>" 62 | # Note: This sometimes displays incorrectly in VSCode. It's a family emoji. 63 | expect to_str(escape("👩‍👩‍👦‍👦")) == "👩‍👩‍👦‍👦" 64 | expect to_str(escape("&👩‍👩‍👦‍👦&")) == "&👩‍👩‍👦‍👦&" 65 | expect to_str(escape("`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?")) == "`~!@#$%^&*()-=_+[]\\{}|;':",./<>?" 66 | # Even though this string doesn't contain the *grapheme* "<" or the grapheme ">", 67 | # it does contain the *codepoints* "<" and ">". The browser still interprets them 68 | # as HTML tags, so it's important to escape them. 69 | expect to_str(escape("something؀

͏bad؀

͏something")) == "something؀<h1>͏bad؀</h1>͏something" 70 | 71 | ## Mark a string as safe for HTML without actually escaping it. 72 | ## DO NOT use this function unless you know the input string is safe. 73 | ## NEVER use this function on user input. 74 | dangerously_mark_safe : Str -> SafeStr 75 | dangerously_mark_safe = |str| @SafeStr(str) 76 | 77 | expect 78 | bad_str = "" 79 | to_str(dangerously_mark_safe(bad_str)) == bad_str 80 | 81 | ## The SafeStr equivalent of Str.withCapacity 82 | with_capacity : U64 -> SafeStr 83 | with_capacity = |capacity| 84 | @SafeStr(Str.with_capacity(capacity)) 85 | 86 | expect to_str(with_capacity(10)) == "" 87 | 88 | ## The SafeStr equivalent of Str.reserve 89 | reserve : SafeStr, U64 -> SafeStr 90 | reserve = |@SafeStr(str), additional_capacity| 91 | @SafeStr(Str.reserve(str, additional_capacity)) 92 | 93 | expect "abc>" |> escape |> reserve(50) |> to_str == "abc>" 94 | 95 | ## The SafeStr equivalent of Str.concat 96 | concat : SafeStr, SafeStr -> SafeStr 97 | concat = |@SafeStr(str1), @SafeStr(str2)| 98 | # If two strings are HTML-safe, their concatenation must be too. 99 | @SafeStr(Str.concat(str1, str2)) 100 | 101 | expect escape("3>2") |> concat(escape("2<3")) |> to_str == "3>22<3" 102 | -------------------------------------------------------------------------------- /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-parts": { 20 | "inputs": { 21 | "nixpkgs-lib": "nixpkgs-lib" 22 | }, 23 | "locked": { 24 | "lastModified": 1760948891, 25 | "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", 26 | "owner": "hercules-ci", 27 | "repo": "flake-parts", 28 | "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "hercules-ci", 33 | "repo": "flake-parts", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils": { 38 | "inputs": { 39 | "systems": "systems" 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 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1761373498, 58 | "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", 59 | "owner": "nixos", 60 | "repo": "nixpkgs", 61 | "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "nixos", 66 | "ref": "nixos-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "nixpkgs-lib": { 72 | "locked": { 73 | "lastModified": 1754788789, 74 | "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", 75 | "owner": "nix-community", 76 | "repo": "nixpkgs.lib", 77 | "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "nix-community", 82 | "repo": "nixpkgs.lib", 83 | "type": "github" 84 | } 85 | }, 86 | "nixpkgs_2": { 87 | "locked": { 88 | "lastModified": 1722403750, 89 | "narHash": "sha256-tRmn6UiFAPX0m9G1AVcEPjWEOc9BtGsxGcs7Bz3MpsM=", 90 | "owner": "nixos", 91 | "repo": "nixpkgs", 92 | "rev": "184957277e885c06a505db112b35dfbec7c60494", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "nixos", 97 | "repo": "nixpkgs", 98 | "rev": "184957277e885c06a505db112b35dfbec7c60494", 99 | "type": "github" 100 | } 101 | }, 102 | "roc": { 103 | "inputs": { 104 | "flake-compat": "flake-compat", 105 | "flake-utils": "flake-utils", 106 | "nixpkgs": "nixpkgs_2", 107 | "rust-overlay": "rust-overlay" 108 | }, 109 | "locked": { 110 | "lastModified": 1757329258, 111 | "narHash": "sha256-pPnOM4hpbAkGCV47aw5eHbpOujjFtJa3v/3/D8gybO8=", 112 | "owner": "roc-lang", 113 | "repo": "roc", 114 | "rev": "d73ea109cc21442da01387c1e5e911607c74692d", 115 | "type": "github" 116 | }, 117 | "original": { 118 | "owner": "roc-lang", 119 | "ref": "alpha4-rolling", 120 | "repo": "roc", 121 | "type": "github" 122 | } 123 | }, 124 | "root": { 125 | "inputs": { 126 | "flake-parts": "flake-parts", 127 | "nixpkgs": "nixpkgs", 128 | "roc": "roc" 129 | } 130 | }, 131 | "rust-overlay": { 132 | "inputs": { 133 | "nixpkgs": [ 134 | "roc", 135 | "nixpkgs" 136 | ] 137 | }, 138 | "locked": { 139 | "lastModified": 1736303309, 140 | "narHash": "sha256-IKrk7RL+Q/2NC6+Ql6dwwCNZI6T6JH2grTdJaVWHF0A=", 141 | "owner": "oxalica", 142 | "repo": "rust-overlay", 143 | "rev": "a0b81d4fa349d9af1765b0f0b4a899c13776f706", 144 | "type": "github" 145 | }, 146 | "original": { 147 | "owner": "oxalica", 148 | "repo": "rust-overlay", 149 | "type": "github" 150 | } 151 | }, 152 | "systems": { 153 | "locked": { 154 | "lastModified": 1681028828, 155 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 156 | "owner": "nix-systems", 157 | "repo": "default", 158 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 159 | "type": "github" 160 | }, 161 | "original": { 162 | "owner": "nix-systems", 163 | "repo": "default", 164 | "type": "github" 165 | } 166 | } 167 | }, 168 | "root": "root", 169 | "version": 7 170 | } 171 | -------------------------------------------------------------------------------- /src/Attribute.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | Attribute, 3 | attribute, 4 | accept, 5 | accept_charset, 6 | accesskey, 7 | action, 8 | align, 9 | allow, 10 | alt, 11 | async, 12 | autocapitalize, 13 | autocomplete, 14 | autofocus, 15 | autoplay, 16 | background, 17 | bgcolor, 18 | border, 19 | buffered, 20 | capture, 21 | challenge, 22 | charset, 23 | checked, 24 | cite, 25 | class, 26 | classes, 27 | code, 28 | codebase, 29 | color, 30 | cols, 31 | colspan, 32 | content, 33 | contenteditable, 34 | contextmenu, 35 | controls, 36 | coords, 37 | crossorigin, 38 | csp, 39 | data, 40 | datetime, 41 | decoding, 42 | default, 43 | defer, 44 | dir, 45 | dirname, 46 | disabled, 47 | download, 48 | draggable, 49 | enctype, 50 | enterkeyhint, 51 | for, 52 | form, 53 | formaction, 54 | formenctype, 55 | formmethod, 56 | formnovalidate, 57 | formtarget, 58 | headers, 59 | height, 60 | hidden, 61 | high, 62 | href, 63 | hreflang, 64 | http_equiv, 65 | icon, 66 | id, 67 | importance, 68 | inputmode, 69 | integrity, 70 | intrinsicsize, 71 | ismap, 72 | itemprop, 73 | keytype, 74 | kind, 75 | label, 76 | lang, 77 | language, 78 | list, 79 | loading, 80 | loop, 81 | low, 82 | manifest, 83 | max, 84 | maxlength, 85 | media, 86 | method, 87 | min, 88 | minlength, 89 | multiple, 90 | muted, 91 | name, 92 | novalidate, 93 | open, 94 | optimum, 95 | pattern, 96 | ping, 97 | placeholder, 98 | poster, 99 | preload, 100 | radiogroup, 101 | readonly, 102 | referrerpolicy, 103 | rel, 104 | required, 105 | reversed, 106 | role, 107 | rows, 108 | rowspan, 109 | sandbox, 110 | scope, 111 | scoped, 112 | selected, 113 | shape, 114 | size, 115 | sizes, 116 | slot, 117 | span, 118 | spellcheck, 119 | src, 120 | srcdoc, 121 | srclang, 122 | srcset, 123 | start, 124 | step, 125 | style, 126 | summary, 127 | tabindex, 128 | target, 129 | title, 130 | translate, 131 | type, 132 | usemap, 133 | value, 134 | width, 135 | wrap, 136 | ] 137 | 138 | Attribute : [Attribute Str Str] 139 | 140 | ## Define a non-standard attribute. 141 | ## You can use this to add attributes that are not already supported. 142 | attribute : Str -> (Str -> Attribute) 143 | attribute = |attr_name| 144 | |attr_value| Attribute(attr_name, attr_value) 145 | 146 | ## Construct a `accept` attribute. 147 | accept : Str -> Attribute 148 | accept = attribute("accept") 149 | 150 | ## Construct a `accept-charset` attribute. 151 | accept_charset : Str -> Attribute 152 | accept_charset = attribute("accept-charset") 153 | 154 | ## Construct a `accesskey` attribute. 155 | accesskey : Str -> Attribute 156 | accesskey = attribute("accesskey") 157 | 158 | ## Construct a `action` attribute. 159 | action : Str -> Attribute 160 | action = attribute("action") 161 | 162 | ## Construct a `align` attribute. 163 | align : Str -> Attribute 164 | align = attribute("align") 165 | 166 | ## Construct a `allow` attribute. 167 | allow : Str -> Attribute 168 | allow = attribute("allow") 169 | 170 | ## Construct a `alt` attribute. 171 | alt : Str -> Attribute 172 | alt = attribute("alt") 173 | 174 | ## Construct a `async` attribute. 175 | async : Str -> Attribute 176 | async = attribute("async") 177 | 178 | ## Construct a `autocapitalize` attribute. 179 | autocapitalize : Str -> Attribute 180 | autocapitalize = attribute("autocapitalize") 181 | 182 | ## Construct a `autocomplete` attribute. 183 | autocomplete : Str -> Attribute 184 | autocomplete = attribute("autocomplete") 185 | 186 | ## Construct a `autofocus` attribute. 187 | autofocus : Str -> Attribute 188 | autofocus = attribute("autofocus") 189 | 190 | ## Construct a `autoplay` attribute. 191 | autoplay : Str -> Attribute 192 | autoplay = attribute("autoplay") 193 | 194 | ## Construct a `background` attribute. 195 | background : Str -> Attribute 196 | background = attribute("background") 197 | 198 | ## Construct a `bgcolor` attribute. 199 | bgcolor : Str -> Attribute 200 | bgcolor = attribute("bgcolor") 201 | 202 | ## Construct a `border` attribute. 203 | border : Str -> Attribute 204 | border = attribute("border") 205 | 206 | ## Construct a `buffered` attribute. 207 | buffered : Str -> Attribute 208 | buffered = attribute("buffered") 209 | 210 | ## Construct a `capture` attribute. 211 | capture : Str -> Attribute 212 | capture = attribute("capture") 213 | 214 | ## Construct a `challenge` attribute. 215 | challenge : Str -> Attribute 216 | challenge = attribute("challenge") 217 | 218 | ## Construct a `charset` attribute. 219 | charset : Str -> Attribute 220 | charset = attribute("charset") 221 | 222 | ## Construct a `checked` attribute. 223 | checked : Str -> Attribute 224 | checked = attribute("checked") 225 | 226 | ## Construct a `cite` attribute. 227 | cite : Str -> Attribute 228 | cite = attribute("cite") 229 | 230 | ## Construct a `class` attribute. 231 | ## 232 | ## See the [classes] function to apply multiple classes simultaneously. 233 | class : Str -> Attribute 234 | class = attribute("class") 235 | 236 | ## Construct a `class` attribute from a list of classes. 237 | ## 238 | ## See the [class] function to apply a single class. 239 | ## For example: 240 | ## ``` 241 | ## classes(["a", "b", "c", "a"]) == class("a b c") 242 | ## ``` 243 | classes : List Str -> Attribute 244 | classes = |list_of_classes| 245 | unique_classes = Set.to_list(Set.from_list(list_of_classes)) 246 | class(Str.join_with(unique_classes, " ")) 247 | 248 | expect 249 | out = classes(["a", "b", "c", "a"]) 250 | out == class("a b c") 251 | 252 | ## Construct a `code` attribute. 253 | code : Str -> Attribute 254 | code = attribute("code") 255 | 256 | ## Construct a `codebase` attribute. 257 | codebase : Str -> Attribute 258 | codebase = attribute("codebase") 259 | 260 | ## Construct a `color` attribute. 261 | color : Str -> Attribute 262 | color = attribute("color") 263 | 264 | ## Construct a `cols` attribute. 265 | cols : Str -> Attribute 266 | cols = attribute("cols") 267 | 268 | ## Construct a `colspan` attribute. 269 | colspan : Str -> Attribute 270 | colspan = attribute("colspan") 271 | 272 | ## Construct a `content` attribute. 273 | content : Str -> Attribute 274 | content = attribute("content") 275 | 276 | ## Construct a `contenteditable` attribute. 277 | contenteditable : Str -> Attribute 278 | contenteditable = attribute("contenteditable") 279 | 280 | ## Construct a `contextmenu` attribute. 281 | contextmenu : Str -> Attribute 282 | contextmenu = attribute("contextmenu") 283 | 284 | ## Construct a `controls` attribute. 285 | controls : Str -> Attribute 286 | controls = attribute("controls") 287 | 288 | ## Construct a `coords` attribute. 289 | coords : Str -> Attribute 290 | coords = attribute("coords") 291 | 292 | ## Construct a `crossorigin` attribute. 293 | crossorigin : Str -> Attribute 294 | crossorigin = attribute("crossorigin") 295 | 296 | ## Construct a `csp` attribute. 297 | csp : Str -> Attribute 298 | csp = attribute("csp") 299 | 300 | ## Construct a `data` attribute. 301 | data : Str -> Attribute 302 | data = attribute("data") 303 | 304 | ## Construct a `datetime` attribute. 305 | datetime : Str -> Attribute 306 | datetime = attribute("datetime") 307 | 308 | ## Construct a `decoding` attribute. 309 | decoding : Str -> Attribute 310 | decoding = attribute("decoding") 311 | 312 | ## Construct a `default` attribute. 313 | default : Str -> Attribute 314 | default = attribute("default") 315 | 316 | ## Construct a `defer` attribute. 317 | defer : Str -> Attribute 318 | defer = attribute("defer") 319 | 320 | ## Construct a `dir` attribute. 321 | dir : Str -> Attribute 322 | dir = attribute("dir") 323 | 324 | ## Construct a `dirname` attribute. 325 | dirname : Str -> Attribute 326 | dirname = attribute("dirname") 327 | 328 | ## Construct a `disabled` attribute. 329 | disabled : Str -> Attribute 330 | disabled = attribute("disabled") 331 | 332 | ## Construct a `download` attribute. 333 | download : Str -> Attribute 334 | download = attribute("download") 335 | 336 | ## Construct a `draggable` attribute. 337 | draggable : Str -> Attribute 338 | draggable = attribute("draggable") 339 | 340 | ## Construct a `enctype` attribute. 341 | enctype : Str -> Attribute 342 | enctype = attribute("enctype") 343 | 344 | ## Construct a `enterkeyhint` attribute. 345 | enterkeyhint : Str -> Attribute 346 | enterkeyhint = attribute("enterkeyhint") 347 | 348 | ## Construct a `for` attribute. 349 | for : Str -> Attribute 350 | for = attribute("for") 351 | 352 | ## Construct a `form` attribute. 353 | form : Str -> Attribute 354 | form = attribute("form") 355 | 356 | ## Construct a `formaction` attribute. 357 | formaction : Str -> Attribute 358 | formaction = attribute("formaction") 359 | 360 | ## Construct a `formenctype` attribute. 361 | formenctype : Str -> Attribute 362 | formenctype = attribute("formenctype") 363 | 364 | ## Construct a `formmethod` attribute. 365 | formmethod : Str -> Attribute 366 | formmethod = attribute("formmethod") 367 | 368 | ## Construct a `formnovalidate` attribute. 369 | formnovalidate : Str -> Attribute 370 | formnovalidate = attribute("formnovalidate") 371 | 372 | ## Construct a `formtarget` attribute. 373 | formtarget : Str -> Attribute 374 | formtarget = attribute("formtarget") 375 | 376 | ## Construct a `headers` attribute. 377 | headers : Str -> Attribute 378 | headers = attribute("headers") 379 | 380 | ## Construct a `height` attribute. 381 | height : Str -> Attribute 382 | height = attribute("height") 383 | 384 | ## Construct a `hidden` attribute. 385 | hidden : Str -> Attribute 386 | hidden = attribute("hidden") 387 | 388 | ## Construct a `high` attribute. 389 | high : Str -> Attribute 390 | high = attribute("high") 391 | 392 | ## Construct a `href` attribute. 393 | href : Str -> Attribute 394 | href = attribute("href") 395 | 396 | ## Construct a `hreflang` attribute. 397 | hreflang : Str -> Attribute 398 | hreflang = attribute("hreflang") 399 | 400 | ## Construct a `http-equiv` attribute. 401 | http_equiv : Str -> Attribute 402 | http_equiv = attribute("http-equiv") 403 | 404 | ## Construct a `icon` attribute. 405 | icon : Str -> Attribute 406 | icon = attribute("icon") 407 | 408 | ## Construct a `id` attribute. 409 | id : Str -> Attribute 410 | id = attribute("id") 411 | 412 | ## Construct a `importance` attribute. 413 | importance : Str -> Attribute 414 | importance = attribute("importance") 415 | 416 | ## Construct a `inputmode` attribute. 417 | inputmode : Str -> Attribute 418 | inputmode = attribute("inputmode") 419 | 420 | ## Construct a `integrity` attribute. 421 | integrity : Str -> Attribute 422 | integrity = attribute("integrity") 423 | 424 | ## Construct a `intrinsicsize` attribute. 425 | intrinsicsize : Str -> Attribute 426 | intrinsicsize = attribute("intrinsicsize") 427 | 428 | ## Construct a `ismap` attribute. 429 | ismap : Str -> Attribute 430 | ismap = attribute("ismap") 431 | 432 | ## Construct a `itemprop` attribute. 433 | itemprop : Str -> Attribute 434 | itemprop = attribute("itemprop") 435 | 436 | ## Construct a `keytype` attribute. 437 | keytype : Str -> Attribute 438 | keytype = attribute("keytype") 439 | 440 | ## Construct a `kind` attribute. 441 | kind : Str -> Attribute 442 | kind = attribute("kind") 443 | 444 | ## Construct a `label` attribute. 445 | label : Str -> Attribute 446 | label = attribute("label") 447 | 448 | ## Construct a `lang` attribute. 449 | lang : Str -> Attribute 450 | lang = attribute("lang") 451 | 452 | ## Construct a `language` attribute. 453 | language : Str -> Attribute 454 | language = attribute("language") 455 | 456 | ## Construct a `list` attribute. 457 | list : Str -> Attribute 458 | list = attribute("list") 459 | 460 | ## Construct a `loading` attribute. 461 | loading : Str -> Attribute 462 | loading = attribute("loading") 463 | 464 | ## Construct a `loop` attribute. 465 | loop : Str -> Attribute 466 | loop = attribute("loop") 467 | 468 | ## Construct a `low` attribute. 469 | low : Str -> Attribute 470 | low = attribute("low") 471 | 472 | ## Construct a `manifest` attribute. 473 | manifest : Str -> Attribute 474 | manifest = attribute("manifest") 475 | 476 | ## Construct a `max` attribute. 477 | max : Str -> Attribute 478 | max = attribute("max") 479 | 480 | ## Construct a `maxlength` attribute. 481 | maxlength : Str -> Attribute 482 | maxlength = attribute("maxlength") 483 | 484 | ## Construct a `media` attribute. 485 | media : Str -> Attribute 486 | media = attribute("media") 487 | 488 | ## Construct a `method` attribute. 489 | method : Str -> Attribute 490 | method = attribute("method") 491 | 492 | ## Construct a `min` attribute. 493 | min : Str -> Attribute 494 | min = attribute("min") 495 | 496 | ## Construct a `minlength` attribute. 497 | minlength : Str -> Attribute 498 | minlength = attribute("minlength") 499 | 500 | ## Construct a `multiple` attribute. 501 | multiple : Str -> Attribute 502 | multiple = attribute("multiple") 503 | 504 | ## Construct a `muted` attribute. 505 | muted : Str -> Attribute 506 | muted = attribute("muted") 507 | 508 | ## Construct a `name` attribute. 509 | name : Str -> Attribute 510 | name = attribute("name") 511 | 512 | ## Construct a `novalidate` attribute. 513 | novalidate : Str -> Attribute 514 | novalidate = attribute("novalidate") 515 | 516 | ## Construct a `open` attribute. 517 | open : Str -> Attribute 518 | open = attribute("open") 519 | 520 | ## Construct a `optimum` attribute. 521 | optimum : Str -> Attribute 522 | optimum = attribute("optimum") 523 | 524 | ## Construct a `pattern` attribute. 525 | pattern : Str -> Attribute 526 | pattern = attribute("pattern") 527 | 528 | ## Construct a `ping` attribute. 529 | ping : Str -> Attribute 530 | ping = attribute("ping") 531 | 532 | ## Construct a `placeholder` attribute. 533 | placeholder : Str -> Attribute 534 | placeholder = attribute("placeholder") 535 | 536 | ## Construct a `poster` attribute. 537 | poster : Str -> Attribute 538 | poster = attribute("poster") 539 | 540 | ## Construct a `preload` attribute. 541 | preload : Str -> Attribute 542 | preload = attribute("preload") 543 | 544 | ## Construct a `radiogroup` attribute. 545 | radiogroup : Str -> Attribute 546 | radiogroup = attribute("radiogroup") 547 | 548 | ## Construct a `readonly` attribute. 549 | readonly : Str -> Attribute 550 | readonly = attribute("readonly") 551 | 552 | ## Construct a `referrerpolicy` attribute. 553 | referrerpolicy : Str -> Attribute 554 | referrerpolicy = attribute("referrerpolicy") 555 | 556 | ## Construct a `rel` attribute. 557 | rel : Str -> Attribute 558 | rel = attribute("rel") 559 | 560 | ## Construct a `required` attribute. 561 | required : Str -> Attribute 562 | required = attribute("required") 563 | 564 | ## Construct a `reversed` attribute. 565 | reversed : Str -> Attribute 566 | reversed = attribute("reversed") 567 | 568 | ## Construct a `role` attribute. 569 | role : Str -> Attribute 570 | role = attribute("role") 571 | 572 | ## Construct a `rows` attribute. 573 | rows : Str -> Attribute 574 | rows = attribute("rows") 575 | 576 | ## Construct a `rowspan` attribute. 577 | rowspan : Str -> Attribute 578 | rowspan = attribute("rowspan") 579 | 580 | ## Construct a `sandbox` attribute. 581 | sandbox : Str -> Attribute 582 | sandbox = attribute("sandbox") 583 | 584 | ## Construct a `scope` attribute. 585 | scope : Str -> Attribute 586 | scope = attribute("scope") 587 | 588 | ## Construct a `scoped` attribute. 589 | scoped : Str -> Attribute 590 | scoped = attribute("scoped") 591 | 592 | ## Construct a `selected` attribute. 593 | selected : Str -> Attribute 594 | selected = attribute("selected") 595 | 596 | ## Construct a `shape` attribute. 597 | shape : Str -> Attribute 598 | shape = attribute("shape") 599 | 600 | ## Construct a `size` attribute. 601 | size : Str -> Attribute 602 | size = attribute("size") 603 | 604 | ## Construct a `sizes` attribute. 605 | sizes : Str -> Attribute 606 | sizes = attribute("sizes") 607 | 608 | ## Construct a `slot` attribute. 609 | slot : Str -> Attribute 610 | slot = attribute("slot") 611 | 612 | ## Construct a `span` attribute. 613 | span : Str -> Attribute 614 | span = attribute("span") 615 | 616 | ## Construct a `spellcheck` attribute. 617 | spellcheck : Str -> Attribute 618 | spellcheck = attribute("spellcheck") 619 | 620 | ## Construct a `src` attribute. 621 | src : Str -> Attribute 622 | src = attribute("src") 623 | 624 | ## Construct a `srcdoc` attribute. 625 | srcdoc : Str -> Attribute 626 | srcdoc = attribute("srcdoc") 627 | 628 | ## Construct a `srclang` attribute. 629 | srclang : Str -> Attribute 630 | srclang = attribute("srclang") 631 | 632 | ## Construct a `srcset` attribute. 633 | srcset : Str -> Attribute 634 | srcset = attribute("srcset") 635 | 636 | ## Construct a `start` attribute. 637 | start : Str -> Attribute 638 | start = attribute("start") 639 | 640 | ## Construct a `step` attribute. 641 | step : Str -> Attribute 642 | step = attribute("step") 643 | 644 | ## Construct a `style` attribute. 645 | style : Str -> Attribute 646 | style = attribute("style") 647 | 648 | ## Construct a `summary` attribute. 649 | summary : Str -> Attribute 650 | summary = attribute("summary") 651 | 652 | ## Construct a `tabindex` attribute. 653 | tabindex : Str -> Attribute 654 | tabindex = attribute("tabindex") 655 | 656 | ## Construct a `target` attribute. 657 | target : Str -> Attribute 658 | target = attribute("target") 659 | 660 | ## Construct a `title` attribute. 661 | title : Str -> Attribute 662 | title = attribute("title") 663 | 664 | ## Construct a `translate` attribute. 665 | translate : Str -> Attribute 666 | translate = attribute("translate") 667 | 668 | ## Construct a `type` attribute. 669 | type : Str -> Attribute 670 | type = attribute("type") 671 | 672 | ## Construct a `usemap` attribute. 673 | usemap : Str -> Attribute 674 | usemap = attribute("usemap") 675 | 676 | ## Construct a `value` attribute. 677 | value : Str -> Attribute 678 | value = attribute("value") 679 | 680 | ## Construct a `width` attribute. 681 | width : Str -> Attribute 682 | width = attribute("width") 683 | 684 | ## Construct a `wrap` attribute. 685 | wrap : Str -> Attribute 686 | wrap = attribute("wrap") 687 | -------------------------------------------------------------------------------- /src/Html.roc: -------------------------------------------------------------------------------- 1 | module [ 2 | Node, 3 | text, 4 | element, 5 | void_element, 6 | render, 7 | render_without_doc_type, 8 | dangerously_include_unescaped_html, 9 | # Content sectioning 10 | address, 11 | article, 12 | aside, 13 | footer, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | header, 21 | main, 22 | nav, 23 | section, 24 | # Demarcating edits 25 | del, 26 | ins, 27 | # Document metadata 28 | base, 29 | head, 30 | link, 31 | meta, 32 | style, 33 | title, 34 | # Embedded content 35 | embed, 36 | iframe, 37 | object, 38 | picture, 39 | portal, 40 | source, 41 | # Forms 42 | button, 43 | datalist, 44 | fieldset, 45 | form, 46 | input, 47 | label, 48 | legend, 49 | meter, 50 | optgroup, 51 | option, 52 | output, 53 | progress, 54 | select, 55 | textarea, 56 | # Image and multimedia 57 | area, 58 | audio, 59 | img, 60 | map, 61 | track, 62 | video, 63 | # Inline text semantics 64 | a, 65 | abbr, 66 | b, 67 | bdi, 68 | bdo, 69 | br, 70 | cite, 71 | code, 72 | data, 73 | dfn, 74 | em, 75 | i, 76 | kbd, 77 | mark, 78 | q, 79 | rp, 80 | rt, 81 | ruby, 82 | s, 83 | samp, 84 | small, 85 | span, 86 | strong, 87 | sub, 88 | sup, 89 | time, 90 | u, 91 | var, 92 | wbr, 93 | # Interactive elements 94 | details, 95 | dialog, 96 | summary, 97 | # Main root 98 | html, 99 | # SVG and MathML 100 | math, 101 | svg, 102 | # Scripting 103 | canvas, 104 | noscript, 105 | script, 106 | # Sectioning root 107 | body, 108 | # Table content 109 | caption, 110 | col, 111 | colgroup, 112 | table, 113 | tbody, 114 | td, 115 | tfoot, 116 | th, 117 | thead, 118 | tr, 119 | # Text content 120 | blockquote, 121 | dd, 122 | div, 123 | dl, 124 | dt, 125 | figcaption, 126 | figure, 127 | hr, 128 | li, 129 | menu, 130 | ol, 131 | p, 132 | pre, 133 | ul, 134 | # Web components 135 | slot, 136 | template, 137 | ] 138 | 139 | import Attribute exposing [Attribute, attribute] 140 | import SafeStr exposing [SafeStr, escape, dangerously_mark_safe] 141 | 142 | ## An HTML node, either an HTML element or some text inside an HTML element. 143 | Node : [Element Str U64 (List Attribute) (List Node), Text Str, UnescapedHtml Str] 144 | 145 | ## Create a `Text` node containing a string. 146 | ## 147 | ## The string will be escaped so that it's safely rendered as a text node even if the string contains HTML tags. 148 | ## 149 | ## ``` 150 | ## expect 151 | ## textNode = Html.text("") 152 | ## Html.render_without_doc_type(textNode) == "<script>alert('hi')</script>" 153 | ## ``` 154 | text : Str -> Node 155 | text = Text 156 | 157 | expect 158 | text_node = text("") 159 | render_without_doc_type(text_node) == "<script>alert('hi')</script>" 160 | 161 | ## Mark a string as safe for HTML without actually escaping it. 162 | ## 163 | ## DO NOT use this function unless you're sure the input string is safe. 164 | ## 165 | ## NEVER use this function on user input; use the `text` function instead. 166 | ## 167 | ## ``` 168 | ## expect 169 | ## htmlNode = Html.dangerously_include_unescaped_html("") 170 | ## Html.render_without_doc_type(htmlNode) == "" 171 | ## ``` 172 | dangerously_include_unescaped_html : Str -> Node 173 | dangerously_include_unescaped_html = UnescapedHtml 174 | 175 | expect 176 | html_node = dangerously_include_unescaped_html("") 177 | render_without_doc_type(html_node) == "" 178 | 179 | ## Define a non-standard HTML element. 180 | ## You can use this to add elements that are not already supported. 181 | ## 182 | ## For example, you could bring back the obsolete element and add some 90's nostalgia to your web page! 183 | ## 184 | ## ``` 185 | ## blink : List Attribute, List Node -> Node 186 | ## blink = element("blink") 187 | ## 188 | ## blink [] [ text("This text is blinking!") ] 189 | ## ``` 190 | element : Str -> (List Attribute, List Node -> Node) 191 | element = |tag_name| 192 | |attrs, children| 193 | # While building the node tree, calculate the size of Str it will render to 194 | with_tag = 2 * (3 + Str.count_utf8_bytes(tag_name)) 195 | with_attrs = List.walk( 196 | attrs, 197 | with_tag, 198 | |acc, Attribute(name, val)| 199 | acc + Str.count_utf8_bytes(name) + Str.count_utf8_bytes(val) + 4, 200 | ) 201 | total_size = List.walk( 202 | children, 203 | with_attrs, 204 | |acc, child| 205 | acc + node_size(child), 206 | ) 207 | 208 | Element(tag_name, total_size, attrs, children) 209 | 210 | ## Define a non-standard HTML [void element](https://developer.mozilla.org/en-US/docs/Glossary/Void_element). 211 | ## A void element is an element that cannot have any children. 212 | void_element : Str -> (List Attribute -> Node) 213 | void_element = |tag_name| 214 | |attrs| 215 | # While building the node tree, calculate the size of Str it will render to 216 | with_tag = 2 * (3 + Str.count_utf8_bytes(tag_name)) 217 | with_attrs = List.walk( 218 | attrs, 219 | with_tag, 220 | |acc, Attribute(name, val)| 221 | acc + Str.count_utf8_bytes(name) + Str.count_utf8_bytes(val) + 4, 222 | ) 223 | 224 | Element(tag_name, with_attrs, attrs, []) 225 | 226 | ## Internal helper to calculate the size of a node 227 | node_size : Node -> U64 228 | node_size = |node| 229 | when node is 230 | Text(content) -> 231 | # We allocate more bytes than the original string had because we'll need extra bytes 232 | # if there are any characters we need to escape. My choice of the proportion 3/2 233 | # was arbitrary. 234 | Str.count_utf8_bytes(content) |> Num.div_ceil(2) |> Num.mul(3) 235 | 236 | UnescapedHtml(content) -> 237 | Str.count_utf8_bytes(content) 238 | 239 | Element(_, size, _, _) -> 240 | size 241 | 242 | ## Render a Node to an HTML string 243 | ## 244 | ## The output has no whitespace between nodes, to make it small. 245 | ## This is intended for generating full HTML documents, so it automatically adds `` to the start of the string. 246 | ## See also `render_without_doc_type`. 247 | render : Node -> Str 248 | render = |node| 249 | buffer = SafeStr.reserve(dangerously_mark_safe(""), node_size(node)) 250 | 251 | render_help(buffer, node) 252 | |> SafeStr.to_str 253 | 254 | expect 255 | example_document = html([], [body([], [p([(attribute("example"))("test")], [text("Hello, World!")])])]) 256 | out = render(example_document) 257 | out == "

Hello, World!

" 258 | 259 | expect 260 | example_document = html([], [body([], [p([(attribute("example"))("test")], [text("")])])]) 261 | out = render(example_document) 262 | out == "

<script>alert('hi')</script>

" 263 | 264 | expect 265 | example_document = html([], [body([], [p([(attribute("example"))("test")], [dangerously_include_unescaped_html("")])])]) 266 | out = render(example_document) 267 | out == "

" 268 | 269 | expect 270 | example_document = html([], [body([], [base([]), link([]), meta([]), embed([]), source([]), input([]), area([]), img([]), track([]), br([]), wbr([]), col([]), hr([])])]) 271 | out = render(example_document) 272 | out == "

" 273 | 274 | ## Render a Node to a string, without a `!DOCTYPE` tag. 275 | render_without_doc_type : Node -> Str 276 | render_without_doc_type = |node| 277 | buffer = SafeStr.with_capacity(node_size(node)) 278 | 279 | render_help(buffer, node) 280 | |> SafeStr.to_str 281 | 282 | ## An internal helper to render a node to a string buffer. 283 | render_help : SafeStr, Node -> SafeStr 284 | render_help = |buffer, node| 285 | when node is 286 | Text(content) -> 287 | SafeStr.concat(buffer, escape(content)) 288 | 289 | UnescapedHtml(content) -> 290 | SafeStr.concat(buffer, dangerously_mark_safe(content)) 291 | 292 | Element(tag_name, _, attrs, children) -> 293 | when tag_name is 294 | # Special case for void elements 295 | "base" | "link" | "meta" | "embed" | "source" | "input" | "area" | "img" | "track" | "br" | "wbr" | "col" | "hr" -> 296 | buffer 297 | |> SafeStr.concat(dangerously_mark_safe("<")) 298 | |> SafeStr.concat(dangerously_mark_safe(tag_name)) 299 | |> |with_tag_name| 300 | if List.is_empty(attrs) then 301 | with_tag_name 302 | else 303 | List.walk(attrs, with_tag_name, render_attr) 304 | |> SafeStr.concat(dangerously_mark_safe(">")) # Don't use self-closing tag syntax for void elements 305 | 306 | _ -> 307 | buffer 308 | |> SafeStr.concat(dangerously_mark_safe("<")) 309 | |> SafeStr.concat(dangerously_mark_safe(tag_name)) 310 | |> |with_tag_name| 311 | if List.is_empty(attrs) then 312 | with_tag_name 313 | else 314 | List.walk(attrs, with_tag_name, render_attr) 315 | |> SafeStr.concat(dangerously_mark_safe(">")) 316 | |> |with_tag| List.walk(children, with_tag, render_help) 317 | |> SafeStr.concat(dangerously_mark_safe(" SafeStr.concat(dangerously_mark_safe(tag_name)) 319 | |> SafeStr.concat(dangerously_mark_safe(">")) 320 | 321 | ## An internal helper to render an attribute to a string buffer. 322 | render_attr : SafeStr, Attribute -> SafeStr 323 | render_attr = |buffer, Attribute(key, value)| 324 | buffer 325 | |> SafeStr.concat(dangerously_mark_safe(" ")) 326 | |> SafeStr.concat(dangerously_mark_safe(key)) 327 | |> SafeStr.concat(dangerously_mark_safe("=\"")) 328 | |> SafeStr.concat(escape(value)) 329 | |> SafeStr.concat(dangerously_mark_safe("\"")) 330 | 331 | # Content sectioning 332 | 333 | ## Construct a `address` element. 334 | address : List Attribute, List Node -> Node 335 | address = element("address") 336 | 337 | ## Construct a `article` element. 338 | article : List Attribute, List Node -> Node 339 | article = element("article") 340 | 341 | ## Construct a `aside` element. 342 | aside : List Attribute, List Node -> Node 343 | aside = element("aside") 344 | 345 | ## Construct a `footer` element. 346 | footer : List Attribute, List Node -> Node 347 | footer = element("footer") 348 | 349 | ## Construct a `h1` element. 350 | h1 : List Attribute, List Node -> Node 351 | h1 = element("h1") 352 | 353 | ## Construct a `h2` element. 354 | h2 : List Attribute, List Node -> Node 355 | h2 = element("h2") 356 | 357 | ## Construct a `h3` element. 358 | h3 : List Attribute, List Node -> Node 359 | h3 = element("h3") 360 | 361 | ## Construct a `h4` element. 362 | h4 : List Attribute, List Node -> Node 363 | h4 = element("h4") 364 | 365 | ## Construct a `h5` element. 366 | h5 : List Attribute, List Node -> Node 367 | h5 = element("h5") 368 | 369 | ## Construct a `h6` element. 370 | h6 : List Attribute, List Node -> Node 371 | h6 = element("h6") 372 | 373 | ## Construct a `header` element. 374 | header : List Attribute, List Node -> Node 375 | header = element("header") 376 | 377 | ## Construct a `main` element. 378 | main : List Attribute, List Node -> Node 379 | main = element("main") 380 | 381 | ## Construct a `nav` element. 382 | nav : List Attribute, List Node -> Node 383 | nav = element("nav") 384 | 385 | ## Construct a `section` element. 386 | section : List Attribute, List Node -> Node 387 | section = element("section") 388 | 389 | # Demarcating edits 390 | 391 | ## Construct a `del` element. 392 | del : List Attribute, List Node -> Node 393 | del = element("del") 394 | 395 | ## Construct a `ins` element. 396 | ins : List Attribute, List Node -> Node 397 | ins = element("ins") 398 | 399 | # Document metadata 400 | 401 | ## Construct a `base` element. 402 | base : List Attribute -> Node 403 | base = void_element("base") 404 | 405 | ## Construct a `head` element. 406 | head : List Attribute, List Node -> Node 407 | head = element("head") 408 | 409 | ## Construct a `link` element. 410 | link : List Attribute -> Node 411 | link = void_element("link") 412 | 413 | ## Construct a `meta` element. 414 | meta : List Attribute -> Node 415 | meta = void_element("meta") 416 | 417 | ## Construct a `style` element. 418 | style : List Attribute, List Node -> Node 419 | style = element("style") 420 | 421 | ## Construct a `title` element. 422 | title : List Attribute, List Node -> Node 423 | title = element("title") 424 | 425 | # Embedded content 426 | 427 | ## Construct a `embed` element. 428 | embed : List Attribute -> Node 429 | embed = void_element("embed") 430 | 431 | ## Construct a `iframe` element. 432 | iframe : List Attribute, List Node -> Node 433 | iframe = element("iframe") 434 | 435 | ## Construct a `object` element. 436 | object : List Attribute, List Node -> Node 437 | object = element("object") 438 | 439 | ## Construct a `picture` element. 440 | picture : List Attribute, List Node -> Node 441 | picture = element("picture") 442 | 443 | ## Construct a `portal` element. 444 | portal : List Attribute, List Node -> Node 445 | portal = element("portal") 446 | 447 | ## Construct a `source` element. 448 | source : List Attribute -> Node 449 | source = void_element("source") 450 | 451 | # Forms 452 | 453 | ## Construct a `button` element. 454 | button : List Attribute, List Node -> Node 455 | button = element("button") 456 | 457 | ## Construct a `datalist` element. 458 | datalist : List Attribute, List Node -> Node 459 | datalist = element("datalist") 460 | 461 | ## Construct a `fieldset` element. 462 | fieldset : List Attribute, List Node -> Node 463 | fieldset = element("fieldset") 464 | 465 | ## Construct a `form` element. 466 | form : List Attribute, List Node -> Node 467 | form = element("form") 468 | 469 | ## Construct a `input` element. 470 | input : List Attribute -> Node 471 | input = void_element("input") 472 | 473 | ## Construct a `label` element. 474 | label : List Attribute, List Node -> Node 475 | label = element("label") 476 | 477 | ## Construct a `legend` element. 478 | legend : List Attribute, List Node -> Node 479 | legend = element("legend") 480 | 481 | ## Construct a `meter` element. 482 | meter : List Attribute, List Node -> Node 483 | meter = element("meter") 484 | 485 | ## Construct a `optgroup` element. 486 | optgroup : List Attribute, List Node -> Node 487 | optgroup = element("optgroup") 488 | 489 | ## Construct a `option` element. 490 | option : List Attribute, List Node -> Node 491 | option = element("option") 492 | 493 | ## Construct a `output` element. 494 | output : List Attribute, List Node -> Node 495 | output = element("output") 496 | 497 | ## Construct a `progress` element. 498 | progress : List Attribute, List Node -> Node 499 | progress = element("progress") 500 | 501 | ## Construct a `select` element. 502 | select : List Attribute, List Node -> Node 503 | select = element("select") 504 | 505 | ## Construct a `textarea` element. 506 | textarea : List Attribute, List Node -> Node 507 | textarea = element("textarea") 508 | 509 | # Image and multimedia 510 | 511 | ## Construct a `area` element. 512 | area : List Attribute -> Node 513 | area = void_element("area") 514 | 515 | ## Construct a `audio` element. 516 | audio : List Attribute, List Node -> Node 517 | audio = element("audio") 518 | 519 | ## Construct a `img` element. 520 | img : List Attribute -> Node 521 | img = void_element("img") 522 | 523 | ## Construct a `map` element. 524 | map : List Attribute, List Node -> Node 525 | map = element("map") 526 | 527 | ## Construct a `track` element. 528 | track : List Attribute -> Node 529 | track = void_element("track") 530 | 531 | ## Construct a `video` element. 532 | video : List Attribute, List Node -> Node 533 | video = element("video") 534 | 535 | # Inline text semantics 536 | 537 | ## Construct a `a` element. 538 | a : List Attribute, List Node -> Node 539 | a = element("a") 540 | 541 | ## Construct a `abbr` element. 542 | abbr : List Attribute, List Node -> Node 543 | abbr = element("abbr") 544 | 545 | ## Construct a `b` element. 546 | b : List Attribute, List Node -> Node 547 | b = element("b") 548 | 549 | ## Construct a `bdi` element. 550 | bdi : List Attribute, List Node -> Node 551 | bdi = element("bdi") 552 | 553 | ## Construct a `bdo` element. 554 | bdo : List Attribute, List Node -> Node 555 | bdo = element("bdo") 556 | 557 | ## Construct a `br` element. 558 | br : List Attribute -> Node 559 | br = void_element("br") 560 | 561 | ## Construct a `cite` element. 562 | cite : List Attribute, List Node -> Node 563 | cite = element("cite") 564 | 565 | ## Construct a `code` element. 566 | code : List Attribute, List Node -> Node 567 | code = element("code") 568 | 569 | ## Construct a `data` element. 570 | data : List Attribute, List Node -> Node 571 | data = element("data") 572 | 573 | ## Construct a `dfn` element. 574 | dfn : List Attribute, List Node -> Node 575 | dfn = element("dfn") 576 | 577 | ## Construct a `em` element. 578 | em : List Attribute, List Node -> Node 579 | em = element("em") 580 | 581 | ## Construct a `i` element. 582 | i : List Attribute, List Node -> Node 583 | i = element("i") 584 | 585 | ## Construct a `kbd` element. 586 | kbd : List Attribute, List Node -> Node 587 | kbd = element("kbd") 588 | 589 | ## Construct a `mark` element. 590 | mark : List Attribute, List Node -> Node 591 | mark = element("mark") 592 | 593 | ## Construct a `q` element. 594 | q : List Attribute, List Node -> Node 595 | q = element("q") 596 | 597 | ## Construct a `rp` element. 598 | rp : List Attribute, List Node -> Node 599 | rp = element("rp") 600 | 601 | ## Construct a `rt` element. 602 | rt : List Attribute, List Node -> Node 603 | rt = element("rt") 604 | 605 | ## Construct a `ruby` element. 606 | ruby : List Attribute, List Node -> Node 607 | ruby = element("ruby") 608 | 609 | ## Construct a `s` element. 610 | s : List Attribute, List Node -> Node 611 | s = element("s") 612 | 613 | ## Construct a `samp` element. 614 | samp : List Attribute, List Node -> Node 615 | samp = element("samp") 616 | 617 | ## Construct a `small` element. 618 | small : List Attribute, List Node -> Node 619 | small = element("small") 620 | 621 | ## Construct a `span` element. 622 | span : List Attribute, List Node -> Node 623 | span = element("span") 624 | 625 | ## Construct a `strong` element. 626 | strong : List Attribute, List Node -> Node 627 | strong = element("strong") 628 | 629 | ## Construct a `sub` element. 630 | sub : List Attribute, List Node -> Node 631 | sub = element("sub") 632 | 633 | ## Construct a `sup` element. 634 | sup : List Attribute, List Node -> Node 635 | sup = element("sup") 636 | 637 | ## Construct a `time` element. 638 | time : List Attribute, List Node -> Node 639 | time = element("time") 640 | 641 | ## Construct a `u` element. 642 | u : List Attribute, List Node -> Node 643 | u = element("u") 644 | 645 | ## Construct a `var` element. 646 | var : List Attribute, List Node -> Node 647 | var = element("var") 648 | 649 | ## Construct a `wbr` element. 650 | wbr : List Attribute -> Node 651 | wbr = void_element("wbr") 652 | 653 | # Interactive elements 654 | 655 | ## Construct a `details` element. 656 | details : List Attribute, List Node -> Node 657 | details = element("details") 658 | 659 | ## Construct a `dialog` element. 660 | dialog : List Attribute, List Node -> Node 661 | dialog = element("dialog") 662 | 663 | ## Construct a `summary` element. 664 | summary : List Attribute, List Node -> Node 665 | summary = element("summary") 666 | 667 | # Main root 668 | 669 | ## Construct a `html` element. 670 | html : List Attribute, List Node -> Node 671 | html = element("html") 672 | 673 | # SVG and MathML 674 | 675 | ## Construct a `math` element. 676 | math : List Attribute, List Node -> Node 677 | math = element("math") 678 | 679 | ## Construct a `svg` element. 680 | svg : List Attribute, List Node -> Node 681 | svg = element("svg") 682 | 683 | # Scripting 684 | 685 | ## Construct a `canvas` element. 686 | canvas : List Attribute, List Node -> Node 687 | canvas = element("canvas") 688 | 689 | ## Construct a `noscript` element. 690 | noscript : List Attribute, List Node -> Node 691 | noscript = element("noscript") 692 | 693 | ## Construct a `script` element. 694 | script : List Attribute, List Node -> Node 695 | script = element("script") 696 | 697 | # Sectioning root 698 | 699 | ## Construct a `body` element. 700 | body : List Attribute, List Node -> Node 701 | body = element("body") 702 | 703 | # Table content 704 | 705 | ## Construct a `caption` element. 706 | caption : List Attribute, List Node -> Node 707 | caption = element("caption") 708 | 709 | ## Construct a `col` element. 710 | col : List Attribute -> Node 711 | col = void_element("col") 712 | 713 | ## Construct a `colgroup` element. 714 | colgroup : List Attribute, List Node -> Node 715 | colgroup = element("colgroup") 716 | 717 | ## Construct a `table` element. 718 | table : List Attribute, List Node -> Node 719 | table = element("table") 720 | 721 | ## Construct a `tbody` element. 722 | tbody : List Attribute, List Node -> Node 723 | tbody = element("tbody") 724 | 725 | ## Construct a `td` element. 726 | td : List Attribute, List Node -> Node 727 | td = element("td") 728 | 729 | ## Construct a `tfoot` element. 730 | tfoot : List Attribute, List Node -> Node 731 | tfoot = element("tfoot") 732 | 733 | ## Construct a `th` element. 734 | th : List Attribute, List Node -> Node 735 | th = element("th") 736 | 737 | ## Construct a `thead` element. 738 | thead : List Attribute, List Node -> Node 739 | thead = element("thead") 740 | 741 | ## Construct a `tr` element. 742 | tr : List Attribute, List Node -> Node 743 | tr = element("tr") 744 | 745 | # Text content 746 | 747 | ## Construct a `blockquote` element. 748 | blockquote : List Attribute, List Node -> Node 749 | blockquote = element("blockquote") 750 | 751 | ## Construct a `dd` element. 752 | dd : List Attribute, List Node -> Node 753 | dd = element("dd") 754 | 755 | ## Construct a `div` element. 756 | div : List Attribute, List Node -> Node 757 | div = element("div") 758 | 759 | ## Construct a `dl` element. 760 | dl : List Attribute, List Node -> Node 761 | dl = element("dl") 762 | 763 | ## Construct a `dt` element. 764 | dt : List Attribute, List Node -> Node 765 | dt = element("dt") 766 | 767 | ## Construct a `figcaption` element. 768 | figcaption : List Attribute, List Node -> Node 769 | figcaption = element("figcaption") 770 | 771 | ## Construct a `figure` element. 772 | figure : List Attribute, List Node -> Node 773 | figure = element("figure") 774 | 775 | ## Construct a `hr` element. 776 | hr : List Attribute -> Node 777 | hr = void_element("hr") 778 | 779 | ## Construct a `li` element. 780 | li : List Attribute, List Node -> Node 781 | li = element("li") 782 | 783 | ## Construct a `menu` element. 784 | menu : List Attribute, List Node -> Node 785 | menu = element("menu") 786 | 787 | ## Construct a `ol` element. 788 | ol : List Attribute, List Node -> Node 789 | ol = element("ol") 790 | 791 | ## Construct a `p` element. 792 | p : List Attribute, List Node -> Node 793 | p = element("p") 794 | 795 | ## Construct a `pre` element. 796 | pre : List Attribute, List Node -> Node 797 | pre = element("pre") 798 | 799 | ## Construct a `ul` element. 800 | ul : List Attribute, List Node -> Node 801 | ul = element("ul") 802 | 803 | # Web components 804 | 805 | ## Construct a `slot` element. 806 | slot : List Attribute, List Node -> Node 807 | slot = element("slot") 808 | 809 | ## Construct a `template` element. 810 | template : List Attribute, List Node -> Node 811 | template = element("template") 812 | --------------------------------------------------------------------------------