-
-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
261 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Text.Encodings.Web; | ||
using System.Threading.Tasks; | ||
using Fluid.Ast; | ||
using Fluid.Tests.Mocks; | ||
using Fluid.Values; | ||
using Xunit; | ||
|
||
namespace Fluid.Tests; | ||
|
||
public class FromStatementTests | ||
{ | ||
#if COMPILED | ||
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true }).Compile(); | ||
#else | ||
private static FluidParser _parser = new FluidParser(new FluidParserOptions { AllowFunctions = true }); | ||
#endif | ||
|
||
[Fact] | ||
public async Task FromStatement_ShouldThrowFileNotFoundException_IfTheFileProviderIsNotPresent() | ||
{ | ||
var expression = new LiteralExpression(new StringValue("_Macros.liquid")); | ||
var sw = new StringWriter(); | ||
|
||
try | ||
{ | ||
var fromStatement = new FromStatement(_parser, expression, new List<string> { "foo" }); | ||
await fromStatement.WriteToAsync(sw, HtmlEncoder.Default, new TemplateContext()); | ||
|
||
Assert.True(false); | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task FromStatement_ShouldOnlyImportListedMacrosToLocalScope() | ||
{ | ||
var expression = new LiteralExpression(new StringValue("_Macros.liquid")); | ||
var sw = new StringWriter(); | ||
|
||
var fileProvider = new MockFileProvider(); | ||
fileProvider.Add("_Macros.liquid", @" | ||
{% macro hello_world() %} | ||
Hello world! | ||
{% endmacro %} | ||
{% macro hello(first, last='Smith') %} | ||
Hello {{first | capitalize}} {{last}}! | ||
{% endmacro %} | ||
"); | ||
|
||
var options = new TemplateOptions { FileProvider = fileProvider }; | ||
var context = new TemplateContext(options); | ||
|
||
var fromStatement = new FromStatement(_parser, expression, new List<string>{"hello_world"}); | ||
await fromStatement.WriteToAsync(sw, HtmlEncoder.Default, context); | ||
|
||
Assert.IsType<FunctionValue>(context.GetValue("hello_world")); | ||
Assert.IsType<NilValue>(context.GetValue("hello")); | ||
} | ||
|
||
[Fact] | ||
public async Task FromStatement_ShouldNotRenderAnyOutput() | ||
{ | ||
var expression = new LiteralExpression(new StringValue("_Macros.liquid")); | ||
var sw = new StringWriter(); | ||
|
||
var fileProvider = new MockFileProvider(); | ||
fileProvider.Add("_Macros.liquid", @" | ||
{% macro hello_world() %} | ||
Hello world! | ||
{% endmacro %} | ||
{% macro hello(first, last='Smith') %} | ||
Hello {{first | capitalize}} {{last}}! | ||
{% endmacro %} | ||
{{ hello_world() }} | ||
"); | ||
|
||
var options = new TemplateOptions { FileProvider = fileProvider }; | ||
var context = new TemplateContext(options); | ||
|
||
var fromStatement = new FromStatement(_parser, expression, new List<string> { "hello_world" }); | ||
await fromStatement.WriteToAsync(sw, HtmlEncoder.Default, context); | ||
|
||
var result = sw.ToString(); | ||
Assert.Equal("", result); | ||
} | ||
|
||
[Fact] | ||
public async Task FromStatement_ShouldInvokeImportedMarcos() | ||
{ | ||
var expression = new LiteralExpression(new StringValue("_Macros.liquid")); | ||
var sw = new StringWriter(); | ||
|
||
var fileProvider = new MockFileProvider(); | ||
fileProvider.Add("_Macros.liquid", @" | ||
{%- macro hello_world() -%} | ||
Hello world! | ||
{%- endmacro -%} | ||
{%- macro hello(first, last='Doe') -%} | ||
Hello {{first | capitalize}} {{last}}! | ||
{%- endmacro -%} | ||
"); | ||
|
||
var source = @" | ||
{%- from '_Macros' import hello_world, hello -%} | ||
{{ hello_world() }} {{ hello('John') }}"; | ||
|
||
|
||
_parser.TryParse(source, out var template, out var error); | ||
|
||
var options = new TemplateOptions { FileProvider = fileProvider }; | ||
var context = new TemplateContext(options); | ||
|
||
var result = await template.RenderAsync(context); | ||
Assert.Equal("Hello world! Hello John Doe!", result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Text.Encodings.Web; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Fluid.Values; | ||
|
||
namespace Fluid.Ast | ||
{ | ||
#pragma warning disable CA1001 // Types that own disposable fields should be disposable | ||
public sealed class FromStatement : Statement | ||
#pragma warning restore CA1001 | ||
{ | ||
public const string ViewExtension = ".liquid"; | ||
|
||
private readonly FluidParser _parser; | ||
private volatile CachedTemplate _cachedTemplate; | ||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); | ||
|
||
public FromStatement(FluidParser parser, Expression path, List<string> functions = null) | ||
{ | ||
_parser = parser; | ||
Path = path; | ||
Functions = functions; | ||
} | ||
|
||
public Expression Path { get; } | ||
public List<String> Functions { get; } | ||
|
||
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) | ||
{ | ||
var relativePath = (await Path.EvaluateAsync(context)).ToStringValue(); | ||
if (!relativePath.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
relativePath += ViewExtension; | ||
} | ||
|
||
if (_cachedTemplate == null || !string.Equals(_cachedTemplate.Name, System.IO.Path.GetFileNameWithoutExtension(relativePath), StringComparison.Ordinal)) | ||
{ | ||
await _semaphore.WaitAsync(); | ||
try | ||
{ | ||
var fileProvider = context.Options.FileProvider; | ||
var fileInfo = fileProvider.GetFileInfo(relativePath); | ||
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 | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
|
||
try | ||
{ | ||
var parentScope = context.LocalScope; | ||
|
||
// Create a dedicated scope so we can list all macros defined in this template | ||
context.EnterChildScope(); | ||
await _cachedTemplate.Template.RenderAsync(TextWriter.Null, encoder, context); | ||
|
||
if (Functions != null) | ||
{ | ||
foreach (var functionName in Functions) | ||
{ | ||
var value = context.LocalScope.GetValue(functionName); | ||
if (value is FunctionValue) | ||
{ | ||
parentScope.SetValue(functionName, value); | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
foreach (var property in context.LocalScope.Properties) | ||
{ | ||
var value = context.LocalScope.GetValue(property); | ||
if (value is FunctionValue) | ||
{ | ||
parentScope.SetValue(property, value); | ||
} | ||
} | ||
} | ||
} | ||
finally | ||
{ | ||
context.ReleaseScope(); | ||
} | ||
|
||
|
||
return Completion.Normal; | ||
} | ||
|
||
private sealed record CachedTemplate(IFluidTemplate Template, string Name); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters