diff --git a/Fluid.Tests/FromStatementTests.cs b/Fluid.Tests/FromStatementTests.cs index b76be350..17c17c50 100644 --- a/Fluid.Tests/FromStatementTests.cs +++ b/Fluid.Tests/FromStatementTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -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] @@ -122,4 +124,4 @@ Hello world! var result = await template.RenderAsync(context); Assert.Equal("Hello world! Hello John Doe!", result); } -} \ No newline at end of file +} diff --git a/Fluid.Tests/ParenthesesTests.cs b/Fluid.Tests/ParenthesesTests.cs new file mode 100644 index 00000000..63226632 --- /dev/null +++ b/Fluid.Tests/ParenthesesTests.cs @@ -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); + } + } +} diff --git a/Fluid/FluidParser.cs b/Fluid/FluidParser.cs index 46ac3267..e2fea937 100644 --- a/Fluid/FluidParser.cs +++ b/Fluid/FluidParser.cs @@ -122,6 +122,12 @@ public FluidParser(FluidParserOptions parserOptions) .Then(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(ErrorMessages.ParenthesesNotAllowed) + ; + Group.Name = "Group"; + // primary => NUMBER | STRING | property Primary.Parser = String.Then(x => new LiteralExpression(StringValue.Create(x))) @@ -141,6 +147,7 @@ public FluidParser(FluidParserOptions parserOptions) return x; })) .Or(Number.Then(x => new LiteralExpression(NumberValue.Create(x)))) + .Or(Group) .Or(Range) ; Primary.Name = "Primary"; @@ -194,7 +201,6 @@ public FluidParser(FluidParserOptions parserOptions) "and" => new AndBinaryExpression(previous, result), _ => throw new ParseException() }; - } return result; diff --git a/Fluid/FluidParserOptions.cs b/Fluid/FluidParserOptions.cs index f645f1a7..fe1f1727 100644 --- a/Fluid/FluidParserOptions.cs +++ b/Fluid/FluidParserOptions.cs @@ -1,4 +1,4 @@ -namespace Fluid +namespace Fluid { /// /// Parser options. @@ -9,5 +9,10 @@ public class FluidParserOptions /// Gets whether functions are allowed in templates. Default is false. /// public bool AllowFunctions { get; set; } + + /// + /// Gets whether parentheses are allowed in templates. Default is false. + /// + public bool AllowParentheses { get; set; } } } diff --git a/Fluid/Parser/ErrorMessages.cs b/Fluid/Parser/ErrorMessages.cs index f4ea86fe..6923c4b3 100644 --- a/Fluid/Parser/ErrorMessages.cs +++ b/Fluid/Parser/ErrorMessages.cs @@ -1,4 +1,4 @@ -namespace Fluid.Parser +namespace Fluid.Parser { public static class ErrorMessages { @@ -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"; diff --git a/README.md b/README.md index b8394321..dd7b38e5 100644 --- a/README.md +++ b/README.md @@ -971,6 +971,29 @@ template.Render(context);
+## 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) }} +``` + +
+ ## 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.