From 1bb797937e1785dd04fa8632c50b55dc79941442 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 23 Jan 2022 20:11:00 -0800 Subject: [PATCH 1/3] Decorators implementation --- source/Directory.Build.props | 2 +- source/Handlebars.Benchmark/EndToEnd.cs | 48 +- .../Handlebars.Test/BasicIntegrationTests.cs | 22 +- source/Handlebars.Test/ClosureBuilderTests.cs | 28 +- source/Handlebars.Test/DecoratorTests.cs | 441 ++++++++++++++++++ .../Handlebars.Test/HandlebarsEnvGenerator.cs | 28 ++ source/Handlebars.Test/IssueTests.cs | 26 ++ source/Handlebars/BindingContext.cs | 16 +- source/Handlebars/Collections/IIndexed.cs | 1 + .../Handlebars/Collections/ObservableIndex.cs | 19 + source/Handlebars/Compiler/ClosureBuilder.cs | 71 ++- source/Handlebars/Compiler/FunctionBuilder.cs | 29 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 25 +- .../BlockAccumulatorContext.cs | 2 +- .../Lexer/Converter/HelperConverter.cs | 8 +- .../Compiler/Lexer/Parsers/WordParser.cs | 2 +- .../ClosureExpressionMiddleware.cs | 6 +- .../ExpressionOptimizerMiddleware.cs | 10 +- .../Expression/BlockHelperFunctionBinder.cs | 177 ++++++- .../Expression/BoolishConverter.cs | 8 +- .../Expression/DecoratorDefinition.cs | 57 +++ .../Expression/FunctionBinderHelpers.cs | 2 +- .../Expression/HelperFunctionBinder.cs | 44 +- .../Translation/Expression/IteratorBinder.cs | 84 +++- .../Translation/Expression/PartialBinder.cs | 72 ++- .../Expression/SubExpressionVisitor.cs | 2 +- .../Handlebars/Configuration/Compatibility.cs | 1 + .../Configuration/HandlebarsConfiguration.cs | 7 + .../HandlebarsConfigurationAdapter.cs | 27 +- .../ICompiledHandlebarsConfiguration.cs | 5 + .../Decorators/BlockDecoratorOptions.cs | 91 ++++ .../Handlebars/Decorators/DecoratorOptions.cs | 32 ++ .../DelegateBlockDecoratorDescriptor.cs | 23 + .../DelegateBlockDecoratorVoidDescriptor.cs | 24 + .../Decorators/DelegateDecoratorDescriptor.cs | 23 + .../DelegateDecoratorVoidDescriptor.cs | 24 + .../Decorators/EmptyBlockDecorator.cs | 17 + .../Handlebars/Decorators/EmptyDecorator.cs | 14 + .../Decorators/IDecoratorDescriptor.cs | 16 + .../Decorators/IDecoratorOptions.cs | 11 + .../InlineBlockDecoratorDescriptor.cs | 34 ++ .../Extensions/EnumerableExtensions.cs | 12 + .../Features/BuildInHelpersFeature.cs | 5 +- source/Handlebars/Handlebars.cs | 71 +-- source/Handlebars/Handlebars.csproj | 5 + source/Handlebars/HandlebarsEnvironment.cs | 44 +- source/Handlebars/HandlebarsExtensions.cs | 15 +- .../InlineBlockHelperDescriptor.cs | 40 -- .../LateBindBlockHelperDescriptor.cs | 6 + .../Handlebars/Helpers/IHelperDescriptor.cs | 4 +- .../Helpers/LateBindHelperDescriptor.cs | 5 + source/Handlebars/IDescriptor.cs | 8 + source/Handlebars/IHandlebars.cs | 19 +- source/Handlebars/IHelperOptions.cs | 3 +- source/Handlebars/IHelpersRegistry.cs | 63 +++ source/Handlebars/IOptions.cs | 7 + .../Handlebars/Pools/BindingContext.Pool.cs | 2 + .../Handlebars/Pools/ClosureBuilder.Pool.cs | 42 ++ source/Handlebars/Pools/GenericObjectPool.cs | 19 + source/Handlebars/Runtime/Ref.cs | 26 +- source/Handlebars/_Delegates.cs | 61 +++ 61 files changed, 1746 insertions(+), 290 deletions(-) create mode 100644 source/Handlebars.Test/DecoratorTests.cs create mode 100644 source/Handlebars.Test/HandlebarsEnvGenerator.cs create mode 100644 source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs create mode 100644 source/Handlebars/Decorators/BlockDecoratorOptions.cs create mode 100644 source/Handlebars/Decorators/DecoratorOptions.cs create mode 100644 source/Handlebars/Decorators/DelegateBlockDecoratorDescriptor.cs create mode 100644 source/Handlebars/Decorators/DelegateBlockDecoratorVoidDescriptor.cs create mode 100644 source/Handlebars/Decorators/DelegateDecoratorDescriptor.cs create mode 100644 source/Handlebars/Decorators/DelegateDecoratorVoidDescriptor.cs create mode 100644 source/Handlebars/Decorators/EmptyBlockDecorator.cs create mode 100644 source/Handlebars/Decorators/EmptyDecorator.cs create mode 100644 source/Handlebars/Decorators/IDecoratorDescriptor.cs create mode 100644 source/Handlebars/Decorators/IDecoratorOptions.cs create mode 100644 source/Handlebars/Decorators/InlineBlockDecoratorDescriptor.cs delete mode 100644 source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs create mode 100644 source/Handlebars/IDescriptor.cs create mode 100644 source/Handlebars/IHelpersRegistry.cs create mode 100644 source/Handlebars/IOptions.cs create mode 100644 source/Handlebars/Pools/ClosureBuilder.Pool.cs create mode 100644 source/Handlebars/Pools/GenericObjectPool.cs create mode 100644 source/Handlebars/_Delegates.cs 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..0f37865d 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 @@ -10,13 +11,14 @@ internal class ClosureExpressionMiddleware : IExpressionMiddleware { public Expression Invoke(Expression expression) where T : Delegate { - var constants = new List(); + using var container = GenericObjectPool>.Shared.Use(); + var constants = container.Value; var closureCollectorVisitor = new ClosureCollectorVisitor(constants); expression = (Expression) closureCollectorVisitor.Visit(expression); if (constants.Count == 0) return expression; - var closureBuilder = new ClosureBuilder(); + using var closureBuilder = ClosureBuilder.Create(); for (var index = 0; index < constants.Count; index++) { var value = constants[index]; 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 HelperResolvers { get; } /// diff --git a/source/Handlebars/Decorators/BlockDecoratorOptions.cs b/source/Handlebars/Decorators/BlockDecoratorOptions.cs new file mode 100644 index 00000000..156fa251 --- /dev/null +++ b/source/Handlebars/Decorators/BlockDecoratorOptions.cs @@ -0,0 +1,91 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Decorators; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.IO; +using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.ValueProviders; + +namespace HandlebarsDotNet +{ + /// + /// Contains properties accessible withing function + /// + public readonly struct BlockDecoratorOptions : IDecoratorOptions + { + internal readonly TemplateDelegate OriginalTemplate; + + public BindingContext Frame { get; } + + public readonly ChainSegment[] BlockVariables; + + internal BlockDecoratorOptions( + PathInfo name, + TemplateDelegate template, + ChainSegment[] blockParamsValues, + BindingContext frame) + { + Name = name; + OriginalTemplate = template; + Frame = frame; + BlockVariables = blockParamsValues; + } + + public DataValues Data => new DataValues(Frame); + + public PathInfo Name { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BindingContext CreateFrame(object value = null) => Frame.CreateFrame(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BindingContext CreateFrame(Context value) => Frame.CreateFrame(value.Value); + + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Template() + { + using var writer = ReusableStringWriter.Get(); + using var encodedTextWriter = new EncodedTextWriter(writer, Frame.Configuration.TextEncoder, FormatterProvider.Current); + + OriginalTemplate(encodedTextWriter, Frame); + + return encodedTextWriter.ToString(); + } + + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Template(in EncodedTextWriter writer, object context) + { + if (context is BindingContext bindingContext) + { + OriginalTemplate(writer, bindingContext); + return; + } + + using var frame = Frame.CreateFrame(context); + OriginalTemplate(writer, frame); + } + + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Template(in EncodedTextWriter writer, in Context context) => Template(writer, context.Value); + + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Template(in EncodedTextWriter writer, BindingContext context) => OriginalTemplate(writer, context); + + IIndexed> IHelpersRegistry.GetHelpers() => Frame.Helpers; + + IIndexed> IHelpersRegistry.GetBlockHelpers() => Frame.BlockHelpers; + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/DecoratorOptions.cs b/source/Handlebars/Decorators/DecoratorOptions.cs new file mode 100644 index 00000000..d6e39294 --- /dev/null +++ b/source/Handlebars/Decorators/DecoratorOptions.cs @@ -0,0 +1,32 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Decorators; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.ValueProviders; + +namespace HandlebarsDotNet +{ + public readonly struct DecoratorOptions : IDecoratorOptions + { + public BindingContext Frame { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DecoratorOptions( + PathInfo name, + BindingContext frame + ) + { + Frame = frame; + Name = name; + } + + public DataValues Data => new DataValues(Frame); + + public PathInfo Name { get; } + + IIndexed> IHelpersRegistry.GetHelpers() => Frame.Helpers; + + IIndexed> IHelpersRegistry.GetBlockHelpers() => Frame.BlockHelpers; + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/DelegateBlockDecoratorDescriptor.cs b/source/Handlebars/Decorators/DelegateBlockDecoratorDescriptor.cs new file mode 100644 index 00000000..0a71f5bb --- /dev/null +++ b/source/Handlebars/Decorators/DelegateBlockDecoratorDescriptor.cs @@ -0,0 +1,23 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class DelegateBlockDecoratorDescriptor : IDecoratorDescriptor + { + private readonly HandlebarsBlockDecorator _helper; + + public DelegateBlockDecoratorDescriptor(string name, HandlebarsBlockDecorator helper) + { + _helper = helper; + Name = name; + } + + public TemplateDelegate Invoke(in TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) + { + return _helper(function, options, context, arguments); + } + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/DelegateBlockDecoratorVoidDescriptor.cs b/source/Handlebars/Decorators/DelegateBlockDecoratorVoidDescriptor.cs new file mode 100644 index 00000000..89d00772 --- /dev/null +++ b/source/Handlebars/Decorators/DelegateBlockDecoratorVoidDescriptor.cs @@ -0,0 +1,24 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class DelegateBlockDecoratorVoidDescriptor : IDecoratorDescriptor + { + private readonly HandlebarsBlockDecoratorVoid _helper; + + public DelegateBlockDecoratorVoidDescriptor(string name, HandlebarsBlockDecoratorVoid helper) + { + _helper = helper; + Name = name; + } + + public TemplateDelegate Invoke(in TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) + { + _helper(function, options, context, arguments); + return function; + } + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/DelegateDecoratorDescriptor.cs b/source/Handlebars/Decorators/DelegateDecoratorDescriptor.cs new file mode 100644 index 00000000..8a42ea0c --- /dev/null +++ b/source/Handlebars/Decorators/DelegateDecoratorDescriptor.cs @@ -0,0 +1,23 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class DelegateDecoratorDescriptor : IDecoratorDescriptor + { + private readonly HandlebarsDecorator _helper; + + public DelegateDecoratorDescriptor(string name, HandlebarsDecorator helper) + { + _helper = helper; + Name = name; + } + + public TemplateDelegate Invoke(in TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) + { + return _helper(function, options, context, arguments); + } + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/DelegateDecoratorVoidDescriptor.cs b/source/Handlebars/Decorators/DelegateDecoratorVoidDescriptor.cs new file mode 100644 index 00000000..1292d4a5 --- /dev/null +++ b/source/Handlebars/Decorators/DelegateDecoratorVoidDescriptor.cs @@ -0,0 +1,24 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class DelegateDecoratorVoidDescriptor : IDecoratorDescriptor + { + private readonly HandlebarsDecoratorVoid _helper; + + public DelegateDecoratorVoidDescriptor(string name, HandlebarsDecoratorVoid helper) + { + _helper = helper; + Name = name; + } + + public TemplateDelegate Invoke(in TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) + { + _helper(function, options, context, arguments); + return function; + } + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/EmptyBlockDecorator.cs b/source/Handlebars/Decorators/EmptyBlockDecorator.cs new file mode 100644 index 00000000..7e1c6f6e --- /dev/null +++ b/source/Handlebars/Decorators/EmptyBlockDecorator.cs @@ -0,0 +1,17 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class EmptyBlockDecorator : IDecoratorDescriptor + { + public EmptyBlockDecorator(PathInfo name) => Name = name; + + public TemplateDelegate Invoke(in TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) + { + return function; + } + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/EmptyDecorator.cs b/source/Handlebars/Decorators/EmptyDecorator.cs new file mode 100644 index 00000000..11f3aba0 --- /dev/null +++ b/source/Handlebars/Decorators/EmptyDecorator.cs @@ -0,0 +1,14 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public sealed class EmptyDecorator : IDecoratorDescriptor + { + public EmptyDecorator(PathInfo name) => Name = name; + + public TemplateDelegate Invoke(in TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) => function; + + public PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/IDecoratorDescriptor.cs b/source/Handlebars/Decorators/IDecoratorDescriptor.cs new file mode 100644 index 00000000..577a04cc --- /dev/null +++ b/source/Handlebars/Decorators/IDecoratorDescriptor.cs @@ -0,0 +1,16 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + public interface IDecoratorDescriptor + { + PathInfo Name { get; } + } + + public interface IDecoratorDescriptor : IDecoratorDescriptor, IDescriptor + where TOptions: struct, IDecoratorOptions + { + TemplateDelegate Invoke(in TemplateDelegate function, in TOptions options, in Context context, in Arguments arguments); + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/IDecoratorOptions.cs b/source/Handlebars/Decorators/IDecoratorOptions.cs new file mode 100644 index 00000000..2fa952db --- /dev/null +++ b/source/Handlebars/Decorators/IDecoratorOptions.cs @@ -0,0 +1,11 @@ +using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.ValueProviders; + +namespace HandlebarsDotNet.Decorators +{ + public interface IDecoratorOptions : IOptions, IHelpersRegistry + { + DataValues Data { get; } + PathInfo Name { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Decorators/InlineBlockDecoratorDescriptor.cs b/source/Handlebars/Decorators/InlineBlockDecoratorDescriptor.cs new file mode 100644 index 00000000..00a81725 --- /dev/null +++ b/source/Handlebars/Decorators/InlineBlockDecoratorDescriptor.cs @@ -0,0 +1,34 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.PathStructure; + +namespace HandlebarsDotNet.Decorators +{ + internal sealed class InlineBlockDecoratorDescriptor : IDecoratorDescriptor + { + public TemplateDelegate Invoke(in TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments) + { + if (arguments.Length != 1) + { + throw new HandlebarsRuntimeException("{{*inline}} helper must have exactly one argument"); + } + + var bindingContext = options.Frame; + + if(arguments[0] is not string key) throw new HandlebarsRuntimeException("Inline argument is not valid"); + + //Inline partials cannot use the Handlebars.RegisterTemplate method + //because it is static and therefore app-wide. To prevent collisions + //this helper will add the compiled partial to a dicionary + //that is passed around in the context without fear of collisions. + var template = options.OriginalTemplate; + bindingContext.InlinePartialTemplates.AddOrReplace(key, (writer, c) => + { + template(writer, c); + }); + + return function; + } + + public PathInfo Name { get; } = "*inline"; + } +} \ No newline at end of file diff --git a/source/Handlebars/Extensions/EnumerableExtensions.cs b/source/Handlebars/Extensions/EnumerableExtensions.cs index 7aa71667..efa39315 100644 --- a/source/Handlebars/Extensions/EnumerableExtensions.cs +++ b/source/Handlebars/Extensions/EnumerableExtensions.cs @@ -40,6 +40,18 @@ public static IEnumerable ApplyOn(this IEnumerable source, Action Append(this TEnumerable source, T item) + where TEnumerable: IEnumerable + { + using var enumerator = source.GetEnumerator(); + while (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + + yield return item; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddOrUpdate(this IDictionary to, TK at, Func add, Action update, TO context) diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index ebb5fa27..14e45e88 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -1,3 +1,4 @@ +using HandlebarsDotNet.Decorators; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.Runtime; @@ -13,14 +14,14 @@ internal class BuildInHelpersFeatureFactory : IFeatureFactory internal class BuildInHelpersFeature : IFeature { private static readonly WithBlockHelperDescriptor WithBlockHelperDescriptor = new WithBlockHelperDescriptor(); - private static readonly InlineBlockHelperDescriptor InlineBlockHelperDescriptor = new InlineBlockHelperDescriptor(); private static readonly LookupReturnHelperDescriptor LookupReturnHelperDescriptor = new LookupReturnHelperDescriptor(); + private static readonly InlineBlockDecoratorDescriptor InlineBlockHelperDescriptor = new InlineBlockDecoratorDescriptor(); public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { configuration.BlockHelpers["with"] = new Ref>(WithBlockHelperDescriptor); - configuration.BlockHelpers["*inline"] = new Ref>(InlineBlockHelperDescriptor); configuration.Helpers["lookup"] = new Ref>(LookupReturnHelperDescriptor); + configuration.BlockDecorators["*inline"] = new Ref>(InlineBlockHelperDescriptor); } public void CompilationCompleted() diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index 9d7b62cf..023ee1dd 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -2,60 +2,11 @@ using System.Collections.Concurrent; using System.IO; using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Decorators; using HandlebarsDotNet.Helpers; namespace HandlebarsDotNet { - /// - /// InlineHelper: {{#helper arg1 arg2}} - /// - /// - /// - /// - public delegate void HandlebarsHelper(EncodedTextWriter output, Context context, Arguments arguments); - - /// - /// InlineHelper: {{#helper arg1 arg2}} - /// - /// - /// - /// - /// - public delegate void HandlebarsHelperWithOptions(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments); - - /// - /// InlineHelper: {{#helper arg1 arg2}}, supports value return - /// - /// - /// - public delegate object HandlebarsReturnHelper(Context context, Arguments arguments); - - /// - /// InlineHelper: {{#helper arg1 arg2}}, supports value return - /// - /// - /// - /// - public delegate object HandlebarsReturnWithOptionsHelper(in HelperOptions options, in Context context, in Arguments arguments); - - /// - /// BlockHelper: {{#helper}}..{{/helper}} - /// - /// - /// - /// - /// - public delegate void HandlebarsBlockHelper(EncodedTextWriter output, BlockHelperOptions options, Context context, Arguments arguments); - - /// - /// BlockHelper: {{#helper}}..{{/helper}} - /// - /// - /// - /// - public delegate object HandlebarsReturnBlockHelper(BlockHelperOptions options, Context context, Arguments arguments); - - public sealed class Handlebars { // Lazy-load Handlebars environment to ensure thread safety. See Jon Skeet's excellent article on this for more info. http://csharpindepth.com/Articles/General/Singleton.aspx @@ -198,6 +149,26 @@ public static void RegisterHelper(IHelperDescriptor helperOb { Instance.RegisterHelper(helperObject); } + + public void RegisterDecorator(string helperName, HandlebarsBlockDecorator helperFunction) + { + Instance.RegisterDecorator(helperName, helperFunction); + } + + public void RegisterDecorator(string helperName, HandlebarsDecorator helperFunction) + { + Instance.RegisterDecorator(helperName, helperFunction); + } + + public void RegisterDecorator(string helperName, HandlebarsBlockDecoratorVoid helperFunction) + { + Instance.RegisterDecorator(helperName, helperFunction); + } + + public void RegisterDecorator(string helperName, HandlebarsDecoratorVoid helperFunction) + { + Instance.RegisterDecorator(helperName, helperFunction); + } /// /// Expose the configuration in order to have access in all Helpers and Templates. diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 41d33494..3b854e6b 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -53,10 +53,15 @@ + + + + + diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index f06b6bea..88e6414a 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Decorators; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.IO; @@ -197,49 +199,39 @@ public void RegisterTemplate(string templateName, string template) RegisterTemplate(templateName, Compile(reader)); } - public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) + public void RegisterDecorator(string helperName, HandlebarsBlockDecorator helperFunction) { - Configuration.Helpers[helperName] = new DelegateHelperDescriptor(helperName, helperFunction); + Configuration.BlockDecorators[$"*{helperName}"] = new DelegateBlockDecoratorDescriptor(helperName, helperFunction); } - - public void RegisterHelper(string helperName, HandlebarsHelperWithOptions helperFunction) - { - Configuration.Helpers[helperName] = new DelegateHelperWithOptionsDescriptor(helperName, helperFunction); - } - - public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + + public void RegisterDecorator(string helperName, HandlebarsDecorator helperFunction) { - Configuration.Helpers[helperName] = new DelegateReturnHelperDescriptor(helperName, helperFunction); + Configuration.Decorators[$"*{helperName}"] = new DelegateDecoratorDescriptor(helperName, helperFunction); } - public void RegisterHelper(string helperName, HandlebarsReturnWithOptionsHelper helperFunction) + public void RegisterDecorator(string helperName, HandlebarsBlockDecoratorVoid helperFunction) { - Configuration.Helpers[helperName] = new DelegateReturnHelperWithOptionsDescriptor(helperName, helperFunction); + Configuration.BlockDecorators[$"*{helperName}"] = new DelegateBlockDecoratorVoidDescriptor(helperName, helperFunction); } - public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) - { - Configuration.BlockHelpers[helperName] = new DelegateBlockHelperDescriptor(helperName, helperFunction); - } - - public void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction) + public void RegisterDecorator(string helperName, HandlebarsDecoratorVoid helperFunction) { - Configuration.BlockHelpers[helperName] = new DelegateReturnBlockHelperDescriptor(helperName, helperFunction); + Configuration.Decorators[$"*{helperName}"] = new DelegateDecoratorVoidDescriptor(helperName, helperFunction); } - public void RegisterHelper(IHelperDescriptor helperObject) + public DisposableContainer Configure() { - Configuration.BlockHelpers[helperObject.Name] = helperObject; + return AmbientContext.Use(_ambientContext); } - - public void RegisterHelper(IHelperDescriptor helperObject) + + public IIndexed> GetHelpers() { - Configuration.Helpers[helperObject.Name] = helperObject; + return Configuration.Helpers; } - public DisposableContainer Configure() + public IIndexed> GetBlockHelpers() { - return AmbientContext.Use(_ambientContext); + return Configuration.BlockHelpers; } } } diff --git a/source/Handlebars/HandlebarsExtensions.cs b/source/Handlebars/HandlebarsExtensions.cs index bf1c92bb..9411b4c5 100644 --- a/source/Handlebars/HandlebarsExtensions.cs +++ b/source/Handlebars/HandlebarsExtensions.cs @@ -24,11 +24,20 @@ public static void WriteSafeString(this in EncodedTextWriter writer, object valu { if (value is string str) { - writer.Write(str, false); + writer.WriteSafeString(str); return; } - - writer.Write(value.ToString(), false); + + var current = writer.SuppressEncoding; + try + { + writer.SuppressEncoding = true; + writer.Write(value); + } + finally + { + writer.SuppressEncoding = current; + } } /// diff --git a/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs deleted file mode 100644 index 0594fba4..00000000 --- a/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using HandlebarsDotNet.PathStructure; - -namespace HandlebarsDotNet.Helpers.BlockHelpers -{ - internal sealed class InlineBlockHelperDescriptor : IHelperDescriptor - { - public PathInfo Name { get; } = "*inline"; - - public object Invoke(in BlockHelperOptions options, in Context context, in Arguments arguments) - { - return this.ReturnInvoke(options, context, arguments); - } - - public void Invoke(in EncodedTextWriter output, in BlockHelperOptions options, in Context context, in Arguments arguments) - { - if (arguments.Length != 1) - { - throw new HandlebarsException("{{*inline}} helper must have exactly one argument"); - } - - //This helper needs the "context" var to be the complete BindingContext as opposed to just the - //data { firstName: "todd" }. The full BindingContext is needed for registering the partial templates. - //This magic happens in BlockHelperFunctionBinder.VisitBlockHelperExpression - - if (!(context.Value is BindingContext bindingContext)) - { - throw new HandlebarsException("{{*inline}} helper must receiving the full BindingContext"); - } - - if(!(arguments[0] is string key)) throw new HandlebarsRuntimeException("Inline argument is not valid"); - - //Inline partials cannot use the Handlebars.RegisterTemplate method - //because it is static and therefore app-wide. To prevent collisions - //this helper will add the compiled partial to a dicionary - //that is passed around in the context without fear of collisions. - var template = options.OriginalTemplate; - bindingContext.InlinePartialTemplates.AddOrReplace(key, (writer, c) => template(writer, c)); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs index 3de0d6f1..96ae9790 100644 --- a/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs @@ -16,6 +16,12 @@ public object Invoke(in BlockHelperOptions options, in Context context, in Argum public void Invoke(in EncodedTextWriter output, in BlockHelperOptions options, in Context context, in Arguments arguments) { + if(options.Frame.BlockHelpers.TryGetValue(Name, out var contextHelper)) + { + contextHelper.Invoke(options, context, arguments); + return; + } + // TODO: add cache var configuration = options.Frame.Configuration; var helperResolvers = (ObservableList) configuration.HelperResolvers; diff --git a/source/Handlebars/Helpers/IHelperDescriptor.cs b/source/Handlebars/Helpers/IHelperDescriptor.cs index ef086d6b..cabdd601 100644 --- a/source/Handlebars/Helpers/IHelperDescriptor.cs +++ b/source/Handlebars/Helpers/IHelperDescriptor.cs @@ -6,8 +6,8 @@ public interface IHelperDescriptor { PathInfo Name { get; } } - - public interface IHelperDescriptor : IHelperDescriptor + + public interface IHelperDescriptor : IHelperDescriptor, IDescriptor where TOptions: struct, IHelperOptions { object Invoke(in TOptions options, in Context context, in Arguments arguments); diff --git a/source/Handlebars/Helpers/LateBindHelperDescriptor.cs b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs index f5ddb544..8230f93a 100644 --- a/source/Handlebars/Helpers/LateBindHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs @@ -12,6 +12,11 @@ public sealed class LateBindHelperDescriptor : IHelperDescriptor public object Invoke(in HelperOptions options, in Context context, in Arguments arguments) { var bindingContext = options.Frame; + + if(options.Frame.Helpers.TryGetValue(Name, out var contextHelper)) + { + return contextHelper.Invoke(options, context, arguments); + } // TODO: add cache var configuration = options.Frame.Configuration; diff --git a/source/Handlebars/IDescriptor.cs b/source/Handlebars/IDescriptor.cs new file mode 100644 index 00000000..baad4662 --- /dev/null +++ b/source/Handlebars/IDescriptor.cs @@ -0,0 +1,8 @@ +namespace HandlebarsDotNet +{ + public interface IDescriptor + where TOptions: struct, IOptions + { + + } +} \ No newline at end of file diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 52ce3783..75d59bf8 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -1,5 +1,6 @@ using System.IO; using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.Runtime; namespace HandlebarsDotNet @@ -16,7 +17,7 @@ public delegate void HandlebarsTemplate(TWrit /// /// /// - public interface IHandlebars + public interface IHandlebars : IHelpersRegistry { /// /// @@ -36,22 +37,14 @@ public interface IHandlebars void RegisterTemplate(string templateName, HandlebarsTemplate template); void RegisterTemplate(string templateName, string template); - - void RegisterHelper(string helperName, HandlebarsHelper helperFunction); - - void RegisterHelper(string helperName, HandlebarsHelperWithOptions helperFunction); - - void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction); - - void RegisterHelper(string helperName, HandlebarsReturnWithOptionsHelper helperFunction); - void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction); + void RegisterDecorator(string helperName, HandlebarsBlockDecorator helperFunction); - void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction); + void RegisterDecorator(string helperName, HandlebarsDecorator helperFunction); - void RegisterHelper(IHelperDescriptor helperObject); + void RegisterDecorator(string helperName, HandlebarsBlockDecoratorVoid helperFunction); - void RegisterHelper(IHelperDescriptor helperObject); + void RegisterDecorator(string helperName, HandlebarsDecoratorVoid helperFunction); /// /// Defines current environment configuration scope. diff --git a/source/Handlebars/IHelperOptions.cs b/source/Handlebars/IHelperOptions.cs index ed3e19ab..25565744 100644 --- a/source/Handlebars/IHelperOptions.cs +++ b/source/Handlebars/IHelperOptions.cs @@ -4,9 +4,8 @@ namespace HandlebarsDotNet { - public interface IHelperOptions + public interface IHelperOptions : IOptions { - BindingContext Frame { get; } DataValues Data { get; } PathInfo Name { get; } } diff --git a/source/Handlebars/IHelpersRegistry.cs b/source/Handlebars/IHelpersRegistry.cs new file mode 100644 index 00000000..2049ddc8 --- /dev/null +++ b/source/Handlebars/IHelpersRegistry.cs @@ -0,0 +1,63 @@ +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; + +namespace HandlebarsDotNet +{ + public interface IHelpersRegistry + { + IIndexed> GetHelpers(); + IIndexed> GetBlockHelpers(); + } + + public static class HelpersRegistryExtensions + { + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsHelper helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetHelpers()[helperName] = new DelegateHelperDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsHelperWithOptions helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetHelpers()[helperName] = new DelegateHelperWithOptionsDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsReturnHelper helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetHelpers()[helperName] = new DelegateReturnHelperDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsReturnWithOptionsHelper helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetHelpers()[helperName] = new DelegateReturnHelperWithOptionsDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, IHelperDescriptor helperObject) + where TRegistry : IHelpersRegistry + { + registry.GetHelpers()[helperObject.Name] = helperObject; + } + + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsBlockHelper helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetBlockHelpers()[helperName] = new DelegateBlockHelperDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, string helperName, HandlebarsReturnBlockHelper helperFunction) + where TRegistry : IHelpersRegistry + { + registry.GetBlockHelpers()[helperName] = new DelegateReturnBlockHelperDescriptor(helperName, helperFunction); + } + + public static void RegisterHelper(this TRegistry registry, IHelperDescriptor helperObject) + where TRegistry : IHelpersRegistry + { + registry.GetBlockHelpers()[helperObject.Name] = helperObject; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/IOptions.cs b/source/Handlebars/IOptions.cs new file mode 100644 index 00000000..90d2852e --- /dev/null +++ b/source/Handlebars/IOptions.cs @@ -0,0 +1,7 @@ +namespace HandlebarsDotNet +{ + public interface IOptions + { + BindingContext Frame { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Pools/BindingContext.Pool.cs b/source/Handlebars/Pools/BindingContext.Pool.cs index 676bc093..a5036505 100644 --- a/source/Handlebars/Pools/BindingContext.Pool.cs +++ b/source/Handlebars/Pools/BindingContext.Pool.cs @@ -50,6 +50,8 @@ public bool Return(BindingContext item) item.ParentContext = null; item.PartialBlockTemplate = null; item.InlinePartialTemplates.Clear(); + item.Helpers.Clear(); + item.BlockHelpers.Clear(); item.Bag.Clear(); item.BlockParamsObject.OptionalClear(); diff --git a/source/Handlebars/Pools/ClosureBuilder.Pool.cs b/source/Handlebars/Pools/ClosureBuilder.Pool.cs new file mode 100644 index 00000000..8d7ee5ff --- /dev/null +++ b/source/Handlebars/Pools/ClosureBuilder.Pool.cs @@ -0,0 +1,42 @@ +using System; +using HandlebarsDotNet.Pools; + +namespace HandlebarsDotNet.Compiler +{ + public partial class ClosureBuilder : IDisposable + { + private static readonly ClosureBuilderPool Pool = new (new Policy()); + + private ClosureBuilder() { } + + public static ClosureBuilder Create() => Pool.Get(); + + public void Dispose() + { + _pathInfos.Clear(); + _templateDelegates.Clear(); + _decoratorDelegates.Clear(); + _blockParams.Clear(); + _helpers.Clear(); + _blockHelpers.Clear(); + _decorators.Clear(); + _blockDecorators.Clear(); + _other.Clear(); + + Pool.Return(this); + } + + private sealed class ClosureBuilderPool : InternalObjectPool + { + public ClosureBuilderPool(Policy policy) : base(policy) + { + } + } + + private readonly struct Policy : IInternalObjectPoolPolicy + { + public ClosureBuilder Create() => new (); + public bool Return(ClosureBuilder item) => true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Pools/GenericObjectPool.cs b/source/Handlebars/Pools/GenericObjectPool.cs new file mode 100644 index 00000000..05f4ab4b --- /dev/null +++ b/source/Handlebars/Pools/GenericObjectPool.cs @@ -0,0 +1,19 @@ +using System; + +namespace HandlebarsDotNet.Pools +{ + internal class GenericObjectPool : InternalObjectPool.Policy> + where T : class, new() + { + private static readonly Lazy> Instance = new(() => new GenericObjectPool()); + public static GenericObjectPool Shared => Instance.Value; + + private GenericObjectPool() : base(new Policy()) { } + + public readonly struct Policy : IInternalObjectPoolPolicy + { + public T Create() => new (); + public bool Return(T item) => true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Runtime/Ref.cs b/source/Handlebars/Runtime/Ref.cs index 1033a738..26285a4e 100644 --- a/source/Handlebars/Runtime/Ref.cs +++ b/source/Handlebars/Runtime/Ref.cs @@ -2,30 +2,18 @@ namespace HandlebarsDotNet.Runtime { - public class Ref where T: class + public sealed class Ref where T: class { - private readonly Ref _parent; - private T _value; - + public Ref() { } + + public Ref(T value) => Value = value; + public T Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _value ?? _parent?.Value; - + get; [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _value = value; - } - - public Ref() { } - - public Ref(T value) => _value = value; - - public Ref(Ref parent) => _parent = parent; - - public Ref(T value, Ref parent) - { - _value = value; - _parent = parent; + set; } } } \ No newline at end of file diff --git a/source/Handlebars/_Delegates.cs b/source/Handlebars/_Delegates.cs new file mode 100644 index 00000000..350ac210 --- /dev/null +++ b/source/Handlebars/_Delegates.cs @@ -0,0 +1,61 @@ +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet +{ + /// + /// InlineHelper: {{#helper arg1 arg2}} + /// + /// + /// + /// + public delegate void HandlebarsHelper(EncodedTextWriter output, Context context, Arguments arguments); + + /// + /// InlineHelper: {{#helper arg1 arg2}} + /// + /// + /// + /// + /// + public delegate void HandlebarsHelperWithOptions(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments); + + /// + /// InlineHelper: {{#helper arg1 arg2}}, supports value return + /// + /// + /// + public delegate object HandlebarsReturnHelper(Context context, Arguments arguments); + + /// + /// InlineHelper: {{#helper arg1 arg2}}, supports value return + /// + /// + /// + /// + public delegate object HandlebarsReturnWithOptionsHelper(in HelperOptions options, in Context context, in Arguments arguments); + + /// + /// BlockHelper: {{#helper}}..{{/helper}} + /// + /// + /// + /// + /// + public delegate void HandlebarsBlockHelper(EncodedTextWriter output, BlockHelperOptions options, Context context, Arguments arguments); + + /// + /// BlockHelper: {{#helper}}..{{/helper}} + /// + /// + /// + /// + public delegate object HandlebarsReturnBlockHelper(BlockHelperOptions options, Context context, Arguments arguments); + + public delegate TemplateDelegate HandlebarsBlockDecorator(TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments); + + public delegate TemplateDelegate HandlebarsDecorator(TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments); + + public delegate void HandlebarsBlockDecoratorVoid(TemplateDelegate function, in BlockDecoratorOptions options, in Context context, in Arguments arguments); + + public delegate void HandlebarsDecoratorVoid(TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments); +} \ No newline at end of file From 5f4bf5035c3ae8567858967a6445d1f69485afc0 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 23 Jan 2022 20:24:48 -0800 Subject: [PATCH 2/3] Update README --- README.md | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) 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 From c127e41b0b4d80979f99bbafd6b3567a5d66ce4d Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 23 Jan 2022 20:40:17 -0800 Subject: [PATCH 3/3] Fix memory leak --- .../ClosureExpressionMiddleware.cs | 21 +++++++++++-------- source/Handlebars/Pools/GenericObjectPool.cs | 9 +++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs index 0f37865d..0b1c297a 100644 --- a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs +++ b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs @@ -11,22 +11,25 @@ internal class ClosureExpressionMiddleware : IExpressionMiddleware { public Expression Invoke(Expression expression) where T : Delegate { - using var container = GenericObjectPool>.Shared.Use(); - var constants = container.Value; + var constants = new List(); var closureCollectorVisitor = new ClosureCollectorVisitor(constants); expression = (Expression) closureCollectorVisitor.Visit(expression); if (constants.Count == 0) return expression; - - using var closureBuilder = ClosureBuilder.Create(); - 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/Pools/GenericObjectPool.cs b/source/Handlebars/Pools/GenericObjectPool.cs index 05f4ab4b..f7d0921e 100644 --- a/source/Handlebars/Pools/GenericObjectPool.cs +++ b/source/Handlebars/Pools/GenericObjectPool.cs @@ -1,13 +1,10 @@ -using System; - -namespace HandlebarsDotNet.Pools +namespace HandlebarsDotNet.Pools { internal class GenericObjectPool : InternalObjectPool.Policy> where T : class, new() { - private static readonly Lazy> Instance = new(() => new GenericObjectPool()); - public static GenericObjectPool Shared => Instance.Value; - + public static GenericObjectPool Shared { get; } = new(); + private GenericObjectPool() : base(new Policy()) { } public readonly struct Policy : IInternalObjectPoolPolicy