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
70 changes: 70 additions & 0 deletions Fluid.Tests/IncludeStatementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,5 +429,75 @@ public void IncludeTag_Caches_Template(bool useExtension)
// The previously cached template should be used
Assert.Equal("AAAA", result);
}

[Fact]
public void IncludeTag_Caches_ParsedTemplate()
{
var templates = "abcdefg".Select(x => new string(x, 10)).ToArray();

var fileProvider = new MockFileProvider();

foreach (var t in templates)
{
fileProvider.Add($"{t[0]}.liquid", t);
}

var fileInfos = templates.Select(x => fileProvider.GetFileInfo($"{x[0]}.liquid")).Cast<MockFileInfo>().ToArray();

var options = new TemplateOptions() { FileProvider = fileProvider, MemberAccessStrategy = UnsafeMemberAccessStrategy.Instance };
_parser.TryParse("{%- include file -%}", out var template);

// The first time a template is included it will be read from the file provider
foreach (var f in fileInfos)
{
var filename = f.Name;

Assert.False(f.Accessed);

var context = new TemplateContext(options);
context.SetValue("file", filename);
var result = template.Render(context);

Assert.True(f.Accessed);
}

foreach (var f in fileInfos)
{
f.Accessed = false;
}

// The next time a template is included it should not be accessed from the file provider but cached instead
foreach (var f in fileInfos)
{
var filename = f.Name;

Assert.False(f.Accessed);

var context = new TemplateContext(options);
context.SetValue("file", filename);
var result = template.Render(context);

Assert.False(f.Accessed);
}

foreach (var f in fileInfos)
{
f.LastModified = DateTime.UtcNow;
}

// If the attributes have changed then the template should be reloaded
foreach (var f in fileInfos)
{
var filename = f.Name;

Assert.False(f.Accessed);

var context = new TemplateContext(options);
context.SetValue("file", filename);
var result = template.Render(context);

Assert.True(f.Accessed);
}
}
}
}
15 changes: 9 additions & 6 deletions Fluid.Tests/Mocks/MockFileInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text;
using Microsoft.Extensions.FileProviders;
Expand All @@ -7,31 +7,34 @@ namespace Fluid.Tests.Mocks
{
public class MockFileInfo : IFileInfo
{
public static readonly MockFileInfo Null = new MockFileInfo("", "") { _exists = false };

private bool _exists = true;
public static readonly MockFileInfo Null = new MockFileInfo("", "") { Exists = false };

public MockFileInfo(string name, string content)
{
Name = name;
Content = content;
Exists = true;
}

public string Content { get; set; }
public bool Exists => _exists;

public bool Exists { get; set; }

public bool IsDirectory => false;

public DateTimeOffset LastModified => DateTimeOffset.MinValue;
public DateTimeOffset LastModified { get; set; } = DateTimeOffset.MinValue;

public long Length => -1;

public string Name { get; }

public string PhysicalPath => null;

public bool Accessed { get; set; }

public Stream CreateReadStream()
{
Accessed = true;
var data = Encoding.UTF8.GetBytes(Content);
return new MemoryStream(data);
}
Expand Down
44 changes: 18 additions & 26 deletions Fluid/Ast/IncludeStatement.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Fluid.Values;
using System.Diagnostics;
using System.Text.Encodings.Web;

namespace Fluid.Ast
Expand All @@ -10,11 +9,6 @@ public sealed class IncludeStatement : Statement
{
public const string ViewExtension = ".liquid";

// Since include statements will rarely vary the filename they render, we cache the most
// recent file only.

private volatile CachedTemplate _cachedTemplate;

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

var cachedTemplate = _cachedTemplate;
var fileProvider = context.Options.FileProvider;

if (cachedTemplate == null || !string.Equals(cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{
var fileProvider = context.Options.FileProvider;
// The file info is requested again to ensure the file hasn't changed and is still existing.

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

if (fileInfo == null || !fileInfo.Exists)
{
throw new FileNotFoundException(relativePath);
}
if (fileInfo == null || !fileInfo.Exists || fileInfo.IsDirectory)
{
throw new FileNotFoundException(relativePath);
}

if (context.Options.TemplateCache == null || !context.Options.TemplateCache.TryGetTemplate(fileInfo, out var template))
{
var content = "";

using (var stream = fileInfo.CreateReadStream())
Expand All @@ -64,17 +58,15 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
content = await streamReader.ReadToEndAsync();
}

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

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

_cachedTemplate = cachedTemplate = new CachedTemplate(template, identifier);
context.Options.TemplateCache?.SetTemplate(fileInfo, template);
}

Debug.Assert(cachedTemplate != null);
var identifier = System.IO.Path.GetFileNameWithoutExtension(relativePath);

context.EnterChildScope();

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

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

await cachedTemplate.Template.RenderAsync(writer, encoder, context);
await template.RenderAsync(writer, encoder, context);
}
else if (AssignStatements.Count > 0)
{
Expand All @@ -96,7 +88,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
await AssignStatements[i].WriteToAsync(writer, encoder, context);
}

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

var item = list[i];

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

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

await _cachedTemplate.Template.RenderAsync(writer, encoder, context);
await 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 @@ -141,7 +133,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
else
{
// no with, for or assignments, e.g. {% include 'products' %}
await cachedTemplate.Template.RenderAsync(writer, encoder, context);
await template.RenderAsync(writer, encoder, context);
}
}
finally
Expand Down
46 changes: 18 additions & 28 deletions Fluid/Ast/RenderStatement.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Fluid.Values;
using System.Diagnostics;
using System.Text.Encodings.Web;

namespace Fluid.Ast
Expand All @@ -13,11 +12,6 @@ public sealed class RenderStatement : Statement
{
public const string ViewExtension = ".liquid";

// Since include statements will rarely vary the filename they render, we cache the most
// recent file only.

private volatile CachedTemplate _cachedTemplate;

public RenderStatement(FluidParser parser, string path, Expression with = null, Expression @for = null, string alias = null, IReadOnlyList<AssignStatement> assignStatements = null)
{
Parser = parser;
Expand Down Expand Up @@ -46,19 +40,17 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
relativePath += ViewExtension;
}

var cachedTemplate = _cachedTemplate;

if (cachedTemplate == null || !string.Equals(cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal))
{
var fileProvider = context.Options.FileProvider;
var fileProvider = context.Options.FileProvider;

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

if (fileInfo == null || !fileInfo.Exists)
{
throw new FileNotFoundException(relativePath);
}
if (fileInfo == null || !fileInfo.Exists || fileInfo.IsDirectory)
{
throw new FileNotFoundException(relativePath);
}

if (context.Options.TemplateCache == null || !context.Options.TemplateCache.TryGetTemplate(fileInfo, out var template))
{
var content = "";

using (var stream = fileInfo.CreateReadStream())
Expand All @@ -67,21 +59,19 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
content = await streamReader.ReadToEndAsync();
}

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

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

_cachedTemplate = cachedTemplate = new CachedTemplate(template, identifier);
context.Options.TemplateCache?.SetTemplate(fileInfo, template);
}

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

context.EnterChildScope();
var previousScope = context.LocalScope;

Debug.Assert(cachedTemplate != null);

try
{
if (With != null)
Expand All @@ -91,8 +81,8 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

context.SetValue(Alias ?? cachedTemplate.Name, with);
await cachedTemplate.Template.RenderAsync(writer, encoder, context);
context.SetValue(Alias ?? identifier, with);
await template.RenderAsync(writer, encoder, context);
}
else if (AssignStatements.Count > 0)
{
Expand All @@ -105,7 +95,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

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

var item = list[i];

context.SetValue(Alias ?? cachedTemplate.Name, item);
context.SetValue(Alias ?? identifier, item);

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

await cachedTemplate.Template.RenderAsync(writer, encoder, context);
await 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 @@ -155,7 +145,7 @@ public override async ValueTask<Completion> WriteToAsync(TextWriter writer, Text
context.LocalScope = new Scope(context.RootScope);
previousScope.CopyTo(context.LocalScope);

await cachedTemplate.Template.RenderAsync(writer, encoder, context);
await template.RenderAsync(writer, encoder, context);
}
}
finally
Expand Down
Loading