mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Add converter
This commit is contained in:
parent
6569efcdbe
commit
88a28e646d
2 changed files with 248 additions and 0 deletions
|
@ -1,4 +1,5 @@
|
|||
[
|
||||
heex_line_length: 300,
|
||||
import_deps: [:ecto, :phoenix],
|
||||
inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
|
|
247
lib/mix/tasks/convert_to_heex.ex
Normal file
247
lib/mix/tasks/convert_to_heex.ex
Normal file
|
@ -0,0 +1,247 @@
|
|||
defmodule Mix.Tasks.ConvertToHeex do
|
||||
@moduledoc """
|
||||
Converts all slime files in the repository to HEEx.
|
||||
|
||||
This file is substantially based on Slime's compiler, which can be found here:
|
||||
https://github.com/slime-lang/slime/blob/master/lib/slime/compiler.ex
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
alias Slime.Parser.Nodes.{
|
||||
DoctypeNode,
|
||||
EExNode,
|
||||
EExCommentNode,
|
||||
HTMLCommentNode,
|
||||
HTMLNode,
|
||||
InlineHTMLNode,
|
||||
VerbatimTextNode
|
||||
}
|
||||
|
||||
alias Slime.Doctype
|
||||
|
||||
@void_elements ~w(
|
||||
area br col doctype embed hr img input link meta base param
|
||||
keygen source menuitem track wbr
|
||||
)
|
||||
|
||||
@indent " "
|
||||
|
||||
def run(_) do
|
||||
Path.wildcard("lib/**/*.html.slime")
|
||||
|> Enum.sort()
|
||||
|> Enum.each(&format_file/1)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp format_file(filename) do
|
||||
Mix.shell().info(filename)
|
||||
|
||||
formatted_content =
|
||||
filename
|
||||
|> File.read!()
|
||||
|> format_string()
|
||||
|
||||
heex_filename = String.replace(filename, ".html.slime", ".html.heex")
|
||||
|
||||
File.write!(heex_filename, [formatted_content])
|
||||
File.rm!(filename)
|
||||
end
|
||||
|
||||
defp format_string(source) do
|
||||
tree = Slime.Parser.parse(source)
|
||||
compile(tree, "")
|
||||
end
|
||||
|
||||
defp compile(tags, indent) when is_list(tags) do
|
||||
Enum.map(tags, &compile(&1, indent))
|
||||
end
|
||||
|
||||
defp compile(%DoctypeNode{name: name}, indent), do: [indent, Doctype.for(name), "\n"]
|
||||
|
||||
defp compile(%VerbatimTextNode{content: content}, indent) do
|
||||
[indent, String.trim(IO.iodata_to_binary(content)), "\n"]
|
||||
end
|
||||
|
||||
defp compile(%HTMLNode{name: name} = tag, indent) do
|
||||
attrs = Enum.map(tag.attributes, &render_attribute/1)
|
||||
tag_head = Enum.join([name | attrs])
|
||||
|
||||
body =
|
||||
cond do
|
||||
tag.closed ->
|
||||
["<", tag_head, "/>\n"]
|
||||
|
||||
name in @void_elements ->
|
||||
["<", tag_head, " />\n"]
|
||||
|
||||
true ->
|
||||
children = compile(tag.children, indent <> @indent)
|
||||
inner = if(tag.children == [], do: [], else: ["\n", children, indent])
|
||||
|
||||
[
|
||||
"<",
|
||||
tag_head,
|
||||
">",
|
||||
inner,
|
||||
"</",
|
||||
name,
|
||||
">\n"
|
||||
]
|
||||
end
|
||||
|
||||
[indent, body]
|
||||
end
|
||||
|
||||
defp compile(%EExNode{content: code, output: output, safe?: safe?} = eex, indent) do
|
||||
if safe? do
|
||||
raise "== operator used to include safe content in template; mark as raw in view instead"
|
||||
end
|
||||
|
||||
tag_indent =
|
||||
if(String.trim(code) == "else", do: unindent(indent), else: indent)
|
||||
|
||||
code = reformat_code(code, eex.children)
|
||||
|
||||
opening = [
|
||||
if(output, do: "<%= ", else: "<% "),
|
||||
convert_multiline(code, tag_indent <> @indent),
|
||||
"%>\n"
|
||||
]
|
||||
|
||||
closing =
|
||||
if Regex.match?(~r/(fn.*->| do)\s*$/, code) do
|
||||
[indent, "<% end %>\n"]
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
[tag_indent, opening, compile(eex.children, tag_indent <> @indent), closing]
|
||||
end
|
||||
|
||||
defp compile(%InlineHTMLNode{}, _indent) do
|
||||
raise "Inline HTML not supported"
|
||||
end
|
||||
|
||||
defp compile(%HTMLCommentNode{content: content}, indent) do
|
||||
[indent, "<!-- ", raw(content), " -->\n"]
|
||||
end
|
||||
|
||||
defp compile(%EExCommentNode{content: content}, indent) do
|
||||
content
|
||||
|> raw()
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&[indent, "<% # ", &1, " %>\n"])
|
||||
end
|
||||
|
||||
defp compile({:eex, eex}, indent), do: [indent, "<%= ", eex, " %>"]
|
||||
defp compile({:safe_eex, _eex}, _indent), do: raise("Safe EEx not supported")
|
||||
defp compile(raw, indent), do: [indent, raw]
|
||||
|
||||
defp render_attribute({name, {safe_eex, content}}) do
|
||||
if safe_eex != :eex do
|
||||
raise "Unsupported attribute type '#{safe_eex}'"
|
||||
end
|
||||
|
||||
case content do
|
||||
"true" ->
|
||||
" #{name}"
|
||||
|
||||
"false" ->
|
||||
""
|
||||
|
||||
"nil" ->
|
||||
""
|
||||
|
||||
_ ->
|
||||
quoted_content = Code.string_to_quoted!(content)
|
||||
render_attribute_code(name, content, quoted_content)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_attribute({name, value}) do
|
||||
if value == true do
|
||||
" #{name}"
|
||||
else
|
||||
value =
|
||||
cond do
|
||||
is_binary(value) -> value
|
||||
is_list(value) -> Enum.join(value, " ")
|
||||
true -> value
|
||||
end
|
||||
|
||||
~s( #{name}="#{value}")
|
||||
end
|
||||
end
|
||||
|
||||
defp render_attribute_code(name, _content, quoted)
|
||||
when is_number(quoted) or is_atom(quoted) do
|
||||
~s[ #{name}="#{quoted}"]
|
||||
end
|
||||
|
||||
defp render_attribute_code(name, _content, quoted) when is_list(quoted) do
|
||||
quoted |> Enum.map_join(" ", &Kernel.to_string/1) |> (&~s[ #{name}="#{&1}"]).()
|
||||
end
|
||||
|
||||
defp render_attribute_code(name, _content, quoted) when is_binary(quoted),
|
||||
do: ~s[ #{name}="#{quoted}"]
|
||||
|
||||
# String with interpolation
|
||||
defp render_attribute_code(
|
||||
name,
|
||||
_content,
|
||||
{:<<>>, _, [{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [line]}, _]}]}
|
||||
) do
|
||||
~s[ #{name}={#{Macro.to_string(line)}}]
|
||||
end
|
||||
|
||||
defp render_attribute_code(name, content, _) do
|
||||
~s[ #{name}={#{content}}]
|
||||
end
|
||||
|
||||
defp raw(value) when is_list(value) do
|
||||
Enum.map(value, &raw/1)
|
||||
end
|
||||
|
||||
defp raw({:eex, value}), do: "\#{" <> value <> "}"
|
||||
defp raw(value), do: value
|
||||
|
||||
defp convert_multiline(code, indent) do
|
||||
case String.split(code, "\n") do
|
||||
[_line] ->
|
||||
[code, " "]
|
||||
|
||||
lines ->
|
||||
lines =
|
||||
Enum.map(lines, fn line ->
|
||||
if String.trim(line) == "" do
|
||||
["\n"]
|
||||
else
|
||||
[indent, line, "\n"]
|
||||
end
|
||||
end)
|
||||
|
||||
["\n", lines, unindent(indent)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unindent(indent), do: String.slice(indent, 0..-3//1)
|
||||
|
||||
defp reformat_code(code, []) do
|
||||
case Code.string_to_quoted(code) do
|
||||
{:ok, ast} ->
|
||||
ast
|
||||
|> Code.quoted_to_algebra()
|
||||
|> Inspect.Algebra.format(300)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
_ ->
|
||||
# Stab or do block
|
||||
code
|
||||
end
|
||||
end
|
||||
|
||||
defp reformat_code(code, _), do: code
|
||||
end
|
Loading…
Reference in a new issue