Current section
Files
Jump to
Current section
Files
lib/serum/template/compiler/include.ex
defmodule Serum.Template.Compiler.Include do
@moduledoc false
_moduledocp = "Provides functions for expanding includes in templates."
alias Serum.Result
alias Serum.Template
alias Serum.Template.Storage, as: TS
@typep context :: %{
template: Template.t(),
stack: [binary()],
error: Result.t() | nil
}
@spec expand(Template.t()) :: Result.t(Template.t())
def expand(template) do
initial_context = %{
template: template,
stack: [],
error: nil
}
case do_expand(template, initial_context) do
{new_template, %{error: nil}} -> {:ok, new_template}
{_, %{error: {:error, _} = error}} -> error
end
end
@spec do_expand(Template.t(), context()) :: {Template.t(), context()}
defp do_expand(template, context)
defp do_expand(%Template{include_resolved?: true} = t, ctx), do: {t, ctx}
defp do_expand(%Template{name: name, type: type, ast: ast} = t, context) do
next_context = %{context | template: t, stack: [name | context.stack]}
with :ok <- check_cycle(name, context),
walk_result = Macro.prewalk(ast, next_context, &prewalk_fun/2),
{new_ast, %{error: nil} = new_context} <- walk_result do
new_template = %Template{t | ast: new_ast, include_resolved?: true}
TS.put(name, type, new_template)
{new_template, new_context}
else
{:error, _} = error -> {t, %{context | error: error}}
{_, %{error: {:error, _} = error}} -> {t, %{context | error: error}}
end
end
@spec prewalk_fun(Macro.t(), context()) :: {Macro.t(), context()}
defp prewalk_fun(ast, context)
defp prewalk_fun(ast, %{error: e} = ctx) when not is_nil(e), do: {ast, ctx}
defp prewalk_fun({:include, _, [arg]} = ast, context) do
case TS.get(arg, :include) do
%Template{} = include ->
{new_include, new_context} = do_expand(include, context)
{quote(do: (fn -> unquote(new_include.ast) end).()), new_context}
nil ->
message = "include not found: \"#{arg}\""
{ast, %{context | error: {:error, {message, context.template.file, 0}}}}
end
end
defp prewalk_fun(ast, context), do: {ast, context}
@spec check_cycle(binary(), context()) :: Result.t()
defp check_cycle(name, context) do
if name in context.stack do
graph =
context.stack
|> Enum.reverse()
|> Enum.drop_while(&(&1 != name))
|> make_graph()
message = [
"cycle detected while expanding includes:\n",
graph,
" Cycles are not allowed when recursively including templates.\n",
" Please refactor your templates to break the cycle.\n",
" Alternatively, you can use the render/1,2 template helper.\n"
]
{:error, {IO.iodata_to_binary(message), context.template.file, 0}}
else
:ok
end
end
endl = [:reset, ?\n]
top = [" ", :red, "\u256d\u2500\u2500\u2500\u2500\u2500\u256e", endl]
arrow = [" ", :red, "\u2502 \u2193", endl]
first_text = [" ", :red, "\u2502 ", :yellow]
rest_text = [" ", :red, "\u2502 "]
bottom1 = [" ", :red, "\u2502 \u2502", endl]
bottom2 = [" ", :red, "\u2570\u2500\u2500\u2500\u2500\u2500\u256f", endl]
@spec make_graph([binary()]) :: iodata()
defp make_graph([first | rest]) do
rest_graph =
Enum.map(rest, fn name ->
[
unquote(arrow),
unquote(rest_text),
name,
unquote(endl)
]
end)
[
unquote(top),
unquote(arrow),
unquote(first_text),
first,
unquote(endl),
rest_graph,
unquote(bottom1),
unquote(bottom2)
]
|> IO.ANSI.format()
end
end