mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-17 22:47:59 +01:00
247 lines
5.7 KiB
Elixir
247 lines
5.7 KiB
Elixir
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
|