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
43 changes: 43 additions & 0 deletions Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -921,5 +921,48 @@ public async Task ShouldSupportCompactNotation(string source, string expected)
var result = await template.RenderAsync(context);
Assert.Equal(expected, result);
}

[Fact]
public void ShouldParseEchoTag()
{
var source = @"{% echo 'welcome to the liquid tag' | upcase %}";

Assert.True(_parser.TryParse(source, out var template, out var errors), errors);
var rendered = template.Render();
Assert.Contains("WELCOME TO THE LIQUID TAG", rendered);
}

[Fact]
public void ShouldParseLiquidTag()
{
var source = @"
{%
liquid
echo
'welcome ' | upcase
echo 'to the liquid tag'
| upcase
%}";

Assert.True(_parser.TryParse(source, out var template, out var errors), errors);
var rendered = template.Render();
Assert.Contains("WELCOME TO THE LIQUID TAG", rendered);
}

[Fact]
public void ShouldParseLiquidTagWithBlocks()
{
var source = @"
{% liquid assign cool = true
if cool
echo 'welcome to the liquid tag' | upcase
endif
%}
";

Assert.True(_parser.TryParse(source, out var template, out var errors), errors);
var rendered = template.Render();
Assert.Contains("WELCOME TO THE LIQUID TAG", rendered);
}
}
}
26 changes: 26 additions & 0 deletions Fluid/Ast/LiquidStatement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Fluid.Ast
{
public class LiquidStatement : TagStatement
{
public LiquidStatement(List<Statement> statements) : base(statements)
{
}

public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context)
{
context.IncrementSteps();

foreach (var statement in Statements)
{
await statement.WriteToAsync(writer, encoder, context);
}

return Completion.Normal;
}
}
}
2 changes: 1 addition & 1 deletion Fluid/Fluid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Parlot" Version="0.0.17" />
<PackageReference Include="Parlot" Version="0.0.18" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="1.1.1" />
<PackageReference Include="TimeZoneConverter" Version="3.5.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
Expand Down
31 changes: 28 additions & 3 deletions Fluid/FluidParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ public FluidParser()
.Then<Statement>(static x => new OutputStatement(x.Item1))
);


var Text = AnyCharBefore(OutputStart.Or(TagStart))
.Then<Statement>(static (ctx, x) =>
{
Expand Down Expand Up @@ -354,6 +353,30 @@ public FluidParser()
})
).ElseError("Invalid 'for' tag");

var LiquidTag = Literals.WhiteSpace(true) // {% liquid %} can start with new lines
.Then((context, x) => { ((FluidParseContext)context).InsideLiquidTag = true; return x;})
.SkipAnd(OneOrMany(Identifier.Switch((context, previous) =>
{
// Because tags like 'else' are not listed, they won't count in TagsList, and will stop being processed
// as inner tags in blocks like {% if %} TagsList {% endif $}

var tagName = previous;

if (RegisteredTags.TryGetValue(tagName, out var tag))
{
return tag;
}
else
{
throw new ParseException($"Unknown tag '{tagName}' at {context.Scanner.Cursor.Position}");
}
})))
.Then((context, x) => { ((FluidParseContext)context).InsideLiquidTag = false; return x; })
.AndSkip(TagEnd).Then<Statement>(x => new LiquidStatement(x))
;

var EchoTag = FilterExpression.AndSkip(TagEnd).Then<Statement>(x => new OutputStatement(x));

RegisteredTags["break"] = BreakTag;
RegisteredTags["continue"] = ContinueTag;
RegisteredTags["comment"] = CommentTag;
Expand All @@ -369,6 +392,8 @@ public FluidParser()
RegisteredTags["unless"] = UnlessTag;
RegisteredTags["case"] = CaseTag;
RegisteredTags["for"] = ForTag;
RegisteredTags["liquid"] = LiquidTag;
RegisteredTags["echo"] = EchoTag;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static (Expression limitResult, Expression offsetResult, bool reversed) ReadForStatementConfiguration(List<ForModifier> modifiers)
Expand Down Expand Up @@ -424,7 +449,7 @@ public FluidParser()
}
}));

var KnownTags = TagStart.SkipAnd(Identifier.ElseError(IdentifierAfterTagStart).Switch((context, previous) =>
var KnownTags = TagStart.SkipAnd(Identifier.ElseError(ErrorMessages.IdentifierAfterTagStart).Switch((context, previous) =>
{
// Because tags like 'else' are not listed, they won't count in TagsList, and will stop being processed
// as inner tags in blocks like {% if %} TagsList {% endif $}
Expand All @@ -442,7 +467,7 @@ public FluidParser()
}));

AnyTagsList.Parser = ZeroOrMany(Output.Or(AnyTags).Or(Text)); // Used in block and stop when an unknown tag is found
KnownTagsList.Parser = ZeroOrMany(Output.Or(KnownTags).Or(Text)); // User in main list and raises an issue when an unknown tag is found
KnownTagsList.Parser = ZeroOrMany(Output.Or(KnownTags).Or(Text)); // Used in main list and raises an issue when an unknown tag is found

Grammar = KnownTagsList;
}
Expand Down
1 change: 1 addition & 0 deletions Fluid/Parser/FluidParseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public FluidParseContext(string text) : base(new Scanner(text))
public bool StripNextTextSpanStatement { get; set; }
public bool PreviousIsTag { get; set; }
public bool PreviousIsOutput { get; set; }
public bool InsideLiquidTag { get; set; } // Used in the {% liquid %} tag to ensure a new line corresponds to '%}'
}
}
70 changes: 65 additions & 5 deletions Fluid/Parser/TagParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ public override bool Parse(ParseContext context, ref ParseResult<TagResult> resu

var start = context.Scanner.Cursor.Position;

var p = (FluidParseContext)context;

if (p.InsideLiquidTag)
{
result.Set(start.Offset, context.Scanner.Cursor.Offset, TagResult.TagOpen);
return true;
}

if (context.Scanner.ReadChar('{') && context.Scanner.ReadChar('%'))
{
var p = (FluidParseContext)context;

var trim = context.Scanner.ReadChar('-');

Expand Down Expand Up @@ -94,19 +101,72 @@ public TagEndParser(bool skipWhiteSpace = false)

public override bool Parse(ParseContext context, ref ParseResult<TagResult> result)
{
var p = (FluidParseContext)context;

var newLineIsPresent = false;

if (_skipWhiteSpace)
{
context.SkipWhiteSpace();
if (p.InsideLiquidTag)
{
var cursor = context.Scanner.Cursor;

while (Character.IsWhiteSpace(cursor.Current))
{
cursor.Advance();
}

if (Character.IsNewLine(cursor.Current))
{
newLineIsPresent = true;
while (Character.IsNewLine(cursor.Current))
{
cursor.Advance();
}
}
}
else
{
context.SkipWhiteSpace();
}
}

var start = context.Scanner.Cursor.Position;
bool trim;

bool trim = context.Scanner.ReadChar('-');
if (p.InsideLiquidTag)
{
if (newLineIsPresent)
{
result.Set(start.Offset, context.Scanner.Cursor.Offset, TagResult.TagClose);
return true;
}
else
{
trim = context.Scanner.ReadChar('-');

if (context.Scanner.ReadChar('%') && context.Scanner.ReadChar('}'))
{
p.StripNextTextSpanStatement = trim;
p.PreviousTextSpanStatement = null;
p.PreviousIsTag = true;
p.PreviousIsOutput = false;

context.Scanner.Cursor.ResetPosition(start);

result.Set(start.Offset, start.Offset, TagResult.TagClose);
return true;
}

context.Scanner.Cursor.ResetPosition(start);
return false;
}
}

trim = context.Scanner.ReadChar('-');

if (context.Scanner.ReadChar('%') && context.Scanner.ReadChar('}'))
{
var p = (FluidParseContext)context;

p.StripNextTextSpanStatement = trim;
p.PreviousTextSpanStatement = null;
p.PreviousIsTag = true;
Expand Down