From 9d52004d26321e6f9f367bc3f7eb276c0246ef04 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 13 Oct 2020 18:59:16 +0300 Subject: [PATCH 1/4] Introduce reactive helper registration, improve general performance --- README.md | 33 +- source/Directory.Build.props | 3 +- source/Handlebars.Benchmark/Program.cs | 3 +- .../Handlebars.Test/BasicIntegrationTests.cs | 56 +- .../Collections/FixedSizeDictionaryTests.cs | 273 ++++++++ .../ComplexIntegrationTests.cs | 5 +- source/Handlebars.Test/HelperTests.cs | 125 +++- source/Handlebars.Test/IteratorTests.cs | 5 +- source/Handlebars.Test/PartialTests.cs | 10 +- source/Handlebars.Test/SubExpressionTests.cs | 12 +- .../ViewEngine/ViewEngineTests.cs | 5 +- .../Adapters/HelperToReturnHelperAdapter.cs | 27 - source/Handlebars/Adapters/LambdaEnricher.cs | 24 - source/Handlebars/Adapters/LambdaReducer.cs | 27 - .../Collections/CascadeCollection.cs | 68 -- .../Collections/CascadeDictionary.cs | 73 +-- .../Handlebars/Collections/DeferredValue.cs | 2 + .../Handlebars/Collections/DictionaryPool.cs | 36 ++ .../Collections/DisposableContainer.cs | 2 +- .../Collections/ExtendedEnumerable.cs | 10 +- .../Collections/FixedSizeDictionary.cs | 590 ++++++++++++++++++ source/Handlebars/Collections/HashSetSlim.cs | 33 - source/Handlebars/Collections/LookupSlim.cs | 60 +- source/Handlebars/Collections/ObjectPool.cs | 70 ++- .../Collections/ObservableDictionary.cs | 308 +++++++++ .../Handlebars/Collections/ObservableList.cs | 257 ++++++++ .../Handlebars/Collections/ObserverBuilder.cs | 53 ++ .../Collections/StringBuilderPool.cs | 24 +- .../Handlebars/Compiler/CompilationContext.cs | 4 +- .../Handlebars/Compiler/ExpressionBuilder.cs | 4 +- source/Handlebars/Compiler/FunctionBuilder.cs | 10 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 20 +- .../Lexer/Converter/BlockAccumulator.cs | 6 +- .../BlockAccumulatorContext.cs | 8 +- .../Converter/ExpressionScopeConverter.cs | 2 +- .../Lexer/Converter/HelperConverter.cs | 13 +- .../Compiler/Structure/BindingContext.Pool.cs | 70 +++ .../Compiler/Structure/BindingContext.cs | 258 +++----- .../Structure/BlockParamsExpression.cs | 14 +- .../Structure/BlockParamsValueProvider.cs | 118 ---- .../Compiler/Structure/Path/ChainSegment.cs | 161 ++++- .../Compiler/Structure/Path/PathInfo.cs | 119 +++- .../Compiler/Structure/Path/PathResolver.cs | 392 +++--------- .../Compiler/Structure/Path/PathSegment.cs | 44 +- .../Structure/UndefinedBindingResult.cs | 8 +- .../Expression/BlockHelperFunctionBinder.cs | 263 +++----- .../Translation/Expression/ContextBinder.cs | 66 +- .../Expression/DeferredSectionBlockHelper.cs | 24 +- .../Expression/HelperFunctionBinder.cs | 83 +-- .../Translation/Expression/IteratorBinder.cs | 312 ++++----- .../Translation/Expression/PartialBinder.cs | 19 +- .../Translation/Expression/PathBinder.cs | 43 +- .../Translation/Expression/StaticReplacer.cs | 5 +- .../Expression/SubExpressionVisitor.cs | 83 +-- .../Configuration/CompileTimeConfiguration.cs | 15 +- .../Configuration/HandlebarsConfiguration.cs | 58 +- .../ICompiledHandlebarsConfiguration.cs | 86 ++- .../InternalHandlebarsConfiguration.cs | 178 ++++-- .../Handlebars/Configuration/PathInfoStore.cs | 16 +- source/Handlebars/EnumerableExtensions.cs | 33 + .../Features/BuildInHelpersFeature.cs | 82 +-- source/Handlebars/Features/ClosureFeature.cs | 7 + .../CollectionMemberAliasProviderFeature.cs | 38 ++ .../Features/DefaultCompilerFeature.cs | 27 +- .../Features/MissingHelperFeature.cs | 163 ++--- source/Handlebars/Features/WarmUpFeature.cs | 1 - .../FileSystemPartialTemplateResolver.cs | 7 +- source/Handlebars/Handlebars.cs | 69 ++ source/Handlebars/Handlebars.csproj | 5 +- source/Handlebars/HandlebarsEnvironment.cs | 91 ++- source/Handlebars/HandlebarsExtensions.cs | 35 +- source/Handlebars/HelperOptions.cs | 106 ++-- .../BlockHelpers/BlockHelperDescriptor.cs | 31 + .../BlockHelpers/BlockHelperDescriptorBase.cs | 18 + .../DelegateBlockHelperDescriptor.cs | 45 ++ .../DelegateReturnBlockHelperDescriptor.cs | 24 + .../InlineBlockHelperDescriptor.cs | 38 ++ .../LateBindBlockHelperDescriptor.cs | 46 ++ .../MissingBlockHelperDescriptor.cs | 23 + .../ReturnBlockHelperDescriptor.cs | 23 + .../BlockHelpers/WithBlockHelperDescriptor.cs | 36 ++ .../Helpers/DelegateHelperDescriptor.cs | 25 + .../Helpers/DelegateReturnHelperDescriptor.cs | 24 + source/Handlebars/Helpers/HelperDescriptor.cs | 30 + .../Helpers/HelperDescriptorBase.cs | 37 ++ source/Handlebars/Helpers/HelperType.cs | 10 + .../Handlebars/Helpers/IHelperDescriptor.cs | 11 + source/Handlebars/Helpers/IHelperResolver.cs | 7 +- .../Helpers/LateBindHelperDescriptor.cs | 58 ++ .../Helpers/LookupReturnHelperDescriptor.cs | 35 ++ .../Helpers/MissingHelperDescriptor.cs | 27 + .../Helpers/ReturnHelperDescriptor.cs | 44 ++ source/Handlebars/IHandlebars.cs | 17 + .../IMissingPartialTemplateHandler.cs | 2 +- source/Handlebars/IO/EncodedTextWriter.cs | 60 +- source/Handlebars/IO/PolledStringWriter.cs | 36 +- .../MemberAccessors/ContextMemberAccessor.cs | 16 - .../GenericClassDictionaryAccessor.cs | 28 + .../GenericStructDictionaryAccessor.cs | 30 + .../StringClassDictionaryAccessor.cs | 24 + .../StringStructDictionaryAccessor.cs | 26 + .../DictionaryMemberAccessor.cs | 5 +- .../MemberAccessors/DynamicMemberAccessor.cs | 5 +- .../ClassEnumerableMemberAccessor.cs | 17 + .../EnumerableMemberAccessor.cs | 75 +++ .../EnumerableAccessors/ListMemberAccessor.cs | 16 + .../ReadOnlyListMemberAccessor.cs | 16 + .../StructEnumerableMemberAccessor.cs | 17 + .../StructListMemberAccessor.cs | 24 + .../StructReadOnlyListMemberAccessor.cs | 24 + .../EnumerableMemberAccessor.cs | 35 -- .../MemberAccessors/IMemberAccessor.cs | 31 +- .../MemberAccessors/MergedMemberAccessor.cs | 26 + .../ReflectionMemberAccessor.cs | 152 ++--- .../CollectionMemberAliasProvider.cs | 55 +- .../DelegatedMemberAliasProvider.cs | 4 +- .../IMemberAliasProvider.cs | 3 +- .../CollectionObjectDescriptor.cs | 56 +- .../ContextObjectDescriptor.cs | 28 - .../DictionaryObjectDescriptor.cs | 15 +- .../DynamicObjectDescriptor.cs | 22 +- .../EnumerableObjectDescriptor.cs | 35 +- ...nericDictionaryObjectDescriptorProvider.cs | 100 +-- .../IObjectDescriptorProvider.cs | 9 +- .../KeyValuePairObjectDescriptorProvider.cs | 42 +- .../ObjectDescriptors/ObjectDescriptor.cs | 18 +- .../ObjectDescriptorFactory.cs | 27 +- .../ObjectDescriptorProvider.cs | 67 +- ...tringDictionaryObjectDescriptorProvider.cs | 55 +- source/Handlebars/ObjectExtensions.cs | 20 - source/Handlebars/Polyfills/ArrayEx.cs | 2 + .../Handlebars/Polyfills/StringExtensions.cs | 15 - source/Handlebars/TypeExtensions.cs | 37 ++ .../BindingContextValueProvider.cs | 57 -- .../ValueProviders/BlockParamsValues.cs | 60 ++ .../Handlebars/ValueProviders/DataValues.cs | 88 +++ .../ValueProviders/IValueProvider.cs | 18 - .../ValueProviders/IteratorValueProvider.cs | 78 --- .../ValueProviders/IteratorValues.cs | 50 ++ .../ObjectEnumeratorValueProvider.cs | 70 --- .../ValueProviders/ObjectIteratorValues.cs | 73 +++ 141 files changed, 5076 insertions(+), 2662 deletions(-) create mode 100644 source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs delete mode 100644 source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs delete mode 100644 source/Handlebars/Adapters/LambdaEnricher.cs delete mode 100644 source/Handlebars/Adapters/LambdaReducer.cs delete mode 100644 source/Handlebars/Collections/CascadeCollection.cs create mode 100644 source/Handlebars/Collections/DictionaryPool.cs create mode 100644 source/Handlebars/Collections/FixedSizeDictionary.cs delete mode 100644 source/Handlebars/Collections/HashSetSlim.cs create mode 100644 source/Handlebars/Collections/ObservableDictionary.cs create mode 100644 source/Handlebars/Collections/ObservableList.cs create mode 100644 source/Handlebars/Collections/ObserverBuilder.cs create mode 100644 source/Handlebars/Compiler/Structure/BindingContext.Pool.cs delete mode 100644 source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs create mode 100644 source/Handlebars/Features/CollectionMemberAliasProviderFeature.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/DelegateHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/HelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/HelperDescriptorBase.cs create mode 100644 source/Handlebars/Helpers/HelperType.cs create mode 100644 source/Handlebars/Helpers/IHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/LateBindHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/MissingHelperDescriptor.cs create mode 100644 source/Handlebars/Helpers/ReturnHelperDescriptor.cs delete mode 100644 source/Handlebars/MemberAccessors/ContextMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DictionaryAccessors/GenericClassDictionaryAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DictionaryAccessors/GenericStructDictionaryAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DictionaryAccessors/StringClassDictionaryAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DictionaryAccessors/StringStructDictionaryAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/ClassEnumerableMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/ListMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/ReadOnlyListMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/StructEnumerableMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/StructListMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableAccessors/StructReadOnlyListMemberAccessor.cs delete mode 100644 source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/MergedMemberAccessor.cs delete mode 100644 source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs delete mode 100644 source/Handlebars/Polyfills/StringExtensions.cs create mode 100644 source/Handlebars/TypeExtensions.cs delete mode 100644 source/Handlebars/ValueProviders/BindingContextValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/BlockParamsValues.cs create mode 100644 source/Handlebars/ValueProviders/DataValues.cs delete mode 100644 source/Handlebars/ValueProviders/IValueProvider.cs delete mode 100644 source/Handlebars/ValueProviders/IteratorValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/IteratorValues.cs delete mode 100644 source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/ObjectIteratorValues.cs diff --git a/README.md b/README.md index 6b13611f..ca014cc9 100644 --- a/README.md +++ b/README.md @@ -124,29 +124,24 @@ Views\{Controller}\{Action}\partials\somepartial.hbs ### Registering Block Helpers ```c# -HandlebarsBlockHelper _stringEqualityBlockHelper = (TextWriter output, HelperOptions options, dynamic context, object[] arguments) => { - if (arguments.Length != 2) - { - throw new HandlebarsException("{{StringEqualityBlockHelper}} helper must have exactly two argument"); - } - string left = arguments[0] as string; - string right = arguments[1] as string; - if (left == right) - { - options.Template(output, null); - } - else - { - options.Inverse(output, null); - } -}; -Handlebars.RegisterHelper("StringEqualityBlockHelper", _stringEqualityBlockHelper); -Dictionary animals = new Dictionary() { +Handlebars.RegisterHelper("StringEqualityBlockHelper", (TextWriter output, HelperOptions options, dynamic context, object[] arguments) => +{ + if (arguments.Length != 2) throw new HandlebarsException("{{StringEqualityBlockHelper}} helper must have exactly two argument"); + + var left = arguments[0] as string; + var right = arguments[1] as string; + if (left == right) options.Template(output, null); + else options.Inverse(output, null); +}); + +var animals = new Dictionary() +{ {"Fluffy", "cat" }, {"Fido", "dog" }, {"Chewy", "hamster" } }; -string template = "{{#each @value}}The animal, {{@key}}, {{StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\r\n{{/each}}"; + +string template = "{{#each this}}The animal, {{@key}}, {{#StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\r\n{{/each}}"; Func compiledTemplate = Handlebars.Compile(template); string templateOutput = compiledTemplate(animals); diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 1e8aae52..80ced1fa 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -10,10 +10,11 @@ true snupkg Rex Morgan; Handlebars-Net + 8 - 1591 + 1591;1574;1584;1658 diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs index 2342e257..87715600 100644 --- a/source/Handlebars.Benchmark/Program.cs +++ b/source/Handlebars.Benchmark/Program.cs @@ -14,8 +14,7 @@ public static void Main(string[] args) .WithLaunchCount(1); var manualConfig = DefaultConfig.Instance - .AddJob(job) - .WithOptions(ConfigOptions.JoinSummary); + .AddJob(job); manualConfig.AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByMethod); diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 1338de34..5c2ee3f7 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Dynamic; +using System.IO; using System.Linq; using System.Reflection; using HandlebarsDotNet.Compiler; @@ -10,6 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers.BlockHelpers; using Xunit.Abstractions; namespace HandlebarsDotNet.Test @@ -19,7 +21,6 @@ public class HandlebarsEnvGenerator : IEnumerable private readonly List _data = new List { Handlebars.Create(), - Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)), Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { @@ -328,7 +329,7 @@ public void CustomAliasedPropertyOnArray(IHandlebars handlebars) var aliasProvider = new DelegatedMemberAliasProvider() .AddAlias("myCountAlias", list => list.Count); - handlebars.Configuration.CompileTimeConfiguration.AliasProviders.Add(aliasProvider); + handlebars.Configuration.AliasProviders.Add(aliasProvider); var source = "Array is {{ names.myCountAlias }} item(s) long"; var template = handlebars.Compile(source); @@ -616,6 +617,42 @@ public void BasicDictionaryEnumerator(IHandlebars handlebars) var result = template(data); Assert.Equal("hello world ", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionaryEnumeratorDeep(IHandlebars handlebars) + { + var source = "{{#each enumerateMe}}{{this.inner.a}}{{this.inner.b}}{{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + enumerateMe = new Dictionary + { + { + "foo", new Dictionary + { + ["inner"] = new Dictionary + { + ["a"] = "1", + ["b"] = "2" + } + } + }, + { + "bar", new Dictionary + { + ["inner"] = new Dictionary + { + ["a"] = "3", + ["b"] = "4" + } + } + } + } + }; + + var result = template(data); + Assert.Equal("1234", result); + } [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void DictionaryEnumeratorWithBlockParams(IHandlebars handlebars) @@ -1633,8 +1670,7 @@ public void BasicReturnFromHelper(IHandlebars Handlebars) [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void CollectionReturnFromHelper(IHandlebars handlebars) { - var getData = $"getData{Guid.NewGuid()}"; - handlebars.RegisterHelper(getData, (context, arguments) => + handlebars.RegisterHelper($"getData", (context, arguments) => { var data = new Dictionary { @@ -1644,7 +1680,7 @@ public void CollectionReturnFromHelper(IHandlebars handlebars) return data; }); - var source = $"{{{{#each ({getData} 'Darmstadt' 'San Francisco')}}}}{{{{@key}}}} lives in {{{{@value}}}}. {{{{/each}}}}"; + var source = "{{#each (getData 'Darmstadt' 'San Francisco')}}{{@key}} lives in {{@value}}. {{/each}}"; var template = handlebars.Compile(source); var result = template(new object()); @@ -1788,9 +1824,10 @@ private void CustomHelperResolverTest(IHandlebars handlebars) [InlineData("one.two")] public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) { + var source = "{{ ./" + helperName + " }}"; + foreach (IHandlebars handlebars in new HandlebarsEnvGenerator().Select(o => o[0])) { - var source = "{{ ./" + helperName + " }}"; handlebars.RegisterHelper("one.two", (context, arguments) => 0); var template = handlebars.Compile(source); @@ -1803,7 +1840,7 @@ public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) private class StringHelperResolver : IHelperResolver { - public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) + public bool TryResolveHelper(string name, Type targetType, out HelperDescriptorBase helper) { if (targetType == typeof(string)) { @@ -1816,7 +1853,8 @@ public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsR return false; } - helper = (context, arguments) => method.Invoke(arguments[0], arguments.Skip(1).ToArray()); + object Helper(dynamic context, object[] arguments) => method.Invoke(arguments[0], arguments.Skip(1).ToArray()); + helper = new DelegateReturnHelperDescriptor(name, Helper); return true; } @@ -1824,7 +1862,7 @@ public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsR return false; } - public bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper) + public bool TryResolveBlockHelper(string name, out BlockHelperDescriptorBase helper) { helper = null; return false; diff --git a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs new file mode 100644 index 00000000..b7a055d4 --- /dev/null +++ b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Collections; +using Xunit; + +namespace HandlebarsDotNet.Test.Collections +{ + public class FixedSizeDictionaryTests : IDisposable + { + public struct ObjectComparer : IEqualityComparer + { + public bool Equals(object x, object y) => ReferenceEquals(x, y); + + public int GetHashCode(object obj) => obj.GetHashCode(); + } + + private static readonly FixedSizeDictionary FixedSizeDictionary; + + static FixedSizeDictionaryTests() + { + FixedSizeDictionary = new FixedSizeDictionary(15, 17, new ObjectComparer()); + } + + [Fact] + public void AddOrReplace() + { + var objects = new object[245]; + for (int i = 0; i < 245; i++) + { + objects[i] = new object(); + } + + for (int iteration = 0; iteration < 5; iteration++) + { + for (int i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + + Assert.True(FixedSizeDictionary.ContainsKey(index)); + Assert.True(FixedSizeDictionary.ContainsKey(key)); + Assert.True(FixedSizeDictionary.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(FixedSizeDictionary.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + } + } + + [Fact] + public void Add() + { + for (int i = 0; i < 245; i++) + { + var key = new object(); + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + + Assert.True(FixedSizeDictionary.ContainsKey(index)); + Assert.True(FixedSizeDictionary.ContainsKey(key)); + Assert.True(FixedSizeDictionary.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(FixedSizeDictionary.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + } + + [Fact] + public void Copy() + { + var keys = new object[245]; + var values = new object[245]; + for (int i = 0; i < 245; i++) + { + keys[i] = new object(); + values[i] = new object(); + } + + var indexes = new EntryIndex[245]; + for (int i = 0; i < 245; i++) + { + var key = keys[i]; + var value = values[i]; + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + indexes[i] = index; + + Assert.True(FixedSizeDictionary.ContainsKey(index)); + Assert.True(FixedSizeDictionary.ContainsKey(key)); + Assert.True(FixedSizeDictionary.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(FixedSizeDictionary.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + + var destination = new FixedSizeDictionary(15, 17, new ObjectComparer()); + FixedSizeDictionary.CopyTo(destination); + FixedSizeDictionary.Clear(); + + for (int i = 0; i < 245; i++) + { + var key = keys[i]; + var value = values[i]; + var index = indexes[i]; + + Assert.True(destination.ContainsKey(index)); + Assert.True(destination.ContainsKey(key)); + Assert.True(destination.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(destination.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + } + + [Fact] + public void ResetClears() + { + var objects = new object[245]; + for (var i = 0; i < 245; i++) + { + objects[i] = new object(); + } + + var indexes = new EntryIndex[245]; + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + indexes[i] = index; + } + + FixedSizeDictionary.Reset(); + + for (var i = 0; i < 245; i++) + { + Assert.False(FixedSizeDictionary.ContainsKey(objects[i])); + Assert.False(FixedSizeDictionary.ContainsKey(indexes[i])); + + Assert.False(FixedSizeDictionary.TryGetValue(objects[i], out _)); + Assert.False(FixedSizeDictionary.TryGetValue(indexes[i], out _)); + } + } + + [Fact] + public void CanAddAfterReset() + { + var objects = new object[245]; + for (var i = 0; i < 245; i++) + { + objects[i] = new object(); + } + + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out _); + } + + FixedSizeDictionary.Reset(); + + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + + Assert.True(FixedSizeDictionary.ContainsKey(index)); + Assert.True(FixedSizeDictionary.ContainsKey(key)); + Assert.True(FixedSizeDictionary.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(FixedSizeDictionary.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + } + + [Fact] + public void ClearClears() + { + var objects = new object[245]; + for (var i = 0; i < 245; i++) + { + objects[i] = new object(); + } + + var indexes = new EntryIndex[245]; + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + indexes[i] = index; + } + + FixedSizeDictionary.Clear(); + + for (var i = 0; i < 245; i++) + { + Assert.False(FixedSizeDictionary.ContainsKey(objects[i])); + Assert.False(FixedSizeDictionary.ContainsKey(indexes[i])); + + Assert.False(FixedSizeDictionary.TryGetValue(objects[i], out _)); + Assert.False(FixedSizeDictionary.TryGetValue(indexes[i], out _)); + } + } + + [Fact] + public void CanAddAfterClear() + { + var objects = new object[245]; + for (var i = 0; i < 245; i++) + { + objects[i] = new object(); + } + + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out _); + } + + FixedSizeDictionary.Clear(); + + for (var i = 0; i < 245; i++) + { + var key = objects[i]; + var value = new object(); + + FixedSizeDictionary.AddOrReplace(key, value, out var index); + + Assert.True(FixedSizeDictionary.ContainsKey(index)); + Assert.True(FixedSizeDictionary.ContainsKey(key)); + Assert.True(FixedSizeDictionary.TryGetValue(key, out var actualValue)); + + Assert.Equal(value, actualValue); + + Assert.True(FixedSizeDictionary.TryGetValue(index, out actualValue)); + + Assert.Equal(value, actualValue); + } + } + + public void Dispose() + { + FixedSizeDictionary.OptionalClear(); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/ComplexIntegrationTests.cs b/source/Handlebars.Test/ComplexIntegrationTests.cs index 19554726..2f365b86 100644 --- a/source/Handlebars.Test/ComplexIntegrationTests.cs +++ b/source/Handlebars.Test/ComplexIntegrationTests.cs @@ -93,8 +93,9 @@ public void IfImplicitIteratorHelper() [Fact] public void BlockHelperWithSameNameVariable() { + var handlebars = Handlebars.Create(); var source = "{{#block_helper}}{{block_helper}}{{/block_helper}}"; - Handlebars.RegisterHelper("block_helper", (writer, options, context, arguments) => + handlebars.RegisterHelper("block_helper", (writer, options, context, arguments) => { options.Template(writer, context); }); @@ -102,7 +103,7 @@ public void BlockHelperWithSameNameVariable() { block_helper = "block_helperValue" }; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(data); Assert.Equal("block_helperValue", result); diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index a09cd202..01a9b96a 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -1,8 +1,13 @@ using Xunit; using System; +using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers.BlockHelpers; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Test { @@ -71,26 +76,60 @@ public void HelperWithDotSeparatedNameWithParameters(string helperName) [Fact] public void BlockHelperWithBlockParams() { - Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { - var count = 0; - options.BlockParams((parameters, binder, deps) => - { - binder(parameters.ElementAtOrDefault(0), ctx => ++count); - }); + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("myHelper", (writer, options, context, args) => + { + using var frame = options.CreateFrame(); + var blockParamsValues = new BlockParamsValues(frame, options.BlockParams); + blockParamsValues.CreateProperty(0, out var _0); - foreach(var arg in args) + for (var index = 0; index < args.Length; index++) { - options.Template(writer, arg); + var arg = args[index]; + blockParamsValues[_0] = index; + frame.Value = arg; + options.Template(writer, frame); } }); var source = "Here are some things: {{#myHelper 'foo' 'bar' as |counter|}}{{counter}}:{{this}}\n{{/myHelper}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); - var expected = "Here are some things: 1:foo\n2:bar\n"; + var expected = "Here are some things: 0:foo\n1:bar\n"; + + Assert.Equal(expected, output); + } + + [Fact] + public void BlockHelperLateBindWithBlockParams() + { + var handlebars = Handlebars.Create(); + + var source = "Here are some things: {{#myHelper 'foo' 'bar' as |counter|}}{{counter}}:{{this}}\n{{/myHelper}}"; + + var template = handlebars.Compile(source); + + handlebars.RegisterHelper("myHelper", (writer, options, context, args) => + { + using var frame = options.CreateFrame(); + var blockParamsValues = new BlockParamsValues(frame, options.BlockParams); + blockParamsValues.CreateProperty(0, out var _0); + + for (var index = 0; index < args.Length; index++) + { + var arg = args[index]; + blockParamsValues[_0] = index; + frame.Value = arg; + options.Template(writer, frame); + } + }); + + var output = template(new { }); + + var expected = "Here are some things: 0:foo\n1:bar\n"; Assert.Equal(expected, output); } @@ -105,20 +144,24 @@ public void BlockHelperLateBound() var template = Handlebars.Compile(source); - Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { - var count = 0; - options.BlockParams((parameters, binder, deps) => - binder(parameters.ElementAtOrDefault(0), ctx => ++count)); + Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => + { + using var frame = options.CreateFrame(); + var blockParamsValues = new BlockParamsValues(frame, options.BlockParams); + blockParamsValues.CreateProperty(0, out var _0); - foreach(var arg in args) + for (var index = 0; index < args.Length; index++) { - options.Template(writer, arg); + var arg = args[index]; + blockParamsValues[_0] = index; + frame.Value = arg; + options.Template(writer, frame); } }); var output = template(new { }); - var expected = "Here are some things: \n1:foo\n2:bar\n"; + var expected = "Here are some things: \n0:foo\n1:bar\n"; Assert.Equal(expected, output); } @@ -256,11 +299,11 @@ public void MissingHelperHookViaFeatureAndMethod() var handlebars = Handlebars.Create(); handlebars.Configuration .RegisterMissingHelperHook( - (context, arguments) => expected + (context, arguments) => "Should be ignored" ); handlebars.RegisterHelper("helperMissing", - (context, arguments) => "Should be ignored" + (context, arguments) => expected ); var source = "{{missing}}"; @@ -309,7 +352,7 @@ public void MissingBlockHelperHook(string helperName) .RegisterMissingHelperHook( blockHelperMissing: (writer, options, context, arguments) => { - var name = options.GetValue("name"); + var name = options.GetValue("name").ToString(); writer.WriteSafeString(string.Format(format, name.Trim('[', ']'))); }); @@ -437,7 +480,7 @@ public void InversionNonEmptySequence() var template = Handlebars.Compile(source); var data = new { - key = new string[] { "element" } + key = new[] { "element" } }; var output = template(data); var expected = ""; @@ -809,6 +852,46 @@ public void HelperWithLiteralValues() var output = template(data); Assert.Equal("True 1 abc", output); } + + [Fact] + public void BlockHelperWithCustomIndex() + { + var handlebars = Handlebars.Create(); + + handlebars.RegisterHelper(new CustomEachBlockHelper()); + + var template = handlebars.Compile("{{#customEach this}}{{@value}}'s index is {{@index}} {{/customEach}}"); + + var result = template(new[] { "one", "two" }); + + Assert.Equal("one's index is 0 two's index is 1 ", result); + } + + private class CustomEachBlockHelper : BlockHelperDescriptor + { + public CustomEachBlockHelper() : base("customEach") + { + } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + { + using var frame = options.CreateFrame(); + frame.Data.CreateProperty(ChainSegment.Index, out var index); + frame.Data.CreateProperty(ChainSegment.Value, null, out var value); + + var iterationIndex = 0; + foreach (var item in (IEnumerable) arguments[0]) + { + frame.Data[index] = iterationIndex; + frame.Data[value] = item; + frame.Value = item; + + options.Template(output, frame); + + ++iterationIndex; + } + } + } } } diff --git a/source/Handlebars.Test/IteratorTests.cs b/source/Handlebars.Test/IteratorTests.cs index e5d43f55..5b7bd754 100644 --- a/source/Handlebars.Test/IteratorTests.cs +++ b/source/Handlebars.Test/IteratorTests.cs @@ -68,6 +68,8 @@ public void WithIndex() [Fact] public void WithParentIndex() { + var handlebars = Handlebars.Create(); + var source = @" {{#each level1}} id={{id}} @@ -87,7 +89,8 @@ public void WithParentIndex() {{/each}} {{/each}} {{/each}}"; - var template = Handlebars.Create().Compile( source ); + + var template = handlebars.Compile( source ); var data = new { level1 = new[]{ diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs index 801f2800..85bea987 100644 --- a/source/Handlebars.Test/PartialTests.cs +++ b/source/Handlebars.Test/PartialTests.cs @@ -277,9 +277,9 @@ public void DynamicPartialWithHelperArguments() [Fact] public void DynamicPartialWithContext() { - var source = "Hello, {{> (lookup name) context }}!"; + var source = "Hello, {{> (lookup1 name) context }}!"; - Handlebars.RegisterHelper("lookup", (output, context, arguments) => + Handlebars.RegisterHelper("lookup1", (output, context, arguments) => { output.WriteSafeString(arguments[0]); }); @@ -309,9 +309,9 @@ public void DynamicPartialWithContext() [Fact] public void DynamicPartialWithParameters() { - var source = "Hello, {{> (lookup name) first='Marc' last='Smith' }}!"; + var source = "Hello, {{> (lookup1 name) first='Marc' last='Smith' }}!"; - Handlebars.RegisterHelper("lookup", (output, context, arguments) => + Handlebars.RegisterHelper("lookup1", (output, context, arguments) => { output.WriteSafeString(arguments[0]); }); @@ -599,7 +599,7 @@ public void TemplateWithSpecialNamedPartial() public class TestMissingPartialTemplateHandler : IMissingPartialTemplateHandler { - public void Handle(HandlebarsConfiguration configuration, string partialName, TextWriter textWriter) + public void Handle(ICompiledHandlebarsConfiguration configuration, string partialName, TextWriter textWriter) { textWriter.Write($"Partial Not Found: {partialName}"); } diff --git a/source/Handlebars.Test/SubExpressionTests.cs b/source/Handlebars.Test/SubExpressionTests.cs index a13a14eb..c67f0813 100644 --- a/source/Handlebars.Test/SubExpressionTests.cs +++ b/source/Handlebars.Test/SubExpressionTests.cs @@ -58,20 +58,20 @@ public void BasicSubExpressionWithStringLiteralArgument() [Fact] public void BasicSubExpressionWithHashArgument() { - var helperName = "helper-" + Guid.NewGuid().ToString(); //randomize helper name - var subHelperName = "subhelper-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(helperName, (writer, context, args) => { + var handlebars = Handlebars.Create(); + + handlebars.RegisterHelper("helper", (writer, context, args) => { writer.Write("Outer " + args[0]); }); - Handlebars.RegisterHelper(subHelperName, (writer, context, args) => { + handlebars.RegisterHelper("subhelper", (writer, context, args) => { var hash = args[0] as Dictionary; writer.Write("Inner " + hash["item1"] + "-" + hash["item2"]); }); - var source = "{{" + helperName + " (" + subHelperName + " item1='inner' item2='arg')}}"; + var source = "{{ helper (subhelper item1='inner' item2='arg')}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 63571124..39faec1d 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -111,7 +111,10 @@ public void CanLoadAViewWithALayoutInTheRootWithAVariable() Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output); } - [Fact] + /* @todo Implement @data property + * @body Implement @data property based on https://handlebarsjs.com/api-reference/data-variables.html + */ + [Fact(Skip = "add @data support: https://handlebarsjs.com/api-reference/data-variables.html")] public void CanRenderAGlobalVariable() { //Given a layout in the root which contains an @ variable diff --git a/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs b/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs deleted file mode 100644 index 5248d982..00000000 --- a/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace HandlebarsDotNet.Adapters -{ - // Will be removed in next iterations - internal class HelperToReturnHelperAdapter - { - private readonly HandlebarsHelper _helper; - private readonly HandlebarsReturnHelper _delegate; - - public HelperToReturnHelperAdapter(HandlebarsHelper helper) - { - _helper = helper; - _delegate = (context, arguments) => - { - using (var writer = new PolledStringWriter()) - { - _helper(writer, context, arguments); - return writer.ToString(); - } - }; - } - - public static implicit operator HandlebarsReturnHelper(HelperToReturnHelperAdapter adapter) - { - return adapter._delegate; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaEnricher.cs b/source/Handlebars/Adapters/LambdaEnricher.cs deleted file mode 100644 index 9e9467dd..00000000 --- a/source/Handlebars/Adapters/LambdaEnricher.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.IO; -using HandlebarsDotNet.Compiler; - -namespace HandlebarsDotNet.Adapters -{ - internal class LambdaEnricher - { - private readonly Action _direct; - private readonly Action _inverse; - - public LambdaEnricher(Action direct, Action inverse) - { - _direct = direct; - _inverse = inverse; - - Direct = (context, writer, arg) => _direct(writer, arg); - Inverse = (context, writer, arg) => _inverse(writer, arg); - } - - public readonly Action Direct; - public readonly Action Inverse; - } -} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaReducer.cs b/source/Handlebars/Adapters/LambdaReducer.cs deleted file mode 100644 index 9d82c093..00000000 --- a/source/Handlebars/Adapters/LambdaReducer.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.IO; -using HandlebarsDotNet.Compiler; - -namespace HandlebarsDotNet.Adapters -{ - // Will be removed in next iterations - internal class LambdaReducer - { - private readonly BindingContext _context; - private readonly Action _direct; - private readonly Action _inverse; - - public LambdaReducer(BindingContext context, Action direct, Action inverse) - { - _context = context; - _direct = direct; - _inverse = inverse; - - Direct = (writer, arg) => _direct(_context, writer, arg); - Inverse = (writer, arg) => _inverse(_context, writer, arg); - } - - public readonly Action Direct; - public readonly Action Inverse; - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/CascadeCollection.cs b/source/Handlebars/Collections/CascadeCollection.cs deleted file mode 100644 index d0518e3d..00000000 --- a/source/Handlebars/Collections/CascadeCollection.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace HandlebarsDotNet.Collections -{ - // Will be removed in next iterations - internal class CascadeCollection : ICollection - { - private readonly ICollection _outer; - private readonly ICollection _inner = new List(); - - public CascadeCollection(ICollection outer) - { - _outer = outer; - } - - public IEnumerator GetEnumerator() - { - foreach (var value in _outer) - { - yield return value; - } - - foreach (var value in _inner) - { - yield return value; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public void Add(T item) - { - _inner.Add(item); - } - - public void Clear() - { - _inner.Clear(); - } - - public bool Contains(T item) - { - return _inner.Contains(item) || _outer.Contains(item); - } - - public void CopyTo(T[] array, int arrayIndex) - { - foreach (var value in this) - { - array[arrayIndex] = value; - arrayIndex++; - } - } - - public bool Remove(T item) - { - return _inner.Remove(item); - } - - public int Count => _outer.Count + _inner.Count; - - public bool IsReadOnly => _inner.IsReadOnly; - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/CascadeDictionary.cs b/source/Handlebars/Collections/CascadeDictionary.cs index 206c5898..cc6fff80 100644 --- a/source/Handlebars/Collections/CascadeDictionary.cs +++ b/source/Handlebars/Collections/CascadeDictionary.cs @@ -6,32 +6,27 @@ namespace HandlebarsDotNet.Collections { internal class CascadeDictionary : IDictionary { - private readonly IDictionary _outer; - private readonly IDictionary _inner; + private static readonly Dictionary Empty = new Dictionary(); - public CascadeDictionary(IDictionary outer, IEqualityComparer comparer = null) + public IDictionary Outer { get; set; } + private readonly Dictionary _inner; + + public CascadeDictionary(IEqualityComparer comparer = null) { - _outer = outer; - _inner = new Dictionary(comparer); + Outer = Empty; + _inner = new Dictionary(comparer ?? EqualityComparer.Default); } public IEnumerator> GetEnumerator() { - foreach (var value in _outer) + foreach (var value in _inner) { - if (_inner.TryGetValue(value.Key, out var innerValue)) - { - yield return new KeyValuePair(value.Key, innerValue); - } - else - { - yield return value; - } + yield return value; } - foreach (var value in _inner) + foreach (var value in Outer) { - if (_outer.ContainsKey(value.Key)) continue; + if (_inner.ContainsKey(value.Key)) continue; yield return value; } @@ -44,22 +39,20 @@ IEnumerator IEnumerable.GetEnumerator() public void Add(KeyValuePair item) { - _inner.Add(item); + _inner.Add(item.Key, item.Value); } public void Clear() { + Outer = Empty; _inner.Clear(); } - public bool Contains(KeyValuePair item) - { - return _inner.Contains(item) || _outer.Contains(item); - } + public bool Contains(KeyValuePair item) => _inner.Contains(item) || Outer.Contains(item); public void CopyTo(KeyValuePair[] array, int arrayIndex) { - foreach (var value in _outer) + foreach (var value in Outer) { if (_inner.TryGetValue(value.Key, out var innerValue)) { @@ -72,29 +65,26 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) arrayIndex++; } } - + foreach (var value in _inner) { - if (_outer.ContainsKey(value.Key)) continue; - + if (Outer.ContainsKey(value.Key)) continue; + array[arrayIndex] = value; arrayIndex++; } } - public bool Remove(KeyValuePair item) - { - return _inner.Remove(item); - } + public bool Remove(KeyValuePair item) => _inner.Remove(item.Key); public int Count { get { - var count = _outer.Count; + var count = Outer.Count; foreach (var value in _inner) { - if (_outer.ContainsKey(value.Key)) continue; + if (Outer.ContainsKey(value.Key)) continue; count++; } @@ -102,31 +92,22 @@ public int Count } } - public bool IsReadOnly => _inner.IsReadOnly; + public bool IsReadOnly => false; - public bool ContainsKey(TKey key) - { - return _inner.ContainsKey(key) || _outer.ContainsKey(key); - } + public bool ContainsKey(TKey key) => _inner.ContainsKey(key) || Outer.ContainsKey(key); - public void Add(TKey key, TValue value) - { - _inner.Add(key, value); - } + public void Add(TKey key, TValue value) => _inner.Add(key, value); - public bool Remove(TKey key) - { - return _inner.Remove(key); - } + public bool Remove(TKey key) => _inner.Remove(key); public bool TryGetValue(TKey key, out TValue value) { - return _inner.TryGetValue(key, out value) || _outer.TryGetValue(key, out value); + return _inner.TryGetValue(key, out value) || Outer.TryGetValue(key, out value); } public TValue this[TKey key] { - get => !_inner.TryGetValue(key, out var value) ? _outer[key] : value; + get => !_inner.TryGetValue(key, out var value) ? Outer[key] : value; set => _inner[key] = value; } diff --git a/source/Handlebars/Collections/DeferredValue.cs b/source/Handlebars/Collections/DeferredValue.cs index fed3e601..4e5ffb42 100644 --- a/source/Handlebars/Collections/DeferredValue.cs +++ b/source/Handlebars/Collections/DeferredValue.cs @@ -27,5 +27,7 @@ public T Value return _value; } } + + public void Reset() => _isValueCreated = false; } } \ No newline at end of file diff --git a/source/Handlebars/Collections/DictionaryPool.cs b/source/Handlebars/Collections/DictionaryPool.cs new file mode 100644 index 00000000..ae143eca --- /dev/null +++ b/source/Handlebars/Collections/DictionaryPool.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal class DictionaryPool : InternalObjectPool> + { + private static readonly Lazy> Self = + new Lazy>(() => new DictionaryPool(EqualityComparer.Default)); + + public static DictionaryPool Shared => Self.Value; + + public DictionaryPool(IEqualityComparer comparer) : base(new Policy(comparer)) + { + } + + public DictionaryPool(IInternalObjectPoolPolicy> policy) : base(policy) + { + } + + private class Policy : IInternalObjectPoolPolicy> + { + private readonly IEqualityComparer _comparer; + + public Policy(IEqualityComparer comparer) => _comparer = comparer; + + public Dictionary Create() => new Dictionary(_comparer); + + public bool Return(Dictionary item) + { + item.Clear(); + return true; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/DisposableContainer.cs b/source/Handlebars/Collections/DisposableContainer.cs index 5d0e795b..9e0529b6 100644 --- a/source/Handlebars/Collections/DisposableContainer.cs +++ b/source/Handlebars/Collections/DisposableContainer.cs @@ -2,7 +2,7 @@ namespace HandlebarsDotNet { - internal struct DisposableContainer : IDisposable + internal readonly struct DisposableContainer : IDisposable { private readonly Action _onDispose; public readonly T Value; diff --git a/source/Handlebars/Collections/ExtendedEnumerable.cs b/source/Handlebars/Collections/ExtendedEnumerable.cs index cc388627..7ac07718 100644 --- a/source/Handlebars/Collections/ExtendedEnumerable.cs +++ b/source/Handlebars/Collections/ExtendedEnumerable.cs @@ -76,7 +76,7 @@ public Container(TValue value) } } - internal struct EnumeratorValue + internal readonly struct EnumeratorValue { public static readonly EnumeratorValue Empty = new EnumeratorValue(); @@ -88,9 +88,9 @@ public EnumeratorValue(T value, int index, bool isLast) IsFirst = index == 0; } - public T Value { get; } - public int Index { get; } - public bool IsFirst { get; } - public bool IsLast { get; } + public readonly T Value; + public readonly int Index; + public readonly bool IsFirst; + public readonly bool IsLast; } } \ No newline at end of file diff --git a/source/Handlebars/Collections/FixedSizeDictionary.cs b/source/Handlebars/Collections/FixedSizeDictionary.cs new file mode 100644 index 00000000..18808f5e --- /dev/null +++ b/source/Handlebars/Collections/FixedSizeDictionary.cs @@ -0,0 +1,590 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace HandlebarsDotNet.Collections +{ + internal static class HashHelper + { + public static readonly int[] Primes = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, + 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199 + }; + } + + /// + /// Append-only data structure of a fixed size that provides dictionary-like lookup capabilities. + /// Performance of , and + /// starts to degrade as number of items comes closer to . + /// and always performs at constant time. + /// + public class FixedSizeDictionary : + IReadOnlyDictionary + where TKey : notnull + where TValue : notnull + where TComparer : notnull, IEqualityComparer + { + private const int MaximumSize = 1024; + + private readonly int _bucketMask; + private readonly int _bucketSize; + + private readonly Entry[] _entries; + private readonly Bucket[] _buckets; + private readonly EntryIndex[] _indexes; // required for fast cleanup and copy + + private readonly TComparer _comparer; + + private byte _version; + private int _count; + + /// + /// + /// + /// Actual number of buckets would be the closest power of 2 and multiplied by itself. Maximum size is 1024. + /// Actual bucket size would be closest prime number. Maximum size is 199 + /// + public FixedSizeDictionary(int bucketsCount, int bucketSize, TComparer comparer) + { + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + if (bucketsCount > MaximumSize) throw new ArgumentException($" cannot be greater then {MaximumSize}", nameof(bucketsCount)); + if (bucketSize > HashHelper.Primes[HashHelper.Primes.Length - 1]) throw new ArgumentException($" cannot be greater then {HashHelper.Primes[HashHelper.Primes.Length - 1]}", nameof(bucketSize)); + + // size is always ^2. + bucketsCount = AlignSize(bucketsCount); + _comparer = comparer; + _bucketMask = bucketsCount - 1; + _bucketSize = FindClosestPrime(bucketSize); + _version = 1; + + _buckets = new Bucket[bucketsCount]; + InitializeBuckets(_buckets); + + _entries = new Entry[bucketsCount * bucketSize]; + _indexes = new EntryIndex[bucketsCount * bucketSize]; + + static int AlignSize(int size) + { + size--; + size |= size >> 1; + size |= size >> 2; + size |= size >> 4; + size |= size >> 8; + size |= size >> 16; + size++; + + return size; + } + + static int FindClosestPrime(int bucketSize) + { + for (int i = 0; i < HashHelper.Primes.Length; i++) + { + int prime = HashHelper.Primes[i]; + if (prime >= bucketSize) return prime; + } + + return HashHelper.Primes[HashHelper.Primes.Length - 1]; + } + + static void InitializeBuckets(Bucket[] buckets) + { + for (var i = 0; i < buckets.Length; i++) + { + buckets[i] = new Bucket(0); + } + } + } + + public int Count => _count; + + /// + /// Amount of items can be added to the dictionary + /// + public int Capacity => _entries.Length; + + /// + /// Calculates current index for the given key + /// + /// + /// + /// + public bool TryGetIndex(TKey key, out EntryIndex index) + { + var hash = _comparer.GetHashCode(key); + var bucketIndex = hash & _bucketMask; + + var inBucketEntryIndex = hash % _bucketSize; + var entryIndex = bucketIndex * _bucketSize + Math.Abs(inBucketEntryIndex); + + if (_buckets[bucketIndex].Version != _version) + { + index = new EntryIndex(entryIndex, _version); + return true; + } + + var entry = _entries[entryIndex]; + if (entry.Version != _version || hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + index = new EntryIndex(entryIndex, _version); + return true; + } + + // handling possible collisions + while (entry.Next != -1) + { + entry = _entries[entry.Next]; + if (!entry.IsNotDefault) break; + if (entry.Version == _version && (hash != entry.Hash || !_comparer.Equals(key, entry.Key))) continue; + + index = new EntryIndex(entry.Index, _version); + return true; + } + + index = new EntryIndex(); + return false; + } + + /// + /// Checks key existence at guarantied O(1) ignoring actual key comparison + /// + public bool ContainsKey(in EntryIndex keyIndex) + { + // No need to extract actual value. EntryIndex should be used only as part of it's issuer + // and as collection is append only it's guarantied to have the value at particular index + return keyIndex.Version == _version; + } + + /// + /// Checks key existence at best O(1) and worst O(m) where 'm' is number of collisions + /// + public bool ContainsKey(in TKey key) + { + var hash = _comparer.GetHashCode(key); + var bucketIndex = hash & _bucketMask; + + if (_buckets[bucketIndex].Version != _version) return false; + + var inBucketEntryIndex = hash % _bucketSize; + var entryIndex = bucketIndex * _bucketSize + Math.Abs(inBucketEntryIndex); + + var entry = _entries[entryIndex]; + if (!entry.IsNotDefault || entry.Version != _version) return false; + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + return true; + } + + // handling possible collisions + while (entry.Next != -1) + { + entry = _entries[entry.Next]; + if (!entry.IsNotDefault || entry.Version != _version) return false; + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + return true; + } + } + + return false; + } + + /// + /// Performs lookup at guarantied O(1) ignoring actual key comparison + /// + public bool TryGetValue(in EntryIndex keyIndex, out TValue value) + { + if (keyIndex.Version != _version) + { + value = default; + return false; + } + + var entry = _entries[keyIndex.Index]; + if (entry.IsNotDefault && entry.Version == _version) + { + value = entry.Value; + return true; + } + + value = default; + return false; + } + + /// + /// Performs lookup at best O(1) and worst O(m) where 'm' is number of collisions + /// + public bool TryGetValue(in TKey key, out TValue value) + { + var hash = _comparer.GetHashCode(key); + var bucketIndex = hash & _bucketMask; + + if (_buckets[bucketIndex].Version != _version) + { + value = default; + return false; + } + + var inBucketEntryIndex = hash % _bucketSize; + var entryIndex = bucketIndex * _bucketSize + Math.Abs(inBucketEntryIndex); + + var entry = _entries[entryIndex]; + if (!entry.IsNotDefault || entry.Version != _version) + { + value = default; + return false; + } + + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + value = entry.Value; + return true; + } + + // handling possible collisions + while (entry.Next != -1) + { + entry = _entries[entry.Next]; + if (!entry.IsNotDefault || entry.Version != _version) + { + value = default; + return false; + } + + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + value = entry.Value; + return true; + } + } + + value = default; + return false; + } + + /// + /// Adds or replaces an item at best O(1) and worst O(m) where 'm' is number of collisions + /// + /// Item cannot be added due to capacity constraint. + public void AddOrReplace(in TKey key, in TValue value, out EntryIndex index) + { + var hash = _comparer.GetHashCode(key); + var bucketIndex = hash & _bucketMask; + ref var bucket = ref _buckets[bucketIndex]; + + var inBucketEntryIndex = hash % _bucketSize; + var entryIndex = bucketIndex * _bucketSize + Math.Abs(inBucketEntryIndex); + + if (bucket.Version != _version) + { + bucket.Version = _version; + _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); + index = new EntryIndex(entryIndex, _version); + _indexes[_count++] = index; + return; + } + + var entry = _entries[entryIndex]; + if (!entry.IsNotDefault || entry.Version != _version) + { + _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); + index = new EntryIndex(entryIndex, _version); + _indexes[_count++] = index; + return; + } + + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + index = new EntryIndex(entryIndex, _version); + _entries[entryIndex].Value = value; + return; + } + + // handling possible collisions + while (entry.Next != -1) + { + entry = _entries[entry.Next]; + if (entry.Version != _version) + { + _entries[entry.Index] = new Entry(hash, entry.Index, key, value, _version); + index = new EntryIndex(entry.Index, _version); + _indexes[_count++] = index; + return; + } + + if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) + { + index = new EntryIndex(entry.Index, _version); + _entries[entry.Index].Value = value; + return; + } + } + + // found no entry to replace in existing collisions + // trying to find the first available spot in an array + + ref var entryReference = ref _entries[entry.Index]; + entryIndex = entryReference.Index + 1; + + for (; entryIndex < _entries.Length; entryIndex++) + { + entry = _entries[entryIndex]; + if (entry.IsNotDefault && entry.Version == _version) continue; + + entryReference.Next = entryIndex; + _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); + index = new EntryIndex(entryIndex, _version); + _indexes[_count++] = index; + return; + } + + entryIndex = (bucketIndex * _bucketMask) - 1; + for (; entryIndex >= 0; entryIndex--) + { + entry = _entries[entryIndex]; + if (entry.IsNotDefault && entry.Version == _version) continue; + + entryReference.Next = entryIndex; + _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); + index = new EntryIndex(entryIndex, _version); + _indexes[_count++] = index; + return; + } + + throw new InvalidOperationException("Item cannot be added due to capacity constraint."); + } + + /// + /// Gets or replaces item at a given index at O(1) + /// + /// + public TValue this[in EntryIndex entryIndex] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (entryIndex.Version != _version) return default; + return _entries[entryIndex.Index].Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + if (entryIndex.Version != _version) return; + _entries[entryIndex.Index].Value = value; + } + } + + /// + /// Copies items from one dictionary to another at O(n) + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(FixedSizeDictionary destination) + { + if (Capacity != destination.Capacity) + throw new ArgumentException(" capacity should be equal to source dictionary", nameof(destination)); + + destination._version = _version; + + for (var index = 0; index < _buckets.Length; index++) + { + destination._buckets[index] = _buckets[index]; + } + + for (var index = 0; index < _indexes.Length; index++) + { + var idx = _indexes[index]; + if(idx.Version != _version || !idx.IsNotEmpty) break; + + var entryIndex = _entries[idx.Index]; + if(!entryIndex.IsNotDefault || entryIndex.Version != _version) continue; + + destination._entries[idx.Index] = entryIndex; + destination[entryIndex.Index] = _entries[entryIndex.Index]; + destination._indexes[index] = idx; + } + } + + /// + /// Performs fast cleanup without cleaning internal storage (does not make objects available for GC) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _count = 0; + + unchecked { ++_version; } + + if (_version == 0) _version = 1; + } + + /// + /// Performs full cleanup + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + for (var index = 0; index < _indexes.Length; index++) + { + var idx = _indexes[index]; + if(idx.Version != _version || !idx.IsNotEmpty) break; + + _entries[idx.Index] = new Entry(); + _indexes[index] = new EntryIndex(); + } + + _count = 0; + + unchecked { ++_version; } + + if (_version == 0) _version = 1; + } + + /// + /// Performs full cleanup once in 3 versions + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OptionalClear() + { + if (_version % 5 == 0) + { + Clear(); + return; + } + + Reset(); + } + + private Entry this[in int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _entries[index] = value; + } + + public IEnumerator> GetEnumerator() + { + for (var index = 0; index < _indexes.Length; index++) + { + var idx = _indexes[index]; + if(idx.Version != _version || !idx.IsNotEmpty) break; + + var entry = _entries[idx.Index]; + + yield return new KeyValuePair(entry.Key, entry.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + bool IReadOnlyDictionary.ContainsKey(TKey key) => ContainsKey(key); + + bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) => TryGetValue(key, out value); + + TValue IReadOnlyDictionary.this[TKey key] + { + get + { + if (!TryGetIndex(key, out var index)) throw new KeyNotFoundException(); + return this[index]; + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + for (var index = 0; index < _indexes.Length; index++) + { + var idx = _indexes[index]; + if(idx.Version != _version || !idx.IsNotEmpty) break; + + yield return _entries[idx.Index].Key; + } + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + for (var index = 0; index < _indexes.Length; index++) + { + var idx = _indexes[index]; + if(idx.Version != _version || !idx.IsNotEmpty) break; + + yield return _entries[idx.Index].Value; + } + } + } + + private struct Entry + { + public readonly int Index; + public readonly int Hash; + public readonly TKey Key; + public readonly bool IsNotDefault; + public readonly byte Version; + public int Next; + public TValue Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Entry(in int hash, in int index, in TKey key, in TValue value, byte version) + { + Index = index; + Hash = hash; + Key = key; + Value = value; + Version = version; + IsNotDefault = true; + Next = -1; + } + + public override string ToString() => $"{Key}: {Value}"; + } + + private struct Bucket + { + public byte Version; + + public Bucket(in byte version) => Version = version; + + public override string ToString() => $"v{Version.ToString()}"; + } + } + + public readonly struct EntryIndex : IEquatable> + { + public readonly int Index; + public readonly byte Version; + public readonly bool IsNotEmpty; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal EntryIndex(in int index, in byte version) + { + Version = version; + Index = index; + IsNotEmpty = true; + } + + public override string ToString() => Index.ToString(); + + public bool Equals(EntryIndex other) + => IsNotEmpty == other.IsNotEmpty && Version == other.Version && Index == other.Index; + + public override bool Equals(object obj) + => obj is EntryIndex other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = Index; + hashCode = (hashCode * 397) ^ Version; + hashCode = (hashCode * 397) ^ IsNotEmpty.GetHashCode(); + return hashCode; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/HashSetSlim.cs b/source/Handlebars/Collections/HashSetSlim.cs deleted file mode 100644 index 37d7f56f..00000000 --- a/source/Handlebars/Collections/HashSetSlim.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace HandlebarsDotNet.Collections -{ - // Will be removed in next iterations - internal sealed class HashSetSlim - { - private HashSet _inner; - private readonly IEqualityComparer _comparer; - - public HashSetSlim(IEqualityComparer comparer = null) - { - _comparer = comparer ?? EqualityComparer.Default; - _inner = new HashSet(_comparer); - } - - public bool Contains(TKey key) - { - return _inner.Contains(key); - } - - public void Add(TKey key) - { - var copy = new HashSet(_inner, _comparer) - { - key - }; - - Interlocked.CompareExchange(ref _inner, copy, _inner); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/LookupSlim.cs b/source/Handlebars/Collections/LookupSlim.cs index 35972c48..66f80c61 100644 --- a/source/Handlebars/Collections/LookupSlim.cs +++ b/source/Handlebars/Collections/LookupSlim.cs @@ -1,52 +1,58 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; namespace HandlebarsDotNet.Collections { - internal sealed class LookupSlim + internal sealed class LookupSlim where TKey : class { + private static readonly DictionaryPool Pool = DictionaryPool.Shared; + private Dictionary _inner; - private readonly IEqualityComparer _comparer; - public LookupSlim(IEqualityComparer comparer = null) - { - _comparer = comparer ?? EqualityComparer.Default; - _inner = new Dictionary(_comparer); - } + public LookupSlim() => _inner = Pool.Get(); - public bool ContainsKey(TKey key) - { - return _inner.ContainsKey(key); - } + public bool ContainsKey(TKey key) => _inner.ContainsKey(key); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue GetOrAdd(TKey key, Func valueFactory) { - return !_inner.TryGetValue(key, out var value) - ? Write(key, valueFactory(key)) - : value; + return _inner.TryGetValue(key, out var value) + ? value + : Write(key, valueFactory(key)); } - + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue GetOrAdd(TKey key, Func valueFactory, TState state) { - return !_inner.TryGetValue(key, out var value) - ? Write(key, valueFactory(key, state)) - : value; + return _inner.TryGetValue(key, out var value) + ? value + : Write(key, valueFactory(key, state)); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TKey key, out TValue value) => _inner.TryGetValue(key, out value); - public bool TryGetValue(TKey key, out TValue value) - { - return _inner.TryGetValue(key, out value); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => _inner.Clear(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue Write(TKey key, TValue value) { - var copy = new Dictionary(_inner, _comparer) + var copy = Pool.Get(); + var inner = _inner; + inner.CopyTo(copy); + copy[key] = value; + + if (Interlocked.CompareExchange(ref _inner, copy, inner) != _inner) + { + Pool.Return(inner); + } + else { - [key] = value - }; - - Interlocked.CompareExchange(ref _inner, copy, _inner); + Pool.Return(copy); + } return value; } diff --git a/source/Handlebars/Collections/ObjectPool.cs b/source/Handlebars/Collections/ObjectPool.cs index ca55c043..339db931 100644 --- a/source/Handlebars/Collections/ObjectPool.cs +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -1,12 +1,78 @@ -using Microsoft.Extensions.ObjectPool; +using System; +using System.Collections.Concurrent; +using System.Threading; namespace HandlebarsDotNet { internal static class ObjectPoolExtensions { - public static DisposableContainer Use(this ObjectPool objectPool) where T : class + public static DisposableContainer Use(this InternalObjectPool objectPool) where T : class { return new DisposableContainer(objectPool.Get(), objectPool.Return); } } + + internal class InternalObjectPool + where T: class + { + private readonly IInternalObjectPoolPolicy _policy; + private ConcurrentQueue _queue = new ConcurrentQueue(); + private T _firstItem; + + public InternalObjectPool(IInternalObjectPoolPolicy policy) + { + Handlebars.Disposables.Enqueue(new Disposer(this)); + + _policy = policy; + + for (var i = 0; i < 5; i++) Return(_policy.Create()); + } + + public T Get() + { + var item = _firstItem; + if (item == null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + if (_queue.TryDequeue(out item)) + { + return item; + } + + item = _policy.Create(); + } + + return item; + } + + public void Return(T obj) + { + if (!_policy.Return(obj)) return; + + if (_firstItem == null) + { + // Intentionally not using interlocked here. + // In a worst case scenario two objects may be stored into same slot. + // It is very unlikely to happen and will only mean that one of the objects will get collected. + _firstItem = obj; + return; + } + + _queue.Enqueue(obj); + } + + private sealed class Disposer : IDisposable + { + private readonly InternalObjectPool _target; + + public Disposer(InternalObjectPool target) => _target = target; + + public void Dispose() => _target._queue = new ConcurrentQueue(); + } + } + + internal interface IInternalObjectPoolPolicy + { + T Create(); + bool Return(T item); + } } \ No newline at end of file diff --git a/source/Handlebars/Collections/ObservableDictionary.cs b/source/Handlebars/Collections/ObservableDictionary.cs new file mode 100644 index 00000000..6303a8e0 --- /dev/null +++ b/source/Handlebars/Collections/ObservableDictionary.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.Collections +{ + internal class ObservableDictionary : IObservable>, IDictionary, IObserver> + { + private readonly HashedCollection>> _observers; + private readonly Dictionary _inner; + + public ObservableDictionary(IDictionary outer = null, IEqualityComparer comparer = null) + { + comparer ??= EqualityComparer.Default; + _inner = outer != null ? new Dictionary(outer, comparer) : new Dictionary(comparer); + _observers = new HashedCollection>>(); + if (outer is ObservableDictionary observableDictionary) + { + observableDictionary.Subscribe(this); + } + } + + public IDisposable Subscribe(IObserver> observer) + { + lock (_observers) + { + _observers.Add(observer); + return new UnsubscribeEvent(_observers, observer); + } + } + + private void Publish(ObservableEvent @event) + { + lock (_observers) + { + for (var index = 0; index < _observers.Count; index++) + { + try + { + _observers[index].OnNext(@event); + } + catch + { + // ignore + } + } + } + } + + void ICollection>.Add(KeyValuePair item) + { + lock (_inner) + { + Publish(new AddedObservableEvent(item.Key, item.Value)); + _inner.Add(item.Key, item.Value); + } + } + + public void Clear() + { + lock (_inner) + { + foreach (var pair in _inner) + { + Publish(new RemovedObservableEvent(pair.Key, pair.Value)); + } + + _inner.Clear(); + } + } + + bool ICollection>.Contains(KeyValuePair item) + { + lock (_inner) + { + return _inner.As>>().Contains(item); + } + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + lock (_inner) + { + _inner.As>>().CopyTo(array, arrayIndex); + } + } + + bool ICollection>.Remove(KeyValuePair item) + { + lock (_inner) + { + var removed = _inner.As>>().Remove(item); + if (removed) + { + Publish(new RemovedObservableEvent(item.Key, item.Value)); + } + + return removed; + } + } + + public int Count + { + get + { + lock (_inner) + { + return _inner.Count; + } + } + } + + bool ICollection>.IsReadOnly { get; } = false; + + public void Add(TKey key, TValue value) + { + lock (_inner) + { + _inner.Add(key, value); + } + + Publish(new AddedObservableEvent(key, value)); + } + + public bool ContainsKey(TKey key) + { + lock (_inner) + { + return _inner.ContainsKey(key); + } + } + + public bool Remove(TKey key) + { + TValue value; + lock (_inner) + { + if (!_inner.TryGetValue(key, out value)) return false; + _inner.Remove(key); + } + + Publish(new RemovedObservableEvent(key, value)); + return true; + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (_inner) + { + return _inner.TryGetValue(key, out value); + } + } + + public TValue this[TKey key] + { + get + { + lock (_inner) + { + return _inner[key]; + } + } + set + { + lock (_inner) + { + if (_inner.TryGetValue(key, out var oldValue)) + { + Publish(new ReplacedObservableEvent(key, oldValue, value)); + _inner[key] = value; + } + else + { + Add(key, value); + } + } + } + } + + public ICollection Keys + { + get + { + lock (_inner) + { + return _inner.Keys.ToList(); + } + } + } + + public ICollection Values + { + get + { + lock (_inner) + { + return _inner.Values.ToList(); + } + } + } + + internal class AddedObservableEvent : ObservableEvent + { + public TKey Key { get; } + + public AddedObservableEvent(TKey key, TValue value) : base(value) + { + Key = key; + } + } + + internal class RemovedObservableEvent : ObservableEvent + { + public TKey Key { get; } + + public RemovedObservableEvent(TKey key, TValue value) : base(value) + { + Key = key; + } + } + + internal class ReplacedObservableEvent : ObservableEvent + { + public TKey Key { get; } + public TValue OldValue { get; } + + public ReplacedObservableEvent(TKey key, TValue oldValue, TValue value) : base(value) + { + Key = key; + OldValue = oldValue; + } + } + + public IEnumerator> GetEnumerator() + { + KeyValuePair[] array; + lock (_inner) + { + array = _inner.ToArray(); + } + + for (int index = 0; index < array.Length; index++) + { + yield return array[index]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(ObservableEvent value) + { + switch (value) + { + case AddedObservableEvent addedObservableEvent: + Add(addedObservableEvent.Key, addedObservableEvent.Value); + break; + case RemovedObservableEvent removedObservableEvent: + Remove(removedObservableEvent.Key); + break; + case ReplacedObservableEvent replacedObservableEvent: + this[replacedObservableEvent.Key] = replacedObservableEvent.Value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } + + internal abstract class ObservableEvent + { + public T Value { get; } + + public ObservableEvent(T value) + { + Value = value; + } + } + + internal class UnsubscribeEvent : IDisposable + { + private readonly ICollection>> _observers; + private readonly IObserver> _observer; + + public UnsubscribeEvent(ICollection>> observers, IObserver> observer) + { + _observers = observers; + _observer = observer; + } + + public void Dispose() + { + _observers.Remove(_observer); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/ObservableList.cs b/source/Handlebars/Collections/ObservableList.cs new file mode 100644 index 00000000..1661454f --- /dev/null +++ b/source/Handlebars/Collections/ObservableList.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal class ObservableList : IList, IReadOnlyList, IObservable>, IObserver> + { + private readonly object _lock = new object(); + + private readonly List>> _observers = new List>>(); + private readonly List _inner; + + public ObservableList(ICollection list = null) + { + _inner = list != null ? new List(list) : new List(); + if (list is ObservableList observableList) + { + observableList.Subscribe(this); + } + } + + public IEnumerator GetEnumerator() + { + T[] array; + lock (_lock) + { + array = _inner.ToArray(); + } + + for (int index = 0; index < array.Length; index++) + { + yield return array[index]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + lock (_lock) + { + return ((IEnumerable) _inner).GetEnumerator(); + } + } + + public void Add(T item) + { + lock (_lock) + { + _inner.Add(item); + } + + Publish(new AddedObservableEvent(item)); + } + + public void Clear() + { + T[] array; + lock (_lock) + { + array = _inner.ToArray(); + _inner.Clear(); + } + + for (var index = 0; index < array.Length; index++) + { + var item = array[index]; + Publish(new RemovedObservableEvent(item)); + } + } + + public bool Contains(T item) + { + lock (_lock) + { + return _inner.Contains(item); + } + } + + public void CopyTo(T[] array, int arrayIndex) + { + lock (_lock) + { + _inner.CopyTo(array, arrayIndex); + } + } + + public bool Remove(T item) + { + bool removed; + lock (_lock) + { + removed = _inner.Remove(item); + } + + if (removed) + { + Publish(new RemovedObservableEvent(item)); + } + + return removed; + } + + public int Count + { + get + { + lock (_lock) + { + return _inner.Count; + } + } + } + + public bool IsReadOnly { get; } = false; + + public int IndexOf(T item) + { + lock (_lock) + { + return _inner.IndexOf(item); + } + } + + public void Insert(int index, T item) + { + lock (_lock) + { + _inner.Insert(index, item); + } + + Publish(new InsertObservableEvent(item, index)); + } + + public void RemoveAt(int index) + { + T value; + lock (_lock) + { + value = _inner[index]; + _inner.RemoveAt(index); + } + + Publish(new RemovedObservableEvent(value, index)); + } + + public T this[int index] + { + get + { + lock (_lock) + { + return _inner[index]; + } + } + set + { + T currentValue; + lock (_lock) + { + currentValue = _inner[index]; + _inner[index] = value; + } + + Publish(new ReplacedObservableEvent(value, currentValue, index)); + } + } + + public IDisposable Subscribe(IObserver> observer) + { + _observers.Add(observer); + return new UnsubscribeEvent(_observers, observer); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(ObservableEvent value) + { + switch (value) + { + case AddedObservableEvent addedObservableEvent: + Add(addedObservableEvent.Value); + break; + case RemovedObservableEvent removedObservableEvent when !removedObservableEvent.Index.HasValue: + Remove(removedObservableEvent.Value); + break; + case RemovedObservableEvent removedObservableEvent when removedObservableEvent.Index.HasValue: + RemoveAt(removedObservableEvent.Index.Value); + break; + case ReplacedObservableEvent replacedObservableEvent: + this[replacedObservableEvent.Index] = replacedObservableEvent.Value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + private void Publish(ObservableEvent @event) + { + for (int index = 0; index < _observers.Count; index++) + { + try + { + _observers[index].OnNext(@event); + } + catch + { + // ignore + } + } + } + + internal class AddedObservableEvent : ObservableEvent + { + public AddedObservableEvent(T value) : base(value) + { + } + } + + internal class InsertObservableEvent : ObservableEvent + { + public int Index { get; } + + public InsertObservableEvent(T value, int index) : base(value) + { + Index = index; + } + } + + internal class ReplacedObservableEvent : ObservableEvent + { + public T OldValue { get; } + public int Index { get; } + + public ReplacedObservableEvent(T value, T oldValue, int index) : base(value) + { + OldValue = oldValue; + Index = index; + } + } + + internal class RemovedObservableEvent : ObservableEvent + { + public int? Index { get; } + + public RemovedObservableEvent(T value, int? index = null) : base(value) + { + Index = index; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/ObserverBuilder.cs b/source/Handlebars/Collections/ObserverBuilder.cs new file mode 100644 index 00000000..7aefffed --- /dev/null +++ b/source/Handlebars/Collections/ObserverBuilder.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal class ObserverBuilder + { + private readonly Dictionary>> _handlers = new Dictionary>>(); + + public ObserverBuilder OnEvent(Action handler, Func predicate = null) where TEvent: T + { + if (!_handlers.TryGetValue(typeof(TEvent), out var handlers)) + { + handlers = new List>(); + _handlers.Add(typeof(TEvent), handlers); + } + + handlers.Add(@event => + { + if(predicate?.Invoke((TEvent) @event) ?? true) handler((TEvent) @event); + }); + + return this; + } + + public IObserver Build() => new Observer(_handlers); + + private class Observer : IObserver + { + private readonly Dictionary>> _handlers; + + public Observer(Dictionary>> handlers) => _handlers = handlers; + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(T value) + { + if (!_handlers.TryGetValue(value.GetType(), out var handlers)) return; + + for (int index = 0; index < handlers.Count; index++) + { + handlers[index](value); + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/StringBuilderPool.cs b/source/Handlebars/Collections/StringBuilderPool.cs index 42b03caf..23756bb9 100644 --- a/source/Handlebars/Collections/StringBuilderPool.cs +++ b/source/Handlebars/Collections/StringBuilderPool.cs @@ -1,10 +1,9 @@ using System; using System.Text; -using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet { - internal class StringBuilderPool : DefaultObjectPool + internal class StringBuilderPool : InternalObjectPool { private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); @@ -14,5 +13,26 @@ public StringBuilderPool(int initialCapacity = 16) : base(new StringBuilderPooledObjectPolicy{ InitialCapacity = initialCapacity }) { } + + public class StringBuilderPooledObjectPolicy : IInternalObjectPoolPolicy + { + public int InitialCapacity { get; set; } = 100; + + public int MaximumRetainedCapacity { get; set; } = 4096; + + public StringBuilder Create() + { + return new StringBuilder(InitialCapacity); + } + + public bool Return(StringBuilder obj) + { + if (obj.Capacity > MaximumRetainedCapacity) + return false; + + obj.Clear(); + return true; + } + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/CompilationContext.cs b/source/Handlebars/Compiler/CompilationContext.cs index 4c67314c..dbf5a5a9 100644 --- a/source/Handlebars/Compiler/CompilationContext.cs +++ b/source/Handlebars/Compiler/CompilationContext.cs @@ -4,13 +4,13 @@ namespace HandlebarsDotNet.Compiler { internal sealed class CompilationContext { - public CompilationContext(InternalHandlebarsConfiguration configuration) + public CompilationContext(ICompiledHandlebarsConfiguration configuration) { Configuration = configuration; BindingContext = Expression.Variable(typeof(BindingContext), "context"); } - public InternalHandlebarsConfiguration Configuration { get; } + public ICompiledHandlebarsConfiguration Configuration { get; } public ParameterExpression BindingContext { get; } } diff --git a/source/Handlebars/Compiler/ExpressionBuilder.cs b/source/Handlebars/Compiler/ExpressionBuilder.cs index 624d6916..11d1cc70 100644 --- a/source/Handlebars/Compiler/ExpressionBuilder.cs +++ b/source/Handlebars/Compiler/ExpressionBuilder.cs @@ -6,9 +6,9 @@ namespace HandlebarsDotNet.Compiler { internal class ExpressionBuilder { - private readonly InternalHandlebarsConfiguration _configuration; + private readonly ICompiledHandlebarsConfiguration _configuration; - public ExpressionBuilder(InternalHandlebarsConfiguration configuration) + public ExpressionBuilder(ICompiledHandlebarsConfiguration configuration) { _configuration = configuration; } diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index f1f95d9f..0f07a759 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -33,7 +33,7 @@ public static Expression Reduce(Expression expression, CompilationContext contex return expression; } - public static Action CompileCore(IEnumerable expressions, InternalHandlebarsConfiguration configuration, string templatePath = null) + public static Action CompileCore(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration, string templatePath = null) { try { @@ -51,7 +51,7 @@ public static Action CompileCore(IEnumerable expression = Reduce(expression, context); var lambda = ContextBinder.Bind(context, expression, templatePath); - return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(lambda); + return configuration.ExpressionCompiler.Compile(lambda); } catch (Exception ex) { @@ -59,7 +59,7 @@ public static Action CompileCore(IEnumerable } } - public static Expression> CompileCore(IEnumerable expressions, Expression parentContext, InternalHandlebarsConfiguration configuration, string templatePath = null) + public static Expression> CompileCore(IEnumerable expressions, Expression parentContext, ICompiledHandlebarsConfiguration configuration, string templatePath = null) { try { @@ -84,13 +84,13 @@ public static Expression> CompileCore(IEnumerable Compile(IEnumerable expressions, InternalHandlebarsConfiguration configuration, string templatePath = null) + public static Action Compile(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration, string templatePath = null) { try { var expression = CompileCore(expressions, null, configuration, templatePath); - return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(expression); + return configuration.ExpressionCompiler.Compile(expression); } catch (Exception ex) { diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index 5123a789..c06155e0 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -8,18 +8,10 @@ namespace HandlebarsDotNet.Compiler { - internal class HandlebarsCompiler + internal static class HandlebarsCompiler { - private readonly HandlebarsConfiguration _configuration; - - public HandlebarsCompiler(HandlebarsConfiguration configuration) - { - _configuration = configuration; - } - - public Action Compile(ExtendedStringReader source) + public static Action Compile(ExtendedStringReader source, ICompiledHandlebarsConfiguration configuration) { - var configuration = new InternalHandlebarsConfiguration(_configuration); var createdFeatures = configuration.Features; for (var index = 0; index < createdFeatures.Count; index++) { @@ -39,7 +31,7 @@ public Action Compile(ExtendedStringReader source) return action; } - internal Action CompileView(ViewReaderFactory readerFactoryFactory, string templatePath, InternalHandlebarsConfiguration configuration) + internal static Action CompileView(ViewReaderFactory readerFactoryFactory, string templatePath, ICompiledHandlebarsConfiguration configuration) { IEnumerable tokens; using (var sr = readerFactoryFactory(configuration, templatePath)) @@ -68,7 +60,7 @@ internal Action CompileView(ViewReaderFactory readerFactoryF return (tw, vm) => { string inner; - using (var innerWriter = new PolledStringWriter(configuration.FormatProvider)) + using (var innerWriter = ReusableStringWriter.Get(configuration.FormatProvider)) { compiledView(innerWriter, vm); inner = innerWriter.ToString(); @@ -78,7 +70,6 @@ internal Action CompileView(ViewReaderFactory readerFactoryF }; } - internal class DynamicViewModel : DynamicObject { private readonly object[] _objects; @@ -119,9 +110,6 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) return false; } } - } - - } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs index a6703271..36e0faf0 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs @@ -9,14 +9,14 @@ internal class BlockAccumulator : TokenConverter { public static IEnumerable Accumulate( IEnumerable tokens, - HandlebarsConfiguration configuration) + ICompiledHandlebarsConfiguration configuration) { return new BlockAccumulator(configuration).ConvertTokens(tokens).ToList(); } - private readonly HandlebarsConfiguration _configuration; + private readonly ICompiledHandlebarsConfiguration _configuration; - private BlockAccumulator(HandlebarsConfiguration configuration) + private BlockAccumulator(ICompiledHandlebarsConfiguration configuration) { _configuration = configuration; } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 6820cc52..7767f238 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -6,7 +6,7 @@ namespace HandlebarsDotNet.Compiler { internal abstract class BlockAccumulatorContext { - public static BlockAccumulatorContext Create(Expression item, HandlebarsConfiguration configuration) + public static BlockAccumulatorContext Create(Expression item, ICompiledHandlebarsConfiguration configuration) { BlockAccumulatorContext context = null; if (IsConditionalBlock(item)) @@ -53,14 +53,14 @@ private static bool IsConditionalBlock(Expression item) return (item is HelperExpression) && new[] { "#if", "#unless" }.Contains(((HelperExpression)item).HelperName); } - private static bool IsBlockHelper(Expression item, HandlebarsConfiguration configuration) + private static bool IsBlockHelper(Expression item, ICompiledHandlebarsConfiguration configuration) { item = UnwrapStatement(item); if (item is HelperExpression hitem) { var helperName = hitem.HelperName; - return hitem.IsBlock || !(configuration.Helpers.ContainsKey(helperName) || configuration.ReturnHelpers.ContainsKey(helperName)) && - configuration.BlockHelpers.ContainsKey(helperName.Replace("#", "").Replace("^", "")); + var helperPathInfo = configuration.PathInfoStore.GetOrAdd(helperName); + return hitem.IsBlock || !configuration.Helpers.ContainsKey(helperPathInfo) && configuration.BlockHelpers.ContainsKey(helperPathInfo); } return false; } diff --git a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs index 8cdcb8ae..16c993e6 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs @@ -44,7 +44,7 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) if (endExpression.IsEscaped != startExpression.IsEscaped) { - throw new HandlebarsCompilerException("Starting and ending handlebars do not match"); + throw new HandlebarsCompilerException("Starting and ending handlebars do not match", endExpression.Context); } yield return HandlebarsExpression.Statement( diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs index f2deaee3..8eca4d5f 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs @@ -15,14 +15,14 @@ internal class HelperConverter : TokenConverter public static IEnumerable Convert( IEnumerable sequence, - InternalHandlebarsConfiguration configuration) + ICompiledHandlebarsConfiguration configuration) { return new HelperConverter(configuration).ConvertTokens(sequence).ToList(); } - private readonly InternalHandlebarsConfiguration _configuration; + private readonly ICompiledHandlebarsConfiguration _configuration; - private HelperConverter(InternalHandlebarsConfiguration configuration) + private HelperConverter(ICompiledHandlebarsConfiguration configuration) { _configuration = configuration; } @@ -73,10 +73,10 @@ private bool IsRegisteredHelperName(string name) { var pathInfo = _configuration.PathInfoStore.GetOrAdd(name); if (!pathInfo.IsValidHelperLiteral && !_configuration.Compatibility.RelaxedHelperNaming) return false; - if (pathInfo.IsBlockHelper || pathInfo.IsInversion || pathInfo.IsBlockClose) return false; + if (pathInfo.IsBlockHelper || pathInfo.IsInversion || pathInfo.IsBlockClose || pathInfo.IsThis) return false; name = pathInfo.TrimmedPath; - return _configuration.Helpers.ContainsKey(name) || _configuration.ReturnHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); + return _configuration.Helpers.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name); } private bool IsRegisteredBlockHelperName(string name, bool isRaw) @@ -85,10 +85,11 @@ private bool IsRegisteredBlockHelperName(string name, bool isRaw) if (!pathInfo.IsValidHelperLiteral && !_configuration.Compatibility.RelaxedHelperNaming) return false; if (!isRaw && !(pathInfo.IsBlockHelper || pathInfo.IsInversion)) return false; if (pathInfo.IsBlockClose) return false; + if (pathInfo.IsThis) return false; name = pathInfo.TrimmedPath; - return _configuration.BlockHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); + return _configuration.BlockHelpers.ContainsKey(pathInfo) || BuiltInHelpers.Contains(name); } private bool IsUnregisteredBlockHelperName(string name, bool isRaw, IEnumerable sequence) diff --git a/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs b/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs new file mode 100644 index 00000000..c040c350 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; + +namespace HandlebarsDotNet.Compiler +{ + public sealed partial class BindingContext + { + private static readonly BindingContextPool Pool = new BindingContextPool(); + + internal static BindingContext Create(ICompiledHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath) + { + return Pool.CreateContext(configuration, value, writer, parent, templatePath, null); + } + + internal static BindingContext Create(ICompiledHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath, + Action partialBlockTemplate) + { + return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate); + } + + public void Dispose() => Pool.Return(this); + + private class BindingContextPool : InternalObjectPool + { + public BindingContextPool() : base(new BindingContextPolicy()) + { + } + + public BindingContext CreateContext(ICompiledHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate) + { + var context = Get(); + context.Configuration = configuration; + context.Value = value; + context.TextWriter = writer; + context.ParentContext = parent; + context.TemplatePath = templatePath; + context.PartialBlockTemplate = partialBlockTemplate; + + context.Initialize(); + + return context; + } + + private class BindingContextPolicy : IInternalObjectPoolPolicy + { + public BindingContext Create() => new BindingContext(); + + public bool Return(BindingContext item) + { + item.Root = null; + item.Value = null; + item.ParentContext = null; + item.TemplatePath = null; + item.TextWriter = null; + item.PartialBlockTemplate = null; + item.InlinePartialTemplates.Clear(); + + item.BlockParamsObject.OptionalClear(); + item.ContextDataObject.OptionalClear(); + + item._objectDescriptor.Reset(); + + return true; + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index f5ceb833..46eacc5f 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -1,77 +1,82 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Reflection; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.ValueProviders; -using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.Compiler { - internal sealed class BindingContext : IDisposable + public sealed partial class BindingContext : IDisposable { - private static readonly BindingContextPool Pool = new BindingContextPool(); + internal readonly EntryIndex[] WellKnownVariables = new EntryIndex[8]; - private readonly HashedCollection _valueProviders = new HashedCollection(); - - public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, - EncodedTextWriter writer, BindingContext parent, string templatePath, - IDictionary> inlinePartialTemplates) - { - return Pool.CreateContext(configuration, value, writer, parent, templatePath, null, inlinePartialTemplates); - } + private readonly DeferredValue _objectDescriptor; - public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, - EncodedTextWriter writer, BindingContext parent, string templatePath, - Action partialBlockTemplate, - IDictionary> inlinePartialTemplates) - { - return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate, inlinePartialTemplates); - } - private BindingContext() { - RegisterValueProvider(new BindingContextValueProvider(this)); + InlinePartialTemplates = new CascadeDictionary>(); + + ContextDataObject = new FixedSizeDictionary(16, 7, ChainSegment.EqualityComparer); + BlockParamsObject = new FixedSizeDictionary(16, 7, ChainSegment.EqualityComparer); + + _objectDescriptor = new DeferredValue(this, context => ObjectDescriptor.Create(context.Value, context.Configuration)); + + Data = new DataValues(this); } + + internal FixedSizeDictionary ContextDataObject { get; } + internal FixedSizeDictionary BlockParamsObject { get; } private void Initialize() { Root = ParentContext?.Root ?? this; - TemplatePath = (ParentContext != null ? ParentContext.TemplatePath : TemplatePath) ?? TemplatePath; + ContextDataObject.AddOrReplace(ChainSegment.Root, Root.Value, out WellKnownVariables[(int) WellKnownVariable.Root]); + + if (ParentContext == null) + { + ContextDataObject.AddOrReplace( + ChainSegment.Parent, + ChainSegment.Parent.GetUndefinedBindingResult(Configuration), + out WellKnownVariables[(int) WellKnownVariable.Parent] + ); + + return; + } + + ContextDataObject.AddOrReplace( + ChainSegment.Parent, + ParentContext.Value, + out WellKnownVariables[(int) WellKnownVariable.Parent] + ); + + ParentContext.BlockParamsObject.CopyTo(BlockParamsObject); + + TemplatePath = ParentContext.TemplatePath ?? TemplatePath; + //Inline partials cannot use the Handlebars.RegisteredTemplate method //because it pollutes the static dictionary and creates collisions //where the same partial name might exist in multiple templates. //To avoid collisions, pass around a dictionary of compiled partials //in the context - if (ParentContext != null) - { - InlinePartialTemplates = ParentContext.InlinePartialTemplates; - - if (Value is HashParameterDictionary dictionary) { - // Populate value with parent context - foreach (var item in GetContextDictionary(ParentContext.Value)) { - if (!dictionary.ContainsKey(item.Key)) - dictionary[item.Key] = item.Value; - } - } - } - else - { - InlinePartialTemplates = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } + InlinePartialTemplates.Outer = ParentContext.InlinePartialTemplates; + + if (!(Value is HashParameterDictionary dictionary) || ParentContext.Value == null || ReferenceEquals(Value, ParentContext.Value)) return; + + // Populate value with parent context + PopulateHash(dictionary, ParentContext.Value, Configuration); } - public string TemplatePath { get; private set; } + internal string TemplatePath { get; private set; } - public InternalHandlebarsConfiguration Configuration { get; private set; } + internal ICompiledHandlebarsConfiguration Configuration { get; private set; } - public EncodedTextWriter TextWriter { get; private set; } + internal EncodedTextWriter TextWriter { get; private set; } - public IDictionary> InlinePartialTemplates { get; private set; } + internal CascadeDictionary> InlinePartialTemplates { get; } - public Action PartialBlockTemplate { get; private set; } + internal Action PartialBlockTemplate { get; private set; } public bool SuppressEncoding { @@ -79,152 +84,67 @@ public bool SuppressEncoding set => TextWriter.SuppressEncoding = value; } - public object Value { get; private set; } + public DataValues Data; + + public object Value { get; set; } - public BindingContext ParentContext { get; private set; } + internal BindingContext ParentContext { get; private set; } - public object Root { get; private set; } + internal BindingContext Root { get; private set; } - public void RegisterValueProvider(IValueProvider valueProvider) + internal bool TryGetVariable(ChainSegment segment, out object value) { - if(valueProvider == null) throw new ArgumentNullException(nameof(valueProvider)); - - _valueProviders.Add(valueProvider); - } - - public void UnregisterValueProvider(IValueProvider valueProvider) - { - _valueProviders.Remove(valueProvider); - } - - public bool TryGetContextVariable(ref ChainSegment segment, out object value) - { - // accessing value providers in reverse order as it gives more probability of hit - for (var index = _valueProviders.Count - 1; index >= 0; index--) + if (segment.WellKnownVariable != WellKnownVariable.None) { - if (_valueProviders[index].TryGetValue(ref segment, out value)) return true; + var wellKnownVariable = WellKnownVariables[(int) segment.WellKnownVariable]; + return BlockParamsObject.TryGetValue(wellKnownVariable, out value) + || (_objectDescriptor.Value?.MemberAccessor.TryGetValue(Value, segment, out value) ?? false); } - - value = null; - return false; + + return BlockParamsObject.TryGetValue(segment, out value) + || (_objectDescriptor.Value?.MemberAccessor.TryGetValue(Value, segment, out value) ?? false); } - public bool TryGetVariable(ref ChainSegment segment, out object value, bool searchContext = false) + internal bool TryGetContextVariable(ChainSegment segment, out object value) { - // accessing value providers in reverse order as it gives more probability of hit - for (var index = _valueProviders.Count - 1; index >= 0; index--) + if (segment.WellKnownVariable != WellKnownVariable.None) { - var valueProvider = _valueProviders[index]; - if(!valueProvider.SupportedValueTypes.HasFlag(ValueTypes.All) && !searchContext) continue; - - if (valueProvider.TryGetValue(ref segment, out value)) return true; + var wellKnownVariable = WellKnownVariables[(int) segment.WellKnownVariable]; + return BlockParamsObject.TryGetValue(segment, out value) + || ContextDataObject.TryGetValue(wellKnownVariable, out value); } - - value = null; - return ParentContext?.TryGetVariable(ref segment, out value, searchContext) ?? false; + + return BlockParamsObject.TryGetValue(segment, out value) + || ContextDataObject.TryGetValue(segment, out value); } - private static IDictionary GetContextDictionary(object target) + internal BindingContext CreateChildContext(object value, Action partialBlockTemplate = null) { - var contextDictionary = new Dictionary(); - - switch (target) - { - case null: - return contextDictionary; - - case IDictionary dictionary: - { - foreach (var item in dictionary) - { - contextDictionary[item.Key] = item.Value; - } - - break; - } - default: - { - var type = target.GetType(); - - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - foreach (var field in fields) - { - contextDictionary[field.Name] = field.GetValue(target); - } - - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - foreach (var property in properties) - { - if (property.GetIndexParameters().Length == 0) - { - contextDictionary[property.Name] = property.GetValue(target); - } - } - - break; - } - } - - return contextDictionary; + return Create(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate); } - - public BindingContext CreateChildContext(object value, Action partialBlockTemplate = null) + + internal BindingContext CreateChildContext() { - return Create(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); + return Create(Configuration, null, TextWriter, this, TemplatePath, PartialBlockTemplate); } - - public void Dispose() + + public BindingContext CreateFrame(object value = null) { - Pool.Return(this); + return Create(Configuration, value, TextWriter, this, TemplatePath, PartialBlockTemplate); } - - private class BindingContextPool : DefaultObjectPool + + private static void PopulateHash(HashParameterDictionary hash, object from, ICompiledHandlebarsConfiguration configuration) { - public BindingContextPool() : base(new BindingContextPolicy()) - { - } - - public BindingContext CreateContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) - { - var context = Get(); - context.Configuration = configuration; - context.Value = value; - context.TextWriter = writer; - context.ParentContext = parent; - context.TemplatePath = templatePath; - context.InlinePartialTemplates = inlinePartialTemplates; - context.PartialBlockTemplate = partialBlockTemplate; - - context.Initialize(); - - return context; - } - - private class BindingContextPolicy : IPooledObjectPolicy + var descriptor = ObjectDescriptor.Create(from, configuration); + var accessor = descriptor.MemberAccessor; + var properties = descriptor.GetProperties(descriptor, from); + var enumerator = properties.GetEnumerator(); + while (enumerator.MoveNext()) { - public BindingContext Create() - { - return new BindingContext(); - } - - public bool Return(BindingContext item) - { - item.Root = null; - item.Value = null; - item.ParentContext = null; - item.TemplatePath = null; - item.TextWriter = null; - item.InlinePartialTemplates = null; - item.PartialBlockTemplate = null; - - var valueProviders = item._valueProviders; - for (var index = valueProviders.Count - 1; index >= 1; index--) - { - valueProviders.Remove(valueProviders[index]); - } - - return true; - } + var segment = ChainSegment.Create(enumerator.Current); + if(hash.ContainsKey(segment)) continue; + if (!accessor.TryGetValue(@from, segment, out var value)) continue; + hash[segment] = value; } } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs index a0a3f5a9..a41098ea 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Linq.Expressions; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { @@ -7,18 +9,20 @@ internal class BlockParamsExpression : HandlebarsExpression { public new static BlockParamsExpression Empty() => new BlockParamsExpression(null); - private readonly BlockParam _blockParam; + public readonly BlockParam BlockParam; private BlockParamsExpression(BlockParam blockParam) { - _blockParam = blockParam; + BlockParam = blockParam; } public BlockParamsExpression(string action, string blockParams) :this(new BlockParam { Action = action, - Parameters = blockParams.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries) + Parameters = blockParams.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries) + .Select(ChainSegment.Create) + .ToArray() }) { } @@ -29,13 +33,13 @@ public BlockParamsExpression(string action, string blockParams) protected override Expression Accept(ExpressionVisitor visitor) { - return visitor.Visit(Constant(_blockParam, typeof(BlockParam))); + return visitor.Visit(Constant(BlockParam, typeof(BlockParam))); } } internal class BlockParam { public string Action { get; set; } - public string[] Parameters { get; set; } + public ChainSegment[] Parameters { get; set; } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs deleted file mode 100644 index fd2e7de4..00000000 --- a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Structure.Path; -using HandlebarsDotNet.ValueProviders; -using Microsoft.Extensions.ObjectPool; - -namespace HandlebarsDotNet.Compiler -{ - /* - * Is going to be changed in next iterations - */ - - /// - /// Configures BlockParameters for current BlockHelper - /// - /// Parameters passed to BlockParams. - /// Function that perform binding of parameter to . - /// Dependencies of current configuration. Used to omit closure creation. - public delegate void ConfigureBlockParams(string[] parameters, ValueBinder valueBinder, object[] dependencies); - - /// - /// Function that perform binding of parameter to . - /// - /// Variable name that would be added to the . - /// Variable value provider that would be invoked when is requested. - /// Context for the binding. - public delegate void ValueBinder(string variableName, Func valueProvider, object context = null); - - /// - internal class BlockParamsValueProvider : IValueProvider - { - private static readonly string[] EmptyParameters = new string[0]; - - private static readonly BlockParamsValueProviderPool Pool = new BlockParamsValueProviderPool(); - - private readonly Dictionary>> _accessors; - - private BlockParam _params; - private Action> _invoker; - - public static BlockParamsValueProvider Create(BindingContext context, object @params) - { - var blockParamsValueProvider = Pool.Get(); - - blockParamsValueProvider._params = @params as BlockParam; - blockParamsValueProvider._invoker = action => action(context); - - return blockParamsValueProvider; - } - - private BlockParamsValueProvider() - { - _accessors = new Dictionary>>(StringComparer.OrdinalIgnoreCase); - } - - public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context | ValueTypes.All; - - /// - /// Configures behavior of BlockParams. - /// - public void Configure(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies) - { - var parameters = _params?.Parameters ?? EmptyParameters; - void BlockParamsAction(BindingContext context) - { - void ValueBinder(string name, Func value, object ctx) - { - if (!string.IsNullOrEmpty(name)) _accessors[name] = new KeyValuePair>(ctx, value); - } - - blockParamsConfiguration.Invoke(parameters, ValueBinder, dependencies); - } - - _invoker(BlockParamsAction); - } - - public bool TryGetValue(ref ChainSegment segment, out object value) - { - if (_accessors.TryGetValue(segment.LowerInvariant, out var provider)) - { - value = provider.Value(provider.Key); - return true; - } - - value = null; - return false; - } - - public void Dispose() - { - Pool.Return(this); - } - - private class BlockParamsValueProviderPool : DefaultObjectPool - { - public BlockParamsValueProviderPool() : base(new BlockParamsValueProviderPolicy()) - { - } - - private class BlockParamsValueProviderPolicy : IPooledObjectPolicy - { - public BlockParamsValueProvider Create() - { - return new BlockParamsValueProvider(); - } - - public bool Return(BlockParamsValueProvider item) - { - item._accessors.Clear(); - item._invoker = null; - item._params = null; - - return true; - } - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index 05f9b6f9..9ca0f966 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -1,27 +1,91 @@ using System; -using HandlebarsDotNet.Polyfills; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; namespace HandlebarsDotNet.Compiler.Structure.Path { + internal enum WellKnownVariable + { + None = -1, + Index = 0, + Key = 1, + Value = 2, + First = 3, + Last = 4, + Root = 5, + Parent = 6, + This = 7, + } + /// /// Represents parts of single separated with dots. /// - public struct ChainSegment : IEquatable + public sealed class ChainSegment : IEquatable { - private readonly string _value; + private static readonly char[] TrimStart = {'@'}; + private static readonly LookupSlim Lookup = new LookupSlim(); - internal readonly string LowerInvariant; + public static ChainSegmentEqualityComparer EqualityComparer { get; } = new ChainSegmentEqualityComparer(); + + static ChainSegment() => Handlebars.Disposables.Enqueue(new Disposer()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ChainSegment Create(string value) => Lookup.GetOrAdd(value, v => new ChainSegment(v)); - public ChainSegment(string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ChainSegment Create(string value, WellKnownVariable variable, bool createVariable = false) { - var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); - var segmentTrimmedValue = TrimSquareBrackets(segmentValue).Intern(); + if (createVariable) + { + Lookup.GetOrAdd($"@{value}", (s, v) => new ChainSegment(s, v), variable); + } + + return Lookup.GetOrAdd(value, (s, v) => new ChainSegment(s, v), variable); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ChainSegment Create(object value) + { + if (value is ChainSegment segment) return segment; + return Lookup.GetOrAdd(value as string ?? value.ToString(), v => new ChainSegment(v)); + } + + public static ChainSegment Index { get; } = Create(nameof(Index), WellKnownVariable.Index, true); + public static ChainSegment First { get; } = Create(nameof(First), WellKnownVariable.First, true); + public static ChainSegment Last { get; } = Create(nameof(Last), WellKnownVariable.Last, true); + public static ChainSegment Value { get; } = Create(nameof(Value), WellKnownVariable.Value, true); + public static ChainSegment Key { get; } = Create(nameof(Key), WellKnownVariable.Key, true); + public static ChainSegment Root { get; } = Create(nameof(Root), WellKnownVariable.Root, true); + public static ChainSegment Parent { get; } = Create(nameof(Parent), WellKnownVariable.Parent, true); + public static ChainSegment This { get; } = Create(nameof(This), WellKnownVariable.This); + + private readonly object _lock = new object(); + + private readonly int _hashCode; + private readonly string _value; + private UndefinedBindingResult _undefinedBindingResult; + + /// + /// + /// + private ChainSegment(string value, WellKnownVariable wellKnownVariable = WellKnownVariable.None) + { + WellKnownVariable = wellKnownVariable; + + var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart(TrimStart); + var segmentTrimmedValue = TrimSquareBrackets(segmentValue); - IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); _value = segmentValue; + IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"); TrimmedValue = segmentTrimmedValue; - LowerInvariant = segmentTrimmedValue.ToLowerInvariant().Intern(); + LowerInvariant = segmentTrimmedValue.ToLowerInvariant(); + + IsValue = LowerInvariant == "value"; + IsKey = LowerInvariant == "key"; + + _hashCode = GetHashCodeImpl(); } /// @@ -39,19 +103,52 @@ public ChainSegment(string value) /// public readonly bool IsThis; + internal readonly string LowerInvariant; + internal readonly bool IsValue; + internal readonly bool IsKey; + internal readonly WellKnownVariable WellKnownVariable; + /// /// Returns string representation of current /// public override string ToString() => _value; /// - public bool Equals(ChainSegment other) => _value == other._value; + public bool Equals(ChainSegment other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return EqualsImpl(other); + } /// - public override bool Equals(object obj) => obj is ChainSegment other && Equals(other); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return EqualsImpl((ChainSegment) obj); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool EqualsImpl(ChainSegment other) + { + return IsThis == other.IsThis + && LowerInvariant == other.LowerInvariant; + } /// - public override int GetHashCode() => _value != null ? _value.GetHashCode() : 0; + public override int GetHashCode() => _hashCode; + + private int GetHashCodeImpl() + { + unchecked + { + var hashCode = IsThis.GetHashCode(); + hashCode = (hashCode * 397) ^ (LowerInvariant.GetHashCode()); + return hashCode; + } + } /// public static bool operator ==(ChainSegment a, ChainSegment b) => a.Equals(b); @@ -61,8 +158,14 @@ public ChainSegment(string value) /// public static implicit operator string(ChainSegment segment) => segment._value; + + /// + /// + /// + + public static implicit operator ChainSegment(string segment) => Create(segment); - internal static string TrimSquareBrackets(string key) + private static string TrimSquareBrackets(string key) { //Only trim a single layer of brackets. if (key.StartsWith("[") && key.EndsWith("]")) @@ -72,5 +175,37 @@ internal static string TrimSquareBrackets(string key) return key; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal UndefinedBindingResult GetUndefinedBindingResult(ICompiledHandlebarsConfiguration configuration) + { + if (_undefinedBindingResult != null) return _undefinedBindingResult; + lock (_lock) + { + return _undefinedBindingResult ??= new UndefinedBindingResult(this, configuration); + } + } + + private class Disposer : IDisposable + { + public void Dispose() + { + Lookup.Clear(); + } + } + + public struct ChainSegmentEqualityComparer : IEqualityComparer + { + public bool Equals(ChainSegment x, ChainSegment y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + + return x._hashCode == y._hashCode && x.IsThis == y.IsThis && x.LowerInvariant == y.LowerInvariant; + } + + public int GetHashCode(ChainSegment obj) => obj._hashCode; + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs index c5a6bb7a..1deebdac 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -1,44 +1,67 @@ using System; +using System.Collections.Generic; using System.Linq; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path { - internal delegate object ProcessSegment(ref PathInfo pathInfo, ref BindingContext context, object instance, HashParameterDictionary hashParameters); - /// /// Represents path expression /// - public struct PathInfo : IEquatable + public class PathInfo : IEquatable { private readonly string _path; - internal readonly ProcessSegment ProcessSegment; internal readonly bool IsValidHelperLiteral; internal readonly bool HasValue; internal readonly bool IsThis; + internal readonly bool IsPureThis; internal PathInfo( bool hasValue, string path, bool isValidHelperLiteral, - PathSegment[] segments, - ProcessSegment processSegment + PathSegment[] segments ) { IsValidHelperLiteral = isValidHelperLiteral; HasValue = hasValue; _path = path; + + unchecked + { + _hashCode = (_path.GetHashCode() * 397) ^ HasValue.GetHashCode(); + } + if(!HasValue) return; + IsVariable = path.StartsWith("@"); IsInversion = path.StartsWith("^"); IsBlockHelper = path.StartsWith("#"); IsBlockClose = path.StartsWith("/"); - Segments = segments; - ProcessSegment = processSegment; - TrimmedPath = string.Join(".", Segments?.SelectMany(o => o.PathChain).Select(o => o.TrimmedValue) ?? ArrayEx.Empty()); - IsThis = string.Equals(path, "this", StringComparison.OrdinalIgnoreCase) || path == "." || TrimmedPath.StartsWith("this.", StringComparison.OrdinalIgnoreCase); + ContextChangeDepth = segments?.Count(o => o.IsContextChange) ?? 0; + HasContextChange = ContextChangeDepth > 0; + var plainSegments = segments?.Where(o => !o.IsContextChange && !string.IsNullOrEmpty(o.ToString())).ToArray() ?? ArrayEx.Empty(); + IsThis = string.Equals(path, "this", StringComparison.OrdinalIgnoreCase) || path == "." || plainSegments.Any(o => o.IsThis); + IsPureThis = string.Equals(path, "this", StringComparison.OrdinalIgnoreCase) || path == "."; + + var segment = plainSegments.SingleOrDefault(o => !o.IsThis); + if (segment == null) + { + IsPureThis = true; + TrimmedPath = "."; + PathChain = ArrayEx.Empty(); + return; + } + + TrimmedPath = string.Join(".", segment.PathChain.Select(o => o.TrimmedValue)); + PathChain = segment.PathChain; + + unchecked + { + _trimmedHashCode = TrimmedPath.GetHashCode(); + } } /// @@ -46,34 +69,94 @@ ProcessSegment processSegment /// public readonly bool IsVariable; - /// - public readonly PathSegment[] Segments; + /// + /// + /// + public readonly ChainSegment[] PathChain; internal readonly string TrimmedPath; internal readonly bool IsInversion; internal readonly bool IsBlockHelper; internal readonly bool IsBlockClose; + internal readonly bool HasContextChange; + internal readonly int ContextChangeDepth; + private UndefinedBindingResult _undefinedBindingResult; + private readonly object _lock = new object(); + private readonly int _hashCode; + private readonly int _trimmedHashCode; + private int _comparerTag; + /// + /// Used for special handling of Relaxed Helper Names + /// + internal void TagComparer() + { + _comparerTag++; + } + /// public bool Equals(PathInfo other) { - return HasValue == other.HasValue - && IsVariable == other.IsVariable - && _path == other._path; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return HasValue == other.HasValue && _path == other._path && _comparerTag == other._comparerTag; } /// - public override bool Equals(object obj) => obj is PathInfo other && Equals(other); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((PathInfo) obj); + } /// - public override int GetHashCode() => _path.GetHashCode(); + public override int GetHashCode() => _hashCode; /// /// Returns string representation of current /// public override string ToString() => _path; - + /// public static implicit operator string(PathInfo pathInfo) => pathInfo._path; + + internal UndefinedBindingResult GetUndefinedBindingResult(ICompiledHandlebarsConfiguration configuration) + { + if (_undefinedBindingResult != null) return _undefinedBindingResult; + lock (_lock) + { + return _undefinedBindingResult ?? + (_undefinedBindingResult = new UndefinedBindingResult(this, configuration)); + } + } + + internal static IEqualityComparer PlainPathComparer { get; } = new TrimmedPathEqualityComparer(false); + internal static IEqualityComparer PlainPathWithPartsCountComparer { get; } = new TrimmedPathEqualityComparer(); + + private sealed class TrimmedPathEqualityComparer : IEqualityComparer + { + private readonly bool _countParts; + + public TrimmedPathEqualityComparer(bool countParts = true) + { + _countParts = countParts; + } + + public bool Equals(PathInfo x, PathInfo y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + + return x._comparerTag == y._comparerTag && (!_countParts || x.PathChain.Length == y.PathChain.Length) && string.Equals(x.TrimmedPath, y.TrimmedPath); + } + + public int GetHashCode(PathInfo obj) + { + return obj._trimmedHashCode; + } + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index da0e9d60..4fa08429 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path { - // A lot is going to be changed here in next iterations - internal static partial class PathResolver + internal static class PathResolver { public static PathInfo GetPathInfo(string path) { if (path == "null") - return new PathInfo(false, path, false, null, null); + return new PathInfo(false, path, false, null); var originalPath = path; @@ -33,126 +33,96 @@ public static PathInfo GetPathInfo(string path) { if (segment == "..") { - segments.Add(new PathSegment(segment, ArrayEx.Empty(), true, null)); + isValidHelperLiteral = false; + segments.Add(new PathSegment(segment, ArrayEx.Empty())); continue; } - + + if (segment == ".") + { + isValidHelperLiteral = false; + segments.Add(new PathSegment(segment, ArrayEx.Empty())); + continue; + } + var segmentString = isVariable ? "@" + segment : segment; var chainSegments = GetPathChain(segmentString).ToArray(); if (chainSegments.Length > 1) isValidHelperLiteral = false; - ProcessPathChain chainDelegate; - switch (chainSegments.Length) - { - case 1: chainDelegate = ProcessPathChain_1; break; - case 2: chainDelegate = ProcessPathChain_2; break; - case 3: chainDelegate = ProcessPathChain_3; break; - case 4: chainDelegate = ProcessPathChain_4; break; - case 5: chainDelegate = ProcessPathChain_5; break; - default: chainDelegate = ProcessPathChain_Generic; break; - - } - - segments.Add(new PathSegment(segmentString, chainSegments, false, chainDelegate)); - } - ProcessSegment @delegate; - switch (segments.Count) - { - case 1: @delegate = ProcessSegment_1; break; - case 2: @delegate = ProcessSegment_2; break; - case 3: @delegate = ProcessSegment_3; break; - case 4: @delegate = ProcessSegment_4; break; - case 5: @delegate = ProcessSegment_5; break; - default: @delegate = ProcessSegment_Generic; break; + segments.Add(new PathSegment(segmentString, chainSegments)); } - - return new PathInfo(true, originalPath, isValidHelperLiteral, segments.ToArray(), @delegate); + + return new PathInfo(true, originalPath, isValidHelperLiteral, segments.ToArray()); } - - //TODO: make path resolution logic smarter - public static object ResolvePath(BindingContext context, ref PathInfo pathInfo) + public static object ResolvePath(BindingContext context, PathInfo pathInfo) { - if (!pathInfo.HasValue) - return null; + if (!pathInfo.HasValue) return null; var instance = context.Value; - var hashParameters = instance as HashParameterDictionary; - - return pathInfo.ProcessSegment(ref pathInfo, ref context, instance, hashParameters); - } - - private static bool TryProcessSegment( - ref PathInfo pathInfo, - ref PathSegment segment, - ref BindingContext context, - ref object instance, - HashParameterDictionary hashParameters - ) - { - if (segment.IsJumpUp) return TryProcessJumpSegment(ref pathInfo, ref instance, ref context); - - instance = segment.ProcessPathChain(context, hashParameters, ref pathInfo, ref segment, instance); - return !(instance is UndefinedBindingResult); - } - private static bool TryProcessJumpSegment( - ref PathInfo pathInfo, - ref object instance, - ref BindingContext context - ) - { - context = context.ParentContext; - if (context == null) + if (pathInfo.HasContextChange) { - if (pathInfo.IsVariable) + for (var i = 0; i < pathInfo.ContextChangeDepth; i++) { - instance = string.Empty; - return false; - } + context = context.ParentContext; + if (context == null) + { + if (!pathInfo.IsVariable) + throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); + + return string.Empty; + } - throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); + instance = context.Value; + } } - instance = context.Value; - return true; - } + if (pathInfo.IsPureThis) return instance; + + var hashParameters = instance as HashParameterDictionary; + + var pathChain = pathInfo.PathChain; + + for (var index = 0; index < pathChain.Length; index++) + { + var chainSegment = pathChain[index]; + instance = ResolveValue(context, instance, chainSegment); - private static object ProcessChainSegment( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref ChainSegment chainSegment, - object instance - ) - { - instance = ResolveValue(context, instance, ref chainSegment); + if (!(instance is UndefinedBindingResult)) + { + continue; + } - if (!(instance is UndefinedBindingResult)) - return instance; + if (hashParameters == null || hashParameters.ContainsKey(chainSegment) || context.ParentContext == null) + { + if (context.Configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo, (instance as UndefinedBindingResult).Value); + + return instance; + } + + instance = ResolveValue(context.ParentContext, context.ParentContext.Value, chainSegment); + if (!(instance is UndefinedBindingResult result)) + { + continue; + } - if (hashParameters == null || hashParameters.ContainsKey(chainSegment) || - context.ParentContext == null) - { if (context.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo, (instance as UndefinedBindingResult).Value); + throw new HandlebarsUndefinedBindingException(pathInfo, result.Value); + return instance; } - instance = ResolveValue(context.ParentContext, context.ParentContext.Value, ref chainSegment); - if (!(instance is UndefinedBindingResult result)) return instance; - - if (context.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo, result.Value); - return result; + return instance; } - + private static IEnumerable GetPathChain(string segmentString) { var insideEscapeBlock = false; - var pathChainParts = segmentString.Split(new[]{'.'}, StringSplitOptions.RemoveEmptyEntries); - if (pathChainParts.Length == 0 && segmentString == ".") return new[] { new ChainSegment("this") }; - + var pathChainParts = segmentString.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries); + if (pathChainParts.Length == 0 && segmentString == ".") return new[] {ChainSegment.Create("this")}; + var pathChain = pathChainParts.Aggregate(new List(), (list, next) => { if (insideEscapeBlock) @@ -162,7 +132,7 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].ToString()}.{next}"); + list[list.Count - 1] = ChainSegment.Create($"{list[list.Count - 1]}.{next}"); return list; } @@ -176,252 +146,62 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list.Add(new ChainSegment(next)); + list.Add(ChainSegment.Create(next)); return list; }); return pathChain; } - private static object ResolveValue(BindingContext context, object instance, ref ChainSegment chainSegment) + private static object ResolveValue(BindingContext context, object instance, ChainSegment chainSegment) { object resolvedValue; if (chainSegment.IsVariable) { - return !context.TryGetContextVariable(ref chainSegment, out resolvedValue) - ? new UndefinedBindingResult(chainSegment, context.Configuration) - : resolvedValue; + return context.TryGetContextVariable(chainSegment, out resolvedValue) + ? resolvedValue + : chainSegment.GetUndefinedBindingResult(context.Configuration); } if (chainSegment.IsThis) return instance; - if (TryAccessMember(instance, ref chainSegment, context.Configuration, out resolvedValue) - || context.TryGetVariable(ref chainSegment, out resolvedValue)) + if (context.TryGetVariable(chainSegment, out resolvedValue) + || TryAccessMember(instance, chainSegment, context.Configuration, out resolvedValue)) { return resolvedValue; } - - if (chainSegment.LowerInvariant == "value" && context.TryGetVariable(ref chainSegment, out resolvedValue, true)) + + if (chainSegment.IsValue && context.TryGetContextVariable(chainSegment, out resolvedValue)) { return resolvedValue; } - return new UndefinedBindingResult(chainSegment, context.Configuration); + return chainSegment.GetUndefinedBindingResult(context.Configuration); } - public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) + public static bool TryAccessMember(object instance, ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) { if (instance == null) { - value = new UndefinedBindingResult(chainSegment, configuration); + value = chainSegment.GetUndefinedBindingResult(configuration); return false; } - var memberName = chainSegment.ToString(); - var instanceType = instance.GetType(); - memberName = TryResolveMemberName(instance, memberName, configuration, out var result) - ? ChainSegment.TrimSquareBrackets(result).Intern() - : chainSegment.TrimmedValue; - - if (!configuration.ObjectDescriptorProvider.CanHandleType(instanceType)) - { - value = new UndefinedBindingResult(memberName, configuration); - return false; - } + chainSegment = ResolveMemberName(instance, chainSegment, configuration); - if (!configuration.ObjectDescriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) - { - value = new UndefinedBindingResult(memberName, configuration); - return false; - } - - return descriptor.MemberAccessor.TryGetValue(instance, instanceType, memberName, out value); + value = null; + return ObjectDescriptor.TryCreate(instance, configuration, out var descriptor) + && descriptor.MemberAccessor.TryGetValue(instance, chainSegment, out value); } - private static bool TryResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration, out string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ChainSegment ResolveMemberName(object instance, ChainSegment memberName, + ICompiledHandlebarsConfiguration configuration) { var resolver = configuration.ExpressionNameResolver; - if (resolver == null) - { - value = null; - return false; - } + if (resolver == null) return memberName; - value = resolver.ResolveExpressionName(instance, memberName); - return true; - } - } - - internal static partial class PathResolver - { - private static object ProcessSegment_1( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters); - return instance; - } - - private static object ProcessSegment_2( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters); - return instance; - } - - private static object ProcessSegment_3( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters); - return instance; - } - - private static object ProcessSegment_4( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[3], ref context, ref instance, hashParameters); - return instance; - } - - private static object ProcessSegment_5( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[3], ref context, ref instance, hashParameters) && - TryProcessSegment(ref pathInfo, ref pathInfo.Segments[4], ref context, ref instance, hashParameters); - return instance; - } - - private static object ProcessSegment_Generic( - ref PathInfo pathInfo, - ref BindingContext context, - object instance, - HashParameterDictionary hashParameters - ) - { - for (var segmentIndex = 0; segmentIndex < pathInfo.Segments.Length; segmentIndex++) - { - if (!TryProcessSegment( - ref pathInfo, - ref pathInfo.Segments[segmentIndex], - ref context, - ref instance, - hashParameters) - ) - { - return instance; - } - } - - return instance; - } - - private static object ProcessPathChain_1( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); - } - private static object ProcessPathChain_2( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); - return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); - } - - private static object ProcessPathChain_3( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); - return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); - } - - private static object ProcessPathChain_4( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); - return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[3], instance); - } - - private static object ProcessPathChain_5( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[3], instance); - return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[4], instance); - } - - private static object ProcessPathChain_Generic( - BindingContext context, - HashParameterDictionary hashParameters, - ref PathInfo pathInfo, - ref PathSegment segment, - object instance - ) - { - for (var pathChainIndex = 0; pathChainIndex < segment.PathChain.Length; pathChainIndex++) - { - ref var chainSegment = ref segment.PathChain[pathChainIndex]; - instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref chainSegment, instance); - } - - return instance; + return resolver.ResolveExpressionName(instance, memberName.TrimmedValue); } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs index fed4e09b..4f470f25 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs @@ -1,27 +1,26 @@ using System; +using System.Collections.Generic; namespace HandlebarsDotNet.Compiler.Structure.Path { - internal delegate object ProcessPathChain(BindingContext context, HashParameterDictionary hashParameters, ref PathInfo pathInfo, ref PathSegment segment, object instance); - /// /// Represents parts of single separated with '/'. /// - public struct PathSegment : IEquatable + public class PathSegment : IEquatable { private readonly string _segment; - internal readonly ProcessPathChain ProcessPathChain; - internal readonly bool IsJumpUp; + internal readonly bool IsContextChange; + internal readonly bool IsThis; - internal PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, ProcessPathChain processPathChain) + internal PathSegment(string segment, ChainSegment[] chain) { _segment = segment; - IsJumpUp = isJumpUp; + IsContextChange = string.Equals("..", segment);; + IsThis = string.Equals(".", segment); PathChain = chain; - ProcessPathChain = processPathChain; } - + /// public readonly ChainSegment[] PathChain; @@ -32,18 +31,35 @@ internal PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, Proces public override string ToString() => _segment; /// - public bool Equals(PathSegment other) => _segment == other._segment; + public bool Equals(PathSegment other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return IsContextChange == other.IsContextChange && _segment == other._segment; + } /// - public override bool Equals(object obj) => obj is PathSegment other && Equals(other); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((PathSegment) obj); + } /// - public override int GetHashCode() => _segment != null ? _segment.GetHashCode() : 0; + public override int GetHashCode() + { + unchecked + { + return ((_segment != null ? _segment.GetHashCode() : 0) * 397) ^ IsContextChange.GetHashCode(); + } + } /// - public static bool operator ==(PathSegment a, PathSegment b) => a.Equals(b); + public static bool operator ==(PathSegment a, PathSegment b) => EqualityComparer.Default.Equals(a, b); /// - public static bool operator !=(PathSegment a, PathSegment b) => !a.Equals(b); + public static bool operator !=(PathSegment a, PathSegment b) => !EqualityComparer.Default.Equals(a, b); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index 69c1b9d1..ee771eac 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -23,7 +23,13 @@ public UndefinedBindingResult(ChainSegment value, ICompiledHandlebarsConfigurati public override string ToString() { - var formatter = _configuration.UnresolvedBindingFormatter ?? string.Empty; + var formatter = _configuration.UnresolvedBindingFormatter; + if (formatter == null) + { + if(string.IsNullOrEmpty(Value)) return string.Empty; + formatter = string.Empty; + } + return string.Format( formatter, Value ); } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 71d17033..91763691 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -2,9 +2,11 @@ using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Runtime.CompilerServices; using Expressions.Shortcuts; -using HandlebarsDotNet.Adapters; using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers.BlockHelpers; +using HandlebarsDotNet.Polyfills; using HandlebarsDotNet.ValueProviders; using static Expressions.Shortcuts.ExpressionShortcuts; @@ -12,13 +14,15 @@ namespace HandlebarsDotNet.Compiler { internal class BlockHelperFunctionBinder : HandlebarsExpressionVisitor { + private enum BlockHelperDirection { Direct, Inverse } + private CompilationContext CompilationContext { get; } public BlockHelperFunctionBinder(CompilationContext compilationContext) { CompilationContext = compilationContext; } - + protected override Expression VisitStatementExpression(StatementExpression sex) { return sex.Body is BlockHelperExpression ? Visit(sex.Body) : sex; @@ -27,184 +31,125 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) { var isInlinePartial = bhex.HelperName == "#*inline"; - - var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(bhex.HelperName); - var context = Arg(CompilationContext.BindingContext); - var bindingContext = isInlinePartial - ? context.Cast() - : context.Property(o => o.Value); - - var readerContext = Arg(bhex.Context); - var body = FunctionBuilder.CompileCore(((BlockExpression) bhex.Body).Expressions, CompilationContext.Configuration); - var inverse = FunctionBuilder.CompileCore(((BlockExpression) bhex.Inversion).Expressions, CompilationContext.Configuration); - var helperName = pathInfo.TrimmedPath; - var helperPrefix = bhex.IsRaw || pathInfo.IsBlockHelper ? '#' : '^'; - var textWriter = context.Property(o => o.TextWriter); - var args = bhex.Arguments - .ApplyOn((PathExpression pex) => pex.Context = PathExpression.ResolutionContext.Parameter) - .Select(o => FunctionBuilder.Reduce(o, CompilationContext)); - var arguments = Array(args); - var configuration = Arg(CompilationContext.Configuration); + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(bhex.HelperName); + var bindingContext = Arg(CompilationContext.BindingContext); + var context = isInlinePartial + ? bindingContext.As() + : bindingContext.Property(o => o.Value); - var reducerNew = New(() => new LambdaReducer(context, body, inverse)); - var reducer = Var(); - - var blockParamsProvider = Var(); - var blockParamsExpression = Call( - () => BlockParamsValueProvider.Create(context, Arg(bhex.BlockParams)) - ); + var readerContext = bhex.Context; + var direct = Compile(bhex.Body); + var inverse = Compile(bhex.Inversion); + var arguments = CreateArguments(); - var helperOptions = CreateHelperOptions(bhex, helperPrefix, reducer, blockParamsProvider, configuration, context); + var helperName = pathInfo.TrimmedPath; + var direction = bhex.IsRaw || pathInfo.IsBlockHelper ? BlockHelperDirection.Direct : BlockHelperDirection.Inverse; + var blockParams = CreateBlockParams(); var blockHelpers = CompilationContext.Configuration.BlockHelpers; - if (blockHelpers.TryGetValue(helperName, out var helper)) + + if (blockHelpers.TryGetValue(pathInfo, out var descriptor)) { - return Block() - .Parameter(reducer, reducerNew) - .Parameter(blockParamsProvider, blockParamsExpression) - .Line(blockParamsProvider.Using((self, builder) => - { - builder - .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) - .Line(Try() - .Body(Call( - () => helper(textWriter, helperOptions, bindingContext, arguments) - )) - .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) - ); - })); + return BindByRef(descriptor); } - foreach (var resolver in CompilationContext.Configuration.HelperResolvers) + var helperResolvers = CompilationContext.Configuration.HelperResolvers; + for (var index = 0; index < helperResolvers.Count; index++) { - if (!resolver.TryResolveBlockHelper(helperName, out helper)) continue; - - return Block() - .Parameter(reducer, reducerNew) - .Parameter(blockParamsProvider, blockParamsExpression) - .Line(blockParamsProvider.Using((self, builder) => - { - builder - .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) - .Line(Try() - .Body(Call( - () => helper(textWriter, helperOptions, bindingContext, arguments) - )) - .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) - ); - })); + var resolver = helperResolvers[index]; + if (!resolver.TryResolveBlockHelper(helperName, out var resolvedDescriptor)) continue; + + return Bind(resolvedDescriptor); } - return Block() - .Parameter(reducer, reducerNew) - .Parameter(blockParamsProvider, blockParamsExpression) - .Line(blockParamsProvider.Using((self, builder) => - { - builder - .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) - .Line(Try() - .Body(Call( - () => LateBoundCall( - helperName, - helperPrefix, - context, - (IReaderContext) readerContext, - textWriter, helperOptions, - body, - inverse, - bindingContext, - self, - arguments - ) - )) - .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) - ); - })); - } + var lateBindBlockHelperDescriptor = new LateBindBlockHelperDescriptor(pathInfo, CompilationContext.Configuration); + var lateBindBlockHelperRef = new StrongBox(lateBindBlockHelperDescriptor); + blockHelpers.Add(pathInfo, lateBindBlockHelperRef); - private static ExpressionContainer CreateHelperOptions( - BlockHelperExpression bhex, - char helperPrefix, - ExpressionContainer reducer, - ExpressionContainer blockParamsProvider, - ExpressionContainer configuration, - ExpressionContainer context) - { - ExpressionContainer helperOptions; - switch (helperPrefix) + return BindByRef(lateBindBlockHelperRef); + + ExpressionContainer CreateBlockParams() { - case '#': - helperOptions = New( - () => new HelperOptions( - reducer.Member(o => o.Direct), - reducer.Member(o => o.Inverse), - blockParamsProvider, - configuration, - context) - ); - break; - - case '^': - helperOptions = New( - () => new HelperOptions( - reducer.Member(o => o.Inverse), - reducer.Member(o => o.Direct), - blockParamsProvider, - configuration, - context) - ); - break; + var parameters = bhex.BlockParams?.BlockParam?.Parameters; + if (parameters == null) + { + parameters = ArrayEx.Empty(); + } - default: - throw new HandlebarsCompilerException($"Helper {bhex.HelperName} referenced with unsupported prefix", bhex.Context); + return Arg(parameters); + } + + ExpressionContainer CreateArguments() + { + var args = bhex.Arguments + .ApplyOn((PathExpression pex) => pex.Context = PathExpression.ResolutionContext.Parameter) + .Select(o => FunctionBuilder.Reduce(o, CompilationContext)); + + return Array(args); + } + + Action Compile(Expression expression) + { + var blockExpression = (BlockExpression) expression; + return FunctionBuilder.CompileCore(blockExpression.Expressions, CompilationContext.Configuration); } - return helperOptions; - } - - private static void LateBoundCall( - string helperName, - char helperPrefix, - BindingContext bindingContext, - IReaderContext readerContext, - TextWriter output, - HelperOptions options, - Action body, - Action inverse, - dynamic context, - BlockParamsValueProvider blockParamsValueProvider, - params object[] arguments - ) - { - try + Expression BindByRef(StrongBox value) { - if (bindingContext.Configuration.BlockHelpers.TryGetValue(helperName, out var helper)) + return direction switch { - helper(output, options, context, arguments); - return; - } - - foreach (var resolver in bindingContext.Configuration.HelperResolvers) - { - if (!resolver.TryResolveBlockHelper(helperName, out helper)) continue; + BlockHelperDirection.Direct => Call(() => + BlockHelperCallBindingByRef(bindingContext, context, blockParams, direct, inverse, arguments, value)), - helper(output, options, context, arguments); - - return; - } - - if(arguments.Length > 0) throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. BlockHelper '{helperName}'", readerContext); - - var pathInfo = bindingContext.Configuration.PathInfoStore.GetOrAdd(helperName); - var value = PathResolver.ResolvePath(bindingContext, ref pathInfo); - DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); + BlockHelperDirection.Inverse => Call(() => + BlockHelperCallBindingByRef(bindingContext, context, blockParams, inverse, direct, arguments, value)), + + _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext) + }; } - catch(Exception e) when(!(e is HandlebarsException)) + + Expression Bind(BlockHelperDescriptorBase value) { - throw new HandlebarsRuntimeException($"Error occured while executing `{helperName}.`", e, readerContext); + return direction switch + { + BlockHelperDirection.Direct => Call(() => + BlockHelperCallBinding(bindingContext, context, blockParams, direct, inverse, arguments, value)), + + BlockHelperDirection.Inverse => Call(() => + BlockHelperCallBinding(bindingContext, context, blockParams, inverse, direct, arguments, value)), + + _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext) + }; } } - } -} + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BlockHelperCallBindingByRef( + BindingContext bindingContext, + object context, + ChainSegment[] blockParamsVariables, + Action direct, + Action inverse, + object[] arguments, + StrongBox helper) + { + using var helperOptions = HelperOptions.Create(direct, inverse, blockParamsVariables, bindingContext); + helper.Value.Invoke(bindingContext.TextWriter, helperOptions, context, arguments); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BlockHelperCallBinding( + BindingContext bindingContext, + object context, + ChainSegment[] blockParamsVariables, + Action direct, + Action inverse, + object[] arguments, + BlockHelperDescriptorBase helper) + { + using var helperOptions = HelperOptions.Create(direct, inverse, blockParamsVariables, bindingContext); + helper.Invoke(bindingContext.TextWriter, helperOptions, context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs index 877d6982..d7727058 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -21,24 +21,21 @@ public static Expression> Bind(CompilationContext con var objectParameter = Parameter("data"); var bindingContext = Arg(context.BindingContext); - var inlinePartialsParameter = Null>>(); var textEncoder = configuration.Property(o => o.TextEncoder); var encodedWriterExpression = Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); var parentContextArg = Arg(parentContext); var newBindingContext = Call( - () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) + () => BindingContext.Create((ICompiledHandlebarsConfiguration) configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath) ); - - var shouldDispose = Var("shouldDispose"); - + Expression blockBuilder = Block() .Parameter(bindingContext) - .Parameter(shouldDispose) + .Parameter(out var shouldDispose) + .Line(bindingContext.Assign(objectParameter.As())) .Line(Condition() - .If(objectParameter.Is()) - .Then(bindingContext.Assign(objectParameter.As())) - .Else(block => + .If(Expression.Equal(bindingContext, Null())) + .Then(block => { block.Line(shouldDispose.Assign(true)); block.Line(bindingContext.Assign(newBindingContext)); @@ -48,7 +45,12 @@ public static Expression> Bind(CompilationContext con .Body(block => block.Lines(((BlockExpression) body).Expressions)) .Finally(Condition() .If(shouldDispose) - .Then(bindingContext.Call(o => o.Dispose())) + .Then(block => + { + block + .Line(bindingContext.Call(o => o.TextWriter.Dispose())) + .Line(bindingContext.Call(o => o.Dispose())); + }) ) ); @@ -63,36 +65,52 @@ public static Expression> Bind(Compil var objectParameter = Parameter("data"); var bindingContext = Arg(context.BindingContext); - var inlinePartialsParameter = Null>>(); var textEncoder = configuration.Property(o => o.TextEncoder); + var writer = Var("writer"); var encodedWriterExpression = Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); var parentContextArg = Var("parentContext"); var newBindingContext = Call( - () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) + () => BindingContext.Create((ICompiledHandlebarsConfiguration) configuration, objectParameter, writer, parentContextArg, templatePath) ); - var shouldDispose = Var("shouldDispose"); - Expression blockBuilder = Block() .Parameter(bindingContext) - .Parameter(shouldDispose) + .Parameter(writer) + .Parameter(out var shouldDispose) + .Parameter(out var shouldDisposeWriter) + .Line(bindingContext.Assign(objectParameter.As())) .Line(Condition() - .If(objectParameter.Is()) - .Then(bindingContext.Assign(objectParameter.As()) - ) - .Else(block => + .If(Expression.Equal(bindingContext, Null())) + .Then(block => { - block.Line(shouldDispose.Assign(true)); + block.Line(shouldDispose.Assign(true)) + .Line(writer.Assign(writerParameter.As())) + .Line(Condition() + .If(Expression.Equal(writer, Null())) + .Then(b => + { + b.Line(shouldDisposeWriter.Assign(true)) + .Line(writer.Assign(encodedWriterExpression)); + }) + ); + block.Line(bindingContext.Assign(newBindingContext)); }) ) .Line(Try() .Body(block => block.Lines(((BlockExpression) body).Expressions)) - .Finally(Condition() - .If(shouldDispose) - .Then(bindingContext.Call(o => o.Dispose())) - ) + .Finally(block => + { + block.Lines( + Condition() + .If(shouldDispose) + .Then(bindingContext.Call(o => o.Dispose())), + Condition() + .If(shouldDisposeWriter) + .Then(writer.Call(o => o.Dispose())) + ); + }) ); return Expression.Lambda>(blockBuilder, (ParameterExpression) parentContextArg.Expression, (ParameterExpression) writerParameter.Expression, (ParameterExpression) objectParameter.Expression); diff --git a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs index e432a82d..13ff95c0 100644 --- a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs +++ b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs @@ -1,31 +1,27 @@ using System; using System.Collections; using System.IO; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Polyfills; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { internal static class DeferredSectionBlockHelper { - public static void Helper(BindingContext context, char prefix, object value, - Action body, Action inverse, - BlockParamsValueProvider blockParamsValueProvider) + private static readonly ChainSegment[] BlockParamsVariables = ArrayEx.Empty(); + + public static void PlainHelper(BindingContext context, object value, + Action body, Action inverse) { - if (prefix == '#') - { - RenderSection(value, context, body, inverse, blockParamsValueProvider); - } - else - { - RenderSection(value, context, inverse, body, blockParamsValueProvider); - } + RenderSection(value, context, body, inverse); } private static void RenderSection( object value, BindingContext context, Action body, - Action inversion, - BlockParamsValueProvider blockParamsValueProvider + Action inversion ) { switch (value) @@ -44,7 +40,7 @@ BlockParamsValueProvider blockParamsValueProvider return; case IEnumerable enumerable: - Iterator.Iterate(context, blockParamsValueProvider, enumerable, body, inversion); + Iterator.Iterate(context, BlockParamsVariables, enumerable, body, inversion); break; default: diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index e7169619..09c54631 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,7 +1,8 @@ using System.Linq; using System.Linq.Expressions; -using System.IO; +using System.Runtime.CompilerServices; using Expressions.Shortcuts; +using HandlebarsDotNet.Helpers; using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler @@ -24,8 +25,7 @@ protected override Expression VisitHelperExpression(HelperExpression hex) { var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(hex.HelperName); if(!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming) return Expression.Empty(); - - var readerContext = Arg(hex.Context); + var helperName = pathInfo.TrimmedPath; var bindingContext = Arg(CompilationContext.BindingContext); var contextValue = bindingContext.Property(o => o.Value); @@ -37,83 +37,24 @@ protected override Expression VisitHelperExpression(HelperExpression hex) var args = Array(arguments); var configuration = CompilationContext.Configuration; - if (configuration.Helpers.TryGetValue(helperName, out var helper)) + if (configuration.Helpers.TryGetValue(pathInfo, out var helper)) { - return Call(() => helper(textWriter, contextValue, args)); + return Call(() => helper.Value.WriteInvoke(bindingContext, textWriter, contextValue, args)); } - if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) + for (var index = 0; index < configuration.HelperResolvers.Count; index++) { - return Call(() => - CaptureResult(textWriter, Call(() => returnHelper(contextValue, args))) - ); - } - - foreach (var resolver in configuration.HelperResolvers) - { - if (resolver.TryResolveReturnHelper(helperName, typeof(object), out var resolvedHelper)) + var resolver = configuration.HelperResolvers[index]; + if (resolver.TryResolveHelper(helperName, typeof(object), out var resolvedHelper)) { - return Call(() => - CaptureResult(textWriter, Call(() => resolvedHelper(contextValue, args))) - ); + return Call(() => resolvedHelper.WriteInvoke(bindingContext, textWriter, contextValue, args)); } } - return Call(() => - CaptureResult(textWriter, Call(() => - LateBindHelperExpression(bindingContext, helperName, args, (IReaderContext) readerContext) - )) - ); - } - - // will be significantly improved in next iterations - public static ResultHolder TryLateBindHelperExpression(BindingContext context, string helperName, object[] arguments) - { - var configuration = context.Configuration; - if (configuration.Helpers.TryGetValue(helperName, out var helper)) - { - using (var write = new PolledStringWriter(configuration.FormatProvider)) - { - helper(write, context.Value, arguments); - var result = write.ToString(); - return new ResultHolder(true, result); - } - } + var lateBindDescriptor = new StrongBox(new LateBindHelperDescriptor(pathInfo, configuration)); + configuration.Helpers.Add(pathInfo, lateBindDescriptor); - if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) - { - var result = returnHelper(context.Value, arguments); - return new ResultHolder(true, result); - } - - var targetType = arguments.FirstOrDefault()?.GetType(); - foreach (var resolver in configuration.HelperResolvers) - { - if (!resolver.TryResolveReturnHelper(helperName, targetType, out returnHelper)) continue; - - var result = returnHelper(context.Value, arguments); - return new ResultHolder(true, result); - } - - return new ResultHolder(false, null); - } - - private static object LateBindHelperExpression(BindingContext context, string helperName, object[] arguments, - IReaderContext readerContext) - { - var result = TryLateBindHelperExpression(context, helperName, arguments); - if (result.Success) - { - return result.Value; - } - - throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. Helper '{helperName}'", readerContext); - } - - private static object CaptureResult(TextWriter writer, object result) - { - writer?.WriteSafeString(result); - return result; + return Call(() => lateBindDescriptor.Value.WriteInvoke(bindingContext, textWriter, contextValue, args)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index 2c5dfc74..ebc9f454 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -1,11 +1,12 @@ using System; -using System.Linq; using System.Linq.Expressions; using System.IO; using System.Collections; using System.Collections.Generic; using Expressions.Shortcuts; using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.MemberAccessors; using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.Polyfills; using HandlebarsDotNet.ValueProviders; @@ -13,10 +14,17 @@ namespace HandlebarsDotNet.Compiler { + internal static class BoxedValues + { + public static readonly object True = true; + public static readonly object False = false; + public static readonly object Zero = 0; + } + internal class IteratorBinder : HandlebarsExpressionVisitor { private CompilationContext CompilationContext { get; } - + public IteratorBinder(CompilationContext compilationContext) { CompilationContext = compilationContext; @@ -25,8 +33,7 @@ public IteratorBinder(CompilationContext compilationContext) protected override Expression VisitIteratorExpression(IteratorExpression iex) { var context = Arg(CompilationContext.BindingContext); - var sequence = Var("sequence"); - + var template = FunctionBuilder.CompileCore(new[] {iex.Template}, CompilationContext.Configuration); var ifEmpty = FunctionBuilder.CompileCore(new[] {iex.IfEmpty}, CompilationContext.Configuration); @@ -36,39 +43,29 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) } var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext)); - var blockParams = Arg(iex.BlockParams); - var blockParamsProvider = Call(() => BlockParamsValueProvider.Create(context, blockParams)); - var blockParamsProviderVar = Var(); + var blockParamsValues = CreateBlockParams(); - return Block() - .Parameter(sequence, compiledSequence) - .Parameter(blockParamsProviderVar, blockParamsProvider) - .Line(blockParamsProviderVar.Using((self, builder) => + return Call(() => + Iterator.Iterate(context, blockParamsValues, compiledSequence, template, ifEmpty) + ); + + ExpressionContainer CreateBlockParams() + { + var parameters = iex.BlockParams?.BlockParam?.Parameters; + if (parameters == null) { - builder - .Line(Call(() => - Iterator.Iterate(context, self, sequence, template, ifEmpty) - )); - })); + parameters = ArrayEx.Empty(); + } + + return Arg(parameters); + } } } internal static class Iterator { - private static readonly ConfigureBlockParams BlockParamsEnumerableConfiguration = (parameters, binder, deps) => - { - binder(parameters.ElementAtOrDefault(0), ctx => ctx.As().Value, deps[0]); - binder(parameters.ElementAtOrDefault(1), ctx => ctx.As().Index, deps[0]); - }; - - private static readonly ConfigureBlockParams BlockParamsObjectEnumeratorConfiguration = (parameters, binder, deps) => - { - binder(parameters.ElementAtOrDefault(0), ctx => ctx.As().Value, deps[0]); - binder(parameters.ElementAtOrDefault(1), ctx => ctx.As().Key, deps[0]); - }; - public static void Iterate(BindingContext context, - BlockParamsValueProvider blockParamsValueProvider, + ChainSegment[] blockParamsVariables, object target, Action template, Action ifEmpty) @@ -80,8 +77,7 @@ public static void Iterate(BindingContext context, } var targetType = target.GetType(); - if (!(context.Configuration.ObjectDescriptorProvider.CanHandleType(targetType) && - context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor))) + if (!context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor)) { ifEmpty(context, context.TextWriter, context.Value); return; @@ -90,173 +86,211 @@ public static void Iterate(BindingContext context, if (!descriptor.ShouldEnumerate) { var properties = descriptor.GetProperties(descriptor, target); - if (properties is IList propertiesList) + if (properties is IList propertiesList) { - IterateObjectWithStaticProperties(context, descriptor, blockParamsValueProvider, target, propertiesList, targetType, template, ifEmpty); + IterateObjectWithStaticProperties(context, blockParamsVariables, descriptor, target, propertiesList, targetType, template, ifEmpty); return; } - IterateObject(context, descriptor, blockParamsValueProvider, target, properties, targetType, template, ifEmpty); + IterateObject(context, descriptor, blockParamsVariables, target, properties, targetType, template, ifEmpty); return; } if (target is IList list) { - IterateList(context, blockParamsValueProvider, list, template, ifEmpty); + IterateList(context, blockParamsVariables, list, template, ifEmpty); return; } - IterateEnumerable(context, blockParamsValueProvider, (IEnumerable) target, template, ifEmpty); + IterateEnumerable(context, blockParamsVariables, (IEnumerable) target, template, ifEmpty); } private static void IterateObject(BindingContext context, ObjectDescriptor descriptor, - BlockParamsValueProvider blockParamsValueProvider, + ChainSegment[] blockParamsVariables, object target, - IEnumerable properties, + IEnumerable properties, Type targetType, Action template, Action ifEmpty) { - using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) - { - blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); - - iterator.Index = 0; - var accessor = descriptor.MemberAccessor; - var enumerable = new ExtendedEnumerable(properties); - bool enumerated = false; + using var innerContext = context.CreateChildContext(); + var iterator = new ObjectIteratorValues(innerContext); + var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); + + blockParams.CreateProperty(0, out var _0); + blockParams.CreateProperty(1, out var _1); + + var accessor = new MemberAccessor(target, descriptor); + var enumerable = new ExtendedEnumerable(properties); + var enumerated = false; - foreach (var enumerableValue in enumerable) - { - enumerated = true; - iterator.Key = enumerableValue.Value.ToString(); - var key = iterator.Key.Intern(); - iterator.Value = accessor.TryGetValue(target, targetType, key, out var value) ? value : null; - iterator.First = enumerableValue.IsFirst; - iterator.Last = enumerableValue.IsLast; - iterator.Index = enumerableValue.Index; + object iteratorValue; + ChainSegment iteratorKey; + + foreach (var enumerableValue in enumerable) + { + enumerated = true; + iteratorKey = ChainSegment.Create(enumerableValue.Value); + + iterator.Key = iteratorKey; + iterator.Index = enumerableValue.Index; + if (enumerableValue.Index == 1) iterator.First = BoxedValues.False; + if (enumerableValue.IsLast) iterator.Last = BoxedValues.True; - using(var innerContext = context.CreateChildContext(iterator.Value)) - { - innerContext.RegisterValueProvider(blockParamsValueProvider); - innerContext.RegisterValueProvider(iterator); - template(context, context.TextWriter, innerContext); - } - } + iteratorValue = accessor[iteratorKey]; + iterator.Value = iteratorValue; + innerContext.Value = iteratorValue; + + blockParams[_0] = iteratorValue; + blockParams[_1] = iteratorKey; + + template(context, context.TextWriter, innerContext); + } - if (iterator.Index == 0 && !enumerated) - { - ifEmpty(context, context.TextWriter, context.Value); - } + if (!enumerated) + { + ifEmpty(context, context.TextWriter, context.Value); } } - + private static void IterateObjectWithStaticProperties(BindingContext context, + ChainSegment[] blockParamsVariables, ObjectDescriptor descriptor, - BlockParamsValueProvider blockParamsValueProvider, object target, - IList properties, + IList properties, Type targetType, Action template, Action ifEmpty) { - using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) + using var innerContext = context.CreateFrame(); + var iterator = new ObjectIteratorValues(innerContext); + var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); + + blockParams.CreateProperty(0, out var _0); + blockParams.CreateProperty(1, out var _1); + + var count = properties.Count; + var accessor = new MemberAccessor(target, descriptor); + + var iterationIndex = 0; + var lastIndex = count - 1; + + object iteratorValue; + ChainSegment iteratorKey; + + for (; iterationIndex < count; iterationIndex++) { - blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); - - var accessor = descriptor.MemberAccessor; + iteratorKey = properties[iterationIndex]; - var count = properties.Count; - for (iterator.Index = 0; iterator.Index < count; iterator.Index++) - { - iterator.Key = properties[iterator.Index].ToString(); - iterator.Value = accessor.TryGetValue(target, targetType, iterator.Key, out var value) ? value : null; - iterator.First = iterator.Index == 0; - iterator.Last = iterator.Index == count - 1; + iterator.Index = iterationIndex; + iterator.Key = iteratorKey; + if (iterationIndex == 1) iterator.First = BoxedValues.False; + if (iterationIndex == lastIndex) iterator.Last = BoxedValues.True; - using (var innerContext = context.CreateChildContext(iterator.Value)) - { - innerContext.RegisterValueProvider(blockParamsValueProvider); - innerContext.RegisterValueProvider(iterator); - template(context, context.TextWriter, innerContext); - } - } + iteratorValue = accessor[iteratorKey]; + iterator.Value = iteratorValue; + innerContext.Value = iteratorValue; + + blockParams[_0] = iteratorValue; + blockParams[_1] = iteratorKey; - if (iterator.Index == 0) - { - ifEmpty(context, context.TextWriter, context.Value); - } + template(context, context.TextWriter, innerContext); + } + + if (iterationIndex == 0) + { + ifEmpty(context, context.TextWriter, context.Value); } } private static void IterateList(BindingContext context, - BlockParamsValueProvider blockParamsValueProvider, + ChainSegment[] blockParamsVariables, IList target, Action template, Action ifEmpty) { - using (var iterator = IteratorValueProvider.Create()) - { - blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); - - var count = target.Count; - for (iterator.Index = 0; iterator.Index < count; iterator.Index++) - { - iterator.Value = target[iterator.Index]; - iterator.First = iterator.Index == 0; - iterator.Last = iterator.Index == count - 1; + using var innerContext = context.CreateFrame(); + var iterator = new IteratorValues(innerContext); + var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); + + blockParams.CreateProperty(0, out var _0); + blockParams.CreateProperty(1, out var _1); + + var count = target.Count; - using(var innerContext = context.CreateChildContext(iterator.Value)) - { - innerContext.RegisterValueProvider(blockParamsValueProvider); - innerContext.RegisterValueProvider(iterator); - template(context, context.TextWriter, innerContext); - } - } + object boxedIndex; + object iteratorValue; + + var iterationIndex = 0; + var lastIndex = count - 1; + for (; iterationIndex < count; iterationIndex++) + { + iteratorValue = target[iterationIndex]; + + iterator.Value = iteratorValue; + if (iterationIndex == 1) iterator.First = BoxedValues.False; + if (iterationIndex == lastIndex) iterator.Last = BoxedValues.True; + + boxedIndex = iterationIndex; + iterator.Index = boxedIndex; + + blockParams[_0] = iteratorValue; + blockParams[_1] = boxedIndex; + + innerContext.Value = iteratorValue; + + template(context, context.TextWriter, innerContext); + } - if (iterator.Index == 0) - { - ifEmpty(context, context.TextWriter, context.Value); - } + if (iterationIndex == 0) + { + ifEmpty(context, context.TextWriter, context.Value); } } private static void IterateEnumerable(BindingContext context, - BlockParamsValueProvider blockParamsValueProvider, + ChainSegment[] blockParamsVariables, IEnumerable target, Action template, Action ifEmpty) { - using (var iterator = IteratorValueProvider.Create()) - { - blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); + using var innerContext = context.CreateChildContext(); + var iterator = new IteratorValues(innerContext); + var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); + + blockParams.CreateProperty(0, out var _0); + blockParams.CreateProperty(1, out var _1); + + var enumerable = new ExtendedEnumerable(target); + var enumerated = false; - iterator.Index = 0; - var enumerable = new ExtendedEnumerable(target); - bool enumerated = false; + object boxedIndex; + object iteratorValue; + + foreach (var enumerableValue in enumerable) + { + enumerated = true; - foreach (var enumerableValue in enumerable) - { - enumerated = true; - iterator.Value = enumerableValue.Value; - iterator.First = enumerableValue.IsFirst; - iterator.Last = enumerableValue.IsLast; + if (enumerableValue.Index == 1) iterator.First = BoxedValues.False; + if (enumerableValue.IsLast) iterator.Last = BoxedValues.True; - using(var innerContext = context.CreateChildContext(iterator.Value)) - { - innerContext.RegisterValueProvider(blockParamsValueProvider); - innerContext.RegisterValueProvider(iterator); - template(context, context.TextWriter, innerContext); - } - - iterator.Index++; - } + boxedIndex = enumerableValue.Index; + iteratorValue = enumerableValue.Value; + iterator.Value = iteratorValue; + iterator.Index = boxedIndex; + + blockParams[_0] = iteratorValue; + blockParams[_1] = boxedIndex; + + innerContext.Value = iteratorValue; + + template(context, context.TextWriter, innerContext); + } - if (iterator.Index == 0 && !enumerated) - { - ifEmpty(context, context.TextWriter, context.Value); - } + if (!enumerated) + { + ifEmpty(context, context.TextWriter, context.Value); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 0fdd85c3..e37cc4c0 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -23,7 +23,7 @@ protected override Expression VisitPartialExpression(PartialExpression pex) { var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var partialBlockTemplate = pex.Fallback != null - ? FunctionBuilder.CompileCore(new[] { pex.Fallback }, null, CompilationContext.Configuration) + ? FunctionBuilder.CompileCore(new[] { pex.Fallback }, CompilationContext.Configuration) : null; if (pex.Argument != null || partialBlockTemplate != null) @@ -36,14 +36,14 @@ protected override Expression VisitPartialExpression(PartialExpression pex) var partialName = ExpressionShortcuts.Cast(pex.PartialName); var configuration = ExpressionShortcuts.Arg(CompilationContext.Configuration); return ExpressionShortcuts.Call(() => - InvokePartialWithFallback(partialName, bindingContext, configuration) + InvokePartialWithFallback(partialName, bindingContext, (ICompiledHandlebarsConfiguration) configuration) ); } private static void InvokePartialWithFallback( string partialName, BindingContext context, - HandlebarsConfiguration configuration) + ICompiledHandlebarsConfiguration configuration) { if (InvokePartial(partialName, context, configuration)) return; if (context.PartialBlockTemplate == null) @@ -55,13 +55,13 @@ private static void InvokePartialWithFallback( return; } - context.PartialBlockTemplate(context.TextWriter, context); + context.PartialBlockTemplate(context, context.TextWriter, context); } private static bool InvokePartial( string partialName, BindingContext context, - HandlebarsConfiguration configuration) + ICompiledHandlebarsConfiguration configuration) { if (partialName.Equals(SpecialPartialBlockName)) { @@ -70,22 +70,23 @@ private static bool InvokePartial( return false; } - context.PartialBlockTemplate(context.TextWriter, context); + context.PartialBlockTemplate(context, context.TextWriter, context); return true; } //if we have an inline partial, skip the file system and RegisteredTemplates collection - if (context.InlinePartialTemplates.ContainsKey(partialName)) + if (context.InlinePartialTemplates.TryGetValue(partialName, out var partial)) { - context.InlinePartialTemplates[partialName](context.TextWriter, context); + partial(context.TextWriter, context); return true; } // Partial is not found, so call the resolver and attempt to load it. if (!configuration.RegisteredTemplates.ContainsKey(partialName)) { + var handlebars = Handlebars.Create(configuration); if (configuration.PartialTemplateResolver == null - || !configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath)) + || !configuration.PartialTemplateResolver.TryRegisterPartial(handlebars, partialName, context.TemplatePath)) { // Template not found. return false; diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index ce0fb05b..4605ae91 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,6 +1,8 @@ using System.Linq.Expressions; +using System.Runtime.CompilerServices; using Expressions.Shortcuts; using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Polyfills; using static Expressions.Shortcuts.ExpressionShortcuts; @@ -27,25 +29,32 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitPathExpression(PathExpression pex) { var context = Arg(CompilationContext.BindingContext); - var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(pex.Path); - - var resolvePath = Call(() => PathResolver.ResolvePath(context, ref pathInfo)); + var configuration = CompilationContext.Configuration; + var pathInfo = configuration.PathInfoStore.GetOrAdd(pex.Path); + var resolvePath = Call(() => PathResolver.ResolvePath(context, pathInfo)); + if (pex.Context == PathExpression.ResolutionContext.Parameter) return resolvePath; - if (!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming || pathInfo.IsThis) return resolvePath; - - var helperName = pathInfo.TrimmedPath; - var tryBoundHelper = Call(() => - HelperFunctionBinder.TryLateBindHelperExpression(context, helperName, ArrayEx.Empty()) - ); - - return Block() - .Parameter(out var result, tryBoundHelper) - .Line(Condition() - .If(result.Member(o => o.Success)) - .Then(result.Member(o => o.Value)) - .Else(resolvePath) - ); + if (pathInfo.IsVariable || pathInfo.IsThis) return resolvePath; + if (!pathInfo.IsValidHelperLiteral && !configuration.Compatibility.RelaxedHelperNaming) return resolvePath; + + if (!configuration.Helpers.TryGetValue(pathInfo, out var helper)) + { + helper = new StrongBox(new LateBindHelperDescriptor(pathInfo, configuration)); + configuration.Helpers.Add(pathInfo, helper); + } + else if (configuration.Compatibility.RelaxedHelperNaming) + { + pathInfo.TagComparer(); + if (!configuration.Helpers.ContainsKey(pathInfo)) + { + helper = new StrongBox(new LateBindHelperDescriptor(pathInfo, configuration)); + configuration.Helpers.Add(pathInfo, helper); + } + } + + var argumentsArg = Arg(ArrayEx.Empty()); + return context.Call(o => helper.Value.ReturnInvoke(o, o.Value, argumentsArg)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs index c9c0155d..a32a0c2b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -14,8 +15,8 @@ public StaticReplacer(CompilationContext compilationContext) protected override Expression VisitStaticExpression(StaticExpression stex) { - var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - var value = ExpressionShortcuts.Arg(stex.Value); + var context = Arg(CompilationContext.BindingContext); + var value = Arg(stex.Value); return context.Call(o => o.TextWriter.Write(value, false)); } diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index af51405b..5d12b882 100644 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,33 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.IO; -using System.Linq; -using System.Reflection; +using System.Linq.Expressions; using Expressions.Shortcuts; +using HandlebarsDotNet.Helpers; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { // will be significantly improved in next iterations internal class SubExpressionVisitor : HandlebarsExpressionVisitor { - private readonly IExpressionCompiler _expressionCompiler; private CompilationContext CompilationContext { get; } public SubExpressionVisitor(CompilationContext compilationContext) { CompilationContext = compilationContext; - - _expressionCompiler = CompilationContext.Configuration.CompileTimeConfiguration.ExpressionCompiler; } protected override Expression VisitSubExpression(SubExpressionExpression subex) { switch (subex.Expression) { - case InvocationExpression invocationExpression: - return HandleInvocationExpression(invocationExpression); - case MethodCallExpression callExpression: return HandleMethodCallExpression(callExpression); @@ -40,70 +31,14 @@ protected override Expression VisitSubExpression(SubExpressionExpression subex) } } - private Expression HandleMethodCallExpression(MethodCallExpression helperCall) + private static Expression HandleMethodCallExpression(MethodCallExpression helperCall) { - if (helperCall.Type != typeof(void)) - { - return helperCall.Update(helperCall.Object, - ReplaceValuesOf(helperCall.Arguments, ExpressionShortcuts.Null()).Select(Visit) - ); - } - - var context = ExpressionShortcuts.Var(); - var writer = ExpressionShortcuts.Var(); - helperCall = helperCall.Update(ExpressionUtils.ReplaceParameters(helperCall.Object, context), - ExpressionUtils.ReplaceParameters( - ReplaceValuesOf(helperCall.Arguments, writer), new Expression[] { context } - ).Select(Visit) - ); - - var formatProvider = ExpressionShortcuts.Arg(CompilationContext.Configuration.FormatProvider); - var block = ExpressionShortcuts.Block() - .Parameter(writer, ExpressionShortcuts.New(() => new PolledStringWriter((IFormatProvider) formatProvider))) - .Line(writer.Using((o, body) => - body.Line(helperCall) - .Line(o.Call(x => (object) x.ToString())) - )); - - var continuation = _expressionCompiler.Compile(Expression.Lambda>(block, (ParameterExpression) context)); - return ExpressionShortcuts.Arg(Expression.Invoke(Expression.Constant(continuation), CompilationContext.BindingContext)); - } - - private static IEnumerable ReplaceValuesOf(IEnumerable instance, Expression newValue) - { - var targetType = typeof(T); - return instance.Select(value => targetType.IsAssignableFrom(value.Type) - ? newValue - : value); - } - - private Expression HandleInvocationExpression(InvocationExpression invocation) - { - if (invocation.Type != typeof(void)) - { - return invocation.Update(invocation.Expression, - ReplaceValuesOf(invocation.Arguments, ExpressionShortcuts.Null()).Select(Visit) - ); - } - - var context = ExpressionShortcuts.Var(); - var writer = ExpressionShortcuts.Var(); - invocation = invocation.Update(ExpressionUtils.ReplaceParameters(invocation.Expression, context), - ExpressionUtils.ReplaceParameters( - ReplaceValuesOf(invocation.Arguments, writer), new Expression[]{ context } - ).Select(Visit) - ); - - var formatProvider = ExpressionShortcuts.Arg(CompilationContext.Configuration.FormatProvider); - var block = ExpressionShortcuts.Block() - .Parameter(writer, ExpressionShortcuts.New(() => new PolledStringWriter((IFormatProvider) formatProvider))) - .Line(writer.Using((o, body) => - body.Line(invocation) - .Line(o.Call(x => (object) x.ToString())) - )); + var bindingContext = Arg(helperCall.Arguments[0]); + var context = Arg(helperCall.Arguments[2]); + var arguments = Arg(helperCall.Arguments[3]); + var helper = Arg(helperCall.Object); - var continuation = _expressionCompiler.Compile(Expression.Lambda>(block, (ParameterExpression) context)); - return ExpressionShortcuts.Arg(Expression.Invoke(Expression.Constant(continuation), CompilationContext.BindingContext)); + return helper.Call(o => o.ReturnInvoke(bindingContext, context, arguments)); } } } diff --git a/source/Handlebars/Configuration/CompileTimeConfiguration.cs b/source/Handlebars/Configuration/CompileTimeConfiguration.cs index 7d458455..71e217a2 100644 --- a/source/Handlebars/Configuration/CompileTimeConfiguration.cs +++ b/source/Handlebars/Configuration/CompileTimeConfiguration.cs @@ -12,26 +12,17 @@ public class CompileTimeConfiguration /// public IList ObjectDescriptorProviders { get; } = new List(); - - public IList ExpressionMiddleware { get; internal set; } = new List(); + public IList ExpressionMiddleware { get; } = new List(); /// - public IList Features { get; internal set; } = new List + public IList Features { get; } = new List { new BuildInHelpersFeatureFactory(), new ClosureFeatureFactory(), new DefaultCompilerFeatureFactory(), new MissingHelperFeatureFactory() }; - - /// - public IList AliasProviders { get; internal set; } = new List(); - - /// - /// Defines whether Handlebars uses aggressive caching to achieve better performance. by default. - /// - public bool UseAggressiveCaching { get; set; } = true; - + /// /// The compiler used to compile /// diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs index 9dcf9836..649f1251 100644 --- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -1,74 +1,64 @@ using HandlebarsDotNet.Compiler.Resolvers; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; +using HandlebarsDotNet.Collections; using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet { - - public class HandlebarsConfiguration + public sealed class HandlebarsConfiguration : IHandlebarsTemplateRegistrations { + public IDictionary Helpers { get; } - public IDictionary Helpers { get; protected set; } + public IDictionary BlockHelpers { get; } - - public IDictionary ReturnHelpers { get; protected set; } - - - public IDictionary BlockHelpers { get; protected set; } - - - public IDictionary> RegisteredTemplates { get; protected set; } + public IDictionary> RegisteredTemplates { get; } /// - public ICollection HelperResolvers { get; protected set; } - + public IList HelperResolvers { get; } - public virtual IExpressionNameResolver ExpressionNameResolver { get; set; } - + public IExpressionNameResolver ExpressionNameResolver { get; set; } - public virtual ITextEncoder TextEncoder { get; set; } = new HtmlEncoder(); + public ITextEncoder TextEncoder { get; set; } = new HtmlEncoder(); /// - public virtual IFormatProvider FormatProvider { get; set; } = CultureInfo.CurrentCulture; - + public IFormatProvider FormatProvider { get; set; } = CultureInfo.CurrentCulture; - public virtual ViewEngineFileSystem FileSystem { get; set; } - + public ViewEngineFileSystem FileSystem { get; set; } - public virtual string UnresolvedBindingFormatter { get; set; } - + public string UnresolvedBindingFormatter { get; set; } - public virtual bool ThrowOnUnresolvedBindingExpression { get; set; } + public bool ThrowOnUnresolvedBindingExpression { get; set; } /// /// The resolver used for unregistered partials. Defaults /// to the . /// - public virtual IPartialTemplateResolver PartialTemplateResolver { get; set; } = new FileSystemPartialTemplateResolver(); + public IPartialTemplateResolver PartialTemplateResolver { get; set; } = new FileSystemPartialTemplateResolver(); /// /// The handler called when a partial template cannot be found. /// - public virtual IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; } + public IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; } + + /// + public IList AliasProviders { get; internal set; } = new List(); /// - public virtual Compatibility Compatibility { get; set; } = new Compatibility(); + public Compatibility Compatibility { get; } = new Compatibility(); /// - public virtual CompileTimeConfiguration CompileTimeConfiguration { get; } = new CompileTimeConfiguration(); - + public CompileTimeConfiguration CompileTimeConfiguration { get; } = new CompileTimeConfiguration(); public HandlebarsConfiguration() { - Helpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - ReturnHelpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - BlockHelpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - RegisteredTemplates = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); - HelperResolvers = new List(); + Helpers = new ObservableDictionary(comparer: StringComparer.OrdinalIgnoreCase); + BlockHelpers = new ObservableDictionary(comparer: StringComparer.OrdinalIgnoreCase); + RegisteredTemplates = new ObservableDictionary>(comparer: StringComparer.OrdinalIgnoreCase); + HelperResolvers = new ObservableList(); } } } diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs index a889ae11..674b26e5 100644 --- a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -1,54 +1,81 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Compiler.Resolvers; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Features; using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.ObjectDescriptors; namespace HandlebarsDotNet { + public interface IHandlebarsTemplateRegistrations + { + IDictionary> RegisteredTemplates { get; } + ViewEngineFileSystem FileSystem { get; } + } - public interface ICompiledHandlebarsConfiguration + /// + /// + /// + public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrations { + /// + /// + /// + HandlebarsConfiguration UnderlingConfiguration { get; } + /// + /// + /// IExpressionNameResolver ExpressionNameResolver { get; } - + /// + /// + /// ITextEncoder TextEncoder { get; } - + /// + /// + /// IFormatProvider FormatProvider { get; } - - ViewEngineFileSystem FileSystem { get; } - - + /// + /// + /// string UnresolvedBindingFormatter { get; } - + /// + /// + /// bool ThrowOnUnresolvedBindingExpression { get; } - + /// + /// + /// IPartialTemplateResolver PartialTemplateResolver { get; } - + /// + /// + /// IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; } + + /// + /// + /// + IDictionary> Helpers { get; } + /// + /// + /// + IDictionary> BlockHelpers { get; } - IDictionary Helpers { get; } - - - IDictionary ReturnHelpers { get; } - - - IDictionary BlockHelpers { get; } - - - ICollection HelperResolvers { get; } - - - IDictionary> RegisteredTemplates { get; } + /// + /// + /// + IList HelperResolvers { get; } /// Compatibility Compatibility { get; } @@ -57,23 +84,20 @@ public interface ICompiledHandlebarsConfiguration IObjectDescriptorProvider ObjectDescriptorProvider { get; } /// - ICollection ExpressionMiddleware { get; } + IList ExpressionMiddleware { get; } /// - ICollection AliasProviders { get; } + IList AliasProviders { get; } /// IExpressionCompiler ExpressionCompiler { get; set; } - - /// - bool UseAggressiveCaching { get; set; } - + /// /// List of associated s /// IReadOnlyList Features { get; } - /// - IReadOnlyPathInfoStore PathInfoStore { get; } + /// + IPathInfoStore PathInfoStore { get; } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs index 4f79b057..0b6d3f85 100644 --- a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -3,94 +3,154 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Resolvers; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Features; using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; +using HandlebarsDotNet.MemberAliasProvider; using HandlebarsDotNet.ObjectDescriptors; namespace HandlebarsDotNet { - internal sealed class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompiledHandlebarsConfiguration + internal class HandlebarsConfigurationAdapter : ICompiledHandlebarsConfiguration { - private readonly HandlebarsConfiguration _configuration; + private readonly PathInfoStore _pathInfoStore; - public override IExpressionNameResolver ExpressionNameResolver => _configuration.ExpressionNameResolver; - public override ITextEncoder TextEncoder => _configuration.TextEncoder; - public override IFormatProvider FormatProvider => _configuration.FormatProvider; - public override ViewEngineFileSystem FileSystem => _configuration.FileSystem; - public override string UnresolvedBindingFormatter => _configuration.UnresolvedBindingFormatter; - public override bool ThrowOnUnresolvedBindingExpression => _configuration.ThrowOnUnresolvedBindingExpression; - public override IPartialTemplateResolver PartialTemplateResolver => _configuration.PartialTemplateResolver; - public override IMissingPartialTemplateHandler MissingPartialTemplateHandler => _configuration.MissingPartialTemplateHandler; - public override Compatibility Compatibility => _configuration.Compatibility; - public override CompileTimeConfiguration CompileTimeConfiguration { get; } - - public List Features { get; } - public IObjectDescriptorProvider ObjectDescriptorProvider { get; } - public ICollection ExpressionMiddleware => CompileTimeConfiguration.ExpressionMiddleware; - public ICollection AliasProviders => CompileTimeConfiguration.AliasProviders; - public IExpressionCompiler ExpressionCompiler + public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) { - get => CompileTimeConfiguration.ExpressionCompiler; - set => CompileTimeConfiguration.ExpressionCompiler = value; + UnderlingConfiguration = configuration; + + HelperResolvers = new ObservableList(configuration.HelperResolvers); + RegisteredTemplates = new ObservableDictionary>(configuration.RegisteredTemplates); + PathInfoStore = _pathInfoStore = new PathInfoStore(); + ObjectDescriptorProvider = CreateObjectDescriptorProvider(); + AliasProviders = new ObservableList(UnderlingConfiguration.AliasProviders); + + ExpressionMiddleware = new ObservableList(UnderlingConfiguration.CompileTimeConfiguration.ExpressionMiddleware); + + Features = UnderlingConfiguration.CompileTimeConfiguration.Features + .Select(o => o.CreateFeature()) + .OrderBy(o => o.GetType().GetTypeInfo().GetCustomAttribute()?.Order ?? 100) + .ToList(); + + CreateHelpersSubscription(); + CreateBlockHelpersSubscription(); + + AliasProviders.Add(new CollectionMemberAliasProvider(this)); } + + public HandlebarsConfiguration UnderlingConfiguration { get; } + public IExpressionNameResolver ExpressionNameResolver => UnderlingConfiguration.ExpressionNameResolver; + public ITextEncoder TextEncoder => UnderlingConfiguration.TextEncoder; + public IFormatProvider FormatProvider => UnderlingConfiguration.FormatProvider; + public ViewEngineFileSystem FileSystem => UnderlingConfiguration.FileSystem; + public string UnresolvedBindingFormatter => UnderlingConfiguration.UnresolvedBindingFormatter; + public bool ThrowOnUnresolvedBindingExpression => UnderlingConfiguration.ThrowOnUnresolvedBindingExpression; + public IPartialTemplateResolver PartialTemplateResolver => UnderlingConfiguration.PartialTemplateResolver; + public IMissingPartialTemplateHandler MissingPartialTemplateHandler => UnderlingConfiguration.MissingPartialTemplateHandler; + public Compatibility Compatibility => UnderlingConfiguration.Compatibility; + + public IObjectDescriptorProvider ObjectDescriptorProvider { get; } + public IList ExpressionMiddleware { get; } + public IList AliasProviders { get; } + public IExpressionCompiler ExpressionCompiler { get; set; } + public IReadOnlyList Features { get; } + public IPathInfoStore PathInfoStore { get; } - public bool UseAggressiveCaching + public IDictionary> Helpers { get; private set; } + public IDictionary> BlockHelpers { get; private set; } + public IList HelperResolvers { get; } + public IDictionary> RegisteredTemplates { get; } + + private void CreateHelpersSubscription() { - get => CompileTimeConfiguration.UseAggressiveCaching; - set => CompileTimeConfiguration.UseAggressiveCaching = value; - } - IReadOnlyList ICompiledHandlebarsConfiguration.Features => Features; + var existingHelpers = UnderlingConfiguration.Helpers.ToDictionary( + o => _pathInfoStore.GetOrAdd($"[{o.Key}]"), + o => new StrongBox(o.Value) + ); - public PathInfoStore PathInfoStore { get; } + Helpers = new ObservableDictionary>(existingHelpers, Compatibility.RelaxedHelperNaming ? PathInfo.PlainPathComparer : PathInfo.PlainPathWithPartsCountComparer); + + var helpersObserver = new ObserverBuilder>() + .OnEvent.ReplacedObservableEvent>( + @event => Helpers[_pathInfoStore.GetOrAdd($"[{@event.Key}]")].Value = @event.Value + ) + .OnEvent.AddedObservableEvent>( + @event => + { + Helpers.AddOrUpdate(_pathInfoStore.GetOrAdd($"[{@event.Key}]"), + h => new StrongBox(h), + (h, o) => o.Value = h, + @event.Value); + }) + .OnEvent.RemovedObservableEvent>(@event => + { + if (Helpers.TryGetValue(_pathInfoStore.GetOrAdd($"[{@event.Key}]"), out var helperToRemove)) + { + helperToRemove.Value = new LateBindHelperDescriptor(@event.Key, this); + } + }) + .Build(); - IReadOnlyPathInfoStore ICompiledHandlebarsConfiguration.PathInfoStore => PathInfoStore; + UnderlingConfiguration.Helpers + .As>() + .Subscribe(helpersObserver); + } - internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) + private void CreateBlockHelpersSubscription() { - _configuration = configuration; - - Helpers = new CascadeDictionary(configuration.Helpers, StringComparer.OrdinalIgnoreCase); - ReturnHelpers = new CascadeDictionary(configuration.ReturnHelpers, StringComparer.OrdinalIgnoreCase); - BlockHelpers = new CascadeDictionary(configuration.BlockHelpers, StringComparer.OrdinalIgnoreCase); - RegisteredTemplates = new CascadeDictionary>(configuration.RegisteredTemplates, StringComparer.OrdinalIgnoreCase); - HelperResolvers = new CascadeCollection(configuration.HelperResolvers); - PathInfoStore = new PathInfoStore(); - - CompileTimeConfiguration = new CompileTimeConfiguration - { - UseAggressiveCaching = _configuration.CompileTimeConfiguration.UseAggressiveCaching, - ExpressionCompiler = _configuration.CompileTimeConfiguration.ExpressionCompiler, - ExpressionMiddleware = new List(configuration.CompileTimeConfiguration.ExpressionMiddleware), - Features = new List(configuration.CompileTimeConfiguration.Features), - AliasProviders = new List(configuration.CompileTimeConfiguration.AliasProviders) + var existingBlockHelpers = UnderlingConfiguration.BlockHelpers.ToDictionary( + o => _pathInfoStore.GetOrAdd($"[{o.Key}]"), + o => new StrongBox(o.Value) + ); + + BlockHelpers = + new ObservableDictionary>(existingBlockHelpers, Compatibility.RelaxedHelperNaming ? PathInfo.PlainPathComparer : PathInfo.PlainPathWithPartsCountComparer); + + var blockHelpersObserver = new ObserverBuilder>() + .OnEvent.ReplacedObservableEvent>( + @event => BlockHelpers[_pathInfoStore.GetOrAdd($"[{@event.Key}]")].Value = @event.Value) + .OnEvent.AddedObservableEvent>( + @event => + { + BlockHelpers.AddOrUpdate(_pathInfoStore.GetOrAdd($"[{@event.Key}]"), + h => new StrongBox(h), + (h, o) => o.Value = h, + @event.Value); + }) + .OnEvent.RemovedObservableEvent>(@event => { - new CollectionMemberAliasProvider(this) // will not be registered by default in next iterations - } - }; - + if (BlockHelpers.TryGetValue(_pathInfoStore.GetOrAdd($"[{@event.Key}]"), out var helperToRemove)) + { + helperToRemove.Value = new LateBindBlockHelperDescriptor(@event.Key, this); + } + }) + .Build(); + + UnderlingConfiguration.BlockHelpers + .As>() + .Subscribe(blockHelpersObserver); + } + + private IObjectDescriptorProvider CreateObjectDescriptorProvider() + { var objectDescriptorProvider = new ObjectDescriptorProvider(this); - var listObjectDescriptor = new CollectionObjectDescriptor(objectDescriptorProvider); - var providers = new List(configuration.CompileTimeConfiguration.ObjectDescriptorProviders) + var providers = new List(UnderlingConfiguration.CompileTimeConfiguration.ObjectDescriptorProviders) { - new ContextObjectDescriptor(), new StringDictionaryObjectDescriptorProvider(), new GenericDictionaryObjectDescriptorProvider(), new DictionaryObjectDescriptor(), - listObjectDescriptor, - new EnumerableObjectDescriptor(listObjectDescriptor), new KeyValuePairObjectDescriptorProvider(), + new CollectionObjectDescriptor(objectDescriptorProvider), + new EnumerableObjectDescriptor(objectDescriptorProvider), objectDescriptorProvider, new DynamicObjectDescriptor() }; - ObjectDescriptorProvider = new ObjectDescriptorFactory(providers); - - Features = CompileTimeConfiguration - .Features.Select(o => o.CreateFeature()) - .OrderBy(o => o.GetType().GetTypeInfo().GetCustomAttribute()?.Order ?? 100) - .ToList(); + return new ObjectDescriptorFactory(providers); } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/PathInfoStore.cs b/source/Handlebars/Configuration/PathInfoStore.cs index ce2aeed0..fe20590c 100644 --- a/source/Handlebars/Configuration/PathInfoStore.cs +++ b/source/Handlebars/Configuration/PathInfoStore.cs @@ -7,11 +7,17 @@ namespace HandlebarsDotNet /// /// Provides access to path expressions in the template /// - public interface IReadOnlyPathInfoStore : IReadOnlyDictionary + public interface IPathInfoStore : IReadOnlyDictionary { + /// + /// + /// + /// + /// + PathInfo GetOrAdd(string path); } - internal class PathInfoStore : IReadOnlyPathInfoStore + internal class PathInfoStore : IPathInfoStore { private readonly Dictionary _paths = new Dictionary(); @@ -21,6 +27,12 @@ public PathInfo GetOrAdd(string path) pathInfo = PathResolver.GetPathInfo(path); _paths.Add(path, pathInfo); + + var trimmedPath = pathInfo.TrimmedPath; + if ((pathInfo.IsBlockHelper || pathInfo.IsInversion) && !_paths.ContainsKey(trimmedPath)) + { + _paths.Add(trimmedPath, PathResolver.GetPathInfo(trimmedPath)); + } return pathInfo; } diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index 82419f04..a2e008c8 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace HandlebarsDotNet { @@ -31,5 +32,37 @@ public static IEnumerable ApplyOn(this IEnumerable source, Action(this IDictionary to, TK at, Func add, Action update) + { + if (to.TryGetValue(at, out var value)) + { + update(value); + return; + } + + to.Add(at, add()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddOrUpdate(this IDictionary to, TK at, Func add, Action update, TO context) + { + if (to.TryGetValue(at, out var value)) + { + update(context, value); + return; + } + + to.Add(at, add(context)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this Dictionary from, Dictionary to) + { + if(from.Count == 0) return; + + foreach (var pair in from) to[pair.Key] = pair.Value; + } } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index 15e8fa2f..94017228 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -1,7 +1,6 @@ -using System.IO; -using System.Linq; -using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Compiler.Structure.Path; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet.Features { @@ -16,84 +15,17 @@ public IFeature CreateFeature() [FeatureOrder(int.MinValue)] internal class BuildInHelpersFeature : IFeature { - private ICompiledHandlebarsConfiguration _configuration; - - private static readonly ConfigureBlockParams WithBlockParamsConfiguration = (parameters, binder, deps) => - { - binder(parameters.ElementAtOrDefault(0), ctx => ctx, deps[0]); - }; - public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - _configuration = configuration; - - configuration.BlockHelpers["with"] = With; - configuration.BlockHelpers["*inline"] = Inline; - - configuration.ReturnHelpers["lookup"] = Lookup; + var pathInfoStore = configuration.PathInfoStore; + configuration.BlockHelpers[pathInfoStore.GetOrAdd("with")] = new StrongBox(new WithBlockHelperDescriptor()); + configuration.BlockHelpers[pathInfoStore.GetOrAdd("*inline")] = new StrongBox(new InlineBlockHelperDescriptor()); + configuration.Helpers[pathInfoStore.GetOrAdd("lookup")] = new StrongBox(new LookupReturnHelperDescriptor(configuration)); } public void CompilationCompleted() { // noting to do here } - - private static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) - { - if (arguments.Length != 1) - { - throw new HandlebarsException("{{with}} helper must have exactly one argument"); - } - - options.BlockParams(WithBlockParamsConfiguration, arguments[0]); - - if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) - { - options.Template(output, arguments[0]); - } - else - { - options.Inverse(output, context); - } - } - - private object Lookup(dynamic context, params object[] arguments) - { - if (arguments.Length != 2) - { - throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); - } - - var memberName = arguments[1].ToString(); - var segment = new ChainSegment(memberName); - return !PathResolver.TryAccessMember(arguments[0], ref segment, _configuration, out var value) - ? new UndefinedBindingResult(memberName, _configuration) - : value; - } - - private static void Inline(TextWriter output, HelperOptions options, dynamic context, params object[] 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 is BindingContext)) - { - throw new HandlebarsException("{{*inline}} helper must receiving the full BindingContext"); - } - - var key = arguments[0] as string; - - //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. - context.InlinePartialTemplates.Add(key, options.Template); - } } } \ No newline at end of file diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs index 3682da66..89d3ede9 100644 --- a/source/Handlebars/Features/ClosureFeature.cs +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -33,6 +33,8 @@ public class ClosureFeature : IFeature /// public TemplateClosure TemplateClosure { get; } = new TemplateClosure(); + internal LinkedList Children { get; } = new LinkedList(); + /// /// Middleware to use in order to apply transformation /// @@ -54,6 +56,11 @@ public void OnCompiling(ICompiledHandlebarsConfiguration configuration) public void CompilationCompleted() { TemplateClosure?.Build(); + + foreach (var child in Children) + { + child.CompilationCompleted(); + } } private class ClosureExpressionMiddleware : IExpressionMiddleware diff --git a/source/Handlebars/Features/CollectionMemberAliasProviderFeature.cs b/source/Handlebars/Features/CollectionMemberAliasProviderFeature.cs new file mode 100644 index 00000000..824d2c3b --- /dev/null +++ b/source/Handlebars/Features/CollectionMemberAliasProviderFeature.cs @@ -0,0 +1,38 @@ +using HandlebarsDotNet.MemberAliasProvider; + +namespace HandlebarsDotNet.Features +{ + internal class CollectionMemberAliasProviderFeature : IFeature + { + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) + { + var aliasProvider = new CollectionMemberAliasProvider(configuration); + configuration.AliasProviders.Add(aliasProvider); + } + + public void CompilationCompleted() + { + //nothing to do here + } + } + + internal class CollectionMemberAliasProviderFeatureFactory : IFeatureFactory + { + public IFeature CreateFeature() => new CollectionMemberAliasProviderFeature(); + } + + public static class CollectionMemberAliasProviderExtensions + { + /// + /// Adds support for resolving `.length` from `.count` and vice versa + /// + /// + /// + public static HandlebarsConfiguration UseCollectionMemberAliasProvider(this HandlebarsConfiguration configuration) + { + configuration.CompileTimeConfiguration.Features.Add(new CollectionMemberAliasProviderFeatureFactory()); + + return configuration; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index fde35512..dc72879b 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -19,8 +19,9 @@ internal class DefaultCompilerFeature : IFeature { public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features - .OfType().SingleOrDefault(); + var templateFeature = configuration.Features + .OfType() + .SingleOrDefault(); configuration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); } @@ -33,35 +34,37 @@ public void CompilationCompleted() private class DefaultExpressionCompiler : IExpressionCompiler { private readonly ClosureFeature _closureFeature; - private readonly TemplateClosure _templateClosure; - private readonly ExpressionContainer _closure; private readonly ICollection _expressionMiddleware; public DefaultExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) { _closureFeature = closureFeature; - _templateClosure = closureFeature?.TemplateClosure; - _closure = closureFeature?.ClosureInternal; _expressionMiddleware = configuration.ExpressionMiddleware; } public T Compile(Expression expression) where T: class { expression = (Expression) _expressionMiddleware.Aggregate((Expression) expression, (e, m) => m.Invoke(e)); - - if (_closureFeature == null) + + var closureFeature = _closureFeature; + + if (closureFeature.TemplateClosure.CurrentIndex == -1) { - return expression.Compile(); + closureFeature = new ClosureFeature(); + _closureFeature.Children.AddLast(closureFeature); } + + var templateClosure = closureFeature.TemplateClosure; + var closure = closureFeature.ClosureInternal; - expression = (Expression) _closureFeature.ExpressionMiddleware.Invoke(expression); + expression = (Expression) closureFeature.ExpressionMiddleware.Invoke(expression); - var parameters = new[] { (ParameterExpression) _closure }.Concat(expression.Parameters); + var parameters = new[] { (ParameterExpression) closure }.Concat(expression.Parameters); var lambda = Expression.Lambda(expression.Body, parameters); var compiledLambda = lambda.Compile(); var outerParameters = expression.Parameters.Select(o => Expression.Parameter(o.Type, o.Name)).ToArray(); - var store = Arg(_templateClosure).Member(o => o.Store); + var store = Arg(templateClosure).Member(o => o.Store); var parameterExpressions = new[] { store.Expression }.Concat(outerParameters); var invocationExpression = Expression.Invoke(Expression.Constant(compiledLambda), parameterExpressions); var outerLambda = Expression.Lambda(invocationExpression, outerParameters); diff --git a/source/Handlebars/Features/MissingHelperFeature.cs b/source/Handlebars/Features/MissingHelperFeature.cs index 7270d478..7e345f41 100644 --- a/source/Handlebars/Features/MissingHelperFeature.cs +++ b/source/Handlebars/Features/MissingHelperFeature.cs @@ -1,9 +1,7 @@ -using System; using System.Linq; -using HandlebarsDotNet.Adapters; -using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Compiler.Structure.Path; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet.Features { @@ -19,25 +17,50 @@ public static class MissingHelperFeatureExtension /// public static HandlebarsConfiguration RegisterMissingHelperHook( this HandlebarsConfiguration configuration, - HandlebarsReturnHelper helperMissing = null, - HandlebarsBlockHelper blockHelperMissing = null + HelperDescriptorBase helperMissing = null, + BlockHelperDescriptorBase blockHelperMissing = null ) { var feature = new MissingHelperFeatureFactory(helperMissing, blockHelperMissing); - configuration.CompileTimeConfiguration.Features.Add(feature); + + var features = configuration.CompileTimeConfiguration.Features; + + features.Remove(features.OfType().Single()); + + features.Add(feature); return configuration; } + + /// + /// Allows to intercept calls to missing helpers. + /// For Handlebarsjs docs see: https://handlebarsjs.com/guide/hooks.html#helpermissing + /// + /// + /// Delegate that returns interceptor for and + /// Delegate that returns interceptor for + /// + public static HandlebarsConfiguration RegisterMissingHelperHook( + this HandlebarsConfiguration configuration, + HandlebarsReturnHelper helperMissing = null, + HandlebarsBlockHelper blockHelperMissing = null + ) + { + return configuration.RegisterMissingHelperHook( + helperMissing != null ? new DelegateReturnHelperDescriptor("helperMissing", helperMissing) : null, + blockHelperMissing != null ? new DelegateBlockHelperDescriptor("blockHelperMissing", blockHelperMissing) : null + ); + } } internal class MissingHelperFeatureFactory : IFeatureFactory { - private readonly HandlebarsReturnHelper _returnHelper; - private readonly HandlebarsBlockHelper _blockHelper; + private readonly HelperDescriptorBase _returnHelper; + private readonly BlockHelperDescriptorBase _blockHelper; public MissingHelperFeatureFactory( - HandlebarsReturnHelper returnHelper = null, - HandlebarsBlockHelper blockHelper = null + HelperDescriptorBase returnHelper = null, + BlockHelperDescriptorBase blockHelper = null ) { _returnHelper = returnHelper; @@ -48,115 +71,51 @@ public MissingHelperFeatureFactory( } [FeatureOrder(int.MaxValue)] - internal class MissingHelperFeature : IFeature, IHelperResolver + internal class MissingHelperFeature : IFeature { - private ICompiledHandlebarsConfiguration _configuration; - private HandlebarsReturnHelper _returnHelper; - private HandlebarsBlockHelper _blockHelper; + private const string HelperMissingKey = "helperMissing"; + private const string BlockHelperMissingKey = "blockHelperMissing"; + + private readonly HelperDescriptorBase _helper; + private readonly BlockHelperDescriptorBase _blockHelper; public MissingHelperFeature( - HandlebarsReturnHelper returnHelper, - HandlebarsBlockHelper blockHelper + HelperDescriptorBase helper, + BlockHelperDescriptorBase blockHelper ) { - _returnHelper = returnHelper; + _helper = helper; _blockHelper = blockHelper; } - public void OnCompiling(ICompiledHandlebarsConfiguration configuration) => _configuration = configuration; - - public void CompilationCompleted() + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - var existingFeatureRegistrations = _configuration - .HelperResolvers - .OfType() - .ToList(); + var pathInfoStore = configuration.PathInfoStore; - if (existingFeatureRegistrations.Any()) - { - existingFeatureRegistrations.ForEach(o => _configuration.HelperResolvers.Remove(o)); - } - - ResolveHelpersRegistrations(); - - _configuration.HelperResolvers.Add(this); - } - - public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) - { - if (_returnHelper == null) + var helperMissingPathInfo = pathInfoStore.GetOrAdd(HelperMissingKey); + if(!configuration.Helpers.ContainsKey(helperMissingPathInfo)) { - _configuration.ReturnHelpers.TryGetValue("helperMissing", out _returnHelper); + var helper = _helper ?? new MissingHelperDescriptor(); + configuration.Helpers.AddOrUpdate(helperMissingPathInfo, + h => new StrongBox(h), + (h, o) => o.Value = h, + helper); } - if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var simpleHelper)) + var blockHelperMissingKeyPathInfo = pathInfoStore.GetOrAdd(BlockHelperMissingKey); + if(!configuration.BlockHelpers.ContainsKey(blockHelperMissingKeyPathInfo)) { - _returnHelper = new HelperToReturnHelperAdapter(simpleHelper); + var blockHelper = _blockHelper ?? new MissingBlockHelperDescriptor(); + configuration.BlockHelpers.AddOrUpdate(blockHelperMissingKeyPathInfo, + h => new StrongBox(h), + (h, o) => o.Value = h, + blockHelper); } - - if (_returnHelper == null) - { - helper = null; - return false; - } - - helper = (context, arguments) => - { - var instance = (object) context; - var chainSegment = new ChainSegment(name); - if (PathResolver.TryAccessMember(instance, ref chainSegment, _configuration, out var value)) - return value; - - var newArguments = new object[arguments.Length + 1]; - Array.Copy(arguments, newArguments, arguments.Length); - newArguments[arguments.Length] = name; - - return _returnHelper(context, newArguments); - }; - - return true; - } - - public bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper) - { - if (_blockHelper == null) - { - _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out _blockHelper); - } - - if (_blockHelper == null) - { - helper = null; - return false; - } - - helper = (output, options, context, arguments) => - { - options["name"] = name; - _blockHelper(output, options, context, arguments); - }; - - return true; } - private void ResolveHelpersRegistrations() + public void CompilationCompleted() { - if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var helperMissing)) - { - _returnHelper = new HelperToReturnHelperAdapter(helperMissing); - } - - if (_returnHelper == null && - _configuration.ReturnHelpers.TryGetValue("helperMissing", out var returnHelperMissing)) - { - _returnHelper = returnHelperMissing; - } - - if (_blockHelper == null && - _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out var blockHelperMissing)) - { - _blockHelper = blockHelperMissing; - } + // nothing to do } } } \ No newline at end of file diff --git a/source/Handlebars/Features/WarmUpFeature.cs b/source/Handlebars/Features/WarmUpFeature.cs index 05b624ca..f2be1132 100644 --- a/source/Handlebars/Features/WarmUpFeature.cs +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -31,7 +31,6 @@ public void OnCompiling(ICompiledHandlebarsConfiguration configuration) var descriptorProvider = configuration.ObjectDescriptorProvider; foreach (var type in _types) { - if(!descriptorProvider.CanHandleType(type)) continue; descriptorProvider.TryGetDescriptor(type, out _); } } diff --git a/source/Handlebars/FileSystemPartialTemplateResolver.cs b/source/Handlebars/FileSystemPartialTemplateResolver.cs index 161734a9..b5c9baa8 100644 --- a/source/Handlebars/FileSystemPartialTemplateResolver.cs +++ b/source/Handlebars/FileSystemPartialTemplateResolver.cs @@ -11,12 +11,13 @@ public bool TryRegisterPartial(IHandlebars env, string partialName, string templ throw new ArgumentNullException(nameof(env)); } - if (env.Configuration?.FileSystem == null || templatePath == null || partialName == null) + var handlebarsTemplateRegistrations = env.Configuration as IHandlebarsTemplateRegistrations ?? env.As().CompiledConfiguration; + if (handlebarsTemplateRegistrations?.FileSystem == null || templatePath == null || partialName == null) { return false; } - var partialPath = env.Configuration.FileSystem.Closest(templatePath, + var partialPath = handlebarsTemplateRegistrations.FileSystem.Closest(templatePath, "partials/" + partialName + ".hbs"); if (partialPath != null) @@ -24,7 +25,7 @@ public bool TryRegisterPartial(IHandlebars env, string partialName, string templ var compiled = env .CompileView(partialPath); - env.Configuration.RegisteredTemplates.Add(partialName, (writer, o) => + handlebarsTemplateRegistrations.RegisteredTemplates.Add(partialName, (writer, o) => { writer.Write(compiled(o)); }); diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index ab2a4e0d..36476ddb 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Concurrent; using System.IO; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet { @@ -26,6 +29,14 @@ namespace HandlebarsDotNet /// /// public delegate void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments); + + /// + /// BlockHelper: {{#helper}}..{{/helper}} + /// + /// + /// + /// + public delegate object HandlebarsReturnBlockHelper(HelperOptions options, dynamic context, params object[] arguments); public sealed class Handlebars @@ -46,6 +57,23 @@ public static IHandlebars Create(HandlebarsConfiguration configuration = null) return new HandlebarsEnvironment(configuration); } + + /// + /// Creates standalone instance of environment + /// + /// + /// + internal static IHandlebars Create(ICompiledHandlebarsConfiguration configuration) + { + configuration ??= new HandlebarsConfigurationAdapter(new HandlebarsConfiguration()); + return new HandlebarsEnvironment(configuration); + } + + /// + /// + /// + /// + /// public static Action Compile(TextReader template) { return Instance.Compile(template); @@ -105,10 +133,51 @@ public static void RegisterHelper(string helperName, HandlebarsBlockHelper helpe { Instance.RegisterHelper(helperName, helperFunction); } + + /// + /// Registers new + /// + /// + /// + public static void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction) + { + Instance.RegisterHelper(helperName, helperFunction); + } + + /// + /// Registers new + /// + /// + public static void RegisterHelper(HelperDescriptorBase helperObject) + { + Instance.RegisterHelper((HelperDescriptor) helperObject); + } + + /// + /// Registers new + /// + /// + public static void RegisterHelper(BlockHelperDescriptorBase helperObject) + { + Instance.RegisterHelper((BlockHelperDescriptor) helperObject); + } /// /// Expose the configuration in order to have access in all Helpers and Templates. /// public static HandlebarsConfiguration Configuration => Instance.Configuration; + + /// + /// Allows to perform cleanup of internal static buffers + /// + public static void Cleanup() + { + while (Disposables.TryDequeue(out var disposable)) + { + disposable.Dispose(); + } + } + + internal static readonly ConcurrentQueue Disposables = new ConcurrentQueue(); } } \ No newline at end of file diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index a0506f97..488449b8 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -7,7 +7,7 @@ netstandard1.3;netstandard2.0 $(TargetFrameworks);net451;net452 2.0.0 - 8 + HandlebarsDotNet @@ -41,18 +41,15 @@ - - - diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 75c31df3..4ac954cc 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -1,13 +1,14 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet { public delegate TextReader ViewReaderFactory(ICompiledHandlebarsConfiguration configuration, string templatePath); - internal class HandlebarsEnvironment : IHandlebars + internal class HandlebarsEnvironment : IHandlebars, ICompiledHandlebars { private static readonly ViewReaderFactory ViewReaderFactory = (configuration, path) => { @@ -25,10 +26,19 @@ public HandlebarsEnvironment(HandlebarsConfiguration configuration) { Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } + + internal HandlebarsEnvironment(ICompiledHandlebarsConfiguration configuration) + { + CompiledConfiguration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public HandlebarsConfiguration Configuration { get; } + internal ICompiledHandlebarsConfiguration CompiledConfiguration { get; } + ICompiledHandlebarsConfiguration ICompiledHandlebars.CompiledConfiguration => CompiledConfiguration; public Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) { - readerFactoryFactory = readerFactoryFactory ?? ViewReaderFactory; + readerFactoryFactory ??= ViewReaderFactory; return CompileViewInternal(templatePath, readerFactoryFactory); } @@ -37,25 +47,23 @@ public Func CompileView(string templatePath) var view = CompileViewInternal(templatePath, ViewReaderFactory); return (vm) => { - using (var writer = new PolledStringWriter(Configuration.FormatProvider)) - { - view(writer, vm); - return writer.ToString(); - } + var formatProvider = Configuration?.FormatProvider ?? CompiledConfiguration.FormatProvider; + using var writer = ReusableStringWriter.Get(formatProvider); + view(writer, vm); + return writer.ToString(); }; } private Action CompileViewInternal(string templatePath, ViewReaderFactory readerFactoryFactory) { - var configuration = new InternalHandlebarsConfiguration(Configuration); + var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration); var createdFeatures = configuration.Features; for (var index = 0; index < createdFeatures.Count; index++) { createdFeatures[index].OnCompiling(configuration); } - - var compiler = new HandlebarsCompiler(configuration); - var compiledView = compiler.CompileView(readerFactoryFactory, templatePath, configuration); + + var compiledView = HandlebarsCompiler.CompileView(readerFactoryFactory, templatePath, configuration); for (var index = 0; index < createdFeatures.Count; index++) { @@ -65,15 +73,11 @@ private Action CompileViewInternal(string templatePath, View return compiledView; } - public HandlebarsConfiguration Configuration { get; } - public Action Compile(TextReader template) { - using (var reader = new ExtendedStringReader(template)) - { - var compiler = new HandlebarsCompiler(Configuration); - return compiler.Compile(reader); - } + var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration); + using var reader = new ExtendedStringReader(template); + return HandlebarsCompiler.Compile(reader, configuration); } public Func Compile(string template) @@ -83,41 +87,64 @@ public Func Compile(string template) var compiledTemplate = Compile(reader); return context => { - using (var writer = new PolledStringWriter(Configuration.FormatProvider)) - { - compiledTemplate(writer, context); - return writer.ToString(); - } + var formatProvider = Configuration?.FormatProvider ?? CompiledConfiguration?.FormatProvider; + using var writer = ReusableStringWriter.Get(formatProvider); + compiledTemplate(writer, context); + return writer.ToString(); }; } } public void RegisterTemplate(string templateName, Action template) { - Configuration.RegisteredTemplates[templateName] = template; + var registrations = Configuration ?? (IHandlebarsTemplateRegistrations) CompiledConfiguration; + registrations.RegisteredTemplates[templateName] = template; } public void RegisterTemplate(string templateName, string template) { - using (var reader = new StringReader(template)) - { - RegisterTemplate(templateName, Compile(reader)); - } + using var reader = new StringReader(template); + RegisterTemplate(templateName,Compile(reader)); } public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) { - Configuration.Helpers[helperName] = helperFunction; + Configuration.Helpers[helperName] = new DelegateHelperDescriptor(helperName, helperFunction); } public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) { - Configuration.ReturnHelpers[helperName] = helperFunction; + Configuration.Helpers[helperName] = new DelegateReturnHelperDescriptor(helperName, helperFunction); } public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { - Configuration.BlockHelpers[helperName] = helperFunction; + Configuration.BlockHelpers[helperName] = new DelegateBlockHelperDescriptor(helperName, helperFunction); + } + + public void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction) + { + Configuration.BlockHelpers[helperName] = new DelegateReturnBlockHelperDescriptor(helperName, helperFunction); + } + + public void RegisterHelper(BlockHelperDescriptor helperObject) + { + Configuration.BlockHelpers[helperObject.Name] = helperObject; + } + + public void RegisterHelper(ReturnBlockHelperDescriptor helperObject) + { + Configuration.BlockHelpers[helperObject.Name] = helperObject; + } + + public void RegisterHelper(HelperDescriptor helperObject) + { + Configuration.Helpers[helperObject.Name] = helperObject; + } + + public void RegisterHelper(ReturnHelperDescriptor helperObject) + { + Configuration.Helpers[helperObject.Name] = helperObject; } } } diff --git a/source/Handlebars/HandlebarsExtensions.cs b/source/Handlebars/HandlebarsExtensions.cs index cb3f2a44..aff3f466 100644 --- a/source/Handlebars/HandlebarsExtensions.cs +++ b/source/Handlebars/HandlebarsExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; namespace HandlebarsDotNet @@ -24,7 +23,13 @@ public static void WriteSafeString(this TextWriter writer, string value) /// public static void WriteSafeString(this TextWriter writer, object value) { - writer.WriteSafeString(value.ToString()); + if (value is string str) + { + writer.Write(new SafeString(str)); + return; + } + + writer.Write(new SafeString(value.ToString())); } /// @@ -40,25 +45,27 @@ public static HandlebarsConfiguration Configure(this HandlebarsConfiguration con return configuration; } - private class SafeString : ISafeString + public static object This(this HelperOptions options, object context, Func> selector) { - private readonly string _value; - - public SafeString(string value) - { - _value = value; - } - - public override string ToString() - { - return _value; - } + using var writer = ReusableStringWriter.Get(options.Configuration.FormatProvider); + selector(options)(writer, context); + return writer.ToString(); } } public interface ISafeString { + string Value { get; } + } + + public class SafeString : ISafeString + { + public string Value { get; } + + public SafeString(string value) => Value = value; + + public override string ToString() => Value; } } diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index fa10326e..9cf07c3c 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,41 +1,44 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet { - - public delegate void BlockParamsConfiguration(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies); - /// /// Contains properties accessible withing function /// - public sealed class HelperOptions : IReadOnlyDictionary + public sealed class HelperOptions : IDisposable { - private readonly Dictionary _extensions; + private static readonly InternalObjectPool Pool = new InternalObjectPool(new Policy()); - internal HelperOptions( - Action template, - Action inverse, - BlockParamsValueProvider blockParamsValueProvider, - InternalHandlebarsConfiguration configuration, + private readonly Dictionary _extensions; + + internal static HelperOptions Create(Action template, + Action inverse, + ChainSegment[] blockParamsValues, BindingContext bindingContext) { - Template = template; - Inverse = inverse; - BlockParams = blockParamsValueProvider.Configure; - - BindingContext = bindingContext; - Configuration = configuration; + var item = Pool.Get(); + + item.OriginalTemplate = template; + item.OriginalInverse = inverse; - _extensions = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - [nameof(Template)] = Template, - [nameof(Inverse)] = Inverse, - [nameof(BlockParams)] = BlockParams - }; + item.BindingContext = bindingContext; + item.Configuration = bindingContext.Configuration; + item.BlockParams = blockParamsValues; + item.Data = new DataValues(bindingContext); + + return item; + } + + private HelperOptions() + { + _extensions = new Dictionary(7); + Template = (writer, o) => OriginalTemplate(BindingContext, writer, o); + Inverse = (writer, o) => OriginalInverse(BindingContext, writer, o); } /// @@ -48,26 +51,22 @@ internal HelperOptions( /// public Action Inverse { get; } - /// - public BlockParamsConfiguration BlockParams { get; } - - /// - internal InternalHandlebarsConfiguration Configuration { get; } - - internal BindingContext BindingContext { get; } + public ChainSegment[] BlockParams { get; private set; } - bool IReadOnlyDictionary.ContainsKey(string key) - { - return _extensions.ContainsKey(key); - } + public DataValues Data { get; private set; } - bool IReadOnlyDictionary.TryGetValue(string key, out object value) + internal ICompiledHandlebarsConfiguration Configuration { get; private set; } + internal BindingContext BindingContext { get; private set; } + internal Action OriginalTemplate { get; private set; } + internal Action OriginalInverse { get; private set; } + + public BindingContext CreateFrame(object value = null) { - return _extensions.TryGetValue(key, out value); + return BindingContext.CreateFrame(value); } - + /// - /// Provides access to dynamic data entries + /// Provides access to dynamic options /// /// public object this[string property] @@ -82,26 +81,27 @@ public object this[string property] /// /// /// - public T GetValue(string property) - { - return (T) this[property]; - } - - IEnumerable IReadOnlyDictionary.Keys => _extensions.Keys; + public T GetValue(string property) => (T) this[property]; - IEnumerable IReadOnlyDictionary.Values => _extensions.Values; + public void Dispose() => Pool.Return(this); - IEnumerator> IEnumerable>.GetEnumerator() + private class Policy : IInternalObjectPoolPolicy { - return _extensions.GetEnumerator(); - } + public HelperOptions Create() => new HelperOptions(); - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable) _extensions).GetEnumerator(); - } + public bool Return(HelperOptions item) + { + item._extensions.Clear(); + + item.Configuration = null; + item.BindingContext = null; + item.BlockParams = null; + item.OriginalInverse = null; + item.OriginalTemplate = null; - int IReadOnlyCollection>.Count => _extensions.Count; + return true; + } + } } } diff --git a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs new file mode 100644 index 00000000..2294c786 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs @@ -0,0 +1,31 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + /// + /// + /// + public abstract class BlockHelperDescriptor : BlockHelperDescriptorBase + { + /// + /// + /// + /// + protected BlockHelperDescriptor(string name) : base(name) + { + } + + /// + /// + /// + /// + protected BlockHelperDescriptor(PathInfo name) : base(name) + { + } + + /// + /// + /// + public sealed override HelperType Type { get; } = HelperType.WriteBlock; + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs new file mode 100644 index 00000000..52b4c538 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs @@ -0,0 +1,18 @@ +using System.IO; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + public abstract class BlockHelperDescriptorBase : IHelperDescriptor + { + protected BlockHelperDescriptorBase(string name) => Name = PathResolver.GetPathInfo(name); + + protected BlockHelperDescriptorBase(PathInfo name) => Name = name; + + public PathInfo Name { get; } + + public abstract HelperType Type { get; } + + public abstract void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments); + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs new file mode 100644 index 00000000..556c690c --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs @@ -0,0 +1,45 @@ +using System.IO; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + /// + /// + /// + public sealed class DelegateBlockHelperDescriptor : BlockHelperDescriptor + { + private readonly HandlebarsBlockHelper _helper; + + /// + /// + /// + /// + /// + public DelegateBlockHelperDescriptor(string name, HandlebarsBlockHelper helper) : base(name) + { + _helper = helper; + } + + /// + /// + /// + /// + /// + public DelegateBlockHelperDescriptor(PathInfo name, HandlebarsBlockHelper helper) : base(name) + { + _helper = helper; + } + + /// + /// + /// + /// + /// + /// + /// + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + { + _helper(output, options, context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs new file mode 100644 index 00000000..67ede7b0 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs @@ -0,0 +1,24 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + public sealed class DelegateReturnBlockHelperDescriptor : ReturnBlockHelperDescriptor + { + private readonly HandlebarsReturnBlockHelper _helper; + + public DelegateReturnBlockHelperDescriptor(string name, HandlebarsReturnBlockHelper helper) : base(name) + { + _helper = helper; + } + + public DelegateReturnBlockHelperDescriptor(PathInfo name, HandlebarsReturnBlockHelper helper) : base(name) + { + _helper = helper; + } + + public override object Invoke(HelperOptions options, object context, params object[] arguments) + { + return _helper(options, context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs new file mode 100644 index 00000000..9d134134 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs @@ -0,0 +1,38 @@ +using System.IO; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + internal sealed class InlineBlockHelperDescriptor : BlockHelperDescriptor + { + public InlineBlockHelperDescriptor() : base("*inline") + { + } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] 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 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.Add(key, (writer, o) => template(bindingContext, writer, o)); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs new file mode 100644 index 00000000..33dea97e --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + internal sealed class LateBindBlockHelperDescriptor : BlockHelperDescriptor + { + private readonly ICompiledHandlebarsConfiguration _configuration; + private readonly StrongBox _blockHelperMissing; + + public LateBindBlockHelperDescriptor(string name, ICompiledHandlebarsConfiguration configuration) : base(configuration.PathInfoStore.GetOrAdd(name)) + { + _configuration = configuration; + var pathInfoStore = _configuration.PathInfoStore; + _blockHelperMissing = _configuration.BlockHelpers[pathInfoStore.GetOrAdd("blockHelperMissing")]; + } + + public LateBindBlockHelperDescriptor(PathInfo name, ICompiledHandlebarsConfiguration configuration) : base(name) + { + _configuration = configuration; + var pathInfoStore = _configuration.PathInfoStore; + _blockHelperMissing = _configuration.BlockHelpers[pathInfoStore.GetOrAdd("blockHelperMissing")]; + } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + { + var helperResolvers = (ObservableList)_configuration.HelperResolvers; + if(helperResolvers.Count != 0) + { + for (var index = 0; index < helperResolvers.Count; index++) + { + if (!helperResolvers[index].TryResolveBlockHelper(Name, out var descriptor)) continue; + + descriptor.Invoke(output, options, context, arguments); + return; + } + } + + options["name"] = Name.TrimmedPath; + options["path"] = Name; + _blockHelperMissing.Value.Invoke(output, options, context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs new file mode 100644 index 00000000..2f4f3781 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs @@ -0,0 +1,23 @@ +using System.IO; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + internal sealed class MissingBlockHelperDescriptor : BlockHelperDescriptor + { + public MissingBlockHelperDescriptor() : base("missingBlockHelper") + { + } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + { + var pathInfo = options.GetValue("path"); + if(arguments.Length > 0) throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. BlockHelper '{pathInfo}'"); + + var bindingContext = options.BindingContext; + var value = PathResolver.ResolvePath(bindingContext, pathInfo); + DeferredSectionBlockHelper.PlainHelper(bindingContext, value, options.OriginalTemplate, options.OriginalInverse); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs new file mode 100644 index 00000000..86d87c48 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs @@ -0,0 +1,23 @@ +using System.IO; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + public abstract class ReturnBlockHelperDescriptor : BlockHelperDescriptorBase + { + protected ReturnBlockHelperDescriptor(string name) : base(name) + { + } + + protected ReturnBlockHelperDescriptor(PathInfo name) : base(name) + { + } + + public sealed override HelperType Type { get; } = HelperType.ReturnBlock; + + public abstract object Invoke(HelperOptions options, object context, params object[] arguments); + + public sealed override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) => + output.Write(Invoke(options, context, arguments)); + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs new file mode 100644 index 00000000..34e901d7 --- /dev/null +++ b/source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs @@ -0,0 +1,36 @@ +using System.IO; +using HandlebarsDotNet.ValueProviders; + +namespace HandlebarsDotNet.Helpers.BlockHelpers +{ + internal sealed class WithBlockHelperDescriptor : BlockHelperDescriptor + { + public WithBlockHelperDescriptor() : base("with") + { + } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{with}} helper must have exactly one argument"); + } + + if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) + { + using var frame = options.CreateFrame(arguments[0]); + var blockParamsValues = new BlockParamsValues(frame, options.BlockParams); + + blockParamsValues.CreateProperty(0, out var _0); + + blockParamsValues[_0] = arguments[0]; + + options.Template(output, frame); + } + else + { + options.Inverse(output, context); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/DelegateHelperDescriptor.cs b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs new file mode 100644 index 00000000..6e9f726b --- /dev/null +++ b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs @@ -0,0 +1,25 @@ +using System.IO; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + public sealed class DelegateHelperDescriptor : HelperDescriptor + { + private readonly HandlebarsHelper _helper; + + public DelegateHelperDescriptor(PathInfo name, HandlebarsHelper helper) : base(name) + { + _helper = helper; + } + + public DelegateHelperDescriptor(string name, HandlebarsHelper helper) : base(name) + { + _helper = helper; + } + + public override void Invoke(TextWriter output, object context, params object[] arguments) + { + _helper(output, context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs b/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs new file mode 100644 index 00000000..2f3be1f0 --- /dev/null +++ b/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs @@ -0,0 +1,24 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + public sealed class DelegateReturnHelperDescriptor : ReturnHelperDescriptor + { + private readonly HandlebarsReturnHelper _helper; + + public DelegateReturnHelperDescriptor(PathInfo name, HandlebarsReturnHelper helper) : base(name) + { + _helper = helper; + } + + public DelegateReturnHelperDescriptor(string name, HandlebarsReturnHelper helper) : base(name) + { + _helper = helper; + } + + public override object Invoke(object context, params object[] arguments) + { + return _helper(context, arguments); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/HelperDescriptor.cs b/source/Handlebars/Helpers/HelperDescriptor.cs new file mode 100644 index 00000000..5d54662e --- /dev/null +++ b/source/Handlebars/Helpers/HelperDescriptor.cs @@ -0,0 +1,30 @@ +using System.IO; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + public abstract class HelperDescriptor : HelperDescriptorBase + { + protected HelperDescriptor(string name) : base(name) + { + } + + protected HelperDescriptor(PathInfo name) : base(name) + { + } + + public sealed override HelperType Type { get; } = HelperType.Write; + + public abstract void Invoke(TextWriter output, object context, params object[] arguments); + + internal sealed override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + { + using var writer = ReusableStringWriter.Get(bindingContext.Configuration.FormatProvider); + WriteInvoke(bindingContext, writer, context, arguments); + return writer.ToString(); + } + + internal sealed override void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments) => Invoke(output, context, arguments); + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/HelperDescriptorBase.cs b/source/Handlebars/Helpers/HelperDescriptorBase.cs new file mode 100644 index 00000000..ff75bf8c --- /dev/null +++ b/source/Handlebars/Helpers/HelperDescriptorBase.cs @@ -0,0 +1,37 @@ +using System.IO; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + /// + /// + /// + public abstract class HelperDescriptorBase : IHelperDescriptor + { + /// + /// + /// + /// + protected HelperDescriptorBase(string name) => Name = PathResolver.GetPathInfo(name); + + /// + /// + /// + /// + protected HelperDescriptorBase(PathInfo name) => Name = name; + + public PathInfo Name { get; } + public abstract HelperType Type { get; } + + internal abstract object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments); + + internal abstract void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments); + + /// + /// Returns helper name + /// + /// + public override string ToString() => Name; + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/HelperType.cs b/source/Handlebars/Helpers/HelperType.cs new file mode 100644 index 00000000..f2268841 --- /dev/null +++ b/source/Handlebars/Helpers/HelperType.cs @@ -0,0 +1,10 @@ +namespace HandlebarsDotNet.Helpers +{ + public enum HelperType + { + Write, + Return, + WriteBlock, + ReturnBlock + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/IHelperDescriptor.cs b/source/Handlebars/Helpers/IHelperDescriptor.cs new file mode 100644 index 00000000..27db7c05 --- /dev/null +++ b/source/Handlebars/Helpers/IHelperDescriptor.cs @@ -0,0 +1,11 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + public interface IHelperDescriptor + { + PathInfo Name { get; } + + HelperType Type { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/IHelperResolver.cs b/source/Handlebars/Helpers/IHelperResolver.cs index 57388af4..503d5fdc 100644 --- a/source/Handlebars/Helpers/IHelperResolver.cs +++ b/source/Handlebars/Helpers/IHelperResolver.cs @@ -1,4 +1,5 @@ using System; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet.Helpers { @@ -14,14 +15,14 @@ public interface IHelperResolver /// /// /// - bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper); - + bool TryResolveHelper(string name, Type targetType, out HelperDescriptorBase helper); + /// /// Resolves /// /// /// /// - bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper); + bool TryResolveBlockHelper(string name, out BlockHelperDescriptorBase helper); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/LateBindHelperDescriptor.cs b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs new file mode 100644 index 00000000..8aa4f2c3 --- /dev/null +++ b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + internal sealed class LateBindHelperDescriptor : ReturnHelperDescriptor + { + private readonly ICompiledHandlebarsConfiguration _configuration; + private readonly StrongBox _helperMissing; + + public LateBindHelperDescriptor(PathInfo name, ICompiledHandlebarsConfiguration configuration) : base(name) + { + _configuration = configuration; + var pathInfoStore = _configuration.PathInfoStore; + _helperMissing = _configuration.Helpers[pathInfoStore.GetOrAdd("helperMissing")]; + } + + public LateBindHelperDescriptor(string name, ICompiledHandlebarsConfiguration configuration) : base(configuration.PathInfoStore.GetOrAdd(name)) + { + _configuration = configuration; + var pathInfoStore = _configuration.PathInfoStore; + _helperMissing = _configuration.Helpers[pathInfoStore.GetOrAdd("helperMissing")]; + } + + internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + { + var helperResolvers = (ObservableList) _configuration.HelperResolvers; + if(helperResolvers.Count != 0) + { + var targetType = arguments.FirstOrDefault()?.GetType(); + for (var index = 0; index < helperResolvers.Count; index++) + { + var resolver = helperResolvers[index]; + if (!resolver.TryResolveHelper(Name, targetType, out var helper)) continue; + + return helper.ReturnInvoke(bindingContext, context, arguments); + } + } + + var value = PathResolver.ResolvePath(bindingContext, Name); + if (!(value is UndefinedBindingResult)) return value; + + var nameIndex = arguments.Length; + Array.Resize(ref arguments, nameIndex + 1); + arguments[nameIndex] = Name.TrimmedPath; + return _helperMissing.Value.ReturnInvoke(bindingContext, context, arguments); + } + + public override object Invoke(object context, params object[] arguments) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs new file mode 100644 index 00000000..5abe544d --- /dev/null +++ b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs @@ -0,0 +1,35 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet.Helpers +{ + internal sealed class LookupReturnHelperDescriptor : ReturnHelperDescriptor + { + private readonly ICompiledHandlebarsConfiguration _configuration; + + public LookupReturnHelperDescriptor(ICompiledHandlebarsConfiguration configuration) : base(configuration.PathInfoStore.GetOrAdd("lookup")) + { + _configuration = configuration; + } + + public override object Invoke(object context, params object[] arguments) + { + if (arguments.Length != 2) + { + throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); + } + + var segment = ChainSegment.Create(arguments[1]); + + if (ObjectDescriptor.TryCreate(arguments[0], _configuration, out var descriptor)) + { + return !PathResolver.TryAccessMember(arguments[0], segment, _configuration, out var value) + ? segment.GetUndefinedBindingResult(_configuration) + : value; + } + + return segment.GetUndefinedBindingResult(_configuration); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/MissingHelperDescriptor.cs b/source/Handlebars/Helpers/MissingHelperDescriptor.cs new file mode 100644 index 00000000..fdbf5d57 --- /dev/null +++ b/source/Handlebars/Helpers/MissingHelperDescriptor.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Helpers +{ + internal sealed class MissingHelperDescriptor : ReturnHelperDescriptor + { + public MissingHelperDescriptor() : base("helperMissing") + { + } + + internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + { + var nameArgument = arguments.Last(); + if (arguments.Length > 1) + { + throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. Helper '{nameArgument}'"); + } + + var name = bindingContext.Configuration.PathInfoStore.GetOrAdd(nameArgument as string ?? nameArgument.ToString()); + return name.GetUndefinedBindingResult(bindingContext.Configuration); + } + + public override object Invoke(object context, params object[] arguments) => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/source/Handlebars/Helpers/ReturnHelperDescriptor.cs b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs new file mode 100644 index 00000000..0d852633 --- /dev/null +++ b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs @@ -0,0 +1,44 @@ +using System.IO; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Helpers +{ + public abstract class ReturnHelperDescriptor : HelperDescriptorBase + { + /// + /// + /// + /// + protected ReturnHelperDescriptor(PathInfo name) : base(name) + { + } + + /// + /// + /// + /// + protected ReturnHelperDescriptor(string name) : base(name) + { + } + + /// + /// + /// + public sealed override HelperType Type { get; } = HelperType.Return; + + /// + /// + /// + /// + /// + /// + public abstract object Invoke(object context, params object[] arguments); + + internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) => + Invoke(context, arguments); + + internal sealed override void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments) => + output.Write(ReturnInvoke(bindingContext, context, arguments)); + } +} \ No newline at end of file diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 7200bb85..82f690ed 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet { @@ -32,6 +34,21 @@ public interface IHandlebars void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction); void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction); + + void RegisterHelper(string helperName, HandlebarsReturnBlockHelper helperFunction); + + void RegisterHelper(BlockHelperDescriptor helperObject); + + void RegisterHelper(HelperDescriptor helperObject); + + void RegisterHelper(ReturnBlockHelperDescriptor helperObject); + + void RegisterHelper(ReturnHelperDescriptor helperObject); + } + + internal interface ICompiledHandlebars + { + ICompiledHandlebarsConfiguration CompiledConfiguration { get; } } } diff --git a/source/Handlebars/IMissingPartialTemplateHandler.cs b/source/Handlebars/IMissingPartialTemplateHandler.cs index 338f69eb..60782036 100644 --- a/source/Handlebars/IMissingPartialTemplateHandler.cs +++ b/source/Handlebars/IMissingPartialTemplateHandler.cs @@ -14,6 +14,6 @@ public interface IMissingPartialTemplateHandler /// The current environment configuration. /// The name of the partial that was not found. /// The output writer. - void Handle(HandlebarsConfiguration configuration, string partialName, TextWriter textWriter); + void Handle(ICompiledHandlebarsConfiguration configuration, string partialName, TextWriter textWriter); } } diff --git a/source/Handlebars/IO/EncodedTextWriter.cs b/source/Handlebars/IO/EncodedTextWriter.cs index 52322967..57267535 100644 --- a/source/Handlebars/IO/EncodedTextWriter.cs +++ b/source/Handlebars/IO/EncodedTextWriter.cs @@ -3,23 +3,27 @@ namespace HandlebarsDotNet { - internal class EncodedTextWriter : TextWriter + internal sealed class EncodedTextWriter : TextWriter { - private readonly ITextEncoder _encoder; + private static readonly EncodedTextWriterPool Pool = new EncodedTextWriterPool(); + + private ITextEncoder _encoder; public bool SuppressEncoding { get; set; } - private EncodedTextWriter(TextWriter writer, ITextEncoder encoder) + private EncodedTextWriter() { - UnderlyingWriter = writer; - _encoder = encoder; } public static EncodedTextWriter From(TextWriter writer, ITextEncoder encoder) { - var encodedTextWriter = writer as EncodedTextWriter; + if (writer is EncodedTextWriter encodedTextWriter) return encodedTextWriter; + + var textWriter = Pool.Get(); + textWriter._encoder = encoder; + textWriter.UnderlyingWriter = writer; - return encodedTextWriter ?? new EncodedTextWriter(writer, encoder); + return textWriter; } public void Write(string value, bool encode) @@ -49,12 +53,48 @@ public override void Write(object value) return; } - var encode = !(value is ISafeString); - Write(value.ToString(), encode); + if (value is ISafeString safeString) + { + Write(safeString.Value, false); + return; + } + + var @string = value as string ?? value.ToString(); + if(string.IsNullOrEmpty(@string)) return; + + Write(@string, true); } - public TextWriter UnderlyingWriter { get; } + public TextWriter UnderlyingWriter { get; private set; } + + protected override void Dispose(bool disposing) + { + Pool.Return(this); + } public override Encoding Encoding => UnderlyingWriter.Encoding; + + private class EncodedTextWriterPool : InternalObjectPool + { + public EncodedTextWriterPool() : base(new Policy()) + { + } + + private class Policy : IInternalObjectPoolPolicy + { + public EncodedTextWriter Create() + { + return new EncodedTextWriter(); + } + + public bool Return(EncodedTextWriter obj) + { + obj._encoder = null; + obj.UnderlyingWriter = null; + + return true; + } + } + } } } \ No newline at end of file diff --git a/source/Handlebars/IO/PolledStringWriter.cs b/source/Handlebars/IO/PolledStringWriter.cs index 7f34fc1b..21bda2a1 100644 --- a/source/Handlebars/IO/PolledStringWriter.cs +++ b/source/Handlebars/IO/PolledStringWriter.cs @@ -1,23 +1,47 @@ using System; +using System.Globalization; using System.IO; namespace HandlebarsDotNet { - internal class PolledStringWriter : StringWriter + internal class ReusableStringWriter : StringWriter { - public PolledStringWriter() : base(StringBuilderPool.Shared.Get()) + private static readonly InternalObjectPool Pool = new InternalObjectPool(new Policy()); + + private IFormatProvider _formatProvider; + + public static ReusableStringWriter Get(IFormatProvider formatProvider = null) { - + var writer = Pool.Get(); + writer._formatProvider = formatProvider ?? CultureInfo.CurrentCulture; + return writer; } - public PolledStringWriter(IFormatProvider formatProvider) : base(StringBuilderPool.Shared.Get(), formatProvider) + private ReusableStringWriter() { } + public override IFormatProvider FormatProvider => _formatProvider; + protected override void Dispose(bool disposing) { - StringBuilderPool.Shared.Return(base.GetStringBuilder()); - base.Dispose(disposing); + _formatProvider = null; + Pool.Return(this); + } + + private class Policy : IInternalObjectPoolPolicy + { + private readonly StringBuilderPool.StringBuilderPooledObjectPolicy _policy = new StringBuilderPool.StringBuilderPooledObjectPolicy(); + + public ReusableStringWriter Create() + { + return new ReusableStringWriter(); + } + + public bool Return(ReusableStringWriter item) + { + return _policy.Return(item.GetStringBuilder()); + } } } } \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs deleted file mode 100644 index a48d9032..00000000 --- a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Compiler.Structure.Path; - -namespace HandlebarsDotNet.MemberAccessors -{ - internal class ContextMemberAccessor : IMemberAccessor - { - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) - { - var bindingContext = (BindingContext) instance; - var segment = new ChainSegment(memberName); - return bindingContext.TryGetContextVariable(ref segment, out value); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericClassDictionaryAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericClassDictionaryAccessor.cs new file mode 100644 index 00000000..d89782e7 --- /dev/null +++ b/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericClassDictionaryAccessor.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors.DictionaryAccessors +{ + internal class GenericClassDictionaryAccessor : IMemberAccessor + where T: IDictionary + where TV: class + { + private static readonly TypeConverter TypeConverter = TypeDescriptor.GetConverter(typeof(TK)); + + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + var key = (TK) TypeConverter.ConvertFromString(memberName.TrimmedValue); + var dictionary = (T) instance; + if (key != null && dictionary.TryGetValue(key, out var v)) + { + value = v; + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericStructDictionaryAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericStructDictionaryAccessor.cs new file mode 100644 index 00000000..b2f5f1d0 --- /dev/null +++ b/source/Handlebars/MemberAccessors/DictionaryAccessors/GenericStructDictionaryAccessor.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors.DictionaryAccessors +{ + internal class GenericStructDictionaryAccessor : IMemberAccessor + where T: IDictionary + where TV: struct + { + private static readonly object BoxedDefault = default(TV); + private static readonly TypeConverter TypeConverter = TypeDescriptor.GetConverter(typeof(TK)); + + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + var key = (TK) TypeConverter.ConvertFromString(memberName.TrimmedValue); + var dictionary = (T) instance; + if (key != null && dictionary.TryGetValue(key, out var v)) + { + value = v; + return true; + } + + value = BoxedDefault; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryAccessors/StringClassDictionaryAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryAccessors/StringClassDictionaryAccessor.cs new file mode 100644 index 00000000..a92f37d0 --- /dev/null +++ b/source/Handlebars/MemberAccessors/DictionaryAccessors/StringClassDictionaryAccessor.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors.DictionaryAccessors +{ + internal sealed class StringClassDictionaryAccessor : IMemberAccessor + where T: IDictionary + where TV: class + { + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + var dictionary = (T) instance; + if (dictionary.TryGetValue(memberName.TrimmedValue, out var v)) + { + value = v; + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryAccessors/StringStructDictionaryAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryAccessors/StringStructDictionaryAccessor.cs new file mode 100644 index 00000000..c868cabb --- /dev/null +++ b/source/Handlebars/MemberAccessors/DictionaryAccessors/StringStructDictionaryAccessor.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors.DictionaryAccessors +{ + internal sealed class StringStructDictionaryAccessor : IMemberAccessor + where T: IDictionary + where TV: struct + { + private static readonly object BoxedDefault = default(TV); + + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + var dictionary = (T) instance; + if (dictionary.TryGetValue(memberName.TrimmedValue, out var v)) + { + value = v; + return true; + } + + value = BoxedDefault; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs index 1edd3503..b4808638 100644 --- a/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs @@ -1,18 +1,19 @@ using System; using System.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.MemberAccessors { internal class DictionaryMemberAccessor : IMemberAccessor { - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + public bool TryGetValue(object instance, ChainSegment memberName, out object value) { value = null; // Check if the instance is IDictionary (ie, System.Collections.Hashtable) // Only string keys supported - indexer takes an object, but no nice // way to check if the hashtable check if it should be a different type. var dictionary = (IDictionary) instance; - value = dictionary[memberName]; + value = dictionary[memberName.LowerInvariant]; return true; } } diff --git a/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs b/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs index 148e1f1c..b843d275 100644 --- a/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs @@ -1,11 +1,12 @@ using System; using System.Dynamic; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.MemberAccessors { internal class DynamicMemberAccessor : IMemberAccessor { - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + public bool TryGetValue(object instance, ChainSegment memberName, out object value) { value = null; //crude handling for dynamic objects that don't have metadata @@ -13,7 +14,7 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o try { - value = GetProperty(metaObjectProvider, memberName); + value = GetProperty(metaObjectProvider, memberName.TrimmedValue); return value != null; } catch diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/ClassEnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/ClassEnumerableMemberAccessor.cs new file mode 100644 index 00000000..c520540b --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/ClassEnumerableMemberAccessor.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class ClassEnumerableMemberAccessor : EnumerableMemberAccessor + where T: class, IEnumerable + where TV: class + { + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var list = (T) instance; + value = list.ElementAtOrDefault(index); + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs new file mode 100644 index 00000000..5b83a530 --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal class EnumerableMemberAccessor : IMemberAccessor + { + public static EnumerableMemberAccessor Create(Type type) + { + if (type.IsAssignableToGenericType(typeof(IList<>), out var genericType)) + { + var typeArgument = genericType.GenericTypeArguments[0]; + var accessorType = typeArgument.GetTypeInfo().IsClass + ? typeof(ListMemberAccessor<,>).MakeGenericType(genericType, typeArgument) + : typeof(StructListMemberAccessor<,>).MakeGenericType(genericType, typeArgument); + + return (EnumerableMemberAccessor) Activator.CreateInstance(accessorType); + } + + if (type.IsAssignableToGenericType(typeof(IReadOnlyList<>), out genericType)) + { + var typeArgument = genericType.GenericTypeArguments[0]; + var accessorType = typeArgument.GetTypeInfo().IsClass + ? typeof(ReadOnlyListMemberAccessor<,>).MakeGenericType(genericType, typeArgument) + : typeof(StructReadOnlyListMemberAccessor<,>).MakeGenericType(genericType, typeArgument); + + return (EnumerableMemberAccessor) Activator.CreateInstance(accessorType); + } + + if (type.IsAssignableToGenericType(typeof(IEnumerable<>), out genericType)) + { + var typeArgument = genericType.GenericTypeArguments[0]; + var accessorType = typeArgument.GetTypeInfo().IsClass + ? typeof(ClassEnumerableMemberAccessor<,>).MakeGenericType(genericType, typeArgument) + : typeof(StructEnumerableMemberAccessor<,>).MakeGenericType(genericType, typeArgument); + + return (EnumerableMemberAccessor) Activator.CreateInstance(accessorType); + } + + return new EnumerableMemberAccessor(); + } + + protected EnumerableMemberAccessor() { } + + public virtual bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + if (int.TryParse(memberName.LowerInvariant, out var index)) + return TryGetValueInternal(instance, index, out value); + + value = null; + return false; + } + + protected virtual bool TryGetValueInternal(object instance, int index, out object value) + { + switch (instance) + { + case IList list: + value = list[index]; + return true; + + case IEnumerable enumerable: + value = enumerable.Cast().ElementAtOrDefault(index); + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/ListMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/ListMemberAccessor.cs new file mode 100644 index 00000000..ba0de77c --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/ListMemberAccessor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class ListMemberAccessor : EnumerableMemberAccessor + where T: IList + where TV: class + { + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var list = (T) instance; + value = list[index]; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/ReadOnlyListMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/ReadOnlyListMemberAccessor.cs new file mode 100644 index 00000000..66748831 --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/ReadOnlyListMemberAccessor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class ReadOnlyListMemberAccessor : EnumerableMemberAccessor + where T: IReadOnlyList + where TV: class + { + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var list = (T) instance; + value = list[index]; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/StructEnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructEnumerableMemberAccessor.cs new file mode 100644 index 00000000..baf6028a --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructEnumerableMemberAccessor.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class StructEnumerableMemberAccessor : EnumerableMemberAccessor + where T: class, IEnumerable + where TV: struct + { + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var enumerable = (T) instance; + value = enumerable.ElementAtOrDefault(index); + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/StructListMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructListMemberAccessor.cs new file mode 100644 index 00000000..1c3b6a9c --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructListMemberAccessor.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class StructListMemberAccessor : EnumerableMemberAccessor + where T: IList + where TV: struct + { + private static readonly object BoxedDefault = default(TV); + + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var list = (T) instance; + if (index >= list.Count) + { + value = BoxedDefault; + return false; + } + + value = list[index]; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/StructReadOnlyListMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructReadOnlyListMemberAccessor.cs new file mode 100644 index 00000000..1fdf5050 --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/StructReadOnlyListMemberAccessor.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors +{ + internal sealed class StructReadOnlyListMemberAccessor : EnumerableMemberAccessor + where T: IReadOnlyList + where TV: struct + { + private static readonly object BoxedDefault = default(TV); + + protected override bool TryGetValueInternal(object instance, int index, out object value) + { + var list = (T) instance; + if (index >= list.Count) + { + value = BoxedDefault; + return false; + } + + value = list[index]; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs deleted file mode 100644 index 85d1ad79..00000000 --- a/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections; -using System.Linq; -using System.Text.RegularExpressions; - -namespace HandlebarsDotNet.MemberAccessors -{ - internal class EnumerableMemberAccessor : IMemberAccessor - { - private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); - - public bool TryGetValue(object instance, Type type, string memberName, out object value) - { - value = null; - - var match = IndexRegex.Match(memberName); - if (!match.Success) return false; - const string indexGroupName = "index"; - if (!match.Groups[indexGroupName].Success || !int.TryParse(match.Groups[indexGroupName].Value, out var index)) return false; - - switch (instance) - { - case IList list: - value = list[index]; - return true; - - case IEnumerable enumerable: - value = enumerable.Cast().ElementAtOrDefault(index); - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/IMemberAccessor.cs b/source/Handlebars/MemberAccessors/IMemberAccessor.cs index b74fe239..5bdc9ca3 100644 --- a/source/Handlebars/MemberAccessors/IMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/IMemberAccessor.cs @@ -1,4 +1,5 @@ -using System; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ObjectDescriptors; namespace HandlebarsDotNet.MemberAccessors { @@ -11,10 +12,34 @@ public interface IMemberAccessor /// Describes mechanism to access members of an object. Returns if operation is successful and contains data, otherwise returns /// /// - /// /// /// /// - bool TryGetValue(object instance, Type instanceType, string memberName, out object value); + bool TryGetValue(object instance, ChainSegment memberName, out object value); + } + + public readonly ref struct MemberAccessor + { + private readonly object _instance; + private readonly IMemberAccessor _accessor; + + public MemberAccessor(object instance, ObjectDescriptor descriptor) + { + _instance = instance; + _accessor = descriptor.MemberAccessor; + } + + public object this[ChainSegment segment] + { + get + { + if (_accessor.TryGetValue(_instance, segment, out var value)) + { + return value; + } + + return null; + } + } } } \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/MergedMemberAccessor.cs b/source/Handlebars/MemberAccessors/MergedMemberAccessor.cs new file mode 100644 index 00000000..6728139d --- /dev/null +++ b/source/Handlebars/MemberAccessors/MergedMemberAccessor.cs @@ -0,0 +1,26 @@ +using System; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class MergedMemberAccessor : IMemberAccessor + { + private readonly IMemberAccessor[] _accessors; + + public MergedMemberAccessor(params IMemberAccessor[] accessors) + { + _accessors = accessors; + } + + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + for (var index = 0; index < _accessors.Length; index++) + { + if (_accessors[index].TryGetValue(instance, memberName, out value)) return true; + } + + value = default(object); + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs index 89b01683..fb947181 100644 --- a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -1,33 +1,33 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.MemberAccessors { - internal class ReflectionMemberAccessor : IMemberAccessor + internal sealed class ReflectionMemberAccessor : IMemberAccessor { - private readonly InternalHandlebarsConfiguration _configuration; - private readonly IMemberAccessor _inner; + private readonly LookupSlim> _descriptors = + new LookupSlim>(); - public ReflectionMemberAccessor(InternalHandlebarsConfiguration configuration) + private static readonly Func> DescriptorsValueFactory = + key => new DeferredValue(key, type => new RawObjectTypeDescriptor(type)); + + private readonly ICompiledHandlebarsConfiguration _configuration; + + public ReflectionMemberAccessor(ICompiledHandlebarsConfiguration configuration) { _configuration = configuration; - _inner = configuration.CompileTimeConfiguration.UseAggressiveCaching - ? (IMemberAccessor) new MemberAccessor() // will be removed in next iterations - : (IMemberAccessor) new MemberAccessor(); } - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + public bool TryGetValue(object instance, ChainSegment memberName, out object value) { - if (_inner.TryGetValue(instance, instanceType, memberName, out value)) - { - return true; - } + var instanceType = instance.GetType(); + if (TryGetValueImpl(instance, instanceType, memberName, out value)) return true; - var aliasProviders = _configuration.CompileTimeConfiguration.AliasProviders; + var aliasProviders = _configuration.AliasProviders; for (var index = 0; index < aliasProviders.Count; index++) { if (aliasProviders[index].TryGetMemberByAlias(instance, instanceType, memberName, out value)) @@ -38,80 +38,61 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o return false; } - private abstract class ObjectTypeDescriptor + private bool TryGetValueImpl(object instance, Type instanceType, ChainSegment memberName, out object value) { - protected readonly LookupSlim, Func>> - Accessors = new LookupSlim, Func>>(); - - protected Type Type { get; } - - public ObjectTypeDescriptor(Type type) + if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) { - Type = type; + deferredValue = _descriptors.GetOrAdd(instanceType, DescriptorsValueFactory); } - public abstract Func GetOrCreateAccessor(string name); + var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); + value = accessor?.Invoke(instance); + return accessor != null; } - private class MemberAccessor : IMemberAccessor - where T : ObjectTypeDescriptor - { - private readonly LookupSlim> _descriptors = - new LookupSlim>(); - - private static readonly Func> ValueFactory = - key => new DeferredValue(key, type => (T) Activator.CreateInstance(typeof(T), type)); - - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) - { - if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) - { - deferredValue = _descriptors.GetOrAdd(instanceType, ValueFactory); - } - - var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); - value = accessor?.Invoke(instance); - return accessor != null; - } - } - - private sealed class RawObjectTypeDescriptor : ObjectTypeDescriptor + private sealed class RawObjectTypeDescriptor { private static readonly MethodInfo CreateGetDelegateMethodInfo = typeof(RawObjectTypeDescriptor) .GetMethod(nameof(CreateGetDelegate), BindingFlags.Static | BindingFlags.NonPublic); - private static readonly Func, Func> ValueGetterFactory = o => GetValueGetter(o.Key, o.Value); + private static readonly Func, Func> ValueGetterFactory = o => GetValueGetter(o.Key, o.Value); - private static readonly Func, Func>> - ValueFactory = (key, state) => new DeferredValue, Func>(new KeyValuePair(key, state), ValueGetterFactory); + private static readonly Func, Func>> + ValueFactory = (key, state) => new DeferredValue, Func>(new KeyValuePair(key, state), ValueGetterFactory); - public RawObjectTypeDescriptor(Type type) : base(type) + private readonly LookupSlim, Func>> + _accessors = new LookupSlim, Func>>(); + + private Type Type { get; } + + public RawObjectTypeDescriptor(Type type) { + Type = type; } - public override Func GetOrCreateAccessor(string name) + public Func GetOrCreateAccessor(ChainSegment name) { - return Accessors.TryGetValue(name, out var deferredValue) + return _accessors.TryGetValue(name, out var deferredValue) ? deferredValue.Value - : Accessors.GetOrAdd(name, ValueFactory, Type).Value; + : _accessors.GetOrAdd(name, ValueFactory, Type).Value; } - private static Func GetValueGetter(string name, Type type) + private static Func GetValueGetter(ChainSegment name, Type type) { - var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => + var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(o => o.GetIndexParameters().Length == 0 && - string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + string.Equals(o.Name, name.LowerInvariant, StringComparison.OrdinalIgnoreCase) + ); if (property != null) { return (Func) CreateGetDelegateMethodInfo .MakeGenericMethod(type, property.PropertyType) - .Invoke(null, new[] {property}); + .Invoke(null, new object[] {property}); } - var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(o => string.Equals(o.Name, name.LowerInvariant, StringComparison.OrdinalIgnoreCase)); + if (field != null) { return o => field.GetValue(o); @@ -126,56 +107,5 @@ private static Func CreateGetDelegate(PropertyInfo pr return o => (object) @delegate((T) o); } } - - private sealed class CompiledObjectTypeDescriptor : ObjectTypeDescriptor - { - private static readonly Func, Func> ValueGetterFactory = - o => GetValueGetter(o.Key, o.Value); - - private static readonly Func, Func>> - ValueFactory = (key, state) => new DeferredValue, Func>(new KeyValuePair(key, state), ValueGetterFactory); - - public CompiledObjectTypeDescriptor(Type type) : base(type) - { - } - - public override Func GetOrCreateAccessor(string name) - { - return Accessors.TryGetValue(name, out var deferredValue) - ? deferredValue.Value - : Accessors.GetOrAdd(name, ValueFactory, Type).Value; - } - - private static Func GetValueGetter(string name, Type type) - { - var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => - o.GetIndexParameters().Length == 0 && - string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - - if (property != null) - { - var instance = Expression.Parameter(typeof(object), "i"); - var memberExpression = Expression.Property(Expression.Convert(instance, type), name); - var convert = Expression.TypeAs(memberExpression, typeof(object)); - - return (Func) Expression.Lambda(convert, instance).Compile(); - } - - var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - - if (field != null) - { - var instance = Expression.Parameter(typeof(object), "i"); - var memberExpression = Expression.Field(Expression.Convert(instance, type), name); - var convert = Expression.TypeAs(memberExpression, typeof(object)); - - return (Func) Expression.Lambda(convert, instance).Compile(); - } - - return null; - } - } } } \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs index 3b5d4f45..c35fee3a 100644 --- a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -3,61 +3,40 @@ using System.Linq; using HandlebarsDotNet.Compiler.Structure.Path; -namespace HandlebarsDotNet +namespace HandlebarsDotNet.MemberAliasProvider { - internal class CollectionMemberAliasProvider : IMemberAliasProvider + internal sealed class CollectionMemberAliasProvider : IMemberAliasProvider { - private readonly InternalHandlebarsConfiguration _configuration; + private readonly ICompiledHandlebarsConfiguration _configuration; + private static readonly ChainSegment Count = ChainSegment.Create("Count"); + private static readonly ChainSegment Length = ChainSegment.Create("Length"); - public CollectionMemberAliasProvider(InternalHandlebarsConfiguration configuration) + public CollectionMemberAliasProvider(ICompiledHandlebarsConfiguration configuration) { _configuration = configuration; } - public bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) + public bool TryGetMemberByAlias(object instance, Type targetType, ChainSegment memberAlias, out object value) { - var segment = new ChainSegment(memberAlias); switch (instance) { - case Array array: - switch (segment.LowerInvariant) - { - case "count": - value = array.Length; - return true; - - default: - value = null; - return false; - } - - case ICollection array: - switch (segment.LowerInvariant) - { - case "length": - value = array.Count; - return true; - - default: - value = null; - return false; - } - - case IEnumerable enumerable: - if (!_configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor)) - { - value = null; - return false; - } + case Array array when memberAlias.Equals(Count): + value = array.Length; + return true; + + case ICollection array when memberAlias.Equals(Length): + value = array.Count; + return true; + case IEnumerable enumerable when _configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor) && descriptor.GetProperties != null: var properties = descriptor.GetProperties(descriptor, enumerable); - var property = properties.FirstOrDefault(o => + var property = properties.OfType().FirstOrDefault(o => { var name = o.ToString().ToLowerInvariant(); return name.Equals("length") || name.Equals("count"); }); - if (property != null && descriptor.MemberAccessor.TryGetValue(enumerable, targetType, property.ToString(), out value)) return true; + if (property != null && descriptor.MemberAccessor.TryGetValue(enumerable, property.ToString(), out value)) return true; value = null; return false; diff --git a/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs index 30ccf332..a01eb675 100644 --- a/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet { @@ -33,7 +34,8 @@ public DelegatedMemberAliasProvider AddAlias(string alias, Func ac return this; } - bool IMemberAliasProvider.TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) + bool IMemberAliasProvider.TryGetMemberByAlias(object instance, Type targetType, ChainSegment memberAlias, + out object value) { if (_aliases.TryGetValue(targetType, out var aliases)) { diff --git a/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs index 382e645d..73c19119 100644 --- a/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs @@ -1,4 +1,5 @@ using System; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet { @@ -7,6 +8,6 @@ namespace HandlebarsDotNet /// public interface IMemberAliasProvider { - bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value); + bool TryGetMemberByAlias(object instance, Type targetType, ChainSegment memberAlias, out object value); } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs index ebdf9a87..7d4d0a30 100644 --- a/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs @@ -2,58 +2,40 @@ using System.Collections; using System.Reflection; using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.MemberAccessors.EnumerableAccessors; namespace HandlebarsDotNet.ObjectDescriptors { internal class CollectionObjectDescriptor : IObjectDescriptorProvider { - private readonly ObjectDescriptorProvider _objectDescriptorProvider; + private readonly IObjectDescriptorProvider _objectDescriptorProvider; + private static readonly Type Type = typeof(ICollection); - public CollectionObjectDescriptor(ObjectDescriptorProvider objectDescriptorProvider) + public CollectionObjectDescriptor(IObjectDescriptorProvider objectDescriptorProvider) { _objectDescriptorProvider = objectDescriptorProvider; } - public bool CanHandleType(Type type) - { - return typeof(ICollection).IsAssignableFrom(type) && _objectDescriptorProvider.CanHandleType(type); - } - public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - if (!_objectDescriptorProvider.TryGetDescriptor(type, out value)) return false; - - var mergedMemberAccessor = new MergedMemberAccessor(new EnumerableMemberAccessor(), value.MemberAccessor); - value = new ObjectDescriptor( - value.DescribedType, - mergedMemberAccessor, - value.GetProperties, - true - ); - - return true; - - } - } - - internal class MergedMemberAccessor : IMemberAccessor - { - private readonly IMemberAccessor[] _accessors; - - public MergedMemberAccessor(params IMemberAccessor[] accessors) - { - _accessors = accessors; - } - - public bool TryGetValue(object instance, Type type, string memberName, out object value) - { - for (var index = 0; index < _accessors.Length; index++) + if (!Type.IsAssignableFrom(type)) { - if (_accessors[index].TryGetValue(instance, type, memberName, out value)) return true; + value = ObjectDescriptor.Empty; + return false; } - value = default(object); - return false; + if (!_objectDescriptorProvider.TryGetDescriptor(type, out value)) + { + value = ObjectDescriptor.Empty; + return false; + } + + var enumerableMemberAccessor = EnumerableMemberAccessor.Create(type); + + var mergedMemberAccessor = new MergedMemberAccessor(enumerableMemberAccessor, value.MemberAccessor); + value = new ObjectDescriptor(value.DescribedType, mergedMemberAccessor, value.GetProperties, true); + + return true; } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs deleted file mode 100644 index 03d4d666..00000000 --- a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.MemberAccessors; - -namespace HandlebarsDotNet.ObjectDescriptors -{ - internal class ContextObjectDescriptor : IObjectDescriptorProvider - { - private static readonly Type BindingContextType = typeof(BindingContext); - private static readonly string[] Properties = { "root", "parent" }; - private static readonly Func> PropertiesDelegate = (descriptor, o) => Properties; - - private static readonly ObjectDescriptor Descriptor = - new ObjectDescriptor(BindingContextType, new ContextMemberAccessor(), PropertiesDelegate); - - public bool CanHandleType(Type type) - { - return type == BindingContextType; - } - - public bool TryGetDescriptor(Type type, out ObjectDescriptor value) - { - value = Descriptor; - return true; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs index 4e80caa3..3a1add66 100644 --- a/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs @@ -13,20 +13,23 @@ internal class DictionaryObjectDescriptor : IObjectDescriptorProvider private static readonly Func> GetProperties = (descriptor, arg) => { return Enumerate((IDictionary) arg); - - IEnumerable Enumerate(IDictionary dictionary) + + static IEnumerable Enumerate(IDictionary dictionary) { foreach (var key in dictionary.Keys) yield return key; } }; - public bool CanHandleType(Type type) - { - return typeof(IDictionary).IsAssignableFrom(type); - } + private static readonly Type Type = typeof(IDictionary); public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { + if (!Type.IsAssignableFrom(type)) + { + value = ObjectDescriptor.Empty;; + return false; + } + value = new ObjectDescriptor(type, DictionaryMemberAccessor, GetProperties); return true; diff --git a/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs index 484db2b4..74f594b3 100644 --- a/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Dynamic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.MemberAccessors; namespace HandlebarsDotNet.ObjectDescriptors @@ -10,15 +12,23 @@ namespace HandlebarsDotNet.ObjectDescriptors internal class DynamicObjectDescriptor : IObjectDescriptorProvider { private static readonly DynamicMemberAccessor DynamicMemberAccessor = new DynamicMemberAccessor(); - private static readonly Func> GetProperties = (descriptor, o) => ((IDynamicMetaObjectProvider) o).GetMetaObject(Expression.Constant(o)).GetDynamicMemberNames(); - - public bool CanHandleType(Type type) + private static readonly Func> GetProperties = (descriptor, o) => { - return typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type); - } - + return ((IDynamicMetaObjectProvider) o) + .GetMetaObject(Expression.Constant(o)) + .GetDynamicMemberNames() + .Select(ChainSegment.Create); + }; + private static readonly Type Type = typeof(IDynamicMetaObjectProvider); + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { + if (!Type.IsAssignableFrom(type)) + { + value = ObjectDescriptor.Empty;; + return false; + } + value = new ObjectDescriptor(type, DynamicMemberAccessor, GetProperties); return true; diff --git a/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs index f09bb06f..2872d2cd 100644 --- a/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs @@ -1,26 +1,41 @@ using System; using System.Collections; using System.Reflection; +using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.MemberAccessors.EnumerableAccessors; namespace HandlebarsDotNet.ObjectDescriptors { internal class EnumerableObjectDescriptor : IObjectDescriptorProvider { - private readonly CollectionObjectDescriptor _collectionObjectDescriptor; + private readonly IObjectDescriptorProvider _descriptorProvider; + private static readonly Type Type = typeof(IEnumerable); + private static readonly Type StringType = typeof(string); - public EnumerableObjectDescriptor(CollectionObjectDescriptor collectionObjectDescriptor) + public EnumerableObjectDescriptor(IObjectDescriptorProvider descriptorProvider) { - _collectionObjectDescriptor = collectionObjectDescriptor; + _descriptorProvider = descriptorProvider; } - - public bool CanHandleType(Type type) - { - return typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string); - } - + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - return _collectionObjectDescriptor.TryGetDescriptor(type, out value); + if (!(type != StringType && Type.IsAssignableFrom(type))) + { + value = ObjectDescriptor.Empty; + return false; + } + + if (!_descriptorProvider.TryGetDescriptor(type, out value)) + { + value = ObjectDescriptor.Empty; + return false; + } + + var enumerableMemberAccessor = EnumerableMemberAccessor.Create(type); + var mergedMemberAccessor = new MergedMemberAccessor(enumerableMemberAccessor, value.MemberAccessor); + value = new ObjectDescriptor(value.DescribedType, mergedMemberAccessor, value.GetProperties, true); + + return true; } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs index 461d29c2..bb66f069 100644 --- a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs @@ -4,24 +4,25 @@ using System.Linq; using System.Reflection; using HandlebarsDotNet.Collections; -using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.MemberAccessors.DictionaryAccessors; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.ObjectDescriptors { internal sealed class GenericDictionaryObjectDescriptorProvider : IObjectDescriptorProvider { - private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) - .GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateClassDescriptorWithClassPropertiesMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateClassDescriptorWithClassProperties), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateClassDescriptorWithStructPropertiesMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateClassDescriptorWithStructProperties), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateStructDescriptorWithClassPropertiesMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateStructDescriptorWithClassProperties), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateStructDescriptorWithStructPropertiesMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateStructDescriptorWithStructProperties), BindingFlags.NonPublic | BindingFlags.Static); + private readonly LookupSlim> _typeCache = new LookupSlim>(); - - public bool CanHandleType(Type type) - { - var deferredValue = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); - return deferredValue.Value != null; - } - + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { var interfaceType = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; @@ -30,9 +31,17 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) value = ObjectDescriptor.Empty; return false; } - - var descriptorCreator = CreateDescriptorMethodInfo - .MakeGenericMethod(interfaceType.GetGenericArguments()); + + var genericArguments = interfaceType.GetGenericArguments(); + var factory = genericArguments[1].GetTypeInfo().IsClass + ? genericArguments[0].GetTypeInfo().IsClass + ? CreateClassDescriptorWithClassPropertiesMethodInfo + : CreateClassDescriptorWithStructPropertiesMethodInfo + : genericArguments[0].GetTypeInfo().IsClass + ? CreateStructDescriptorWithClassPropertiesMethodInfo + : CreateStructDescriptorWithStructPropertiesMethodInfo; + + var descriptorCreator = factory.MakeGenericMethod(type, genericArguments[0], genericArguments[1]); value = (ObjectDescriptor) descriptorCreator.Invoke(null, ArrayEx.Empty()); return true; @@ -49,37 +58,52 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) ); }); - private static ObjectDescriptor CreateDescriptor() + private static ObjectDescriptor CreateClassDescriptorWithClassProperties() + where T : IDictionary + where TK: class + where TV: class { - IEnumerable Enumerate(IDictionary o) - { - foreach (var key in o.Keys) yield return key; - } - return new ObjectDescriptor( - typeof(IDictionary), - new DictionaryAccessor(), - (descriptor, o) => Enumerate((IDictionary) o) + typeof(IDictionary), + new GenericClassDictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys ); } - private class DictionaryAccessor : IMemberAccessor + private static ObjectDescriptor CreateClassDescriptorWithStructProperties() + where T : IDictionary + where TK: struct + where TV: class { - private static readonly TypeConverter TypeConverter = TypeDescriptor.GetConverter(typeof(T)); - - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) - { - var key = (T) TypeConverter.ConvertFromString(memberName); - var dictionary = (IDictionary) instance; - if (dictionary.TryGetValue(key, out var v)) - { - value = v; - return true; - } - - value = default(TV); - return false; - } + return new ObjectDescriptor( + typeof(IDictionary), + new GenericClassDictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys + ); + } + + private static ObjectDescriptor CreateStructDescriptorWithClassProperties() + where T : IDictionary + where TK: class + where TV: struct + { + return new ObjectDescriptor( + typeof(IDictionary), + new GenericStructDictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys + ); + } + + private static ObjectDescriptor CreateStructDescriptorWithStructProperties() + where T : IDictionary + where TK: struct + where TV: struct + { + return new ObjectDescriptor( + typeof(IDictionary), + new GenericStructDictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys + ); } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs index e29dd458..e21a80e6 100644 --- a/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs @@ -8,14 +8,7 @@ namespace HandlebarsDotNet.ObjectDescriptors public interface IObjectDescriptorProvider { /// - /// Lightweight method to check whether descriptor can be created - /// - /// - /// - bool CanHandleType(Type type); - - /// - /// Tries to create for . Methods is guarantied to be called if return . + /// Tries to create for . /// /// /// diff --git a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs index 2ce84577..11d93119 100644 --- a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.MemberAccessors; using HandlebarsDotNet.Polyfills; @@ -11,14 +12,16 @@ internal sealed class KeyValuePairObjectDescriptorProvider : IObjectDescriptorPr private static readonly string[] Properties = { "key", "value" }; private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(KeyValuePairObjectDescriptorProvider).GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); private static readonly Func> GetProperties = (descriptor, o) => Properties; - - public bool CanHandleType(Type type) - { - return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>); - } - + private static readonly Type Type = typeof(KeyValuePair<,>); + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { + if (!(type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == Type)) + { + value = ObjectDescriptor.Empty; + return false; + } + var genericArguments = type.GetGenericArguments(); var descriptorCreator = CreateDescriptorMethodInfo .MakeGenericMethod(genericArguments[0], genericArguments[1]); @@ -34,23 +37,24 @@ private static ObjectDescriptor CreateDescriptor() private class KeyValuePairAccessor : IMemberAccessor { - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + public bool TryGetValue(object instance, ChainSegment memberName, out object value) { var keyValuePair = (KeyValuePair) instance; - switch (memberName.ToLowerInvariant()) + + if (memberName.IsKey) + { + value = keyValuePair.Key; + return true; + } + + if (memberName.IsValue) { - case "key": - value = keyValuePair.Key; - return true; - - case "value": - value = keyValuePair.Value; - return true; - - default: - value = default(TV); - return false; + value = keyValuePair.Value; + return true; } + + value = default(TV); + return false; } } } diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs index 998d5920..dfc7ead6 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using HandlebarsDotNet.MemberAccessors; @@ -9,9 +10,20 @@ namespace HandlebarsDotNet.ObjectDescriptors /// public class ObjectDescriptor : IEquatable { - public static readonly ObjectDescriptor Empty = new ObjectDescriptor(); + public static ObjectDescriptor Create(object from, ICompiledHandlebarsConfiguration configuration) + { + if (from == null) return null; + if (!configuration.ObjectDescriptorProvider.TryGetDescriptor(@from.GetType(), out var descriptor)) return null; + return descriptor; + } + + public static bool TryCreate(object from, ICompiledHandlebarsConfiguration configuration, out ObjectDescriptor descriptor) + { + return configuration.ObjectDescriptorProvider.TryGetDescriptor(from.GetType(), out descriptor); + } + private readonly bool _isNotEmpty; /// @@ -25,7 +37,7 @@ public class ObjectDescriptor : IEquatable public ObjectDescriptor( Type describedType, IMemberAccessor memberAccessor, - Func> getProperties, + Func getProperties, bool shouldEnumerate = false, params object[] dependencies ) @@ -59,7 +71,7 @@ private ObjectDescriptor(){ } /// /// Factory enabling receiving properties of specific instance /// - public readonly Func> GetProperties; + public readonly Func GetProperties; /// /// associated with the diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs index 41ef1855..7aeba230 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs @@ -7,16 +7,13 @@ namespace HandlebarsDotNet.ObjectDescriptors internal class ObjectDescriptorFactory : IObjectDescriptorProvider { private readonly IList _providers; - private readonly HashSetSlim _descriptorsNegativeCache = new HashSetSlim(); private readonly LookupSlim> _descriptorsCache = new LookupSlim>(); private static readonly Func, DeferredValue> ValueFactory = (key, providers) => new DeferredValue(key, t => { for (var index = 0; index < providers.Count; index++) { - var descriptorProvider = providers[index]; - if (!descriptorProvider.CanHandleType(t)) continue; - if (!descriptorProvider.TryGetDescriptor(t, out var descriptor)) continue; + if (!providers[index].TryGetDescriptor(t, out var descriptor)) continue; return descriptor; } @@ -28,26 +25,12 @@ public ObjectDescriptorFactory(IList providers) { _providers = providers; } - - public bool CanHandleType(Type type) - { - if (_descriptorsNegativeCache.Contains(type)) return false; - if (_descriptorsCache.TryGetValue(type, out var deferredValue) && !ReferenceEquals(deferredValue.Value, ObjectDescriptor.Empty)) return true; - - deferredValue = _descriptorsCache.GetOrAdd(type, ValueFactory, _providers); - return !ReferenceEquals(deferredValue.Value, ObjectDescriptor.Empty); - } - + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - if (_descriptorsCache.TryGetValue(type, out var deferredValue)) - { - value = deferredValue.Value; - return true; - } - - value = ObjectDescriptor.Empty; - return false; + var deferredValue = _descriptorsCache.GetOrAdd(type, ValueFactory, _providers); + value = deferredValue.Value; + return !ReferenceEquals(value, ObjectDescriptor.Empty); } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs index 9446f667..e876c9d0 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -1,49 +1,88 @@ using System; +using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.MemberAccessors; namespace HandlebarsDotNet.ObjectDescriptors { internal class ObjectDescriptorProvider : IObjectDescriptorProvider { + private static readonly Type StringType = typeof(string); + private static readonly DynamicObjectDescriptor DynamicObjectDescriptor = new DynamicObjectDescriptor(); + private readonly Type _dynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); - private readonly LookupSlim> _membersCache = new LookupSlim>(); + private readonly LookupSlim> _membersCache = new LookupSlim>(); private readonly ReflectionMemberAccessor _reflectionMemberAccessor; - public ObjectDescriptorProvider(InternalHandlebarsConfiguration configuration) + public ObjectDescriptorProvider(ICompiledHandlebarsConfiguration configuration) { _reflectionMemberAccessor = new ReflectionMemberAccessor(configuration); } - public bool CanHandleType(Type type) - { - return !_dynamicMetaObjectProviderType.IsAssignableFrom(type) && type != typeof(string); - } - public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - value = new ObjectDescriptor(type, _reflectionMemberAccessor, (descriptor, o) => + if (type == StringType) + { + value = ObjectDescriptor.Empty; + return false; + } + + if (_dynamicMetaObjectProviderType.IsAssignableFrom(type)) { - var cache = (LookupSlim>) descriptor.Dependencies[0]; - return cache.GetOrAdd(descriptor.DescribedType, DescriptorValueFactory).Value; - }, dependencies: _membersCache); + if (DynamicObjectDescriptor.TryGetDescriptor(type, out var dynamicDescriptor)) + { + var mergedMemberAccessor = new MergedMemberAccessor(_reflectionMemberAccessor, dynamicDescriptor.MemberAccessor); + value = new ObjectDescriptor(type, + mergedMemberAccessor, + (descriptor, o) => + { + var dynamicDescriptorGetProperties = dynamicDescriptor.GetProperties(descriptor, o) + .OfType(); + + return GetProperties(descriptor, o) + .OfType() + .Concat(dynamicDescriptorGetProperties); + }, + dependencies: _membersCache + ); + + return true; + } + + value = ObjectDescriptor.Empty; + return false; + } + + value = new ObjectDescriptor(type, _reflectionMemberAccessor, GetProperties, dependencies: _membersCache); return true; } - private static readonly Func> DescriptorValueFactory = + private static readonly Func> DescriptorValueFactory = key => { - return new DeferredValue(key, type => + return new DeferredValue(key, type => { var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(o => o.CanRead && o.GetIndexParameters().Length == 0); var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - return properties.Cast().Concat(fields).Select(o => o.Name).ToArray(); + return properties + .Cast() + .Concat(fields) + .Select(o => o.Name) + .Select(ChainSegment.Create) + .ToArray(); }); }; + + private static readonly Func> GetProperties = (descriptor, o) => + { + var cache = (LookupSlim>) descriptor.Dependencies[0]; + return cache.GetOrAdd(descriptor.DescribedType, DescriptorValueFactory).Value; + }; } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs index b1f591c7..50fbbe9d 100644 --- a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Reflection; using HandlebarsDotNet.Collections; -using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.MemberAccessors.DictionaryAccessors; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.ObjectDescriptors @@ -11,29 +11,29 @@ namespace HandlebarsDotNet.ObjectDescriptors internal sealed class StringDictionaryObjectDescriptorProvider : IObjectDescriptorProvider { private static readonly object[] EmptyArray = ArrayEx.Empty(); - private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(StringDictionaryObjectDescriptorProvider).GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateClassDescriptorMethodInfo = typeof(StringDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateClassDescriptor), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateStructDescriptorMethodInfo = typeof(StringDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateStructDescriptor), BindingFlags.NonPublic | BindingFlags.Static); private readonly LookupSlim> _typeCache = new LookupSlim>(); - public bool CanHandleType(Type type) - { - return _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value != null; - } - public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - var interfaceType = _typeCache.TryGetValue(type, out var deferredValue) - ? deferredValue.Value - : _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; - + var interfaceType = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; if (interfaceType == null) { value = ObjectDescriptor.Empty; return false; } - var descriptorCreator = CreateDescriptorMethodInfo - .MakeGenericMethod(interfaceType.GetGenericArguments()[1]); + var typeArgument = interfaceType.GetGenericArguments()[1]; + var factory = typeArgument.GetTypeInfo().IsClass + ? CreateClassDescriptorMethodInfo + : CreateStructDescriptorMethodInfo; + + var descriptorCreator = factory + .MakeGenericMethod(type, typeArgument); value = (ObjectDescriptor) descriptorCreator.Invoke(null, EmptyArray); return true; @@ -48,29 +48,26 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) i.GetGenericArguments()[0] == typeof(string)); }); - private static ObjectDescriptor CreateDescriptor() + private static ObjectDescriptor CreateClassDescriptor() + where T : IDictionary + where TV: class { return new ObjectDescriptor( typeof(IDictionary), - new DictionaryAccessor(), - (descriptor, o) => ((IDictionary) o).Keys + new StringClassDictionaryAccessor(), + (descriptor, o) => ((T) o).Keys ); } - private class DictionaryAccessor : IMemberAccessor + private static ObjectDescriptor CreateStructDescriptor() + where T : IDictionary + where TV: struct { - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) - { - var dictionary = (IDictionary) instance; - if (dictionary.TryGetValue(memberName, out var v)) - { - value = v; - return true; - } - - value = default(TV); - return false; - } + return new ObjectDescriptor( + typeof(IDictionary), + new StringStructDictionaryAccessor(), + (descriptor, o) => ((T) o).Keys + ); } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectExtensions.cs b/source/Handlebars/ObjectExtensions.cs index 98a5db1f..70a4252e 100644 --- a/source/Handlebars/ObjectExtensions.cs +++ b/source/Handlebars/ObjectExtensions.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Threading; namespace HandlebarsDotNet { @@ -10,23 +9,4 @@ internal static class ObjectExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T As(this object source) => (T) source; } - - internal static class ReadWriteLockExtensions - { - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DisposableContainer UseRead(this ReaderWriterLockSlim @lock) - { - @lock.EnterReadLock(); - return new DisposableContainer(@lock, self => self.ExitReadLock()); - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DisposableContainer UseWrite(this ReaderWriterLockSlim @lock) - { - @lock.EnterWriteLock(); - return new DisposableContainer(@lock, self => self.ExitWriteLock()); - } - } } \ No newline at end of file diff --git a/source/Handlebars/Polyfills/ArrayEx.cs b/source/Handlebars/Polyfills/ArrayEx.cs index 40dda581..23c50e5c 100644 --- a/source/Handlebars/Polyfills/ArrayEx.cs +++ b/source/Handlebars/Polyfills/ArrayEx.cs @@ -1,9 +1,11 @@ using System; +using System.Runtime.CompilerServices; namespace HandlebarsDotNet.Polyfills { internal static class ArrayEx { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T[] Empty() { #if !netstandard diff --git a/source/Handlebars/Polyfills/StringExtensions.cs b/source/Handlebars/Polyfills/StringExtensions.cs deleted file mode 100644 index bd462fc9..00000000 --- a/source/Handlebars/Polyfills/StringExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace HandlebarsDotNet.Polyfills -{ - internal static class StringExtensions - { - public static string Intern(this string str) - { - if (string.IsNullOrEmpty(str)) return str; -#if netstandard1_3 - return str; -#else - return string.Intern(str); -#endif - } - } -} \ No newline at end of file diff --git a/source/Handlebars/TypeExtensions.cs b/source/Handlebars/TypeExtensions.cs new file mode 100644 index 00000000..94add9f9 --- /dev/null +++ b/source/Handlebars/TypeExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; + +namespace HandlebarsDotNet +{ + internal static class TypeExtensions + { + public static bool IsAssignableToGenericType(this Type givenType, Type genericType, out Type resolvedType) + { + while (true) + { + var interfaceTypes = givenType.GetInterfaces(); + + for (var index = 0; index < interfaceTypes.Length; index++) + { + resolvedType = interfaceTypes[index]; + if (resolvedType.GetTypeInfo().IsGenericType && resolvedType.GetGenericTypeDefinition() == genericType) return true; + } + + if (givenType.GetTypeInfo().IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + resolvedType = givenType; + return true; + } + + var baseType = givenType.GetTypeInfo().BaseType; + if (baseType == null) + { + resolvedType = null; + return false; + } + + givenType = baseType; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs deleted file mode 100644 index 518bc99c..00000000 --- a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs +++ /dev/null @@ -1,57 +0,0 @@ -using HandlebarsDotNet.Compiler; -using HandlebarsDotNet.Compiler.Structure.Path; - -namespace HandlebarsDotNet.ValueProviders -{ - internal class BindingContextValueProvider : IValueProvider - { - private readonly BindingContext _context; - - public BindingContextValueProvider(BindingContext context) - { - _context = context; - } - - public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; - - public bool TryGetValue(ref ChainSegment segment, out object value) - { - switch (segment.LowerInvariant) - { - case "root": - value = _context.Root; - return true; - - case "parent": - value = _context.ParentContext; - return true; - - default: - return TryGetContextVariable(_context.Value, ref segment, out value); - } - } - - private bool TryGetContextVariable(object instance, ref ChainSegment segment, out object value) - { - value = null; - if (instance == null) return false; - - var instanceType = instance.GetType(); - var descriptorProvider = _context.Configuration.ObjectDescriptorProvider; - if( - descriptorProvider.CanHandleType(instanceType) && - descriptorProvider.TryGetDescriptor(instanceType, out var descriptor) && - descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment, out value) - ) - { - return true; - } - - return _context.ParentContext?.TryGetContextVariable(ref segment, out value) ?? false; - } - - public void Dispose() - { - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/BlockParamsValues.cs b/source/Handlebars/ValueProviders/BlockParamsValues.cs new file mode 100644 index 00000000..87abc610 --- /dev/null +++ b/source/Handlebars/ValueProviders/BlockParamsValues.cs @@ -0,0 +1,60 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + public readonly ref struct BlockParamsValues + { + private readonly ChainSegment[] _variables; + private readonly FixedSizeDictionary _values; + private readonly ICompiledHandlebarsConfiguration _configuration; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BlockParamsValues(BindingContext context, ChainSegment[] variables) + { + _variables = variables; + if (context != null) + { + _values = context.BlockParamsObject; + _configuration = context.Configuration; + } + else + { + _values = null; + _configuration = null; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CreateProperty(in int variableIndex, out EntryIndex index) + { + var variable = GetVariable(variableIndex); + if (ReferenceEquals(variable, null)) + { + index = new EntryIndex(-1, 0); + return; + } + var value = variable.GetUndefinedBindingResult(_configuration); + + _values.AddOrReplace(variable, value, out index); + } + + public object this[in EntryIndex index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + if(_values == null) return; + _values[index] = value; + } + } + + private ChainSegment GetVariable(int index) + { + if (_variables == null || _variables.Length == 0 || index >= _variables.Length) return null; + return _variables[index]; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/DataValues.cs b/source/Handlebars/ValueProviders/DataValues.cs new file mode 100644 index 00000000..d7c82f67 --- /dev/null +++ b/source/Handlebars/ValueProviders/DataValues.cs @@ -0,0 +1,88 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + public readonly struct DataValues + { + private readonly BindingContext _context; + private readonly EntryIndex[] _wellKnownVariables; + private readonly FixedSizeDictionary _data; + + private ICompiledHandlebarsConfiguration Configuration + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _context.Configuration; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DataValues(BindingContext context) + { + _context = context; + _data = _context.ContextDataObject; + _wellKnownVariables = context.WellKnownVariables; + } + + public object this[ChainSegment segment] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var knownVariableIndex = (int) segment.WellKnownVariable; + if (segment.WellKnownVariable != WellKnownVariable.None && _wellKnownVariables[knownVariableIndex].IsNotEmpty) + { + return _data[_wellKnownVariables[knownVariableIndex]]; + } + + return _data.TryGetValue(segment, out var value) ? value : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + var knownVariableIndex = (int) segment.WellKnownVariable; + if (segment.WellKnownVariable != WellKnownVariable.None) + { + _data.AddOrReplace(segment, value, out _wellKnownVariables[knownVariableIndex]); + return; + } + + _data.AddOrReplace(segment, value, out _); + } + } + + public object this[in EntryIndex entryIndex] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _data[entryIndex]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _data[entryIndex] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CreateProperty(ChainSegment variable, out EntryIndex index) + { + var value = variable.GetUndefinedBindingResult(Configuration); + _data.AddOrReplace(variable, value, out index); + + if (variable.WellKnownVariable != WellKnownVariable.None) + { + _wellKnownVariables[(int) variable.WellKnownVariable] = index; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CreateProperty(ChainSegment variable, object defaultValue, out EntryIndex index) + { + _data.AddOrReplace(variable, defaultValue, out index); + + if (variable.WellKnownVariable != WellKnownVariable.None) + { + _wellKnownVariables[(int) variable.WellKnownVariable] = index; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/IValueProvider.cs b/source/Handlebars/ValueProviders/IValueProvider.cs deleted file mode 100644 index 03ee9839..00000000 --- a/source/Handlebars/ValueProviders/IValueProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using HandlebarsDotNet.Compiler.Structure.Path; - -namespace HandlebarsDotNet.ValueProviders -{ - [Flags] - internal enum ValueTypes - { - Context = 1, - All = 2 - } - - internal interface IValueProvider : IDisposable - { - ValueTypes SupportedValueTypes { get; } - bool TryGetValue(ref ChainSegment segment, out object value); - } -} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/IteratorValueProvider.cs b/source/Handlebars/ValueProviders/IteratorValueProvider.cs deleted file mode 100644 index ed5b24bb..00000000 --- a/source/Handlebars/ValueProviders/IteratorValueProvider.cs +++ /dev/null @@ -1,78 +0,0 @@ -using HandlebarsDotNet.Compiler.Structure.Path; -using Microsoft.Extensions.ObjectPool; - -namespace HandlebarsDotNet.ValueProviders -{ - internal class IteratorValueProvider : IValueProvider - { - private static readonly IteratorValueProviderPool Pool = new IteratorValueProviderPool(); - - public static IteratorValueProvider Create() - { - return Pool.Get(); - } - - public object Value { get; set; } - - public int Index { get; set; } - - public bool First { get; set; } - - public bool Last { get; set; } - - public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; - - public virtual bool TryGetValue(ref ChainSegment segment, out object value) - { - switch (segment.LowerInvariant) - { - case "index": - value = Index; - return true; - case "first": - value = First; - return true; - case "last": - value = Last; - return true; - case "value": - value = Value; - return true; - - default: - value = null; - return false; - } - } - - public virtual void Dispose() - { - Pool.Return(this); - } - - private class IteratorValueProviderPool : DefaultObjectPool - { - public IteratorValueProviderPool() : base(new IteratorValueProviderPolicy()) - { - } - - private class IteratorValueProviderPolicy : IPooledObjectPolicy - { - IteratorValueProvider IPooledObjectPolicy.Create() - { - return new IteratorValueProvider(); - } - - bool IPooledObjectPolicy.Return(IteratorValueProvider item) - { - item.First = true; - item.Last = false; - item.Index = 0; - item.Value = null; - - return true; - } - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/IteratorValues.cs b/source/Handlebars/ValueProviders/IteratorValues.cs new file mode 100644 index 00000000..666bc919 --- /dev/null +++ b/source/Handlebars/ValueProviders/IteratorValues.cs @@ -0,0 +1,50 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + public readonly ref struct IteratorValues + { + private readonly FixedSizeDictionary _data; + + private readonly EntryIndex[] _wellKnownVariables; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IteratorValues(BindingContext bindingContext) : this() + { + _data = bindingContext.ContextDataObject; + _wellKnownVariables = bindingContext.WellKnownVariables; + + _data.AddOrReplace(ChainSegment.Value, null, out _wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.First, BoxedValues.True, out _wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.Last, BoxedValues.False, out _wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.Index, BoxedValues.Zero, out _wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]); + } + + public object Value + { + get => _data[_wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]] = value; + } + + public object First + { + get => _data[_wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]] = value; + } + + public object Index + { + get => _data[_wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]] = value; + } + + public object Last + { + get => _data[_wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]] = value; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs deleted file mode 100644 index c62d5c07..00000000 --- a/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs +++ /dev/null @@ -1,70 +0,0 @@ -using HandlebarsDotNet.Compiler.Structure.Path; -using Microsoft.Extensions.ObjectPool; - -namespace HandlebarsDotNet.ValueProviders -{ - internal class ObjectEnumeratorValueProvider : IteratorValueProvider - { - private HandlebarsConfiguration _configuration; - - private static readonly ObjectEnumeratorValueProviderPool Pool = new ObjectEnumeratorValueProviderPool(); - - public static ObjectEnumeratorValueProvider Create(HandlebarsConfiguration configuration) - { - var provider = Pool.Get(); - provider._configuration = configuration; - return provider; - } - - public string Key { get; set; } - - public override bool TryGetValue(ref ChainSegment segment, out object value) - { - switch (segment.LowerInvariant) - { - case "key": - value = Key; - return true; - - case "last" when !_configuration.Compatibility.SupportLastInObjectIterations: - value = null; - return true; - - default: - return base.TryGetValue(ref segment, out value); - } - } - - public override void Dispose() - { - Pool.Return(this); - } - - private class ObjectEnumeratorValueProviderPool : DefaultObjectPool - { - public ObjectEnumeratorValueProviderPool() : base(new ObjectEnumeratorValueProviderPolicy()) - { - } - - private class ObjectEnumeratorValueProviderPolicy : IPooledObjectPolicy - { - ObjectEnumeratorValueProvider IPooledObjectPolicy.Create() - { - return new ObjectEnumeratorValueProvider(); - } - - bool IPooledObjectPolicy.Return(ObjectEnumeratorValueProvider item) - { - item.First = true; - item.Last = false; - item.Index = 0; - item.Value = null; - item.Key = null; - item._configuration = null; - - return true; - } - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/ObjectIteratorValues.cs b/source/Handlebars/ValueProviders/ObjectIteratorValues.cs new file mode 100644 index 00000000..11d13592 --- /dev/null +++ b/source/Handlebars/ValueProviders/ObjectIteratorValues.cs @@ -0,0 +1,73 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + public readonly ref struct ObjectIteratorValues + { + private readonly FixedSizeDictionary _data; + private readonly bool _supportLastInObjectIterations; + + private readonly EntryIndex[] _wellKnownVariables; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ObjectIteratorValues(BindingContext bindingContext) : this() + { + var configuration = bindingContext.Configuration; + + _data = bindingContext.ContextDataObject; + _supportLastInObjectIterations = configuration.Compatibility.SupportLastInObjectIterations; + _wellKnownVariables = bindingContext.WellKnownVariables; + if (!_supportLastInObjectIterations) + { + var undefined = ChainSegment.Last.GetUndefinedBindingResult(configuration); + _data.AddOrReplace(ChainSegment.Last, undefined, out _wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]); + } + else + { + _data.AddOrReplace(ChainSegment.Last, BoxedValues.False, out _wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]); + } + + _data.AddOrReplace(ChainSegment.Key, null, out _wellKnownVariables[(int) ChainSegment.Key.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.Value, null, out _wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.First, BoxedValues.True, out _wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]); + _data.AddOrReplace(ChainSegment.Index, BoxedValues.Zero, out _wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]); + } + + public object Key + { + get => _data[_wellKnownVariables[(int) ChainSegment.Key.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Key.WellKnownVariable]] = value; + } + + public object Value + { + get => _data[_wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Value.WellKnownVariable]] = value; + } + + public object First + { + get => _data[_wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.First.WellKnownVariable]] = value; + } + + public object Index + { + get => _data[_wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]]; + set => _data[_wellKnownVariables[(int) ChainSegment.Index.WellKnownVariable]] = value; + } + + public object Last + { + get => _data[_wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]]; + set + { + if(!_supportLastInObjectIterations) return; + _data[_wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]] = value; + } + } + } +} \ No newline at end of file From e834b93f199c0c0b6ec4ddbb8e5d3db971906496 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Wed, 14 Oct 2020 01:49:58 +0300 Subject: [PATCH 2/4] Make Casper test cross-platform, fix concurrency, some refactoring --- .../Collections/FixedSizeDictionaryTests.cs | 20 ++--- source/Handlebars.Test/Handlebars.Test.csproj | 1 + .../Handlebars.Test/ViewEngine/CasparTests.cs | 14 ++- .../Collections/DisposableContainer.cs | 5 +- .../Collections/ExtendedEnumerable.cs | 87 +++++++------------ source/Handlebars/Collections/ObjectPool.cs | 28 ++---- .../Compiler/Structure/Path/ChainSegment.cs | 5 +- .../Translation/Expression/IteratorBinder.cs | 14 +-- .../BlockHelpers/BlockHelperDescriptor.cs | 16 +--- .../DelegateBlockHelperDescriptor.cs | 36 ++------ .../Helpers/DelegateHelperDescriptor.cs | 20 ++--- .../Helpers/DelegateReturnHelperDescriptor.cs | 20 ++--- .../Helpers/HelperDescriptorBase.cs | 17 +--- .../Helpers/LookupReturnHelperDescriptor.cs | 13 +-- .../Helpers/ReturnHelperDescriptor.cs | 19 +--- source/Handlebars/Polyfills/ArrayEx.cs | 9 +- 16 files changed, 96 insertions(+), 228 deletions(-) diff --git a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs index b7a055d4..4e736204 100644 --- a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs +++ b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs @@ -5,21 +5,19 @@ namespace HandlebarsDotNet.Test.Collections { - public class FixedSizeDictionaryTests : IDisposable + public struct ObjectComparer : IEqualityComparer { - public struct ObjectComparer : IEqualityComparer - { - public bool Equals(object x, object y) => ReferenceEquals(x, y); + public new bool Equals(object x, object y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => obj.GetHashCode(); - } - + public int GetHashCode(object obj) => obj.GetHashCode(); + } + + public class FixedSizeDictionaryTests : IDisposable + { private static readonly FixedSizeDictionary FixedSizeDictionary; - static FixedSizeDictionaryTests() - { - FixedSizeDictionary = new FixedSizeDictionary(15, 17, new ObjectComparer()); - } + static FixedSizeDictionaryTests() + => FixedSizeDictionary = new FixedSizeDictionary(15, 17, new ObjectComparer()); [Fact] public void AddOrReplace() diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 8eb69d95..79466b9e 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -59,6 +59,7 @@ + diff --git a/source/Handlebars.Test/ViewEngine/CasparTests.cs b/source/Handlebars.Test/ViewEngine/CasparTests.cs index 08a40b47..7dad1332 100644 --- a/source/Handlebars.Test/ViewEngine/CasparTests.cs +++ b/source/Handlebars.Test/ViewEngine/CasparTests.cs @@ -5,12 +5,11 @@ namespace HandlebarsDotNet.Test.ViewEngine { public class CasparTests { -#if !netstandard [Fact] public void CanRenderCasparIndexTemplate() { - var fs = (new DiskFileSystem()); - var handlebars = HandlebarsDotNet.Handlebars.Create(new HandlebarsConfiguration() + var fs = new DiskFileSystem(); + var handlebars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = fs }); @@ -40,8 +39,8 @@ public void CanRenderCasparIndexTemplate() [Fact] public void CanRenderCasparPostTemplate() { - var fs = (new DiskFileSystem()); - var handlebars = HandlebarsDotNet.Handlebars.Create(new HandlebarsConfiguration() + var fs = new DiskFileSystem(); + var handlebars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = fs }); @@ -64,7 +63,7 @@ public void CanRenderCasparPostTemplate() var cq = CsQuery.CQ.CreateDocument(output); Assert.Equal("My Post Title", cq["h1.post-title"].Html()); } -#endif + private static void AddHelpers(IHandlebars handlebars) { handlebars.RegisterHelper("asset", @@ -78,7 +77,7 @@ private static void AddHelpers(IHandlebars handlebars) handlebars.RegisterHelper("url", (writer, context, arguments) => writer.Write("url:" + string.Join("|", arguments))); handlebars.RegisterHelper("excerpt", (writer, context, arguments) => writer.Write("url:" + string.Join("|", arguments))); } -#if !netstandard + [Fact] public void CanRenderCasparPostNoLayoutTemplate() { @@ -135,7 +134,6 @@ public void CanRenderCasparIndexTemplateWithStaticInstance() var cq = CsQuery.CQ.CreateDocument(output); Assert.Equal("My Post Title", cq["h2.post-title a"].Text()); } -#endif class DiskFileSystem : ViewEngineFileSystem { diff --git a/source/Handlebars/Collections/DisposableContainer.cs b/source/Handlebars/Collections/DisposableContainer.cs index 9e0529b6..6c61144b 100644 --- a/source/Handlebars/Collections/DisposableContainer.cs +++ b/source/Handlebars/Collections/DisposableContainer.cs @@ -13,9 +13,6 @@ public DisposableContainer(T value, Action onDispose) Value = value; } - public void Dispose() - { - _onDispose(Value); - } + public void Dispose() => _onDispose(Value); } } \ No newline at end of file diff --git a/source/Handlebars/Collections/ExtendedEnumerable.cs b/source/Handlebars/Collections/ExtendedEnumerable.cs index 7ac07718..a38c4479 100644 --- a/source/Handlebars/Collections/ExtendedEnumerable.cs +++ b/source/Handlebars/Collections/ExtendedEnumerable.cs @@ -1,81 +1,56 @@ using System.Collections; +using System.Runtime.CompilerServices; namespace HandlebarsDotNet.Collections { - /// - /// Wraps and provide additional information about the iteration via - /// - internal sealed class ExtendedEnumerable + internal ref struct ExtendedEnumerator { - private Enumerator _enumerator; + private readonly IEnumerator _enumerator; + private T _next; + private int _index; - public ExtendedEnumerable(IEnumerable enumerable) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ExtendedEnumerator(IEnumerator enumerator) : this() { - _enumerator = new Enumerator(enumerable.GetEnumerator()); + _enumerator = enumerator; + PerformIteration(); } - public ref Enumerator GetEnumerator() - { - return ref _enumerator; - } + public EnumeratorValue Current { get; private set; } - internal struct Enumerator + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() { - private readonly IEnumerator _enumerator; - private Container _next; - private int _index; - - public Enumerator(IEnumerator enumerator) : this() - { - _enumerator = enumerator; - PerformIteration(); - } - - public EnumeratorValue Current { get; private set; } - - public bool MoveNext() - { - if (_next == null) return false; + if (_next == null) return false; - PerformIteration(); + PerformIteration(); - return true; - } + return true; + } - private void PerformIteration() + private void PerformIteration() + { + if (!_enumerator.MoveNext()) { - if (!_enumerator.MoveNext()) - { - Current = _next != null - ? new EnumeratorValue(_next.Value, _index++, true) - : EnumeratorValue.Empty; - - _next = null; - return; - } + Current = _next != null + ? new EnumeratorValue(_next, _index++, true) + : EnumeratorValue.Empty; - if (_next == null) - { - _next = new Container((T) _enumerator.Current); - return; - } - - Current = new EnumeratorValue(_next.Value, _index++, false); - _next.Value = (T) _enumerator.Current; + _next = default; + return; } - } - - private class Container - { - public TValue Value { get; set; } - public Container(TValue value) + if (_next == null) { - Value = value; + _next = (T) _enumerator.Current; + return; } + + Current = new EnumeratorValue(_next, _index++, false); + _next = (T) _enumerator.Current; } } - + internal readonly struct EnumeratorValue { public static readonly EnumeratorValue Empty = new EnumeratorValue(); diff --git a/source/Handlebars/Collections/ObjectPool.cs b/source/Handlebars/Collections/ObjectPool.cs index 339db931..1dbf6124 100644 --- a/source/Handlebars/Collections/ObjectPool.cs +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Threading; namespace HandlebarsDotNet { @@ -17,8 +16,7 @@ internal class InternalObjectPool { private readonly IInternalObjectPoolPolicy _policy; private ConcurrentQueue _queue = new ConcurrentQueue(); - private T _firstItem; - + public InternalObjectPool(IInternalObjectPoolPolicy policy) { Handlebars.Disposables.Enqueue(new Disposer(this)); @@ -30,33 +28,17 @@ public InternalObjectPool(IInternalObjectPoolPolicy policy) public T Get() { - var item = _firstItem; - if (item == null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + if (_queue.TryDequeue(out var item)) { - if (_queue.TryDequeue(out item)) - { - return item; - } - - item = _policy.Create(); + return item; } - - return item; + + return _policy.Create(); } public void Return(T obj) { if (!_policy.Return(obj)) return; - - if (_firstItem == null) - { - // Intentionally not using interlocked here. - // In a worst case scenario two objects may be stored into same slot. - // It is very unlikely to happen and will only mean that one of the objects will get collected. - _firstItem = obj; - return; - } - _queue.Enqueue(obj); } diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index 9ca0f966..83efb360 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -188,10 +188,7 @@ internal UndefinedBindingResult GetUndefinedBindingResult(ICompiledHandlebarsCon private class Disposer : IDisposable { - public void Dispose() - { - Lookup.Clear(); - } + public void Dispose() => Lookup.Clear(); } public struct ChainSegmentEqualityComparer : IEqualityComparer diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index ebc9f454..d539c13a 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -122,15 +122,16 @@ private static void IterateObject(BindingContext context, blockParams.CreateProperty(1, out var _1); var accessor = new MemberAccessor(target, descriptor); - var enumerable = new ExtendedEnumerable(properties); + var enumerable = new ExtendedEnumerator(properties.GetEnumerator()); var enumerated = false; object iteratorValue; ChainSegment iteratorKey; - - foreach (var enumerableValue in enumerable) + + while (enumerable.MoveNext()) { enumerated = true; + var enumerableValue = enumerable.Current; iteratorKey = ChainSegment.Create(enumerableValue.Value); iterator.Key = iteratorKey; @@ -262,15 +263,16 @@ private static void IterateEnumerable(BindingContext context, blockParams.CreateProperty(0, out var _0); blockParams.CreateProperty(1, out var _1); - var enumerable = new ExtendedEnumerable(target); + var enumerator = new ExtendedEnumerator(target.GetEnumerator()); var enumerated = false; object boxedIndex; object iteratorValue; - - foreach (var enumerableValue in enumerable) + + while (enumerator.MoveNext()) { enumerated = true; + var enumerableValue = enumerator.Current; if (enumerableValue.Index == 1) iterator.First = BoxedValues.False; if (enumerableValue.IsLast) iterator.Last = BoxedValues.True; diff --git a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs index 2294c786..039d443e 100644 --- a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptor.cs @@ -2,30 +2,16 @@ namespace HandlebarsDotNet.Helpers.BlockHelpers { - /// - /// - /// public abstract class BlockHelperDescriptor : BlockHelperDescriptorBase { - /// - /// - /// - /// protected BlockHelperDescriptor(string name) : base(name) { } - /// - /// - /// - /// protected BlockHelperDescriptor(PathInfo name) : base(name) { } - - /// - /// - /// + public sealed override HelperType Type { get; } = HelperType.WriteBlock; } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs index 556c690c..df39d802 100644 --- a/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs @@ -3,43 +3,19 @@ namespace HandlebarsDotNet.Helpers.BlockHelpers { - /// - /// - /// public sealed class DelegateBlockHelperDescriptor : BlockHelperDescriptor { private readonly HandlebarsBlockHelper _helper; - - /// - /// - /// - /// - /// - public DelegateBlockHelperDescriptor(string name, HandlebarsBlockHelper helper) : base(name) - { - _helper = helper; - } - /// - /// - /// - /// - /// + public DelegateBlockHelperDescriptor(string name, HandlebarsBlockHelper helper) : base(name) + => _helper = helper; + public DelegateBlockHelperDescriptor(PathInfo name, HandlebarsBlockHelper helper) : base(name) { _helper = helper; } - - /// - /// - /// - /// - /// - /// - /// - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) - { - _helper(output, options, context, arguments); - } + + public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + => _helper(output, options, context, arguments); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/DelegateHelperDescriptor.cs b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs index 6e9f726b..1e4d4351 100644 --- a/source/Handlebars/Helpers/DelegateHelperDescriptor.cs +++ b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs @@ -7,19 +7,13 @@ public sealed class DelegateHelperDescriptor : HelperDescriptor { private readonly HandlebarsHelper _helper; - public DelegateHelperDescriptor(PathInfo name, HandlebarsHelper helper) : base(name) - { - _helper = helper; - } - - public DelegateHelperDescriptor(string name, HandlebarsHelper helper) : base(name) - { - _helper = helper; - } + public DelegateHelperDescriptor(PathInfo name, HandlebarsHelper helper) : base(name) + => _helper = helper; - public override void Invoke(TextWriter output, object context, params object[] arguments) - { - _helper(output, context, arguments); - } + public DelegateHelperDescriptor(string name, HandlebarsHelper helper) : base(name) + => _helper = helper; + + public override void Invoke(TextWriter output, object context, params object[] arguments) + => _helper(output, context, arguments); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs b/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs index 2f3be1f0..580b448a 100644 --- a/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs @@ -6,19 +6,13 @@ public sealed class DelegateReturnHelperDescriptor : ReturnHelperDescriptor { private readonly HandlebarsReturnHelper _helper; - public DelegateReturnHelperDescriptor(PathInfo name, HandlebarsReturnHelper helper) : base(name) - { - _helper = helper; - } - - public DelegateReturnHelperDescriptor(string name, HandlebarsReturnHelper helper) : base(name) - { - _helper = helper; - } + public DelegateReturnHelperDescriptor(PathInfo name, HandlebarsReturnHelper helper) : base(name) + => _helper = helper; - public override object Invoke(object context, params object[] arguments) - { - return _helper(context, arguments); - } + public DelegateReturnHelperDescriptor(string name, HandlebarsReturnHelper helper) : base(name) + => _helper = helper; + + public override object Invoke(object context, params object[] arguments) + => _helper(context, arguments); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/HelperDescriptorBase.cs b/source/Handlebars/Helpers/HelperDescriptorBase.cs index ff75bf8c..67a6e493 100644 --- a/source/Handlebars/Helpers/HelperDescriptorBase.cs +++ b/source/Handlebars/Helpers/HelperDescriptorBase.cs @@ -4,21 +4,10 @@ namespace HandlebarsDotNet.Helpers { - /// - /// - /// public abstract class HelperDescriptorBase : IHelperDescriptor { - /// - /// - /// - /// protected HelperDescriptorBase(string name) => Name = PathResolver.GetPathInfo(name); - - /// - /// - /// - /// + protected HelperDescriptorBase(PathInfo name) => Name = name; public PathInfo Name { get; } @@ -28,10 +17,6 @@ public abstract class HelperDescriptorBase : IHelperDescriptor internal abstract void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments); - /// - /// Returns helper name - /// - /// public override string ToString() => Name; } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs index 5abe544d..3c9c5f6f 100644 --- a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs @@ -1,6 +1,4 @@ -using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; -using HandlebarsDotNet.ObjectDescriptors; namespace HandlebarsDotNet.Helpers { @@ -22,14 +20,9 @@ public override object Invoke(object context, params object[] arguments) var segment = ChainSegment.Create(arguments[1]); - if (ObjectDescriptor.TryCreate(arguments[0], _configuration, out var descriptor)) - { - return !PathResolver.TryAccessMember(arguments[0], segment, _configuration, out var value) - ? segment.GetUndefinedBindingResult(_configuration) - : value; - } - - return segment.GetUndefinedBindingResult(_configuration); + return !PathResolver.TryAccessMember(arguments[0], segment, _configuration, out var value) + ? segment.GetUndefinedBindingResult(_configuration) + : value; } } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/ReturnHelperDescriptor.cs b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs index 0d852633..a9d1e06e 100644 --- a/source/Handlebars/Helpers/ReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs @@ -6,33 +6,16 @@ namespace HandlebarsDotNet.Helpers { public abstract class ReturnHelperDescriptor : HelperDescriptorBase { - /// - /// - /// - /// protected ReturnHelperDescriptor(PathInfo name) : base(name) { } - /// - /// - /// - /// protected ReturnHelperDescriptor(string name) : base(name) { } - - /// - /// - /// + public sealed override HelperType Type { get; } = HelperType.Return; - /// - /// - /// - /// - /// - /// public abstract object Invoke(object context, params object[] arguments); internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) => diff --git a/source/Handlebars/Polyfills/ArrayEx.cs b/source/Handlebars/Polyfills/ArrayEx.cs index 23c50e5c..0f497a5e 100644 --- a/source/Handlebars/Polyfills/ArrayEx.cs +++ b/source/Handlebars/Polyfills/ArrayEx.cs @@ -9,10 +9,17 @@ internal static class ArrayEx public static T[] Empty() { #if !netstandard - return new T[0]; + return EmptyArray.Value; #else return Array.Empty(); #endif } + +#if !netstandard + private static class EmptyArray + { + public static readonly T[] Value = new T[0]; + } +#endif } } \ No newline at end of file From e95bd702551c3a9220681c10ba2afc04531ba830 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Wed, 14 Oct 2020 02:12:17 +0300 Subject: [PATCH 3/4] Drop DictionaryPool --- .../Handlebars/Collections/DictionaryPool.cs | 36 -------------- source/Handlebars/Collections/LookupSlim.cs | 49 ++++++++----------- 2 files changed, 20 insertions(+), 65 deletions(-) delete mode 100644 source/Handlebars/Collections/DictionaryPool.cs diff --git a/source/Handlebars/Collections/DictionaryPool.cs b/source/Handlebars/Collections/DictionaryPool.cs deleted file mode 100644 index ae143eca..00000000 --- a/source/Handlebars/Collections/DictionaryPool.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace HandlebarsDotNet.Collections -{ - internal class DictionaryPool : InternalObjectPool> - { - private static readonly Lazy> Self = - new Lazy>(() => new DictionaryPool(EqualityComparer.Default)); - - public static DictionaryPool Shared => Self.Value; - - public DictionaryPool(IEqualityComparer comparer) : base(new Policy(comparer)) - { - } - - public DictionaryPool(IInternalObjectPoolPolicy> policy) : base(policy) - { - } - - private class Policy : IInternalObjectPoolPolicy> - { - private readonly IEqualityComparer _comparer; - - public Policy(IEqualityComparer comparer) => _comparer = comparer; - - public Dictionary Create() => new Dictionary(_comparer); - - public bool Return(Dictionary item) - { - item.Clear(); - return true; - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/LookupSlim.cs b/source/Handlebars/Collections/LookupSlim.cs index 66f80c61..6928c9b6 100644 --- a/source/Handlebars/Collections/LookupSlim.cs +++ b/source/Handlebars/Collections/LookupSlim.cs @@ -1,58 +1,49 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; namespace HandlebarsDotNet.Collections { - internal sealed class LookupSlim where TKey : class + internal sealed class LookupSlim { - private static readonly DictionaryPool Pool = DictionaryPool.Shared; - private Dictionary _inner; + private readonly IEqualityComparer _comparer; - public LookupSlim() => _inner = Pool.Get(); + public LookupSlim(IEqualityComparer comparer = null) + { + _comparer = comparer ?? EqualityComparer.Default; + _inner = new Dictionary(_comparer); + } public bool ContainsKey(TKey key) => _inner.ContainsKey(key); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue GetOrAdd(TKey key, Func valueFactory) { - return _inner.TryGetValue(key, out var value) - ? value - : Write(key, valueFactory(key)); + return !_inner.TryGetValue(key, out var value) + ? Write(key, valueFactory(key)) + : value; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue GetOrAdd(TKey key, Func valueFactory, TState state) { - return _inner.TryGetValue(key, out var value) - ? value - : Write(key, valueFactory(key, state)); + return !_inner.TryGetValue(key, out var value) + ? Write(key, valueFactory(key, state)) + : value; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TKey key, out TValue value) => _inner.TryGetValue(key, out value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() => _inner.Clear(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue Write(TKey key, TValue value) { - var copy = Pool.Get(); var inner = _inner; - inner.CopyTo(copy); - copy[key] = value; - - if (Interlocked.CompareExchange(ref _inner, copy, inner) != _inner) - { - Pool.Return(inner); - } - else + var copy = new Dictionary(inner, _comparer) { - Pool.Return(copy); - } + [key] = value + }; + + Interlocked.CompareExchange(ref _inner, copy, inner); return value; } From f2127066a9c131f460820fdbee4831009a01a1a0 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Wed, 14 Oct 2020 02:29:23 +0300 Subject: [PATCH 4/4] Make test results compatible with Github Actions --- .github/workflows/ci.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .gitignore | 5 ++++- source/Handlebars.Test/Handlebars.Test.csproj | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31819740..793b165a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: dotnet-version: 3.1.102 - name: Test working-directory: ./source - run: dotnet test Handlebars.sln --logger:trx + run: dotnet test Handlebars.sln --logger:trx --logger:GitHubActions sonar-ci: name: SonarCloud diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 104cf227..9d3c86fc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -33,7 +33,7 @@ jobs: dotnet-version: 3.1.201 - name: Test working-directory: ./source - run: dotnet test Handlebars.sln --logger:trx + run: dotnet test Handlebars.sln --logger:trx --logger:GitHubActions sonar-pr: name: SonarCloud diff --git a/.gitignore b/.gitignore index 36c6a479..76570484 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,7 @@ nupkgs .idea/ #DotNet Benchmark -BenchmarkDotNet.Artifacts/ \ No newline at end of file +BenchmarkDotNet.Artifacts/ + +#Test results +TestResults/ diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 79466b9e..17cadaac 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -42,6 +42,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive +