├── .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 | [![Package Version](https://img.shields.io/hexpm/v/html_lustre_converter)](https://hex.pm/packages/html_lustre_converter) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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 | "I will die mad that this element was removed" 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 | 217 | 218 | 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 | "wibble wobble" 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 | --------------------------------------------------------------------------------