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

362 lines
9 KiB
Elixir

defmodule Temple.Renderer do
@moduledoc false
alias Temple.Parser.ElementList
alias Temple.Parser.Text
alias Temple.Parser.Components
alias Temple.Parser.Slot
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.VoidElementsAliases
alias Temple.Parser.AnonymousFunctions
alias Temple.Parser.RightArrow
alias Temple.Parser.DoExpressions
alias Temple.Parser.Match
alias Temple.Parser.Default
alias Temple.Parser.Empty
alias Temple.Parser.Utils
@default_engine EEx.SmartEngine
defmacro compile(opts \\ [], do: block) do
block
|> Temple.Parser.parse()
|> Temple.Renderer.render(opts)
# |> Temple.Parser.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, @default_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,
assigns: assigns,
children: children,
slots: slots
}) do
child_quoted =
if Enum.any?(children) do
children_buffer = state.engine.handle_begin(buffer)
children_buffer =
for child <- children(children), reduce: children_buffer do
children_buffer ->
render(children_buffer, state, child)
end
state.engine.handle_end(children_buffer)
end
slot_quotes =
for slot <- slots do
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)
[quoted] =
quote do
{unquote(slot.name), unquote(slot.assigns)} ->
unquote(ast)
end
quoted
end
{:fn, meta, clauses} =
quote do
fn
{:default, _} -> unquote(child_quoted)
end
end
slot_func = {:fn, meta, slot_quotes ++ clauses}
expr =
quote do
component(
unquote(function),
Map.put(Map.new(unquote(assigns)), :__slots__, unquote(slot_func))
)
end
state.engine.handle_expr(buffer, "=", expr)
end
def render(buffer, state, %Slot{} = ast) do
render_slot_func =
quote do
var!(assigns).__slots__.({unquote(ast.name), Map.new(unquote(ast.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 = ""
new_indent = nil
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)}"
)
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, [], ">")
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, [], "")
inner_quoted = state.engine.handle_end(new_buffer)
{name, meta, args} = ast.elixir_ast
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} =
Temple.Parser.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, [], "")
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: ""
def new_line(%{terminal_node: true}), do: ""
end