lib/liquex/parser/tag.ex

defmodule Liquex.Parser.Tag do
  @moduledoc """
  Helper methods for parsing tags
  """

  import NimbleParsec

  alias Liquex.Parser.Argument
  alias Liquex.Parser.Literal

  @doc """
  Parse open tags

  ## Examples

      * "{%"
      * "{%-"
  """
  @spec open_tag(NimbleParsec.t()) :: NimbleParsec.t()
  def open_tag(combinator \\ empty()) do
    combinator
    |> string("{%")
    |> optional(string("-"))
    |> Literal.whitespace()
  end

  @doc """
  Parse close tags

  ## Examples

      * "%}"
      * "-%} "
  """
  @spec close_tag(NimbleParsec.t()) :: NimbleParsec.t()
  def close_tag(combinator \\ empty()) do
    combinator
    |> Literal.whitespace()
    |> choice([close_tag_remove_whitespace(), string("%}")])
  end

  @doc """
  Read to end of a line within a liquid tag. Reads to end of line ("\\r" and/or
  "\\n") or to a closing tag "%}".
  """
  @spec end_liquid_line(NimbleParsec.t()) :: NimbleParsec.t()
  def end_liquid_line(combinator \\ empty()) do
    combinator
    |> utf8_string([?\s, ?\t], min: 0)
    |> choice([
      empty()
      |> utf8_string([?\r, ?\n], 1)
      |> Literal.whitespace(),
      lookahead(choice([string("-%}"), string("%}")]))
    ])
  end

  @doc """
  Parse basic tag with no arguments

  ## Examples

      * "{% break %}"
      * "{% endfor %}"
  """
  @spec tag_directive(NimbleParsec.t(), String.t()) :: NimbleParsec.t()
  def tag_directive(combinator \\ empty(), name) do
    combinator
    |> open_tag()
    |> string(name)
    |> close_tag()
  end

  @spec liquid_tag_directive(NimbleParsec.t(), String.t()) :: NimbleParsec.t()
  def liquid_tag_directive(combinator \\ empty(), name) do
    combinator
    |> string(name)
    |> end_liquid_line()
  end

  @doc """
  Parse tag with no expression

  ## Examples

      * "{% if a == 5 %}"
      * "{% elsif b >= 10 and a < 4 %}"
  """
  @spec expression_tag(NimbleParsec.t(), String.t()) :: NimbleParsec.t()
  def expression_tag(combinator \\ empty(), tag_name) do
    combinator
    |> ignore(open_tag())
    |> ignore(string(tag_name))
    |> ignore(Literal.whitespace(empty(), 1))
    |> tag(boolean_expression(), :expression)
    |> ignore(close_tag())
  end

  def liquid_tag_expression(combinator \\ empty(), tag_name) do
    combinator
    |> ignore(string(tag_name))
    |> ignore(Literal.whitespace(empty(), 1))
    |> tag(boolean_expression(), :expression)
    |> ignore(end_liquid_line())
  end

  # Close tag that also removes the whitespace after it
  defp close_tag_remove_whitespace do
    string("-%}")
    |> Literal.whitespace()
  end

  defp boolean_expression(combinator \\ empty()) do
    operator =
      choice([
        replace(string("=="), :==),
        replace(string("!="), :!=),
        replace(string(">="), :>=),
        replace(string("<="), :<=),
        replace(string(">"), :>),
        replace(string("<"), :<),
        replace(string("contains"), :contains)
      ])

    boolean_operation =
      tag(Argument.argument(), :left)
      |> ignore(Literal.whitespace())
      |> unwrap_and_tag(operator, :op)
      |> ignore(Literal.whitespace())
      |> tag(Argument.argument(), :right)
      |> wrap()

    combinator
    |> choice([boolean_operation, Literal.literal(), Argument.argument()])
    |> repeat(
      ignore(Literal.whitespace(empty(), 1))
      |> choice([
        replace(string("and"), :and),
        replace(string("or"), :or)
      ])
      |> ignore(Literal.whitespace(empty(), 1))
      |> choice([boolean_operation, Literal.literal(), Argument.argument()])
    )
  end
end