Skip to content

Commit

Permalink
Added else support to for tag (dotliquid#425)
Browse files Browse the repository at this point in the history
* Added else support to for tag.

* Replaced TestForWithElse with TestForElse from shopify test suite

* Added reference to shopify test suite in contributing guide

* Changed For XML doc to better match shopify. Moved For.Else from protected to private to keep public API from changing.

* Fixed issue of else block being rendered instead of for block

* Using modern syntax (pattern matching)
  • Loading branch information
rdennis authored Mar 31, 2021
1 parent 93d1b9d commit 2ccdb90
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 15 deletions.
3 changes: 2 additions & 1 deletion contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ We prefer if each new feature must have been discussed first before submitting i

## Testing

- Tests are mandatory for new functionality, please add some in the tests suite.
- Tests are mandatory for new functionality, please add some in the tests suite.
- See [the Shopify tests](https://github.com/Shopify/liquid/tree/master/test) as a starting point.

You can see the result either:
- in Visual Studio or other IDE supporting NUnit 3
Expand Down
9 changes: 9 additions & 0 deletions src/DotLiquid.Tests/Tags/StandardTagTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ public void TestForAndIf()
Hash.FromAnonymousObject(new { array = new[] { 1, 2, 3 } }));
}

[Test]
public void TestForElse()
{
// parity tests with https://github.com/Shopify/liquid/blob/master/test/integration/tags/for_tag_test.rb
Helper.AssertTemplateResult("+++", "{%for item in array%}+{%else%}-{%endfor%}", Hash.FromAnonymousObject(new { array = new[] { 1, 2, 3 } }));
Helper.AssertTemplateResult("-", "{%for item in array%}+{%else%}-{%endfor%}", Hash.FromAnonymousObject(new { array = new int[0] }));
Helper.AssertTemplateResult("-", "{%for item in array%}+{%else%}-{%endfor%}", Hash.FromAnonymousObject(new { array = (int[])null }));
}

[Test]
public void TestLimiting()
{
Expand Down
63 changes: 49 additions & 14 deletions src/DotLiquid/Tags/For.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ namespace DotLiquid.Tags
/// <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
Expand Down Expand Up @@ -60,17 +62,21 @@ public class For : DotLiquid.Block
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">Toeksn 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);
Expand All @@ -87,6 +93,24 @@ public override void Initialize(string tagName, string markup, List<string> toke
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>
Expand All @@ -96,10 +120,16 @@ public override void Render(Context context, TextWriter result)
{
context.Registers["for"] = context.Registers["for"] ?? new Hash(0);

object collection = context[_collectionName];

if (!(collection is IEnumerable))
// treat non IEnumerable as empty
if (!(context[_collectionName] is IEnumerable collection))
{
if (ElseBlock != null)
context.Stack(() =>
{
RenderAll(ElseBlock.Attachment, context, result);
});
return;
}

int from = (_attributes.ContainsKey("offset"))
? (_attributes["offset"] == "continue")
Expand All @@ -110,10 +140,7 @@ public override void Render(Context context, TextWriter result)
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, (IEnumerable) collection, from, to);

if (!segment.Any())
return;
List<object> segment = SliceCollectionUsingEach(context, collection, from, to);

if (_reversed)
segment.Reverse();
Expand All @@ -125,18 +152,26 @@ public override void Render(Context context, TextWriter result)

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 (item is KeyValuePair<string,object>)
if (item is KeyValuePair<string, object> pair)
{
var itemKey = ((KeyValuePair<string, object>) item).Key;
var itemValue = ((KeyValuePair<string, object>) item).Value;
var itemKey = pair.Key;
var itemValue = pair.Value;
BuildContext(context, _variableName, itemKey, itemValue);

} else
}
else
context[_variableName] = item;

context["forloop"] = Hash.FromDictionary(new Dictionary<string, object>
Expand All @@ -152,7 +187,7 @@ public override void Render(Context context, TextWriter result)
});
try
{
RenderAll(NodeList, context, result);
RenderAll(ForBlock, context, result);
}
catch (BreakInterrupt)
{
Expand Down Expand Up @@ -196,7 +231,7 @@ private void BuildContext(Context context, string parent, string key, object val
{
hashValue["itemName"] = key;
context[parent] = value;

foreach (var hashItem in (Hash)value)
{
if (hashItem.Value is Hash)
Expand Down

0 comments on commit 2ccdb90

Please sign in to comment.