From e98b3abc27692962b79e8f0d42706d9f2e491fba Mon Sep 17 00:00:00 2001 From: Nikita Kotlyarov Date: Sun, 26 Dec 2021 15:53:49 +0400 Subject: [PATCH 01/37] Add checks for loose closing blocks Check loose closing blocks on compile time --- source/Handlebars.Test/ExceptionTests.cs | 47 +++++++++++++++- .../Lexer/Converter/BlockAccumulator.cs | 12 ++--- .../BlockAccumulatorContext.cs | 54 ++++++++++++++++++- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/source/Handlebars.Test/ExceptionTests.cs b/source/Handlebars.Test/ExceptionTests.cs index 5aa3f6f0..434dc999 100644 --- a/source/Handlebars.Test/ExceptionTests.cs +++ b/source/Handlebars.Test/ExceptionTests.cs @@ -12,5 +12,50 @@ public void TestNonClosingBlockExpressionException() Handlebars.Compile("{{#if 0}}test")(new { }); }); } - } + + [Fact] + public void TestLooseClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 0}}test{{/if}}{{/unless}}")(new { }); + }); + } + + [Fact] + public void TestNestedLooseClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 1}}{{#unless 0}}test{{/if}}{{/unless}}{{/if}}")(new { }); + }); + } + + [Fact] + public void TestUnmatchedClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 0}}test{{/unless}}")(new { }); + }); + } + + [Fact] + public void TestLooseClosingBlockInIteratorExpressionException() + { + var data = new + { + enumerateMe = new + { + foo = "hello", + bar = "world" + } + }; + + Assert.Throws(() => + { + Handlebars.Compile("{{#each enumerateMe}}test{{/if}}{{/each}}")(data); + }); + } + } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs index d252974e..40068729 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -27,10 +26,10 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (enumerator.MoveNext()) { var item = (Expression)enumerator.Current; - var context = BlockAccumulatorContext.Create(item, _configuration); + var context = BlockAccumulatorContext.Create(item, null, _configuration); if (context != null) { - yield return AccumulateBlock(enumerator, context); + yield return AccumulateBlock(item, enumerator, context); } else { @@ -40,16 +39,17 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) } private Expression AccumulateBlock( + Expression parentItem, IEnumerator enumerator, BlockAccumulatorContext context) { while (enumerator.MoveNext()) { var item = (Expression)enumerator.Current; - var innerContext = BlockAccumulatorContext.Create(item, _configuration); + var innerContext = BlockAccumulatorContext.Create(item, parentItem, _configuration); if (innerContext != null) { - context.HandleElement(AccumulateBlock(enumerator, innerContext)); + context.HandleElement(AccumulateBlock(item, enumerator, innerContext)); } else if (context.IsClosingElement(item)) { diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 195e8c6a..fc906161 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -10,7 +10,7 @@ internal abstract class BlockAccumulatorContext private static readonly HashSet ConditionHelpers = new HashSet(StringComparer.OrdinalIgnoreCase){ "#if", "#unless", "^if", "^unless" }; private static readonly HashSet IteratorHelpers = new HashSet(StringComparer.OrdinalIgnoreCase){ "#each", "^each" }; - public static BlockAccumulatorContext Create(Expression item, ICompiledHandlebarsConfiguration configuration) + public static BlockAccumulatorContext Create(Expression item, Expression parentItem, ICompiledHandlebarsConfiguration configuration) { BlockAccumulatorContext context = null; if (IsConditionalBlock(item)) @@ -29,6 +29,10 @@ public static BlockAccumulatorContext Create(Expression item, ICompiledHandlebar { context = new BlockHelperAccumulatorContext(item); } + else if (IsLooseClosingElement(item, parentItem, out var looseBlockName)) + { + throw new HandlebarsCompilerException($"Loose closing block '{looseBlockName}' was found"); + } return context; } @@ -77,6 +81,54 @@ private static bool IsPartialBlock (Expression item) } } + private static bool IsLooseClosingElement(Expression item, Expression parentItem, out string looseBlockName) + { + looseBlockName = null; + + var itemBlockName = GetBlockName(item); + + if (itemBlockName == null) return false; + + var parentBlockName = GetBlockName(parentItem); + + if (!itemBlockName.StartsWith("/")) return false; + + if (parentBlockName == null || IsClosingBlockNotMatchParentBlock(itemBlockName, parentBlockName)) + { + looseBlockName = itemBlockName; + + return true; + } + + return false; + } + + private static bool IsClosingBlockNotMatchParentBlock(string itemBlockName, string parentBlockName) + { + if (itemBlockName == null) throw new ArgumentNullException(nameof(itemBlockName)); + if (parentBlockName == null) throw new ArgumentNullException(nameof(parentBlockName)); + + if (!parentBlockName.StartsWith("#") || parentBlockName.StartsWith("#>") || parentBlockName.StartsWith("#*")) return false; + + return parentBlockName.Substring(1) != itemBlockName.Substring(1); + } + + private static string GetBlockName(Expression item) + { + item = UnwrapStatement(item); + switch( item ) + { + case PathExpression pathExpression: + return pathExpression.Path; + + case HelperExpression helperExpression: + return helperExpression.HelperName; + + default: + return null; + } + } + protected static Expression UnwrapStatement(Expression item) { if (item is StatementExpression expression) From a1d3683da7775018cea2177f87939a2c03f1777c Mon Sep 17 00:00:00 2001 From: Nikita Kotlyarov Date: Wed, 29 Dec 2021 10:50:00 +0400 Subject: [PATCH 02/37] Improve performance for substrings + fix code style --- .../BlockAccumulatorContext.cs | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index fc906161..4314b379 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.StringUtils; namespace HandlebarsDotNet.Compiler { @@ -68,17 +69,12 @@ private static bool IsIteratorBlock(Expression item) private static bool IsPartialBlock (Expression item) { item = UnwrapStatement (item); - switch (item) + return item switch { - case PathExpression expression: - return expression.Path.StartsWith("#>"); - - case HelperExpression helperExpression: - return helperExpression.HelperName.StartsWith("#>"); - - default: - return false; - } + PathExpression expression => expression.Path.StartsWith("#>"), + HelperExpression helperExpression => helperExpression.HelperName.StartsWith("#>"), + _ => false, + }; } private static bool IsLooseClosingElement(Expression item, Expression parentItem, out string looseBlockName) @@ -110,23 +106,18 @@ private static bool IsClosingBlockNotMatchParentBlock(string itemBlockName, stri if (!parentBlockName.StartsWith("#") || parentBlockName.StartsWith("#>") || parentBlockName.StartsWith("#*")) return false; - return parentBlockName.Substring(1) != itemBlockName.Substring(1); + return new Substring(parentBlockName, 1) != new Substring(itemBlockName, 1); } private static string GetBlockName(Expression item) { item = UnwrapStatement(item); - switch( item ) + return item switch { - case PathExpression pathExpression: - return pathExpression.Path; - - case HelperExpression helperExpression: - return helperExpression.HelperName; - - default: - return null; - } + PathExpression pathExpression => pathExpression.Path, + HelperExpression helperExpression => helperExpression.HelperName, + _ => null, + }; } protected static Expression UnwrapStatement(Expression item) From d538b4cf7df2fb636cee3e18489d7887c96ee73c Mon Sep 17 00:00:00 2001 From: Nikita Kotlyarov Date: Thu, 30 Dec 2021 11:15:47 +0400 Subject: [PATCH 03/37] Update BlockAccumulatorContext.cs Update error message + make renaming --- .../BlockAccumulatorContext.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 4314b379..8ec0682e 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -30,9 +30,9 @@ public static BlockAccumulatorContext Create(Expression item, Expression parentI { context = new BlockHelperAccumulatorContext(item); } - else if (IsLooseClosingElement(item, parentItem, out var looseBlockName)) + else if (IsDetachedClosingElement(item, parentItem, out var closingElement)) { - throw new HandlebarsCompilerException($"Loose closing block '{looseBlockName}' was found"); + throw new HandlebarsCompilerException($"A closing element '{closingElement}' was found without a matching open element"); } return context; @@ -77,21 +77,21 @@ private static bool IsPartialBlock (Expression item) }; } - private static bool IsLooseClosingElement(Expression item, Expression parentItem, out string looseBlockName) + private static bool IsDetachedClosingElement(Expression item, Expression parentItem, out string closingElement) { - looseBlockName = null; + closingElement = null; - var itemBlockName = GetBlockName(item); + var itemElement = GetItemElement(item); - if (itemBlockName == null) return false; + if (itemElement == null) return false; - var parentBlockName = GetBlockName(parentItem); + var parentItemElement = GetItemElement(parentItem); - if (!itemBlockName.StartsWith("/")) return false; + if (!itemElement.StartsWith("/")) return false; - if (parentBlockName == null || IsClosingBlockNotMatchParentBlock(itemBlockName, parentBlockName)) + if (parentItemElement == null || IsClosingElementNotMatchOpenElement(itemElement, parentItemElement)) { - looseBlockName = itemBlockName; + closingElement = itemElement; return true; } @@ -99,17 +99,17 @@ private static bool IsLooseClosingElement(Expression item, Expression parentItem return false; } - private static bool IsClosingBlockNotMatchParentBlock(string itemBlockName, string parentBlockName) + private static bool IsClosingElementNotMatchOpenElement(string closingElement, string openElement) { - if (itemBlockName == null) throw new ArgumentNullException(nameof(itemBlockName)); - if (parentBlockName == null) throw new ArgumentNullException(nameof(parentBlockName)); + if (closingElement == null) throw new ArgumentNullException(nameof(closingElement)); + if (openElement == null) throw new ArgumentNullException(nameof(openElement)); - if (!parentBlockName.StartsWith("#") || parentBlockName.StartsWith("#>") || parentBlockName.StartsWith("#*")) return false; + if (!openElement.StartsWith("#") || openElement.StartsWith("#>") || openElement.StartsWith("#*")) return false; - return new Substring(parentBlockName, 1) != new Substring(itemBlockName, 1); + return new Substring(openElement, 1) != new Substring(closingElement, 1); } - private static string GetBlockName(Expression item) + private static string GetItemElement(Expression item) { item = UnwrapStatement(item); return item switch From c64ffc41943cc4fc2226861f3558ec575f3d1357 Mon Sep 17 00:00:00 2001 From: RomainHautefeuille Date: Wed, 5 Jan 2022 19:20:48 +0100 Subject: [PATCH 04/37] Remove duplicate AliasProviders initialization Already initialized a few lines later --- .../Handlebars/Configuration/HandlebarsConfigurationAdapter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs index 5f3d2d0c..15884a99 100644 --- a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs +++ b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs @@ -23,7 +23,6 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) { UnderlingConfiguration = configuration; - AliasProviders = new ObservableList(configuration.AliasProviders); HelperResolvers = new ObservableList(configuration.HelperResolvers); RegisteredTemplates = new ObservableIndex, StringEqualityComparer>(new StringEqualityComparer(StringComparison.OrdinalIgnoreCase), configuration.RegisteredTemplates); AliasProviders = new ObservableList(configuration.AliasProviders); @@ -137,4 +136,4 @@ private ObservableList CreateObjectDescriptorProvider return objectDescriptorProviders; } } -} \ No newline at end of file +} From d57003f1ac17764cdf6c159fe8533d2b88ffb821 Mon Sep 17 00:00:00 2001 From: Nikita Kotlyarov Date: Sun, 23 Jan 2022 12:43:26 +0400 Subject: [PATCH 05/37] Enable support for chained path iterators Fix issue #442 --- .../Handlebars.Test/BasicIntegrationTests.cs | 19 +++++++++++++++++++ source/Handlebars/PathStructure/PathInfo.cs | 1 - 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 00a4049f..621056cf 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -2044,6 +2044,25 @@ public void HtmlEncoderCompatibilityIntegration_LateChangeConfig(bool useLegacyH Assert.Equal(expected, actual); } + + [Fact] + public void ChainedPathIteratorHelper() + { + var context = new + { + bundles = new + { + styles = new + { + vendor = new[] { "a", "b", "c" } + } + } + }; + + var nestedObjectsHelperResult = Handlebars.Compile("{{#bundles.styles.vendor}}{{this}}{{/bundles.styles.vendor}}")(context); + + Assert.Equal("abc", nestedObjectsHelperResult); + } private class StringHelperResolver : IHelperResolver { diff --git a/source/Handlebars/PathStructure/PathInfo.cs b/source/Handlebars/PathStructure/PathInfo.cs index f39d344f..8dc57396 100644 --- a/source/Handlebars/PathStructure/PathInfo.cs +++ b/source/Handlebars/PathStructure/PathInfo.cs @@ -189,7 +189,6 @@ public static PathInfo Parse(string path) } var chainSegments = GetPathChain(segment); - if (chainSegments.Length > 1) isValidHelperLiteral = false; segments.Add(new PathSegment(segment, chainSegments)); } From 370a3a6f9db50353db498db260b0ce2f06571ddd Mon Sep 17 00:00:00 2001 From: Nikita Kotlyarov Date: Sun, 23 Jan 2022 13:02:24 +0400 Subject: [PATCH 06/37] Fix condition --- source/Handlebars/PathStructure/PathInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/Handlebars/PathStructure/PathInfo.cs b/source/Handlebars/PathStructure/PathInfo.cs index 8dc57396..06f08041 100644 --- a/source/Handlebars/PathStructure/PathInfo.cs +++ b/source/Handlebars/PathStructure/PathInfo.cs @@ -190,6 +190,8 @@ public static PathInfo Parse(string path) var chainSegments = GetPathChain(segment); + if (chainSegments.Length > 1 && pathType != PathType.BlockHelper) isValidHelperLiteral = false; + segments.Add(new PathSegment(segment, chainSegments)); } From 1bb797937e1785dd04fa8632c50b55dc79941442 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 23 Jan 2022 20:11:00 -0800 Subject: [PATCH 07/37] 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 08/37] 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 09/37] 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 From b1218133ca91c9c4d2b4770b60f2601f4550706a Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Thu, 3 Mar 2022 19:24:16 -0800 Subject: [PATCH 10/37] Use `windows-2019` instead of `windows-latest` Recently Github Actions switched to Windows 2022 as their `latest`. Unfortunately, it does not support netfx451 --- .github/workflows/ci.yml | 6 +++--- .github/workflows/pull_request.yml | 6 +++--- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b4db42..0330d4e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: build: name: Build - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@master - name: Setup dotnet @@ -27,7 +27,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ macos-latest, ubuntu-latest, windows-latest ] + os: [ macos-latest, ubuntu-latest, windows-2019 ] steps: - uses: actions/checkout@master - name: Setup dotnet 2.1 @@ -47,7 +47,7 @@ jobs: sonar-ci: name: SonarCloud - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ea3ff8b5..fae4b16c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,7 +7,7 @@ on: jobs: build: name: Build - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@master - name: Setup dotnet @@ -27,7 +27,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ macos-latest, ubuntu-latest, windows-latest ] + os: [ macos-latest, ubuntu-latest, windows-2019 ] steps: - uses: actions/checkout@master - name: Setup dotnet 2.1 @@ -47,7 +47,7 @@ jobs: sonar-pr: name: SonarCloud - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8bc70fa..f8fca10a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: publish: name: Publish - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@v2 From 2b95626fbbffa1ff15e1730b6b4ae14a59a206e8 Mon Sep 17 00:00:00 2001 From: periklis92 Date: Sat, 5 Mar 2022 21:34:39 +0200 Subject: [PATCH 11/37] Swapped expected and actual values in Assert.Equal --- source/Handlebars.Test/SubstringTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Handlebars.Test/SubstringTests.cs b/source/Handlebars.Test/SubstringTests.cs index 0f1b9e14..62662be5 100644 --- a/source/Handlebars.Test/SubstringTests.cs +++ b/source/Handlebars.Test/SubstringTests.cs @@ -37,7 +37,7 @@ public void Split(string input, char splitChar, string[] expected) var index = 0; while (split.MoveNext()) { - Assert.Equal(split.Current, expected[index++]); + Assert.Equal(expected[index++], split.Current) ; } } @@ -49,7 +49,7 @@ public void TrimStart(string input, char trimChar, string expected) var substring = Substring.TrimStart(input, trimChar); Assert.Equal(expected, substring.ToString()); - } + } [Theory] [InlineData("abc", 'c', "ab")] From 602339832bcbf903574d98c9087eefd5fef53bd2 Mon Sep 17 00:00:00 2001 From: periklis92 Date: Sat, 5 Mar 2022 21:35:04 +0200 Subject: [PATCH 12/37] Added Split substring test with remove empty options --- source/Handlebars.Test/SubstringTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/source/Handlebars.Test/SubstringTests.cs b/source/Handlebars.Test/SubstringTests.cs index 62662be5..ba0cd4af 100644 --- a/source/Handlebars.Test/SubstringTests.cs +++ b/source/Handlebars.Test/SubstringTests.cs @@ -40,6 +40,21 @@ public void Split(string input, char splitChar, string[] expected) Assert.Equal(expected[index++], split.Current) ; } } + + [Theory] + [InlineData("ab//bc", '/', new []{ "ab", "bc" })] + [InlineData("/a//c/d/e//", '/', new []{ "a", "c", "d", "e" })] + public void SplitRemoveEmpty(string input, char splitChar, string[] expected) + { + var substring = new Substring(input); + var split = Substring.Split(substring, splitChar, System.StringSplitOptions.RemoveEmptyEntries); + + var index = 0; + while (split.MoveNext()) + { + Assert.Equal(expected[index++], split.Current); + } + } [Theory] [InlineData("abc", 'a', "bc")] From 96365374ba2df18007f20ef32a7157cf9543e84a Mon Sep 17 00:00:00 2001 From: periklis92 Date: Sat, 5 Mar 2022 21:37:27 +0200 Subject: [PATCH 13/37] Fixed bug in SplitEnumerator that ommited last letter --- source/Handlebars/StringUtils/Substring.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/Handlebars/StringUtils/Substring.cs b/source/Handlebars/StringUtils/Substring.cs index c9b377d2..9f62d50d 100644 --- a/source/Handlebars/StringUtils/Substring.cs +++ b/source/Handlebars/StringUtils/Substring.cs @@ -271,11 +271,12 @@ public bool MoveNext() { var substringStart = _index; var substringLength = 0; - for (; _index < _substring.Length; _index++) + while (_index < _substring.Length) { if (_substring[_index] != _separator) { substringLength++; + _index++; continue; } From eeb92a364dab03cd5df418d93ca210f6def21ee6 Mon Sep 17 00:00:00 2001 From: periklis92 Date: Sat, 5 Mar 2022 22:43:41 +0200 Subject: [PATCH 14/37] Added a test that covers issue #500 --- source/Handlebars.Test/IssueTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 17f6bfa2..fd7ede77 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Dynamic; using System.IO; using System.Linq; @@ -677,5 +678,21 @@ public void ConfigNoEscapeHtmlCharsShouldNotBeEscapedAfterWritingTripleCurlyValu Assert.Equal(expected, actual); } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/500 + // Issue refers to the last letter being cut off when using + // keys set in context + [Fact] + public void LastLetterCutOff() + { + var context = ImmutableDictionary.Empty + .Add("Name", "abcd"); + + var template = "{{.Name}}"; + var compiledTemplate = Handlebars.Compile(template); + string templateOutput = compiledTemplate(context); + + Assert.Equal("abcd", templateOutput); + } } } \ No newline at end of file From 27ceabd998e905dc9df31144a86e655bdcdde0e6 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 9 Mar 2022 11:32:45 +0100 Subject: [PATCH 15/37] Update readme.md to include extra projects --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 92bb9642..ab6ca7a1 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ Handlebars.Net doesn't use a scripting engine to run a Javascript library - it * dotnet add package Handlebars.Net +## Extensions +The following projects are extending Handlebars.Net: +- [Handlebars.Net.Extension.Json](https://github.com/Handlebars-Net/Handlebars.Net.Extension.Json) (Adds `System.Text.Json.JsonDocument` support) +- [Handlebars.Net.Extension.NewtonsoftJson](https://github.com/Handlebars-Net/Handlebars.Net.Extension.NewtonsoftJson) (Adds `Newtonsoft.Json` support) +- [Handlebars.Net.Helpers](https://github.com/Handlebars-Net/Handlebars.Net.Helpers) (Additional helpers in the categories: 'Constants', 'Enumerable', 'Math', 'Regex', 'String', 'DateTime', 'Url' , 'DynamicLinq', 'Humanizer', 'Json', 'Random', 'Xeger' and 'XPath'.) + + ## Usage ```c# From 7052ed032708ae5be0248d022d6813e7ac403e14 Mon Sep 17 00:00:00 2001 From: Everett Grassler <45568848+Dragwar@users.noreply.github.com> Date: Wed, 23 Mar 2022 22:09:06 -0500 Subject: [PATCH 16/37] Allow System.String property access --- source/Handlebars.Test/IssueTests.cs | 14 +++++++++++++- .../ObjectDescriptorProvider.cs | 16 ++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index fd7ede77..143f751b 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -623,7 +623,19 @@ public void WeirdBehaviour() Assert.Equal(expected, actual1); Assert.Equal(expected, actual2); } - + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/457 + [Fact] + public void StringLength() + { + var handlebars = Handlebars.Create(); + var render = handlebars.Compile("{{str.length}}"); + object data = new { str = "string" }; + + var actual = render(data); + Assert.Equal("6", actual); + } + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/470 [Theory] [ClassData(typeof(EscapeExpressionGenerator))] diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs index 8c280fc7..f377d631 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -13,8 +13,6 @@ namespace HandlebarsDotNet.ObjectDescriptors { public sealed class ObjectDescriptorProvider : IObjectDescriptorProvider { - private static readonly Type StringType = typeof(string); - private readonly LookupSlim, ReferenceEqualityComparer> _membersCache = new LookupSlim, ReferenceEqualityComparer>(new ReferenceEqualityComparer()); private readonly ReflectionMemberAccessor _reflectionMemberAccessor; @@ -25,17 +23,11 @@ public ObjectDescriptorProvider(IReadOnlyList aliasProvide public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - if (type == StringType) - { - value = ObjectDescriptor.Empty; - return false; - } - value = new ObjectDescriptor( - type, - _reflectionMemberAccessor, - GetProperties, - self => new ObjectIterator(self), + type, + _reflectionMemberAccessor, + GetProperties, + self => new ObjectIterator(self), dependencies: _membersCache ); From 9eecb5a0243a8bafa30257d89da5a5a7b0c926bf Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 25 Mar 2022 22:40:13 -0700 Subject: [PATCH 17/37] Fix collision handling in`FixedSizeDictionary` --- .../Collections/FixedSizeDictionaryTests.cs | 25 ++++++++++++++++ .../Collections/FixedSizeDictionary.cs | 6 +++- source/Handlebars/Runtime/AmbientContext.cs | 30 +++++++------------ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs index 386d2d8b..f4cf7394 100644 --- a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs +++ b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs @@ -15,6 +15,31 @@ static FixedSizeDictionaryTests() FixedSizeDictionary = new FixedSizeDictionary>(15, 17, referenceEqualityComparer); } + [Fact] + public void AddOrReplace_Collisions() + { + var comparer = new CollisionsComparer(new Random().Next()); + var dictionary = new FixedSizeDictionary(16, 7, comparer); + for (var i = 0; i < dictionary.Capacity; i++) + { + dictionary.AddOrReplace(new object(), new object(), out _); + } + } + + private readonly struct CollisionsComparer : IEqualityComparer + { + private readonly int _hash; + + public CollisionsComparer(int hash) + { + _hash = hash; + } + + public bool Equals(object x, object y) => false; + + public int GetHashCode(object obj) => _hash; + } + [Fact] public void AddOrReplace() { diff --git a/source/Handlebars/Collections/FixedSizeDictionary.cs b/source/Handlebars/Collections/FixedSizeDictionary.cs index 97187667..b3c73689 100644 --- a/source/Handlebars/Collections/FixedSizeDictionary.cs +++ b/source/Handlebars/Collections/FixedSizeDictionary.cs @@ -288,6 +288,7 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde ref var entryReference = ref _entries[entry.Index]; entryIndex = entryReference.Index + 1; + var downstreamEntryIndex = entryIndex - 1; for (; entryIndex < _entries.Length; entryIndex++) { @@ -301,7 +302,10 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde return; } - entryIndex = (bucketIndex * _bucketMask) - 1; + // we've searched all entries in -> direction, now visiting in <- direction + entryIndex = downstreamEntryIndex - 1; + // handling special case when `downstreamEntryIndex` can result into value >= _entries.Length + if (entryIndex >= _entries.Length) entryIndex = _entries.Length - 1; for (; entryIndex >= 0; entryIndex--) { entry = _entries[entryIndex]; diff --git a/source/Handlebars/Runtime/AmbientContext.cs b/source/Handlebars/Runtime/AmbientContext.cs index 561e6e80..70a0c89b 100644 --- a/source/Handlebars/Runtime/AmbientContext.cs +++ b/source/Handlebars/Runtime/AmbientContext.cs @@ -1,12 +1,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -#if !NET451 && !NET452 -using System.Threading; -#else -using HandlebarsDotNet.Polyfills; -#endif -using HandlebarsDotNet.Collections; using HandlebarsDotNet.IO; using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.PathStructure; @@ -16,14 +10,18 @@ namespace HandlebarsDotNet.Runtime { public sealed class AmbientContext : IDisposable { - private static readonly InternalObjectPool Pool = new InternalObjectPool(new Policy()); + private static readonly InternalObjectPool Pool = new(new Policy()); - private static readonly AsyncLocal> Local = new AsyncLocal>(); + [ThreadStatic] + private static Stack _local; + + private static Stack Local => + _local ??= new Stack(); public static AmbientContext Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Local.Value.Peek(); + get => Local.Count > 0 ? Local.Peek() : null; } public static AmbientContext Create( @@ -67,12 +65,9 @@ public static AmbientContext Create( public static DisposableContainer Use(AmbientContext ambientContext) { - Local.Value = Local.Value.Push(ambientContext); + Local.Push(ambientContext); - return new DisposableContainer(() => - { - Local.Value = Local.Value.Pop(out _); - }); + return new DisposableContainer(() => Local.Pop()); } private AmbientContext() @@ -89,14 +84,11 @@ private AmbientContext() public ObjectDescriptorFactory ObjectDescriptorFactory { get; private set; } - public Dictionary Bag { get; } = new Dictionary(); + public Dictionary Bag { get; } = new(); private struct Policy : IInternalObjectPoolPolicy { - public AmbientContext Create() - { - return new AmbientContext(); - } + public AmbientContext Create() => new(); public bool Return(AmbientContext item) { From a69939f09760cb2a0748495c6bb3992248c54bb0 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 25 Mar 2022 23:53:09 -0700 Subject: [PATCH 18/37] Introduce `SharedEnvironment` #513 --- README.md | 50 ++++++++++++++ .../Handlebars.Test/BasicIntegrationTests.cs | 43 +++++++++++- source/Handlebars.Test/Handlebars.Test.csproj | 2 +- .../Handlebars.Test/HandlebarsEnvGenerator.cs | 1 + source/Handlebars/Collections/CascadeIndex.cs | 66 ++++++++++++------- .../HandlebarsConfigurationAdapter.cs | 14 ++-- ...InfoLight.PathInfoLightEqualityComparer.cs | 2 +- source/Handlebars/Handlebars.cs | 15 ++++- source/Handlebars/HandlebarsEnvironment.cs | 12 +++- source/Handlebars/IHandlebars.cs | 8 +++ source/Handlebars/PathInfoLight.cs | 4 +- 11 files changed, 175 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index ab6ca7a1..aa05f778 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,56 @@ public void DateTimeFormatter(IHandlebars handlebars) #### Notes - Formatters are resolved in reverse order according to registration. If multiple providers can provide formatter for a type the last registered would be used. +### Shared environment + +By default Handlebars will create standalone copy of environment for each compiled template. This is done in order to eliminate a chance of altering behavior of one template from inside of other one. + +Unfortunately, in case runtime has a lot of compiled templates (regardless of the template size) it may have significant memory footprint. This can be solved by using `SharedEnvironment`. + +Templates compiled in `SharedEnvironment` will share the same configuration. + +#### Limitations + +Only runtime configuration properties can be changed after the shared environment has been created. Changes to `Configuration.CompileTimeConfiguration` and other compile-time properties will have no effect. + +#### Example + +```c# +[Fact] +public void BasicSharedEnvironment() +{ + var handlebars = Handlebars.CreateSharedEnvironment(); + handlebars.RegisterHelper("registerLateHelper", + (in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) => + { + var configuration = options.Frame + .GetType() + .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)? + .GetValue(options.Frame) as ICompiledHandlebarsConfiguration; + + var helpers = configuration?.Helpers; + + const string name = "lateHelper"; + if (helpers?.TryGetValue(name, out var @ref) ?? false) + { + @ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42); + } + }); + + var _0_template = "{{registerLateHelper}}"; + var _0 = handlebars.Compile(_0_template); + var _1_template = "{{lateHelper}}"; + var _1 = handlebars.Compile(_1_template); + + var result = _1(null); + Assert.Equal("", result); // `lateHelper` is not registered yet + + _0(null); + result = _1(null); + Assert.Equal("42", result); +} +``` + ### Compatibility feature toggles Compatibility feature toggles defines a set of settings responsible for controlling compilation/rendering behavior. Each of those settings would enable certain feature that would break compatibility with canonical Handlebars. diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 2e3f0842..ea076246 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -12,6 +12,7 @@ using HandlebarsDotNet.Features; using HandlebarsDotNet.IO; using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.Runtime; using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Test @@ -56,6 +57,42 @@ public void BasicPath(IHandlebars handlebars) var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } + + [Fact] + public void BasicSharedEnvironment() + { + var handlebars = Handlebars.CreateSharedEnvironment(); + handlebars.RegisterHelper("registerLateHelper", + (in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) => + { + var configuration = options.Frame + .GetType() + .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)? + .GetValue(options.Frame) as ICompiledHandlebarsConfiguration; + + if(configuration == null) return; + + var helpers = configuration.Helpers; + + const string name = "lateHelper"; + if (helpers.TryGetValue(name, out var @ref)) + { + @ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42); + } + }); + + var _0_template = "{{registerLateHelper}}"; + var _0 = handlebars.Compile(_0_template); + var _1_template = "{{lateHelper}}"; + var _1 = handlebars.Compile(_1_template); + + var result = _1(null); + Assert.Equal("", result); // `lateHelper` is not registered yet + + _0(null); + result = _1(null); + Assert.Equal("42", result); + } [Theory] [ClassData(typeof(HandlebarsEnvGenerator))] @@ -110,7 +147,7 @@ public void PathUnresolvedBindingFormatter(IHandlebars handlebars) } [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void CustcomDateTimeFormat(IHandlebars handlebars) + public void CustomDateTimeFormat(IHandlebars handlebars) { var source = "{{now}}"; @@ -420,6 +457,8 @@ public void BasicPropertyOnArray(IHandlebars handlebars) [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void AliasedPropertyOnArray(IHandlebars handlebars) { + if(handlebars.IsSharedEnvironment) return; + var source = "Array is {{ names.count }} item(s) long"; handlebars.Configuration.UseCollectionMemberAliasProvider(); var template = handlebars.Compile(source); @@ -452,6 +491,8 @@ public void CustomAliasedPropertyOnArray(IHandlebars handlebars) [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void AliasedPropertyOnList(IHandlebars handlebars) { + if(handlebars.IsSharedEnvironment) return; + var source = "Array is {{ names.Length }} item(s) long"; handlebars.Configuration.UseCollectionMemberAliasProvider(); var template = handlebars.Compile(source); diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 0ff1fb0d..753c1e17 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1;netcoreapp3.1 + netcoreapp3.1 $(TargetFrameworks);net452;net46;net461;net472 6BA232A6-8C4D-4C7D-BD75-1844FE9774AF HandlebarsDotNet.Test diff --git a/source/Handlebars.Test/HandlebarsEnvGenerator.cs b/source/Handlebars.Test/HandlebarsEnvGenerator.cs index ffc29502..577d23b4 100644 --- a/source/Handlebars.Test/HandlebarsEnvGenerator.cs +++ b/source/Handlebars.Test/HandlebarsEnvGenerator.cs @@ -10,6 +10,7 @@ public class HandlebarsEnvGenerator : IEnumerable private readonly List _data = new() { Handlebars.Create(), + Handlebars.CreateSharedEnvironment(), Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)), Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { diff --git a/source/Handlebars/Collections/CascadeIndex.cs b/source/Handlebars/Collections/CascadeIndex.cs index 9a3b38ed..6d3a592e 100644 --- a/source/Handlebars/Collections/CascadeIndex.cs +++ b/source/Handlebars/Collections/CascadeIndex.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -16,72 +17,84 @@ namespace HandlebarsDotNet.Collections public class CascadeIndex : IIndexed where TComparer : IEqualityComparer { + private readonly TComparer _comparer; public IReadOnlyIndexed Outer { get; set; } - private readonly DictionarySlim _inner; + private DictionarySlim _inner; public CascadeIndex(TComparer comparer) + : this(null, comparer) { - Outer = null; - _inner = new DictionarySlim(comparer); } - public int Count => _inner.Count + OuterEnumerable().Count(); + public CascadeIndex(IReadOnlyIndexed outer, TComparer comparer) + { + _comparer = comparer; + Outer = outer; + } + + public int Count => (_inner?.Count ?? 0) + OuterEnumerable().Count(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddOrReplace(in TKey key, in TValue value) { - _inner.AddOrReplace(key, value); + (_inner ??= new DictionarySlim(_comparer)).AddOrReplace(key, value); } public void Clear() { Outer = null; - _inner.Clear(); + _inner?.Clear(); } public bool ContainsKey(in TKey key) { - return _inner.ContainsKey(key) || (Outer?.ContainsKey(key) ?? false); + return (_inner?.ContainsKey(key) ?? false) + || (Outer?.ContainsKey(key) ?? false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetValue(in TKey key, out TValue value) { - if (_inner.TryGetValue(key, out value)) return true; - return Outer?.TryGetValue(key, out value) ?? false; + value = default; + return (_inner?.TryGetValue(key, out value) ?? false) + || (Outer?.TryGetValue(key, out value) ?? false); } public TValue this[in TKey key] { get { - if (_inner.TryGetValue(key, out var value)) return value; - if (Outer?.TryGetValue(key, out value) ?? false) return value; - throw new KeyNotFoundException($"{key}"); + if (TryGetValue(key, out var value)) return value; + Throw.KeyNotFoundException($"{key}"); + return default; // will never reach this point } - set => _inner.AddOrReplace(key, value); + set => AddOrReplace(key, value); } public IEnumerator> GetEnumerator() { - var enumerator = _inner.GetEnumerator(); - while (enumerator.MoveNext()) - { - yield return enumerator.Current; - } + foreach (var pair in InnerEnumerable()) yield return pair; + foreach (var pair in OuterEnumerable()) yield return pair; + } - foreach (var pair in OuterEnumerable()) + private IEnumerable> InnerEnumerable() + { + if(_inner == null) yield break; + + var outerEnumerator = _inner.GetEnumerator(); + while (outerEnumerator.MoveNext()) { - yield return pair; + if (_inner.ContainsKey(outerEnumerator.Current.Key)) continue; + yield return outerEnumerator.Current; } } - + private IEnumerable> OuterEnumerable() { - var outerEnumerator = Outer?.GetEnumerator(); - if (outerEnumerator == null) yield break; - + if(Outer == null) yield break; + + using var outerEnumerator = Outer.GetEnumerator(); while (outerEnumerator.MoveNext()) { if (_inner.ContainsKey(outerEnumerator.Current.Key)) continue; @@ -90,5 +103,10 @@ private IEnumerable> OuterEnumerable() } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private static class Throw + { + public static void KeyNotFoundException(string message, Exception exception = null) => throw new KeyNotFoundException(message, exception); + } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs index 9650f43f..94f5ff61 100644 --- a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs +++ b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs @@ -18,6 +18,7 @@ namespace HandlebarsDotNet { internal class HandlebarsConfigurationAdapter : ICompiledHandlebarsConfiguration { + // observers are in WeakCollection, need to keep reference somewhere private readonly List _observers = new List(); public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) @@ -33,6 +34,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) new CollectionFormatterProvider(), new ReadOnlyCollectionFormatterProvider() }.AddMany(configuration.FormatterProviders); + configuration.FormatterProviders.Subscribe(FormatterProviders); ObjectDescriptorProviders = CreateObjectDescriptorProvider(UnderlingConfiguration.ObjectDescriptorProviders); ExpressionMiddlewares = new ObservableList(configuration.CompileTimeConfiguration.ExpressionMiddleware) @@ -78,7 +80,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) public IAppendOnlyList HelperResolvers { get; } public IIndexed> RegisteredTemplates { get; } - private ObservableIndex, IEqualityComparer> CreateHelpersSubscription(IIndexed source) + private ObservableIndex, PathInfoLight.PathInfoLightEqualityComparer> CreateHelpersSubscription(IIndexed source) where TOptions : struct, IOptions where TDescriptor : class, IDescriptor { @@ -89,7 +91,7 @@ private ObservableIndex, IEqualityComparer, IEqualityComparer>(equalityComparer, existingHelpers); + var target = new ObservableIndex, PathInfoLight.PathInfoLightEqualityComparer>(equalityComparer, existingHelpers); var observer = ObserverBuilder>.Create(target) .OnEvent>( @@ -130,14 +132,8 @@ private ObservableList CreateObjectDescriptorProvider } .AddMany(descriptorProviders); - var observer = ObserverBuilder>.Create(objectDescriptorProviders) - .OnEvent>((@event, state) => { state.Add(@event.Value); }) - .Build(); + descriptorProviders.Subscribe(objectDescriptorProviders); - _observers.Add(observer); - - descriptorProviders.Subscribe(observer); - return objectDescriptorProviders; } } diff --git a/source/Handlebars/EqualityComparers/PathInfoLight.PathInfoLightEqualityComparer.cs b/source/Handlebars/EqualityComparers/PathInfoLight.PathInfoLightEqualityComparer.cs index 6c077721..85c41329 100644 --- a/source/Handlebars/EqualityComparers/PathInfoLight.PathInfoLightEqualityComparer.cs +++ b/source/Handlebars/EqualityComparers/PathInfoLight.PathInfoLightEqualityComparer.cs @@ -5,7 +5,7 @@ namespace HandlebarsDotNet { public readonly partial struct PathInfoLight { - internal sealed class PathInfoLightEqualityComparer : IEqualityComparer + internal readonly struct PathInfoLightEqualityComparer : IEqualityComparer { private readonly PathInfo.TrimmedPathEqualityComparer _comparer; diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index 023ee1dd..4de3f1de 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -24,8 +24,19 @@ public static IHandlebars Create(HandlebarsConfiguration configuration = null) configuration = configuration ?? new HandlebarsConfiguration(); return new HandlebarsEnvironment(configuration); } - - + + /// + /// Creates shared Handlebars environment that is used to compile templates that share the same configuration + /// Runtime only changes can be applied after object creation! + /// + /// + /// + public static IHandlebars CreateSharedEnvironment(HandlebarsConfiguration configuration = null) + { + configuration ??= new HandlebarsConfiguration(); + return new HandlebarsEnvironment(new HandlebarsConfigurationAdapter(configuration)); + } + /// /// Creates standalone instance of environment /// diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 88e6414a..cfa9872c 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -4,7 +4,6 @@ using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Decorators; using HandlebarsDotNet.Helpers; -using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.IO; using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.Runtime; @@ -37,8 +36,11 @@ public HandlebarsEnvironment(HandlebarsConfiguration configuration) internal HandlebarsEnvironment(ICompiledHandlebarsConfiguration configuration) { CompiledConfiguration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + Configuration = CompiledConfiguration.UnderlingConfiguration; + IsSharedEnvironment = true; } - + + public bool IsSharedEnvironment { get; } public HandlebarsConfiguration Configuration { get; } internal ICompiledHandlebarsConfiguration CompiledConfiguration { get; } ICompiledHandlebarsConfiguration ICompiledHandlebars.CompiledConfiguration => CompiledConfiguration; @@ -115,6 +117,12 @@ private HandlebarsTemplate CompileViewInternal(strin }; } + public IHandlebars CreateSharedEnvironment() + { + var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration); + return new HandlebarsEnvironment(configuration); + } + public HandlebarsTemplate Compile(TextReader template) { using var container = AmbientContext.Use(_ambientContext); diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 75d59bf8..d34515a2 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -19,6 +19,14 @@ public delegate void HandlebarsTemplate(TWrit /// public interface IHandlebars : IHelpersRegistry { + /// + /// Creates shared Handlebars environment that is used to compile templates that share the same configuration + /// Runtime only changes can be applied after object creation! + /// + IHandlebars CreateSharedEnvironment(); + + bool IsSharedEnvironment { get; } + /// /// /// diff --git a/source/Handlebars/PathInfoLight.cs b/source/Handlebars/PathInfoLight.cs index 55c253a3..02f71e5a 100644 --- a/source/Handlebars/PathInfoLight.cs +++ b/source/Handlebars/PathInfoLight.cs @@ -25,9 +25,9 @@ private PathInfoLight(PathInfo pathInfo, int comparerTag) _comparerTag = comparerTag; } - internal static IEqualityComparer PlainPathComparer { get; } = new PathInfoLightEqualityComparer(false); + internal static PathInfoLightEqualityComparer PlainPathComparer { get; } = new PathInfoLightEqualityComparer(countParts: false, ignoreCase: true); - internal static IEqualityComparer PlainPathWithPartsCountComparer { get; } = new PathInfoLightEqualityComparer(); + internal static PathInfoLightEqualityComparer PlainPathWithPartsCountComparer { get; } = new PathInfoLightEqualityComparer(countParts: true, ignoreCase: true); /// /// Used for special handling of Relaxed Helper Names From 392cc5caf34be585348e9d35dfd4087cc6212355 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Wed, 6 Apr 2022 22:38:15 -0700 Subject: [PATCH 19/37] Add issue test --- source/Handlebars.Test/IssueTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 143f751b..6c1402d7 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -161,6 +161,23 @@ End outer partial block
Assert.Equal(expected, result); } + // issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/515 + [Fact] + public void ValidContextInNestedPartialBlock() + { + const string template = @"{{#> [a/b] c=this }}{{c.value}}{{/ [a/b] }}"; + const string partial = "{{c.value}} {{> @partial-block }}"; + + var handlebars = Handlebars.Create(); + handlebars.RegisterTemplate("a/b", @partial); + + var callback = handlebars.Compile(template); + var result = callback(new { value = 42 }); + + const string expected = @"42 42"; + Assert.Equal(expected, result); + } + // issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/395 [Fact] public void RenderingWithUnusedPartial() From e68a2c826c77bcf0e6e365db205fe6cd9f8906fc Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Wed, 6 Apr 2022 22:38:34 -0700 Subject: [PATCH 20/37] Pass current context instead of parent context --- source/Handlebars/BindingContext.cs | 2 +- .../Translation/Expression/PartialBinder.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/source/Handlebars/BindingContext.cs b/source/Handlebars/BindingContext.cs index 0bc4d784..089cf276 100644 --- a/source/Handlebars/BindingContext.cs +++ b/source/Handlebars/BindingContext.cs @@ -115,7 +115,7 @@ out WellKnownVariables[(int) WellKnownVariable.Parent] internal CascadeIndex, StringEqualityComparer> BlockHelpers { get; } - internal TemplateDelegate PartialBlockTemplate { get; private set; } + internal TemplateDelegate PartialBlockTemplate { get; set; } public object Value { get; set; } diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index c435469c..247e9c58 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using Expressions.Shortcuts; +using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Polyfills; using static Expressions.Shortcuts.ExpressionShortcuts; @@ -92,6 +93,7 @@ private static void InvokePartialWithFallback( EncodedTextWriter writer, ICompiledHandlebarsConfiguration configuration) { + partialName = partialName != null ? ChainSegment.Create(partialName).TrimmedValue : null; if (InvokePartial(partialName, context, writer, configuration)) return; if (context.PartialBlockTemplate == null) { @@ -118,7 +120,16 @@ private static bool InvokePartial( return false; } - context.PartialBlockTemplate(writer, context.ParentContext); + var partialBlockTemplate = context.PartialBlockTemplate; + try + { + context.PartialBlockTemplate = context.ParentContext.PartialBlockTemplate; + partialBlockTemplate(writer, context); + } + finally + { + context.PartialBlockTemplate = partialBlockTemplate; + } return true; } From 8966512f9d1653baee68eb3a438396cc245c9497 Mon Sep 17 00:00:00 2001 From: Nick Lund Stenroos-Dam Date: Thu, 22 Dec 2022 10:40:55 +0100 Subject: [PATCH 21/37] Added failing test for rendering inline blocks #524 --- .../ViewEngine/ViewEngineTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 50ad1ea5..f6bcf5d5 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -73,6 +73,28 @@ public void CanLoadAViewWithALayoutInTheRoot() Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output); } + [Fact] + public void CanRenderInlineBlocks() + { + // This sample is based on https://handlebarsjs.com/examples/partials/inline-blocks.html + + var files = new FakeFileSystem() + { + //Given a layout in a subfolder + { "partials/layout.hbs", "
\r\n{{> nav}}\r\n
\r\n
\r\n{{> content}}\r\n
"}, + + { "template.hbs", "{{#> layout}}\r\n{{#*inline \"nav\"}}\r\nMy Nav\r\n{{/inline}}\r\n{{#*inline \"content\"}}\r\nMy Content\r\n{{/inline}}\r\n{{/layout}}"} + }; + + //When a viewengine renders that view + var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); + var renderView = handleBars.CompileView("template.hbs"); + var output = renderView(null); + + //Then the correct output should be rendered + Assert.Equal("
\r\nMy Nav\r\n
\r\n
\r\nMy Content\r\n
", output); + } + [Fact] public void CanLoadAViewWithALayoutWithAVariable() { From b2a73bdad6d2faaba7eb7f6ab396a5ff37f93d84 Mon Sep 17 00:00:00 2001 From: Nick Lund Stenroos-Dam Date: Thu, 22 Dec 2022 15:14:27 +0100 Subject: [PATCH 22/37] Inline blocks should not be additionally escaped --- source/Handlebars.Test/ViewEngine/ViewEngineTests.cs | 9 ++++++--- source/Handlebars/FileSystemPartialTemplateResolver.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index f6bcf5d5..40182e12 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -83,16 +83,19 @@ public void CanRenderInlineBlocks() //Given a layout in a subfolder { "partials/layout.hbs", "
\r\n{{> nav}}\r\n
\r\n
\r\n{{> content}}\r\n
"}, - { "template.hbs", "{{#> layout}}\r\n{{#*inline \"nav\"}}\r\nMy Nav\r\n{{/inline}}\r\n{{#*inline \"content\"}}\r\nMy Content\r\n{{/inline}}\r\n{{/layout}}"} + { "template.hbs", "{{#> layout}}\r\n{{#*inline \"nav\"}}\r\n{{Text}}\r\n{{/inline}}\r\n{{#*inline \"content\"}}\r\nMy Content\r\n{{/inline}}\r\n{{/layout}}"} }; //When a viewengine renders that view var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); var renderView = handleBars.CompileView("template.hbs"); - var output = renderView(null); + var output = renderView(new Dictionary + { + { "Text", "" } + }); //Then the correct output should be rendered - Assert.Equal("
\r\nMy Nav\r\n
\r\n
\r\nMy Content\r\n
", output); + Assert.Equal("
\r\n<My Nav>\r\n
\r\n
\r\nMy Content\r\n
", output); } [Fact] diff --git a/source/Handlebars/FileSystemPartialTemplateResolver.cs b/source/Handlebars/FileSystemPartialTemplateResolver.cs index 80bde51e..8b0338e9 100644 --- a/source/Handlebars/FileSystemPartialTemplateResolver.cs +++ b/source/Handlebars/FileSystemPartialTemplateResolver.cs @@ -27,7 +27,7 @@ public bool TryRegisterPartial(IHandlebars env, string partialName, string templ handlebarsTemplateRegistrations.RegisteredTemplates.AddOrReplace(partialName, (writer, o, data) => { - writer.Write(compiled(o, data)); + ((EncodedTextWriterWrapper)writer).Write(compiled(o, data), false); }); return true; From 7d7e3c0ac0a741131af608b026ebbae61dd3910d Mon Sep 17 00:00:00 2001 From: Anthony Halliday Date: Tue, 13 Dec 2022 12:06:06 +0000 Subject: [PATCH 23/37] Exit loop when reaching the reader end --- source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs index 4b861705..aedc18f0 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -23,7 +23,7 @@ private static string AccumulateWord(ExtendedStringReader reader) reader.Read(); - while (reader.Peek() != '|') + while (reader.Peek() != '|' && reader.Peek() != -1) { buffer.Append((char) reader.Read()); } From 8dc42f601ea4a1abce34352ead3f03cb5c63cb80 Mon Sep 17 00:00:00 2001 From: Anthony Halliday Date: Tue, 13 Dec 2022 12:07:37 +0000 Subject: [PATCH 24/37] Added unit test for issue 535 --- source/Handlebars.Test/IssueTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 6c1402d7..31136117 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -723,5 +723,15 @@ public void LastLetterCutOff() Assert.Equal("abcd", templateOutput); } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/535 + // Issue refers to invalid template causing OutOfMemoryException + [Fact] + public void UnrecognisedExpressionThrowsOutOfMemoryException() + { + var source = "{{Name | invalid}}"; + + Assert.Throws(()=> Handlebars.Compile(source)); + } } } \ No newline at end of file From cb86f6e7e6b6a0a13d6c2448e13b5f67aedcc14a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 00:15:27 +0000 Subject: [PATCH 25/37] Bump Newtonsoft.Json in /source/Handlebars.Benchmark Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 9.0.1 to 13.0.1. - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/9.0.1...13.0.1) --- updated-dependencies: - dependency-name: Newtonsoft.Json dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- source/Handlebars.Benchmark/Handlebars.Benchmark.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index d145c7bb..1174d8a2 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -10,7 +10,7 @@ - + From ce1f01b8516b74e2c1893beda8a8792603de7ecf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 15:28:17 +0000 Subject: [PATCH 26/37] Bump Newtonsoft.Json from 9.0.1 to 13.0.1 in /source/Handlebars.Test Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 9.0.1 to 13.0.1. - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/9.0.1...13.0.1) --- updated-dependencies: - dependency-name: Newtonsoft.Json dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- source/Handlebars.Test/Handlebars.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 753c1e17..a06a07ea 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -46,7 +46,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 0338d53c6c775c4bc2fcaf871613cd851fce25ca Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 14 Feb 2023 17:42:47 -0800 Subject: [PATCH 27/37] Create FUNDING.yml --- .github/FUNDING.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..2866c977 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: Handlebars-Net/Handlebars.Net # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 8d3eba46f803e422008a9a5cbe8ce12e3fbc1aeb Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 21 Feb 2023 09:59:44 +0100 Subject: [PATCH 28/37] Add optional 3rd argument to lookup helper --- .../Handlebars.Test/BasicIntegrationTests.cs | 51 +++++++++++++++++++ source/Handlebars.sln | 32 ++++++------ .../Helpers/LookupReturnHelperDescriptor.cs | 17 ++++--- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index ea076246..69bd90fa 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -441,6 +441,57 @@ public void PathRelativeBinding(IHandlebars handlebars) Assert.Equal("Garry Finch gazraa Karen Finch photobasics", actual); } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void PathRelativeBinding_WithDefaultValue(IHandlebars handlebars) + { + var template = + @"{{#each users}} + {{this/person.name/firstName}} + {{#with this/person.name}} + {{lastName}} + {{lookup (lookup ../this/../users @index) 'twitter' 'N/A'}} + {{/with}} + {{/each}}"; + + var handlebarsTemplate = handlebars.Compile(template); + + var data = new + { + users = new object[] + { + new + { + person = new + { + name = new + { + firstName = "Garry", + lastName = "Finch" + } + }, + jobTitle = "Front End Technical Lead", + }, + new + { + person = new + { + name = new + { + firstName = "Karen", + lastName = "Finch" + } + }, + jobTitle = "Photographer", + twitter = "photobasics" + } + } + }; + + var result = handlebarsTemplate(data); + var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' '))); + Assert.Equal("Garry Finch N/A Karen Finch photobasics", actual); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicPropertyOnArray(IHandlebars handlebars) { diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 0224c9d9..adaa5299 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -1,19 +1,18 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33403.182 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars", "Handlebars\Handlebars.csproj", "{A09CFF95-B671-48FE-96A8-D3CBDC110B75}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handlebars", "Handlebars\Handlebars.csproj", "{9822C7B8-7E51-42BC-9A49-72A10491B202}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Test", "Handlebars.Test\Handlebars.Test.csproj", "{2BD48FB6-C852-4141-B734-12E501B1D761}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handlebars.Test", "Handlebars.Test\Handlebars.Test.csproj", "{6BA232A6-8C4D-4C7D-BD75-1844FE9774AF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" ProjectSection(SolutionItems) = preProject - ..\README.md = ..\README.md Directory.Build.props = Directory.Build.props + ..\README.md = ..\README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{417E2E51-2DD2-4045-84E5-BA66484E957B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{417E2E51-2DD2-4045-84E5-BA66484E957B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,14 +20,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.Build.0 = Release|Any CPU - {2BD48FB6-C852-4141-B734-12E501B1D761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BD48FB6-C852-4141-B734-12E501B1D761}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.Build.0 = Release|Any CPU + {9822C7B8-7E51-42BC-9A49-72A10491B202}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9822C7B8-7E51-42BC-9A49-72A10491B202}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9822C7B8-7E51-42BC-9A49-72A10491B202}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9822C7B8-7E51-42BC-9A49-72A10491B202}.Release|Any CPU.Build.0 = Release|Any CPU + {6BA232A6-8C4D-4C7D-BD75-1844FE9774AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BA232A6-8C4D-4C7D-BD75-1844FE9774AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BA232A6-8C4D-4C7D-BD75-1844FE9774AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BA232A6-8C4D-4C7D-BD75-1844FE9774AF}.Release|Any CPU.Build.0 = Release|Any CPU {417E2E51-2DD2-4045-84E5-BA66484E957B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {417E2E51-2DD2-4045-84E5-BA66484E957B}.Debug|Any CPU.Build.0 = Debug|Any CPU {417E2E51-2DD2-4045-84E5-BA66484E957B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -37,6 +36,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0260B1C7-30DF-49B6-A96E-9FD2F2AEFA1B} + EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.TextStylePolicy = $1 diff --git a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs index 6cd6849f..655791de 100644 --- a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs @@ -4,19 +4,22 @@ namespace HandlebarsDotNet.Helpers { public sealed class LookupReturnHelperDescriptor : IHelperDescriptor { - public PathInfo Name { get; } = "lookup"; + public PathInfo Name => "lookup"; public object Invoke(in HelperOptions options, in Context context, in Arguments arguments) { - if (arguments.Length != 2) + if (arguments.Length != 2 && arguments.Length != 3) { - throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); + throw new HandlebarsException("{{lookup}} helper must have two or three arguments"); } - + var segment = ChainSegment.Create(arguments[1]); - return !options.TryAccessMember(arguments[0], segment, out var value) - ? UndefinedBindingResult.Create(segment) - : value; + + var defaultValueIfNotFound = arguments.Length == 3 ? arguments[2] : UndefinedBindingResult.Create(segment); + + return options.TryAccessMember(arguments[0], segment, out var value) + ? value + : defaultValueIfNotFound; } public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments) From 6b68f5dd204e0b73f86c2d848e4b6ad4e5d5d9f8 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 21 Feb 2023 11:20:20 +0100 Subject: [PATCH 29/37] ex --- .../Handlebars.Test/BasicIntegrationTests.cs | 333 ++++++++++-------- 1 file changed, 186 insertions(+), 147 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 69bd90fa..73bcefb0 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -1,4 +1,3 @@ -using Xunit; using System; using System.Collections; using System.Collections.Generic; @@ -6,14 +5,14 @@ using System.Linq; using System.Reflection; using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers; using HandlebarsDotNet.IO; using HandlebarsDotNet.PathStructure; -using HandlebarsDotNet.Runtime; -using HandlebarsDotNet.ValueProviders; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NSubstitute.ExceptionExtensions; +using Xunit; namespace HandlebarsDotNet.Test { @@ -57,21 +56,21 @@ public void BasicPath(IHandlebars handlebars) var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } - + [Fact] public void BasicSharedEnvironment() { var handlebars = Handlebars.CreateSharedEnvironment(); - handlebars.RegisterHelper("registerLateHelper", + handlebars.RegisterHelper("registerLateHelper", (in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) => { var configuration = options.Frame .GetType() .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)? .GetValue(options.Frame) as ICompiledHandlebarsConfiguration; - - if(configuration == null) return; - + + if (configuration == null) return; + var helpers = configuration.Helpers; const string name = "lateHelper"; @@ -80,12 +79,12 @@ public void BasicSharedEnvironment() @ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42); } }); - + var _0_template = "{{registerLateHelper}}"; var _0 = handlebars.Compile(_0_template); var _1_template = "{{lateHelper}}"; var _1 = handlebars.Compile(_1_template); - + var result = _1(null); Assert.Equal("", result); // `lateHelper` is not registered yet @@ -127,7 +126,7 @@ public void BasicPathUnresolvedBindingFormatter(IHandlebars handlebars) var expected = HtmlEncodeStringHelper(handlebars, "Hello, ('foo' is undefined)!"); Assert.Equal(expected, result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void PathUnresolvedBindingFormatter(IHandlebars handlebars) { @@ -145,7 +144,7 @@ public void PathUnresolvedBindingFormatter(IHandlebars handlebars) var expected = HtmlEncodeStringHelper(handlebars, "Hello, ('foo' is undefined)!"); Assert.Equal(expected, result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void CustomDateTimeFormat(IHandlebars handlebars) { @@ -160,23 +159,23 @@ public void CustomDateTimeFormat(IHandlebars handlebars) { now = DateTime.Now }; - + var result = template(data); Assert.Equal(data.now.ToString(format), result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void DefaultDateTimeFormat(IHandlebars handlebars) { var source = "{{time}}"; - + var template = handlebars.Compile(source); var time = "2020-11-19T23:36:08.4256520Z"; var data = new { time = DateTime.Parse(time).ToUniversalTime() }; - + var result = template(data); Assert.Equal(time, result); } @@ -193,7 +192,7 @@ public void BasicPathThrowOnUnresolvedBindingExpression(IHandlebars handlebars) { name = "Handlebars.Net" }; - + Assert.Throws(() => template(data)); } @@ -203,7 +202,7 @@ public void BasicPathThrowOnNestedUnresolvedBindingExpression(IHandlebars handle var source = "Hello, {{foo.bar}}!"; handlebars.Configuration.ThrowOnUnresolvedBindingExpression = true; - + var template = handlebars.Compile(source); var data = new @@ -211,7 +210,7 @@ public void BasicPathThrowOnNestedUnresolvedBindingExpression(IHandlebars handle foo = (object)null }; var ex = Assert.Throws(() => template(data)); - + Assert.Equal("bar is undefined", ex.Message); } @@ -347,7 +346,7 @@ public void BasicPathArrayNoSquareBracketsChildPath(IHandlebars handlebars) var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicPathEnumerableNoSquareBracketsChildPath(IHandlebars handlebars) { @@ -437,7 +436,7 @@ public void PathRelativeBinding(IHandlebars handlebars) }; var result = handlebarsTemplate(data); - var actual = string.Join(" ", result.Split(new []{"\r\n"}, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' '))); + var actual = string.Join(" ", result.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim(' '))); Assert.Equal("Garry Finch gazraa Karen Finch photobasics", actual); } @@ -492,6 +491,45 @@ public void PathRelativeBinding_WithDefaultValue(IHandlebars handlebars) Assert.Equal("Garry Finch N/A Karen Finch photobasics", actual); } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void PathRelativeBinding_WrongNumberOfArguments(IHandlebars handlebars) + { + // Arrange + var template = + @"{{#each users}} + {{this/person.name/firstName}} + {{#with this/person.name}} + {{lastName}} + {{lookup (lookup ../this/../users @index)}} + {{/with}} + {{/each}}"; + + var handlebarsTemplate = handlebars.Compile(template); + + var data = new + { + users = new object[] + { + new + { + person = new + { + name = new + { + firstName = "Garry", + lastName = "Finch" + } + }, + twitter = "test" + } + } + }; + + // Act + var ex = Assert.Throws(() => handlebarsTemplate(data)); + Assert.Equal("{{lookup}} helper must have two or three arguments", ex.Message); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicPropertyOnArray(IHandlebars handlebars) { @@ -504,11 +542,11 @@ public void BasicPropertyOnArray(IHandlebars handlebars) var result = template(data); Assert.Equal("Array is 2 item(s) long", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void AliasedPropertyOnArray(IHandlebars handlebars) { - if(handlebars.IsSharedEnvironment) return; + if (handlebars.IsSharedEnvironment) return; var source = "Array is {{ names.count }} item(s) long"; handlebars.Configuration.UseCollectionMemberAliasProvider(); @@ -520,15 +558,15 @@ public void AliasedPropertyOnArray(IHandlebars handlebars) var result = template(data); Assert.Equal("Array is 2 item(s) long", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void CustomAliasedPropertyOnArray(IHandlebars handlebars) { var aliasProvider = new DelegatedMemberAliasProvider() .AddAlias("myCountAlias", list => list.Count); - + handlebars.Configuration.AliasProviders.Add(aliasProvider); - + var source = "Array is {{ names.myCountAlias }} item(s) long"; var template = handlebars.Compile(source); var data = new @@ -538,12 +576,12 @@ public void CustomAliasedPropertyOnArray(IHandlebars handlebars) var result = template(data); Assert.Equal("Array is 2 item(s) long", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void AliasedPropertyOnList(IHandlebars handlebars) { - if(handlebars.IsSharedEnvironment) return; - + if (handlebars.IsSharedEnvironment) return; + var source = "Array is {{ names.Length }} item(s) long"; handlebars.Configuration.UseCollectionMemberAliasProvider(); var template = handlebars.Compile(source); @@ -632,7 +670,7 @@ public void BasicWith(IHandlebars handlebars) var result = template(data); Assert.Equal("Hello, my good friend Erik!", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void GlobalDataPropagation(IHandlebars handlebars) { @@ -643,7 +681,8 @@ public void GlobalDataPropagation(IHandlebars handlebars) input = new { first = 1, - second = new { + second = new + { third = 3 } } @@ -651,7 +690,7 @@ public void GlobalDataPropagation(IHandlebars handlebars) var result = template(data, new { global1 = 2, global2 = 4 }); Assert.Equal("1 2 3 4", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void TestSingleLoopDictionary(IHandlebars handlebars) { @@ -667,7 +706,7 @@ public void TestSingleLoopDictionary(IHandlebars handlebars) var result = template(data); Assert.Equal("ii=0 ii=1 ii=2 ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void WithWithBlockParams(IHandlebars handlebars) { @@ -751,7 +790,7 @@ public void BasicObjectEnumerator(IHandlebars handlebars) var result = template(data); Assert.Equal("hello world ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicListEnumerator(IHandlebars handlebars) { @@ -768,7 +807,7 @@ public void BasicListEnumerator(IHandlebars handlebars) var result = template(data); Assert.Equal("hello world ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicObjectEnumeratorWithLast(IHandlebars handlebars) { @@ -802,7 +841,7 @@ public void BasicObjectEnumeratorWithKey(IHandlebars handlebars) var result = template(data); Assert.Equal("foo: hello bar: world ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void ObjectEnumeratorWithBlockParams(IHandlebars handlebars) { @@ -819,7 +858,7 @@ public void ObjectEnumeratorWithBlockParams(IHandlebars handlebars) var result = template(data); Assert.Equal("hello: foo world: bar ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void ObjectEnumeratorWithWithContainingBlockParams(IHandlebars handlebars) { @@ -853,7 +892,7 @@ public void BasicDictionaryEnumerator(IHandlebars handlebars) var result = template(data); Assert.Equal("hello world ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicDictionaryEnumeratorDeep(IHandlebars handlebars) { @@ -885,7 +924,7 @@ public void BasicDictionaryEnumeratorDeep(IHandlebars handlebars) } } }; - + var result = template(data); Assert.Equal("1234", result); } @@ -906,7 +945,7 @@ public void DictionaryEnumeratorWithBlockParams(IHandlebars handlebars) var result = template(data); Assert.Equal("hello foo world bar ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void DictionaryWithLastEnumerator(IHandlebars handlebars) { @@ -1822,74 +1861,74 @@ public void ImplicitIDictionaryImplementationShouldNotThrowNullref(IHandlebars h // Act compile.Invoke(mock); } - - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void ShouldBeAbleToHandleFieldContainingDots(IHandlebars handlebars) - { - var source = "Everybody was {{ foo.bar }}-{{ [foo.bar] }} {{ foo.[bar.baz].buz }}!"; - var template = handlebars.Compile(source); - var data = new Dictionary() - { - {"foo.bar", "fu"}, - {"foo", new Dictionary{{ "bar", "kung" }, { "bar.baz", new Dictionary {{ "buz", "fighting" }} }} } - }; - var result = template(data); - Assert.Equal("Everybody was kung-fu fighting!", result); - } - - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void ShouldBeAbleToHandleListWithNumericalFields(IHandlebars handlebars) - { - var source = "{{ [0] }}"; - var template = handlebars.Compile(source); - var data = new List {"FOOBAR"}; - var result = template(data); - Assert.Equal("FOOBAR", result); - } - - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void ShouldBeAbleToHandleDictionaryWithNumericalFields(IHandlebars handlebars) - { - var source = "{{ [0] }}"; - var template = handlebars.Compile(source); - var data = new Dictionary - { - {"0", "FOOBAR"}, - }; - var result = template(data); - Assert.Equal("FOOBAR", result); - } - - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void ShouldBeAbleToHandleJObjectsWithNumericalFields(IHandlebars handlebars) - { - var source = "{{ [0] }}"; - var template = handlebars.Compile(source); - var data = new JObject - { - {"0", "FOOBAR"}, - }; - var result = template(data); - Assert.Equal("FOOBAR", result); - } - - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets(IHandlebars handlebars) - { - var source = - "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; - var template = handlebars.Compile(source); - var data = new Dictionary - { - {"noBracket", "foo"}, - {"[startsWithBracket", "bar"}, - {"endsWithBracket]", "baz"}, - {"[bothBrackets]", "buz"} - }; - var result = template(data); - Assert.Equal("foo foo bar baz buz", result); - } - + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleFieldContainingDots(IHandlebars handlebars) + { + var source = "Everybody was {{ foo.bar }}-{{ [foo.bar] }} {{ foo.[bar.baz].buz }}!"; + var template = handlebars.Compile(source); + var data = new Dictionary() + { + {"foo.bar", "fu"}, + {"foo", new Dictionary{{ "bar", "kung" }, { "bar.baz", new Dictionary {{ "buz", "fighting" }} }} } + }; + var result = template(data); + Assert.Equal("Everybody was kung-fu fighting!", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleListWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new List { "FOOBAR" }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleDictionaryWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new Dictionary + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleJObjectsWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new JObject + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets(IHandlebars handlebars) + { + var source = + "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; + var template = handlebars.Compile(source); + var data = new Dictionary + { + {"noBracket", "foo"}, + {"[startsWithBracket", "bar"}, + {"endsWithBracket]", "baz"}, + {"[bothBrackets]", "buz"} + }; + var result = template(data); + Assert.Equal("foo foo bar baz buz", result); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicReturnFromHelper(IHandlebars Handlebars) { @@ -1897,11 +1936,11 @@ public void BasicReturnFromHelper(IHandlebars Handlebars) Handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); var source = $"{{{{{getData} 'data'}}}}"; var template = Handlebars.Compile(source); - + var result = template(new object()); Assert.Equal("data", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void CollectionReturnFromHelper(IHandlebars handlebars) { @@ -1912,17 +1951,17 @@ public void CollectionReturnFromHelper(IHandlebars handlebars) {"Nils", arguments[0].ToString()}, {"Yehuda", arguments[1].ToString()} }; - + return data; }); var source = "{{#each (getData 'Darmstadt' 'San Francisco')}}{{@key}} lives in {{@value}}. {{/each}}"; var template = handlebars.Compile(source); - + var result = template(new object()); Assert.Equal("Nils lives in Darmstadt. Yehuda lives in San Francisco. ", result); } - - + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void ReturnFromHelperWithSubExpression(IHandlebars handlebars) { @@ -1933,42 +1972,42 @@ public void ReturnFromHelperWithSubExpression(IHandlebars handlebars) writer.WriteSafeString(" "); writer.WriteSafeString(arguments[1]); }); - + var getData = $"getData{Guid.NewGuid()}"; handlebars.RegisterHelper(getData, (context, arguments) => { return arguments[0]; }); - + var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; var template = handlebars.Compile(source); - + var result = template(new object()); Assert.Equal("data 42", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void ReturnFromHelperLateBindWithSubExpression(IHandlebars handlebars) { var formatData = $"formatData{Guid.NewGuid()}"; var getData = $"getData{Guid.NewGuid()}"; - + var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; var template = handlebars.Compile(source); - + handlebars.RegisterHelper(formatData, (writer, context, arguments) => { writer.WriteSafeString(arguments[0]); writer.WriteSafeString(" "); writer.WriteSafeString(arguments[1]); }); - + handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); - + var result = template(new object()); Assert.Equal("data 42", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicLookup(IHandlebars handlebars) { @@ -1976,14 +2015,14 @@ public void BasicLookup(IHandlebars handlebars) var template = handlebars.Compile(source); var data = new { - people = new[]{"Nils", "Yehuda"}, - cities = new[]{"Darmstadt", "San Francisco"} + people = new[] { "Nils", "Yehuda" }, + cities = new[] { "Darmstadt", "San Francisco" } }; - + var result = template(data); Assert.Equal("Nils lives in Darmstadt Yehuda lives in San Francisco ", result); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void LookupAsSubExpression(IHandlebars handlebars) { @@ -2018,7 +2057,7 @@ public void LookupAsSubExpression(IHandlebars handlebars) } } }; - + var result = template(data); Assert.Equal("Nils lives in Darmstadt (Germany)Yehuda lives in San Francisco (USA)", result); } @@ -2035,10 +2074,10 @@ private void StringConditionTest(IHandlebars handlebars) var func = handlebars.Compile(template); var actual = func(data); - + Assert.Equal(expected, actual); } - + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] private void CustomHelperResolverTest(IHandlebars handlebars) { @@ -2046,9 +2085,9 @@ private void CustomHelperResolverTest(IHandlebars handlebars) var template = "{{ toLower input }}"; var func = handlebars.Compile(template); var data = new { input = "ABC" }; - + var actual = func(data); - + Assert.Equal(data.input.ToLower(), actual); } @@ -2060,7 +2099,7 @@ private void CustomHelperResolverTest(IHandlebars handlebars) public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) { var source = "{{ ./" + helperName + " }}"; - + foreach (IHandlebars handlebars in new HandlebarsEnvGenerator().Select(o => o[0])) { handlebars.RegisterHelper("one.two", (context, arguments) => 0); @@ -2068,8 +2107,8 @@ public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) var template = handlebars.Compile(source); var actual = template(new { one = new { two = 42 } }); - - Assert.Equal("42", actual); + + Assert.Equal("42", actual); } } @@ -2111,12 +2150,12 @@ public void HtmlEncoderCompatibilityIntegration_LateChangeConfig(bool useLegacyH var handlebars = Handlebars.Create(config); handlebars.Configuration.TextEncoder = useLegacyHandlebarsNetHtmlEncoding ? (ITextEncoder)new HtmlEncoderLegacy() : new HtmlEncoder(); var compiledTemplate = handlebars.Compile(template); - + var actual = compiledTemplate(value); Assert.Equal(expected, actual); } - + [Fact] public void ChainedPathIteratorHelper() { @@ -2150,11 +2189,11 @@ public bool TryResolveHelper(PathInfo name, Type targetType, out IHelperDescript helper = null; return false; } - + helper = new HelperDescriptor(name, method); return true; } - + helper = null; return false; } @@ -2164,7 +2203,7 @@ public bool TryResolveBlockHelper(PathInfo name, out IHelperDescriptor { private readonly MethodInfo _methodInfo; @@ -2187,7 +2226,7 @@ public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Con } } } - + private class CustomUndefinedFormatter : IFormatter, IFormatterProvider { public void Format(T value, in EncodedTextWriter writer) @@ -2207,7 +2246,7 @@ public bool TryCreateFormatter(Type type, out IFormatter formatter) return true; } } - + private class CustomDateTimeFormatter : IFormatter, IFormatterProvider { private readonly string _format; @@ -2216,9 +2255,9 @@ private class CustomDateTimeFormatter : IFormatter, IFormatterProvider public void Format(T value, in EncodedTextWriter writer) { - if(!(value is DateTime dateTime)) + if (!(value is DateTime dateTime)) throw new ArgumentException("supposed to be DateTime"); - + writer.Write($"{dateTime.ToString(_format)}"); } From d27753b2d380c93558e97a1b32f97ff71f9ae833 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 22 Feb 2023 08:59:36 +0100 Subject: [PATCH 30/37] reformat file --- .../Handlebars.Test/BasicIntegrationTests.cs | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 73bcefb0..62aea4f6 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -1,3 +1,4 @@ +using Xunit; using System; using System.Collections; using System.Collections.Generic; @@ -5,14 +6,14 @@ using System.Linq; using System.Reflection; using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Features; using HandlebarsDotNet.Helpers; -using HandlebarsDotNet.IO; -using HandlebarsDotNet.PathStructure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using NSubstitute.ExceptionExtensions; -using Xunit; +using HandlebarsDotNet.Features; +using HandlebarsDotNet.IO; +using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.Runtime; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Test { @@ -440,6 +441,19 @@ public void PathRelativeBinding(IHandlebars handlebars) Assert.Equal("Garry Finch gazraa Karen Finch photobasics", actual); } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPropertyOnArray(IHandlebars handlebars) + { + var source = "Array is {{ names.Length }} item(s) long"; + var template = handlebars.Compile(source); + var data = new + { + names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } + }; + var result = template(data); + Assert.Equal("Array is 2 item(s) long", result); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void PathRelativeBinding_WithDefaultValue(IHandlebars handlebars) { @@ -530,19 +544,6 @@ public void PathRelativeBinding_WrongNumberOfArguments(IHandlebars handlebars) Assert.Equal("{{lookup}} helper must have two or three arguments", ex.Message); } - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] - public void BasicPropertyOnArray(IHandlebars handlebars) - { - var source = "Array is {{ names.Length }} item(s) long"; - var template = handlebars.Compile(source); - var data = new - { - names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } - }; - var result = template(data); - Assert.Equal("Array is 2 item(s) long", result); - } - [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void AliasedPropertyOnArray(IHandlebars handlebars) { @@ -1769,12 +1770,12 @@ public void TestNoWhitespaceBetweenExpressions(IHandlebars handlebars) var source = @"{{#is ProgramID """"}}no program{{/is}}{{#is ProgramID ""1081""}}some program text{{/is}}"; handlebars.RegisterHelper("is", (output, options, context, args) => + { + if (args[0] == args[1]) { - if (args[0] == args[1]) - { - options.Template(output, context); - } - }); + options.Template(output, context); + } + }); var template = handlebars.Compile(source); From e6f589049b039c51189d286223ee7201a98c6918 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 22 Feb 2023 09:03:08 +0100 Subject: [PATCH 31/37] cc --- .../Handlebars/Helpers/LookupReturnHelperDescriptor.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs index 655791de..7b8f80ff 100644 --- a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs @@ -4,7 +4,7 @@ namespace HandlebarsDotNet.Helpers { public sealed class LookupReturnHelperDescriptor : IHelperDescriptor { - public PathInfo Name => "lookup"; + public PathInfo Name { get; } = "lookup"; public object Invoke(in HelperOptions options, in Context context, in Arguments arguments) { @@ -15,11 +15,9 @@ public object Invoke(in HelperOptions options, in Context context, in Arguments var segment = ChainSegment.Create(arguments[1]); - var defaultValueIfNotFound = arguments.Length == 3 ? arguments[2] : UndefinedBindingResult.Create(segment); - - return options.TryAccessMember(arguments[0], segment, out var value) - ? value - : defaultValueIfNotFound; + return !options.TryAccessMember(arguments[0], segment, out var value) + ? arguments.Length == 3 ? arguments[2] : UndefinedBindingResult.Create(segment) + : value; } public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments) From ed5c5c4082e4ef721b2f8742c3267572023db4ba Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 22 Feb 2023 09:04:41 +0100 Subject: [PATCH 32/37] . --- source/Handlebars.Test/BasicIntegrationTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 62aea4f6..bc942451 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -682,8 +682,7 @@ public void GlobalDataPropagation(IHandlebars handlebars) input = new { first = 1, - second = new - { + second = new { third = 3 } } From 68e5209d2092a1896dc79f104c29ba4e630fba36 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 8 Dec 2023 18:51:47 +0100 Subject: [PATCH 33/37] Fix LiteralConverter to support long --- source/Handlebars.Test/NumericLiteralTests.cs | 56 +++++++++++++------ source/Handlebars/Arguments.cs | 2 +- .../Lexer/Converter/LiteralConverter.cs | 31 +++++----- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/source/Handlebars.Test/NumericLiteralTests.cs b/source/Handlebars.Test/NumericLiteralTests.cs index c9ab1683..552de90f 100644 --- a/source/Handlebars.Test/NumericLiteralTests.cs +++ b/source/Handlebars.Test/NumericLiteralTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using HandlebarsDotNet.Compiler; +using System.Linq; using Xunit; namespace HandlebarsDotNet.Test @@ -14,10 +12,37 @@ public NumericLiteralTests() var arr = args.AsEnumerable().Select(a => (object)int.Parse(a.ToString())); writer.Write(arr.Aggregate(0, (a, i) => a + (int)i)); }); + + Handlebars.RegisterHelper("longAdd", (writer, context, args) => + { + var arr = args.AsEnumerable().Select(a => long.Parse(a.ToString())); + var sum = arr.Sum(); + writer.Write(sum); + }); + } + + [Theory] + [InlineData("{{longAdd 1000000000 9999999999}}")] + [InlineData("{{longAdd 1000000000 9999999999}}")] + [InlineData("{{longAdd 1000000000 9999999999 }}")] + [InlineData("{{longAdd 1000000000 9999999999}}")] + [InlineData("{{longAdd 1000000000 9999999999}}")] + [InlineData("{{longAdd 1000000000 \"9999999999\"}}")] + [InlineData("{{longAdd 1000000000 \"9999999999\" }}")] + [InlineData("{{longAdd 1000000000 \"9999999999\"}}")] + [InlineData("{{longAdd 1000000000 \"9999999999\" }}")] + [InlineData("{{longAdd \"1000000000\" 9999999999}}")] + [InlineData("{{longAdd \"1000000000\" \"9999999999\"}}")] + public void NumericLiteralLongTests(string source) + { + var template = Handlebars.Compile(source); + var data = new { }; + var result = template(data); + Assert.Equal("10999999999", result); } [Fact] - public void NumericLiteralTest1() + public void NumericLiteralIntegerTest1() { var source = "{{numericLiteralAdd 3 4}}"; var template = Handlebars.Compile(source); @@ -27,7 +52,7 @@ public void NumericLiteralTest1() } [Fact] - public void NumericLiteralTest2() + public void NumericLiteralIntegerTest2() { var source = "{{numericLiteralAdd 3 4}}"; var template = Handlebars.Compile(source); @@ -37,7 +62,7 @@ public void NumericLiteralTest2() } [Fact] - public void NumericLiteralTest3() + public void NumericLiteralIntegerTest3() { var source = "{{numericLiteralAdd 3 4 }}"; var template = Handlebars.Compile(source); @@ -47,7 +72,7 @@ public void NumericLiteralTest3() } [Fact] - public void NumericLiteralTest4() + public void NumericLiteralIntegerTest4() { var source = "{{numericLiteralAdd 3 4 }}"; var template = Handlebars.Compile(source); @@ -57,7 +82,7 @@ public void NumericLiteralTest4() } [Fact] - public void NumericLiteralTest5() + public void NumericLiteralIntegerTest5() { var source = "{{numericLiteralAdd 3 4 }}"; var template = Handlebars.Compile(source); @@ -67,7 +92,7 @@ public void NumericLiteralTest5() } [Fact] - public void NumericLiteralTest6() + public void NumericLiteralIntegerTest6() { var source = "{{numericLiteralAdd 3 \"4\"}}"; var template = Handlebars.Compile(source); @@ -77,7 +102,7 @@ public void NumericLiteralTest6() } [Fact] - public void NumericLiteralTest7() + public void NumericLiteralIntegerTest7() { var source = "{{numericLiteralAdd 3 \"4\" }}"; var template = Handlebars.Compile(source); @@ -87,7 +112,7 @@ public void NumericLiteralTest7() } [Fact] - public void NumericLiteralTest8() + public void NumericLiteralIntegerTest8() { var source = "{{numericLiteralAdd 3 \"4\" }}"; var template = Handlebars.Compile(source); @@ -97,7 +122,7 @@ public void NumericLiteralTest8() } [Fact] - public void NumericLiteralTest9() + public void NumericLiteralIntegerTest9() { var source = "{{numericLiteralAdd 3 \"4\" }}"; var template = Handlebars.Compile(source); @@ -107,7 +132,7 @@ public void NumericLiteralTest9() } [Fact] - public void NumericLiteralTest10() + public void NumericLiteralIntegerTest10() { var source = "{{numericLiteralAdd \"3\" 4}}"; var template = Handlebars.Compile(source); @@ -117,7 +142,7 @@ public void NumericLiteralTest10() } [Fact] - public void NumericLiteralTest11() + public void NumericLiteralIntegerTest11() { var source = "{{numericLiteralAdd \"3\" 4 }}"; var template = Handlebars.Compile(source); @@ -126,5 +151,4 @@ public void NumericLiteralTest11() Assert.Equal("7", result); } } -} - +} \ No newline at end of file diff --git a/source/Handlebars/Arguments.cs b/source/Handlebars/Arguments.cs index 035a4b7a..1708a40a 100644 --- a/source/Handlebars/Arguments.cs +++ b/source/Handlebars/Arguments.cs @@ -58,7 +58,7 @@ public Arguments(object arg1) : this() Length = 1; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // [MethodImpl(MethodImplOptions.AggressiveInlining)] public Arguments(object arg1, object arg2) : this() { _useArray = false; diff --git a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs index 8c81689f..6bf092f1 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs @@ -1,15 +1,14 @@ -using System; using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Lexer; -using System.Linq.Expressions; using System.Linq; +using System.Linq.Expressions; +using HandlebarsDotNet.Compiler.Lexer; namespace HandlebarsDotNet.Compiler { internal class LiteralConverter : TokenConverter { private static readonly LiteralConverter Converter = new LiteralConverter(); - + public static IEnumerable Convert(IEnumerable sequence) { return Converter.ConvertTokens(sequence).ToList(); @@ -28,15 +27,22 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) switch (item) { case LiteralExpressionToken literalExpression: - { - result = Expression.Convert(Expression.Constant(literalExpression.Value), typeof(object)); - if (!literalExpression.IsDelimitedLiteral && int.TryParse(literalExpression.Value, out var intValue)) { - result = Expression.Convert(Expression.Constant(intValue), typeof(object)); + result = Expression.Convert(Expression.Constant(literalExpression.Value), typeof(object)); + if (!literalExpression.IsDelimitedLiteral) + { + if (int.TryParse(literalExpression.Value, out var intValue)) + { + result = Expression.Convert(Expression.Constant(intValue), typeof(object)); + } + else if (long.TryParse(literalExpression.Value, out var longValue)) + { + result = Expression.Convert(Expression.Constant(longValue), typeof(object)); + } + } + + break; } - - break; - } case WordExpressionToken wordExpression when bool.TryParse(wordExpression.Value, out var boolValue): result = Expression.Convert(Expression.Constant(boolValue), typeof(object)); @@ -47,5 +53,4 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) } } } -} - +} \ No newline at end of file From fcee926811c75abc9d65a740067922a1ba17901c Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 8 Dec 2023 19:14:46 +0100 Subject: [PATCH 34/37] java 17 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/pull_request.yml | 2 +- source/Handlebars.sln | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0330d4e5..48139aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,9 +60,9 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.x - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: - java-version: '13' # The JDK version to make available on the path. + java-version: '17' # The JDK version to make available on the path. - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155 working-directory: ./source run: dotnet clean -c Release && dotnet nuget locals all --clear diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fae4b16c..97d02eb2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -62,7 +62,7 @@ jobs: dotnet-version: 3.1.x - uses: actions/setup-java@v1 with: - java-version: '13' # The JDK version to make available on the path. + java-version: '17' # The JDK version to make available on the path. - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155 working-directory: ./source run: dotnet clean -c Release && dotnet nuget locals all --clear diff --git a/source/Handlebars.sln b/source/Handlebars.sln index adaa5299..fa8e7177 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -9,11 +9,19 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props + ..\.github\workflows\pull_request.yml = ..\.github\workflows\pull_request.yml ..\README.md = ..\README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{417E2E51-2DD2-4045-84E5-BA66484E957B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github Actions", "Github Actions", "{0683EE49-625C-473D-B600-079FDB9AF55B}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml + ..\.github\workflows\pull_request.yml = ..\.github\workflows\pull_request.yml + ..\.github\workflows\release.yml = ..\.github\workflows\release.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 0838af83af7df8478449db61ef0e605e56a0a48b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 15 Dec 2023 08:30:50 +0100 Subject: [PATCH 35/37] [MethodImpl(MethodImplOptions.AggressiveInlining)] --- source/Handlebars/Arguments.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars/Arguments.cs b/source/Handlebars/Arguments.cs index 1708a40a..035a4b7a 100644 --- a/source/Handlebars/Arguments.cs +++ b/source/Handlebars/Arguments.cs @@ -58,7 +58,7 @@ public Arguments(object arg1) : this() Length = 1; } - // [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Arguments(object arg1, object arg2) : this() { _useArray = false; From 8b6bc71ca33acfff3d604bb04794119bb913f8d3 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 5 Jan 2024 09:07:05 +0100 Subject: [PATCH 36/37] Use PackageLicenseExpression in NuGet package --- source/Directory.Build.props | 46 +++++++++++++++--------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 6fd7da97..f2a72002 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -1,29 +1,21 @@ - - false - ToBeObtained.snk - - - - LICENSE - true - Rex Morgan; Handlebars-Net - true - false - true - snupkg - 9 - - - - 1591;1574;1584;1658 - - - - - false - true - . - - + + false + ToBeObtained.snk + + + + MIT + true + Rex Morgan; Handlebars-Net + true + false + true + snupkg + 9 + + + + 1591;1574;1584;1658 + \ No newline at end of file From 3f10b2822d1c0f4e9788e7ab62226c1ab9083899 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Mon, 1 Apr 2024 17:24:14 +1100 Subject: [PATCH 37/37] Further csproj cleanup --- .github/workflows/ci.yml | 9 +++--- source/Handlebars.Test/Handlebars.Test.csproj | 16 +++++----- source/Handlebars/Handlebars.csproj | 31 ++++--------------- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 967989c4..3059dd4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: [ master ] - + jobs: build: name: Build @@ -60,6 +60,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | + 2.1.x 3.1.x 5.0.x 6.0.x @@ -99,7 +100,7 @@ jobs: .\.sonar\scanner\dotnet-sonarscanner begin /k:"Handlebars-Net_Handlebars.Net" /o:"handlebars-net" /d:sonar.login="${{ env.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.coverage.exclusions="**/*.md;source/Handlebars.Benchmark/**/*.*" /d:sonar.cpd.exclusions="source/Handlebars/Iterators/**/*.*" dotnet build source/Handlebars.sln -c Release .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ env.SONAR_TOKEN }}" - + benchmark: name: Run Benchmark.Net runs-on: ubuntu-latest @@ -143,8 +144,8 @@ jobs: uses: actions/upload-artifact@v2 with: name: Benchmark - path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/ - + path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/ + update_release_draft: name: Release Drafter runs-on: ubuntu-latest diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 062526f4..c517dc50 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -10,7 +10,7 @@ false true - + 0618;1701 @@ -27,11 +27,11 @@ $(DefineConstants);netFramework - + $(DefineConstants);netcoreapp;netstandard - + $(DefineConstants);netcoreapp;netstandard @@ -60,7 +60,7 @@ - + @@ -68,18 +68,18 @@ - - + + - + - + diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 039e1fc9..a9e2d18e 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -39,7 +39,7 @@ https://github.com/Handlebars-Net/Handlebars.Net/releases/tag/$(Version) true - + false @@ -47,7 +47,7 @@ . - + @@ -58,40 +58,21 @@ - + - - - - - - - - - - - - - + - - - - - all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - +