/
For.cs
263 lines (235 loc) · 10.2 KB
/
For.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using DotLiquid.Exceptions;
using DotLiquid.Util;
namespace DotLiquid.Tags
{
/// <summary>
/// "For" iterates over an array or collection.
/// Several useful variables are available to you within the loop.
///
/// == Basic usage:
/// {% for item in collection %}
/// {{ forloop.index }}: {{ item.name }}
/// {% endfor %}
///
/// == Advanced usage:
/// {% for item in collection %}
/// <div {% if forloop.first %}class="first"{% endif %}>
/// Item {{ forloop.index }}: {{ item.name }}
/// </div>
/// {% else %}
/// There is nothing in the collection.
/// {% endfor %}
///
/// You can also define a limit and offset much like SQL. Remember
/// that offset starts at 0 for the first item.
///
/// {% for item in collection limit:5 offset:10 %}
/// {{ item.name }}
/// {% end %}
///
/// To reverse the for loop simply use {% for item in collection reversed %}
///
/// == Available variables:
///
/// forloop.name:: 'item-collection'
/// forloop.length:: Length of the loop
/// forloop.index:: The current item's position in the collection;
/// forloop.index starts at 1.
/// This is helpful for non-programmers who start believe
/// the first item in an array is 1, not 0.
/// forloop.index0:: The current item's position in the collection
/// where the first item is 0
/// forloop.rindex:: Number of items remaining in the loop
/// (length - index) where 1 is the last item.
/// forloop.rindex0:: Number of items remaining in the loop
/// where 0 is the last item.
/// forloop.first:: Returns true if the item is the first item.
/// forloop.last:: Returns true if the item is the last item.
/// </summary>
public class For : DotLiquid.Block
{
private static readonly Regex Syntax = R.B(R.Q(@"(\w+)\s+in\s+({0}+)\s*(reversed)?"), Liquid.QuotedFragment);
private static string ForTagMaxIterationsExceededException = Liquid.ResourceManager.GetString("ForTagMaximumIterationsExceededException");
private string _variableName, _collectionName, _name;
private bool _reversed;
private Dictionary<string, string> _attributes;
private List<object> ForBlock { get; set; }
private Condition ElseBlock { get; set; }
/// <summary>
/// Initializes the for tag
/// </summary>
/// <param name="tagName">Name of the parsed tag</param>
/// <param name="markup">Markup of the parsed tag</param>
/// <param name="tokens">Tokens of the parsed tag</param>
public override void Initialize(string tagName, string markup, List<string> tokens)
{
Match match = Syntax.Match(markup);
if (match.Success)
{
NodeList = ForBlock = new List<object>();
_variableName = match.Groups[1].Value;
_collectionName = match.Groups[2].Value;
_name = string.Format("{0}-{1}", _variableName, _collectionName);
_reversed = (!string.IsNullOrEmpty(match.Groups[3].Value));
_attributes = new Dictionary<string, string>(Template.NamingConvention.StringComparer);
R.Scan(markup, Liquid.TagAttributes,
(key, value) => _attributes[key] = value);
}
else
{
throw new SyntaxException(Liquid.ResourceManager.GetString("ForTagSyntaxException"));
}
base.Initialize(tagName, markup, tokens);
}
/// <summary>
/// Handles the else tag
/// </summary>
/// <param name="tag"></param>
/// <param name="markup"></param>
/// <param name="tokens"></param>
public override void UnknownTag(string tag, string markup, List<string> tokens)
{
if (tag == "else")
{
ElseBlock = new ElseCondition();
NodeList = ElseBlock.Attach(new List<object>());
return;
}
base.UnknownTag(tag, markup, tokens);
}
/// <summary>
/// Renders the for tag
/// </summary>
/// <param name="context"></param>
/// <param name="result"></param>
public override void Render(Context context, TextWriter result)
{
// treat non IEnumerable as empty
if (!(context[_collectionName] is IEnumerable collection))
{
if (ElseBlock != null)
context.Stack(() =>
{
RenderAll(ElseBlock.Attachment, context, result);
});
return;
}
var register = GetRegister<object>(context, "for");
int from = (_attributes.ContainsKey("offset"))
? (_attributes["offset"] == "continue")
? Convert.ToInt32(register[_name])
: Convert.ToInt32(context[_attributes["offset"]])
: 0;
int? limit = _attributes.ContainsKey("limit") ? (int?)Convert.ToInt32(context[_attributes["limit"]]) : null;
int? to = (limit != null) ? (int?)(limit.Value + from) : null;
List<object> segment = SliceCollectionUsingEach(context, collection, from, to);
if (_reversed)
segment.Reverse();
int length = segment.Count;
// Store our progress through the collection for the continue flag
register[_name] = from + length;
context.Stack(() =>
{
if (!segment.Any())
{
if (ElseBlock != null)
RenderAll(ElseBlock.Attachment, context, result);
return;
}
for (var index = 0; index < segment.Count; index++)
{
context.CheckTimeout();
var item = segment[index];
if (context.SyntaxCompatibilityLevel < SyntaxCompatibility.DotLiquid22 && item is KeyValuePair<string, object> pair && pair.Value is IDictionary<string, object> valueDict)
context[_variableName] = new LegacyKeyValueDrop(pair.Key, valueDict);
else
context[_variableName] = item;
// Ensure the 'for-loop' object is available to templates.
// See: https://shopify.dev/api/liquid/objects/for-loops
context["forloop"] = new Dictionary<string, object>
{
["name"] = _name,
["length"] = length,
["index"] = index + 1,
["index0"] = index,
["rindex"] = length - index,
["rindex0"] = length - index - 1,
["first"] = (index == 0),
["last"] = (index == length - 1)
};
try
{
RenderAll(ForBlock, context, result);
}
catch (BreakInterrupt)
{
break;
}
catch (ContinueInterrupt)
{
// ContinueInterrupt is used only to skip the current value but not to stop the iteration
}
}
});
}
private static List<object> SliceCollectionUsingEach(Context context, IEnumerable collection, int from, int? to)
{
List<object> segments = new List<object>();
int index = 0;
foreach (object item in collection)
{
context.CheckTimeout();
if (to != null && to.Value <= index)
break;
if (from <= index)
segments.Add(item);
++index;
if (context.MaxIterations > 0 && index > context.MaxIterations)
{
throw new MaximumIterationsExceededException(For.ForTagMaxIterationsExceededException, context.MaxIterations.ToString());
}
}
return segments;
}
}
/// <summary>
/// internal class to encapsulate pre DotLiquid 2.2 compatibility in for loop.
/// </summary>
/// <remarks>
/// DotLiquid 2.2 compatibility includes:
/// * An undocumented property `itemName` which is an implicit alias for KeyValuePair.Key
/// * Implicit access of properties within a nested IDictionary Value
/// </remarks>
internal class LegacyKeyValueDrop : Drop
{
private readonly string key;
private readonly IDictionary<string, object> value;
public LegacyKeyValueDrop(string key, IDictionary<string, object> value)
{
this.key = key;
this.value = value;
}
public override object BeforeMethod(string method)
{
if (method.SafeTypeInsensitiveEqual(0L) || method.Equals("Key") || method.Equals("itemName"))
return key;
else if (method.SafeTypeInsensitiveEqual(1L) || method.Equals("Value"))
return value;
else if (value.ContainsKey(method))
return value[method];
return null;
}
public override bool ContainsKey(object name)
{
string method = name.ToString();
return method.SafeTypeInsensitiveEqual(0L) || method.Equals("Key") || method.Equals("itemName")
|| method.SafeTypeInsensitiveEqual(1L) || method.Equals("Value") || value.ContainsKey(method);
}
}
}