lib/mustache.ex

defmodule Mustache do

  def render(template, data \\%{}) do
    Enum.reduce(strategies(), template, fn(strategy, template) ->
      predicate = elem(strategy, 0)
      function = elem(strategy, 1)
      if predicate.(template) do
        function.(template, data)
      else
        template
      end
    end)
  end

  defp double_mustaches(template, data) do
    scans = Regex.scan(double_regex(), template) |> List.flatten
    case scans do
      [] -> template
      _  ->
        first_scan = List.first(scans)
        variable = first_scan |> clean(["{{", "}}"])
        value = if escape?(first_scan) do
          key = variable |> String.trim
          data |> indifferent_access(key) |> to_string |> escape
        else
          key = String.replace(variable, "&", "") |> String.trim
          data |> indifferent_access(key) |> to_string
        end
        if value == nil do
          template
        else
          double_mustaches(String.replace(template, "{{#{variable}}}", value), data)
        end
    end
  end

  defp process_section(template, data) do
    matches = Regex.run(section_regex(), template)
    case matches do
      nil -> template
      [full, predicate, var, body] ->
        val = indifferent_access(data, var)
        section_val = case predicate do
          "#" -> process_if(body, val)
          "^" -> process_unless(body, val)
        end
        process_section(String.replace(template, full, section_val), data)
    end
  end

  defp process_if(template, val) do
    case val do
      nil -> ""
      false -> ""
      [] -> ""
      [_ | _] -> val
                 |> Stream.map(&(render(template, &1)))
                 |> Enum.join()
      val -> render(template, val)
    end
  end

  defp process_unless(template, val) do
    case val do
      nil -> render(template, val)
      false -> render(template, val)
      [] -> render(template, val)
      _val -> ""
    end
  end

  defp indifferent_access(map, string_key) do
    case Access.get(map, string_key) do
      nil -> Access.get(map, resolve_key(string_key))
      val -> val
    end
  end

  defp resolve_key(key) do
    try do
      String.to_existing_atom(key)
    rescue
      ArgumentError -> key
    end
  end

  defp scan_for_dot(template, data) do
    regex = regex("{{", "}}", "\\w+(\\.\\w+)+")
    matches = Regex.run(regex, template)
    case matches do
      nil -> template
      _  ->
        path = List.first(matches) |> clean(["{{", "}}"])
        scan_for_dot(interpolate(template, data, path), data)
    end
  end

  defp triple_mustaches(template, data) do
    scans = Regex.scan(triple_regex(), template) |> List.flatten
    case scans do
      [] -> template
      _  ->
        variable = List.first(scans) |> clean(["{{{", "}}}"])
        key = variable |> String.trim
        value = data |> indifferent_access(key) |> to_string
        if value == nil do
          template
        else
          triple_mustaches(String.replace(template, "{{{#{variable}}}}", value), data)
        end
    end
  end

  defp interpolate(template, data, path) do
    value = resolve(data, String.split(path, "."))
    String.replace(template, "{{#{path}}}", to_string(value))
  end

  def resolve(data, [key | []]), do: indifferent_access(data, key)
  def resolve(data, [key | rest]) do
    data
    |> indifferent_access(key)
    |> resolve(rest)
  end

  defp double_regex do
    regex("{{\\s*", "\\s*}}", "&?\\s*\\w+")
  end

  defp triple_regex do
    regex("{{{\\s*", "\\s*}}}")
  end

  defp section_regex do
    ~r<{{\s*(#|\^)\s*([\w.]+)\s*}}(.*?){{\s*/\s*\2\s*}}>
  end

  defp regex(otag, ctag, body \\ "\\w+") do
    Regex.compile!("#{otag}#{body}#{ctag}")
  end

  defp escape?(template) do
    !String.contains?(template, "&")
  end

  defp escape(non_escaped) do
    forbidden = [{"&", "&amp;"}, {"<","&lt;" }, {">", "&gt;"}, {"\"", "&quot;"}]
    Enum.reduce(forbidden, non_escaped, fn (x, str) ->
      String.replace(str, elem(x, 0), elem(x, 1))
    end)
  end

  defp clean(non_cleaned, patterns) do
    Enum.reduce(patterns, non_cleaned, fn(pattern, str) ->
      String.replace(str, pattern, "")
    end)
  end

  defp strategies do
    [{ fn(template) -> Regex.match?(section_regex(), template) end,
        fn(template, data) -> process_section(template, data) end },
    { fn(template) -> Regex.match?(triple_regex(), template) end,
        fn(template, data) -> triple_mustaches(template, data) end},
    { fn(template) -> Regex.match?(regex("{{", "}}", "\\w+(\\.\\w+)+"), template) end,
        fn(template, data) -> scan_for_dot(template, data) end },
    { fn(template) -> Regex.match?(double_regex(), template) end,
        fn(template, data) -> double_mustaches(template, data) end}]
  end
end