diff --git a/Fluid.Tests/ParserTests.cs b/Fluid.Tests/ParserTests.cs index 9acbba44..ec41372a 100644 --- a/Fluid.Tests/ParserTests.cs +++ b/Fluid.Tests/ParserTests.cs @@ -461,5 +461,20 @@ public void SizeAppliedToStrings(string source, string expected) Assert.Equal(expected, rendered); } - } + + [Theory] + [InlineData("{{ '{{ {% %} }}' }}{% assign x = '{{ {% %} }}' %}{{ x }}", "{{ {% %} }}{{ {% %} }}")] + public void StringsCanContainCurlies(string source, string expected) + { + var result = _parser.TryParse(source, out var template, out var errors); + + Assert.True(result); + Assert.NotNull(template); + Assert.Null(errors); + + var rendered = template.Render(); + + Assert.Equal(expected, rendered); + } + } } diff --git a/Fluid/Ast/FilterExpression.cs b/Fluid/Ast/FilterExpression.cs index 0a3a2010..f9516706 100644 --- a/Fluid/Ast/FilterExpression.cs +++ b/Fluid/Ast/FilterExpression.cs @@ -10,7 +10,7 @@ public FilterExpression(Expression input, string name, List para { Input = input; Name = name; - Parameters = parameters; + Parameters = parameters ?? new List(); } public Expression Input { get; } diff --git a/Fluid/Ast/NamedExpressionList.cs b/Fluid/Ast/NamedExpressionList.cs new file mode 100644 index 00000000..90b44611 --- /dev/null +++ b/Fluid/Ast/NamedExpressionList.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; + +namespace Fluid.Ast +{ + public class NamedExpressionList + { + public static readonly NamedExpressionList Empty = new NamedExpressionList(); + + private List _positional; + private Dictionary _named; + + public int Count => _positional?.Count ?? 0; + + public bool HasNamed(string name) + { + return _named != null && _named.ContainsKey(name); + } + + public Expression this[int index] + { + get + { + if (_positional == null || index >= _positional.Count) + { + return null; + } + + return _positional[index]; + } + } + + public Expression this[string name] + { + get + { + if (_named != null && _named.TryGetValue(name, out var value)) + { + return value; + } + + return null; + } + } + + public Expression this[string name, int index] + { + get + { + return this[name] ?? this[index]; + } + } + + public NamedExpressionList() + { + } + + public NamedExpressionList(params Expression[] values) + { + _positional = new List(values); + } + + public NamedExpressionList(List arguments) + { + foreach (var argument in arguments) + { + Add(argument.Name, argument.Expression); + } + } + + public NamedExpressionList Add(string name, Expression value) + { + if (name != null) + { + if (_named == null) + { + _named = new Dictionary(); + } + + _named.Add(name, value); + } + + if (_positional == null) + { + _positional = new List(); + } + + _positional.Add(value); + + return this; + } + + public IEnumerable Names => _named?.Keys ?? System.Linq.Enumerable.Empty(); + + public IEnumerable Values => _positional; + } +} diff --git a/Fluid/FluidParser.cs b/Fluid/FluidParser.cs index a7d1400f..2db46eb5 100644 --- a/Fluid/FluidParser.cs +++ b/Fluid/FluidParser.cs @@ -50,8 +50,9 @@ public class FluidParser protected static readonly Parser Identifier = Terms.Identifier(extraPart: static c => c == '-').Then(x => x.ToString()); + protected readonly Parser> ArgumentsList; + protected readonly Parser LogicalExpression; protected static readonly Deferred Primary = Deferred(); - protected static readonly Parser LogicalExpression; protected static readonly Deferred FilterExpression = Deferred(); protected readonly Deferred> KnownTagsList = Deferred>(); protected readonly Deferred> AnyTagsList = Deferred>(); @@ -99,7 +100,7 @@ public FluidParser() // TODO: 'and' has a higher priority than 'or', either create a new scope, or implement operators priority - var LogicalExpression = Primary.And(ZeroOrMany(OneOf(BinaryOr, BinaryAnd, Contains, DoubleEquals, NotEquals, Different, GreaterOr, LowerOr, Greater, Lower).And(Primary))) + LogicalExpression = Primary.And(ZeroOrMany(OneOf(BinaryOr, BinaryAnd, Contains, DoubleEquals, NotEquals, Different, GreaterOr, LowerOr, Greater, Lower).And(Primary))) .Then(static x => { if (x.Item2.Count == 0) @@ -133,19 +134,19 @@ public FluidParser() return result; }); - // Primary ( | identifer [ ':' ([name :] value ,)+ )! ] )* + // ([name :] value ,)+ + ArgumentsList = Separated(Comma, + OneOf( + Identifier.AndSkip(Colon).And(Primary).Then(static x => new FilterArgument(x.Item1, x.Item2)), + Primary.Then(static x => new FilterArgument(null, x)) + )); + + // Primary ( | identifer ( ':' ArgumentsList )! ] )* FilterExpression.Parser = Primary .And(ZeroOrMany( Pipe .SkipAnd(Identifier) - .And(ZeroOrOne(Colon.SkipAnd( - Separated(Comma, - OneOf( - Identifier.AndSkip(Colon).And(Primary).Then(static x => new FilterArgument(x.Item1, x.Item2)), - Primary.Then(static x => new FilterArgument(null, x)) - )) - ))) - )) + .And(ZeroOrOne(Colon.SkipAnd(ArgumentsList))))) .Then(x => { // Primary @@ -157,14 +158,12 @@ public FluidParser() var identifier = pipeResult.Item1; var arguments = pipeResult.Item2; - result = new FilterExpression(result, identifier, arguments ?? new List()); + result = new FilterExpression(result, identifier, arguments); } return result; }); - //LogicalExpression.Parser = LogicalOperator; - var Output = OutputStart.SkipAnd(FilterExpression.And(OutputEnd) .Then(static x => new OutputStatement(x.Item1)) .ElseError("Invalid tag, expected an expression") @@ -405,19 +404,9 @@ public FluidParser() public static Parser CreateTag(string tagName) => TagStart.SkipAnd(Terms.Text(tagName)).AndSkip(TagEnd); - public void RegisterEmptyTag(string tagName, Func> render) - { - RegisteredTags[tagName] = TagEnd.Then(x => new EmptyTagStatement(render)); - } - public void RegisterIdentifierTag(string tagName, Func> render) { - RegisteredTags[tagName] = Identifier.AndSkip(TagEnd).Then(x => new IdentifierTagStatement(x, render)); - } - - public void RegisterEmptyBlock(string tagName, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) - { - RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName)).Then(x => new EmptyBlockStatement(x, render)); + RegisterParserTag(tagName, Identifier, render); } public void RegisterIdentifierBlock(string tagName, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) @@ -427,12 +416,32 @@ public void RegisterIdentifierBlock(string tagName, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) { - RegisterParserBlock(tagName, Primary, render); + RegisterParserBlock(tagName, FilterExpression, render); + } + + public void RegisterExpressionTag(string tagName, Func> render) + { + RegisterParserTag(tagName, FilterExpression, render); } public void RegisterParserBlock(string tagName, Parser parser, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) { RegisteredTags[tagName] = parser.AndSkip(TagEnd).And(AnyTagsList).AndSkip(CreateTag("end" + tagName)).Then(x => new ParserBlockStatement(x.Item1, x.Item2, render)); } + + public void RegisterParserTag(string tagName, Parser parser, Func> render) + { + RegisteredTags[tagName] = parser.AndSkip(TagEnd).Then(x => new ParserTagStatement(x, render)); + } + + public void RegisterEmptyTag(string tagName, Func> render) + { + RegisteredTags[tagName] = TagEnd.Then(x => new EmptyTagStatement(render)); + } + + public void RegisterEmptyBlock(string tagName, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) + { + RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName)).Then(x => new EmptyBlockStatement(x, render)); + } } } diff --git a/Fluid/Parser/IdentifierTagStatement.cs b/Fluid/Parser/IdentifierTagStatement.cs deleted file mode 100644 index 69e73535..00000000 --- a/Fluid/Parser/IdentifierTagStatement.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Fluid.Ast; -using System; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; - -namespace Fluid.Parser -{ - internal sealed class IdentifierTagStatement : Statement - { - private readonly Func> _render; - - public IdentifierTagStatement(string identifier, Func> render) - { - Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier)); - _render = render ?? throw new ArgumentNullException(nameof(render)); - } - - public string Identifier { get; } - - public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) - { - return _render(Identifier, writer, encoder, context); - } - } -} diff --git a/Fluid/Parser/ParserTagStatement.cs b/Fluid/Parser/ParserTagStatement.cs new file mode 100644 index 00000000..679fccac --- /dev/null +++ b/Fluid/Parser/ParserTagStatement.cs @@ -0,0 +1,26 @@ +using Fluid.Ast; +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Fluid.Parser +{ + internal sealed class ParserTagStatement : Statement + { + private readonly Func> _render; + + public ParserTagStatement(T value, Func> render) + { + Value = value; + _render = render ?? throw new ArgumentNullException(nameof(render)); + } + + public T Value { get; } + + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + { + return _render(Value, writer, encoder, context); + } + } +} diff --git a/README.md b/README.md index 72651c9d..5e35bcd4 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,7 @@ Each custom tag needs to provide a delegate that is evaluated when the tag is ma - __Empty__: Tag with no parameter, like `{% renderbody %}` - __Identifier__: Tag taking an identifier as parameter, like `{% increment my_variable %}` +- __Expression__: Tag taking an expression as parameter, like `{% layout 'home' | append: '.liquid' %}` Here are some examples: @@ -446,6 +447,14 @@ Hi! Hi! Hi!
+### Custom parsers + +If __identifier__, __empty__ and __expression__ parsers are not sufficient, the methods `RegisterParserBlock` and `RegisterParserTag` accept +any custom parser construct. These can be the standard ones defined in the `FluidParser` class, like `Primary`, or any other composition of them. + +For instance, `RegisterParseTag(Primary.AndSkip(Comma).And(Primary), ...)` will expect two `Primary` elements separated by a comma. The delegate will then +be invoked with a `ValueTuple` representing the two `Primary` expressions. + ## ASP.NET MVC View Engine To provide a convenient view engine implementation for ASP.NET Core MVC the grammar is extended as described in [Customizing tags](#customizing-tags) by adding these new tags: