From 2f042506b68552e18e5c0125cf8ddd9ba7274738 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Sun, 11 Sep 2022 22:39:31 -0400 Subject: [PATCH] feat: Mix task to convert HTML into Temple (#180) --- .github/workflows/ci.yml | 14 ++--- .tool-versions | 2 +- CHANGELOG.md | 6 +- guides/converting-html.md | 25 ++++++++ lib/mix/tasks/temple.convert.ex | 34 +++++++++++ lib/temple/converter.ex | 103 ++++++++++++++++++++++++++++++++ mix.exs | 2 + mix.lock | 2 + test/temple/converter_test.exs | 79 ++++++++++++++++++++++++ 9 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 guides/converting-html.md create mode 100644 lib/mix/tasks/temple.convert.ex create mode 100644 lib/temple/converter.ex create mode 100644 test/temple/converter_test.exs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dda566a..126b9e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: strategy: matrix: - otp: [23.x, 24.x] - elixir: [1.13.x] + otp: [23.x, 24.x, 25.x] + elixir: [1.13.x, 1.14.x] steps: - uses: actions/checkout@v2 @@ -86,22 +86,22 @@ jobs: formatter: runs-on: ubuntu-latest - name: Formatter (1.13.x.x/24.x) + name: Formatter (1.14.x.x/25.x) steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 with: - otp-version: 24.x - elixir-version: 1.13.x + otp-version: 25.x + elixir-version: 1.14.x - uses: actions/cache@v3 with: path: | deps _build - key: ${{ runner.os }}-mix-24-1.13-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-mix-23-1.14-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-mix-24-1.13- + ${{ runner.os }}-mix-23-1.14- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' diff --git a/.tool-versions b/.tool-versions index 1b09c5a..fd164c8 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir ref:v1.13.4 +elixir 1.14.0-otp-25 erlang 25.0-rc2 diff --git a/CHANGELOG.md b/CHANGELOG.md index be7342e..9991a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## Main -### 0.9.0-rc.0 +### Enhancements + +- mix temple.convert task to convert HTML into Temple syntax. + +### 0.9.0 ### Breaking Changes diff --git a/guides/converting-html.md b/guides/converting-html.md new file mode 100644 index 0000000..d6ccebe --- /dev/null +++ b/guides/converting-html.md @@ -0,0 +1,25 @@ +# Converting HTML + +If you want to use something like [TailwindUI](https://tailwindui.com) with Temple, you're going to have to convert a ton of vanilla HTML into Temple syntax. + +Luckily, Temple provides a mix task for converting an HTML file into Temple syntax and writes it to stdout. + +## Usage + +First, we would want to create a temporary HTML file with the HTML we'd like to convert. + +> #### Hint {: .tip} +> +> The following examples use the `pbpaste` and `pbcopy` utilities found on macOS. These are used to send your clipboard contents into stdout and put stdout into your clipboard. + +```shell +$ pbpaste > temp.html +``` + +Then, we can convert that file and copy the output into our clipboard. + +```shell +$ mix temple.convert temp.html | pbcopy +``` + +Now, you are free to paste the new temple syntax into your project! diff --git a/lib/mix/tasks/temple.convert.ex b/lib/mix/tasks/temple.convert.ex new file mode 100644 index 0000000..4a0967a --- /dev/null +++ b/lib/mix/tasks/temple.convert.ex @@ -0,0 +1,34 @@ +defmodule Mix.Tasks.Temple.Convert do + use Mix.Task + + @shortdoc "A task to convert vanilla HTML into Temple syntax" + @moduledoc """ + This task is useful for converting a ton of HTML into Temple syntax. + + > #### Note about EEx and HEEx {: .tip} + > + > In the future, this should be able to convert EEx and HEEx as well, but that would involve invoking or forking their parsers. That is certainly doable, but is out of scope for what I needed right now. Contributions are welcome! + + ## Usage + + ```shell + $ mix temple.convert some_file.html + ``` + """ + + @doc false + def run(argv) do + case argv do + [] -> + Mix.raise( + "You need to provide the path to an HTML file you would like to convert to Temple syntax" + ) + + [file] -> + file + |> File.read!() + |> Temple.Converter.convert() + |> IO.puts() + end + end +end diff --git a/lib/temple/converter.ex b/lib/temple/converter.ex new file mode 100644 index 0000000..fd99b00 --- /dev/null +++ b/lib/temple/converter.ex @@ -0,0 +1,103 @@ +defmodule Temple.Converter do + @moduledoc false + + @boolean_attributes ~w[ + allowfullscreen + async + autofocus + autoplay + checked + controls + default + defer + disabled + formnovalidate + ismap + itemscope + loop + multiple + muted + nomodule + novalidate + open + playsinline + readonly + required + reversed + selected + truespeed + ] + + def convert(html) do + html + |> Floki.parse_fragment!() + |> to_temple() + |> :erlang.iolist_to_binary() + |> Code.format_string!() + |> :erlang.iolist_to_binary() + end + + def to_temple([]) do + [] + end + + def to_temple([{tag, attrs, children} | rest]) do + [ + to_string(tag), + " ", + to_temple_attrs(attrs), + " do\n", + to_temple(children), + "end\n" + ] ++ to_temple(rest) + end + + def to_temple([{:comment, comment} | rest]) do + [ + comment + |> String.split("\n") + |> Enum.map_join("\n", fn line -> + if String.trim(line) != "" do + "# #{line}" + else + "" + end + end), + "\n" + ] ++ to_temple(rest) + end + + def to_temple([text | rest]) when is_binary(text) do + [ + text + |> String.split("\n") + |> Enum.map_join("\n", fn line -> + if String.trim(line) != "" do + escaped = String.replace(line, ~s|"|, ~s|\\"|) + ~s|"#{String.trim(escaped)}"| + else + "" + end + end), + "\n" + ] ++ to_temple(rest) + end + + defp to_temple_attrs([]) do + "" + end + + defp to_temple_attrs(attrs) do + Enum.map_join(attrs, ", ", fn + {attr, _value} when attr in @boolean_attributes -> + to_attr_name(attr) <> ": true" + + {attr, value} -> + ~s|#{to_attr_name(attr)}: "#{value}"| + end) + end + + defp to_attr_name(name) do + String.replace(name, "-", "_") + end +end diff --git a/mix.exs b/mix.exs index b8b0a88..e692367 100644 --- a/mix.exs +++ b/mix.exs @@ -36,6 +36,7 @@ defmodule Temple.MixProject do "guides/getting-started.md", "guides/your-first-template.md", "guides/components.md", + "guides/converting-html.md", "guides/migrating/0.8-to-0.9.md" ], groups_for_extras: groups_for_extras() @@ -60,6 +61,7 @@ defmodule Temple.MixProject do defp deps do [ + {:floki, ">= 0.0.0"}, {:ex_doc, "~> 0.28.3", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index fbadb50..9b44c3a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"}, + "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, diff --git a/test/temple/converter_test.exs b/test/temple/converter_test.exs new file mode 100644 index 0000000..01562dc --- /dev/null +++ b/test/temple/converter_test.exs @@ -0,0 +1,79 @@ +defmodule Temple.ConverterTest do + use ExUnit.Case, async: true + + alias Temple.Converter + + describe "convert/1" do + test "converts basic html" do + # html + html = """ +
+ + I'm some content! +
+ """ + + assert Converter.convert(html) === + """ + div class: "container", disabled: true, aria_label: "alice" do + # this is a comment + + "I'm some content!" + end + """ + |> String.trim() + end + + test "multiline html comments" do + # html + html = """ +
+ +
+ """ + + assert Converter.convert(html) === + """ + div do + # this is a comment + # and this is some multi + + # stuff + end + """ + |> String.trim() + end + + test "script and style tag" do + # html + html = """ + + + + """ + + assert Converter.convert(html) |> tap(&IO.puts/1) === + """ + script do + "console.log(\\"ayy yoo\\");" + end + + style do + ".foo {" + "color: red;" + "}" + end + """ + |> String.trim() + end + end +end