├── .github
└── workflows
│ ├── deploy.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── app
├── .gitignore
├── README.md
├── gleam.toml
├── index.html
├── manifest.toml
├── src
│ └── app.gleam
├── tailwind.config.js
└── test
│ └── app_test.gleam
├── gleam.toml
├── manifest.toml
├── src
└── html_lustre_converter.gleam
└── test
└── html_lustre_converter_test.gleam
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 | on:
3 | push:
4 | branches: ["main"]
5 |
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 |
13 | concurrency:
14 | group: "pages"
15 | cancel-in-progress: false
16 |
17 | jobs:
18 | deploy:
19 | environment:
20 | name: github-pages
21 | url: ${{ steps.deployment.outputs.page_url }}
22 |
23 | runs-on: ubuntu-latest
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v4
27 | - uses: erlef/setup-beam@v1
28 | with:
29 | otp-version: "27"
30 | gleam-version: "1.10.0"
31 | rebar3-version: "3"
32 |
33 | - name: Build site
34 | run: |
35 | cd app
36 | gleam run -m lustre/dev -- build app --minify
37 | # TODO: remove once Lustre dev tools have a way to build deploy ready code
38 | cat index.html | sed 's|priv/static/app.css|app.min.css|' | sed 's|priv/static/app.mjs|app.min.mjs|' > priv/static/index.html
39 |
40 | - name: Setup Pages
41 | uses: actions/configure-pages@v4
42 |
43 | - name: Upload artifact
44 | uses: actions/upload-pages-artifact@v3
45 | with:
46 | path: 'app/priv/static'
47 |
48 | - name: Deploy to GitHub Pages
49 | id: deployment
50 | uses: actions/deploy-pages@v4
51 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | pull_request:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: denoland/setup-deno@v1
16 | with:
17 | deno-version: v1.x
18 | - uses: erlef/setup-beam@v1
19 | with:
20 | otp-version: "27"
21 | gleam-version: "1.10.0"
22 | rebar3-version: "3"
23 | # elixir-version: "1.15.4"
24 | - run: gleam deps download
25 | - run: gleam test
26 | - run: gleam format --check src test
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v1.4.2 - 2025-04-27
4 |
5 | - Fixed a bug with escaping slashes.
6 |
7 | ## v1.4.1 - 2025-04-27
8 |
9 | - Fixed a bug with whitespace preservation.
10 |
11 | ## v1.4.0 - 2024-12-07
12 |
13 | - Updated for `gleam_stdlib` v0.45.0.
14 |
15 | ## v1.3.1 - 2024-11-19
16 |
17 | - Fixed the generated code for the `style` element.
18 |
19 | ## v1.3.0 - 2024-11-10
20 |
21 | - Fixed the generated code for `script`, `title` and `option` elements.
22 | - Fixed the generated code for `text` elements inside an `svg` element.
23 |
24 | ## v1.2.0 - 2024-10-07
25 |
26 | - The generated code now uses the `html.text` function fully qualified.
27 |
28 | ## v1.1.0 - 2024-07-16
29 |
30 | - SVGs are now supported.
31 |
32 | ## v1.0.0 - 2024-05-22
33 |
34 | - Initial release.
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # html_lustre_converter
2 |
3 | The Lustreizer. Convert regular HTML markup into Lustre syntax.
4 |
5 | [](https://hex.pm/packages/html_lustre_converter)
6 | [](https://hexdocs.pm/html_lustre_converter/)
7 |
8 | This package depends on the `javascript_dom_parser` package, which only works in
9 | the browser. If you wish to run this using the Deno runtime you will need to
10 | call the `install_polyfill` function from the `javascript_dom_parser/deno_polyfill`
11 | module. It may not be possible to use this library elsewhere.
12 |
13 | ```sh
14 | gleam add html_lustre_converter
15 | ```
16 | ```gleam
17 | import html_lustre_converter
18 |
19 | pub fn main() {
20 | "
Hello, Joe!
"
21 | |> html_lustre_converter.convert
22 | |> should.equal("html.h1([], [text(\"Hello, Joe!\")])")
23 | }
24 | ```
25 |
26 | Further documentation can be found at .
27 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /priv/static
4 | /build
5 | erl_crash.dump
6 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # App
2 |
3 | Ooooh! It's the converter wrapped in a website so you can use it from your
4 | browser. How convenient ✨
5 |
--------------------------------------------------------------------------------
/app/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "app"
2 | version = "1.0.0"
3 | target = "javascript"
4 |
5 | # Fill out these fields if you intend to generate HTML documentation or publish
6 | # your project to the Hex package manager.
7 | #
8 | # description = ""
9 | # licences = ["Apache-2.0"]
10 | # repository = { type = "github", user = "username", repo = "project" }
11 | # links = [{ title = "Website", href = "https://gleam.run" }]
12 | #
13 | # For a full reference of all the available options, you can have a look at
14 | # https://gleam.run/writing-gleam/gleam-toml/.
15 |
16 | [dependencies]
17 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
18 | lustre = ">= 4.2.4 and < 5.0.0"
19 | html_lustre_converter = { path = ".." }
20 | plinth = ">= 0.2.0 and < 1.0.0"
21 | gleam_javascript = ">= 0.8.0 and < 1.0.0"
22 |
23 | [dev-dependencies]
24 | gleeunit = ">= 1.0.0 and < 2.0.0"
25 | lustre_dev_tools = ">= 1.3.2 and < 2.0.0"
26 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | HTML Lustre converter
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
6 | { name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" },
7 | { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" },
8 | { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
9 | { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
10 | { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
11 | { name = "fs", version = "11.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "DD00A61D89EAC01D16D3FC51D5B0EB5F0722EF8E3C1A3A547CD086957F3260A9" },
12 | { name = "glam", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "4932A2D139AB0389E149396407F89654928D7B815E212BB02F13C66F53B1BBA1" },
13 | { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
14 | { name = "gleam_community_colour", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "FDD6AC62C6EC8506C005949A4FCEF032038191D5EAAEC3C9A203CD53AE956ACA" },
15 | { name = "gleam_crypto", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "917BC8B87DBD584830E3B389CBCAB140FFE7CB27866D27C6D0FB87A9ECF35602" },
16 | { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" },
17 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
18 | { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" },
19 | { name = "gleam_httpc", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "1A38507AF26CACA145248733688703EADCB734EA971D4E34FB97B7613DECF132" },
20 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" },
21 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
22 | { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
23 | { name = "gleam_package_interface", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "C2D2CA097831D27A20DAFA62D44F5D1B12E8470272337FD133368ACA4969A317" },
24 | { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
25 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
26 | { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
27 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
28 | { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" },
29 | { name = "glisten", version = "7.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "1A53CF9FB3231A93FF7F1BD519A43DC968C1722F126CDD278403A78725FC5189" },
30 | { name = "gramps", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "59194B3980110B403EE6B75330DB82CDE05FC8138491C2EAEACBC7AAEF30B2E8" },
31 | { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
32 | { name = "html_lustre_converter", version = "1.4.1", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib", "javascript_dom_parser"], source = "local", path = ".." },
33 | { name = "javascript_dom_parser", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "javascript_dom_parser", source = "hex", outer_checksum = "0E140ED4E2E0CDE6B747327BA62AA625B275DBD5F5871592AB15F674F7945B0A" },
34 | { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
35 | { name = "lustre", version = "4.6.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "CC59564624A4A1D855B5FEB55D979A072B328D0368E82A1639F180840D6288E9" },
36 | { name = "lustre_dev_tools", version = "1.7.1", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_deque", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_regexp", "gleam_stdlib", "glint", "glisten", "lustre", "mist", "repeatedly", "simplifile", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "B426F3E518B44144643CAE956D072E3ADAA9BBC71ECE08CD559CA0276A74C167" },
37 | { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
38 | { name = "mist", version = "4.0.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "ED319E5A7F2056E08340B6976EA5E717F3C3BB36056219AF826D280D9C077952" },
39 | { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
40 | { name = "plinth", version = "0.5.7", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A05130CA6A993FA6AC7557B5BE07CC3EC9DB4558F778BCECD2B3BE52CCCA9EEA" },
41 | { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" },
42 | { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" },
43 | { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
44 | { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
45 | { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
46 | { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
47 | { name = "wisp", version = "1.6.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "AE1C568FE30718C358D3B37666DF0A0743ECD96094AD98C9F4921475075F660A" },
48 | ]
49 |
50 | [requirements]
51 | gleam_javascript = { version = ">= 0.8.0 and < 1.0.0" }
52 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
53 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
54 | html_lustre_converter = { path = ".." }
55 | lustre = { version = ">= 4.2.4 and < 5.0.0" }
56 | lustre_dev_tools = { version = ">= 1.3.2 and < 2.0.0" }
57 | plinth = { version = ">= 0.2.0 and < 1.0.0" }
58 |
--------------------------------------------------------------------------------
/app/src/app.gleam:
--------------------------------------------------------------------------------
1 | import gleam/javascript/promise
2 | import html_lustre_converter
3 | import lustre
4 | import lustre/attribute
5 | import lustre/effect.{type Effect}
6 | import lustre/element.{type Element}
7 | import lustre/element/html
8 | import lustre/event
9 | import plinth/browser/clipboard
10 | import plinth/javascript/global
11 |
12 | pub fn main() {
13 | let app = lustre.application(init, update, view)
14 | let assert Ok(_) = lustre.start(app, "#app", Nil)
15 | Nil
16 | }
17 |
18 | pub type Model {
19 | Model(html: String, rendered_lustre: String, copy_button_text: String)
20 | }
21 |
22 | const copy_button_default_text = "Copy 🧟"
23 |
24 | const copy_button_copied_text = "Copied 🥰"
25 |
26 | fn init(_flags) -> #(Model, Effect(e)) {
27 | let model =
28 | Model(
29 | html: "",
30 | rendered_lustre: "",
31 | copy_button_text: copy_button_default_text,
32 | )
33 | #(model, effect.none())
34 | }
35 |
36 | pub type Msg {
37 | UserUpdatedHtml(String)
38 | UserClickedCopy
39 | CopyFeedbackWindowEnded
40 | }
41 |
42 | pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
43 | case msg {
44 | UserClickedCopy -> {
45 | let copy_effect = effect.from(copy_to_clipboard(_, model.rendered_lustre))
46 | let model = Model(..model, copy_button_text: copy_button_copied_text)
47 | #(model, copy_effect)
48 | }
49 |
50 | CopyFeedbackWindowEnded -> {
51 | let model = Model(..model, copy_button_text: copy_button_default_text)
52 | #(model, effect.none())
53 | }
54 |
55 | UserUpdatedHtml(html) -> {
56 | let rendered_lustre = html_lustre_converter.convert(html)
57 | let model = Model(..model, html:, rendered_lustre:)
58 | #(model, effect.none())
59 | }
60 | }
61 | }
62 |
63 | fn view(model: Model) -> Element(Msg) {
64 | html.div([attribute.class("grid grid-cols-2 font-mono h-screen")], [
65 | html.section(
66 | [
67 | attribute.class(
68 | "block w-full h-full border-2 border-r-1 border-[#ffaff3]",
69 | ),
70 | ],
71 | [
72 | html.textarea(
73 | [
74 | attribute.class("bg-transparent p-4 block w-full h-full"),
75 | attribute.placeholder(
76 | "Hello!\nPaste your HTML here and I'll convert it to Lustre",
77 | ),
78 | event.on_input(UserUpdatedHtml),
79 | ],
80 | model.html,
81 | ),
82 | ],
83 | ),
84 | html.section(
85 | [
86 | attribute.class(
87 | "bg-[#282c34] border-2 border-l-1 border-[#ffaff3] relative",
88 | ),
89 | ],
90 | [
91 | html.textarea(
92 | [
93 | attribute.class(
94 | "bg-transparent text-gray-300 p-4 block w-full h-full",
95 | ),
96 | ],
97 | model.rendered_lustre,
98 | ),
99 | html.button(
100 | [
101 | event.on_click(UserClickedCopy),
102 | attribute.class(
103 | "absolute bottom-3 right-3 bg-[#ffaff3] py-2 px-3 rounded-md font-bold transition-opacity hover:opacity-75",
104 | ),
105 | ],
106 | [element.text(model.copy_button_text)],
107 | ),
108 | ],
109 | ),
110 | ])
111 | }
112 |
113 | fn copy_to_clipboard(dispatch: fn(Msg) -> Nil, text: String) -> Nil {
114 | {
115 | use _ <- promise.map(clipboard.write_text(text))
116 | use <- global.set_timeout(1000)
117 | dispatch(CopyFeedbackWindowEnded)
118 | }
119 | Nil
120 | }
121 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./index.html", "./src/**/*.{gleam,mjs}"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | };
8 |
--------------------------------------------------------------------------------
/app/test/app_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "html_lustre_converter"
2 | version = "1.4.2"
3 | description = "Convert regular HTML markup into Lustre syntax"
4 | licences = ["Apache-2.0"]
5 | repository = { type = "github", user = "lpil", repo = "html-lustre-converter" }
6 | links = [{ title = "Lustre", href = "https://lustre.build" }]
7 |
8 | target = "javascript"
9 |
10 | [javascript]
11 | runtime = "deno"
12 |
13 | [javascript.deno]
14 | allow_read = ["test", "gleam.toml"]
15 |
16 | [dependencies]
17 | gleam_stdlib = ">= 0.45.0 and < 2.0.0"
18 | javascript_dom_parser = ">= 1.0.0 and < 2.0.0"
19 | glam = ">= 2.0.0 and < 3.0.0"
20 |
21 | [dev-dependencies]
22 | gleeunit = ">= 1.0.0 and < 2.0.0"
23 |
--------------------------------------------------------------------------------
/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "glam", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "4932A2D139AB0389E149396407F89654928D7B815E212BB02F13C66F53B1BBA1" },
6 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
7 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
8 | { name = "javascript_dom_parser", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "javascript_dom_parser", source = "hex", outer_checksum = "0E140ED4E2E0CDE6B747327BA62AA625B275DBD5F5871592AB15F674F7945B0A" },
9 | ]
10 |
11 | [requirements]
12 | glam = { version = ">= 2.0.0 and < 3.0.0" }
13 | gleam_stdlib = { version = ">= 0.45.0 and < 2.0.0" }
14 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
15 | javascript_dom_parser = { version = ">= 1.0.0 and < 2.0.0" }
16 |
--------------------------------------------------------------------------------
/src/html_lustre_converter.gleam:
--------------------------------------------------------------------------------
1 | import glam/doc.{type Document}
2 | import gleam/list
3 | import gleam/string
4 | import javascript_dom_parser.{type HtmlNode, Comment, Element, Text} as parser
5 |
6 | /// Convert a string of HTML in to the same document but using the Lustre HTML
7 | /// syntax.
8 | ///
9 | /// The resulting code is expected to be in a module with these imports:
10 | ///
11 | /// ```gleam
12 | /// import lustre/element/html
13 | /// import lustre/attribute.{attribute}
14 | /// import lustre/element.{element, text}
15 | /// ```
16 | ///
17 | /// If the source document contains SVGs, we need one more import from lustre/element:
18 | /// ```gleam
19 | /// import lustre/element.{element, text, svg}
20 | /// ```
21 | ///
22 | /// If you are only using SVGs, that's all you need to import
23 | /// ```gleam
24 | /// import lustre/element/svg
25 | /// ```
26 | ///
27 | pub fn convert(html: String) -> String {
28 | let documents =
29 | html
30 | |> parser.parse_to_records
31 | |> strip_body_wrapper(html)
32 | |> print_children(StripWhitespace, Html)
33 |
34 | case documents {
35 | [] -> doc.empty
36 | [document] -> document
37 | _ -> wrap(documents, "[", "]")
38 | }
39 | |> doc.to_string(80)
40 | }
41 |
42 | type WhitespaceMode {
43 | PreserveWhitespace
44 | StripWhitespace
45 | }
46 |
47 | type OutputMode {
48 | Svg
49 | Html
50 | }
51 |
52 | fn strip_body_wrapper(html: HtmlNode, source: String) -> List(HtmlNode) {
53 | let full_page = string.contains(source, "")
54 | case html {
55 | Element("HTML", [], [Element("HEAD", [], []), Element("BODY", [], nodes)])
56 | if !full_page
57 | -> nodes
58 | _ -> [html]
59 | }
60 | }
61 |
62 | fn print_text(t: String) -> Document {
63 | doc.from_string("html.text(" <> print_string(t) <> ")")
64 | }
65 |
66 | fn print_string(t: String) -> String {
67 | let string =
68 | t
69 | |> string.replace("\\", "\\\\")
70 | |> string.replace("\"", "\\\"")
71 | "\"" <> string <> "\""
72 | }
73 |
74 | fn print_svg_element(
75 | tag: String,
76 | attributes: List(#(String, String)),
77 | children: List(HtmlNode),
78 | ws: WhitespaceMode,
79 | ) -> Document {
80 | let tag = string.lowercase(tag)
81 | let attributes =
82 | list.map(attributes, fn(a) { print_attribute(a, Svg) })
83 | |> wrap("[", "]")
84 |
85 | case tag {
86 | // SVG non-container elements
87 | // Anmation elements
88 | "animate"
89 | | "animatemotion"
90 | | "animatetransform"
91 | | "mpath"
92 | | "set"
93 | | // Basic shapes
94 | "circle"
95 | | "ellipse"
96 | | "line"
97 | | "polygon"
98 | | "polyline"
99 | | "rect"
100 | | // Filter effects
101 | "feblend"
102 | | "fecolormatrix"
103 | | "fecomponenttransfer"
104 | | "fecomposite"
105 | | "feconvolvematrix"
106 | | "fedisplacementmap"
107 | | "fedropshadow"
108 | | "feflood"
109 | | "fefunca"
110 | | "fefuncb"
111 | | "fefuncg"
112 | | "fefuncr"
113 | | "fegaussianblur"
114 | | "feimage"
115 | | "femergenode"
116 | | "femorphology"
117 | | "feoffset"
118 | | "feturbulance"
119 | | // Gradient elements
120 | "stop"
121 | | // Graphical elements
122 | "image"
123 | | "path"
124 | | // Lighting elements
125 | "fedistantlight"
126 | | "fepointlight"
127 | | "fespotlight"
128 | | "title" -> {
129 | doc.from_string("svg." <> tag <> "(")
130 | |> doc.append(attributes)
131 | |> doc.append(doc.from_string(")"))
132 | }
133 |
134 | "textarea" -> {
135 | let content = doc.from_string(print_string(get_text_content(children)))
136 | doc.from_string("text." <> tag)
137 | |> doc.append(wrap([attributes, content], "(", ")"))
138 | }
139 |
140 | "text" -> {
141 | let content = doc.from_string(print_string(get_text_content(children)))
142 | doc.from_string("svg." <> tag)
143 | |> doc.append(wrap([attributes, content], "(", ")"))
144 | }
145 |
146 | "use" -> {
147 | doc.from_string("svg.use_")
148 | |> doc.append(attributes)
149 | }
150 |
151 | // SVG container elements
152 | "defs"
153 | | "g"
154 | | "marker"
155 | | "mask"
156 | | "missing-glyph"
157 | | "pattern"
158 | | "switch"
159 | | "symbol"
160 | | // Descriptive elements
161 | "desc"
162 | | "metadata"
163 | | // Filter effects
164 | "fediffuselighting"
165 | | "femerge"
166 | | "fespecularlighting"
167 | | "fetile"
168 | | // Gradient Elements
169 | "lineargradient"
170 | | "radialgradient" -> {
171 | let children = wrap(print_children(children, ws, Svg), "[", "]")
172 | doc.from_string("svg." <> string.replace(tag, "-", "_"))
173 | |> doc.append(wrap([attributes, children], "(", ")"))
174 | }
175 |
176 | _ -> {
177 | let children = wrap(print_children(children, ws, Svg), "[", "]")
178 | let tag = doc.from_string(print_string(tag))
179 | doc.from_string("element")
180 | |> doc.append(wrap([tag, attributes, children], "(", ")"))
181 | }
182 | }
183 | }
184 |
185 | fn print_element(
186 | tag: String,
187 | given_attributes: List(#(String, String)),
188 | children: List(HtmlNode),
189 | ws: WhitespaceMode,
190 | ) -> Document {
191 | let tag = string.lowercase(tag)
192 | let attributes =
193 | list.map(given_attributes, fn(a) { print_attribute(a, Html) })
194 | |> wrap("[", "]")
195 |
196 | case tag {
197 | "area"
198 | | "base"
199 | | "br"
200 | | "col"
201 | | "embed"
202 | | "hr"
203 | | "img"
204 | | "input"
205 | | "link"
206 | | "meta"
207 | | "param"
208 | | "source"
209 | | "track"
210 | | "wbr" -> {
211 | doc.from_string("html." <> tag <> "(")
212 | |> doc.append(attributes)
213 | |> doc.append(doc.from_string(")"))
214 | }
215 |
216 | "a"
217 | | "abbr"
218 | | "address"
219 | | "article"
220 | | "aside"
221 | | "audio"
222 | | "b"
223 | | "bdi"
224 | | "bdo"
225 | | "blockquote"
226 | | "body"
227 | | "button"
228 | | "canvas"
229 | | "caption"
230 | | "cite"
231 | | "code"
232 | | "colgroup"
233 | | "data"
234 | | "datalist"
235 | | "dd"
236 | | "del"
237 | | "details"
238 | | "dfn"
239 | | "dialog"
240 | | "div"
241 | | "dl"
242 | | "dt"
243 | | "em"
244 | | "fieldset"
245 | | "figcaption"
246 | | "figure"
247 | | "footer"
248 | | "form"
249 | | "h1"
250 | | "h2"
251 | | "h3"
252 | | "h4"
253 | | "h5"
254 | | "h6"
255 | | "head"
256 | | "header"
257 | | "hgroup"
258 | | "html"
259 | | "i"
260 | | "iframe"
261 | | "ins"
262 | | "kbd"
263 | | "label"
264 | | "legend"
265 | | "li"
266 | | "main"
267 | | "map"
268 | | "mark"
269 | | "math"
270 | | "menu"
271 | | "meter"
272 | | "nav"
273 | | "noscript"
274 | | "object"
275 | | "ol"
276 | | "optgroup"
277 | | "output"
278 | | "p"
279 | | "picture"
280 | | "portal"
281 | | "progress"
282 | | "q"
283 | | "rp"
284 | | "rt"
285 | | "ruby"
286 | | "s"
287 | | "samp"
288 | | "search"
289 | | "section"
290 | | "select"
291 | | "slot"
292 | | "small"
293 | | "span"
294 | | "strong"
295 | | "sub"
296 | | "summary"
297 | | "sup"
298 | | "table"
299 | | "tbody"
300 | | "td"
301 | | "template"
302 | | "text"
303 | | "tfoot"
304 | | "th"
305 | | "thead"
306 | | "time"
307 | | "tr"
308 | | "u"
309 | | "ul"
310 | | "var"
311 | | "video" -> {
312 | let children = wrap(print_children(children, ws, Html), "[", "]")
313 | doc.from_string("html." <> tag)
314 | |> doc.append(wrap([attributes, children], "(", ")"))
315 | }
316 |
317 | "svg" -> {
318 | let attributes =
319 | list.map(given_attributes, fn(a) { print_attribute(a, Svg) })
320 | |> wrap("[", "]")
321 |
322 | let children = wrap(print_children(children, ws, Svg), "[", "]")
323 | doc.from_string("svg.svg")
324 | |> doc.append(wrap([attributes, children], "(", ")"))
325 | }
326 |
327 | "pre" -> {
328 | let children =
329 | wrap(print_children(children, PreserveWhitespace, Html), "[", "]")
330 | doc.from_string("html." <> tag)
331 | |> doc.append(wrap([attributes, children], "(", ")"))
332 | }
333 |
334 | "script" | "style" | "textarea" | "title" | "option" -> {
335 | let content = doc.from_string(print_string(get_text_content(children)))
336 | doc.from_string("html." <> tag)
337 | |> doc.append(wrap([attributes, content], "(", ")"))
338 | }
339 |
340 | _ -> {
341 | let children = wrap(print_children(children, ws, Html), "[", "]")
342 | let tag = doc.from_string(print_string(tag))
343 | doc.from_string("element")
344 | |> doc.append(wrap([tag, attributes, children], "(", ")"))
345 | }
346 | }
347 | }
348 |
349 | fn get_text_content(nodes: List(HtmlNode)) -> String {
350 | list.filter_map(nodes, fn(node) {
351 | case node {
352 | Text(t) -> Ok(t)
353 | _ -> Error(Nil)
354 | }
355 | })
356 | |> string.concat
357 | }
358 |
359 | fn print_children(
360 | children: List(HtmlNode),
361 | ws: WhitespaceMode,
362 | mode: OutputMode,
363 | ) -> List(Document) {
364 | print_children_loop(children, ws, mode, [])
365 | }
366 |
367 | fn print_children_loop(
368 | in: List(HtmlNode),
369 | ws: WhitespaceMode,
370 | mode: OutputMode,
371 | acc: List(Document),
372 | ) -> List(Document) {
373 | case in {
374 | [] -> list.reverse(acc)
375 |
376 | [Element(tag, attrs, children), ..in] if mode == Svg -> {
377 | let child = print_svg_element(tag, attrs, children, ws)
378 | print_children_loop(in, ws, mode, [child, ..acc])
379 | }
380 |
381 | [Element(tag, attrs, children), ..in] -> {
382 | let child = print_element(tag, attrs, children, ws)
383 | print_children_loop(in, ws, mode, [child, ..acc])
384 | }
385 |
386 | [Comment(_), ..in] -> print_children_loop(in, ws, mode, acc)
387 |
388 | [Text(input), ..in] if ws == StripWhitespace -> {
389 | let trimmed = string.trim(input)
390 |
391 | let trimmed = case input {
392 | _ if trimmed == "" -> trimmed
393 | " " <> _ | "\t" <> _ | "\n" <> _ -> " " <> trimmed
394 | _ -> trimmed
395 | }
396 |
397 | let trimmed = case
398 | trimmed != ""
399 | && {
400 | string.ends_with(input, " ")
401 | || string.ends_with(input, "\n")
402 | || string.ends_with(input, "\t")
403 | }
404 | {
405 | True -> trimmed <> " "
406 | False -> trimmed
407 | }
408 |
409 | case trimmed {
410 | "" -> print_children_loop(in, ws, mode, acc)
411 | t -> print_children_loop(in, ws, mode, [print_text(t), ..acc])
412 | }
413 | }
414 |
415 | [Text(t), ..in] -> {
416 | print_children_loop(in, ws, mode, [print_text(t), ..acc])
417 | }
418 | }
419 | }
420 |
421 | fn print_attribute(attribute: #(String, String), mode: OutputMode) -> Document {
422 | case attribute.0 {
423 | "action"
424 | | "alt"
425 | | "attribute"
426 | | "autocomplete"
427 | | "class"
428 | | "download"
429 | | "enctype"
430 | | "for"
431 | | "form_action"
432 | | "form_enctype"
433 | | "form_method"
434 | | "form_target"
435 | | "href"
436 | | "id"
437 | | "map"
438 | | "max"
439 | | "method"
440 | | "min"
441 | | "msg"
442 | | "name"
443 | | "none"
444 | | "on"
445 | | "pattern"
446 | | "placeholder"
447 | | "rel"
448 | | "role"
449 | | "src"
450 | | "step"
451 | | "target"
452 | | "value"
453 | | "wrap" -> {
454 | doc.from_string(
455 | "attribute." <> attribute.0 <> "(" <> print_string(attribute.1) <> ")",
456 | )
457 | }
458 |
459 | "viewbox" ->
460 | doc.from_string(
461 | "attribute(\"viewBox\", " <> print_string(attribute.1) <> ")",
462 | )
463 |
464 | "type" ->
465 | doc.from_string("attribute.type_(" <> print_string(attribute.1) <> ")")
466 |
467 | "checked"
468 | | "controls"
469 | | "disabled"
470 | | "form_novalidate"
471 | | "loop"
472 | | "novalidate"
473 | | "readonly"
474 | | "required"
475 | | "selected" -> {
476 | doc.from_string("attribute." <> attribute.0 <> "(True)")
477 | }
478 |
479 | "width" | "height" | "cols" | "rows" -> {
480 | case mode {
481 | Svg -> {
482 | let children = [
483 | doc.from_string(print_string(attribute.0)),
484 | doc.from_string(print_string(attribute.1)),
485 | ]
486 | doc.from_string("attribute")
487 | |> doc.append(wrap(children, "(", ")"))
488 | }
489 | Html ->
490 | doc.from_string(
491 | "attribute." <> attribute.0 <> "(" <> attribute.1 <> ")",
492 | )
493 | }
494 | }
495 |
496 | _ -> {
497 | let children = [
498 | doc.from_string(print_string(attribute.0)),
499 | doc.from_string(print_string(attribute.1)),
500 | ]
501 | doc.from_string("attribute")
502 | |> doc.append(wrap(children, "(", ")"))
503 | }
504 | }
505 | }
506 |
507 | fn wrap(items: List(Document), open: String, close: String) -> Document {
508 | let comma = doc.concat([doc.from_string(","), doc.space])
509 | let open = doc.concat([doc.from_string(open), doc.soft_break])
510 | let trailing_comma = doc.break("", ",")
511 | let close = doc.concat([trailing_comma, doc.from_string(close)])
512 |
513 | items
514 | |> doc.join(with: comma)
515 | |> doc.prepend(open)
516 | |> doc.nest(by: 2)
517 | |> doc.append(close)
518 | |> doc.group
519 | }
520 |
--------------------------------------------------------------------------------
/test/html_lustre_converter_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 | import html_lustre_converter
4 | import javascript_dom_parser/deno_polyfill
5 |
6 | pub fn main() {
7 | deno_polyfill.install_polyfill()
8 | gleeunit.main()
9 | }
10 |
11 | pub fn empty_test() {
12 | ""
13 | |> html_lustre_converter.convert
14 | |> should.equal("")
15 | }
16 |
17 | pub fn h1_test() {
18 | ""
19 | |> html_lustre_converter.convert
20 | |> should.equal("html.h1([], [])")
21 | }
22 |
23 | pub fn h1_2_test() {
24 | ""
25 | |> html_lustre_converter.convert
26 | |> should.equal("[html.h1([], []), html.h1([], [])]")
27 | }
28 |
29 | pub fn h1_3_test() {
30 | ""
31 | |> html_lustre_converter.convert
32 | |> should.equal(
33 | "[
34 | html.h1([], []),
35 | html.h1([], []),
36 | html.h1([], []),
37 | html.h1([], []),
38 | html.h1([], []),
39 | ]",
40 | )
41 | }
42 |
43 | pub fn h1_4_test() {
44 | "Jello, Hoe!
"
45 | |> html_lustre_converter.convert
46 | |> should.equal("html.h1([], [html.text(\"Jello, Hoe!\")])")
47 | }
48 |
49 | pub fn text_test() {
50 | "Hello, Joe!"
51 | |> html_lustre_converter.convert
52 | |> should.equal("html.text(\"Hello, Joe!\")")
53 | }
54 |
55 | pub fn element_lustre_does_not_have_a_helper_for_test() {
56 | ""
57 | |> html_lustre_converter.convert
58 | |> should.equal(
59 | "element(
60 | \"marquee\",
61 | [],
62 | [html.text(\"I will die mad that this element was removed\")],
63 | )",
64 | )
65 | }
66 |
67 | pub fn attribute_test() {
68 | "The best site"
69 | |> html_lustre_converter.convert
70 | |> should.equal(
71 | "html.a([attribute.href(\"https://gleam.run/\")], [html.text(\"The best site\")])",
72 | )
73 | }
74 |
75 | pub fn other_attribute_test() {
76 | "The best site"
77 | |> html_lustre_converter.convert
78 | |> should.equal(
79 | "html.a([attribute(\"data-thing\", \"1\")], [html.text(\"The best site\")])",
80 | )
81 | }
82 |
83 | pub fn no_value_attribute_test() {
84 | ""
85 | |> html_lustre_converter.convert
86 | |> should.equal("html.p([attribute.type_(\"good\")], [])")
87 | }
88 |
89 | pub fn void_br_test() {
90 | "
"
91 | |> html_lustre_converter.convert
92 | |> should.equal("html.br([])")
93 | }
94 |
95 | pub fn void_br_with_attrs_test() {
96 | "
"
97 | |> html_lustre_converter.convert
98 | |> should.equal("html.br([attribute.class(\"good\")])")
99 | }
100 |
101 | pub fn its_already_a_page_test() {
102 | "HiYo"
103 | |> html_lustre_converter.convert
104 | |> should.equal(
105 | "html.html(
106 | [],
107 | [html.head([], [html.title([], \"Hi\")]), html.body([], [html.text(\"Yo\")])],
108 | )",
109 | )
110 | }
111 |
112 | pub fn its_already_a_page_1_test() {
113 | "Yo"
114 | |> html_lustre_converter.convert
115 | |> should.equal(
116 | "html.html([], [html.head([], []), html.body([], [html.text(\"Yo\")])])",
117 | )
118 | }
119 |
120 | pub fn text_with_a_quote_in_it_test() {
121 | "Here is a quote \" "
122 | |> html_lustre_converter.convert
123 | |> should.equal("html.text(\"Here is a quote \\\" \")")
124 | }
125 |
126 | pub fn non_string_attribute_test() {
127 | "
"
128 | |> html_lustre_converter.convert
129 | |> should.equal("html.br([attribute(\"autoplay\", \"\")])")
130 | }
131 |
132 | pub fn bool_attribute_test() {
133 | "
"
134 | |> html_lustre_converter.convert
135 | |> should.equal("html.br([attribute.required(True)])")
136 | }
137 |
138 | pub fn int_attribute_test() {
139 | "
"
140 | |> html_lustre_converter.convert
141 | |> should.equal("html.br([attribute.width(400)])")
142 | }
143 |
144 | pub fn full_page_test() {
145 | let code =
146 | "
147 |
148 |
149 |
150 | Hello!
151 |
152 |
153 | Goodbye!
154 |
155 |
156 | "
157 | |> html_lustre_converter.convert
158 |
159 | code
160 | |> should.equal(
161 | "html.html(
162 | [],
163 | [
164 | html.head([], [html.title([], \"Hello!\")]),
165 | html.body([], [html.h1([], [html.text(\"Goodbye!\")])]),
166 | ],
167 | )",
168 | )
169 | }
170 |
171 | pub fn comment_test() {
172 | ""
173 | |> html_lustre_converter.convert
174 | |> should.equal("html.h1([], [])")
175 | }
176 |
177 | pub fn trailing_whitespace_test() {
178 | "Hello
world
"
179 | |> html_lustre_converter.convert
180 | |> should.equal(
181 | "[html.h1([], [html.text(\"Hello \")]), html.h2([], [html.text(\"world\")])]",
182 | )
183 | }
184 |
185 | pub fn textarea_whitespace_test() {
186 | "
187 |
190 |
"
191 | |> html_lustre_converter.convert
192 | |> should.equal("html.div([], [html.textarea([], \" Hello!\n \")])")
193 | }
194 |
195 | pub fn pre_whitespace_test() {
196 | "
197 |
198 | Hello!
199 |
200 |
"
201 | |> html_lustre_converter.convert
202 | |> should.equal(
203 | "html.pre(
204 | [],
205 | [
206 | html.text(\" \"),
207 | html.code([], [html.text(\"\n Hello!\n \")]),
208 | html.text(\"\n \"),
209 | ],
210 | )",
211 | )
212 | }
213 |
214 | pub fn svg_test() {
215 | "
216 |
219 |
"
220 | |> html_lustre_converter.convert
221 | |> should.equal(
222 | "html.div(
223 | [],
224 | [
225 | svg.svg(
226 | [
227 | attribute(\"viewBox\", \"0 0 10\"),
228 | attribute(\"height\", \"10\"),
229 | attribute(\"width\", \"10\"),
230 | ],
231 | [svg.rect([attribute(\"height\", \"8\"), attribute(\"width\", \"8\")])],
232 | ),
233 | ],
234 | )",
235 | )
236 | }
237 |
238 | pub fn script_test() {
239 | ""
240 | |> html_lustre_converter.convert
241 | |> should.equal("html.div([], [html.script([], \"const a = 1\")])")
242 | }
243 |
244 | pub fn style_test() {
245 | ""
246 | |> html_lustre_converter.convert
247 | |> should.equal("html.div([], [html.style([], \"body { padding: 5px }\")])")
248 | }
249 |
250 | pub fn title_test() {
251 | "wibble wobble"
252 | |> html_lustre_converter.convert
253 | |> should.equal("html.div([], [html.title([], \"wibble wobble\")])")
254 | }
255 |
256 | pub fn option_test() {
257 | ""
258 | |> html_lustre_converter.convert
259 | |> should.equal("html.div([], [html.option([], \"wibble wobble\")])")
260 | }
261 |
262 | pub fn svg_text_test() {
263 | ""
264 | |> html_lustre_converter.convert
265 | |> should.equal("svg.svg([], [svg.text([], \"wibble wobble\")])")
266 | }
267 |
268 | pub fn whitespace_after_test() {
269 | "one two three
"
270 | |> html_lustre_converter.convert
271 | |> should.equal(
272 | "html.p(
273 | [],
274 | [
275 | html.text(\"one \"),
276 | html.a([attribute.href(\".\")], [html.text(\"two\")]),
277 | html.text(\" three\"),
278 | ],
279 | )",
280 | )
281 | }
282 |
283 | pub fn escape_test() {
284 | "\\"
285 | |> html_lustre_converter.convert
286 | |> should.equal("html.text(\"\\\\\")")
287 | }
288 |
--------------------------------------------------------------------------------