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
27 changes: 27 additions & 0 deletions Fluid.Tests/IncludeStatementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid.Ast;
using Fluid.Parser;
using Fluid.Tests.Mocks;
using Fluid.Values;
using Xunit;
Expand Down Expand Up @@ -251,6 +252,32 @@ public void RenderTag_With_Default_Name()
Assert.Equal("Product: Draft 151cm ", result);
}

[Fact]
public void Increment_Is_Isolated_Between_Renders()
{
var fileProvider = new MockFileProvider();
fileProvider.Add("incr.liquid", "{% increment %}");

var options = new TemplateOptions() { FileProvider = fileProvider, MemberAccessStrategy = UnsafeMemberAccessStrategy.Instance };
var context = new TemplateContext(options);
_parser.TryParse("{% increment %}{% increment %}{% render 'incr' %}", out var template, out var error);
Assert.Null(error);
var result = template.Render(context);

Assert.Equal("010", result);
}

[Fact]
public void RenderTagCantUseDynamicName()
{
var fileProvider = new MockFileProvider();
var options = new TemplateOptions() { FileProvider = fileProvider, MemberAccessStrategy = UnsafeMemberAccessStrategy.Instance };
var context = new TemplateContext(options);
var result = _parser.TryParse("{% assign name = 'snippet' %}{% render name %}", out var template, out var error);
Assert.False(result);
Assert.Contains(ErrorMessages.ExpectedStringRender, error);
}

[Fact]
public void IncludeTag_For_Loop()
{
Expand Down
9 changes: 9 additions & 0 deletions Fluid.Tests/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,15 @@ public Task IncrementDoesntAffectVariable(string source, string expected)
return CheckAsync(source, expected);
}

[Theory]
[InlineData("{% increment %}{% increment %}{% increment %}", "012")]
[InlineData("{% decrement %}{% decrement %}{% decrement %}", "0-1-2")]
[InlineData("{% increment %}{% decrement %}{% increment %}", "0-10")]
public Task IncrementCanBeUsedWithoutIdentifier(string source, string expected)
{
return CheckAsync(source, expected);
}

[Fact]
public async Task ModelIsUsedAsFallback()
{
Expand Down
69 changes: 44 additions & 25 deletions Fluid/Ast/IncludeStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;

namespace Fluid.Ast
Expand All @@ -12,8 +13,8 @@ public class IncludeStatement : Statement
{
public const string ViewExtension = ".liquid";
private readonly FluidParser _parser;
private IFluidTemplate _template;
private string _identifier;
private volatile CachedTemplate _cachedTemplate;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

public IncludeStatement(FluidParser parser, Expression path, Expression with = null, Expression @for = null, string alias = null, IList<AssignStatement> assignStatements = null)
{
Expand Down Expand Up @@ -42,31 +43,46 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
relativePath += ViewExtension;
}

if (_template == null || !string.Equals(_identifier, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.OrdinalIgnoreCase))
if (_cachedTemplate == null || !string.Equals(_cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{
var fileProvider = context.Options.FileProvider;
await _semaphore.WaitAsync();

var fileInfo = fileProvider.GetFileInfo(relativePath);

if (fileInfo == null || !fileInfo.Exists)
try
{
throw new FileNotFoundException(relativePath);
}
if (_cachedTemplate == null || !string.Equals(_cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{

var content = "";
var fileProvider = context.Options.FileProvider;

using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
{
content = await streamReader.ReadToEndAsync();
}
var fileInfo = fileProvider.GetFileInfo(relativePath);

if (!_parser.TryParse(content, out _template, out var errors))
if (fileInfo == null || !fileInfo.Exists)
{
throw new FileNotFoundException(relativePath);
}

var content = "";

using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
{
content = await streamReader.ReadToEndAsync();
}

if (!_parser.TryParse(content, out var template, out var errors))
{
throw new ParseException(errors);
}

var identifier = System.IO.Path.GetFileNameWithoutExtension(relativePath);

_cachedTemplate = new CachedTemplate(template, identifier);
}
}
finally
{
throw new ParseException(errors);
_semaphore.Release();
}

_identifier = System.IO.Path.GetFileNameWithoutExtension(relativePath);
}

try
Expand All @@ -77,9 +93,9 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
{
var with = await With.EvaluateAsync(context);

context.SetValue(Alias ?? _identifier, with);
context.SetValue(Alias ?? _cachedTemplate.Name, with);

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
else if (AssignStatements != null)
{
Expand All @@ -89,7 +105,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
await AssignStatements[i].WriteToAsync(writer, encoder, context);
}

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
else if (For != null)
{
Expand All @@ -109,7 +125,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text

var item = list[i];

context.SetValue(Alias ?? _identifier, item);
context.SetValue(Alias ?? _cachedTemplate.Name, item);

// Set helper variables
forloop.Index = i + 1;
Expand All @@ -119,7 +135,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
forloop.First = i == 0;
forloop.Last = i == length - 1;

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);

// Restore the forloop property after every statement in case it replaced it,
// for instance if it contains a nested for loop
Expand All @@ -134,7 +150,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
else
{
// no with, for or assignments, e.g. {% include 'products' %}
await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
}
finally
Expand All @@ -144,5 +160,8 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text

return Completion.Normal;
}

private record class CachedTemplate(IFluidTemplate Template, string Name);

}
}
2 changes: 1 addition & 1 deletion Fluid/Ast/IncrementStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class IncrementStatement : Statement
public const string Prefix = "$$incdec$$$";
public IncrementStatement(string identifier)
{
Identifier = identifier;
Identifier = identifier ?? "";
}

public string Identifier { get; }
Expand Down
74 changes: 46 additions & 28 deletions Fluid/Ast/RenderStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using System.Threading;

namespace Fluid.Ast
{
Expand All @@ -15,10 +16,10 @@ public class RenderStatement : Statement
{
public const string ViewExtension = ".liquid";
private readonly FluidParser _parser;
private IFluidTemplate _template;
private string _identifier;
private volatile CachedTemplate _cachedTemplate;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

public RenderStatement(FluidParser parser, Expression path, Expression with = null, Expression @for = null, string alias = null, IList<AssignStatement> assignStatements = null)
public RenderStatement(FluidParser parser, string path, Expression with = null, Expression @for = null, string alias = null, IList<AssignStatement> assignStatements = null)
{
_parser = parser;
Path = path;
Expand All @@ -28,7 +29,7 @@ public RenderStatement(FluidParser parser, Expression path, Expression with = nu
AssignStatements = assignStatements;
}

public Expression Path { get; }
public string Path { get; }
public IList<AssignStatement> AssignStatements { get; }
public Expression With { get; }
public Expression For { get; }
Expand All @@ -38,38 +39,52 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
{
context.IncrementSteps();

var relativePath = (await Path.EvaluateAsync(context)).ToStringValue();
var relativePath = Path;

if (!relativePath.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase))
{
relativePath += ViewExtension;
}

if (_template == null || !string.Equals(_identifier, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.OrdinalIgnoreCase))
if (_cachedTemplate == null || !string.Equals(_cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{
var fileProvider = context.Options.FileProvider;
await _semaphore.WaitAsync();

var fileInfo = fileProvider.GetFileInfo(relativePath);

if (fileInfo == null || !fileInfo.Exists)
try
{
throw new FileNotFoundException(relativePath);
}
if (_cachedTemplate == null || !string.Equals(_cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{
var fileProvider = context.Options.FileProvider;

var content = "";
var fileInfo = fileProvider.GetFileInfo(relativePath);

using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
{
content = await streamReader.ReadToEndAsync();
}
if (fileInfo == null || !fileInfo.Exists)
{
throw new FileNotFoundException(relativePath);
}

var content = "";

using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
{
content = await streamReader.ReadToEndAsync();
}

if (!_parser.TryParse(content, out var template, out var errors))
{
throw new ParseException(errors);
}

var identifier = System.IO.Path.GetFileNameWithoutExtension(relativePath);

if (!_parser.TryParse(content, out _template, out var errors))
_cachedTemplate = new CachedTemplate(template, identifier);
}
}
finally
{
throw new ParseException(errors);
_semaphore.Release();
}

_identifier = System.IO.Path.GetFileNameWithoutExtension(relativePath);
}

context.EnterChildScope();
Expand All @@ -84,8 +99,8 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

context.SetValue(Alias ?? _identifier, with);
await _template.RenderAsync(writer, encoder, context);
context.SetValue(Alias ?? _cachedTemplate.Name, with);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
else if (AssignStatements != null)
{
Expand All @@ -98,7 +113,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
else if (For != null)
{
Expand All @@ -121,7 +136,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text

var item = list[i];

context.SetValue(Alias ?? _identifier, item);
context.SetValue(Alias ?? _cachedTemplate.Name, item);

// Set helper variables
forloop.Index = i + 1;
Expand All @@ -131,7 +146,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
forloop.First = i == 0;
forloop.Last = i == length - 1;

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);

// Restore the forloop property after every statement in case it replaced it,
// for instance if it contains a nested for loop
Expand All @@ -148,7 +163,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

await _template.RenderAsync(writer, encoder, context);
await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
}
}
finally
Expand All @@ -159,5 +174,8 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text

return Completion.Normal;
}

private record class CachedTemplate (IFluidTemplate Template, string Name);

}
}
Loading