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
13 changes: 13 additions & 0 deletions Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ public void ShouldAcceptDashesInIdentifiers(string source)
Assert.True(result);
}

[Theory]
[InlineData("{% assign 1f = 123 %}{{ 1f }}")]
[InlineData("{% assign 123f = 123 %}{{ 123f }}")]
[InlineData("{% assign 1_ = 123 %}{{ 1_ }}")]
[InlineData("{% assign 1-1 = 123 %}{{ 1-1 }}")]
public void ShouldAcceptDigitsAtStartOfIdentifiers(string source)
{
var result = _parser.TryParse(source, out var template, out var error);

Assert.True(result, error);
Assert.Equal("123", template.Render());
}

[Theory]
[InlineData(@"abc
{% {{ %}
Expand Down
23 changes: 4 additions & 19 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,7 @@ public class FluidParser
protected static readonly Parser<string> BinaryOr = Terms.Text("or");
protected static readonly Parser<string> BinaryAnd = Terms.Text("and");

protected static readonly Parser<string> Identifier = Terms.Identifier(extraPart: static c => c == '-').Then((ctx, x) =>
{
// Detect patterns like {{ size-}} to exclude the '-' from the identifier
// c.f. https://github.com/sebastienros/fluid/issues/347
if (x.Buffer[x.Offset + x.Length - 1] == '-' && (ctx.Scanner.Cursor.Current == '%' || ctx.Scanner.Cursor.Current == '}'))
{
// Trim the '-'
x = new TextSpan(x.Buffer, x.Offset, x.Length - 1);

// Reset the cursor to the '-'
var current = ctx.Scanner.Cursor.Position;
ctx.Scanner.Cursor.ResetPosition(new TextPosition(current.Offset - 1, current.Line, current.Column - 1));
}

return x.ToString();
});
protected static readonly Parser<string> Identifier = SkipWhiteSpace(new IdentifierParser()).Then(x => x.ToString());

protected readonly Parser<List<FilterArgument>> ArgumentsList;
protected readonly Parser<Expression> LogicalExpression;
Expand Down Expand Up @@ -111,13 +96,13 @@ public FluidParser()

// primary => NUMBER | STRING | BOOLEAN | property
Primary.Parser =
Number.Then<Expression>(x => new LiteralExpression(NumberValue.Create(x)))
.Or(String.Then<Expression>(x => new LiteralExpression(StringValue.Create(x))))
String.Then<Expression>(x => new LiteralExpression(StringValue.Create(x)))
.Or(True.Then<Expression>(x => new LiteralExpression(BooleanValue.True)))
.Or(False.Then<Expression>(x => new LiteralExpression(BooleanValue.False)))
.Or(Empty.Then<Expression>(x => new LiteralExpression(EmptyValue.Instance)))
.Or(Blank.Then<Expression>(x => new LiteralExpression(BlankValue.Instance)))
.Or(Member.Then<Expression>(x => x))
.Or(Number.Then<Expression>(x => new LiteralExpression(NumberValue.Create(x))))
;

RegisteredOperators["contains"] = (a, b) => new ContainsBinaryExpression(a, b);
Expand Down Expand Up @@ -282,7 +267,7 @@ public FluidParser()
;

var RawTag = TagEnd.SkipAnd(AnyCharBefore(CreateTag("endraw"), consumeDelimiter: true, failOnEof: true).Then<Statement>(x => new RawStatement(x))).ElseError("Not end tag found for {% raw %}");
var AssignTag = Identifier.ElseError(IdentifierAfterAssign).AndSkip(Equal.ElseError(EqualAfterAssignIdentifier)).And(FilterExpression).AndSkip(TagEnd.ElseError(ExpectedTagEnd)).Then<Statement>(x => new AssignStatement(x.Item1, x.Item2));
var AssignTag = Identifier.Then(x => x).ElseError(IdentifierAfterAssign).AndSkip(Equal.ElseError(EqualAfterAssignIdentifier)).And(FilterExpression).AndSkip(TagEnd.ElseError(ExpectedTagEnd)).Then<Statement>(x => new AssignStatement(x.Item1, x.Item2));
var IfTag = LogicalExpression
.AndSkip(TagEnd)
.And(AnyTagsList)
Expand Down
96 changes: 96 additions & 0 deletions Fluid/Parser/IdentifierParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Parlot;
using Parlot.Fluent;

namespace Fluid.Parser
{
public sealed class IdentifierParser : Parser<TextSpan>
{
public override bool Parse(ParseContext context, ref ParseResult<TextSpan> result)
{
context.EnterParser(this);
var cursor = context.Scanner.Cursor;

var current = cursor.Current;

var nonDigits = 0;
var lastIsDash = false;

var start = cursor.Position;
var lastDashPosition = cursor.Position;

if (IsNonDigitStart(current))
{
nonDigits++;
}
else if (char.IsDigit(current))
{
}
else
{
// Doesn't start with a letter or a digit
return false;
}

// Read while it's an identifier part. and ensure we have at least a letter or it's a number

cursor.Advance();

while (!context.Scanner.Cursor.Eof)
{
current = cursor.Current;

if (IsNonDigitStart(current))
{
nonDigits++;
lastIsDash = false;
}
else if (current == '-')
{
lastDashPosition = cursor.Position;
nonDigits++;
lastIsDash = true;
}
else if (char.IsDigit(current))
{
lastIsDash = false;
}
else
{
break;
}

cursor.Advance();
}

var end = cursor.Offset;

// Exclude the trailing '-' if it is next to an end tag
// c.f. https://github.com/sebastienros/fluid/issues/347
current = cursor.Current;

if (lastIsDash && !cursor.Eof && (current == '%' || current == '}'))
{
nonDigits--;
end = end - 1;
cursor.ResetPosition(lastDashPosition);
}

if (nonDigits == 0)
{
// Invalid identifier, only digits
cursor.ResetPosition(start);
return false;
}

result.Set(start.Offset, end, new TextSpan(context.Scanner.Buffer, start.Offset, end - start.Offset));
return true;
}

private static bool IsNonDigitStart(char ch)
=>
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch == '_')
;
}
}