Skip to content

Commit

Permalink
Fix operators precedence and whitespaces (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Jan 22, 2021
1 parent fbc0882 commit 76cc72c
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 156 deletions.
9 changes: 9 additions & 0 deletions Fluid.Tests/BinaryExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,14 @@ public Task StringLiteralTrue(string source, string expected, object value)
return CheckAsync(source, expected, t => t.SetValue("var", value));
}

[Theory]
[InlineData("true or false and false", "true")]
[InlineData("true and false and false or true", "false")]
public Task OperatorsShouldBeEvaluatedFromRightToLeft(string source, string expected)
{
// https://shopify.github.io/liquid/basics/operators/
return CheckAsync(source, expected);
}

}
}
199 changes: 91 additions & 108 deletions Fluid.Tests/WhiteSpaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,102 +15,85 @@ private IReadOnlyList<Statement> Parse(string source)
_parser.TryParse(source, out var template, out var errors);
return template.Statements;
}

[Fact]
public async Task ShouldRenderSampleWithStandardLiquid()
public async Task ShouldPreserveSpace()
{
var sample = @"
<ul id=""products"">
{%- for product in products -%}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.name | prettyprint | paragraph }}
</li>
{%- endfor -%}
</ul>
";
var source = @"# With whitespace control
{% assign name = 'John G. Chalmers-Smith' %}
{% if name and name.size > 10 %}
Wow, {{ name }}, you have a long name!
{% else %}
Hello there!
{% endif %}";

var expected = @"
<ul id=""products"">
<li>
<h2>product 1</h2>
Only 1
product 1
</li>
<li>
<h2>product 2</h2>
Only 2
product 2
</li>
<li>
<h2>product 3</h2>
Only 3
product 3
</li>
</ul>
var expected = @"# With whitespace control
Wow, John G. Chalmers-Smith, you have a long name!
";

var _products = new[]
{
new { name = "product 1", price = 1 },
new { name = "product 2", price = 2 },
new { name = "product 3", price = 3 },
};
_parser.TryParse(source, out var template);
var result = await template.RenderAsync();

_parser.TryParse(sample, out var template, out var messages);
Assert.Equal(expected, result);
}

var context = new TemplateContext();
context.SetValue("products", _products);
context.Filters.AddFilter("prettyprint", (input, args, ctx) => input);
context.Filters.AddFilter("paragraph", (input, args, ctx) => input);
context.Filters.AddFilter("price", (input, args, ctx) => input);
context.MemberAccessStrategy.Register(new { name = "", price = 0 }.GetType());
[Fact]
public async Task ShouldStripSpace()
{
var source = @"# With whitespace control
{% assign name = 'John G. Chalmers-Smith' -%}
{%- if name and name.size > 10 -%}
Wow, {{ name }}, you have a long name!
{%- else -%}
Hello there!
{%- endif -%}";

var expected = @"# With whitespace control
Wow, John G. Chalmers-Smith, you have a long name!";

_parser.TryParse(source, out var template);
var result = await template.RenderAsync();

var result = await template.RenderAsync(context);
Assert.Equal(expected, result);
}

[Fact]
public async Task ShouldRenderSampleWithStandardLiquidAndNoStripEmptyLines()
public async Task ShouldRenderSampleWithDashes()
{
var sample = @"
<ul id=""products"">
{%- for product in products -%}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.name | prettyprint | paragraph }}
</li>
{%- endfor -%}
{%- for product in products %}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.name | prettyprint | paragraph }}
</li>
{%- endfor %}
</ul>
";

var expected = @"
<ul id=""products"">
<li>
<h2>product 1</h2>
Only 1
product 1
</li>
<li>
<h2>product 2</h2>
Only 2
product 2
</li>
<li>
<h2>product 3</h2>
Only 3
product 3
</li>
<li>
<h2>product 1</h2>
Only 1
product 1
</li>
<li>
<h2>product 2</h2>
Only 2
product 2
</li>
<li>
<h2>product 3</h2>
Only 3
product 3
</li>
</ul>
";

Expand All @@ -135,42 +118,42 @@ product 3
}

[Fact]
public async Task ShouldRenderSampleWithDashes()
public async Task ShouldRenderSampleWithDashesRight()
{
var sample = @"
<ul id=""products"">
{%- for product in products -%}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.name | prettyprint | paragraph }}
</li>
{%- endfor -%}
{% for product in products -%}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.name | prettyprint | paragraph }}
</li>
{% endfor -%}
</ul>
";

var expected = @"
<ul id=""products"">
<li>
<h2>product 1</h2>
Only 1
product 1
</li>
<li>
<h2>product 2</h2>
Only 2
product 2
</li>
<li>
<h2>product 3</h2>
Only 3
product 3
</li>
</ul>
<li>
<h2>product 1</h2>
Only 1
product 1
</li>
<li>
<h2>product 2</h2>
Only 2
product 2
</li>
<li>
<h2>product 3</h2>
Only 3
product 3
</li>
</ul>
";

var _products = new[]
Expand Down Expand Up @@ -225,10 +208,10 @@ public async Task ShouldNotTrimOutputTag(string source, string expected)
[InlineData(" {{- 1 }} ", "1 ")]
[InlineData(" {{ 1 -}} ", " 1")]
[InlineData(" {{ 1 -}} \n", " 1")]
[InlineData(" {{ 1 -}} \n ", " 1 ")]
[InlineData(" {{ 1 -}} \n\n ", " 1\n ")]
[InlineData(" {{ 1 -}} \r\n ", " 1 ")]
[InlineData(" {{ 1 -}} \r\n\r\n ", " 1\r\n ")]
[InlineData(" {{ 1 -}} \n ", " 1")]
[InlineData(" {{ 1 -}} \n\n ", " 1")]
[InlineData(" {{ 1 -}} \r\n ", " 1")]
[InlineData(" {{ 1 -}} \r\n\r\n ", " 1")]
[InlineData("a {{ 1 }}", "a 1")]
[InlineData("a {% assign a = '' %}", "a ")]
[InlineData("1<div class=\"a{% if true %}b{% endif %}\" />", "1<div class=\"ab\" />")]
Expand Down
19 changes: 1 addition & 18 deletions Fluid/Ast/TextSpanStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,6 @@ public override ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncode
if (Character.IsWhiteSpaceOrNewLine(c))
{
start++;

// Read the first CR/LF or LF and stop skipping
if (c == '\r')
{
if (i + 1 <= end && span[_text.Offset + i + 1] == '\n')
{
start++;
break;
}
}
else
{
if (c == '\n')
{
break;
}
}
}
else
{
Expand All @@ -85,7 +68,7 @@ public override ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncode
{
var c = span[_text.Offset + i];

if (Character.IsWhiteSpace(c))
if (Character.IsWhiteSpaceOrNewLine(c))
{
end--;
}
Expand Down
2 changes: 1 addition & 1 deletion Fluid/Fluid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Parlot" Version="0.0.7" />
<PackageReference Include="Parlot" Version="0.0.8" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="1.1.1" />
<PackageReference Include="TimeZoneConverter" Version="3.3.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
Expand Down
50 changes: 21 additions & 29 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class FluidParser
protected static readonly Parser<string> Identifier = Terms.Identifier(extraPart: static c => c == '-').Then(x => x.ToString());

protected static readonly Deferred<Expression> Primary = Deferred<Expression>();
protected static readonly Deferred<Expression> LogicalExpression = Deferred<Expression>();
protected static readonly Parser<Expression> LogicalExpression;
protected static readonly Deferred<Expression> FilterExpression = Deferred<Expression>();
protected readonly Deferred<List<Statement>> KnownTagsList = Deferred<List<Statement>>();
protected readonly Deferred<List<Statement>> AnyTagsList = Deferred<List<Statement>>();
Expand Down Expand Up @@ -98,41 +98,33 @@ public FluidParser()

// TODO: 'and' has a higher priority than 'or', either create a new scope, or implement operators priority

var Logical = Primary.And(ZeroOrMany(OneOf(BinaryOr, BinaryAnd, Contains).And(Primary)))
var LogicalExpression = Primary.And(ZeroOrMany(OneOf(BinaryOr, BinaryAnd, Contains, DoubleEquals, NotEquals, Different, GreaterOr, LowerOr, Greater, Lower).And(Primary)))
.Then(static x =>
{
var result = x.Item1;

foreach (var op in x.Item2)
if (!x.Item2.Any())
{
result = op.Item1 switch
{
"or" => new OrBinaryExpression(result, op.Item2),
"and" => new AndBinaryExpression(result, op.Item2),
"contains" => new ContainsBinaryExpression(result, op.Item2),
_ => null
};
return x.Item1;
}

return result;
});

var Comparison = Logical.And(ZeroOrMany(OneOf(DoubleEquals, NotEquals, Different, GreaterOr, LowerOr, Greater, Lower).And(Logical)))
.Then(static x =>
{
var result = x.Item1;
var result = x.Item2.Last().Item2;

foreach (var op in x.Item2)
for (var i = x.Item2.Count - 1; i >= 0; i--)
{
result = op.Item1 switch
var current = x.Item2[i];
var previous = i == 0 ? x.Item1 : x.Item2[i - 1].Item2;

result = current.Item1 switch
{
"==" => new EqualBinaryExpression(result, op.Item2),
"!=" => new NotEqualBinaryExpression(result, op.Item2),
"<>" => new NotEqualBinaryExpression(result, op.Item2),
">" => new GreaterThanBinaryExpression(result, op.Item2, true),
"<" => new LowerThanExpression(result, op.Item2, true),
">=" => new GreaterThanBinaryExpression(result, op.Item2, false),
"<=" => new LowerThanExpression(result, op.Item2, false),
"or" => new OrBinaryExpression(previous, current.Item2),
"and" => new AndBinaryExpression(previous, current.Item2),
"contains" => new ContainsBinaryExpression(previous, current.Item2),
"==" => new EqualBinaryExpression(previous, current.Item2),
"!=" => new NotEqualBinaryExpression(previous, current.Item2),
"<>" => new NotEqualBinaryExpression(previous, current.Item2),
">" => new GreaterThanBinaryExpression(previous, current.Item2, true),
"<" => new LowerThanExpression(previous, current.Item2, true),
">=" => new GreaterThanBinaryExpression(previous, current.Item2, false),
"<=" => new LowerThanExpression(previous, current.Item2, false),
_ => null
};
}
Expand Down Expand Up @@ -170,7 +162,7 @@ public FluidParser()
return result;
});

LogicalExpression.Parser = Comparison;
//LogicalExpression.Parser = LogicalOperator;

var Output = OutputStart.SkipAnd(FilterExpression.And(OutputEnd)
.Then<Statement>(static x => new OutputStatement(x.Item1))
Expand Down

0 comments on commit 76cc72c

Please sign in to comment.