lib/liquex/tag/case_tag.ex

defmodule Liquex.Tag.CaseTag do
  @moduledoc """
  Creates a switch statement to execute a particular block of code when a
  variable has a specified value. case initializes the switch statement, and
  when statements define the various conditions.

  An optional else statement at the end of the case provides code to execute if
  none of the conditions are met.

  ### Input

      {% assign handle = "cake" %}
      {% case handle %}
        {% when "cake" %}
          This is a cake
        {% when "cookie", "biscuit" %}
          This is a cookie
        {% else %}
          This is not a cake nor a cookie
      {% endcase %}

  ### Output

      This is a cake
  """

  @behaviour Liquex.Tag
  import NimbleParsec

  alias Liquex.Argument
  alias Liquex.Parser
  alias Liquex.Parser.Literal
  alias Liquex.Parser.Tag
  alias Liquex.Render

  def parse do
    case_tag()
    |> ignore(Literal.whitespace())
    |> times(tag(when_tag(), :when), min: 1)
    |> optional(else_tag())
    |> ignore(Tag.tag_directive("endcase"))
  end

  def parse_liquid_tag do
    case_contents()
    |> ignore(Tag.end_liquid_line())
    |> times(
      tag(
        when_contents()
        |> ignore(Tag.end_liquid_line())
        |> tag(parsec(:liquid_tag_contents), :contents),
        :when
      ),
      min: 1
    )
    |> optional(
      else_contents(empty())
      |> ignore(Tag.end_liquid_line())
      |> tag(parsec(:liquid_tag_contents), :contents)
      |> tag(:else)
    )
    |> ignore(Literal.whitespace())
    |> ignore(string("endcase"))
    |> ignore(Tag.end_liquid_line())
  end

  defp case_tag do
    ignore(Tag.open_tag())
    |> case_contents()
    |> ignore(Tag.close_tag())
  end

  defp case_contents(combinator \\ empty()) do
    combinator
    |> ignore(string("case"))
    |> ignore(Literal.whitespace(empty(), 1))
    |> concat(Parser.Argument.argument())
  end

  defp when_tag do
    ignore(Tag.open_tag())
    |> when_contents()
    |> ignore(Tag.close_tag())
    |> tag(parsec(:document), :contents)
  end

  defp when_contents(combinator \\ empty()) do
    combinator
    |> ignore(string("when"))
    |> ignore(Literal.whitespace(empty(), 1))
    |> tag(
      Literal.literal()
      |> repeat(
        ignore(string(","))
        |> ignore(Literal.whitespace(empty(), 1))
        |> Literal.literal()
      ),
      :expression
    )
  end

  def else_tag do
    ignore(Tag.open_tag())
    |> else_contents()
    |> ignore(Tag.close_tag())
    |> tag(parsec(:document), :contents)
    |> tag(:else)
  end

  defp else_contents(combinator) do
    combinator
    |> ignore(string("else"))
  end

  @spec render(list, Liquex.Context.t()) :: Render.result_t()
  def render([argument | tail], context) do
    match = Argument.eval(argument, context)
    do_render(tail, context, match)
  end

  defp do_render([{:when, [expression: expressions, contents: contents]} | tail], context, match) do
    result = Enum.any?(expressions, &(match == Argument.eval(&1, context)))

    if result do
      Render.render!(contents, context)
    else
      do_render(tail, context, match)
    end
  end

  defp do_render([{:else, [contents: contents]} | _tail], context, _),
    do: Render.render!(contents, context)

  defp do_render([], context, _), do: {[], context}
end