lib/kalevala/character/view.ex

defmodule Kalevala.Character.View do
  @moduledoc """
  Render output from the game
  """

  defmacro __using__(_opts) do
    quote do
      import Kalevala.Character.View.Macro

      alias Kalevala.Character.View
    end
  end

  @doc """
  "Join" an IO data list with a separator string

  Similar to Enum.join, but leaves as an IO data list
  """
  def join([], _separator), do: []

  def join([line], _separator), do: [line]

  def join([line | lines], separator) do
    [line, separator | join(lines, separator)]
  end

  @doc """
  Trim empty lines from io data

  Detects empty lines in io data (for example when rendered via EEx templating),
  and removes them. This way the template string works as expected, returning an
  empty string from a sub-render will strip the line.
  """
  def trim_lines([]), do: []

  def trim_lines(["\n", "", "\n" | segments]), do: ["\n" | trim_lines(segments)]

  def trim_lines(["\n", nil, "\n" | segments]), do: ["\n" | trim_lines(segments)]

  def trim_lines([segment | segments]), do: [segment | trim_lines(segments)]
end

defmodule Kalevala.Character.View.Macro do
  @moduledoc """
  Imported into views
  """

  alias Kalevala.Character.View

  @doc """
  Creates ~E which runs through EEx templating
  """
  defmacro sigil_E({:<<>>, _, [expr]}, opts) do
    string =
      EEx.compile_string(expr,
        line: __CALLER__.line + 1,
        sigil_opts: opts,
        engine: Kalevala.Character.View.EExKalevala
      )

    quote do
      View.trim_lines(unquote(string))
    end
  end

  @doc """
  Creates ~i to create IO lists that look like standard interpolation
  """
  defmacro sigil_i({:<<>>, _, text}, _) do
    Enum.map(text, &sigil_i_unwrap/1)
  end

  defp sigil_i_unwrap({:"::", _, interpolation}) do
    [text | _] = interpolation
    {_, _, [text]} = text

    quote do
      to_string(unquote(text))
    end
  end

  defp sigil_i_unwrap(text) when is_binary(text) do
    :elixir_interpolation.unescape_chars(text)
  end
end

defmodule Kalevala.Character.View.EExKalevala do
  @moduledoc """
  An EEx Engine that returns IO data instead of a string

  Taken from [Phoenix.HTML.Engine](https://github.com/phoenixframework/phoenix_html/blob/master/lib/phoenix_html/engine.ex)
  """

  @behaviour EEx.Engine

  @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
    iodata = Enum.reverse(iodata)
    {:__block__, [], Enum.reverse([iodata | dynamic])}
  end

  @impl true
  def handle_text(state, text) do
    %{iodata: iodata} = state
    %{state | iodata: [text | iodata]}
  end

  @impl true
  def handle_expr(state, "=", ast) do
    ast = Macro.prewalk(ast, &EEx.Engine.handle_assign/1)
    %{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 = Macro.prewalk(ast, &EEx.Engine.handle_assign/1)
    %{dynamic: dynamic} = state
    %{state | dynamic: [ast | dynamic]}
  end

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