Current section

Files

Jump to
iona lib iona template engine.ex
Raw

lib/iona/template/engine.ex

# Taken in part from the Phoenix HTML project,
# https://github.com/phoenixframework/phoenix_html
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
defmodule Iona.Template.Engine do
@moduledoc false
@behaviour EEx.Engine
@anno (if :erlang.system_info(:otp_release) >= '19' do
[generated: true]
else
[line: -1]
end)
@doc """
Encodes the HTML templates to iodata.
"""
def encode_to_iodata!({:safe, body}), do: body
def encode_to_iodata!(body) when is_binary(body), do: Iona.Template.Helper.escape(body)
def encode_to_iodata!(other), do: Iona.Template.Safe.to_iodata(other)
@impl true
def init(_opts) do
%{
iodata: [],
dynamic: [],
vars_count: 0
}
end
@impl true
def handle_begin(state) do
%{state | iodata: [], dynamic: []}
end
@impl true
def handle_end(quoted) do
handle_body(quoted)
end
@impl true
def handle_body(state) do
%{iodata: iodata, dynamic: dynamic} = state
safe = {:safe, Enum.reverse(iodata)}
{:__block__, [], Enum.reverse([safe | dynamic])}
end
@impl true
def handle_text(state, _meta, text) do
%{iodata: iodata} = state
%{state | iodata: [text | iodata]}
end
@impl true
def handle_expr(state, "=", ast) do
ast = traverse(ast)
%{iodata: iodata, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast = quote do: unquote(var) = unquote(to_safe(ast))
%{state | dynamic: [ast | dynamic], iodata: [var | iodata], vars_count: vars_count + 1}
end
def handle_expr(state, "", ast) do
ast = traverse(ast)
%{dynamic: dynamic} = state
%{state | dynamic: [ast | dynamic]}
end
def handle_expr(state, marker, ast) do
EEx.Engine.handle_expr(state, marker, ast)
end
## Safe conversion
defp to_safe(ast) do
to_safe(ast, line_from_expr(ast))
end
defp line_from_expr({_, meta, _}) when is_list(meta), do: Keyword.get(meta, :line)
defp line_from_expr(_), do: nil
# We can do the work at compile time
defp to_safe(literal, _line)
when is_binary(literal) or is_atom(literal) or is_number(literal) do
Iona.Template.Safe.to_iodata(literal)
end
# We can do the work at runtime
defp to_safe(literal, line) when is_list(literal) do
quote line: line, do: Iona.Template.Safe.List.to_iodata(unquote(literal))
end
# We need to check at runtime and we do so by optimizing common cases.
defp to_safe(expr, line) do
# Keep stacktraces for protocol dispatch and coverage
safe_return = quote line: line, do: data
bin_return = quote line: line, do: Iona.Template.Helper.escape_to_iodata(bin)
other_return = quote line: line, do: Iona.Template.Safe.to_iodata(other)
# However ignore them for the generated clauses to avoid warnings
quote @anno do
case unquote(expr) do
{:safe, data} -> unquote(safe_return)
bin when is_binary(bin) -> unquote(bin_return)
other -> unquote(other_return)
end
end
end
## Traversal
defp traverse(expr) do
Macro.prewalk(expr, &handle_assign/1)
end
defp handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
quote line: meta[:line] || 0 do
Iona.Template.Engine.fetch_assign!(var!(assigns), unquote(name))
end
end
defp handle_assign(arg), do: arg
@doc false
def fetch_assign!(assigns, key) do
case Access.fetch(assigns, key) do
{:ok, val} ->
val
:error ->
raise ArgumentError, """
assign @#{key} not available in eex template.
Please make sure all proper assigns have been set. If this
is a child template, ensure assigns are given explicitly by
the parent template as they are not automatically forwarded.
Available assigns: #{inspect(Enum.map(assigns, &elem(&1, 0)))}
"""
end
end
end