Skip to content

Commit

Permalink
Implement parentheses support (#727)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Dec 6, 2024
1 parent d262d07 commit 02137f5
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 8 deletions.
10 changes: 6 additions & 4 deletions Fluid.Tests/FromStatementTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
Expand All @@ -11,10 +11,12 @@ namespace Fluid.Tests;

public class FromStatementTests
{
// Enable all parsing options to ensure these custom features don't interfere with standard templates.

#if COMPILED
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true }).Compile();
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true, AllowParentheses = true }).Compile();
#else
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true });
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true, AllowParentheses = true });
#endif

[Fact]
Expand Down Expand Up @@ -122,4 +124,4 @@ Hello world!
var result = await template.RenderAsync(context);
Assert.Equal("Hello world! Hello John Doe!", result);
}
}
}
36 changes: 36 additions & 0 deletions Fluid.Tests/ParenthesesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Fluid.Parser;
using Xunit;

namespace Fluid.Tests
{
public class ParenthesesTests
{
#if COMPILED
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowParentheses = true }).Compile();
#else
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowParentheses = true });
#endif

[Fact]
public void ShouldGroupFilters()
{
Assert.True(_parser.TryParse("{{ 1 | plus : (2 | times: 3) }}", out var template, out var errors));
Assert.Equal("7", template.Render());
}

[Fact]
public void ShouldNotParseParentheses()
{
var options = new FluidParserOptions { AllowParentheses = false };

#if COMPILED
var parser = new FluidParser(options).Compile();
#else
var parser = new FluidParser(options);
#endif

Assert.False(parser.TryParse("{{ 1 | plus : (2 | times: 3) }}", out var template, out var errors));
Assert.Contains(ErrorMessages.ParenthesesNotAllowed, errors);
}
}
}
8 changes: 7 additions & 1 deletion Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ public FluidParser(FluidParserOptions parserOptions)
.Then<Expression>(x => new RangeExpression(x.Item1, x.Item2));
Range.Name = "Range";

var Group = parserOptions.AllowParentheses
? LParen.SkipAnd(FilterExpression).AndSkip(RParen)
: LParen.SkipAnd(FilterExpression).AndSkip(RParen).Error<Expression>(ErrorMessages.ParenthesesNotAllowed)
;
Group.Name = "Group";

// primary => NUMBER | STRING | property
Primary.Parser =
String.Then<Expression>(x => new LiteralExpression(StringValue.Create(x)))
Expand All @@ -141,6 +147,7 @@ public FluidParser(FluidParserOptions parserOptions)
return x;
}))
.Or(Number.Then<Expression>(x => new LiteralExpression(NumberValue.Create(x))))
.Or(Group)
.Or(Range)
;
Primary.Name = "Primary";
Expand Down Expand Up @@ -194,7 +201,6 @@ public FluidParser(FluidParserOptions parserOptions)
"and" => new AndBinaryExpression(previous, result),
_ => throw new ParseException()
};

}

return result;
Expand Down
7 changes: 6 additions & 1 deletion Fluid/FluidParserOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Fluid
namespace Fluid
{
/// <summary>
/// Parser options.
Expand All @@ -9,5 +9,10 @@ public class FluidParserOptions
/// Gets whether functions are allowed in templates. Default is <c>false</c>.
/// </summary>
public bool AllowFunctions { get; set; }

/// <summary>
/// Gets whether parentheses are allowed in templates. Default is <c>false</c>.
/// </summary>
public bool AllowParentheses { get; set; }
}
}
5 changes: 3 additions & 2 deletions Fluid/Parser/ErrorMessages.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Fluid.Parser
namespace Fluid.Parser
{
public static class ErrorMessages
{
Expand All @@ -10,7 +10,8 @@ public static class ErrorMessages
public const string ExpectedTagEnd = "End of tag '%}' was expected";
public const string ExpectedOutputEnd = "End of tag '}}' was expected";
public const string ExpectedStringRender = "A quoted string value is required for the render tag";
public const string FunctionsNotAllowed = "Functions are not allowed";
public const string FunctionsNotAllowed = "Functions are not allowed. To enable the feature use the 'AllowFunctions' option.";
public const string ParenthesesNotAllowed = "Parentheses are not allowed in order to group expressions. To enable the feature use the 'AllowParentheses' option.";
[Obsolete("Error no longer used")] public const string IdentifierAfterMacro = "An identifier was expected after the 'macro' tag";
public const string IdentifierAfterTag = "An identifier was expected after the '{0}' tag";
public const string ParentesesAfterFunctionName = "Start of arguments '(' is expected after a function name";
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,29 @@ template.Render(context);

<br>

## Order of execution

With tags with more than one `and` or `or` operator, operators are checked in order from right to left. You cannot change the order of operations using parentheses. This is the same for filters which are executed from left to right.
However Fluid provides an option to support grouping expression with parentheses.

### Enabling parentheses

When instantiating a `FluidParser` set the `FluidParserOptions.AllowParentheses` property to `true`.

```
var parser = new FluidParser(new FluidParserOptions { AllowParentheses = true });
```

When parentheses are used while the feature is not enabled, a parse error will be returned (unless for ranges like `(1..4)`).

At that point a template like the following will work:

```liquid
{{ 1 | plus : (2 | times: 3) }}
```

<br>

## Visiting and altering a template

Fluid provides a __Visitor__ pattern allowing you to analyze what a template is made of, but also altering it. This can be used for instance to check if a specific identifier is used, replace some filters by another one, or remove any expression that might not be authorized.
Expand Down

0 comments on commit 02137f5

Please sign in to comment.