diff --git a/README.md b/README.md
index 594cd2c1..92bb9642 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,38 @@ The animal, Chewy, is not a dog.
*/
```
+### Registering Decorators
+
+```c#
+[Fact]
+public void BasicDecorator(IHandlebars handlebars)
+{
+ string source = "{{#block @value-from-decorator}}{{*decorator 42}}{{@value}}{{/block}}";
+
+ var handlebars = Handlebars.Create();
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+}
+```
+For more examples see [DecoratorTests.cs](https://github.com/Handlebars-Net/Handlebars.Net/tree/master/source/Handlebars.Test/DecoratorTests.cs)
+
+#### Known limitations:
+- helpers registered inside of a decorator will not override existing registrations
+
### Register custom value formatter
In case you need to apply custom value formatting (e.g. `DateTime`) you can use `IFormatter` and `IFormatterProvider` interfaces:
@@ -262,7 +294,7 @@ Will not encode:\
` (backtick)\
' (single quote)
-Will encode non-ascii characters `â`, `ß`, ...\
+Will encode non-ascii characters `�`, `�`, ...\
Into HTML entities (`<`, `â`, `ß`, ...).
##### Areas
@@ -277,12 +309,12 @@ public void UseCanonicalHtmlEncodingRules()
handlebars.Configuration.TextEncoder = new HtmlEncoder();
var source = "{{Text}}";
- var value = new { Text = "< â" };
+ var value = new { Text = "< �" };
var template = handlebars.Compile(source);
var actual = template(value);
- Assert.Equal("< â", actual);
+ Assert.Equal("< �", actual);
}
```
@@ -301,8 +333,6 @@ Nearly all time spent in rendering is in the routine that resolves values agains
- Rendering starts to get slower (into the tens of milliseconds or more) on dynamic objects.
- The slowest (up to hundreds of milliseconds or worse) tend to be objects with custom type implementations (such as `ICustomTypeDescriptor`) that are not optimized for heavy reflection.
-~~A frequent performance issue that comes up is JSON.NET's `JObject`, which for reasons we haven't fully researched, has very slow reflection characteristics when used as a model in Handlebars.Net. A simple fix is to just use JSON.NET's built-in ability to deserialize a JSON string to an `ExpandoObject` instead of a `JObject`. This will yield nearly an order of magnitude improvement in render times on average.~~
-
## Future roadmap
TBD
diff --git a/source/Directory.Build.props b/source/Directory.Build.props
index 261a4872..6fd7da97 100644
--- a/source/Directory.Build.props
+++ b/source/Directory.Build.props
@@ -12,7 +12,7 @@
false
true
snupkg
- 8
+ 9
diff --git a/source/Handlebars.Benchmark/EndToEnd.cs b/source/Handlebars.Benchmark/EndToEnd.cs
index b12de82b..2c65981c 100644
--- a/source/Handlebars.Benchmark/EndToEnd.cs
+++ b/source/Handlebars.Benchmark/EndToEnd.cs
@@ -3,6 +3,8 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using HandlebarsDotNet;
+using HandlebarsDotNet.Helpers;
+using HandlebarsDotNet.PathStructure;
namespace HandlebarsNet.Benchmark
{
@@ -66,9 +68,9 @@ public void Setup()
var handlebars = Handlebars.Create();
using(handlebars.Configure())
{
- handlebars.RegisterHelper("pow1", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow2", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow5", (output, options, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
+ handlebars.RegisterHelper(new PowHelper("pow1"));
+ handlebars.RegisterHelper(new PowHelper("pow2"));
+ handlebars.RegisterHelper(new BlockPowHelper("pow5"));
}
using (var reader = new StringReader(template))
@@ -78,10 +80,10 @@ public void Setup()
using(handlebars.Configure())
{
- handlebars.RegisterHelper("pow3", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow4", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
+ handlebars.RegisterHelper(new PowHelper("pow3"));
+ handlebars.RegisterHelper(new PowHelper("pow4"));
}
-
+
List ObjectLevel1Generator()
{
var level = new List();
@@ -171,6 +173,40 @@ List> DictionaryLevel3Generator(int id1, int id2)
}
}
+ private class PowHelper : IHelperDescriptor
+ {
+ public PowHelper(PathInfo name) => Name = name;
+
+ public PathInfo Name { get; }
+
+ public object Invoke(in HelperOptions options, in Context context, in Arguments arguments)
+ {
+ return ((int)arguments[0] * (int)arguments[0]).ToString();
+ }
+
+ public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments)
+ {
+ output.WriteSafeString(((int)arguments[0] * (int)arguments[0]).ToString());
+ }
+ }
+
+ private class BlockPowHelper : IHelperDescriptor
+ {
+ public BlockPowHelper(PathInfo name) => Name = name;
+
+ public PathInfo Name { get; }
+
+ public object Invoke(in BlockHelperOptions options, in Context context, in Arguments arguments)
+ {
+ return ((int)arguments[0] * (int)arguments[0]).ToString();
+ }
+
+ public void Invoke(in EncodedTextWriter output, in BlockHelperOptions options, in Context context, in Arguments arguments)
+ {
+ output.WriteSafeString(((int)arguments[0] * (int)arguments[0]).ToString());
+ }
+ }
+
[Benchmark]
public void Default() => _default(TextWriter.Null, _data);
}
diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs
index 621056cf..2e3f0842 100644
--- a/source/Handlebars.Test/BasicIntegrationTests.cs
+++ b/source/Handlebars.Test/BasicIntegrationTests.cs
@@ -12,30 +12,10 @@
using HandlebarsDotNet.Features;
using HandlebarsDotNet.IO;
using HandlebarsDotNet.PathStructure;
+using HandlebarsDotNet.ValueProviders;
namespace HandlebarsDotNet.Test
{
- public class HandlebarsEnvGenerator : IEnumerable
- {
- private readonly List _data = new List
- {
- Handlebars.Create(),
- Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)),
- Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types =>
- {
- types.Add(typeof(Dictionary));
- types.Add(typeof(Dictionary));
- types.Add(typeof(Dictionary));
- types.Add(typeof(Dictionary));
- })),
- Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.TextEncoder = new HtmlEncoder())),
- };
-
- public IEnumerator GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
- }
-
public class BasicIntegrationTests
{
private static string HtmlEncodeStringHelper(IHandlebars handlebars, string inputString)
diff --git a/source/Handlebars.Test/ClosureBuilderTests.cs b/source/Handlebars.Test/ClosureBuilderTests.cs
index 7055630e..d3c3858c 100644
--- a/source/Handlebars.Test/ClosureBuilderTests.cs
+++ b/source/Handlebars.Test/ClosureBuilderTests.cs
@@ -13,11 +13,12 @@ public class ClosureBuilderTests
[Fact]
public void GeneratesClosureWithOverflow()
{
- var builder = new ClosureBuilder();
+ using var builder = ClosureBuilder.Create();
var paths = GeneratePaths(builder, 6);
var helpers = GenerateHelpers(builder, 6);
var blockHelpers = GenerateBlockHelpers(builder, 6);
+ var decoratorDelegates = GenerateDecoratorDelegates(builder, 6);
var others = GenerateOther(builder, 6);
_ = builder.Build(out var closure);
@@ -34,6 +35,10 @@ public void GeneratesClosureWithOverflow()
Assert.Equal(blockHelpers[3], closure.BHD3);
Assert.Equal(blockHelpers[5], closure.BHDA[1]);
+ Assert.Equal(decoratorDelegates[0], closure.DDD0);
+ Assert.Equal(decoratorDelegates[3], closure.DDD3);
+ Assert.Equal(decoratorDelegates[5], closure.DDDA[1]);
+
Assert.Equal(others[0], closure.A[0]);
Assert.Equal(others[3], closure.A[3]);
Assert.Equal(others[5], closure.A[5]);
@@ -42,11 +47,12 @@ public void GeneratesClosureWithOverflow()
[Fact]
public void GeneratesClosureWithoutOverflow()
{
- var builder = new ClosureBuilder();
+ using var builder = ClosureBuilder.Create();
var paths = GeneratePaths(builder, 2);
var helpers = GenerateHelpers(builder, 2);
var blockHelpers = GenerateBlockHelpers(builder, 2);
+ var decorators = GenerateDecoratorDelegates(builder, 2);
var others = GenerateOther(builder, 2);
_ = builder.Build(out var closure);
@@ -63,6 +69,11 @@ public void GeneratesClosureWithoutOverflow()
Assert.Equal(blockHelpers[1], closure.BHD1);
Assert.Null(closure.BHDA);
+ Assert.Equal(decorators[0], closure.DDD0);
+ Assert.Equal(decorators[1], closure.DDD1);
+ Assert.Null(closure.DDD2);
+ Assert.Null(closure.BHDA);
+
Assert.Equal(others[0], closure.A[0]);
Assert.Equal(others[1], closure.A[1]);
Assert.Equal(2, closure.A.Length);
@@ -106,6 +117,19 @@ private static List[>> GenerateHelpers(Closu
return helpers;
}
+
+ private static List] GenerateDecoratorDelegates(ClosureBuilder builder, int count)
+ {
+ var helpers = new List();
+ for (int i = 0; i < count; i++)
+ {
+ DecoratorDelegate helper = (in EncodedTextWriter writer, BindingContext context, TemplateDelegate function) => function;
+ builder.Add(Const(helper));
+ helpers.Add(helper);
+ }
+
+ return helpers;
+ }
private static List GeneratePaths(ClosureBuilder builder, int count)
{
diff --git a/source/Handlebars.Test/DecoratorTests.cs b/source/Handlebars.Test/DecoratorTests.cs
new file mode 100644
index 00000000..81f35713
--- /dev/null
+++ b/source/Handlebars.Test/DecoratorTests.cs
@@ -0,0 +1,441 @@
+using System.Collections.Generic;
+using HandlebarsDotNet.Compiler;
+using HandlebarsDotNet.ValueProviders;
+using Xunit;
+
+namespace HandlebarsDotNet.Test
+{
+ public class DecoratorTests
+ {
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{*decorator 42}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void OverrideFunctionWithDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block}}{{*decorator 4}}2{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ var value = arguments.At(0);
+ return (in EncodedTextWriter writer, BindingContext bindingContext) =>
+ {
+ writer.WriteSafeString(value);
+ function(writer, bindingContext);
+ };
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void RegisterMethodFromDecorator(IHandlebars handlebars)
+ {
+ string source = "{{*decorator 42}}{{#block}}{{method-from-decorator 1}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ var value = arguments.At(0);
+ options.RegisterHelper("method-from-decorator", (c, a) => value * a.At(0));
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicLateDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{*decorator 42}}{{@value}}{{/block}}";
+
+ var template = handlebars.Compile(source);
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+
+ return function;
+ });
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void DecoratorInCorrectContext(IHandlebars handlebars)
+ {
+ string source = "{{#with inner}}{{*decorator outer}}{{#block @value-from-decorator}}{{@value}}{{/block}}{{/with}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(new
+ {
+ outer = 42,
+ inner = 24
+ });
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicBlockDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator}}42{{/decorator}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Template(), out _);
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void InnerDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator}}{{*decorator1 42}}{{/decorator}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Data["value-from-inner-decorator"], out _);
+
+ return function;
+ });
+
+ handlebars.RegisterDecorator("decorator1",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-inner-decorator", arguments.At(0), out _);
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void InnerBlockDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator}}{{#*decorator1}}42{{/decorator1}}{{/decorator}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Data["value-from-inner-decorator"], out _);
+ });
+
+ handlebars.RegisterDecorator("decorator1",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-inner-decorator", options.Template(), out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicLateBlockDecorator(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator}}42{{/decorator}}{{@value}}{{/block}}";
+
+ var template = handlebars.Compile(source);
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Template(), out _);
+
+ return function;
+ });
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicBlockDecoratorWithBlockParams(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator as |value-from-decorator| }}42{{/decorator}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ var blockParamsValues = new BlockParamsValues(options.Frame, options.BlockVariables);
+ blockParamsValues[0] = options.Template();
+
+ return function;
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BasicBlockDecoratorWithParameter(IHandlebars handlebars)
+ {
+ string source = "{{#block @value-from-decorator}}{{#*decorator outer as |value-from-decorator| }}2{{/decorator}}{{@value}}{{/block}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate _, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ var blockParamsValues = new BlockParamsValues(options.Frame, options.BlockVariables);
+ blockParamsValues[0] = $"{arguments.At(0)}{options.Template()}";
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(new { outer = 4});
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BlockDecoratorInCorrectContext(IHandlebars handlebars)
+ {
+ string source = "{{#with inner}}{{#*decorator}}{{outer}}{{/decorator}}{{#block @value-from-decorator}}{{@value}}{{/block}}{{/with}}";
+
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Template(), out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(new
+ {
+ outer = 42,
+ inner = 24
+ });
+ Assert.Equal("42", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void DecoratorInIterator(IHandlebars handlebars)
+ {
+ var source = "{{#each enumerateMe}}{{*decorator 42}}{{@value-from-decorator}}-{{this}} {{/each}}";
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+ var data = new
+ {
+ enumerateMe = new Dictionary
+ {
+ { "foo", "hello" },
+ { "bar", "world" }
+ }
+ };
+ var result = template(data);
+ Assert.Equal("42-hello 42-world ", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void BlockDecoratorInIterator(IHandlebars handlebars)
+ {
+ var source = "{{#each enumerateMe}}{{#*decorator}}42{{/decorator}}{{@value-from-decorator}}-{{this}} {{/each}}";
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", options.Template(), out _);
+ });
+
+ var template = handlebars.Compile(source);
+ var data = new
+ {
+ enumerateMe = new Dictionary
+ {
+ { "foo", "hello" },
+ { "bar", "world" }
+ }
+ };
+ var result = template(data);
+ Assert.Equal("42-hello 42-world ", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void DecoratorInCondition(IHandlebars handlebars)
+ {
+ string source = "{{#if @value-from-decorator}}{{*decorator truthy}}{{@value-from-decorator}} is Truthy!{{/if}}";
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var data = new
+ {
+ truthy = 1
+ };
+
+ var result = template(data);
+ Assert.Equal("1 is Truthy!", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void DecoratorInDeferredBlockString(IHandlebars handlebars)
+ {
+ string source = "{{#person}}{{*decorator this.person}}{{@value-from-decorator}} is {{this}}{{/person}}";
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(new { person = "Bill" });
+ Assert.Equal("Bill is Bill", result);
+ }
+
+ [Theory, ClassData(typeof(HandlebarsEnvGenerator))]
+ public void DecoratorInDeferredBlockEnumerable(IHandlebars handlebars)
+ {
+ string source = "{{#people}}{{*decorator this.outer}}{{@value-from-decorator}}->{{this}} {{/people}}";
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var data = new
+ {
+ outer = 42,
+ people = new[] {
+ "Bill",
+ "Mary"
+ }
+ };
+
+ var result = template(data);
+ Assert.Equal("42->Bill 42->Mary ", result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Handlebars.Test/HandlebarsEnvGenerator.cs b/source/Handlebars.Test/HandlebarsEnvGenerator.cs
new file mode 100644
index 00000000..ffc29502
--- /dev/null
+++ b/source/Handlebars.Test/HandlebarsEnvGenerator.cs
@@ -0,0 +1,28 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using HandlebarsDotNet.Features;
+
+namespace HandlebarsDotNet.Test
+{
+ public class HandlebarsEnvGenerator : IEnumerable
+ {
+ private readonly List _data = new()
+ {
+ Handlebars.Create(),
+ Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)),
+ Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types =>
+ {
+ types.Add(typeof(Dictionary));
+ types.Add(typeof(Dictionary));
+ types.Add(typeof(Dictionary));
+ types.Add(typeof(Dictionary));
+ })),
+ Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.TextEncoder = new HtmlEncoder())),
+ };
+
+ public IEnumerator GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
\ No newline at end of file
diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs
index 47442345..17f6bfa2 100644
--- a/source/Handlebars.Test/IssueTests.cs
+++ b/source/Handlebars.Test/IssueTests.cs
@@ -411,6 +411,32 @@ public void SwitchCaseTest()
Assert.Equal("the value is not provided", c);
}
+ // issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/300
+ [Fact]
+ public void PartialLayoutAndInlineBlock()
+ {
+ string layout = "{{#>body}}{{fallback}}{{/body}}";
+ string page = @"{{#>layout}}{{#*inline ""body""}}{{truebody}}{{/inline}}{{/body}}{{/layout}}";
+
+ var handlebars = Handlebars.Create();
+ var template = handlebars.Compile(page);
+
+ var data = new
+ {
+ fallback = "aaa",
+ truebody = "Hello world"
+ };
+
+ using (var reader = new StringReader(layout))
+ {
+ var partialTemplate = handlebars.Compile(reader);
+ handlebars.RegisterTemplate("layout", partialTemplate);
+ }
+
+ var result = template(data);
+ Assert.Equal("Hello world", result);
+ }
+
private class SwitchHelper : IHelperDescriptor
{
public PathInfo Name { get; } = "switch";
diff --git a/source/Handlebars/BindingContext.cs b/source/Handlebars/BindingContext.cs
index a3c6ac4f..0bc4d784 100644
--- a/source/Handlebars/BindingContext.cs
+++ b/source/Handlebars/BindingContext.cs
@@ -3,6 +3,7 @@
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.Compiler;
using HandlebarsDotNet.EqualityComparers;
+using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.ObjectDescriptors;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;
@@ -10,7 +11,7 @@
namespace HandlebarsDotNet
{
- public sealed partial class BindingContext : IDisposable
+ public sealed partial class BindingContext : IDisposable, IHelpersRegistry
{
internal readonly EntryIndex[] WellKnownVariables = new EntryIndex[8];
@@ -19,6 +20,8 @@ public sealed partial class BindingContext : IDisposable
private BindingContext()
{
InlinePartialTemplates = new CascadeIndex, StringEqualityComparer>(new StringEqualityComparer(StringComparison.OrdinalIgnoreCase));
+ Helpers = new CascadeIndex, StringEqualityComparer>(new StringEqualityComparer());
+ BlockHelpers = new CascadeIndex, StringEqualityComparer>(new StringEqualityComparer());
Bag = new CascadeIndex(new StringEqualityComparer(StringComparison.OrdinalIgnoreCase));
ContextDataObject = new FixedSizeDictionary(16, 7, ChainSegment.EqualityComparer);
@@ -95,6 +98,9 @@ out WellKnownVariables[(int) WellKnownVariable.Parent]
//in the context
InlinePartialTemplates.Outer = ParentContext.InlinePartialTemplates;
+ Helpers.Outer = ParentContext.Helpers;
+ BlockHelpers.Outer = ParentContext.BlockHelpers;
+
if (!(Value is HashParameterDictionary dictionary) || ParentContext.Value == null || ReferenceEquals(Value, ParentContext.Value)) return;
// Populate value with parent context
@@ -104,6 +110,10 @@ out WellKnownVariables[(int) WellKnownVariable.Parent]
internal ICompiledHandlebarsConfiguration Configuration { get; private set; }
internal CascadeIndex, StringEqualityComparer> InlinePartialTemplates { get; }
+
+ internal CascadeIndex, StringEqualityComparer> Helpers { get; }
+
+ internal CascadeIndex, StringEqualityComparer> BlockHelpers { get; }
internal TemplateDelegate PartialBlockTemplate { get; private set; }
@@ -176,5 +186,9 @@ private static void PopulateHash(HashParameterDictionary hash, object from)
hash[segment] = value;
}
}
+
+ IIndexed> IHelpersRegistry.GetHelpers() => Helpers;
+
+ IIndexed> IHelpersRegistry.GetBlockHelpers() => BlockHelpers;
}
}
diff --git a/source/Handlebars/Collections/IIndexed.cs b/source/Handlebars/Collections/IIndexed.cs
index e478ef60..ac2edcbe 100644
--- a/source/Handlebars/Collections/IIndexed.cs
+++ b/source/Handlebars/Collections/IIndexed.cs
@@ -10,5 +10,6 @@ public interface IIndexed : IReadOnlyIndexed
{
void AddOrReplace(in TKey key, in TValue value);
new TValue this[in TKey key] { get; set; }
+ void Clear();
}
}
\ No newline at end of file
diff --git a/source/Handlebars/Collections/ObservableIndex.cs b/source/Handlebars/Collections/ObservableIndex.cs
index 912455e7..0e9e27f5 100644
--- a/source/Handlebars/Collections/ObservableIndex.cs
+++ b/source/Handlebars/Collections/ObservableIndex.cs
@@ -112,6 +112,16 @@ public TValue this[in TKey key]
set => AddOrReplace(key, value);
}
+ public void Clear()
+ {
+ using (_itemsLock.WriteLock())
+ {
+ _inner.Clear();
+ }
+
+ Publish(new DictionaryClearedObservableEvent());
+ }
+
public IEnumerator> GetEnumerator()
{
KeyValuePair[] array;
@@ -145,6 +155,10 @@ public void OnNext(ObservableEvent value)
case DictionaryAddedObservableEvent addedObservableEvent:
AddOrReplace(addedObservableEvent.Key, addedObservableEvent.Value);
break;
+ case DictionaryClearedObservableEvent:
+ Clear();
+ break;
+
default:
throw new ArgumentOutOfRangeException(nameof(value));
}
@@ -157,4 +171,9 @@ internal class DictionaryAddedObservableEvent : ObservableEvent Key = key;
}
+
+ internal class DictionaryClearedObservableEvent : ObservableEvent
+ {
+ public DictionaryClearedObservableEvent() : base(default) {}
+ }
}
\ No newline at end of file
diff --git a/source/Handlebars/Compiler/ClosureBuilder.cs b/source/Handlebars/Compiler/ClosureBuilder.cs
index 0bd74fc1..d215617b 100644
--- a/source/Handlebars/Compiler/ClosureBuilder.cs
+++ b/source/Handlebars/Compiler/ClosureBuilder.cs
@@ -3,20 +3,24 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;
namespace HandlebarsDotNet.Compiler
{
- public class ClosureBuilder
+ public partial class ClosureBuilder
{
- private readonly List> _pathInfos = new List>();
- private readonly List> _templateDelegates = new List>();
- private readonly List> _blockParams = new List>();
- private readonly List>>> _helpers = new List>>>();
- private readonly List>>> _blockHelpers = new List>>>();
- private readonly List> _other = new List>();
+ private readonly List> _pathInfos = new();
+ private readonly List> _templateDelegates = new();
+ private readonly List> _decoratorDelegates = new();
+ private readonly List> _blockParams = new();
+ private readonly List>>> _helpers = new();
+ private readonly List>>> _blockHelpers = new();
+ private readonly List>>> _decorators = new();
+ private readonly List>>> _blockDecorators = new();
+ private readonly List> _other = new();
public void Add(ConstantExpression constantExpression)
{
@@ -32,10 +36,22 @@ public void Add(ConstantExpression constantExpression)
{
_blockHelpers.Add(new KeyValuePair>>(constantExpression, (Ref>) constantExpression.Value));
}
+ else if (constantExpression.Type == typeof(Ref>))
+ {
+ _decorators.Add(new KeyValuePair>>(constantExpression, (Ref>) constantExpression.Value));
+ }
+ else if (constantExpression.Type == typeof(Ref>))
+ {
+ _blockDecorators.Add(new KeyValuePair>>(constantExpression, (Ref>) constantExpression.Value));
+ }
else if (constantExpression.Type == typeof(TemplateDelegate))
{
_templateDelegates.Add(new KeyValuePair(constantExpression, (TemplateDelegate) constantExpression.Value));
}
+ else if (constantExpression.Type == typeof(DecoratorDelegate))
+ {
+ _decoratorDelegates.Add(new KeyValuePair(constantExpression, (DecoratorDelegate) constantExpression.Value));
+ }
else if (constantExpression.Type == typeof(ChainSegment[]))
{
_blockParams.Add(new KeyValuePair(constantExpression, (ChainSegment[]) constantExpression.Value));
@@ -60,6 +76,9 @@ public KeyValuePair> Bui
BuildKnownValues(arguments, _blockHelpers, 4);
BuildKnownValues(arguments, _templateDelegates, 4);
BuildKnownValues(arguments, _blockParams, 1);
+ BuildKnownValues(arguments, _decorators, 4);
+ BuildKnownValues(arguments, _blockDecorators, 4);
+ BuildKnownValues(arguments, _decoratorDelegates, 4);
arguments.Add(_other.Select(o => o.Value).ToArray());
closure = (Closure) constructor.Invoke(arguments.ToArray());
@@ -73,6 +92,9 @@ public KeyValuePair> Bui
BuildKnownValuesExpressions(closureExpression, mapping, _blockHelpers, "BHD", 4);
BuildKnownValuesExpressions(closureExpression, mapping, _templateDelegates, "TD", 4);
BuildKnownValuesExpressions(closureExpression, mapping, _blockParams, "BP", 1);
+ BuildKnownValuesExpressions(closureExpression, mapping, _decorators, "DD", 4);
+ BuildKnownValuesExpressions(closureExpression, mapping, _blockDecorators, "BDD", 4);
+ BuildKnownValuesExpressions(closureExpression, mapping, _decoratorDelegates, "DDD", 4);
var arrayField = closureType.GetField("A");
var array = Expression.Field(closureExpression, arrayField!);
@@ -137,18 +159,36 @@ public sealed class Closure
public readonly Ref> BHD3;
public readonly Ref>[] BHDA;
+ public readonly Ref> DD0;
+ public readonly Ref> DD1;
+ public readonly Ref> DD2;
+ public readonly Ref> DD3;
+ public readonly Ref>[] DDA;
+
+ public readonly Ref> BDD0;
+ public readonly Ref> BDD1;
+ public readonly Ref> BDD2;
+ public readonly Ref> BDD3;
+ public readonly Ref>[] BDDA;
+
public readonly TemplateDelegate TD0;
public readonly TemplateDelegate TD1;
public readonly TemplateDelegate TD2;
public readonly TemplateDelegate TD3;
public readonly TemplateDelegate[] TDA;
+ public readonly DecoratorDelegate DDD0;
+ public readonly DecoratorDelegate DDD1;
+ public readonly DecoratorDelegate DDD2;
+ public readonly DecoratorDelegate DDD3;
+ public readonly DecoratorDelegate[] DDDA;
+
public readonly ChainSegment[] BP0;
public readonly ChainSegment[][] BPA;
public readonly object[] A;
- internal Closure(PathInfo pi0, PathInfo pi1, PathInfo pi2, PathInfo pi3, PathInfo[] pia, Ref> hd0, Ref> hd1, Ref> hd2, Ref> hd3, Ref>[] hda, Ref> bhd0, Ref> bhd1, Ref> bhd2, Ref> bhd3, Ref>[] bhda, TemplateDelegate td0, TemplateDelegate td1, TemplateDelegate td2, TemplateDelegate td3, TemplateDelegate[] tda, ChainSegment[] bp0, ChainSegment[][] bpa, object[] a)
+ internal Closure(PathInfo pi0, PathInfo pi1, PathInfo pi2, PathInfo pi3, PathInfo[] pia, Ref> hd0, Ref> hd1, Ref> hd2, Ref> hd3, Ref>[] hda, Ref> bhd0, Ref> bhd1, Ref> bhd2, Ref> bhd3, Ref>[] bhda, TemplateDelegate td0, TemplateDelegate td1, TemplateDelegate td2, TemplateDelegate td3, TemplateDelegate[] tda, ChainSegment[] bp0, ChainSegment[][] bpa, Ref> dd0, Ref> dd1, Ref> dd2, Ref> dd3, Ref>[] dda, Ref> bdd0, Ref> bdd1, Ref> bdd2, Ref> bdd3, Ref>[] bdda, DecoratorDelegate ddd0, DecoratorDelegate ddd1, DecoratorDelegate ddd2, DecoratorDelegate ddd3, DecoratorDelegate[] ddda, object[] a)
{
PI0 = pi0;
PI1 = pi1;
@@ -172,6 +212,21 @@ internal Closure(PathInfo pi0, PathInfo pi1, PathInfo pi2, PathInfo pi3, PathInf
TDA = tda;
BP0 = bp0;
BPA = bpa;
+ DD0 = dd0;
+ DD1 = dd1;
+ DD2 = dd2;
+ DD3 = dd3;
+ DDA = dda;
+ BDD0 = bdd0;
+ BDD1 = bdd1;
+ BDD2 = bdd2;
+ BDD3 = bdd3;
+ BDDA = bdda;
+ DDD0 = ddd0;
+ DDD1 = ddd1;
+ DDD2 = ddd2;
+ DDD3 = ddd3;
+ DDDA = ddda;
A = a;
}
}
diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs
index c8692c0d..993c8457 100644
--- a/source/Handlebars/Compiler/FunctionBuilder.cs
+++ b/source/Handlebars/Compiler/FunctionBuilder.cs
@@ -3,24 +3,28 @@
using System.Linq;
using System.Linq.Expressions;
using Expressions.Shortcuts;
+using HandlebarsDotNet.Polyfills;
using static Expressions.Shortcuts.ExpressionShortcuts;
namespace HandlebarsDotNet.Compiler
{
internal static class FunctionBuilder
{
- private static readonly TemplateDelegate EmptyLambda =
+ private static readonly TemplateDelegate EmptyTemplateLambda =
(in EncodedTextWriter writer, BindingContext context) => { };
- public static Expression Reduce(Expression expression, CompilationContext context)
+ public static Expression Reduce(Expression expression, CompilationContext context, out IReadOnlyList decorators)
{
+ var _decorators = new List();
+ decorators = _decorators;
+
expression = new CommentVisitor().Visit(expression);
expression = new UnencodedStatementVisitor(context).Visit(expression);
expression = new PartialBinder(context).Visit(expression);
expression = new StaticReplacer(context).Visit(expression);
expression = new IteratorBinder(context).Visit(expression);
- expression = new BlockHelperFunctionBinder(context).Visit(expression);
- expression = new HelperFunctionBinder(context).Visit(expression);
+ expression = new BlockHelperFunctionBinder(context, _decorators).Visit(expression);
+ expression = new HelperFunctionBinder(context, _decorators).Visit(expression);
expression = new BoolishConverter(context).Visit(expression);
expression = new PathBinder(context).Visit(expression);
expression = new SubExpressionVisitor(context).Visit(expression);
@@ -29,22 +33,19 @@ public static Expression Reduce(Expression expression, CompilationContext contex
return expression;
}
- public static ExpressionContainer CreateExpression(IEnumerable expressions, CompilationContext compilationContext)
+ public static ExpressionContainer CreateExpression(IEnumerable expressions, CompilationContext compilationContext, out IReadOnlyList decorators)
{
try
{
+ decorators = ArrayEx.Empty();
var enumerable = expressions as Expression[] ?? expressions.ToArray();
- if (!enumerable.Any())
+ if (!enumerable.Any() || enumerable.IsOneOf())
{
- return Arg(EmptyLambda);
- }
- if (enumerable.IsOneOf())
- {
- return Arg(EmptyLambda);
+ return Arg(EmptyTemplateLambda);
}
var expression = (Expression) Expression.Block(enumerable);
- expression = Reduce(expression, compilationContext);
+ expression = Reduce(expression, compilationContext, out decorators);
return Arg(ContextBinder.Bind(compilationContext, expression));
}
@@ -54,11 +55,11 @@ public static ExpressionContainer CreateExpression(IEnumerable
}
}
- public static TemplateDelegate Compile(IEnumerable expressions, CompilationContext compilationContext)
+ public static TemplateDelegate Compile(IEnumerable expressions, CompilationContext compilationContext, out IReadOnlyList decorators)
{
try
{
- var expression = CreateExpression(expressions, compilationContext);
+ var expression = CreateExpression(expressions, compilationContext, out decorators);
if (expression.Expression is ConstantExpression constantExpression)
{
return (TemplateDelegate) constantExpression.Value;
diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs
index 4e1220f7..fbf63a64 100644
--- a/source/Handlebars/Compiler/HandlebarsCompiler.cs
+++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Expressions.Shortcuts;
using HandlebarsDotNet.Compiler.Lexer;
using HandlebarsDotNet.IO;
using HandlebarsDotNet.PathStructure;
@@ -22,7 +23,17 @@ public static TemplateDelegate Compile(ExtendedStringReader source, CompilationC
var tokens = Tokenizer.Tokenize(source).ToArray();
var expressions = ExpressionBuilder.ConvertTokensToExpressions(tokens, configuration);
- var action = FunctionBuilder.Compile(expressions, compilationContext);
+ var action = FunctionBuilder.Compile(expressions, compilationContext, out var decorators);
+
+ if (decorators.Count > 0)
+ {
+ var a1 = action;
+ var decorator = decorators.Compile(compilationContext);
+ action = (in EncodedTextWriter writer, BindingContext context) =>
+ {
+ decorator(writer, context, a1)(writer, context);
+ };
+ }
for (var index = 0; index < createdFeatures.Count; index++)
{
@@ -47,7 +58,17 @@ internal static TemplateDelegate CompileView(ViewReaderFactory readerFactoryFact
var layoutToken = tokens.OfType().SingleOrDefault();
var expressions = ExpressionBuilder.ConvertTokensToExpressions(tokens, configuration);
- var compiledView = FunctionBuilder.Compile(expressions, compilationContext);
+ var compiledView = FunctionBuilder.Compile(expressions, compilationContext, out var decorators);
+ if (decorators.Count > 0)
+ {
+ var a1 = compiledView;
+ var decorator = decorators.Compile(compilationContext);
+ compiledView = (in EncodedTextWriter writer, BindingContext context) =>
+ {
+ decorator(writer, context, a1)(writer, context);
+ };
+ }
+
if (layoutToken == null) return compiledView;
var fs = configuration.FileSystem;
diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs
index 8ec0682e..345137c0 100644
--- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs
+++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs
@@ -54,7 +54,7 @@ private static bool IsBlockHelper(Expression item, ICompiledHandlebarsConfigurat
{
var helperName = hitem.HelperName;
var helperPathInfo = PathInfo.Parse(helperName);
- return hitem.IsBlock || !configuration.Helpers.ContainsKey(helperPathInfo) && configuration.BlockHelpers.ContainsKey(helperPathInfo);
+ return hitem.IsBlock || !configuration.Helpers.ContainsKey(helperPathInfo) && (configuration.BlockHelpers.ContainsKey(helperPathInfo) || configuration.BlockDecorators.ContainsKey(helperPathInfo));
}
return false;
}
diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs
index cfcee589..d4fa215b 100644
--- a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs
+++ b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs
@@ -77,7 +77,7 @@ private bool IsRegisteredHelperName(string name)
if (pathInfo.IsBlockHelper || pathInfo.IsInversion || pathInfo.IsBlockClose || pathInfo.IsThis) return false;
name = pathInfo.TrimmedPath;
- return _configuration.Helpers.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name);
+ return _configuration.Helpers.ContainsKey(pathInfo) || _configuration.Decorators.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name);
}
private bool IsRegisteredBlockHelperName(string name, bool isRaw)
@@ -90,7 +90,7 @@ private bool IsRegisteredBlockHelperName(string name, bool isRaw)
name = pathInfo.TrimmedPath;
- return _configuration.BlockHelpers.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name);
+ return _configuration.BlockHelpers.ContainsKey(pathInfo) || _configuration.BlockDecorators.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name);
}
private bool IsUnregisteredBlockHelperName(string name, bool isRaw, IEnumerable sequence)
@@ -100,6 +100,10 @@ private bool IsUnregisteredBlockHelperName(string name, bool isRaw, IEnumerable<
if (!isRaw && !(pathInfo.IsBlockHelper || pathInfo.IsInversion)) return false;
name = name.Substring(1);
+ if (name.StartsWith("*"))
+ {
+ name = name.Substring(1);
+ }
var expectedBlockName = $"/{name}";
return sequence.OfType().Any(o =>
diff --git a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs
index 3b943361..32800c64 100644
--- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs
+++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs
@@ -8,7 +8,7 @@ namespace HandlebarsDotNet.Compiler.Lexer
{
internal class WordParser : Parser
{
- private const string ValidWordStartCharactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]";
+ private const string ValidWordStartCharactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]*";
private static readonly HashSet ValidWordStartCharacters = new HashSet();
static WordParser()
diff --git a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs
index f7088235..0b1c297a 100644
--- a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs
+++ b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
+using HandlebarsDotNet.Pools;
using static Expressions.Shortcuts.ExpressionShortcuts;
namespace HandlebarsDotNet.Compiler.Middlewares
@@ -15,16 +16,20 @@ public Expression Invoke(Expression expression) where T : Delegate
expression = (Expression) closureCollectorVisitor.Visit(expression);
if (constants.Count == 0) return expression;
-
- var closureBuilder = new ClosureBuilder();
- for (var index = 0; index < constants.Count; index++)
+
+ KeyValuePair> closureDefinition;
+ Closure closure;
+ using (var closureBuilder = ClosureBuilder.Create())
{
- var value = constants[index];
- closureBuilder.Add(value);
+ for (var index = 0; index < constants.Count; index++)
+ {
+ var value = constants[index];
+ closureBuilder.Add(value);
+ }
+
+ closureDefinition = closureBuilder.Build(out closure);
}
- var closureDefinition = closureBuilder.Build(out var closure);
-
var closureVisitor = new ClosureVisitor(closureDefinition);
expression = (Expression) closureVisitor.Visit(expression);
diff --git a/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs
index 147c1877..d0023b66 100644
--- a/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs
+++ b/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
+using HandlebarsDotNet.Pools;
namespace HandlebarsDotNet.Compiler.Middlewares
{
@@ -8,13 +9,14 @@ internal class ExpressionOptimizerMiddleware : IExpressionMiddleware
{
public Expression Invoke(Expression expression) where T : Delegate
{
- var visitor = new OptimizationVisitor();
+ using var container = GenericObjectPool.Shared.Use();
+ using var visitor = container.Value;
return (Expression) visitor.Visit(expression);
}
- private class OptimizationVisitor : ExpressionVisitor
+ private class OptimizationVisitor : ExpressionVisitor, IDisposable
{
- private readonly Dictionary _constantExpressions = new Dictionary();
+ private readonly Dictionary _constantExpressions = new();
protected override Expression VisitBlock(BlockExpression node)
{
@@ -55,6 +57,8 @@ protected override Expression VisitConstant(ConstantExpression node)
return node;
}
+
+ public void Dispose() => _constantExpressions.Clear();
}
}
}
\ No newline at end of file
diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs
index 92541994..44d7e995 100644
--- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs
@@ -1,5 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Linq.Expressions;
using Expressions.Shortcuts;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.Helpers.BlockHelpers;
using HandlebarsDotNet.PathStructure;
@@ -9,14 +13,17 @@
namespace HandlebarsDotNet.Compiler
{
+ internal enum BlockHelperDirection { Direct, Inverse }
+
internal class BlockHelperFunctionBinder : HandlebarsExpressionVisitor
{
- private enum BlockHelperDirection { Direct, Inverse }
-
+ private readonly List _decorators;
+
private CompilationContext CompilationContext { get; }
- public BlockHelperFunctionBinder(CompilationContext compilationContext)
+ public BlockHelperFunctionBinder(CompilationContext compilationContext, List decorators)
{
+ _decorators = decorators;
CompilationContext = compilationContext;
}
@@ -27,20 +34,30 @@ protected override Expression VisitStatementExpression(StatementExpression sex)
protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex)
{
- var isInlinePartial = bhex.HelperName == "#*inline";
-
var pathInfo = PathInfoStore.Current.GetOrAdd(bhex.HelperName);
var bindingContext = CompilationContext.Args.BindingContext;
- var context = isInlinePartial
- ? bindingContext.As()
- : bindingContext.Property(o => o.Value);
+
+ var direction = bhex.IsRaw || pathInfo.IsBlockHelper ? BlockHelperDirection.Direct : BlockHelperDirection.Inverse;
+ var isDecorator = direction switch
+ {
+ BlockHelperDirection.Direct => bhex.HelperName.StartsWith("#*"),
+ BlockHelperDirection.Inverse => bhex.HelperName.StartsWith("^*"),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ if (isDecorator)
+ {
+ _decorators.AddRange(VisitDecoratorBlockExpression(bhex));
+ return Expression.Empty();
+ }
var readerContext = bhex.Context;
- var direct = Compile(bhex.Body);
- var inverse = Compile(bhex.Inversion);
+ var direct = Compile(bhex.Body, out var directDecorators);
+ var inverse = Compile(bhex.Inversion, out var inverseDecorators);
var args = FunctionBinderHelpers.CreateArguments(bhex.Arguments, CompilationContext);
- var direction = bhex.IsRaw || pathInfo.IsBlockHelper ? BlockHelperDirection.Direct : BlockHelperDirection.Inverse;
+ var context = bindingContext.Property(o => o.Value);
+
var blockParams = CreateBlockParams();
var blockHelpers = CompilationContext.Configuration.BlockHelpers;
@@ -76,25 +93,143 @@ ExpressionContainer CreateBlockParams()
return Arg(parameters);
}
- TemplateDelegate Compile(Expression expression)
+ TemplateDelegate Compile(Expression expression, out IReadOnlyList decorators)
{
var blockExpression = (BlockExpression) expression;
- return FunctionBuilder.Compile(blockExpression.Expressions, new CompilationContext(CompilationContext));
+ return FunctionBuilder.Compile(blockExpression.Expressions, CompilationContext, out decorators);
}
Expression BindByRef(PathInfo name, Ref> helperBox)
{
- var writer = CompilationContext.Args.EncodedWriter;
-
- var helperOptions = direction switch
+ switch (direction)
{
- BlockHelperDirection.Direct => New(() => new BlockHelperOptions(name, direct, inverse, blockParams, bindingContext)),
- BlockHelperDirection.Inverse => New(() => new BlockHelperOptions(name, inverse, direct, blockParams, bindingContext)),
- _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext)
- };
+ case BlockHelperDirection.Direct when directDecorators.Count > 0:
+ {
+ var helperOptions = direction switch
+ {
+ BlockHelperDirection.Direct => New(() => new BlockHelperOptions(name, direct, inverse, blockParams, bindingContext)),
+ BlockHelperDirection.Inverse => New(() => new BlockHelperOptions(name, inverse, direct, blockParams, bindingContext)),
+ _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext)
+ };
+
+ var callContext = New(() => new Context(bindingContext, context));
+
+ var writer = CompilationContext.Args.EncodedWriter;
+ var directDecorator = directDecorators.Compile(CompilationContext);
+ var templateDelegate = FunctionBuilder.Compile(
+ new []
+ {
+ Call(() => helperBox.Value.Invoke(writer, helperOptions, callContext, args)).Expression
+ },
+ CompilationContext,
+ out _
+ );
+
+ return Call(() => directDecorator.Invoke(writer, bindingContext, templateDelegate))
+ .Call(f => f.Invoke(writer, bindingContext));
+ }
+ case BlockHelperDirection.Inverse when inverseDecorators.Count > 0:
+ {
+ var helperOptions = direction switch
+ {
+ BlockHelperDirection.Direct => New(() => new BlockHelperOptions(name, direct, inverse, blockParams, bindingContext)),
+ BlockHelperDirection.Inverse => New(() => new BlockHelperOptions(name, inverse, direct, blockParams, bindingContext)),
+ _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext)
+ };
+
+ var callContext = New(() => new Context(bindingContext, context));
+
+ var writer = CompilationContext.Args.EncodedWriter;
+ var inverseDecorator = inverseDecorators.Compile(CompilationContext);
+ var templateDelegate = FunctionBuilder.Compile(
+ new []
+ {
+ Call(() => helperBox.Value.Invoke(writer, helperOptions, callContext, args)).Expression
+ },
+ CompilationContext,
+ out _
+ );
+
+ return Call(() => inverseDecorator.Invoke(writer, bindingContext, templateDelegate))
+ .Call(f => f.Invoke(writer, bindingContext));
+ }
+ default:
+ {
+ var helperOptions = direction switch
+ {
+ BlockHelperDirection.Direct => New(() => new BlockHelperOptions(name, direct, inverse, blockParams, bindingContext)),
+ BlockHelperDirection.Inverse => New(() => new BlockHelperOptions(name, inverse, direct, blockParams, bindingContext)),
+ _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext)
+ };
+
+ var callContext = New(() => new Context(bindingContext, context));
+ var writer = CompilationContext.Args.EncodedWriter;
+ return Call(() => helperBox.Value.Invoke(writer, helperOptions, callContext, args));
+ }
+ }
+ }
+ }
+
+ private IEnumerable VisitDecoratorBlockExpression(BlockHelperExpression bhex)
+ {
+ var pathInfo = PathInfoStore.Current.GetOrAdd(bhex.HelperName);
+ var bindingContext = CompilationContext.Args.BindingContext;
+ var direction = bhex.IsRaw || pathInfo.IsBlockHelper ? BlockHelperDirection.Direct : BlockHelperDirection.Inverse;
+ if (direction == BlockHelperDirection.Inverse)
+ {
+ throw new HandlebarsCompilerException("^ is not supported for decorators", bhex.Context);
+ }
+
+ var direct = Compile(bhex.Body, out var decorators);
+ for (var index = 0; index < decorators.Count; index++)
+ {
+ yield return decorators[index];
+ }
+
+ var args = FunctionBinderHelpers.CreateArguments(bhex.Arguments, CompilationContext);
+
+ var context = bindingContext.Property(o => o.Value);
+ var blockParams = CreateBlockParams();
+
+ var blockDecorators = CompilationContext.Configuration.BlockDecorators;
+ if (blockDecorators.TryGetValue(pathInfo, out var descriptor))
+ {
+ var binding = BindDecoratorByRef(pathInfo, descriptor, out var f1);
+ yield return new DecoratorDefinition(binding, f1);
+ yield break;
+ }
+
+ var emptyBlockDecorator = new EmptyBlockDecorator(pathInfo);
+ var emptyBlockDecoratorRef = new Ref>(emptyBlockDecorator);
+ blockDecorators.AddOrReplace(pathInfo, emptyBlockDecoratorRef);
+
+ var emptyBinding = BindDecoratorByRef(pathInfo, emptyBlockDecoratorRef, out var f2);
+ yield return new DecoratorDefinition(emptyBinding, f2);
+
+ ExpressionContainer CreateBlockParams()
+ {
+ var parameters = bhex.BlockParams?.BlockParam?.Parameters;
+ parameters ??= ArrayEx.Empty();
+
+ return Arg(parameters);
+ }
+
+ TemplateDelegate Compile(Expression expression, out IReadOnlyList decorators)
+ {
+ var blockExpression = (BlockExpression) expression;
+ return FunctionBuilder.Compile(blockExpression.Expressions, CompilationContext, out decorators);
+ }
+
+ Expression BindDecoratorByRef(PathInfo name, Ref> helperBox, out ExpressionContainer function)
+ {
+ function = Parameter();
+ var f = function;
+
+ var helperOptions = New(() => new BlockDecoratorOptions(name, direct, blockParams, bindingContext));
var callContext = New(() => new Context(bindingContext, context));
- return Call(() => helperBox.Value.Invoke(writer, helperOptions, callContext, args));
+
+ return Call(() => helperBox.Value.Invoke(f, helperOptions, callContext, args));
}
}
}
diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs
index a00ccb0b..3b38d409 100644
--- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs
@@ -1,5 +1,5 @@
using System.Linq.Expressions;
-using Expressions.Shortcuts;
+using static Expressions.Shortcuts.ExpressionShortcuts;
namespace HandlebarsDotNet.Compiler
{
@@ -15,9 +15,9 @@ public BoolishConverter(CompilationContext compilationContext)
protected override Expression VisitBoolishExpression(BoolishExpression bex)
{
var condition = Visit(bex.Condition);
- condition = FunctionBuilder.Reduce(condition, _compilationContext);
- var @object = ExpressionShortcuts.Arg(condition);
- return ExpressionShortcuts.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(@object));
+ condition = FunctionBuilder.Reduce(condition, _compilationContext, out _);
+ var @object = Arg(condition);
+ return Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(@object));
}
}
}
diff --git a/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs
new file mode 100644
index 00000000..f0b3821b
--- /dev/null
+++ b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using Expressions.Shortcuts;
+
+namespace HandlebarsDotNet.Compiler
+{
+ public delegate TemplateDelegate DecoratorDelegate(in EncodedTextWriter writer, BindingContext context, TemplateDelegate function);
+
+ internal readonly struct DecoratorDefinition
+ {
+ public DecoratorDefinition(Expression decorator, ExpressionContainer function)
+ {
+ Decorator = decorator;
+ Function = function;
+ }
+
+ public Expression Decorator { get; }
+
+ public ExpressionContainer Function { get; }
+
+ public DecoratorDelegate Compile(CompilationContext context)
+ {
+ if (Function is null || Decorator is null) return (in EncodedTextWriter writer, BindingContext bindingContext, TemplateDelegate function) => function;
+
+ var lambda = Expression.Lambda(
+ Decorator,
+ context.EncodedWriter,
+ context.BindingContext,
+ Function.Expression as ParameterExpression
+ );
+
+ return context.Configuration.ExpressionCompiler.Compile(lambda);
+ }
+ }
+
+ internal static class DecoratorDefinitionsExtensions
+ {
+ public static DecoratorDelegate Compile(
+ this IReadOnlyList decoratorDefinitions,
+ CompilationContext context
+ )
+ {
+ var decorator = decoratorDefinitions[0].Compile(context);
+
+ for (var index = 1; index < decoratorDefinitions.Count; index++)
+ {
+ var definition = decoratorDefinitions[index];
+ var f = definition.Compile(context);
+ var current = decorator;
+ decorator = (in EncodedTextWriter writer, BindingContext bindingContext, TemplateDelegate function) =>
+ f(writer, bindingContext, current(writer, bindingContext, function));
+ }
+
+ return decorator;
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs
index 8b1a64fa..fb35f1c8 100644
--- a/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs
@@ -35,7 +35,7 @@ public static ExpressionContainer CreateArguments(IEnumerable(path => path.Context = PathExpression.ResolutionContext.Parameter)
- .Select(o => FunctionBuilder.Reduce(o, compilationContext))
+ .Select(o => FunctionBuilder.Reduce(o, compilationContext, out _))
.ToArray();
if (arguments.Length == 0) return New(() => new Arguments(0));
diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs
index c2ab6b6c..d1006cac 100644
--- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs
@@ -1,5 +1,9 @@
+using System.Collections.Generic;
using System.Linq.Expressions;
+using Expressions.Shortcuts;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.Helpers;
+using HandlebarsDotNet.Helpers.BlockHelpers;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;
using static Expressions.Shortcuts.ExpressionShortcuts;
@@ -8,10 +12,12 @@ namespace HandlebarsDotNet.Compiler
{
internal class HelperFunctionBinder : HandlebarsExpressionVisitor
{
+ private readonly List _decorators;
private CompilationContext CompilationContext { get; }
- public HelperFunctionBinder(CompilationContext compilationContext)
+ public HelperFunctionBinder(CompilationContext compilationContext, List decorators)
{
+ _decorators = decorators;
CompilationContext = compilationContext;
}
@@ -22,6 +28,12 @@ protected override Expression VisitStatementExpression(StatementExpression sex)
protected override Expression VisitHelperExpression(HelperExpression hex)
{
+ if (hex.HelperName.StartsWith("*"))
+ {
+ _decorators.Add(VisitDecoratorExpression(hex));
+ return Expression.Empty();
+ }
+
var pathInfo = PathInfoStore.Current.GetOrAdd(hex.HelperName);
if(!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming) return Expression.Empty();
@@ -54,5 +66,35 @@ protected override Expression VisitHelperExpression(HelperExpression hex)
return Call(() => lateBindDescriptor.Value.Invoke(textWriter, options, contextValue, args));
}
+
+ private DecoratorDefinition VisitDecoratorExpression(HelperExpression hex)
+ {
+ var pathInfo = PathInfoStore.Current.GetOrAdd(hex.HelperName);
+ if(!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming) return new DecoratorDefinition();
+
+ var bindingContext = CompilationContext.Args.BindingContext;
+ var options = New(() => new DecoratorOptions(pathInfo, bindingContext));
+
+ var contextValue = New(() => new Context(bindingContext));
+ var args = FunctionBinderHelpers.CreateArguments(hex.Arguments, CompilationContext);
+
+ var parameter = Parameter();
+ var configuration = CompilationContext.Configuration;
+ if (configuration.Decorators.TryGetValue(pathInfo, out var helper))
+ {
+ return new DecoratorDefinition(
+ Call(() => helper.Value.Invoke(parameter, options, contextValue, args)),
+ parameter
+ );
+ }
+
+ var emptyDecorator = new Ref>(new EmptyDecorator(pathInfo));
+ configuration.Decorators.AddOrReplace(pathInfo, emptyDecorator);
+
+ return new DecoratorDefinition(
+ Call(() => emptyDecorator.Value.Invoke(parameter, options, contextValue, args)),
+ parameter
+ );
+ }
}
}
diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs
index 12b68826..9929eb52 100644
--- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs
@@ -18,26 +18,82 @@ public IteratorBinder(CompilationContext compilationContext)
protected override Expression VisitIteratorExpression(IteratorExpression iex)
{
- var context = CompilationContext.Args.BindingContext;
- var writer = CompilationContext.Args.EncodedWriter;
-
- var template = FunctionBuilder.Compile(new[] {iex.Template}, new CompilationContext(CompilationContext));
- var ifEmpty = FunctionBuilder.Compile(new[] {iex.IfEmpty}, new CompilationContext(CompilationContext));
+ var direction = iex.HelperName[0] switch
+ {
+ '#' => BlockHelperDirection.Direct,
+ '^' => BlockHelperDirection.Inverse,
+ _ => throw new HandlebarsCompilerException($"Tried to convert {iex.HelperName} expression to iterator block", iex.Context)
+ };
+
+ var template = FunctionBuilder.Compile(new[] {iex.Template}, CompilationContext, out var directDecorators);
+ var ifEmpty = FunctionBuilder.Compile(new[] {iex.IfEmpty}, CompilationContext, out var inverseDecorators);
if (iex.Sequence is PathExpression pathExpression)
{
pathExpression.Context = PathExpression.ResolutionContext.Parameter;
}
-
- var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext));
- var blockParamsValues = CreateBlockParams();
-
- return iex.HelperName[0] switch
+
+ switch (direction)
{
- '#' => Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, template, ifEmpty)),
- '^' => Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, ifEmpty, template)),
- _ => throw new HandlebarsCompilerException($"Tried to convert {iex.HelperName} expression to iterator block", iex.Context)
- };
+ case BlockHelperDirection.Direct when directDecorators.Count > 0:
+ {
+ var context = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+ var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext, out _));
+ var blockParamsValues = CreateBlockParams();
+ var templateDelegate = FunctionBuilder.Compile(
+ new []
+ {
+ Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, template, ifEmpty)).Expression
+ },
+ CompilationContext,
+ out _
+ );
+
+ var decorator = directDecorators.Compile(CompilationContext);
+ return Call(() => decorator.Invoke(writer, context, templateDelegate))
+ .Call(f => f.Invoke(writer, context));
+ }
+ case BlockHelperDirection.Inverse when inverseDecorators.Count > 0:
+ {
+ var context = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+ var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext, out _));
+ var blockParamsValues = CreateBlockParams();
+ var templateDelegate = FunctionBuilder.Compile(
+ new []
+ {
+ Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, ifEmpty, template)).Expression
+ },
+ CompilationContext,
+ out _
+ );
+
+ var decorator = inverseDecorators.Compile(CompilationContext);
+ return Call(() => decorator.Invoke(writer, context, templateDelegate))
+ .Call(f => f.Invoke(writer, context));
+ }
+ case BlockHelperDirection.Direct:
+ {
+ var context = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+ var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext, out _));
+ var blockParamsValues = CreateBlockParams();
+ return Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, template, ifEmpty));
+ }
+ case BlockHelperDirection.Inverse:
+ {
+ var context = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+ var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext, out _));
+ var blockParamsValues = CreateBlockParams();
+ return Call(() => Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, ifEmpty, template));
+ }
+ default:
+ {
+ throw new HandlebarsCompilerException($"Tried to convert {iex.HelperName} expression to iterator block", iex.Context);
+ }
+ }
ExpressionContainer CreateBlockParams()
{
diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs
index c39efdea..c435469c 100644
--- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Linq.Expressions;
using Expressions.Shortcuts;
+using HandlebarsDotNet.Polyfills;
using static Expressions.Shortcuts.ExpressionShortcuts;
namespace HandlebarsDotNet.Compiler
@@ -22,28 +24,66 @@ public PartialBinder(CompilationContext compilationContext)
protected override Expression VisitPartialExpression(PartialExpression pex)
{
- var bindingContext = CompilationContext.Args.BindingContext;
- var writer = CompilationContext.Args.EncodedWriter;
-
+ IReadOnlyList decorators = ArrayEx.Empty();
var partialBlockTemplate = pex.Fallback != null
- ? FunctionBuilder.Compile(new[] { pex.Fallback }, new CompilationContext(CompilationContext))
+ ? FunctionBuilder.Compile(new[] { pex.Fallback }, CompilationContext, out decorators)
: null;
-
- if (pex.Argument != null || partialBlockTemplate != null)
+
+ if (decorators.Count > 0)
{
- var value = pex.Argument != null
- ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext))
- : bindingContext.Property(o => o.Value);
+ var bindingContext = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+
+ var parentContext = bindingContext;
+ if (pex.Argument != null || partialBlockTemplate != null)
+ {
+ var value = pex.Argument != null
+ ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _))
+ : bindingContext.Property(o => o.Value);
- var partialTemplate = Arg(partialBlockTemplate);
- bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate));
+ var partialTemplate = Arg(partialBlockTemplate);
+ bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate));
+ }
+
+ var partialName = Cast(pex.PartialName);
+ var configuration = Arg(CompilationContext.Configuration);
+ var templateDelegate = FunctionBuilder.Compile(
+ new []
+ {
+ Call(() =>
+ InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration)
+ ).Expression
+ },
+ CompilationContext,
+ out _
+ );
+
+ var decorator = decorators.Compile(CompilationContext);
+ return Call(() => decorator.Invoke(writer, parentContext, templateDelegate))
+ .Call(f => f.Invoke(writer, parentContext));
}
+ else
+ {
+ var bindingContext = CompilationContext.Args.BindingContext;
+ var writer = CompilationContext.Args.EncodedWriter;
+
+ if (pex.Argument != null || partialBlockTemplate != null)
+ {
+ var value = pex.Argument != null
+ ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _))
+ : bindingContext.Property(o => o.Value);
+
+ var partialTemplate = Arg(partialBlockTemplate);
+ bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate));
+ }
- var partialName = Cast(pex.PartialName);
- var configuration = Arg(CompilationContext.Configuration);
- return Call(() =>
- InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration)
- );
+ var partialName = Cast(pex.PartialName);
+ var configuration = Arg(CompilationContext.Configuration);
+
+ return Call(() =>
+ InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration)
+ );
+ }
}
private static void InvokePartialWithFallback(
diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs
index e0de802d..8630246b 100644
--- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs
+++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs
@@ -22,7 +22,7 @@ protected override Expression VisitSubExpression(SubExpressionExpression subex)
return HandleMethodCallExpression(callExpression);
default:
- var expression = FunctionBuilder.Reduce(subex.Expression, CompilationContext);
+ var expression = FunctionBuilder.Reduce(subex.Expression, CompilationContext, out _);
if (expression is MethodCallExpression lateBoundCall)
return HandleMethodCallExpression(lateBoundCall);
diff --git a/source/Handlebars/Configuration/Compatibility.cs b/source/Handlebars/Configuration/Compatibility.cs
index 73f6f5f4..c2b46674 100644
--- a/source/Handlebars/Configuration/Compatibility.cs
+++ b/source/Handlebars/Configuration/Compatibility.cs
@@ -18,6 +18,7 @@ public class Compatibility
/// This enables helper names to be not-valid Handlebars identifiers (e.g. {{ one.two }}
)
/// Such naming is not supported in Handlebarsjs and would break compatibility.
///
+ [Obsolete("Toggle will be removed in the next major release")]
public bool RelaxedHelperNaming { get; set; } = false;
}
}
\ No newline at end of file
diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs
index 99b3062a..c7d4d09a 100644
--- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs
+++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using System.IO;
using HandlebarsDotNet.Collections;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.EqualityComparers;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.IO;
@@ -18,6 +19,10 @@ public sealed class HandlebarsConfiguration : IHandlebarsTemplateRegistrations
public IIndexed> BlockHelpers { get; }
+ public IIndexed> Decorators { get; }
+
+ public IIndexed> BlockDecorators { get; }
+
public IIndexed> RegisteredTemplates { get; }
///
@@ -73,6 +78,8 @@ public HandlebarsConfiguration()
var stringEqualityComparer = new StringEqualityComparer(StringComparison.OrdinalIgnoreCase);
Helpers = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer);
BlockHelpers = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer);
+ Decorators = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer);
+ BlockDecorators = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer);
RegisteredTemplates = new ObservableIndex, StringEqualityComparer>(stringEqualityComparer);
HelperResolvers = new ObservableList();
diff --git a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs
index 15884a99..9650f43f 100644
--- a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs
+++ b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs
@@ -6,6 +6,7 @@
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.Compiler.Middlewares;
using HandlebarsDotNet.Compiler.Resolvers;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.EqualityComparers;
using HandlebarsDotNet.Features;
using HandlebarsDotNet.Helpers;
@@ -45,8 +46,10 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
.OrderBy(o => o.GetType().GetTypeInfo().GetCustomAttribute()?.Order ?? 100)
.ToList();
- Helpers = CreateHelpersSubscription(configuration.Helpers);
- BlockHelpers = CreateHelpersSubscription(configuration.BlockHelpers);
+ Helpers = CreateHelpersSubscription, HelperOptions>(configuration.Helpers);
+ BlockHelpers = CreateHelpersSubscription, BlockHelperOptions>(configuration.BlockHelpers);
+ Decorators = CreateHelpersSubscription, DecoratorOptions>(configuration.Decorators);
+ BlockDecorators = CreateHelpersSubscription, BlockDecoratorOptions>(configuration.BlockDecorators);
}
public HandlebarsConfiguration UnderlingConfiguration { get; }
@@ -70,24 +73,26 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
public IIndexed>> Helpers { get; }
public IIndexed>> BlockHelpers { get; }
+ public IIndexed>> Decorators { get; }
+ public IIndexed>> BlockDecorators { get; }
public IAppendOnlyList HelperResolvers { get; }
public IIndexed> RegisteredTemplates { get; }
- private ObservableIndex>, IEqualityComparer> CreateHelpersSubscription(
- IIndexed> source)
- where TOptions : struct, IHelperOptions
+ private ObservableIndex, IEqualityComparer> CreateHelpersSubscription(IIndexed source)
+ where TOptions : struct, IOptions
+ where TDescriptor : class, IDescriptor
{
var equalityComparer = Compatibility.RelaxedHelperNaming ? PathInfoLight.PlainPathComparer : PathInfoLight.PlainPathWithPartsCountComparer;
var existingHelpers = source.ToIndexed(
o => (PathInfoLight) $"[{o.Key}]",
- o => new Ref>(o.Value),
+ o => new Ref(o.Value),
equalityComparer
);
- var target = new ObservableIndex>, IEqualityComparer>(equalityComparer, existingHelpers);
+ var target = new ObservableIndex, IEqualityComparer>(equalityComparer, existingHelpers);
- var observer = ObserverBuilder>>.Create(target)
- .OnEvent>>(
+ var observer = ObserverBuilder>.Create(target)
+ .OnEvent>(
(@event, state) =>
{
PathInfoLight key = $"[{@event.Key}]";
@@ -97,13 +102,13 @@ private ObservableIndex>, IEquali
return;
}
- state.AddOrReplace(key, new Ref>(@event.Value));
+ state.AddOrReplace(key, new Ref(@event.Value));
})
.Build();
_observers.Add(observer);
- source.As, StringEqualityComparer>>()?.Subscribe(observer);
+ source.As>()?.Subscribe(observer);
return target;
}
diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs
index 18dc4d2c..600e5f21 100644
--- a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs
+++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs
@@ -3,6 +3,7 @@
using System.IO;
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.Compiler.Resolvers;
+using HandlebarsDotNet.Decorators;
using HandlebarsDotNet.Features;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.IO;
@@ -39,6 +40,10 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat
IIndexed>> BlockHelpers { get; }
+ IIndexed>> Decorators { get; }
+
+ IIndexed>> BlockDecorators { get; }
+
IAppendOnlyList