lib/exampple/template/interpolation.ex

defmodule Exampple.Template.Interpolation do
  @moduledoc """
  Interpolation let us interchange a variable inside of a string. The problem with
  the Elixir interpolation is it's made in compilation time which isn't useful when
  we need to perform the same action in runtime.

  This module helps perform interpolations. It's based on `Gettext.Interpolation.Default`.
  """

  @doc """
  Interpolate a string adding the binding variables into the correct places.

  Examples:

      iex> Exampple.Template.Interpolation.interpolate("hello world!", [])
      "hello world!"

      iex> Exampple.Template.Interpolation.interpolate("hello %{name}!", name: "world")
      "hello world!"

      iex> Exampple.Template.Interpolation.interpolate("hello %{none}!", [])
      "hello !"
  """
  def interpolate(string, bindings) do
    start_pattern = :binary.compile_pattern("%{")
    end_pattern = :binary.compile_pattern("}")

    interpolate(string, "", [], start_pattern, end_pattern)
    |> Enum.map(&if is_atom(&1), do: bindings[&1] || "", else: &1)
    |> Enum.reverse()
    |> Enum.join()
  end

  defp interpolate(string, current, acc, start_pattern, end_pattern) do
    case :binary.split(string, start_pattern) do
      [rest] ->
        prepend_if_not_empty(current <> rest, acc)

      [before, "}" <> rest] ->
        current = current <> before <> "%{}"
        interpolate(rest, current, acc, start_pattern, end_pattern)

      [before, binding_and_rest] ->
        case :binary.split(binding_and_rest, end_pattern) do
          [_] ->
            [current <> string | acc]

          [binding, rest] ->
            acc = [String.to_atom(binding) | prepend_if_not_empty(before, acc)]
            interpolate(rest, "", acc, start_pattern, end_pattern)
        end
    end
  end

  defp prepend_if_not_empty("", list), do: list
  defp prepend_if_not_empty(string, list), do: [string | list]
end