lib/engine/engine.ex

defmodule SnapFramework.Engine do
  @moduledoc """
  The EEx template engine.
  """

  @behaviour EEx.Engine
  require Logger

  def encode_to_iodata!({:safe, body}), do: body
  def encode_to_iodata!(body) when is_binary(body), do: body

  def compile(path, assigns, info, _env) do
    quoted = EEx.compile_file(path, info)
    {result, _binding} = Code.eval_quoted(quoted, assigns)
    result
  end

  def compile_string(string, assigns, info, env) do
    quoted = EEx.compile_string(string, info)
    {result, _binding} = Code.eval_quoted(quoted, assigns, env)
    result
  end

  @doc false
  def init(opts) do
    %{
      iodata: [],
      dynamic: [],
      vars_count: 0,
      assigns: opts[:assigns] || []
    }
  end

  @doc false
  def handle_begin(state) do
    Macro.var(:assigns, __MODULE__)
    %{state | iodata: [], dynamic: []}
  end

  @doc false
  def handle_end(quoted) do
    quoted
    |> handle_body()
  end

  @doc false
  def handle_body(state) do
    %{iodata: iodata, dynamic: dynamic} = state
    safe = Enum.reverse(iodata)
    {:__block__, [], Enum.reverse([safe | dynamic])}
  end

  @doc false
  def handle_text(state, text) do
    handle_text(state, [], text)
  end

  @doc false
  def handle_text(state, _meta, text) do
    %{iodata: iodata} = state
    %{state | iodata: [text | iodata]}
  end

  @doc false
  def handle_expr(state, "=", ast) do
    ast = traverse(ast, state.assigns)
    %{iodata: iodata, dynamic: dynamic, vars_count: vars_count} = state
    var = Macro.var(:"arg#{vars_count}", __MODULE__)
    ast = quote do: unquote(var) = unquote(ast)
    %{state | dynamic: [ast | dynamic], iodata: [var | iodata], vars_count: vars_count + 1}
  end

  def handle_expr(state, "", ast) do
    ast = traverse(ast, state.assigns)
    %{dynamic: dynamic} = state
    %{state | dynamic: [ast | dynamic]}
  end

  def handle_expr(state, marker, ast) do
    EEx.Engine.handle_expr(state, marker, ast)
  end

  ## Traversal

  defp traverse(expr, assigns) do
    expr
    |> Macro.prewalk(&SnapFramework.Parser.Assigns.run(&1, assigns))
    # |> Macro.prewalk(&SnapFramework.Parser.Enumeration.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Graph.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Component.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Primitive.run/1)
    |> Macro.prewalk(&SnapFramework.Parser.Outlet.run(&1, assigns))
  end

  @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