Skip to content

Commit

Permalink
Expose ArgumentsList (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Jan 25, 2021
1 parent 1bd9334 commit 1925b6a
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 54 deletions.
17 changes: 16 additions & 1 deletion Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion Fluid/Ast/FilterExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public FilterExpression(Expression input, string name, List<FilterArgument> para
{
Input = input;
Name = name;
Parameters = parameters;
Parameters = parameters ?? new List<FilterArgument>();
}

public Expression Input { get; }
Expand Down
96 changes: 96 additions & 0 deletions Fluid/Ast/NamedExpressionList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Collections.Generic;

namespace Fluid.Ast
{
public class NamedExpressionList
{
public static readonly NamedExpressionList Empty = new NamedExpressionList();

private List<Expression> _positional;
private Dictionary<string, Expression> _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<Expression>(values);
}

public NamedExpressionList(List<FilterArgument> 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<string, Expression>();
}

_named.Add(name, value);
}

if (_positional == null)
{
_positional = new List<Expression>();
}

_positional.Add(value);

return this;
}

public IEnumerable<string> Names => _named?.Keys ?? System.Linq.Enumerable.Empty<string>();

public IEnumerable<Expression> Values => _positional;
}
}
61 changes: 35 additions & 26 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ public class FluidParser

protected static readonly Parser<string> Identifier = Terms.Identifier(extraPart: static c => c == '-').Then(x => x.ToString());

protected readonly Parser<List<FilterArgument>> ArgumentsList;
protected readonly Parser<Expression> LogicalExpression;
protected static readonly Deferred<Expression> Primary = Deferred<Expression>();
protected static readonly Parser<Expression> LogicalExpression;
protected static readonly Deferred<Expression> FilterExpression = Deferred<Expression>();
protected readonly Deferred<List<Statement>> KnownTagsList = Deferred<List<Statement>>();
protected readonly Deferred<List<Statement>> AnyTagsList = Deferred<List<Statement>>();
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -157,14 +158,12 @@ public FluidParser()
var identifier = pipeResult.Item1;
var arguments = pipeResult.Item2;

result = new FilterExpression(result, identifier, arguments ?? new List<FilterArgument>());
result = new FilterExpression(result, identifier, arguments);
}

return result;
});

//LogicalExpression.Parser = LogicalOperator;

var Output = OutputStart.SkipAnd(FilterExpression.And(OutputEnd)
.Then<Statement>(static x => new OutputStatement(x.Item1))
.ElseError("Invalid tag, expected an expression")
Expand Down Expand Up @@ -405,19 +404,9 @@ public FluidParser()

public static Parser<string> CreateTag(string tagName) => TagStart.SkipAnd(Terms.Text(tagName)).AndSkip(TagEnd);

public void RegisterEmptyTag(string tagName, Func<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.Then<Statement>(x => new EmptyTagStatement(render));
}

public void RegisterIdentifierTag(string tagName, Func<string, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = Identifier.AndSkip(TagEnd).Then<Statement>(x => new IdentifierTagStatement(x, render));
}

public void RegisterEmptyBlock(string tagName, Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName)).Then<Statement>(x => new EmptyBlockStatement(x, render));
RegisterParserTag(tagName, Identifier, render);
}

public void RegisterIdentifierBlock(string tagName, Func<string, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
Expand All @@ -427,12 +416,32 @@ public void RegisterIdentifierBlock(string tagName, Func<string, IReadOnlyList<S

public void RegisterExpressionBlock(string tagName, Func<Expression, IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisterParserBlock(tagName, Primary, render);
RegisterParserBlock(tagName, FilterExpression, render);
}

public void RegisterExpressionTag(string tagName, Func<Expression, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisterParserTag(tagName, FilterExpression, render);
}

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)).Then<Statement>(x => new ParserBlockStatement<T>(x.Item1, x.Item2, render));
}

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));
}

public void RegisterEmptyTag(string tagName, Func<TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.Then<Statement>(x => new EmptyTagStatement(render));
}

public void RegisterEmptyBlock(string tagName, Func<IReadOnlyList<Statement>, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
RegisteredTags[tagName] = TagEnd.SkipAnd(AnyTagsList).AndSkip(CreateTag("end" + tagName)).Then<Statement>(x => new EmptyBlockStatement(x, render));
}
}
}
26 changes: 0 additions & 26 deletions Fluid/Parser/IdentifierTagStatement.cs

This file was deleted.

26 changes: 26 additions & 0 deletions Fluid/Parser/ParserTagStatement.cs
Original file line number Diff line number Diff line change
@@ -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<T> : Statement
{
private readonly Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> _render;

public ParserTagStatement(T value, Func<T, TextWriter, TextEncoder, TemplateContext, ValueTask<Completion>> render)
{
Value = value;
_render = render ?? throw new ArgumentNullException(nameof(render));
}

public T Value { get; }

public override ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context)
{
return _render(Value, writer, encoder, context);
}
}
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -446,6 +447,14 @@ Hi! Hi! Hi!

<br>

### 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<Expression, Expression>` 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:
Expand Down

0 comments on commit 1925b6a

Please sign in to comment.