diff --git a/Fluid.Tests/ParserTests.cs b/Fluid.Tests/ParserTests.cs index 24e5acdf..93373b87 100644 --- a/Fluid.Tests/ParserTests.cs +++ b/Fluid.Tests/ParserTests.cs @@ -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); + } } } diff --git a/Fluid/Ast/LiquidStatement.cs b/Fluid/Ast/LiquidStatement.cs new file mode 100644 index 00000000..dc2caf6c --- /dev/null +++ b/Fluid/Ast/LiquidStatement.cs @@ -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 statements) : base(statements) + { + } + + public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + { + context.IncrementSteps(); + + foreach (var statement in Statements) + { + await statement.WriteToAsync(writer, encoder, context); + } + + return Completion.Normal; + } + } +} diff --git a/Fluid/Fluid.csproj b/Fluid/Fluid.csproj index 9ae537b1..2027b28b 100644 --- a/Fluid/Fluid.csproj +++ b/Fluid/Fluid.csproj @@ -10,7 +10,7 @@ - + diff --git a/Fluid/FluidParser.cs b/Fluid/FluidParser.cs index 02224042..dfb8bc23 100644 --- a/Fluid/FluidParser.cs +++ b/Fluid/FluidParser.cs @@ -201,7 +201,6 @@ public FluidParser() .Then(static x => new OutputStatement(x.Item1)) ); - var Text = AnyCharBefore(OutputStart.Or(TagStart)) .Then(static (ctx, x) => { @@ -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(x => new LiquidStatement(x)) + ; + + var EchoTag = FilterExpression.AndSkip(TagEnd).Then(x => new OutputStatement(x)); + RegisteredTags["break"] = BreakTag; RegisteredTags["continue"] = ContinueTag; RegisteredTags["comment"] = CommentTag; @@ -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 modifiers) @@ -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 $} @@ -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; } diff --git a/Fluid/Parser/FluidParseContext.cs b/Fluid/Parser/FluidParseContext.cs index 71750ae6..e28e55ed 100644 --- a/Fluid/Parser/FluidParseContext.cs +++ b/Fluid/Parser/FluidParseContext.cs @@ -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 '%}' } } diff --git a/Fluid/Parser/TagParsers.cs b/Fluid/Parser/TagParsers.cs index 21c4cb24..5a79a09d 100644 --- a/Fluid/Parser/TagParsers.cs +++ b/Fluid/Parser/TagParsers.cs @@ -54,9 +54,16 @@ public override bool Parse(ParseContext context, ref ParseResult 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('-'); @@ -94,19 +101,72 @@ public TagEndParser(bool skipWhiteSpace = false) public override bool Parse(ParseContext context, ref ParseResult 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;