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
61 changes: 60 additions & 1 deletion Fluid.Tests/VisitorTest.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<LiteralExpression>(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<string>(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);
}
}
}
45 changes: 45 additions & 0 deletions Fluid.Tests/Visitors/ParserVisitor.cs
Original file line number Diff line number Diff line change
@@ -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<Statement> Statements { get; set; }

protected override Statement VisitParserTagStatement<T>(ParserTagStatement<T> parserTagStatement)
{
TagName = parserTagStatement.TagName;
Value = parserTagStatement.Value;

return parserTagStatement;
}

protected override Statement VisitParserBlockStatement<T>(ParserBlockStatement<T> 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;
}
}
}
32 changes: 31 additions & 1 deletion Fluid/Ast/AstRewriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Fluid.Ast.BinaryExpressions;
using Fluid.Ast.BinaryExpressions;
using Fluid.Parser;

namespace Fluid.Ast
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -467,6 +482,21 @@ protected internal override Statement VisitOutputStatement(OutputStatement outpu
return outputStatement;
}

protected internal override Statement VisitParserBlockStatement<T>(ParserBlockStatement<T> parserBlockStatement)
{
if (TryRewriteStatements(parserBlockStatement.Statements, out var newStatements))
{
return new ParserBlockStatement<T>(parserBlockStatement.TagName, parserBlockStatement.Value, newStatements.ToList(), parserBlockStatement.Render);
}

return parserBlockStatement;
}

protected internal override Statement VisitParserTagStatement<T>(ParserTagStatement<T> parserTagStatement)
{
return parserTagStatement;
}

protected internal override Expression VisitRangeExpression(RangeExpression rangeExpression)
{
if (TryRewriteExpression(rangeExpression.From, out var newFrom) |
Expand Down
33 changes: 32 additions & 1 deletion Fluid/Ast/AstVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Fluid.Ast.BinaryExpressions;
using Fluid.Ast.BinaryExpressions;
using Fluid.Parser;

namespace Fluid.Ast
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -376,6 +392,21 @@ protected internal virtual Statement VisitNoOpStatement(NoOpStatement noOpStatem
return noOpStatement;
}

protected internal virtual Statement VisitParserBlockStatement<T>(ParserBlockStatement<T> parserBlockStatement)
{
foreach (var statement in parserBlockStatement.Statements)
{
Visit(statement);
}

return parserBlockStatement;
}

protected internal virtual Statement VisitParserTagStatement<T>(ParserTagStatement<T> parserTagStatement)
{
return parserTagStatement;
}

protected internal virtual Statement VisitOutputStatement(OutputStatement outputStatement)
{
Visit(outputStatement.Expression);
Expand Down
8 changes: 4 additions & 4 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,27 +599,27 @@ public void RegisterExpressionTag(string tagName, Func<Expression, TextWriter, T
public void RegisterParserBlock<T>(string tagName, Parser<T> parser, Func<T, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = parser.AndSkip(TagEnd).And(AnyTagsList).AndSkip(CreateTag("end" + tagName).ElseError($"'{{% end{tagName} %}}' was expected"))
.Then<Statement>(x => new ParserBlockStatement<T>(x.Item1, x.Item2, render))
.Then<Statement>(x => new ParserBlockStatement<T>(tagName, x.Item1, x.Item2, render))
.ElseError($"Invalid {tagName} tag")
;
}

public void RegisterParserTag<T>(string tagName, Parser<T> parser, Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = parser.AndSkip(TagEnd).Then<Statement>(x => new ParserTagStatement<T>(x, render));
RegisteredTags[tagName] = parser.AndSkip(TagEnd).Then<Statement>(x => new ParserTagStatement<T>(tagName, x, render));
RegisteredTags[tagName].Name = tagName;
}

public void RegisterEmptyTag(string tagName, Func<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.Then<Statement>(x => new EmptyTagStatement(render)).ElseError($"Unexpected arguments in {tagName} tag");
RegisteredTags[tagName] = TagEnd.Then<Statement>(x => new EmptyTagStatement(tagName, render)).ElseError($"Unexpected arguments in {tagName} tag");
RegisteredTags[tagName].Name = tagName;
}

public void RegisterEmptyBlock(string tagName, Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName).ElseError($"'{{% end{tagName} %}}' was expected"))
.Then<Statement>(x => new EmptyBlockStatement(x, render))
.Then<Statement>(x => new EmptyBlockStatement(tagName, x, render))
.ElseError($"Invalid '{tagName}' tag")
;
RegisteredTags[tagName].Name = tagName;
Expand Down
19 changes: 12 additions & 7 deletions Fluid/Parser/EmptyBlockStatement.cs
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> _render;

public EmptyBlockStatement(IReadOnlyList<Statement> statements, Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
public EmptyBlockStatement(string tagName, IReadOnlyList<Statement> statements, Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> 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<Statement> Statements { get; }

public Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> Render { get; }

public override ValueTask<Completion> 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);
}
}
11 changes: 8 additions & 3 deletions Fluid/Parser/EmptyTagStatement.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> _render;

public EmptyTagStatement(Func<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
public string TagName { get; }

public EmptyTagStatement(string tagName, Func<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
TagName = tagName ?? throw new ArgumentNullException(nameof(tagName));
_render = render ?? throw new ArgumentNullException(nameof(render));
}

public override ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context)
{
return _render(writer, encoder, context);
}

protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitEmptyTagStatement(this);
}
}
18 changes: 11 additions & 7 deletions Fluid/Parser/ParserBlockStatement.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
using Fluid.Ast;
using Fluid.Ast;
using System.Text.Encodings.Web;

namespace Fluid.Parser
{
internal sealed class ParserBlockStatement<T> : TagStatement
public sealed class ParserBlockStatement<T> : TagStatement
{
private readonly Func<T, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> _render;

public ParserBlockStatement(T value, IReadOnlyList<Statement> statements, Func<T, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render) : base(statements)
public ParserBlockStatement(string tagName, T value, IReadOnlyList<Statement> statements, Func<T, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> 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<T, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> Render { get; }

public string TagName { get; }

public T Value { get; }

public override ValueTask<Completion> 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);
}
}
19 changes: 12 additions & 7 deletions Fluid/Parser/ParserTagStatement.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
using Fluid.Ast;
using Fluid.Ast;
using System.Text.Encodings.Web;

namespace Fluid.Parser
{
internal sealed class ParserTagStatement<T> : Statement
public sealed class ParserTagStatement<T> : Statement
{
private readonly Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> _render;

public ParserTagStatement(T value, Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
public ParserTagStatement(string tagName, T value, Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> 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<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> Render { get; }

public string TagName { get; }

public T Value { get; }

public override ValueTask<Completion> 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);
}
}
Loading