372 lines
9.5 KiB
Elixir
372 lines
9.5 KiB
Elixir
defmodule Temple.Renderer do
|
|
@moduledoc false
|
|
|
|
alias Temple.Ast.ElementList
|
|
alias Temple.Ast.Text
|
|
alias Temple.Ast.Components
|
|
alias Temple.Ast.Slot
|
|
alias Temple.Ast.Slottable
|
|
alias Temple.Ast.NonvoidElementsAliases
|
|
alias Temple.Ast.VoidElementsAliases
|
|
alias Temple.Ast.AnonymousFunctions
|
|
alias Temple.Ast.RightArrow
|
|
alias Temple.Ast.DoExpressions
|
|
alias Temple.Ast.Match
|
|
alias Temple.Ast.Default
|
|
alias Temple.Ast.Empty
|
|
|
|
alias Temple.Ast.Utils
|
|
|
|
@engine Application.compile_env(:temple, :engine, Phoenix.HTML.Engine)
|
|
@doc false
|
|
def engine(), do: @engine
|
|
|
|
defmacro compile(do: block) do
|
|
block
|
|
|> Temple.Parser.parse()
|
|
|> Temple.Renderer.render(engine: @engine)
|
|
|
|
# |> Temple.Ast.Utils.inspect_ast()
|
|
end
|
|
|
|
def render(asts, opts \\ [])
|
|
|
|
def render(asts, opts) when is_list(asts) and is_list(opts) do
|
|
engine = Keyword.get(opts, :engine, Phoenix.HTML.Engine)
|
|
|
|
state = %{
|
|
engine: engine,
|
|
indentation: 0,
|
|
terminal_node: false
|
|
}
|
|
|
|
buffer = engine.init([])
|
|
|
|
buffer =
|
|
for ast <- asts, reduce: buffer do
|
|
buffer ->
|
|
render(buffer, state, ast)
|
|
end
|
|
|
|
if function_exported?(engine, :handle_body, 2) do
|
|
engine.handle_body(buffer, root: length(asts) == 1)
|
|
else
|
|
engine.handle_body(buffer)
|
|
end
|
|
end
|
|
|
|
def render(buffer, state, %Text{text: text}) do
|
|
t = Utils.indent(state.indentation) <> text <> new_line(state)
|
|
|
|
unless t == "" do
|
|
state.engine.handle_text(
|
|
buffer,
|
|
[],
|
|
t
|
|
)
|
|
end
|
|
end
|
|
|
|
def render(buffer, state, asts) when is_list(asts) do
|
|
for ast <- asts, reduce: buffer do
|
|
buffer ->
|
|
render(buffer, state, ast)
|
|
end
|
|
end
|
|
|
|
def render(buffer, state, %Components{
|
|
function: function,
|
|
arguments: arguments,
|
|
slots: slots
|
|
}) do
|
|
slot_quotes =
|
|
Enum.group_by(slots, & &1.name, fn %Slottable{} = slot ->
|
|
slot_buffer = state.engine.handle_begin(buffer)
|
|
|
|
slot_buffer =
|
|
for child <- children(slot.content), reduce: slot_buffer do
|
|
slot_buffer ->
|
|
render(slot_buffer, state, child)
|
|
end
|
|
|
|
ast = state.engine.handle_end(slot_buffer)
|
|
|
|
inner_block =
|
|
quote do
|
|
inner_block unquote(slot.name) do
|
|
unquote(slot.parameter || quote(do: _)) ->
|
|
unquote(ast)
|
|
end
|
|
end
|
|
|
|
{rest, attributes} = Keyword.pop(slot.attributes, :rest!, [])
|
|
|
|
slot =
|
|
{:%{}, [],
|
|
[
|
|
__slot__: slot.name,
|
|
inner_block: inner_block
|
|
] ++ attributes}
|
|
|
|
quote do
|
|
Map.merge(unquote(slot), Map.new(unquote(rest)))
|
|
end
|
|
end)
|
|
|
|
{rest, arguments} = Keyword.pop(arguments, :rest!, [])
|
|
|
|
component_arguments =
|
|
{:%{}, [],
|
|
arguments
|
|
|> Map.new()
|
|
|> Map.merge(slot_quotes)
|
|
|> Enum.to_list()}
|
|
|
|
component_arguments =
|
|
quote do
|
|
Map.merge(unquote(component_arguments), Map.new(unquote(rest)))
|
|
end
|
|
|
|
expr =
|
|
quote do
|
|
component(
|
|
unquote(function),
|
|
unquote(component_arguments),
|
|
{__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
|
|
)
|
|
end
|
|
|
|
state.engine.handle_expr(buffer, "=", expr)
|
|
end
|
|
|
|
def render(buffer, state, %Slot{} = ast) do
|
|
render_slot_func =
|
|
quote do
|
|
{rest, args} = Map.pop(Map.new(unquote(ast.args)), :rest!, [])
|
|
args = Map.merge(args, Map.new(rest))
|
|
render_slot(unquote(ast.name), args)
|
|
end
|
|
|
|
state.engine.handle_expr(buffer, "=", render_slot_func)
|
|
end
|
|
|
|
def render(buffer, state, %ElementList{} = ast) do
|
|
render(buffer, state, ast.children)
|
|
end
|
|
|
|
def render(buffer, state, %NonvoidElementsAliases{} = ast) do
|
|
current_indent = Utils.indent(state.indentation)
|
|
|
|
inside_new_lines = if ast.meta.whitespace == :tight, do: "", else: "\n"
|
|
new_indent = if ast.meta.whitespace == :tight, do: nil, else: state.indentation + 1
|
|
|
|
buffer =
|
|
state.engine.handle_text(
|
|
buffer,
|
|
[],
|
|
"#{current_indent}<#{ast.name}"
|
|
)
|
|
|
|
buffer =
|
|
for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do
|
|
buffer ->
|
|
case node do
|
|
{:text, text} ->
|
|
state.engine.handle_text(buffer, [], text)
|
|
|
|
{:expr, expr} ->
|
|
state.engine.handle_expr(buffer, "=", expr)
|
|
end
|
|
end
|
|
|
|
buffer =
|
|
state.engine.handle_text(
|
|
buffer,
|
|
[],
|
|
">#{inside_new_lines}"
|
|
)
|
|
|
|
buffer =
|
|
if Enum.any?(children(ast.children)) do
|
|
for {child, index} <- Enum.with_index(children(ast.children), 1), reduce: buffer do
|
|
buffer ->
|
|
render(
|
|
buffer,
|
|
%{
|
|
state
|
|
| indentation: new_indent,
|
|
terminal_node: index == length(children(ast.children))
|
|
},
|
|
child
|
|
)
|
|
end
|
|
else
|
|
buffer
|
|
end
|
|
|
|
state.engine.handle_text(
|
|
buffer,
|
|
[],
|
|
"#{inside_new_lines}#{Utils.indent(if(ast.meta.whitespace == :loose, do: state.indentation, else: nil))}</#{ast.name}>#{new_line(state)}\n"
|
|
)
|
|
end
|
|
|
|
def render(buffer, state, %VoidElementsAliases{} = ast) do
|
|
current_indent = Utils.indent(state.indentation)
|
|
|
|
buffer =
|
|
state.engine.handle_text(
|
|
buffer,
|
|
[],
|
|
"#{current_indent}<#{ast.name}"
|
|
)
|
|
|
|
buffer =
|
|
for node <- Utils.compile_attrs(ast.attrs), reduce: buffer do
|
|
buffer ->
|
|
case node do
|
|
{:text, text} ->
|
|
state.engine.handle_text(buffer, [], text)
|
|
|
|
{:expr, expr} ->
|
|
state.engine.handle_expr(buffer, "=", expr)
|
|
end
|
|
end
|
|
|
|
state.engine.handle_text(buffer, [], ">\n")
|
|
end
|
|
|
|
def render(buffer, state, %AnonymousFunctions{} = ast) do
|
|
new_buffer = state.engine.handle_begin(buffer)
|
|
|
|
new_buffer =
|
|
for child <- children(ast.children), child != nil, reduce: new_buffer do
|
|
new_buffer ->
|
|
render(new_buffer, state, child)
|
|
end
|
|
|
|
new_buffer = state.engine.handle_text(new_buffer, [], "\n")
|
|
|
|
inner_quoted = state.engine.handle_end(new_buffer)
|
|
|
|
{name, meta, args} = ast.elixir_ast
|
|
|
|
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} =
|
|
Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
|
|
|
|
full_ast =
|
|
{name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2}
|
|
|
|
state.engine.handle_expr(buffer, "=", full_ast)
|
|
end
|
|
|
|
def render(buffer, state, %RightArrow{elixir_ast: elixir_ast} = ast) do
|
|
new_buffer = state.engine.handle_begin(buffer)
|
|
|
|
new_buffer =
|
|
for child <- children(ast.children), child != nil, reduce: new_buffer do
|
|
new_buffer ->
|
|
render(new_buffer, state, child)
|
|
end
|
|
|
|
inner_quoted = state.engine.handle_end(new_buffer)
|
|
|
|
{func, meta, [first]} = elixir_ast
|
|
|
|
full_ast = {func, meta, [first | [inner_quoted]]}
|
|
|
|
state.engine.handle_expr(buffer, "", full_ast)
|
|
end
|
|
|
|
def render(buffer, state, %DoExpressions{} = ast) do
|
|
{func, meta, args} = ast.elixir_ast
|
|
new_buffer = state.engine.handle_begin(buffer)
|
|
|
|
[do_block, else_block] = ast.children
|
|
|
|
do_inner_quoted =
|
|
case do_block do
|
|
[%RightArrow{} | _] = bodies ->
|
|
for b <- bodies do
|
|
new_buffer = state.engine.handle_begin(buffer)
|
|
|
|
new_buffer = render(new_buffer, state, b)
|
|
|
|
{:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer)
|
|
quoted
|
|
end
|
|
|
|
block ->
|
|
for child <- children(block), child != nil, reduce: new_buffer do
|
|
new_buffer ->
|
|
render(new_buffer, state, child)
|
|
end
|
|
|> state.engine.handle_end()
|
|
end
|
|
|
|
else_inner_quoted =
|
|
if else_block do
|
|
case else_block do
|
|
[%RightArrow{} | _] = bodies ->
|
|
for b <- bodies do
|
|
new_buffer = state.engine.handle_begin(buffer)
|
|
|
|
new_buffer = render(new_buffer, state, b)
|
|
|
|
{:__block__, _, [quoted | _]} = state.engine.handle_end(new_buffer)
|
|
quoted
|
|
end
|
|
|
|
block ->
|
|
for child <- children(block), child != nil, reduce: new_buffer do
|
|
new_buffer ->
|
|
render(new_buffer, state, child)
|
|
end
|
|
|> state.engine.handle_end()
|
|
end
|
|
end
|
|
|
|
new_args =
|
|
then([do: do_inner_quoted], fn args ->
|
|
if else_inner_quoted do
|
|
Keyword.put(args, :else, else_inner_quoted) |> Enum.reverse()
|
|
else
|
|
args
|
|
end
|
|
end)
|
|
|
|
full_ast = {func, meta, args ++ [new_args]}
|
|
|
|
state.engine.handle_expr(buffer, "=", full_ast)
|
|
end
|
|
|
|
def render(buffer, state, %Match{elixir_ast: elixir_ast}) do
|
|
state.engine.handle_expr(buffer, "", elixir_ast)
|
|
end
|
|
|
|
def render(buffer, state, %Default{elixir_ast: elixir_ast}) do
|
|
buffer =
|
|
if state.indentation && state.indentation > 0 do
|
|
state.engine.handle_text(buffer, [], Utils.indent(state.indentation))
|
|
else
|
|
buffer
|
|
end
|
|
|
|
buffer = state.engine.handle_expr(buffer, "=", elixir_ast)
|
|
|
|
if not state.terminal_node do
|
|
state.engine.handle_text(buffer, [], "\n")
|
|
else
|
|
buffer
|
|
end
|
|
end
|
|
|
|
def render(buffer, _state, %Empty{}), do: buffer
|
|
|
|
defp children(%ElementList{children: children}), do: children
|
|
defp children(list) when is_list(list), do: list
|
|
|
|
def new_line(%{terminal_node: false}), do: "\n"
|
|
def new_line(%{terminal_node: true}), do: ""
|
|
end
|