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