defmodule Liquex.Tag.ForTag do
@moduledoc """
Repeatedly executes a block of code. For a full list of attributes available
within a for loop, see forloop (object).
### Input
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
### Output
hat shirt pants
## else
Specifies a fallback case for a for loop which will run if the loop has zero length.
### Input
{% for product in collection.products %}
{{ product.title }}
{% else %}
The collection is empty.
{% endfor %}
### Output
The collection is empty.
## break
Causes the loop to stop iterating when it encounters the break tag.
### Input
{% for i in (1..5) %}
{% if i == 4 %}
{% break %}
{% else %}
{{ i }}
{% endif %}
{% endfor %}
### Output
1 2 3
## continue
Causes the loop to skip the current iteration when it encounters the continue
tag.
### Input
{% for i in (1..5) %}
{% if i == 4 %}
{% continue %}
{% else %}
{{ i }}
{% endif %}
{% endfor %}
### Output
1 2 3 5
# for (parameters)
## limit
Limits the loop to the specified number of iterations.
### Input
<!-- if array = [1,2,3,4,5,6] -->
{% for item in array limit:2 %}
{{ item }}
{% endfor %}
### Output
1 2
## offset
Begins the loop at the specified index.
### Input
<!-- if array = [1,2,3,4,5,6] -->
{% for item in array offset:2 %}
{{ item }}
{% endfor %}
### Output
3 4 5 6
To start a loop from where the last loop using the same iterator left off,
pass the special word continue.
### Input
<!-- if array = [1,2,3,4,5,6] -->
{% for item in array limit: 3 %}
{{ item }}
{% endfor %}
{% for item in array limit: 3 offset: continue %}
{{ item }}
{% endfor %}
### Output
1 2 3
4 5 6
## range
Defines a range of numbers to loop through. The range can be defined by both
literal and variable numbers, and can be pulled from a variable.
### Input
{% for i in (3..5) %}
{{ i }}
{% endfor %}
{% assign num = 4 %}
{% assign range = (1..num) %}
{% for i in range %}
{{ i }}
{% endfor %}
### Output
3 4 5
1 2 3 4
## reversed
Reverses the order of the loop. Note that this flag’s spelling is different
from the filter reverse.
### Input
<!-- if array = [1,2,3,4,5,6] -->
{% for item in array reversed %}
{{ item }}
{% endfor %}
### Output
6 5 4 3 2 1
"""
@behaviour Liquex.Tag
import NimbleParsec
alias Liquex.Collection
alias Liquex.Context
alias Liquex.Expression
alias Liquex.Parser.Argument
alias Liquex.Parser.Field
alias Liquex.Parser.Literal
alias Liquex.Parser.Tag
def parse do
ignore(Tag.open_tag())
|> do_for_in()
|> ignore(Tag.close_tag())
|> tag(parsec(:document), :contents)
|> optional(else_tag())
|> ignore(Tag.tag_directive("endfor"))
end
def parse_liquid_tag do
do_for_in()
|> ignore(Tag.end_liquid_line())
|> tag(parsec(:liquid_tag_contents), :contents)
|> ignore(Tag.liquid_tag_directive("endfor"))
end
defp do_for_in(combinator \\ empty()) do
collection = choice([Literal.range(), Argument.argument()])
combinator
|> string("for")
|> Literal.whitespace(1)
|> ignore()
|> unwrap_and_tag(Field.identifier(), :identifier)
|> ignore(Literal.whitespace(empty(), 1))
|> ignore(string("in"))
|> ignore(Literal.whitespace(empty(), 1))
|> tag(collection, :collection)
|> ignore(Literal.non_breaking_whitespace())
|> tag(for_parameters(), :parameters)
end
defp for_parameters do
reversed =
replace(string("reversed"), :reversed)
|> unwrap_and_tag(:order)
|> ignore(Literal.non_breaking_whitespace())
limit =
ignore(string("limit:"))
|> unwrap_and_tag(integer(min: 1), :limit)
|> ignore(Literal.non_breaking_whitespace())
offset =
ignore(string("offset:"))
|> unwrap_and_tag(integer(min: 1), :offset)
|> ignore(Literal.non_breaking_whitespace())
repeat(
choice([
reversed,
limit,
offset
])
)
end
defp else_tag do
ignore(Tag.tag_directive("else"))
|> tag(parsec(:document), :contents)
|> tag(:else)
end
def render(
[
identifier: identifier,
collection: collection,
parameters: parameters,
contents: contents,
else: [contents: else_contents]
],
%Context{} = context
) do
collection
|> Liquex.Argument.eval(context)
|> Expression.eval_collection(parameters)
|> Collection.to_enumerable()
|> render_collection(identifier, contents, else_contents, context)
end
def render(content, %Context{} = context) do
(content ++ [else: [contents: []]])
|> render(context)
end
def render_collection(nil, _, _, contents, context),
do: Liquex.Render.render!(contents, context)
def render_collection([], _, _, contents, context),
do: Liquex.Render.render!(contents, context)
def render_collection(results, identifier, contents, _, context) do
len = Enum.count(results)
{result, context} =
results
|> Enum.with_index(0)
|> Enum.reduce({[], context}, fn {record, index}, {acc, ctx} ->
# Assign the loop variables
ctx =
Context.push_scope(ctx, %{
"forloop" => forloop(index, len),
identifier => record
})
case Liquex.Render.render!(contents, ctx) do
{r, ctx} ->
{[r | acc], Context.pop_scope(ctx)}
{:continue, content, ctx} ->
{content ++ acc, Context.pop_scope(ctx)}
{:break, content, ctx} ->
throw({:break, content ++ acc, Context.pop_scope(ctx)})
end
end)
{Enum.reverse(result), context}
catch
{:break, result, context} ->
# credo:disable-for-next-line
{Enum.reverse(result), context}
end
defp forloop(index, length) do
%{
"index" => index + 1,
"index0" => index,
"rindex" => length - index,
"rindex0" => length - index - 1,
"first" => index == 0,
"last" => index == length - 1,
"length" => length
}
end
end