forked from sebastienros/fluid
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement functions (sebastienros#434)
- Loading branch information
1 parent
91aa570
commit 7bd01cc
Showing
14 changed files
with
645 additions
and
14 deletions.
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,120 @@ | ||
using Fluid.Values; | ||
using System; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
namespace Fluid.Tests | ||
{ | ||
public class FunctionTests | ||
{ | ||
#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 FunctionCallsShouldDefaultToNil() | ||
{ | ||
var source = @" | ||
{{- a() -}} | ||
"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
var context = new TemplateContext(); | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("", result); | ||
} | ||
|
||
[Fact] | ||
public async Task FunctionCallsAreInvoked() | ||
{ | ||
var source = @"{{ a() | append: b()}}"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
Assert.True(template != null, error); | ||
var context = new TemplateContext(); | ||
context.SetValue("a", new FunctionValue((args, c) => new StringValue("hello"))); | ||
context.SetValue("b", new FunctionValue((args, c) => new ValueTask<FluidValue>(new StringValue("world")))); | ||
|
||
// Use a loop to exercise the arguments cache | ||
for (var i = 0; i < 10; i++) | ||
{ | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("helloworld", result); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task FunctionCallsUseArguments() | ||
{ | ||
var source = @"{{ a('x', 2) }}"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
Assert.True(template != null, error); | ||
var context = new TemplateContext(); | ||
context.SetValue("a", new FunctionValue((args, c) => new StringValue(new String(args.At(0).ToStringValue()[0], (int) args.At(1).ToNumberValue())))); | ||
|
||
// Use a loop to exercise the arguments cache | ||
for (var i = 0; i < 10; i++) | ||
{ | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("xx", result); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task FunctionCallsUseNamedArguments() | ||
{ | ||
var source = @"{{ a(c = 'x', r = 2) }}"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
Assert.True(template != null, error); | ||
var context = new TemplateContext(); | ||
context.SetValue("a", new FunctionValue((args, c) => new StringValue(new String(args["c"].ToStringValue()[0], (int)args["r"].ToNumberValue())))); | ||
|
||
// Use a loop to exercise the arguments cache | ||
for (var i = 0; i < 10; i++) | ||
{ | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("xx", result); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task FunctionCallsRecursively() | ||
{ | ||
var source = @"{{ a(b(), r = 2) }}"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
Assert.True(template != null, error); | ||
var context = new TemplateContext(); | ||
context.SetValue("a", new FunctionValue((args, c) => new StringValue(new String(args.At(0).ToStringValue()[0], (int)args["r"].ToNumberValue())))); | ||
context.SetValue("b", new FunctionValue((args, c) => new StringValue("hello"))); | ||
|
||
// Use a loop to exercise the arguments cache | ||
for (var i = 0; i < 10; i++) | ||
{ | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("hh", result); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task ShouldDefineMacro() | ||
{ | ||
var source = @" | ||
{%- macro hello(first, last='Smith') -%} | ||
Hello {{first | capitalize }} {{last}} | ||
{%- endmacro -%} | ||
{{- hello('mike') }} {{ hello(last= 'ros', first='sebastien') -}} | ||
"; | ||
|
||
_parser.TryParse(source, out var template, out var error); | ||
var context = new TemplateContext(); | ||
var result = await template.RenderAsync(context); | ||
Assert.Equal("Hello Mike Smith Hello Sebastien ros", 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace Fluid.Ast | ||
{ | ||
public readonly struct FunctionCallArgument | ||
{ | ||
public FunctionCallArgument(string name, Expression expression) | ||
{ | ||
Name = name; | ||
Expression = expression; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the name of the argument, or <c>null</c> if not defined. | ||
/// </summary> | ||
public string Name { get; } | ||
|
||
public Expression Expression { get; } | ||
} | ||
} |
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,60 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Fluid.Values; | ||
|
||
namespace Fluid.Ast | ||
{ | ||
public class FunctionCallSegment : MemberSegment | ||
{ | ||
private static readonly FunctionArguments NonCacheableArguments = new(); | ||
private volatile FunctionArguments _cachedArguments = null; | ||
|
||
public FunctionCallSegment(IReadOnlyList<FunctionCallArgument> arguments) | ||
{ | ||
Arguments = arguments; | ||
} | ||
|
||
public IReadOnlyList<FunctionCallArgument> Arguments { get; } | ||
|
||
public override async ValueTask<FluidValue> ResolveAsync(FluidValue value, TemplateContext context) | ||
{ | ||
var arguments = _cachedArguments; | ||
|
||
// Do we need to evaluate arguments? | ||
|
||
if (arguments == null || arguments == NonCacheableArguments) | ||
{ | ||
if (Arguments.Count == 0) | ||
{ | ||
arguments = FunctionArguments.Empty; | ||
_cachedArguments = arguments; | ||
} | ||
else | ||
{ | ||
var newArguments = new FunctionArguments(); | ||
|
||
foreach (var argument in Arguments) | ||
{ | ||
newArguments.Add(argument.Name, await argument.Expression.EvaluateAsync(context)); | ||
} | ||
|
||
// The arguments can be cached if all the parameters are LiteralExpression | ||
|
||
if (arguments == null && Arguments.All(x => x.Expression is LiteralExpression)) | ||
{ | ||
_cachedArguments = newArguments; | ||
} | ||
else | ||
{ | ||
_cachedArguments = NonCacheableArguments; | ||
} | ||
|
||
arguments = newArguments; | ||
} | ||
} | ||
|
||
return await value.InvokeAsync(arguments, context); | ||
} | ||
} | ||
} |
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,96 @@ | ||
using Fluid.Utils; | ||
using Fluid.Values; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Text.Encodings.Web; | ||
using System.Threading.Tasks; | ||
|
||
namespace Fluid.Ast | ||
{ | ||
public class MacroStatement : TagStatement | ||
{ | ||
public MacroStatement(string identifier, IReadOnlyList<FunctionCallArgument> arguments, List<Statement> statements): base(statements) | ||
{ | ||
Identifier = identifier; | ||
Arguments = arguments; | ||
} | ||
|
||
public string Identifier { get; } | ||
public IReadOnlyList<FunctionCallArgument> Arguments { get; } | ||
|
||
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) | ||
{ | ||
// Evaluate all default values only once | ||
var defaultValues = new Dictionary<string, FluidValue>(); | ||
|
||
for (var i = 0; i < Arguments.Count; i++) | ||
{ | ||
var argument = Arguments[i]; | ||
defaultValues[argument.Name] = argument.Expression == null ? NilValue.Instance : await argument.Expression.EvaluateAsync(context); | ||
} | ||
|
||
var f = new FunctionValue(async (args, c) => | ||
{ | ||
using var sb = StringBuilderPool.GetInstance(); | ||
using var sw = new StringWriter(sb.Builder); | ||
|
||
context.EnterChildScope(); | ||
|
||
try | ||
{ | ||
// Initialize the local context with the default values | ||
foreach (var a in defaultValues) | ||
{ | ||
context.SetValue(a.Key, a.Value); | ||
} | ||
|
||
var namedArguments = false; | ||
|
||
// Apply all arguments from the invocation. | ||
// As soon as a named argument is used, all subsequent ones need a name too. | ||
|
||
for (var i = 0; i < args.Count; i++) | ||
{ | ||
var positionalName = Arguments[i].Name; | ||
|
||
namedArguments |= args.HasNamed(positionalName); | ||
|
||
if (!namedArguments) | ||
{ | ||
context.SetValue(positionalName, args.At(i)); | ||
} | ||
else | ||
{ | ||
context.SetValue(positionalName, args[positionalName]); | ||
} | ||
} | ||
|
||
for (var i = 0; i < _statements.Count; i++) | ||
{ | ||
var completion = await _statements[i].WriteToAsync(sw, encoder, context); | ||
|
||
if (completion != Completion.Normal) | ||
{ | ||
// Stop processing the block statements | ||
// We return the completion to flow it to the outer loop | ||
break; | ||
} | ||
} | ||
|
||
var result = sw.ToString(); | ||
|
||
// Don't encode the result | ||
return new StringValue(result, false); | ||
} | ||
finally | ||
{ | ||
context.ReleaseScope(); | ||
} | ||
}); | ||
|
||
context.SetValue(Identifier, f); | ||
|
||
return Completion.Normal; | ||
} | ||
} | ||
} |
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
Oops, something went wrong.