lib/fermo/compilers/slim.ex

defmodule Fermo.Compilers.Slim do
  import Mix.Fermo.Paths
  import Fermo.Naming
  alias Fermo.Compilers.EEx

  @file_impl Application.get_env(:fermo, :file_impl, File)

  @callback compile(String.t()) :: {:ok}
  def compile(template_project_path) do
    template_source_path = absolute_to_source(template_project_path)

    name = source_path_to_module(template_source_path)

    {frontmatter, content_fors, offset, body} = parse_template(template_project_path)

    eex_source = precompile_slim(body, template_project_path)

    cfs_eex = Enum.map(content_fors, fn [key, block, offset] ->
      eex = precompile_slim(block, template_project_path, "content_for(:#{key})")
      [key, eex, offset]
    end)

    EEx.compile_module(
      %EEx{
        name: name,
        source: eex_source,
        frontmatter: frontmatter,
        content_fors: cfs_eex,
        template_project_path: template_project_path,
        offset: offset
      }
    )

    {:ok}
  end

  defp precompile_slim(body, template, type \\ "template") do
    try do
      Slime.Renderer.precompile(body)
    rescue
      e in Slime.TemplateSyntaxError ->
        line = e.line_number
        message = """
        SLIM template error: #{e.message}
        Template type: #{type}
        Path: '#{template}', line #{line + 1}

        #{body}
        """
        raise Fermo.Error, message: message
    end
  end

  defp parse_template(template) do
    source = @file_impl.read!(template)

    [frontmatter, body] = Fermo.Compilers.EEx.extract_frontmatter(source)

    {content_fors, offset, body} = extract_content_for_blocks(body)

    # Strip leading space, or EEx compilation fails
    body = String.replace(body, ~r/^[\s\r\n]*/, "")

    {frontmatter, content_fors, offset, body}
  end

  defp extract_content_for_blocks(body) do
    [head | parts] = String.split(body, ~r{(?<=\n|^)- content_for(?=(\s+\:\w+|\(\:\w+\))\n)})
    {content_fors, offset, cleaned_parts} = Enum.reduce(parts, {[], 0, []}, fn (part, {cfs, offset, ps}) ->
      {new_cf, lines, cleaned} = extract_content_for_block(part, offset)
      {cfs ++ [new_cf], offset + lines, ps ++ cleaned}
    end)
    {content_fors, offset, Enum.join([head] ++ cleaned_parts, "\n")}
  end

  defp extract_content_for_block(part, offset) do
    # Extract the content_for block (until the next line that isn't indented)
    # TODO: the block should not stop at the first non-indented **empty** line,
    #   it should continue to the first non-indented line with text
    [key | [block | cleaned]] = Regex.run(~r/^(?:[\(\s]\:)([^\n\)]+)\)?\n((?:\s{2}[^\n]+\n)+)(.*)/s, part, capture: :all_but_first)
    lines = count_lines(block) + 1
    # Strip leading blank lines
    block = String.replace(block, ~r/^[\s\r\n]*/, "", global: false)
    # Strip indentation
    block = String.replace(block, ~r/^  /m, "")
    # Strip newlines at end
    block = String.replace(block, ~r/\n+\z/, "")

    {[key, block, offset], lines, cleaned}
  end

  defp count_lines(text), do: length(String.split(text, "\n"))
end