lib/liquex/parser/object.ex

defmodule Liquex.Parser.Object do
  @moduledoc """
  Helper methods for parsing object tags and arguments used by objects
  """

  import NimbleParsec

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

  @doc """
  Parse arguments. Arguments are key/value pairs, but a key may have multiple
  values separated by commas.

  ## Examples

      * "img_url: '400x400', crop: 'bottom', filter: 'blur'"
      * "img_size: 800, 600"
  """
  @spec arguments(NimbleParsec.t()) :: NimbleParsec.t()
  def arguments(combinator \\ empty()) do
    combinator
    |> choice([
      Argument.argument()
      |> lookahead_not(string(":"))
      |> repeat(
        ignore(Literal.whitespace())
        |> ignore(string(","))
        |> ignore(Literal.whitespace())
        |> concat(Argument.argument())
        |> lookahead_not(string(":"))
      )
      |> optional(
        ignore(Literal.whitespace())
        |> ignore(string(","))
        |> ignore(Literal.whitespace())
        |> keyword_fields()
      ),
      keyword_fields()
    ])
  end

  @spec keyword_fields(NimbleParsec.t()) :: NimbleParsec.t()
  def keyword_fields(combinator \\ empty()) do
    combinator
    |> keyword_field()
    |> repeat(
      ignore(Literal.whitespace())
      |> ignore(string(","))
      |> ignore(Literal.whitespace())
      |> keyword_field()
    )
  end

  @doc """
  Parse keyword field

  ## Examples

      * "key: value"
  """
  def keyword_field(combinator) do
    combinator
    |> concat(Field.identifier())
    |> ignore(string(":"))
    |> ignore(Literal.whitespace())
    |> concat(Argument.argument())
    |> tag(:keyword)
  end

  @doc """
  Parses filter that starts with a pipe

  ## Examples

      * "| sort"
      * "| at_most: 5"
  """
  @spec filter(NimbleParsec.t()) :: NimbleParsec.t()
  def filter(combinator \\ empty()) do
    combinator
    |> ignore(Literal.whitespace())
    |> ignore(utf8_char([?|]))
    |> ignore(Literal.whitespace())
    |> concat(Field.identifier())
    |> tag(
      optional(
        ignore(string(":"))
        |> ignore(Literal.whitespace())
        |> concat(arguments())
      ),
      :arguments
    )
    |> tag(:filter)
  end

  @doc """
  Parses object. May contain arguments, literals, and filters.

  It special cases space removing tags such as `{{-` and `-}}` to properly
  remove any spaces leading and trailing spaces if requested.

  ## Examples

      * "{{ 'hello world' }}"
      * "{{ 5 + 5 }}"
      * "{{ variable_a | at_most: 5 }}"
      * "{{- my_array | sort -}}"
  """
  @spec object(NimbleParsec.t()) :: NimbleParsec.t()
  def object(combinator \\ empty()) do
    combinator
    |> ignore(string("{{"))
    |> ignore(optional(string("-")))
    |> ignore(Literal.whitespace())
    |> Argument.argument()
    |> optional(tag(repeat(filter()), :filters))
    |> ignore(Literal.whitespace())
    |> ignore(choice([close_object_remove_whitespace(), string("}}")]))
    |> tag(:object)
  end

  @doc """
  Parses closing object tag with white space removing.

  ## Examples

      * "-}}  "
  """
  @spec close_object_remove_whitespace(NimbleParsec.t()) :: NimbleParsec.t()
  def close_object_remove_whitespace(combinator \\ empty()) do
    combinator
    |> string("-}}")
    |> Literal.whitespace()
  end
end