Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mix task to convert HTML into Temple #180

Merged
merged 1 commit into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir ref:v1.13.4
elixir 1.14.0-otp-25
erlang 25.0-rc2
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions guides/converting-html.md
Original file line number Diff line number Diff line change
@@ -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!
34 changes: 34 additions & 0 deletions lib/mix/tasks/temple.convert.ex
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions lib/temple/converter.ex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down
79 changes: 79 additions & 0 deletions test/temple/converter_test.exs
Original file line number Diff line number Diff line change
@@ -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 = """
<div class="container" disabled aria-label="alice">
<!-- this is a comment -->
I'm some content!
</div>
"""

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 = """
<div >
<!-- this is a comment
and this is some multi
stuff -->
</div>
"""

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 = """
<script>
console.log("ayy yoo");
</script>
<style>
.foo {
color: red;
}
</style>
"""

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