Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Fluid.Tests/BinaryExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,39 @@ public void ArrayValuesShouldBeEqual()
Assert.True(actual.Equals(expected));
}

[Theory]
[InlineData("{{ 2 == 3 }}", "2")]
[InlineData("{{ 5 == 5 }}", "5")]
[InlineData("{{ 10 != 5 }}", "10")]
[InlineData("{{ 10 > 5 }}", "10")]
[InlineData("{{ 3 < 5 }}", "3")]
[InlineData("{{ 10 >= 5 }}", "10")]
[InlineData("{{ 3 <= 5 }}", "3")]
[InlineData("{{ true and false }}", "true")]
[InlineData("{{ true or false }}", "true")]
[InlineData("{{ 'abc' contains 'a' }}", "abc")]
[InlineData("{{ 'abc' startswith 'a' }}", "abc")]
[InlineData("{{ 'abc' endswith 'c' }}", "abc")]
public async Task BinaryExpressionsReturnLeftOperand(string source, string expected)
{
// Binary expressions should return the left operand according to Liquid standard
_parser.TryParse(source, out var template, out var messages);
var result = await template.RenderAsync();
Assert.Equal(expected, result);
}

[Theory]
[InlineData("{{ 2 == 3 | plus: 10 | minus: 3 }}", "9")]
[InlineData("{{ 5 == 5 | plus: 3 }}", "8")]
[InlineData("{{ 10 > 5 | minus: 2 }}", "8")]
public async Task BinaryExpressionsWithFilters(string source, string expected)
{
// Binary expressions return left operand which can then be filtered
_parser.TryParse(source, out var template, out var messages);
var result = await template.RenderAsync();
Assert.Equal(expected, result);
}

[Fact]
public async Task ObjectValuesShouldCompareByValueNotReference()
{
Expand Down
3 changes: 2 additions & 1 deletion Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,8 @@ public void ModelShouldNotImpactBlank()
var model = new { a = " ", b = "" };
var context = new TemplateContext(model);
var template = _parser.Parse(source);
Assert.Equal("true", template.Render(context));
// Binary expressions return the left operand when output
Assert.Equal(" ", template.Render(context));
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public AndBinaryExpression(Expression left, Expression right) : base(left, right

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return BooleanValue.Create(leftValue.ToBooleanValue() && rightValue.ToBooleanValue());
var comparisonResult = leftValue.ToBooleanValue() && rightValue.ToBooleanValue();
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitAndBinaryExpression(this);
Expand Down
5 changes: 2 additions & 3 deletions Fluid/Ast/BinaryExpressions/ContainsBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ public ContainsBinaryExpression(Expression left, Expression right) : base(left,

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue.Contains(rightValue)
? BooleanValue.True
: BooleanValue.False;
var comparisonResult = leftValue.Contains(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitContainsBinaryExpression(this);
Expand Down
11 changes: 5 additions & 6 deletions Fluid/Ast/BinaryExpressions/EndsWithBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@ public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext contex
var leftValue = await Left.EvaluateAsync(context);
var rightValue = await Right.EvaluateAsync(context);

bool comparisonResult;
if (leftValue is ArrayValue)
{
var first = await leftValue.GetValueAsync("last", context);
return first.Equals(rightValue)
? BooleanValue.True
: BooleanValue.False;
comparisonResult = first.Equals(rightValue);
}
else
{
return leftValue.ToStringValue().EndsWith(rightValue.ToStringValue())
? BooleanValue.True
: BooleanValue.False;
comparisonResult = leftValue.ToStringValue().EndsWith(rightValue.ToStringValue());
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitEndsWithBinaryExpression(this);
Expand Down
5 changes: 2 additions & 3 deletions Fluid/Ast/BinaryExpressions/EqualBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ public EqualBinaryExpression(Expression left, Expression right) : base(left, rig

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue.Equals(rightValue)
? BooleanValue.True
: BooleanValue.False;
var comparisonResult = leftValue.Equals(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitEqualBinaryExpression(this);
Expand Down
34 changes: 19 additions & 15 deletions Fluid/Ast/BinaryExpressions/GreaterThanBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,37 @@ public GreaterThanBinaryExpression(Expression left, Expression right, bool stric

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
bool comparisonResult;

if (leftValue.IsNil() || rightValue.IsNil())
{
if (Strict)
{
return BooleanValue.False;
comparisonResult = false;
}
else
{
comparisonResult = leftValue.IsNil() && rightValue.IsNil();
}

return leftValue.IsNil() && rightValue.IsNil()
? BooleanValue.True
: BooleanValue.False;
}

if (leftValue is NumberValue)
else if (leftValue is NumberValue)
{
if (Strict)
{
return leftValue.ToNumberValue() > rightValue.ToNumberValue()
? BooleanValue.True
: BooleanValue.False;
comparisonResult = leftValue.ToNumberValue() > rightValue.ToNumberValue();
}

return leftValue.ToNumberValue() >= rightValue.ToNumberValue()
? BooleanValue.True
: BooleanValue.False;
else
{
comparisonResult = leftValue.ToNumberValue() >= rightValue.ToNumberValue();
}
}
else
{
// For non-number types, return nil as left operand with false comparison
return new BinaryExpressionFluidValue(NilValue.Instance, false);
}

return NilValue.Instance;
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitGreaterThanBinaryExpression(this);
Expand Down
34 changes: 19 additions & 15 deletions Fluid/Ast/BinaryExpressions/LowerThanBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,37 @@ public LowerThanBinaryExpression(Expression left, Expression right, bool strict)

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
bool comparisonResult;

if (leftValue.IsNil() || rightValue.IsNil())
{
if (Strict)
{
return BooleanValue.False;
comparisonResult = false;
}
else
{
comparisonResult = leftValue.IsNil() && rightValue.IsNil();
}

return leftValue.IsNil() && rightValue.IsNil()
? BooleanValue.True
: BooleanValue.False;
}

if (leftValue is NumberValue)
else if (leftValue is NumberValue)
{
if (Strict)
{
return leftValue.ToNumberValue() < rightValue.ToNumberValue()
? BooleanValue.True
: BooleanValue.False;
comparisonResult = leftValue.ToNumberValue() < rightValue.ToNumberValue();
}

return leftValue.ToNumberValue() <= rightValue.ToNumberValue()
? BooleanValue.True
: BooleanValue.False;
else
{
comparisonResult = leftValue.ToNumberValue() <= rightValue.ToNumberValue();
}
}
else
{
// For non-number types, return nil as left operand with false comparison
return new BinaryExpressionFluidValue(NilValue.Instance, false);
}

return NilValue.Instance;
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitLowerThanBinaryExpression(this);
Expand Down
5 changes: 2 additions & 3 deletions Fluid/Ast/BinaryExpressions/NotEqualBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ public NotEqualBinaryExpression(Expression left, Expression right) : base(left,

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue.Equals(rightValue)
? BooleanValue.False
: BooleanValue.True;
var comparisonResult = !leftValue.Equals(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitNotEqualBinaryExpression(this);
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public OrBinaryExpression(Expression left, Expression right) : base(left, right)

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return BooleanValue.Create(leftValue.ToBooleanValue() || rightValue.ToBooleanValue());
var comparisonResult = leftValue.ToBooleanValue() || rightValue.ToBooleanValue();
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitOrBinaryExpression(this);
Expand Down
11 changes: 5 additions & 6 deletions Fluid/Ast/BinaryExpressions/StartsWithBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@ public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext contex
var leftValue = await Left.EvaluateAsync(context);
var rightValue = await Right.EvaluateAsync(context);

bool comparisonResult;
if (leftValue is ArrayValue)
{
var first = await leftValue.GetValueAsync("first", context);
return first.Equals(rightValue)
? BooleanValue.True
: BooleanValue.False;
comparisonResult = first.Equals(rightValue);
}
else
{
return leftValue.ToStringValue().StartsWith(rightValue.ToStringValue())
? BooleanValue.True
: BooleanValue.False;
comparisonResult = leftValue.ToStringValue().StartsWith(rightValue.ToStringValue());
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitStartsWithBinaryExpression(this);
Expand Down
82 changes: 82 additions & 0 deletions Fluid/Values/BinaryExpressionFluidValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Globalization;
using System.Text.Encodings.Web;

namespace Fluid.Values
{
/// <summary>
/// A FluidValue that wraps a binary expression result.
/// It returns the comparison result when used in a boolean context (e.g., {% if %})
/// and returns the left operand when rendered (e.g., {{ }}).
/// </summary>
public sealed class BinaryExpressionFluidValue : FluidValue
{
private readonly FluidValue _leftOperand;
private readonly bool _comparisonResult;

public BinaryExpressionFluidValue(FluidValue leftOperand, bool comparisonResult)
{
_leftOperand = leftOperand ?? NilValue.Instance;
_comparisonResult = comparisonResult;
}

public override FluidValues Type => _leftOperand.Type;

public override bool Equals(FluidValue other)
{
return _leftOperand.Equals(other);
}

public override bool ToBooleanValue()
{
// Return the comparison result for conditional logic
return _comparisonResult;
}

public override decimal ToNumberValue()
{
return _leftOperand.ToNumberValue();
}

public override string ToStringValue()
{
// When converted to string (e.g., for filters), return the boolean result as string
return _comparisonResult ? "true" : "false";
}

public override object ToObjectValue()
{
return _leftOperand.ToObjectValue();
}

public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
// Delegate rendering to the left operand
return _leftOperand.WriteToAsync(writer, encoder, cultureInfo);
}

public override bool IsNil()
{
return _leftOperand.IsNil();
}

public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
return _leftOperand.GetValueAsync(name, context);
}

public override ValueTask<FluidValue> GetIndexAsync(FluidValue index, TemplateContext context)
{
return _leftOperand.GetIndexAsync(index, context);
}

public override bool Contains(FluidValue value)
{
return _leftOperand.Contains(value);
}

public override IEnumerable<FluidValue> Enumerate(TemplateContext context)
{
return _leftOperand.Enumerate(context);
}
}
}