This repository has been archived on 2023-08-07. You can view files and clone it, but cannot push or open issues or pull requests.
temple/lib/temple/renderer.ex

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