Packages

Component-based HTML templates for Elixir/Phoenix, inspired by Vue.

Current section

Files

Jump to
x_component lib x transformer.ex
Raw

lib/x/transformer.ex

defmodule X.Transformer do
@moduledoc """
Contains a set of functions to transform compiled Elixir AST
into more performance optimized AST.
Also, it contains functions to transform Elixir AST for inline components.
"""
@spec compact_ast(Macro.t()) :: Macro.t()
def compact_ast(tree) when is_list(tree) do
tree
|> List.flatten()
|> join_binary()
end
def compact_ast(tree) do
tree
end
@doc """
Transform given Elixir AST into a valid X template AST.
* transforms globals `@var` into assigns `Map.get/2` function call.
* transforms `@assigns` and `@yield` into local variables.
* add given `context` module to the local variables context.
* transforms imported function call into function call from the imported module.
* expands all aliases.
"""
@spec transform_expresion(Macro.t(), atom(), Macro.Env.t()) :: Macro.t()
def transform_expresion(ast, context, env) do
Macro.postwalk(ast, fn
{:@, meta, [{name, _, atom}]} when is_atom(name) and is_atom(atom) ->
line = Keyword.get(meta, :line, 0)
case name do
:assigns ->
quote(line: line, do: unquote(Macro.var(:assigns, nil)))
:yield ->
quote(line: line, do: unquote(Macro.var(:yield, context)))
_ ->
quote line: line do
Map.get(unquote(Macro.var(:assigns, nil)), unquote(name))
end
end
ast = {:__aliases__, _, _} ->
Macro.expand(ast, env)
{variable, meta, nil} when is_atom(variable) ->
{variable, meta, context}
ast = {function, _, args}
when not is_nil(context) and is_atom(function) and is_list(args) ->
alias_function(ast, env)
a ->
a
end)
end
@doc """
Transform given X template Elixir AST into optimized inline component AST.
* replaces dynamic `:attrs` with string build in compile time when it's possible.
* replaces local variables with given `assigns`.
* replaces `yield` local variables with given `children` AST.
"""
@spec transform_inline_component(atom(), Keyword.t(), Macro.t(), integer()) :: Macro.t()
def transform_inline_component(module, assigns, children, line) do
module.template_ast()
|> Macro.postwalk(fn
ast = {{:., _, [{:__aliases__, _, [:Map]}, :get]}, _, [{:assigns, _, _}, variable]} ->
Keyword.get(assigns, variable, ast)
{:yield, _, _} ->
children
{:assigns, _, context} when not is_nil(context) ->
{:%{}, [line: line], assigns}
{variable, _, ^module} when is_atom(variable) ->
case Keyword.get(module.assigns(), variable) do
nil ->
{variable, [line: line], module}
true ->
Keyword.get_lazy(assigns, variable, fn ->
throw({:missing_assign, {1, line}, variable})
end)
false ->
Keyword.get(assigns, variable)
end
{term, _meta, args} ->
{term, [line: line], args}
ast ->
ast
end)
|> Macro.prewalk(fn
ast = {:case, _, [{:{}, _, [attrs, base_attrs, static_attrs]}, _]} ->
if is_list(attrs) || is_nil(attrs) do
transform_inline_attributes(attrs || [], base_attrs, static_attrs, line)
else
ast
end
ast ->
ast
end)
end
@spec transform_inline_attributes(list(), list(), list(), integer()) :: Macro.t()
defp transform_inline_attributes(attrs, base_attrs, static_attrs, line) do
attrs = X.Html.merge_attrs(X.Html.merge_attrs(base_attrs, static_attrs), attrs)
{dynamic_attrs, static_attrs} =
Enum.split_with(attrs, fn
{{_, _, _}, _} ->
true
{_, value} ->
case value do
{_, _, _} ->
true
value when is_list(value) ->
Enum.any?(value, fn
{key, value} -> is_tuple(value) or is_tuple(key)
value -> is_tuple(value)
end)
_ ->
false
end
end)
case dynamic_attrs do
[] ->
[?\s, X.Html.attrs_to_iodata(static_attrs)]
_ ->
dynamic_ast =
quote line: line do
X.Html.attrs_to_iodata(unquote(dynamic_attrs))
end
[?\s, dynamic_ast, ?\s, X.Html.attrs_to_iodata(static_attrs)]
end
end
@spec alias_function(Macro.expr(), Macro.Env.t()) :: Macro.t()
defp alias_function(ast = {function, meta, args}, env) do
context = env.functions ++ env.macros
args_length = length(args)
if Macro.special_form?(function, args_length) || Macro.operator?(function, args_length) do
ast
else
imported_module =
Enum.find(context, fn {_, fns} ->
Enum.any?(fns, fn {name, arity} ->
name == function && args_length == arity
end)
end)
alias_module =
case imported_module do
{module, _} -> module
nil -> env.module
end
{{:., meta, [{:__aliases__, [], [alias_module]}, function]}, meta, args}
end
end
@spec join_binary(Macro.t(), list(), list()) :: Macro.t()
defp join_binary(list, iodata \\ [], acc \\ [])
defp join_binary([ast = {_, _, _} | tail], [], acc) do
join_binary(tail, [], [ast | acc])
end
defp join_binary([ast = {_, _, _} | tail], iodata, acc) do
join_binary(tail, [], [ast, IO.iodata_to_binary(iodata) | acc])
end
defp join_binary([head | tail], iodata, acc) do
join_binary(tail, [iodata, head], acc)
end
defp join_binary([], [], []) do
[]
end
defp join_binary([], iodata, []) do
IO.iodata_to_binary(iodata)
end
defp join_binary([], [], acc) do
:lists.reverse(acc)
end
defp join_binary([], iodata, acc) do
:lists.reverse([IO.iodata_to_binary(iodata) | acc])
end
end