diff --git a/Fluid.Tests/VisitorTest.cs b/Fluid.Tests/VisitorTest.cs index eb23704a..2b5b59c6 100644 --- a/Fluid.Tests/VisitorTest.cs +++ b/Fluid.Tests/VisitorTest.cs @@ -1,5 +1,8 @@ -using Fluid.Tests.Visitors; +using Fluid.Ast; +using Fluid.Tests.Visitors; using Fluid.Values; +using Fluid.ViewEngine; +using Parlot.Fluent; using Xunit; namespace Fluid.Tests @@ -73,5 +76,61 @@ public void ShouldDetectForLoopUsage() Assert.True(result1); Assert.False(result2); } + + [Fact] + public void VisitorShouldVisitParserTag() + { + var template = new FluidViewParser().Parse("{% layout '_Layout' %}"); + + var visitor = new ParserVisitor(); + visitor.VisitTemplate(template); + + Assert.Equal("layout", visitor.TagName); + Assert.IsType(visitor.Value); + } + + [Fact] + public void VisitorShouldVisitParserBlock() + { + var template = new FluidViewParser().Parse("{% section body %}HELLO{% endsection %}"); + + var visitor = new ParserVisitor(); + visitor.VisitTemplate(template); + + Assert.Equal("section", visitor.TagName); + Assert.IsType(visitor.Value); + Assert.Single(visitor.Statements); + } + + [Fact] + public void VisitorShouldVisitEmptyTag() + { + var template = new FluidViewParser().Parse("{% renderbody %}"); + + var visitor = new ParserVisitor(); + visitor.VisitTemplate(template); + + Assert.Equal("renderbody", visitor.TagName); + } + + [Fact] + public void VisitorShouldVisitEmptyBlock() + { + var parser = new FluidParser(); + + parser.RegisterEmptyBlock("hello", static (s, w, e, c) => + { + w.Write("Hello World"); + return s.RenderStatementsAsync(w, e, c); + }); + + var template = parser.Parse("{% hello %}HELLO{% endhello %}"); + + var visitor = new ParserVisitor(); + visitor.VisitTemplate(template); + + Assert.Equal("hello", visitor.TagName); + Assert.Single(visitor.Statements); + } } } diff --git a/Fluid.Tests/Visitors/ParserVisitor.cs b/Fluid.Tests/Visitors/ParserVisitor.cs new file mode 100644 index 00000000..2ce853bc --- /dev/null +++ b/Fluid.Tests/Visitors/ParserVisitor.cs @@ -0,0 +1,45 @@ +using Fluid.Ast; +using Fluid.Parser; +using System.Collections.Generic; + +namespace Fluid.Tests.Visitors +{ + internal class ParserVisitor : AstVisitor + { + public string TagName { get; set; } + public object Value { get; set; } + public IReadOnlyList Statements { get; set; } + + protected override Statement VisitParserTagStatement(ParserTagStatement parserTagStatement) + { + TagName = parserTagStatement.TagName; + Value = parserTagStatement.Value; + + return parserTagStatement; + } + + protected override Statement VisitParserBlockStatement(ParserBlockStatement parserBlockStatement) + { + TagName = parserBlockStatement.TagName; + Value = parserBlockStatement.Value; + Statements = parserBlockStatement.Statements; + + return parserBlockStatement; + } + + protected override Statement VisitEmptyTagStatement(EmptyTagStatement emptyTagStatement) + { + TagName = emptyTagStatement.TagName; + + return emptyTagStatement; + } + + protected override Statement VisitEmptyBlockStatement(EmptyBlockStatement emptyBlockStatement) + { + TagName = emptyBlockStatement.TagName; + Statements = emptyBlockStatement.Statements; + + return emptyBlockStatement; + } + } +} diff --git a/Fluid/Ast/AstRewriter.cs b/Fluid/Ast/AstRewriter.cs index 2c5c54ef..4b5f6c05 100644 --- a/Fluid/Ast/AstRewriter.cs +++ b/Fluid/Ast/AstRewriter.cs @@ -1,4 +1,4 @@ -using Fluid.Ast.BinaryExpressions; +using Fluid.Ast.BinaryExpressions; using Fluid.Parser; namespace Fluid.Ast @@ -331,6 +331,21 @@ protected internal override Statement VisitElseStatement(ElseStatement elseState return elseStatement; } + protected internal override Statement VisitEmptyBlockStatement(EmptyBlockStatement emptyBlockStatement) + { + if (TryRewriteStatements(emptyBlockStatement.Statements, out var newStatements)) + { + return new EmptyBlockStatement(emptyBlockStatement.TagName, newStatements.ToList(), emptyBlockStatement.Render); + } + + return emptyBlockStatement; + } + + protected internal override Statement VisitEmptyTagStatement(EmptyTagStatement emptyTagStatement) + { + return emptyTagStatement; + } + protected internal override Expression VisitFilterExpression(FilterExpression filterExpression) { var updated = false; @@ -467,6 +482,21 @@ protected internal override Statement VisitOutputStatement(OutputStatement outpu return outputStatement; } + protected internal override Statement VisitParserBlockStatement(ParserBlockStatement parserBlockStatement) + { + if (TryRewriteStatements(parserBlockStatement.Statements, out var newStatements)) + { + return new ParserBlockStatement(parserBlockStatement.TagName, parserBlockStatement.Value, newStatements.ToList(), parserBlockStatement.Render); + } + + return parserBlockStatement; + } + + protected internal override Statement VisitParserTagStatement(ParserTagStatement parserTagStatement) + { + return parserTagStatement; + } + protected internal override Expression VisitRangeExpression(RangeExpression rangeExpression) { if (TryRewriteExpression(rangeExpression.From, out var newFrom) | diff --git a/Fluid/Ast/AstVisitor.cs b/Fluid/Ast/AstVisitor.cs index f8a51c12..d0e98122 100644 --- a/Fluid/Ast/AstVisitor.cs +++ b/Fluid/Ast/AstVisitor.cs @@ -1,4 +1,5 @@ -using Fluid.Ast.BinaryExpressions; +using Fluid.Ast.BinaryExpressions; +using Fluid.Parser; namespace Fluid.Ast { @@ -262,6 +263,21 @@ protected internal virtual Statement VisitElseStatement(ElseStatement elseStatem return elseStatement; } + protected internal virtual Statement VisitEmptyBlockStatement(EmptyBlockStatement emptyBlockStatement) + { + foreach (var statement in emptyBlockStatement.Statements) + { + Visit(statement); + } + + return emptyBlockStatement; + } + + protected internal virtual Statement VisitEmptyTagStatement(EmptyTagStatement emptyTagStatement) + { + return emptyTagStatement; + } + protected internal virtual Expression VisitFilterExpression(FilterExpression filterExpression) { Visit(filterExpression.Input); @@ -376,6 +392,21 @@ protected internal virtual Statement VisitNoOpStatement(NoOpStatement noOpStatem return noOpStatement; } + protected internal virtual Statement VisitParserBlockStatement(ParserBlockStatement parserBlockStatement) + { + foreach (var statement in parserBlockStatement.Statements) + { + Visit(statement); + } + + return parserBlockStatement; + } + + protected internal virtual Statement VisitParserTagStatement(ParserTagStatement parserTagStatement) + { + return parserTagStatement; + } + protected internal virtual Statement VisitOutputStatement(OutputStatement outputStatement) { Visit(outputStatement.Expression); diff --git a/Fluid/FluidParser.cs b/Fluid/FluidParser.cs index 9879e248..b4818c55 100644 --- a/Fluid/FluidParser.cs +++ b/Fluid/FluidParser.cs @@ -599,27 +599,27 @@ public void RegisterExpressionTag(string tagName, Func(string tagName, Parser parser, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) { RegisteredTags[tagName] = parser.AndSkip(TagEnd).And(AnyTagsList).AndSkip(CreateTag("end" + tagName).ElseError($"'{{% end{tagName} %}}' was expected")) - .Then(x => new ParserBlockStatement(x.Item1, x.Item2, render)) + .Then(x => new ParserBlockStatement(tagName, x.Item1, x.Item2, render)) .ElseError($"Invalid {tagName} tag") ; } public void RegisterParserTag(string tagName, Parser parser, Func> render) { - RegisteredTags[tagName] = parser.AndSkip(TagEnd).Then(x => new ParserTagStatement(x, render)); + RegisteredTags[tagName] = parser.AndSkip(TagEnd).Then(x => new ParserTagStatement(tagName, x, render)); RegisteredTags[tagName].Name = tagName; } public void RegisterEmptyTag(string tagName, Func> render) { - RegisteredTags[tagName] = TagEnd.Then(x => new EmptyTagStatement(render)).ElseError($"Unexpected arguments in {tagName} tag"); + RegisteredTags[tagName] = TagEnd.Then(x => new EmptyTagStatement(tagName, render)).ElseError($"Unexpected arguments in {tagName} tag"); RegisteredTags[tagName].Name = tagName; } public void RegisterEmptyBlock(string tagName, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) { RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName).ElseError($"'{{% end{tagName} %}}' was expected")) - .Then(x => new EmptyBlockStatement(x, render)) + .Then(x => new EmptyBlockStatement(tagName, x, render)) .ElseError($"Invalid '{tagName}' tag") ; RegisteredTags[tagName].Name = tagName; diff --git a/Fluid/Parser/EmptyBlockStatement.cs b/Fluid/Parser/EmptyBlockStatement.cs index e542eab8..dc138284 100644 --- a/Fluid/Parser/EmptyBlockStatement.cs +++ b/Fluid/Parser/EmptyBlockStatement.cs @@ -1,23 +1,28 @@ -using Fluid.Ast; +using Fluid.Ast; using System.Text.Encodings.Web; namespace Fluid.Parser { - internal sealed class EmptyBlockStatement : Statement + public sealed class EmptyBlockStatement : Statement { - private readonly Func, TextWriter, TextEncoder, TemplateContext, ValueTask> _render; - - public EmptyBlockStatement(IReadOnlyList statements, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) + public EmptyBlockStatement(string tagName, IReadOnlyList statements, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) { - _render = render ?? throw new ArgumentNullException(nameof(render)); + TagName = tagName ?? throw new ArgumentNullException(nameof(tagName)); + Render = render ?? throw new ArgumentNullException(nameof(render)); Statements = statements ?? []; } + public string TagName { get; } + public IReadOnlyList Statements { get; } + public Func, TextWriter, TextEncoder, TemplateContext, ValueTask> Render { get; } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { - return _render(Statements, writer, encoder, context); + return Render(Statements, writer, encoder, context); } + + protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitEmptyBlockStatement(this); } } diff --git a/Fluid/Parser/EmptyTagStatement.cs b/Fluid/Parser/EmptyTagStatement.cs index 94ef3f35..6b2a508c 100644 --- a/Fluid/Parser/EmptyTagStatement.cs +++ b/Fluid/Parser/EmptyTagStatement.cs @@ -1,14 +1,17 @@ -using Fluid.Ast; +using Fluid.Ast; using System.Text.Encodings.Web; namespace Fluid.Parser { - internal sealed class EmptyTagStatement : Statement + public sealed class EmptyTagStatement : Statement { private readonly Func> _render; - public EmptyTagStatement(Func> render) + public string TagName { get; } + + public EmptyTagStatement(string tagName, Func> render) { + TagName = tagName ?? throw new ArgumentNullException(nameof(tagName)); _render = render ?? throw new ArgumentNullException(nameof(render)); } @@ -16,5 +19,7 @@ public override ValueTask WriteToAsync(TextWriter writer, TextEncode { return _render(writer, encoder, context); } + + protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitEmptyTagStatement(this); } } diff --git a/Fluid/Parser/ParserBlockStatement.cs b/Fluid/Parser/ParserBlockStatement.cs index b91e6c2b..dc588306 100644 --- a/Fluid/Parser/ParserBlockStatement.cs +++ b/Fluid/Parser/ParserBlockStatement.cs @@ -1,23 +1,27 @@ -using Fluid.Ast; +using Fluid.Ast; using System.Text.Encodings.Web; namespace Fluid.Parser { - internal sealed class ParserBlockStatement : TagStatement + public sealed class ParserBlockStatement : TagStatement { - private readonly Func, TextWriter, TextEncoder, TemplateContext, ValueTask> _render; - - public ParserBlockStatement(T value, IReadOnlyList statements, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) : base(statements) + public ParserBlockStatement(string tagName, T value, IReadOnlyList statements, Func, TextWriter, TextEncoder, TemplateContext, ValueTask> render) : base(statements) { Value = value; - _render = render ?? throw new ArgumentNullException(nameof(render)); + TagName = tagName ?? throw new ArgumentNullException(nameof(tagName)); + Render = render ?? throw new ArgumentNullException(nameof(render)); } + public Func, TextWriter, TextEncoder, TemplateContext, ValueTask> Render { get; } + + public string TagName { get; } public T Value { get; } public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { - return _render(Value, Statements, writer, encoder, context); + return Render(Value, Statements, writer, encoder, context); } + + protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitParserBlockStatement(this); } } diff --git a/Fluid/Parser/ParserTagStatement.cs b/Fluid/Parser/ParserTagStatement.cs index 0a04702c..6c695395 100644 --- a/Fluid/Parser/ParserTagStatement.cs +++ b/Fluid/Parser/ParserTagStatement.cs @@ -1,23 +1,28 @@ -using Fluid.Ast; +using Fluid.Ast; using System.Text.Encodings.Web; namespace Fluid.Parser { - internal sealed class ParserTagStatement : Statement + public sealed class ParserTagStatement : Statement { - private readonly Func> _render; - - public ParserTagStatement(T value, Func> render) + public ParserTagStatement(string tagName, T value, Func> render) { Value = value; - _render = render ?? throw new ArgumentNullException(nameof(render)); + TagName = tagName ?? throw new ArgumentNullException(nameof(tagName)); + Render = render ?? throw new ArgumentNullException(nameof(render)); } + public Func> Render { get; } + + public string TagName { get; } + public T Value { get; } public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { - return _render(Value, writer, encoder, context); + return Render(Value, writer, encoder, context); } + + protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitParserTagStatement(this); } } diff --git a/README.md b/README.md index 4a2a89b1..684f78e4 100644 --- a/README.md +++ b/README.md @@ -1103,6 +1103,17 @@ var result = changed.Render(); Console.WriteLine(result); // writes -1 ``` +### Custom parsers + +The [custom statements and expressions](#custom-parsers) can also be visited by using one of these methods: + +- `VisitParserTagStatement(ParserTagStatement)` +- `VisitParserBlockStatement(ParserBlockStatement)` +- `VisitEmptyTagStatement(EmptyTagStatement)` +- `VisitEmptyBlockStatement(EmptyBlockStatement)` + +They all expose a `TagName` property and optionally a `Statements` and `Value` ones when it applies. + ## Performance ### Caching