lib/liquex/tag/cycle_tag.ex

defmodule Liquex.Tag.CycleTag do
  @moduledoc """
  Loops through a group of strings and prints them in the order that they were
  passed as arguments. Each time cycle is called, the next string argument is
  printed.

  cycle must be used within a for loop block.

  ### Input

      {% cycle "one", "two", "three" %}
      {% cycle "one", "two", "three" %}
      {% cycle "one", "two", "three" %}
      {% cycle "one", "two", "three" %}

  ### Output

      one
      two
      three
      one

  Uses for cycle include:

    * applying odd/even classes to rows in a table
    * applying a unique class to the last product thumbnail in a row

  ## cycle (parameters)

  cycle accepts a “cycle group” parameter in cases where you need multiple cycle
  blocks in one template. If no name is supplied for the cycle group, then it is
  assumed that multiple calls with the same parameters are one group.

  ### Input

      {% cycle "first": "one", "two", "three" %}
      {% cycle "second": "one", "two", "three" %}
      {% cycle "second": "one", "two", "three" %}
      {% cycle "first": "one", "two", "three" %}

  ### Output

      one
      one
      two
      two
  """

  @behaviour Liquex.Tag

  alias Liquex.Context
  alias Liquex.Parser.Argument
  alias Liquex.Parser.Literal
  alias Liquex.Parser.Tag

  import NimbleParsec

  def parse do
    ignore(Tag.open_tag())
    |> do_parse_cycle()
    |> ignore(Tag.close_tag())
  end

  def parse_liquid_tag do
    do_parse_cycle()
    |> ignore(Tag.end_liquid_line())
  end

  def do_parse_cycle(combinator \\ empty()) do
    cycle_group =
      Literal.literal()
      |> ignore(string(":"))
      |> ignore(Literal.non_breaking_whitespace())

    combinator
    |> ignore(string("cycle"))
    |> ignore(Literal.non_breaking_whitespace(empty(), 1))
    |> optional(unwrap_and_tag(cycle_group, :group))
    |> tag(argument_sequence(), :sequence)
  end

  defp argument_sequence(combinator \\ empty()) do
    combinator
    |> Argument.argument()
    |> repeat(
      ignore(string(","))
      |> ignore(Literal.non_breaking_whitespace())
      |> Argument.argument()
    )
  end

  def render([], context), do: {[], context}

  def render([sequence: sequence], %Context{} = context),
    do: render([group: sequence, sequence: sequence], context)

  def render([group: group, sequence: sequence], %Context{cycles: cycles} = context) do
    index = Map.get(cycles, group, 0)

    next_index = rem(index + 1, length(sequence))

    result =
      sequence
      |> Enum.at(index)
      |> Liquex.Argument.eval(context)

    {result, %{context | cycles: Map.put(cycles, group, next_index)}}
  end
end