diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1e1ae25..4dc7e673 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: [ master ] - + jobs: build: name: Build @@ -27,7 +27,11 @@ jobs: os: [ macos-latest, ubuntu-latest, windows-latest ] steps: - uses: actions/checkout@master - - name: Setup dotnet + - name: Setup dotnet 2.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.1.811 + - name: Setup dotnet 3.1 uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.403 @@ -42,6 +46,14 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup dotnet 2.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.1.811 + - name: Setup dotnet 3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.201 - uses: actions/setup-java@v1 with: java-version: '13' # The JDK version to make available on the path. diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ddfaccf7..40894601 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,11 @@ jobs: os: [ macos-latest, ubuntu-latest, windows-latest ] steps: - uses: actions/checkout@master - - name: Setup dotnet + - name: Setup dotnet 2.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.1.811 + - name: Setup dotnet 3.1 uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.403 @@ -42,6 +46,14 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup dotnet 2.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.1.811 + - name: Setup dotnet 3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.201 - uses: actions/setup-java@v1 with: java-version: '13' # The JDK version to make available on the path. diff --git a/source/Handlebars.Benchmark/Compilation.cs b/source/Handlebars.Benchmark/Compilation.cs index 85410cd4..51d5e53e 100644 --- a/source/Handlebars.Benchmark/Compilation.cs +++ b/source/Handlebars.Benchmark/Compilation.cs @@ -19,7 +19,7 @@ public void Setup() } [Benchmark] - public Func Template() + public HandlebarsTemplate Template() { const string template = @" childCount={{level1.Count}} diff --git a/source/Handlebars.Benchmark/EndToEnd.cs b/source/Handlebars.Benchmark/EndToEnd.cs index 377ba5cd..33ddc4af 100644 --- a/source/Handlebars.Benchmark/EndToEnd.cs +++ b/source/Handlebars.Benchmark/EndToEnd.cs @@ -9,7 +9,7 @@ namespace HandlebarsNet.Benchmark public class EndToEnd { private object _data; - private Action _default; + private HandlebarsTemplate _default; [Params(5)] public int N { get; set; } diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index c5b15f77..f9681d8c 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -8,7 +8,7 @@ namespace HandlebarsNet.Benchmark { public class Execution { - private readonly List> _templates = new List>(); + private readonly List> _templates = new List>(); [GlobalSetup] public void Setup() diff --git a/source/Handlebars.Test/ArgumentsTests.cs b/source/Handlebars.Test/ArgumentsTests.cs new file mode 100644 index 00000000..ddfaf90e --- /dev/null +++ b/source/Handlebars.Test/ArgumentsTests.cs @@ -0,0 +1,161 @@ +using System.Linq; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class ArgumentsTests + { + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(10)] + public void ArgumentsCtor(int count) + { + var values = new object[count]; + CreateValues(values); + var arguments = CreateArguments(values); + + for (var index = 0; index < values.Length; index++) + { + Assert.Equal(values[index], arguments[index]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(10)] + public void ArgumentsEnumerator(int count) + { + var values = new object[count]; + CreateValues(values); + var arguments = CreateArguments(values); + + var valuesEnumerator = values.GetEnumerator(); + var argumentsEnumerator = arguments.GetEnumerator(); + + for (int index = 0; index < values.Length; index++) + { + Assert.Equal(valuesEnumerator.MoveNext(), argumentsEnumerator.MoveNext()); + Assert.Equal(valuesEnumerator.Current, argumentsEnumerator.Current); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(10)] + public void ArgumentsEnumerable(int count) + { + var values = new object[count]; + CreateValues(values); + var arguments = CreateArguments(values); + + using var valuesEnumerator = values.AsEnumerable().GetEnumerator(); + using var argumentsEnumerator = arguments.GetEnumerator(); + + for (int index = 0; index < values.Length; index++) + { + Assert.Equal(valuesEnumerator.MoveNext(), argumentsEnumerator.MoveNext()); + Assert.Equal(valuesEnumerator.Current, argumentsEnumerator.Current); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(10)] + public void ArgumentsAdd(int count) + { + var values = new object[count]; + CreateValues(values); + var arguments = CreateArguments(values); + + var value = new object(); + var newArguments = arguments.Add(value); + + Assert.Equal(arguments.Length + 1, newArguments.Length); + Assert.Equal(value, newArguments[newArguments.Length - 1]); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(10)] + public void ArgumentsEquals(int count) + { + var values = new object[count]; + CreateValues(values); + var arguments1 = CreateArguments(values); + var arguments2 = CreateArguments(values); + + Assert.Equal(arguments1, arguments2); + } + + private static void CreateValues(object[] values) + { + for (var i = 0; i < values.Length; i++) + { + values[i] = new object(); + } + } + + private static Arguments CreateArguments(object[] values) + { + int count = values.Length; + var constructor = + typeof(Arguments) + .GetConstructors() + .SingleOrDefault(o => + { + var parameterInfos = o.GetParameters(); + return parameterInfos.Length == count && parameterInfos[0].ParameterType != typeof(object[]); + }) + ?? + typeof(Arguments) + .GetConstructors() + .Single(o => + { + var parameterInfos = o.GetParameters(); + return parameterInfos.Length == 1 && parameterInfos[0].ParameterType == typeof(object[]); + }); + + Arguments arguments; + if (values.Length <= 6) + { + arguments = (Arguments) constructor.Invoke(values); + } + else + { + arguments = (Arguments) constructor.Invoke(new object[] {values}); + } + + return arguments; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 5c2ee3f7..241c380e 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -314,6 +314,7 @@ public void BasicPropertyOnArray(IHandlebars handlebars) public void AliasedPropertyOnArray(IHandlebars handlebars) { var source = "Array is {{ names.count }} item(s) long"; + handlebars.Configuration.UseCollectionMemberAliasProvider(); var template = handlebars.Compile(source); var data = new { @@ -345,6 +346,7 @@ public void CustomAliasedPropertyOnArray(IHandlebars handlebars) public void AliasedPropertyOnList(IHandlebars handlebars) { var source = "Array is {{ names.Length }} item(s) long"; + handlebars.Configuration.UseCollectionMemberAliasProvider(); var template = handlebars.Compile(source); var data = new { @@ -432,6 +434,25 @@ public void BasicWith(IHandlebars handlebars) Assert.Equal("Hello, my good friend Erik!", result); } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void GlobalDataPropagation(IHandlebars handlebars) + { + var source = "{{#with input}}{{first}} {{@global1}} {{#with second}}{{third}} {{@global2}}{{/with}}{{/with}}"; + var template = handlebars.Compile(source); + var data = new + { + input = new + { + first = 1, + second = new { + third = 3 + } + } + }; + var result = template(data, new { global1 = 2, global2 = 4 }); + Assert.Equal("1 2 3 4", result); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void TestSingleLoopDictionary(IHandlebars handlebars) { @@ -1853,7 +1874,7 @@ public bool TryResolveHelper(string name, Type targetType, out HelperDescriptorB return false; } - object Helper(dynamic context, object[] arguments) => method.Invoke(arguments[0], arguments.Skip(1).ToArray()); + object Helper(object context, Arguments arguments) => method.Invoke(arguments[0], arguments.AsEnumerable().Skip(1).ToArray()); helper = new DelegateReturnHelperDescriptor(name, Helper); return true; } diff --git a/source/Handlebars.Test/ClosureBuilderTests.cs b/source/Handlebars.Test/ClosureBuilderTests.cs new file mode 100644 index 00000000..74c33156 --- /dev/null +++ b/source/Handlebars.Test/ClosureBuilderTests.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class ClosureBuilderTests + { + [Fact] + public void GeneratesClosureWithOverflow() + { + var builder = new ClosureBuilder(); + + var paths = GeneratePaths(builder, 6); + var helpers = GenerateHelpers(builder, 6); + var blockHelpers = GenerateBlockHelpers(builder, 6); + var others = GenerateOther(builder, 6); + + _ = builder.Build(out var closure); + + Assert.Equal(paths[0], closure.PI0); + Assert.Equal(paths[3], closure.PI3); + Assert.Equal(paths[5], closure.PIA[1]); + + Assert.Equal(helpers[0], closure.HD0); + Assert.Equal(helpers[3], closure.HD3); + Assert.Equal(helpers[5], closure.HDA[1]); + + Assert.Equal(blockHelpers[0], closure.BHD0); + Assert.Equal(blockHelpers[3], closure.BHD3); + Assert.Equal(blockHelpers[5], closure.BHDA[1]); + + Assert.Equal(others[0], closure.A[0]); + Assert.Equal(others[3], closure.A[3]); + Assert.Equal(others[5], closure.A[5]); + } + + [Fact] + public void GeneratesClosureWithoutOverflow() + { + var builder = new ClosureBuilder(); + + var paths = GeneratePaths(builder, 2); + var helpers = GenerateHelpers(builder, 2); + var blockHelpers = GenerateBlockHelpers(builder, 2); + var others = GenerateOther(builder, 2); + + _ = builder.Build(out var closure); + + Assert.Equal(paths[0], closure.PI0); + Assert.Equal(paths[1], closure.PI1); + Assert.Null(closure.PIA); + + Assert.Equal(helpers[0], closure.HD0); + Assert.Equal(helpers[1], closure.HD1); + Assert.Null(closure.HDA); + + Assert.Equal(blockHelpers[0], closure.BHD0); + Assert.Equal(blockHelpers[1], closure.BHD1); + Assert.Null(closure.BHDA); + + Assert.Equal(others[0], closure.A[0]); + Assert.Equal(others[1], closure.A[1]); + Assert.Equal(2, closure.A.Length); + } + + private static List GenerateOther(ClosureBuilder builder, int count) + { + var others = new List(); + for (int i = 0; i < count; i++) + { + var other = new object(); + builder.Add(Const(other)); + others.Add(other); + } + + return others; + } + + private static List> GenerateBlockHelpers(ClosureBuilder builder, int count) + { + var blockHelpers = new List>(); + for (int i = 0; i < count; i++) + { + var blockHelper = new StrongBox(); + builder.Add(Const(blockHelper)); + blockHelpers.Add(blockHelper); + } + + return blockHelpers; + } + + private static List> GenerateHelpers(ClosureBuilder builder, int count) + { + var helpers = new List>(); + for (int i = 0; i < count; i++) + { + var helper = new StrongBox(); + builder.Add(Const(helper)); + helpers.Add(helper); + } + + return helpers; + } + + private static List GeneratePaths(ClosureBuilder builder, int count) + { + var paths = new List(); + for (int i = 0; i < count; i++) + { + var pathInfo = PathInfoStore.Shared.GetOrAdd($"{i}"); + builder.Add(Const(pathInfo)); + paths.Add(pathInfo); + } + + return paths; + } + + private static ConstantExpression Const(T value) => Expression.Constant(value, typeof(T)); + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs index 4e736204..29ecd055 100644 --- a/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs +++ b/source/Handlebars.Test/Collections/FixedSizeDictionaryTests.cs @@ -111,7 +111,7 @@ public void Copy() { var key = keys[i]; var value = values[i]; - var index = indexes[i]; + destination.TryGetIndex(key, out var index); Assert.True(destination.ContainsKey(index)); Assert.True(destination.ContainsKey(key)); diff --git a/source/Handlebars.Test/CustomConfigurationTests.cs b/source/Handlebars.Test/CustomConfigurationTests.cs index f26b10b2..4b4e053e 100644 --- a/source/Handlebars.Test/CustomConfigurationTests.cs +++ b/source/Handlebars.Test/CustomConfigurationTests.cs @@ -1,4 +1,7 @@ using System; +using System.Globalization; +using System.IO; +using System.Text; using HandlebarsDotNet.Compiler.Resolvers; using Newtonsoft.Json; using Xunit; @@ -61,10 +64,19 @@ public void SnakeCaseInputModelNaming() private class JsonEncoder : ITextEncoder { - public string Encode(string value) + public void Encode(StringBuilder text, TextWriter target) { - return value != null ? JsonConvert.ToString(value, '"').Trim('"') : String.Empty; + target.Write(JsonConvert.ToString(text.ToString(), '"').Trim('"')); } + + public void Encode(string text, TextWriter target) + { + target.Write(JsonConvert.ToString(text, '"').Trim('"')); + } + + public bool ShouldEncode(char c) => true; + + public IFormatProvider FormatProvider { get; } = CultureInfo.InvariantCulture; } diff --git a/source/Handlebars.Test/EncodedTextWriterTests.cs b/source/Handlebars.Test/EncodedTextWriterTests.cs new file mode 100644 index 00000000..3e5a3019 --- /dev/null +++ b/source/Handlebars.Test/EncodedTextWriterTests.cs @@ -0,0 +1,38 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using HandlebarsDotNet.Compiler; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class EncodedTextWriterTests + { + public class DataGenerator : IEnumerable + { + private readonly List _data = new List + { + 1, 1F, 1D, 1L, (short)1, "1", UndefinedBindingResult.Create("undefined"), true, false, 1U, (ushort)1, 1UL, '1', (decimal)1 + }; + + public IEnumerator GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Theory] + [ClassData(typeof(DataGenerator))] + public void Write(object value) + { + var stringWriter = new StringWriter(); + static string Formatter(UndefinedBindingResult undefined) => undefined.ToString(); + + using var writer = new EncodedTextWriter(stringWriter, null, Formatter); + + writer.Write(value); + + Assert.Equal(value.ToString(), stringWriter.ToString()); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 17cadaac..8fda3b54 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -1,14 +1,14 @@ - full - netcoreapp3.1 - $(TargetFrameworks);net452;net461;net472 + netcoreapp2.1;netcoreapp3.1 + $(TargetFrameworks);net452;net46;net461;net472 6BA232A6-8C4D-4C7D-BD75-1844FE9774AF HandlebarsDotNet.Test false false false + true @@ -18,7 +18,10 @@ $(DefineConstants);netFramework - + + $(DefineConstants);netFramework + + $(DefineConstants);netFramework @@ -42,14 +45,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 01a9b96a..936ee0b9 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -7,6 +7,8 @@ using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Features; using HandlebarsDotNet.Helpers.BlockHelpers; +using HandlebarsDotNet; +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Test @@ -278,7 +280,7 @@ public void MissingHelperHook(string helperName) .RegisterMissingHelperHook( (context, arguments) => { - var name = arguments.Last().ToString(); + var name = arguments[arguments.Length - 1].ToString(); return string.Format(format, name.Trim('[', ']')); }); @@ -325,7 +327,7 @@ public void MissingHelperHookViaHelperRegistration(string helperName) var format = "Missing helper: {0}"; handlebars.RegisterHelper("helperMissing", (context, arguments) => { - var name = arguments.Last().ToString(); + var name = arguments[arguments.Length - 1].ToString(); return string.Format(format, name.Trim('[', ']')); }); @@ -495,11 +497,11 @@ public void BlockHelperWithArbitraryInversion() Handlebars.RegisterHelper("ifCond", (writer, options, context, arguments) => { if(arguments[0] == arguments[1]) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } }); @@ -559,43 +561,43 @@ public void BlockHelperWithArbitraryInversionAndComplexOperator() case ">": if (val1.Length > val2.Length) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "=": case "==": if (val1 == val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "<": if (val1.Length < val2.Length) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "!=": case "<>": if (val1 != val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; } @@ -610,43 +612,43 @@ public void BlockHelperWithArbitraryInversionAndComplexOperator() case ">": if (val1 > val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "=": case "==": if (val1 == val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "<": if (val1 < val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; case "!=": case "<>": if (val1 != val2) { - options.Template(writer, (object)context); + options.Template(writer, context); } else { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); } break; } @@ -795,7 +797,7 @@ public void EmptyBlockHelperWithInversion() var source = "{{#ifCond}}{{else}}Inverse{{/ifCond}}"; Handlebars.RegisterHelper("ifCond", (writer, options, context, arguments) => { - options.Inverse(writer, (object)context); + options.Inverse(writer, context); }); var data = new @@ -873,17 +875,17 @@ public CustomEachBlockHelper() : base("customEach") { } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments arguments) { using var frame = options.CreateFrame(); - frame.Data.CreateProperty(ChainSegment.Index, out var index); - frame.Data.CreateProperty(ChainSegment.Value, null, out var value); + var data = new DataValues(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; + data[ChainSegment.Index] = iterationIndex; + data[value] = item; frame.Value = item; options.Template(output, frame); diff --git a/source/Handlebars.Test/HtmlEncoderTests.cs b/source/Handlebars.Test/HtmlEncoderTests.cs index 1fa90d7e..889e88c8 100644 --- a/source/Handlebars.Test/HtmlEncoderTests.cs +++ b/source/Handlebars.Test/HtmlEncoderTests.cs @@ -1,8 +1,8 @@ -using HandlebarsDotNet; -using System; +using System.Globalization; +using System.IO; using Xunit; -namespace Handlebars.Test +namespace HandlebarsDotNet.Test { public class HtmlEncoderTests { @@ -15,19 +15,21 @@ public class HtmlEncoderTests [InlineData(">", ">")] [InlineData(" > "," > ")] [InlineData("�", "�")] + [InlineData("�a", "�a")] [InlineData("\"", """)] [InlineData("&a&", "&a&")] [InlineData("a&a", "a&a")] public void EncodeTest(string input, string expected) { // Arrange - var htmlEncoder = new HtmlEncoder(); + var htmlEncoder = new HtmlEncoder(CultureInfo.InvariantCulture); + using var writer = new StringWriter(); // Act - var result = htmlEncoder.Encode(input); + htmlEncoder.Encode(input, writer); // Assert - Assert.Equal(expected, result); + Assert.Equal(expected, writer.ToString()); } } } diff --git a/source/Handlebars.Test/NumericLiteralTests.cs b/source/Handlebars.Test/NumericLiteralTests.cs index d054218a..c9ab1683 100644 --- a/source/Handlebars.Test/NumericLiteralTests.cs +++ b/source/Handlebars.Test/NumericLiteralTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using HandlebarsDotNet.Compiler; using Xunit; namespace HandlebarsDotNet.Test @@ -10,8 +11,8 @@ public NumericLiteralTests() { Handlebars.RegisterHelper("numericLiteralAdd", (writer, context, args) => { - args = args.Select(a => (object)int.Parse(a.ToString())).ToArray(); - writer.Write(args.Aggregate(0, (a, i) => a + (int)i)); + var arr = args.AsEnumerable().Select(a => (object)int.Parse(a.ToString())); + writer.Write(arr.Aggregate(0, (a, i) => a + (int)i)); }); } diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs index 85bea987..f9d07a6d 100644 --- a/source/Handlebars.Test/PartialTests.cs +++ b/source/Handlebars.Test/PartialTests.cs @@ -599,7 +599,7 @@ public void TemplateWithSpecialNamedPartial() public class TestMissingPartialTemplateHandler : IMissingPartialTemplateHandler { - public void Handle(ICompiledHandlebarsConfiguration configuration, string partialName, TextWriter textWriter) + public void Handle(ICompiledHandlebarsConfiguration configuration, string partialName, in EncodedTextWriter textWriter) { textWriter.Write($"Partial Not Found: {partialName}"); } diff --git a/source/Handlebars.Test/SubExpressionTests.cs b/source/Handlebars.Test/SubExpressionTests.cs index c67f0813..2aa33e81 100644 --- a/source/Handlebars.Test/SubExpressionTests.cs +++ b/source/Handlebars.Test/SubExpressionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using HandlebarsDotNet.Compiler; using Xunit; namespace HandlebarsDotNet.Test @@ -10,19 +11,14 @@ public class SubExpressionTests [Fact] public void BasicSubExpression() { - var helperName = "helper-" + Guid.NewGuid().ToString(); //randomize helper name - var subHelperName = "subhelper-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(helperName, (writer, context, args) => { - writer.Write("Hello " + args[0]); - }); + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("helper", (context, args) => "Hello " + args[0]); - Handlebars.RegisterHelper(subHelperName, (writer, context, args) => { - writer.Write("world"); - }); + handlebars.RegisterHelper("subhelper", (context, args) => "world"); - var source = "{{" + helperName + " (" + subHelperName + ")}}"; + var source = "{{helper (subhelper)}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); @@ -34,19 +30,18 @@ public void BasicSubExpression() [Fact] public void BasicSubExpressionWithStringLiteralArgument() { - var helperName = "helper-" + Guid.NewGuid().ToString(); //randomize helper name - var subHelperName = "subhelper-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(helperName, (writer, context, args) => { - writer.Write("Outer " + args[0]); + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("helper", (writer, context, args) => { + writer.Write($"Outer {args[0]}"); }); - Handlebars.RegisterHelper(subHelperName, (writer, context, args) => { - writer.Write("Inner " + args[0]); + handlebars.RegisterHelper("subhelper", (writer, context, args) => { + writer.Write($"Inner {args[0]}"); }); - var source = "{{" + helperName + " (" + subHelperName + " 'inner-arg')}}"; + var source = "{{helper (subhelper 'inner-arg')}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); @@ -61,12 +56,12 @@ public void BasicSubExpressionWithHashArgument() var handlebars = Handlebars.Create(); handlebars.RegisterHelper("helper", (writer, context, args) => { - writer.Write("Outer " + args[0]); + writer.Write($"Outer {args[0]}"); }); - handlebars.RegisterHelper("subhelper", (writer, context, args) => { - var hash = args[0] as Dictionary; - writer.Write("Inner " + hash["item1"] + "-" + hash["item2"]); + handlebars.RegisterHelper("subhelper", (writer, context, args) => + { + writer.Write($"Inner {args["item1"]}-{args["item2"]}"); }); var source = "{{ helper (subhelper item1='inner' item2='arg')}}"; @@ -83,19 +78,13 @@ public void BasicSubExpressionWithHashArgument() [Fact] public void BasicSubExpressionWithNumericLiteralArguments() { - var helperName = "helper-" + Guid.NewGuid().ToString(); //randomize helper name - var subHelperName = "subhelper-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(helperName, (writer, context, args) => { - writer.Write("Math " + args[0]); - }); - - Handlebars.RegisterHelper(subHelperName, (writer, context, args) => { - writer.Write((int)args[0] + (int)args[1]); - }); + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("helper", (writer, context, args) => writer.Write($"Math {args[0]}")); + handlebars.RegisterHelper("subhelper", (writer, context, args) => writer.Write((int)args[0] + (int)args[1])); - var source = "{{" + helperName + " (" + subHelperName + " 1 2)}}"; + var source = "{{helper (subhelper 1 2)}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); @@ -107,19 +96,13 @@ public void BasicSubExpressionWithNumericLiteralArguments() [Fact] public void BasicSubExpressionWithPathArgument() { - var helperName = "helper-" + Guid.NewGuid().ToString(); //randomize helper name - var subHelperName = "subhelper-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(helperName, (writer, context, args) => { - writer.Write("Outer " + args[0]); - }); - - Handlebars.RegisterHelper(subHelperName, (writer, context, args) => { - writer.Write("Inner " + args[0]); - }); + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("helper", (writer, context, args) => writer.Write($"Outer {args[0]}")); + handlebars.RegisterHelper("subhelper", (writer, context, args) => writer.Write($"Inner {args[0]}")); - var source = "{{" + helperName + " (" + subHelperName + " property)}}"; + var source = "{{helper (subhelper property)}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { property = "inner-arg" @@ -133,19 +116,18 @@ public void BasicSubExpressionWithPathArgument() [Fact] public void TwoBasicSubExpressionsWithNumericLiteralArguments() { - var mathHelper = "math-" + Guid.NewGuid().ToString(); //randomize helper name - var addHelper = "add-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(mathHelper, (writer, context, args) => { + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("math", (writer, context, args) => { writer.Write("Math " + args[0] + " " + args[1]); }); - Handlebars.RegisterHelper(addHelper, (writer, context, args) => { + handlebars.RegisterHelper("add", (writer, context, args) => { writer.Write((int)args[0] + (int)args[1]); }); - var source = "{{" + mathHelper + " (" + addHelper + " 1 2) (" + addHelper + " 3 4)}}"; + var source = "{{math (add 1 2) (add 3 4)}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); @@ -157,19 +139,18 @@ public void TwoBasicSubExpressionsWithNumericLiteralArguments() [Fact] public void BasicSubExpressionWithNumericAndStringLiteralArguments() { - var writeHelper = "write-" + Guid.NewGuid().ToString(); //randomize helper name - var addHelper = "add-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(writeHelper, (writer, context, args) => { + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("write", (writer, context, args) => { writer.Write(args[0] + " " + args[1]); }); - Handlebars.RegisterHelper(addHelper, (writer, context, args) => { + handlebars.RegisterHelper("add", (writer, context, args) => { writer.Write((int)args[0] + (int)args[1]); }); - var source = "{{" + writeHelper + " (" + addHelper + " 1 2) \"hello\"}}"; + var source = "{{write (add 1 2) \"hello\"}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); @@ -181,20 +162,17 @@ public void BasicSubExpressionWithNumericAndStringLiteralArguments() [Fact] public void NestedSubExpressionsWithNumericLiteralArguments() { - var writeHelper = "write-" + Guid.NewGuid().ToString(); //randomize helper name - var addHelper = "add-" + Guid.NewGuid().ToString(); //randomize helper name - Handlebars.RegisterHelper(writeHelper, (writer, context, args) => { + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("write", (writer, context, args) => { writer.Write(args[0]); }); - Handlebars.RegisterHelper(addHelper, (writer, context, args) => { - args = args.Select(a => (object)int.Parse(a.ToString())).ToArray(); - writer.Write((int)args[0] + (int)args[1]); - }); + handlebars.RegisterHelper("add", (context, args) + => args.At(0) + args.At(1)); - var source = "{{" + writeHelper + " (" + addHelper + " (" + addHelper + " 1 2) 3 )}}"; + var source = "{{write (add (add 1 2) 3 )}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var output = template(new { }); diff --git a/source/Handlebars.Test/ViewEngine/CasparTests.cs b/source/Handlebars.Test/ViewEngine/CasparTests.cs index 7dad1332..16ff500d 100644 --- a/source/Handlebars.Test/ViewEngine/CasparTests.cs +++ b/source/Handlebars.Test/ViewEngine/CasparTests.cs @@ -17,11 +17,6 @@ public void CanRenderCasparIndexTemplate() var renderView = handlebars.CompileView("ViewEngine/Casper-master/index.hbs"); var output = renderView(new { - blog = new - { - url = "http://someblog.com", - title = "This is the blog title" - }, posts = new[] { new @@ -31,6 +26,13 @@ public void CanRenderCasparIndexTemplate() post_class = "somepostclass" } } + }, new + { + blog = new + { + url = "http://someblog.com", + title = "This is the blog title" + } }); var cq = CsQuery.CQ.CreateDocument(output); Assert.Equal("My Post Title", cq["h2.post-title a"].Text()); diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 39faec1d..47ff9c21 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -110,11 +110,8 @@ public void CanLoadAViewWithALayoutInTheRootWithAVariable() //Then the correct output should be rendered Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output); } - - /* @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")] + + [Fact] public void CanRenderAGlobalVariable() { //Given a layout in the root which contains an @ variable @@ -127,7 +124,7 @@ public void CanRenderAGlobalVariable() var handlebarsConfiguration = new HandlebarsConfiguration() {FileSystem = files}; var handlebars = Handlebars.Create(handlebarsConfiguration); var render = handlebars.CompileView("views\\someview.hbs"); - var output = render(new {@body = new {title = "THING"}}); + var output = render(null, new {body = new {title = "THING"}}); //Then the correct output should be rendered Assert.Equal("This is the THING", output); diff --git a/source/Handlebars/Arguments.cs b/source/Handlebars/Arguments.cs new file mode 100644 index 00000000..78a08707 --- /dev/null +++ b/source/Handlebars/Arguments.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet +{ + /// + /// Mimics behavior however in most cases does not require memory allocation. + /// + public readonly struct Arguments : IEquatable, IEnumerable + { + private readonly object[] _array; + private readonly bool _useArray; + + private readonly object _element0; + private readonly object _element1; + private readonly object _element2; + private readonly object _element3; + private readonly object _element4; + private readonly object _element5; + + public readonly int Length; + + /// + /// Ctor used to bypass struct limitations + /// + /// Should always by 0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Arguments(int dummy = 0) : this() + { + _useArray = false; + _array = null; + _element0 = null; + _element1 = null; + _element2 = null; + _element3 = null; + _element4 = null; + _element5 = null; + Length = dummy; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = null; + _element2 = null; + _element3 = null; + _element4 = null; + _element5 = null; + Length = 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1, object arg2) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = arg2; + _element2 = null; + _element3 = null; + _element4 = null; + _element5 = null; + Length = 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1, object arg2, object arg3) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = arg2; + _element2 = arg3; + _element3 = null; + _element4 = null; + _element5 = null; + Length = 3; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1, object arg2, object arg3, object arg4) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = arg2; + _element2 = arg3; + _element3 = arg4; + _element4 = null; + _element5 = null; + Length = 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1, object arg2, object arg3, object arg4, object arg5) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = arg2; + _element2 = arg3; + _element3 = arg4; + _element4 = arg5; + _element5 = null; + Length = 5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object arg1, object arg2, object arg3, object arg4, object arg5, object arg6) : this() + { + _useArray = false; + _array = null; + _element0 = arg1; + _element1 = arg2; + _element2 = arg3; + _element3 = arg4; + _element4 = arg5; + _element5 = arg6; + Length = 6; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Arguments(object[] args) : this() + { + _useArray = true; + _array = args; + Length = args.Length; + + _element0 = null; + _element1 = null; + _element2 = null; + _element3 = null; + _element4 = null; + _element5 = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerator GetEnumerator() => Enumerator.Create(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Returns without boxing + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable AsEnumerable() => new Enumerable(this); + + public IReadOnlyDictionary Hash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (Length == 0) return HashParameterDictionary.Empty; + return this[Length - 1] as HashParameterDictionary ?? HashParameterDictionary.Empty; + } + } + + public object this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if(index < 0 || index >= Length) throw new IndexOutOfRangeException(); + + if(_useArray) return _array[index]; + + return index switch + { + 0 => _element0, + 1 => _element1, + 2 => _element2, + 3 => _element3, + 4 => _element4, + 5 => _element5, + _ => throw new IndexOutOfRangeException() + }; + } + } + + public object this[string name] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Hash?[name]; + } + + [Pure] + public Arguments Add(object value) + { + if (Length <= 5) + { + return Length switch + { + 0 => new Arguments(value), + 1 => new Arguments(_element0, value), + 2 => new Arguments(_element0, _element1, value), + 3 => new Arguments(_element0, _element1, _element2, value), + 4 => new Arguments(_element0, _element1, _element2, _element3, value), + 5 => new Arguments(_element0, _element1, _element2, _element3, _element4, value), + _ => throw new IndexOutOfRangeException() + }; + } + + if (!_useArray) + { + var array = new[] + { + _element0, + _element1, + _element2, + _element3, + _element4, + _element5, + value + }; + + return new Arguments(array); + } + else + { + var array = new object[_array.Length + 1]; + for (var i = 0; i < _array.Length; i++) + { + array[i] = _array[i]; + } + + array[_array.Length] = value; + return new Arguments(array); + } + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T At(in int index) + { + var obj = this[index]; + if (obj is null) return default; + if (obj is T value) return value; + + var converter = TypeDescriptor.GetConverter(obj.GetType()); + return (T) converter.ConvertTo(obj, typeof(T)); + } + + public static implicit operator Arguments(object[] array) + { + return array.Length == 0 + ? new Arguments() + : new Arguments(array); + } + + public bool Equals(Arguments other) + { + if (_useArray && _useArray == other._useArray) + { + if (Length != other.Length || _array.Length != other._array.Length) return false; + for (int i = 0; i < _array.Length; i++) + { + if (!_array[i].Equals(other._array[i])) return false; + } + + return true; + } + + return Length == other.Length + && Equals(_element0, other._element0) + && Equals(_element1, other._element1) + && Equals(_element2, other._element2) + && Equals(_element3, other._element3) + && Equals(_element4, other._element4) + && Equals(_element5, other._element5); + } + + public override bool Equals(object obj) + { + return obj is Arguments other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (_array != null ? _array.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ _useArray.GetHashCode(); + hashCode = (hashCode * 397) ^ (_element0 != null ? _element0.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_element1 != null ? _element1.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_element2 != null ? _element2.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_element3 != null ? _element3.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_element4 != null ? _element4.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_element5 != null ? _element5.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ Length; + return hashCode; + } + } + + private sealed class Enumerable : IEnumerable + { + private readonly Arguments _arguments; + + public Enumerable(in Arguments arguments) => _arguments = arguments; + + public IEnumerator GetEnumerator() => Enumerator.Create(_arguments); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + } + + private sealed class Enumerator : IEnumerator + { + private static readonly InternalObjectPool Pool = new InternalObjectPool(new Policy()); + + public static Enumerator Create(in Arguments arguments) + { + var enumerator = Pool.Get(); + enumerator._index = -1; + enumerator._arguments = arguments; + + return enumerator; + } + + private Arguments _arguments; + + private int _index; + + private Enumerator() + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => ++_index < _arguments.Length; + + public void Reset() => _index = -1; + + public object Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_index == -1) return null; + if(_arguments._useArray) return _arguments._array[_index]; + + return _index switch + { + 0 => _arguments._element0, + 1 => _arguments._element1, + 2 => _arguments._element2, + 3 => _arguments._element3, + 4 => _arguments._element4, + 5 => _arguments._element5, + _ => throw new ArgumentOutOfRangeException() + }; + } + } + + public void Dispose() + { + _arguments = new Arguments(); + Pool.Return(this); + } + + private class Policy : IInternalObjectPoolPolicy + { + public Enumerator Create() => new Enumerator(); + + public bool Return(Enumerator item) => true; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/FixedSizeDictionary.cs b/source/Handlebars/Collections/FixedSizeDictionary.cs index 18808f5e..5632752c 100644 --- a/source/Handlebars/Collections/FixedSizeDictionary.cs +++ b/source/Handlebars/Collections/FixedSizeDictionary.cs @@ -31,7 +31,6 @@ public class FixedSizeDictionary : 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; @@ -58,9 +57,6 @@ public FixedSizeDictionary(int bucketsCount, int bucketSize, TComparer comparer) _bucketSize = FindClosestPrime(bucketSize); _version = 1; - _buckets = new Bucket[bucketsCount]; - InitializeBuckets(_buckets); - _entries = new Entry[bucketsCount * bucketSize]; _indexes = new EntryIndex[bucketsCount * bucketSize]; @@ -87,14 +83,6 @@ static int FindClosestPrime(int bucketSize) 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; @@ -112,22 +100,22 @@ static void InitializeBuckets(Bucket[] buckets) /// public bool TryGetIndex(TKey key, out EntryIndex index) { + if (_count == 0) + { + index = default; + return false; + } + 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); + index = new EntryIndex(entryIndex, _version, this); return true; } @@ -138,7 +126,7 @@ public bool TryGetIndex(TKey key, out EntryIndex index) if (!entry.IsNotDefault) break; if (entry.Version == _version && (hash != entry.Hash || !_comparer.Equals(key, entry.Key))) continue; - index = new EntryIndex(entry.Index, _version); + index = new EntryIndex(entry.Index, _version, this); return true; } @@ -153,19 +141,19 @@ 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; + return keyIndex.Version == _version && ReferenceEquals(keyIndex.Producer, this); } /// /// Checks key existence at best O(1) and worst O(m) where 'm' is number of collisions /// - public bool ContainsKey(in TKey key) + public bool ContainsKey(TKey key) { + if (_count == 0) return false; + 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); @@ -195,7 +183,7 @@ public bool ContainsKey(in TKey key) /// public bool TryGetValue(in EntryIndex keyIndex, out TValue value) { - if (keyIndex.Version != _version) + if (_count == 0 || keyIndex.Version != _version || !ReferenceEquals(keyIndex.Producer, this)) { value = default; return false; @@ -217,15 +205,15 @@ public bool TryGetValue(in EntryIndex keyIndex, out TValue value) /// public bool TryGetValue(in TKey key, out TValue value) { - var hash = _comparer.GetHashCode(key); - var bucketIndex = hash & _bucketMask; - - if (_buckets[bucketIndex].Version != _version) + if (_count == 0) { value = default; return false; } - + + var hash = _comparer.GetHashCode(key); + var bucketIndex = hash & _bucketMask; + var inBucketEntryIndex = hash % _bucketSize; var entryIndex = bucketIndex * _bucketSize + Math.Abs(inBucketEntryIndex); @@ -271,32 +259,22 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde { 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); + index = new EntryIndex(entryIndex, _version, this); _indexes[_count++] = index; return; } if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) { - index = new EntryIndex(entryIndex, _version); + index = new EntryIndex(entryIndex, _version, this); _entries[entryIndex].Value = value; return; } @@ -308,14 +286,14 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde if (entry.Version != _version) { _entries[entry.Index] = new Entry(hash, entry.Index, key, value, _version); - index = new EntryIndex(entry.Index, _version); + index = new EntryIndex(entry.Index, _version, this); _indexes[_count++] = index; return; } if (hash == entry.Hash && _comparer.Equals(key, entry.Key)) { - index = new EntryIndex(entry.Index, _version); + index = new EntryIndex(entry.Index, _version, this); _entries[entry.Index].Value = value; return; } @@ -334,7 +312,7 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde entryReference.Next = entryIndex; _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); - index = new EntryIndex(entryIndex, _version); + index = new EntryIndex(entryIndex, _version, this); _indexes[_count++] = index; return; } @@ -347,7 +325,7 @@ public void AddOrReplace(in TKey key, in TValue value, out EntryIndex inde entryReference.Next = entryIndex; _entries[entryIndex] = new Entry(hash, entryIndex, key, value, _version); - index = new EntryIndex(entryIndex, _version); + index = new EntryIndex(entryIndex, _version, this); _indexes[_count++] = index; return; } @@ -364,14 +342,14 @@ public TValue this[in EntryIndex entryIndex] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (entryIndex.Version != _version) return default; + if (entryIndex.Version != _version || !ReferenceEquals(entryIndex.Producer, this)) return default; return _entries[entryIndex.Index].Value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { - if (entryIndex.Version != _version) return; + if (entryIndex.Version != _version || !ReferenceEquals(entryIndex.Producer, this)) return; _entries[entryIndex.Index].Value = value; } } @@ -386,25 +364,27 @@ 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]; - } + //destination._version++; + if(_count == 0) return; + for (var index = 0; index < _indexes.Length; index++) { var idx = _indexes[index]; - if(idx.Version != _version || !idx.IsNotEmpty) break; + if (idx.Version != _version || !idx.IsNotEmpty) + { + destination._indexes[index] = new EntryIndex(idx.Index, destination._version, destination); + break; + } - var entryIndex = _entries[idx.Index]; - if(!entryIndex.IsNotDefault || entryIndex.Version != _version) continue; + var entry = _entries[idx.Index]; + if(!entry.IsNotDefault || entry.Version != _version) continue; - destination._entries[idx.Index] = entryIndex; - destination[entryIndex.Index] = _entries[entryIndex.Index]; - destination._indexes[index] = idx; + destination._indexes[index] = new EntryIndex(idx.Index, destination._version, destination); + destination._entries[idx.Index] = new Entry(entry, destination._version); } + + destination._count = _count; } /// @@ -457,12 +437,6 @@ public void OptionalClear() 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++) @@ -478,8 +452,6 @@ public IEnumerator> GetEnumerator() 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] @@ -530,7 +502,7 @@ private struct Entry public TValue Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Entry(in int hash, in int index, in TKey key, in TValue value, byte version) + internal Entry(in int hash, in int index, in TKey key, in TValue value, in byte version) { Index = index; Hash = hash; @@ -540,30 +512,35 @@ internal Entry(in int hash, in int index, in TKey key, in TValue value, byte ver IsNotDefault = true; Next = -1; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Entry(Entry entry, byte version) + { + Index = entry.Index; + Hash = entry.Hash; + Key = entry.Key; + Value = entry.Value; + Version = version; + Next = entry.Next; + IsNotDefault = true; + } 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 object Producer; public readonly bool IsNotEmpty; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal EntryIndex(in int index, in byte version) + internal EntryIndex(in int index, in byte version, object producer) { Version = version; + Producer = producer; Index = index; IsNotEmpty = true; } diff --git a/source/Handlebars/Collections/ObjectPool.cs b/source/Handlebars/Collections/ObjectPool.cs index 1dbf6124..206ba94a 100644 --- a/source/Handlebars/Collections/ObjectPool.cs +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; namespace HandlebarsDotNet @@ -15,12 +14,10 @@ internal class InternalObjectPool where T: class { private readonly IInternalObjectPoolPolicy _policy; - private ConcurrentQueue _queue = new ConcurrentQueue(); + private readonly ConcurrentQueue _queue = new ConcurrentQueue(); public InternalObjectPool(IInternalObjectPoolPolicy policy) { - Handlebars.Disposables.Enqueue(new Disposer(this)); - _policy = policy; for (var i = 0; i < 5; i++) Return(_policy.Create()); @@ -41,15 +38,6 @@ public void Return(T obj) if (!_policy.Return(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 diff --git a/source/Handlebars/Collections/SafeDeferredValue.cs b/source/Handlebars/Collections/SafeDeferredValue.cs new file mode 100644 index 00000000..f648324d --- /dev/null +++ b/source/Handlebars/Collections/SafeDeferredValue.cs @@ -0,0 +1,40 @@ +using System; + +namespace HandlebarsDotNet.Collections +{ + internal class SafeDeferredValue + { + private readonly object _lock = new object(); + + private readonly TState _state; + private readonly Func _factory; + + private T _value; + private volatile bool _isValueCreated; + + public SafeDeferredValue(TState state, Func factory) + { + _state = state; + _factory = factory; + } + + public T Value + { + get + { + if (_isValueCreated) return _value; + + lock (_lock) + { + if (_isValueCreated) return _value; + + _value = _factory(_state); + _isValueCreated = true; + return _value; + } + } + } + + public void Reset() => _isValueCreated = false; + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/ClosureBuilder.cs b/source/Handlebars/Compiler/ClosureBuilder.cs new file mode 100644 index 00000000..102276c8 --- /dev/null +++ b/source/Handlebars/Compiler/ClosureBuilder.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.Helpers.BlockHelpers; + +namespace HandlebarsDotNet.Compiler +{ + public class ClosureBuilder + { + private readonly List> _pathInfos = new List>(); + private readonly List> _templateDelegates = new List>(); + private readonly List> _blockParams = new List>(); + private readonly List>> _helpers = new List>>(); + private readonly List>> _blockHelpers = new List>>(); + private readonly List> _other = new List>(); + + public void Add(ConstantExpression constantExpression) + { + if (constantExpression.Type == typeof(PathInfo)) + { + _pathInfos.Add(new KeyValuePair(constantExpression, (PathInfo) constantExpression.Value)); + } + else if (constantExpression.Type == typeof(StrongBox)) + { + _helpers.Add(new KeyValuePair>(constantExpression, (StrongBox) constantExpression.Value)); + } + else if (constantExpression.Type == typeof(StrongBox)) + { + _blockHelpers.Add(new KeyValuePair>(constantExpression, (StrongBox) constantExpression.Value)); + } + else if (constantExpression.Type == typeof(TemplateDelegate)) + { + _templateDelegates.Add(new KeyValuePair(constantExpression, (TemplateDelegate) constantExpression.Value)); + } + else if (constantExpression.Type == typeof(ChainSegment[])) + { + _blockParams.Add(new KeyValuePair(constantExpression, (ChainSegment[]) constantExpression.Value)); + } + else + { + _other.Add(new KeyValuePair(constantExpression, constantExpression.Value)); + } + } + + public KeyValuePair> Build(out Closure closure) + { + var closureType = typeof(Closure); + var constructor = closureType + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(); + + var arguments = new List(); + + BuildKnownValues(arguments, _pathInfos, 4); + BuildKnownValues(arguments, _helpers, 4); + BuildKnownValues(arguments, _blockHelpers, 4); + BuildKnownValues(arguments, _templateDelegates, 4); + BuildKnownValues(arguments, _blockParams, 1); + arguments.Add(_other.Select(o => o.Value).ToArray()); + + closure = (Closure) constructor.Invoke(arguments.ToArray()); + + var mapping = new Dictionary(); + + var closureExpression = Expression.Variable(typeof(Closure), "closure"); + + BuildKnownValuesExpressions(closureExpression, mapping, _pathInfos, "PI", 4); + BuildKnownValuesExpressions(closureExpression, mapping, _helpers, "HD", 4); + BuildKnownValuesExpressions(closureExpression, mapping, _blockHelpers, "BHD", 4); + BuildKnownValuesExpressions(closureExpression, mapping, _templateDelegates, "TD", 4); + BuildKnownValuesExpressions(closureExpression, mapping, _blockParams, "BP", 1); + + var arrayField = closureType.GetField("A"); + var array = Expression.Field(closureExpression, arrayField!); + for (int index = 0; index < _other.Count; index++) + { + var indexExpression = Expression.ArrayAccess(array, Expression.Constant(index)); + mapping.Add(_other[index].Key, indexExpression); + } + + return new KeyValuePair>(closureExpression, mapping); + } + + private static void BuildKnownValues(List arguments, List> knowValues, int fieldsCount) + where T: class + { + for (var index = 0; index < fieldsCount; index++) + { + arguments.Add(knowValues.ElementAtOrDefault(index).Value); + } + + arguments.Add(knowValues.Count > fieldsCount ? knowValues.Skip(fieldsCount).Select(o => o.Value).ToArray() : null); + } + + private static void BuildKnownValuesExpressions(Expression closure, Dictionary expressions, List> knowValues, string prefix, int fieldsCount) + where T: class + { + var type = typeof(Closure); + for (var index = 0; index < fieldsCount && index < knowValues.Count; index++) + { + var field = type.GetField($"{prefix}{index}"); + expressions.Add(knowValues[index].Key, Expression.Field(closure, field!)); + } + + var arrayField = type.GetField($"{prefix}A"); + var array = Expression.Field(closure, arrayField!); + for (int index = fieldsCount, arrayIndex = 0; index < knowValues.Count; index++, arrayIndex++) + { + var indexExpression = Expression.ArrayAccess(array, Expression.Constant(arrayIndex)); + expressions.Add(knowValues[index].Key, indexExpression); + } + } + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class Closure + { + public readonly PathInfo PI0; + public readonly PathInfo PI1; + public readonly PathInfo PI2; + public readonly PathInfo PI3; + public readonly PathInfo[] PIA; + + public readonly StrongBox HD0; + public readonly StrongBox HD1; + public readonly StrongBox HD2; + public readonly StrongBox HD3; + public readonly StrongBox[] HDA; + + public readonly StrongBox BHD0; + public readonly StrongBox BHD1; + public readonly StrongBox BHD2; + public readonly StrongBox BHD3; + public readonly StrongBox[] BHDA; + + public readonly TemplateDelegate TD0; + public readonly TemplateDelegate TD1; + public readonly TemplateDelegate TD2; + public readonly TemplateDelegate TD3; + public readonly TemplateDelegate[] TDA; + + public readonly ChainSegment[] BP0; + public readonly ChainSegment[][] BPA; + + public readonly object[] A; + + internal Closure(PathInfo pi0, PathInfo pi1, PathInfo pi2, PathInfo pi3, PathInfo[] pia, StrongBox hd0, StrongBox hd1, StrongBox hd2, StrongBox hd3, StrongBox[] hda, StrongBox bhd0, StrongBox bhd1, StrongBox bhd2, StrongBox bhd3, StrongBox[] bhda, TemplateDelegate td0, TemplateDelegate td1, TemplateDelegate td2, TemplateDelegate td3, TemplateDelegate[] tda, ChainSegment[] bp0, ChainSegment[][] bpa, object[] a) + { + PI0 = pi0; + PI1 = pi1; + PI2 = pi2; + PI3 = pi3; + PIA = pia; + HD0 = hd0; + HD1 = hd1; + HD2 = hd2; + HD3 = hd3; + HDA = hda; + BHD0 = bhd0; + BHD1 = bhd1; + BHD2 = bhd2; + BHD3 = bhd3; + BHDA = bhda; + TD0 = td0; + TD1 = td1; + TD2 = td2; + TD3 = td3; + TDA = tda; + BP0 = bp0; + BPA = bpa; + A = a; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/CompilationContext.cs b/source/Handlebars/Compiler/CompilationContext.cs index dbf5a5a9..f7ad75be 100644 --- a/source/Handlebars/Compiler/CompilationContext.cs +++ b/source/Handlebars/Compiler/CompilationContext.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { @@ -7,11 +8,31 @@ internal sealed class CompilationContext public CompilationContext(ICompiledHandlebarsConfiguration configuration) { Configuration = configuration; - BindingContext = Expression.Variable(typeof(BindingContext), "context"); + BindingContext = Expression.Parameter(typeof(BindingContext), "context"); + EncodedWriter = Expression.Parameter(typeof(EncodedTextWriter).MakeByRefType(), "writer"); + + Args = new CompilationContextArgs(this); } public ICompiledHandlebarsConfiguration Configuration { get; } public ParameterExpression BindingContext { get; } + + public ParameterExpression EncodedWriter { get; } + + public CompilationContextArgs Args { get; } + + internal class CompilationContextArgs + { + public CompilationContextArgs(CompilationContext context) + { + BindingContext = new ExpressionContainer(context.BindingContext); + EncodedWriter = new ExpressionContainer(context.EncodedWriter); + } + + public ExpressionContainer BindingContext { get; } + + public ExpressionContainer EncodedWriter { get; } + } } } diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index 0f07a759..c2f110ec 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -1,20 +1,16 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; +using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal static class FunctionBuilder { - private static readonly Expression> EmptyLambda = - Expression.Lambda>( - Expression.Empty(), - Expression.Parameter(typeof(TextWriter)), - Expression.Parameter(typeof(object))); - - private static readonly Action EmptyLambdaWithContext = (context, writer, arg3) => {}; + private static readonly TemplateDelegate EmptyLambda = + (in EncodedTextWriter writer, BindingContext context) => { }; public static Expression Reduce(Expression expression, CompilationContext context) { @@ -33,64 +29,44 @@ public static Expression Reduce(Expression expression, CompilationContext contex return expression; } - public static Action CompileCore(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration, string templatePath = null) + public static ExpressionContainer CreateExpression(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration) { try { - if (!expressions.Any()) + var enumerable = expressions as Expression[] ?? expressions.ToArray(); + if (!enumerable.Any()) { - return EmptyLambdaWithContext; + return Arg(EmptyLambda); } - if (expressions.IsOneOf()) + if (enumerable.IsOneOf()) { - return EmptyLambdaWithContext; + return Arg(EmptyLambda); } var context = new CompilationContext(configuration); - var expression = (Expression) Expression.Block(expressions); + var expression = (Expression) Expression.Block(enumerable); expression = Reduce(expression, context); - var lambda = ContextBinder.Bind(context, expression, templatePath); - return configuration.ExpressionCompiler.Compile(lambda); + return Arg(ContextBinder.Bind(context, expression)); } catch (Exception ex) { throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); } } - - public static Expression> CompileCore(IEnumerable expressions, Expression parentContext, ICompiledHandlebarsConfiguration configuration, string templatePath = null) + + public static TemplateDelegate Compile(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration) { try { - if (!expressions.Any()) + var expression = CreateExpression(expressions, configuration); + if (expression.Expression is ConstantExpression constantExpression) { - return EmptyLambda; + return (TemplateDelegate) constantExpression.Value; } - if (expressions.IsOneOf()) - { - return EmptyLambda; - } - - var context = new CompilationContext(configuration); - var expression = (Expression) Expression.Block(expressions); - expression = Reduce(expression, context); - return ContextBinder.Bind(context, expression, parentContext, templatePath); - } - catch (Exception ex) - { - throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); - } - } - - public static Action Compile(IEnumerable expressions, ICompiledHandlebarsConfiguration configuration, string templatePath = null) - { - try - { - var expression = CompileCore(expressions, null, configuration, templatePath); - - return configuration.ExpressionCompiler.Compile(expression); + var lambda = (Expression) expression.Expression; + return configuration.ExpressionCompiler.Compile(lambda); } catch (Exception ex) { diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index c06155e0..1d7ec051 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -using System.Dynamic; -using System.IO; using System.Linq; -using System.Reflection; using HandlebarsDotNet.Compiler.Lexer; namespace HandlebarsDotNet.Compiler { + public delegate void TemplateDelegate(in EncodedTextWriter writer, BindingContext context); + internal static class HandlebarsCompiler { - public static Action Compile(ExtendedStringReader source, ICompiledHandlebarsConfiguration configuration) + public static TemplateDelegate Compile(ExtendedStringReader source, ICompiledHandlebarsConfiguration configuration) { var createdFeatures = configuration.Features; for (var index = 0; index < createdFeatures.Count; index++) @@ -31,7 +30,7 @@ public static Action Compile(ExtendedStringReader source, IC return action; } - internal static Action CompileView(ViewReaderFactory readerFactoryFactory, string templatePath, ICompiledHandlebarsConfiguration configuration) + internal static TemplateDelegate CompileView(ViewReaderFactory readerFactoryFactory, string templatePath, ICompiledHandlebarsConfiguration configuration) { IEnumerable tokens; using (var sr = readerFactoryFactory(configuration, templatePath)) @@ -46,69 +45,30 @@ internal static Action CompileView(ViewReaderFactory readerF var expressionBuilder = new ExpressionBuilder(configuration); var expressions = expressionBuilder.ConvertTokensToExpressions(tokens); - var compiledView = FunctionBuilder.Compile(expressions, configuration, templatePath); + var compiledView = FunctionBuilder.Compile(expressions, configuration); if (layoutToken == null) return compiledView; var fs = configuration.FileSystem; var layoutPath = fs.Closest(templatePath, layoutToken.Value + ".hbs"); if (layoutPath == null) - throw new InvalidOperationException("Cannot find layout '" + layoutPath + "' for template '" + - templatePath + "'"); + throw new InvalidOperationException($"Cannot find layout '{layoutToken.Value}' for template '{templatePath}'"); var compiledLayout = CompileView(readerFactoryFactory, layoutPath, configuration); - - return (tw, vm) => - { - string inner; - using (var innerWriter = ReusableStringWriter.Get(configuration.FormatProvider)) - { - compiledView(innerWriter, vm); - inner = innerWriter.ToString(); - } - - compiledLayout(tw, new DynamicViewModel(new[] {new {body = inner}, vm})); - }; - } - - internal class DynamicViewModel : DynamicObject - { - private readonly object[] _objects; - private static readonly BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - - public DynamicViewModel(params object[] objects) + + return (in EncodedTextWriter writer, BindingContext context) => { - _objects = objects; - } + var config = context.Configuration; + using var innerWriter = ReusableStringWriter.Get(config.FormatProvider); + using var textWriter = new EncodedTextWriter(innerWriter, config.TextEncoder, config.UnresolvedBindingFormat, true); + compiledView(textWriter, context); + var inner = innerWriter.ToString(); - public override IEnumerable GetDynamicMemberNames() - { - return _objects.Select(o => o.GetType()) - .SelectMany(t => t.GetMembers(BindingFlags)) - .Select(m => m.Name); - } + var vmContext = new [] {new {body = inner}, context.Value}; + var viewModel = new DynamicViewModel(vmContext); + using var bindingContext = BindingContext.Create(config, viewModel); - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - result = null; - foreach (var target in _objects) - { - var member = target.GetType().GetMember(binder.Name, BindingFlags); - if (member.Length > 0) - { - if (member[0] is PropertyInfo) - { - result = ((PropertyInfo)member[0]).GetValue(target, null); - return true; - } - if (member[0] is FieldInfo) - { - result = ((FieldInfo)member[0]).GetValue(target); - return true; - } - } - } - return false; - } + compiledLayout(writer, bindingContext); + }; } } } diff --git a/source/Handlebars/IExpressionCompiler.cs b/source/Handlebars/Compiler/IExpressionCompiler.cs similarity index 68% rename from source/Handlebars/IExpressionCompiler.cs rename to source/Handlebars/Compiler/IExpressionCompiler.cs index e557dbfc..22f9400c 100644 --- a/source/Handlebars/IExpressionCompiler.cs +++ b/source/Handlebars/Compiler/IExpressionCompiler.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.Contracts; using System.Linq.Expressions; namespace HandlebarsDotNet @@ -8,6 +9,7 @@ namespace HandlebarsDotNet /// public interface IExpressionCompiler { - T Compile(Expression expression) where T: class; + [Pure] + T Compile(Expression expression) where T: class, Delegate; } } \ No newline at end of file diff --git a/source/Handlebars/IExpressionMiddleware.cs b/source/Handlebars/Compiler/IExpressionMiddleware.cs similarity index 66% rename from source/Handlebars/IExpressionMiddleware.cs rename to source/Handlebars/Compiler/IExpressionMiddleware.cs index 0f3b7640..ffe63b36 100644 --- a/source/Handlebars/IExpressionMiddleware.cs +++ b/source/Handlebars/Compiler/IExpressionMiddleware.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics.Contracts; using System.Linq.Expressions; namespace HandlebarsDotNet @@ -7,6 +9,7 @@ namespace HandlebarsDotNet /// public interface IExpressionMiddleware { - Expression Invoke(Expression expression); + [Pure] + Expression Invoke(Expression expression) where T: Delegate; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs index f6187c07..904b86c5 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using System.Linq; using System.Text; +using HandlebarsDotNet.StringUtils; namespace HandlebarsDotNet.Compiler.Lexer { diff --git a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs new file mode 100644 index 00000000..f7088235 --- /dev/null +++ b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using static Expressions.Shortcuts.ExpressionShortcuts; + +namespace HandlebarsDotNet.Compiler.Middlewares +{ + internal class ClosureExpressionMiddleware : IExpressionMiddleware + { + public Expression Invoke(Expression expression) where T : Delegate + { + var constants = new List(); + var closureCollectorVisitor = new ClosureCollectorVisitor(constants); + expression = (Expression) closureCollectorVisitor.Visit(expression); + + if (constants.Count == 0) return expression; + + var closureBuilder = new ClosureBuilder(); + for (var index = 0; index < constants.Count; index++) + { + var value = constants[index]; + closureBuilder.Add(value); + } + + var closureDefinition = closureBuilder.Build(out var closure); + + var closureVisitor = new ClosureVisitor(closureDefinition); + expression = (Expression) closureVisitor.Visit(expression); + + var block = Block() + .Parameter(closureDefinition.Key) + .Line(Expression.Assign(closureDefinition.Key, Arg(closure))); + + if (expression!.Body is BlockExpression blockExpression) + { + var variables = blockExpression.Variables; + for (var index = 0; index < blockExpression.Variables.Count; index++) + { + block.Parameter(variables[index]); + } + + block.Lines(blockExpression.Expressions); + } + else + { + block.Line(expression!.Body); + } + + var lambda = block.Lambda(expression.Parameters); + return lambda; + } + + private class ClosureVisitor : ExpressionVisitor + { + private readonly Dictionary _expressions; + + public ClosureVisitor(KeyValuePair> closureDefinition) + { + _expressions = closureDefinition.Value; + } + + protected override Expression VisitConstant(ConstantExpression node) + { + switch (node.Value) + { + case null: + case string _: + return node; + + default: + if (node.Type.GetTypeInfo().IsValueType) return node; + break; + } + + if (_expressions.TryGetValue(node, out var replacement)) + { + if (node.Type != replacement.Type) + { + return Expression.Convert(replacement, node.Type); + } + + return replacement; + } + + return base.VisitConstant(node); + } + } + + private class ClosureCollectorVisitor : ExpressionVisitor + { + private readonly List _expressions; + + public ClosureCollectorVisitor(List expressions) + { + _expressions = expressions; + } + + protected override Expression VisitLambda(Expression node) + { + var body = Visit(node.Body); + var expression = body; + if (expression == null) throw new InvalidOperationException("Cannot create closure"); + + return node.Update(expression, node.Parameters); + } + + protected override Expression VisitConstant(ConstantExpression node) + { + switch (node.Value) + { + case null: + case string _: + return node; + + default: + if (node.Type.GetTypeInfo().IsValueType) return node; + break; + } + + _expressions.Add(node); + + return node; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (!(node.Expression is ConstantExpression constantExpression)) return base.VisitMember(node); + + switch (node.Member) + { + case PropertyInfo property: + { + var value = property.GetValue(constantExpression.Value); + return VisitConstant(Expression.Constant(value, property.PropertyType)); + } + + case FieldInfo field: + { + var value = field.GetValue(constantExpression.Value); + return VisitConstant(Expression.Constant(value, field.FieldType)); + } + + default: + { + var constant = VisitConstant(constantExpression); + return node.Update(constant); + } + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs new file mode 100644 index 00000000..147c1877 --- /dev/null +++ b/source/Handlebars/Compiler/Middlewares/ExpressionOptimizerMiddleware.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.Compiler.Middlewares +{ + internal class ExpressionOptimizerMiddleware : IExpressionMiddleware + { + public Expression Invoke(Expression expression) where T : Delegate + { + var visitor = new OptimizationVisitor(); + return (Expression) visitor.Visit(expression); + } + + private class OptimizationVisitor : ExpressionVisitor + { + private readonly Dictionary _constantExpressions = new Dictionary(); + + protected override Expression VisitBlock(BlockExpression node) + { + if (node.Variables.Count == 0 && node.Expressions.Count == 1 && node.Expressions[0] is BlockExpression blockExpression) + { + return VisitBlock(blockExpression); + } + + return base.VisitBlock(node); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + switch (node.NodeType) + { + case ExpressionType.Convert: + if (node.Operand.Type == node.Type) + { + return node.Operand; + } + break; + } + + return base.VisitUnary(node); + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if(node.Value != null && _constantExpressions.TryGetValue(node.Value, out var storedNode)) + { + return storedNode; + } + + if (node.Value != null) + { + _constantExpressions.Add(node.Value, node); + } + + return node; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs b/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs index c040c350..ddeb4367 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.Pool.cs @@ -1,5 +1,4 @@ using System; -using System.IO; namespace HandlebarsDotNet.Compiler { @@ -7,17 +6,15 @@ 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) + internal static BindingContext Create(ICompiledHandlebarsConfiguration configuration, object value, string templatePath = null) { - return Pool.CreateContext(configuration, value, writer, parent, templatePath, null); + return Pool.CreateContext(configuration, value, null, templatePath, null); } - internal static BindingContext Create(ICompiledHandlebarsConfiguration configuration, object value, - EncodedTextWriter writer, BindingContext parent, string templatePath, - Action partialBlockTemplate) + internal static BindingContext Create(ICompiledHandlebarsConfiguration configuration, object value, BindingContext parent, string templatePath, + TemplateDelegate partialBlockTemplate) { - return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate); + return Pool.CreateContext(configuration, value, parent, templatePath, partialBlockTemplate); } public void Dispose() => Pool.Return(this); @@ -28,12 +25,11 @@ public BindingContextPool() : base(new BindingContextPolicy()) { } - public BindingContext CreateContext(ICompiledHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate) + public BindingContext CreateContext(ICompiledHandlebarsConfiguration configuration, object value, BindingContext parent, string templatePath, TemplateDelegate partialBlockTemplate) { var context = Get(); context.Configuration = configuration; context.Value = value; - context.TextWriter = writer; context.ParentContext = parent; context.TemplatePath = templatePath; context.PartialBlockTemplate = partialBlockTemplate; @@ -53,10 +49,11 @@ public bool Return(BindingContext item) item.Value = null; item.ParentContext = null; item.TemplatePath = null; - item.TextWriter = null; item.PartialBlockTemplate = null; item.InlinePartialTemplates.Clear(); + item.RootDataObject.Clear(); + item.Extensions.OptionalClear(); item.BlockParamsObject.OptionalClear(); item.ContextDataObject.OptionalClear(); diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 46eacc5f..b2102418 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -1,9 +1,8 @@ using System; -using System.IO; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.ObjectDescriptors; -using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { @@ -12,33 +11,49 @@ public sealed partial class BindingContext : IDisposable internal readonly EntryIndex[] WellKnownVariables = new EntryIndex[8]; private readonly DeferredValue _objectDescriptor; - + private BindingContext() { - InlinePartialTemplates = new CascadeDictionary>(); + InlinePartialTemplates = new CascadeDictionary>(); + RootDataObject = new FixedSizeDictionary(16, 7, ChainSegment.EqualityComparer); 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 RootDataObject { get; } internal FixedSizeDictionary ContextDataObject { get; } internal FixedSizeDictionary BlockParamsObject { get; } + internal void SetDataObject(object data) + { + if(data == null) return; + + var objectDescriptor = ObjectDescriptor.Create(data, Configuration); + var objectAccessor = new ObjectAccessor(data, objectDescriptor); + + foreach (var property in objectAccessor.Properties) + { + var value = objectAccessor[property]; + RootDataObject.AddOrReplace(property, value, out _); + ContextDataObject.AddOrReplace(property, value, out _); + } + } + private void Initialize() { Root = ParentContext?.Root ?? this; + if(!ReferenceEquals(Root, this)) Root.RootDataObject.CopyTo(ContextDataObject); ContextDataObject.AddOrReplace(ChainSegment.Root, Root.Value, out WellKnownVariables[(int) WellKnownVariable.Root]); if (ParentContext == null) { ContextDataObject.AddOrReplace( ChainSegment.Parent, - ChainSegment.Parent.GetUndefinedBindingResult(Configuration), + UndefinedBindingResult.Create(ChainSegment.Parent), out WellKnownVariables[(int) WellKnownVariable.Parent] ); @@ -68,23 +83,15 @@ out WellKnownVariables[(int) WellKnownVariable.Parent] PopulateHash(dictionary, ParentContext.Value, Configuration); } + internal FixedSizeDictionary Extensions { get; } = new FixedSizeDictionary(8, 7, StringComparer.OrdinalIgnoreCase); + internal string TemplatePath { get; private set; } internal ICompiledHandlebarsConfiguration Configuration { get; private set; } - internal EncodedTextWriter TextWriter { get; private set; } - - internal CascadeDictionary> InlinePartialTemplates { get; } + internal CascadeDictionary> InlinePartialTemplates { get; } - internal Action PartialBlockTemplate { get; private set; } - - public bool SuppressEncoding - { - get => TextWriter.SuppressEncoding; - set => TextWriter.SuppressEncoding = value; - } - - public DataValues Data; + internal TemplateDelegate PartialBlockTemplate { get; private set; } public object Value { get; set; } @@ -118,19 +125,15 @@ internal bool TryGetContextVariable(ChainSegment segment, out object value) || ContextDataObject.TryGetValue(segment, out value); } - internal BindingContext CreateChildContext(object value, Action partialBlockTemplate = null) - { - return Create(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate); - } - - internal BindingContext CreateChildContext() + internal BindingContext CreateChildContext(object value, TemplateDelegate partialBlockTemplate = null) { - return Create(Configuration, null, TextWriter, this, TemplatePath, PartialBlockTemplate); + return Create(Configuration, value ?? Value, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public BindingContext CreateFrame(object value = null) { - return Create(Configuration, value, TextWriter, this, TemplatePath, PartialBlockTemplate); + return Create(Configuration, value, this, TemplatePath, PartialBlockTemplate); } private static void PopulateHash(HashParameterDictionary hash, object from, ICompiledHandlebarsConfiguration configuration) diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index 83efb360..1b86e0df 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; @@ -24,31 +25,36 @@ internal enum WellKnownVariable public sealed class ChainSegment : IEquatable { private static readonly char[] TrimStart = {'@'}; - private static readonly LookupSlim Lookup = new LookupSlim(); + + // TODO: migrate to WeakReference? + private static readonly LookupSlim> Lookup = new LookupSlim>(); + + private static readonly Func> ValueFactory = (s, v) => + { + return new SafeDeferredValue(new CreationProperties(s, v), properties => new ChainSegment(properties.String, properties.KnownVariable)); + }; 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, ValueFactory, WellKnownVariable.None).Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ChainSegment Create(string value) => Lookup.GetOrAdd(value, v => new ChainSegment(v)); + public static ChainSegment Create(object value) + { + if (value is ChainSegment segment) return segment; + return Lookup.GetOrAdd(value as string ?? value.ToString(), ValueFactory, WellKnownVariable.None).Value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ChainSegment Create(string value, WellKnownVariable variable, bool createVariable = false) { if (createVariable) { - Lookup.GetOrAdd($"@{value}", (s, v) => new ChainSegment(s, v), variable); + Lookup.GetOrAdd($"@{value}", ValueFactory, 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)); + return Lookup.GetOrAdd(value, ValueFactory, variable).Value; } public static ChainSegment Index { get; } = Create(nameof(Index), WellKnownVariable.Index, true); @@ -60,12 +66,12 @@ public static ChainSegment Create(object value) 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(); - + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly int _hashCode; - private readonly string _value; - private UndefinedBindingResult _undefinedBindingResult; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _value; + /// /// /// @@ -83,9 +89,11 @@ private ChainSegment(string value, WellKnownVariable wellKnownVariable = WellKno LowerInvariant = segmentTrimmedValue.ToLowerInvariant(); IsValue = LowerInvariant == "value"; - IsKey = LowerInvariant == "key"; _hashCode = GetHashCodeImpl(); + + if (IsThis) WellKnownVariable = WellKnownVariable.This; + if (IsValue) WellKnownVariable = WellKnownVariable.Value; } /// @@ -105,7 +113,7 @@ private ChainSegment(string value, WellKnownVariable wellKnownVariable = WellKno internal readonly string LowerInvariant; internal readonly bool IsValue; - internal readonly bool IsKey; + internal readonly WellKnownVariable WellKnownVariable; /// @@ -175,24 +183,11 @@ private 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 { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ChainSegment x, ChainSegment y) { if (ReferenceEquals(x, y)) return true; @@ -202,7 +197,21 @@ public bool Equals(ChainSegment x, ChainSegment y) return x._hashCode == y._hashCode && x.IsThis == y.IsThis && x.LowerInvariant == y.LowerInvariant; } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetHashCode(ChainSegment obj) => obj._hashCode; } + + private readonly struct CreationProperties + { + public readonly string String; + public readonly WellKnownVariable KnownVariable; + + public CreationProperties(string @string, WellKnownVariable knownVariable = WellKnownVariable.None) + { + String = @string; + KnownVariable = knownVariable; + } + } } } \ 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 1deebdac..8a44a56d 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -80,26 +80,16 @@ PathSegment[] segments 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) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return HasValue == other.HasValue && _path == other._path && _comparerTag == other._comparerTag; + return HasValue == other.HasValue && _path == other._path; } /// @@ -122,20 +112,7 @@ public override bool Equals(object obj) /// 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 + internal sealed class TrimmedPathEqualityComparer : IEqualityComparer { private readonly bool _countParts; @@ -150,7 +127,7 @@ public bool Equals(PathInfo x, PathInfo y) 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); + return (!_countParts || x.PathChain.Length == y.PathChain.Length) && string.Equals(x.TrimmedPath, y.TrimmedPath); } public int GetHashCode(PathInfo obj) diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index 4fa08429..3644f9ea 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -1,60 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using HandlebarsDotNet.ObjectDescriptors; -using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path { - internal static class PathResolver + public static class PathResolver { - public static PathInfo GetPathInfo(string path) - { - if (path == "null") - return new PathInfo(false, path, false, null); - - var originalPath = path; - - var isValidHelperLiteral = true; - var isVariable = path.StartsWith("@"); - var isInversion = path.StartsWith("^"); - var isBlockHelper = path.StartsWith("#"); - if (isVariable || isBlockHelper || isInversion) - { - isValidHelperLiteral = isBlockHelper || isInversion; - path = path.Substring(1); - } - - var segments = new List(); - var pathParts = path.Split('/'); - if (pathParts.Length > 1) isValidHelperLiteral = false; - foreach (var segment in pathParts) - { - if (segment == "..") - { - 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; - - segments.Add(new PathSegment(segmentString, chainSegments)); - } - - return new PathInfo(true, originalPath, isValidHelperLiteral, segments.ToArray()); - } - public static object ResolvePath(BindingContext context, PathInfo pathInfo) { if (!pathInfo.HasValue) return null; @@ -89,7 +39,7 @@ public static object ResolvePath(BindingContext context, PathInfo pathInfo) var chainSegment = pathChain[index]; instance = ResolveValue(context, instance, chainSegment); - if (!(instance is UndefinedBindingResult)) + if (!(instance is UndefinedBindingResult undefined)) { continue; } @@ -97,7 +47,7 @@ public static object ResolvePath(BindingContext context, PathInfo pathInfo) 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, undefined); return instance; } @@ -109,50 +59,14 @@ public static object ResolvePath(BindingContext context, PathInfo pathInfo) } if (context.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo, result.Value); + throw new HandlebarsUndefinedBindingException(pathInfo, result); return instance; } 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[] {ChainSegment.Create("this")}; - - var pathChain = pathChainParts.Aggregate(new List(), (list, next) => - { - if (insideEscapeBlock) - { - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list[list.Count - 1] = ChainSegment.Create($"{list[list.Count - 1]}.{next}"); - return list; - } - - if (next.StartsWith("[")) - { - insideEscapeBlock = true; - } - - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list.Add(ChainSegment.Create(next)); - return list; - }); - - return pathChain; - } - + private static object ResolveValue(BindingContext context, object instance, ChainSegment chainSegment) { object resolvedValue; @@ -160,7 +74,7 @@ private static object ResolveValue(BindingContext context, object instance, Chai { return context.TryGetContextVariable(chainSegment, out resolvedValue) ? resolvedValue - : chainSegment.GetUndefinedBindingResult(context.Configuration); + : UndefinedBindingResult.Create(chainSegment); } if (chainSegment.IsThis) return instance; @@ -176,14 +90,14 @@ private static object ResolveValue(BindingContext context, object instance, Chai return resolvedValue; } - return chainSegment.GetUndefinedBindingResult(context.Configuration); + return UndefinedBindingResult.Create(chainSegment); } - public static bool TryAccessMember(object instance, ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) + internal static bool TryAccessMember(object instance, ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) { if (instance == null) { - value = chainSegment.GetUndefinedBindingResult(configuration); + value = UndefinedBindingResult.Create(chainSegment); return false; } diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index ee771eac..ca9cf461 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -1,37 +1,29 @@ -using System.Diagnostics; -using HandlebarsDotNet.Compiler.Structure.Path; +using System; +using System.Diagnostics; +using HandlebarsDotNet.Collections; -namespace HandlebarsDotNet.Compiler +namespace HandlebarsDotNet { - [DebuggerDisplay("undefined")] - internal class UndefinedBindingResult + [DebuggerDisplay("undefined")] + public class UndefinedBindingResult : IEquatable { - public readonly string Value; - private readonly ICompiledHandlebarsConfiguration _configuration; + // TODO: migrate to WeakReference? + private static readonly LookupSlim Cache = new LookupSlim(); - public UndefinedBindingResult(string value, ICompiledHandlebarsConfiguration configuration) - { - Value = value; - _configuration = configuration; - } + public static UndefinedBindingResult Create(string value) => Cache.GetOrAdd(value, s => new UndefinedBindingResult(s)); - public UndefinedBindingResult(ChainSegment value, ICompiledHandlebarsConfiguration configuration) - { - Value = value; - _configuration = configuration; - } - - public override string ToString() - { - var formatter = _configuration.UnresolvedBindingFormatter; - if (formatter == null) - { - if(string.IsNullOrEmpty(Value)) return string.Empty; - formatter = string.Empty; - } - - return string.Format( formatter, Value ); - } + + public readonly string Value; + + private UndefinedBindingResult(string value) => Value = value; + + public override string ToString() => string.Empty; + + public bool Equals(UndefinedBindingResult other) => Value == other.Value; + + public override bool Equals(object obj) => obj is UndefinedBindingResult other && Equals(other); + + public override int GetHashCode() => Value != null ? Value.GetHashCode() : 0; } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 91763691..ede65f67 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,21 +1,29 @@ -using System; -using System.IO; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Runtime.CompilerServices; using Expressions.Shortcuts; +using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.Polyfills; -using HandlebarsDotNet.ValueProviders; using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class BlockHelperFunctionBinder : HandlebarsExpressionVisitor { + private static readonly LookupSlim> ArgumentsConstructorsMap = new LookupSlim>(); + private static readonly MethodInfo HelperInvokeMethodInfo = typeof(BlockHelperDescriptorBase).GetMethod(nameof(BlockHelperDescriptorBase.Invoke)); + + private static readonly ConstructorInfo HelperOptionsCtor = typeof(HelperOptions) + .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + .Single(o => o.GetParameters().Length > 0); + private enum BlockHelperDirection { Direct, Inverse } - + private CompilationContext CompilationContext { get; } public BlockHelperFunctionBinder(CompilationContext compilationContext) @@ -33,7 +41,7 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b var isInlinePartial = bhex.HelperName == "#*inline"; var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(bhex.HelperName); - var bindingContext = Arg(CompilationContext.BindingContext); + var bindingContext = CompilationContext.Args.BindingContext; var context = isInlinePartial ? bindingContext.As() : bindingContext.Property(o => o.Value); @@ -41,7 +49,7 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b var readerContext = bhex.Context; var direct = Compile(bhex.Body); var inverse = Compile(bhex.Inversion); - var arguments = CreateArguments(); + var args = FunctionBinderHelpers.CreateArguments(bhex.Arguments, CompilationContext); var helperName = pathInfo.TrimmedPath; var direction = bhex.IsRaw || pathInfo.IsBlockHelper ? BlockHelperDirection.Direct : BlockHelperDirection.Inverse; @@ -60,7 +68,10 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b var resolver = helperResolvers[index]; if (!resolver.TryResolveBlockHelper(helperName, out var resolvedDescriptor)) continue; - return Bind(resolvedDescriptor); + descriptor = new StrongBox(resolvedDescriptor); + blockHelpers.Add(pathInfo, descriptor); + + return BindByRef(descriptor); } var lateBindBlockHelperDescriptor = new LateBindBlockHelperDescriptor(pathInfo, CompilationContext.Configuration); @@ -80,76 +91,25 @@ ExpressionContainer CreateBlockParams() 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) + TemplateDelegate Compile(Expression expression) { var blockExpression = (BlockExpression) expression; - return FunctionBuilder.CompileCore(blockExpression.Expressions, CompilationContext.Configuration); + return FunctionBuilder.Compile(blockExpression.Expressions, CompilationContext.Configuration); } - Expression BindByRef(StrongBox value) + Expression BindByRef(StrongBox helperBox) { - return direction switch + var writer = CompilationContext.Args.EncodedWriter; + + var helperOptions = direction switch { - BlockHelperDirection.Direct => Call(() => - BlockHelperCallBindingByRef(bindingContext, context, blockParams, direct, inverse, arguments, value)), - - BlockHelperDirection.Inverse => Call(() => - BlockHelperCallBindingByRef(bindingContext, context, blockParams, inverse, direct, arguments, value)), - + BlockHelperDirection.Direct => New(() => new HelperOptions(direct, inverse, blockParams, bindingContext)), + BlockHelperDirection.Inverse => New(() => new HelperOptions(inverse, direct, blockParams, bindingContext)), _ => throw new HandlebarsCompilerException("Helper referenced with unknown prefix", readerContext) }; + + return Call(() => helperBox.Value.Invoke(writer, helperOptions, context, args)); } - - Expression Bind(BlockHelperDescriptorBase value) - { - 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 d7727058..c0baaa29 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -1,119 +1,19 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq.Expressions; -using Expressions.Shortcuts; -using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { - internal class ContextBinder : HandlebarsExpressionVisitor + internal static class ContextBinder { - private ContextBinder() + public static Expression Bind(CompilationContext context, Expression body) { - } - - public static Expression> Bind(CompilationContext context, Expression body, Expression parentContext, string templatePath) - { - var configuration = Arg(context.Configuration); - - var writerParameter = Parameter("buffer"); - var objectParameter = Parameter("data"); + var blockExpression = (BlockExpression) body; - var bindingContext = Arg(context.BindingContext); - var textEncoder = configuration.Property(o => o.TextEncoder); - var encodedWriterExpression = Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); - var parentContextArg = Arg(parentContext); - - var newBindingContext = Call( - () => BindingContext.Create((ICompiledHandlebarsConfiguration) configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath) + return Expression.Lambda( + blockExpression, + context.EncodedWriter, + context.BindingContext ); - - Expression blockBuilder = Block() - .Parameter(bindingContext) - .Parameter(out var shouldDispose) - .Line(bindingContext.Assign(objectParameter.As())) - .Line(Condition() - .If(Expression.Equal(bindingContext, Null())) - .Then(block => - { - block.Line(shouldDispose.Assign(true)); - block.Line(bindingContext.Assign(newBindingContext)); - }) - ) - .Line(Try() - .Body(block => block.Lines(((BlockExpression) body).Expressions)) - .Finally(Condition() - .If(shouldDispose) - .Then(block => - { - block - .Line(bindingContext.Call(o => o.TextWriter.Dispose())) - .Line(bindingContext.Call(o => o.Dispose())); - }) - ) - ); - - return Expression.Lambda>(blockBuilder, (ParameterExpression) writerParameter.Expression, (ParameterExpression) objectParameter.Expression); - } - - public static Expression> Bind(CompilationContext context, Expression body, string templatePath) - { - var configuration = Arg(context.Configuration); - - var writerParameter = Parameter("buffer"); - var objectParameter = Parameter("data"); - - var bindingContext = Arg(context.BindingContext); - 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((ICompiledHandlebarsConfiguration) configuration, objectParameter, writer, parentContextArg, templatePath) - ); - - Expression blockBuilder = Block() - .Parameter(bindingContext) - .Parameter(writer) - .Parameter(out var shouldDispose) - .Parameter(out var shouldDisposeWriter) - .Line(bindingContext.Assign(objectParameter.As())) - .Line(Condition() - .If(Expression.Equal(bindingContext, Null())) - .Then(block => - { - 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(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); } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs index 13ff95c0..46014f44 100644 --- a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs +++ b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs @@ -1,9 +1,7 @@ using System; using System.Collections; -using System.IO; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Polyfills; -using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { @@ -11,41 +9,51 @@ internal static class DeferredSectionBlockHelper { private static readonly ChainSegment[] BlockParamsVariables = ArrayEx.Empty(); - public static void PlainHelper(BindingContext context, object value, - Action body, Action inverse) + public static void PlainHelper( + BindingContext context, + EncodedTextWriter writer, + object value, + TemplateDelegate body, + TemplateDelegate inverse + ) { - RenderSection(value, context, body, inverse); + RenderSection(value, context, writer, body, inverse); } - private static void RenderSection( - object value, + private static void RenderSection(object value, BindingContext context, - Action body, - Action inversion - ) + EncodedTextWriter writer, + TemplateDelegate body, + TemplateDelegate inversion) { switch (value) { case bool boolValue when boolValue: - body(context, context.TextWriter, context); + body(writer, context); return; case null: case object _ when HandlebarsUtils.IsFalsyOrEmpty(value): - inversion(context, context.TextWriter, context); + inversion(writer, context); return; case string _: - body(context, context.TextWriter, value); + { + using var frame = context.CreateFrame(value); + body(writer, frame); return; + } case IEnumerable enumerable: - Iterator.Iterate(context, BlockParamsVariables, enumerable, body, inversion); + Iterator.Iterate(context, writer, BlockParamsVariables, enumerable, body, inversion); break; default: - body(context, context.TextWriter, value); + { + using var frame = context.CreateFrame(value); + body(writer, frame); break; + } } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs new file mode 100644 index 00000000..ab73b2bc --- /dev/null +++ b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Expressions.Shortcuts; +using HandlebarsDotNet.Collections; +using static Expressions.Shortcuts.ExpressionShortcuts; + +namespace HandlebarsDotNet.Compiler +{ + internal static class FunctionBinderHelpers + { + private static readonly LookupSlim> ArgumentsConstructorsMap + = new LookupSlim>(); + + private static readonly Func> CtorFactory = i => + { + return new DeferredValue(i, count => + { + var objectType = typeof(object); + var argumentTypes = new Type[count]; + for (var index = 0; index < argumentTypes.Length; index++) + { + argumentTypes[index] = objectType; + } + + return typeof(Arguments).GetConstructor(argumentTypes); + }); + }; + + public static ExpressionContainer CreateArguments(IEnumerable expressions, CompilationContext compilationContext) + { + var arguments = expressions + .ApplyOn(path => path.Context = PathExpression.ResolutionContext.Parameter) + .Select(o => FunctionBuilder.Reduce(o, compilationContext)) + .ToArray(); + + if (arguments.Length == 0) return New(() => new Arguments(0)); + + var constructor = ArgumentsConstructorsMap.GetOrAdd(arguments.Length, CtorFactory).Value; + if (!ReferenceEquals(constructor, null)) + { + var newExpression = Expression.New(constructor, arguments); + return Arg(newExpression); + } + + var arr = Array(arguments); + return New(() => new Arguments(arr)); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs index 77139d03..a38f1d9f 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -4,7 +4,7 @@ namespace HandlebarsDotNet.Compiler { - internal abstract class HandlebarsExpressionVisitor : ExpressionVisitor + internal class HandlebarsExpressionVisitor : ExpressionVisitor { public override Expression Visit(Expression exp) { @@ -59,7 +59,7 @@ protected virtual Expression VisitPathExpression(PathExpression pex) protected virtual Expression VisitHelperExpression(HelperExpression hex) { var arguments = VisitExpressionList(hex.Arguments); - if (arguments != hex.Arguments) + if (!Equals(arguments, hex.Arguments)) { return HandlebarsExpression.Helper(hex.HelperName, hex.IsBlock, arguments, hex.IsRaw); } @@ -149,35 +149,32 @@ protected virtual Expression VisitHashParametersExpression(HashParametersExpress return hpex; } - IEnumerable VisitExpressionList(IEnumerable original) + private IEnumerable VisitExpressionList(IEnumerable original) { - if (original == null) - { - return original; - } + if (original == null) return null; var originalAsList = original as IReadOnlyList ?? original.ToArray(); List list = null; - for (int i = 0, n = originalAsList.Count; i < n; i++) + for (int index = 0; index < originalAsList.Count; index++) { - Expression p = Visit(originalAsList[i]); + var p = Visit(originalAsList[index]); if (list != null) { list.Add(p); + continue; } - else if (p != originalAsList[i]) + + if (p == originalAsList[index]) continue; + + list = new List(originalAsList.Count); + for (var j = 0; j < index; j++) { - list = new List(n); - for (int j = 0; j < i; j++) - { - list.Add(originalAsList[j]); - } - list.Add(p); + list.Add(originalAsList[j]); } + list.Add(p); } - if (list != null) - return list.ToArray(); - return original; + + return list?.ToArray() ?? originalAsList; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs index cd1470e8..c629e53d 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs @@ -6,12 +6,18 @@ public class HandlebarsUndefinedBindingException : Exception { public HandlebarsUndefinedBindingException(string path, string missingKey) : base(missingKey + " is undefined") { - this.Path = path; - this.MissingKey = missingKey; + Path = path; + MissingKey = missingKey; + } + + public HandlebarsUndefinedBindingException(string path, UndefinedBindingResult undefined) : base(undefined.Value + " is undefined") + { + Path = path; + MissingKey = undefined.Value; } - public string Path { get; set; } + public string Path { get; } - public string MissingKey { get; set; } + public string MissingKey { get; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs index 77c09999..43194010 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs @@ -5,6 +5,8 @@ namespace HandlebarsDotNet.Compiler { public class HashParameterDictionary : Dictionary { + internal static readonly HashParameterDictionary Empty = new HashParameterDictionary(); + public HashParameterDictionary() :base(StringComparer.OrdinalIgnoreCase) { diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 09c54631..d21bbcb9 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; using Expressions.Shortcuts; @@ -27,14 +26,11 @@ protected override Expression VisitHelperExpression(HelperExpression hex) if(!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming) return Expression.Empty(); var helperName = pathInfo.TrimmedPath; - var bindingContext = Arg(CompilationContext.BindingContext); - var contextValue = bindingContext.Property(o => o.Value); - var textWriter = bindingContext.Property(o => o.TextWriter); - var arguments = hex.Arguments - .ApplyOn(path => path.Context = PathExpression.ResolutionContext.Parameter) - .Select(o => FunctionBuilder.Reduce(o, CompilationContext)); + var bindingContext = CompilationContext.Args.BindingContext; + var textWriter = CompilationContext.Args.EncodedWriter; - var args = Array(arguments); + var contextValue = bindingContext.Property(o => o.Value); + var args = FunctionBinderHelpers.CreateArguments(hex.Arguments, CompilationContext); var configuration = CompilationContext.Configuration; if (configuration.Helpers.TryGetValue(pathInfo, out var helper)) @@ -47,6 +43,8 @@ protected override Expression VisitHelperExpression(HelperExpression hex) var resolver = configuration.HelperResolvers[index]; if (resolver.TryResolveHelper(helperName, typeof(object), out var resolvedHelper)) { + helper = new StrongBox(resolvedHelper); + configuration.Helpers.Add(pathInfo, helper); return Call(() => resolvedHelper.WriteInvoke(bindingContext, textWriter, contextValue, args)); } } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index d539c13a..b927f54e 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -32,10 +32,11 @@ public IteratorBinder(CompilationContext compilationContext) protected override Expression VisitIteratorExpression(IteratorExpression iex) { - var context = Arg(CompilationContext.BindingContext); + var context = CompilationContext.Args.BindingContext; + var writer = CompilationContext.Args.EncodedWriter; - var template = FunctionBuilder.CompileCore(new[] {iex.Template}, CompilationContext.Configuration); - var ifEmpty = FunctionBuilder.CompileCore(new[] {iex.IfEmpty}, CompilationContext.Configuration); + var template = FunctionBuilder.Compile(new[] {iex.Template}, CompilationContext.Configuration); + var ifEmpty = FunctionBuilder.Compile(new[] {iex.IfEmpty}, CompilationContext.Configuration); if (iex.Sequence is PathExpression pathExpression) { @@ -46,7 +47,7 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) var blockParamsValues = CreateBlockParams(); return Call(() => - Iterator.Iterate(context, blockParamsValues, compiledSequence, template, ifEmpty) + Iterator.Iterate(context, writer, blockParamsValues, compiledSequence, template, ifEmpty) ); ExpressionContainer CreateBlockParams() @@ -64,22 +65,26 @@ ExpressionContainer CreateBlockParams() internal static class Iterator { - public static void Iterate(BindingContext context, + public static void Iterate( + BindingContext context, + EncodedTextWriter writer, ChainSegment[] blockParamsVariables, object target, - Action template, - Action ifEmpty) + TemplateDelegate template, + TemplateDelegate ifEmpty) { if (!HandlebarsUtils.IsTruthy(target)) { - ifEmpty(context, context.TextWriter, context.Value); + using var frame = context.CreateFrame(context.Value); + ifEmpty(writer, frame); return; } var targetType = target.GetType(); if (!context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor)) { - ifEmpty(context, context.TextWriter, context.Value); + using var frame = context.CreateFrame(context.Value); + ifEmpty(writer, frame); return; } @@ -88,33 +93,35 @@ public static void Iterate(BindingContext context, var properties = descriptor.GetProperties(descriptor, target); if (properties is IList propertiesList) { - IterateObjectWithStaticProperties(context, blockParamsVariables, descriptor, target, propertiesList, targetType, template, ifEmpty); + IterateObjectWithStaticProperties(context, writer, blockParamsVariables, descriptor, target, propertiesList, targetType, template, ifEmpty); return; } - IterateObject(context, descriptor, blockParamsVariables, target, properties, targetType, template, ifEmpty); + IterateObject(context, writer, descriptor, blockParamsVariables, target, properties, targetType, template, ifEmpty); return; } if (target is IList list) { - IterateList(context, blockParamsVariables, list, template, ifEmpty); + IterateList(context, writer, blockParamsVariables, list, template, ifEmpty); return; } - IterateEnumerable(context, blockParamsVariables, (IEnumerable) target, template, ifEmpty); + IterateEnumerable(context, writer, blockParamsVariables, (IEnumerable) target, template, ifEmpty); } - private static void IterateObject(BindingContext context, + private static void IterateObject( + BindingContext context, + EncodedTextWriter writer, ObjectDescriptor descriptor, ChainSegment[] blockParamsVariables, object target, IEnumerable properties, Type targetType, - Action template, - Action ifEmpty) + TemplateDelegate template, + TemplateDelegate ifEmpty) { - using var innerContext = context.CreateChildContext(); + using var innerContext = context.CreateFrame(); var iterator = new ObjectIteratorValues(innerContext); var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); @@ -146,23 +153,26 @@ private static void IterateObject(BindingContext context, blockParams[_0] = iteratorValue; blockParams[_1] = iteratorKey; - template(context, context.TextWriter, innerContext); + template(writer, innerContext); } if (!enumerated) { - ifEmpty(context, context.TextWriter, context.Value); + innerContext.Value = context.Value; + ifEmpty(writer, innerContext); } } - private static void IterateObjectWithStaticProperties(BindingContext context, + private static void IterateObjectWithStaticProperties( + BindingContext context, + EncodedTextWriter writer, ChainSegment[] blockParamsVariables, ObjectDescriptor descriptor, object target, IList properties, Type targetType, - Action template, - Action ifEmpty) + TemplateDelegate template, + TemplateDelegate ifEmpty) { using var innerContext = context.CreateFrame(); var iterator = new ObjectIteratorValues(innerContext); @@ -196,20 +206,23 @@ private static void IterateObjectWithStaticProperties(BindingContext context, blockParams[_0] = iteratorValue; blockParams[_1] = iteratorKey; - template(context, context.TextWriter, innerContext); + template(writer, innerContext); } if (iterationIndex == 0) { - ifEmpty(context, context.TextWriter, context.Value); + innerContext.Value = context.Value; + ifEmpty(writer, innerContext); } } - private static void IterateList(BindingContext context, + private static void IterateList( + BindingContext context, + EncodedTextWriter writer, ChainSegment[] blockParamsVariables, IList target, - Action template, - Action ifEmpty) + TemplateDelegate template, + TemplateDelegate ifEmpty) { using var innerContext = context.CreateFrame(); var iterator = new IteratorValues(innerContext); @@ -241,22 +254,25 @@ private static void IterateList(BindingContext context, innerContext.Value = iteratorValue; - template(context, context.TextWriter, innerContext); + template(writer, innerContext); } if (iterationIndex == 0) { - ifEmpty(context, context.TextWriter, context.Value); + innerContext.Value = context.Value; + ifEmpty(writer, innerContext); } } - private static void IterateEnumerable(BindingContext context, + private static void IterateEnumerable( + BindingContext context, + EncodedTextWriter writer, ChainSegment[] blockParamsVariables, IEnumerable target, - Action template, - Action ifEmpty) + TemplateDelegate template, + TemplateDelegate ifEmpty) { - using var innerContext = context.CreateChildContext(); + using var innerContext = context.CreateFrame(); var iterator = new IteratorValues(innerContext); var blockParams = new BlockParamsValues(innerContext, blockParamsVariables); @@ -287,12 +303,13 @@ private static void IterateEnumerable(BindingContext context, innerContext.Value = iteratorValue; - template(context, context.TextWriter, innerContext); + template(writer, innerContext); } if (!enumerated) { - ifEmpty(context, context.TextWriter, context.Value); + innerContext.Value = context.Value; + ifEmpty(writer, innerContext); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index e37cc4c0..aac54f66 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -21,46 +22,50 @@ public PartialBinder(CompilationContext compilationContext) protected override Expression VisitPartialExpression(PartialExpression pex) { - var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var bindingContext = CompilationContext.Args.BindingContext; + var writer = CompilationContext.Args.EncodedWriter; + var partialBlockTemplate = pex.Fallback != null - ? FunctionBuilder.CompileCore(new[] { pex.Fallback }, CompilationContext.Configuration) + ? FunctionBuilder.Compile(new[] { pex.Fallback }, CompilationContext.Configuration) : null; if (pex.Argument != null || partialBlockTemplate != null) { - var value = ExpressionShortcuts.Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext)); - var partialTemplate = ExpressionShortcuts.Arg(partialBlockTemplate); + var value = Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext)); + var partialTemplate = Arg(partialBlockTemplate); bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate)); } - var partialName = ExpressionShortcuts.Cast(pex.PartialName); - var configuration = ExpressionShortcuts.Arg(CompilationContext.Configuration); - return ExpressionShortcuts.Call(() => - InvokePartialWithFallback(partialName, bindingContext, (ICompiledHandlebarsConfiguration) configuration) + var partialName = Cast(pex.PartialName); + var configuration = Arg(CompilationContext.Configuration); + return Call(() => + InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration) ); } private static void InvokePartialWithFallback( string partialName, BindingContext context, + EncodedTextWriter writer, ICompiledHandlebarsConfiguration configuration) { - if (InvokePartial(partialName, context, configuration)) return; + if (InvokePartial(partialName, context, writer, configuration)) return; if (context.PartialBlockTemplate == null) { if (configuration.MissingPartialTemplateHandler == null) throw new HandlebarsRuntimeException($"Referenced partial name {partialName} could not be resolved"); - configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); + configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, writer); return; } - context.PartialBlockTemplate(context, context.TextWriter, context); + context.PartialBlockTemplate(writer, context); } private static bool InvokePartial( string partialName, BindingContext context, + EncodedTextWriter writer, ICompiledHandlebarsConfiguration configuration) { if (partialName.Equals(SpecialPartialBlockName)) @@ -70,14 +75,14 @@ private static bool InvokePartial( return false; } - context.PartialBlockTemplate(context, context.TextWriter, context); + context.PartialBlockTemplate(writer, context); return true; } //if we have an inline partial, skip the file system and RegisteredTemplates collection if (context.InlinePartialTemplates.TryGetValue(partialName, out var partial)) { - partial(context.TextWriter, context); + partial(writer, context); return true; } @@ -95,7 +100,8 @@ private static bool InvokePartial( try { - configuration.RegisteredTemplates[partialName](context.TextWriter, context); + using var textWriter = writer.CreateWrapper(); + configuration.RegisteredTemplates[partialName](textWriter, context); return true; } catch (Exception exception) diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 4605ae91..875db732 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -3,7 +3,6 @@ using Expressions.Shortcuts; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.Helpers; -using HandlebarsDotNet.Polyfills; using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler @@ -20,15 +19,16 @@ public PathBinder(CompilationContext compilationContext) protected override Expression VisitStatementExpression(StatementExpression sex) { if (!(sex.Body is PathExpression)) return Visit(sex.Body); + + var writer = CompilationContext.Args.EncodedWriter; - var context = Arg(CompilationContext.BindingContext); var value = Arg(Visit(sex.Body)); - return context.Call(o => o.TextWriter.Write(value)); + return writer.Call(o => o.Write(value)); } protected override Expression VisitPathExpression(PathExpression pex) { - var context = Arg(CompilationContext.BindingContext); + var context = CompilationContext.Args.BindingContext; var configuration = CompilationContext.Configuration; var pathInfo = configuration.PathInfoStore.GetOrAdd(pex.Path); @@ -37,23 +37,24 @@ protected override Expression VisitPathExpression(PathExpression pex) if (pex.Context == PathExpression.ResolutionContext.Parameter) return resolvePath; if (pathInfo.IsVariable || pathInfo.IsThis) return resolvePath; if (!pathInfo.IsValidHelperLiteral && !configuration.Compatibility.RelaxedHelperNaming) return resolvePath; - - if (!configuration.Helpers.TryGetValue(pathInfo, out var helper)) + + var pathInfoLight = new PathInfoLight(pathInfo); + if (!configuration.Helpers.TryGetValue(pathInfoLight, out var helper)) { helper = new StrongBox(new LateBindHelperDescriptor(pathInfo, configuration)); - configuration.Helpers.Add(pathInfo, helper); + configuration.Helpers.Add(pathInfoLight, helper); } else if (configuration.Compatibility.RelaxedHelperNaming) { - pathInfo.TagComparer(); - if (!configuration.Helpers.ContainsKey(pathInfo)) + pathInfoLight = pathInfoLight.TagComparer(); + if (!configuration.Helpers.ContainsKey(pathInfoLight)) { helper = new StrongBox(new LateBindHelperDescriptor(pathInfo, configuration)); - configuration.Helpers.Add(pathInfo, helper); + configuration.Helpers.Add(pathInfoLight, helper); } } - var argumentsArg = Arg(ArrayEx.Empty()); + var argumentsArg = New(() => new Arguments(0)); 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 a32a0c2b..ba8641ba 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -15,10 +15,10 @@ public StaticReplacer(CompilationContext compilationContext) protected override Expression VisitStaticExpression(StaticExpression stex) { - var context = Arg(CompilationContext.BindingContext); + var writer = CompilationContext.Args.EncodedWriter; var value = Arg(stex.Value); - return context.Call(o => o.TextWriter.Write(value, false)); + return writer.Call(o => o.Write(value, false)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index 5d12b882..f9dc8e0e 100644 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -35,7 +35,7 @@ private static Expression HandleMethodCallExpression(MethodCallExpression helper { var bindingContext = Arg(helperCall.Arguments[0]); var context = Arg(helperCall.Arguments[2]); - var arguments = Arg(helperCall.Arguments[3]); + var arguments = Arg(helperCall.Arguments[3]); var helper = Arg(helperCall.Object); return helper.Call(o => o.ReturnInvoke(bindingContext, context, arguments)); diff --git a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs index a247d0c3..1d02c572 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Expressions.Shortcuts; using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler @@ -16,14 +17,13 @@ protected override Expression VisitStatementExpression(StatementExpression sex) { if (!sex.IsEscaped) { - var context = Arg(CompilationContext.BindingContext); - var suppressEncoding = context.Property(o => o.SuppressEncoding); + var writer = CompilationContext.Args.EncodedWriter; + var suppressEncoding = writer.Property(o => o.SuppressEncoding); - return Block(typeof(void)) + return Block() .Line(suppressEncoding.Assign(true)) .Line(sex) - .Line(suppressEncoding.Assign(false)) - .Line(Expression.Empty()); + .Line(suppressEncoding.Assign(false)); } return sex; diff --git a/source/Handlebars/Compiler/Translation/ResultHolder.cs b/source/Handlebars/Compiler/Translation/ResultHolder.cs deleted file mode 100644 index aa805d90..00000000 --- a/source/Handlebars/Compiler/Translation/ResultHolder.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace HandlebarsDotNet.Compiler -{ - // Will be removed in next iterations - internal readonly struct ResultHolder - { - public readonly bool Success; - public readonly object Value; - - public ResultHolder(bool success, object value) - { - Success = success; - Value = value; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Configuration/CompileTimeConfiguration.cs b/source/Handlebars/Configuration/CompileTimeConfiguration.cs index 71e217a2..720ce9c0 100644 --- a/source/Handlebars/Configuration/CompileTimeConfiguration.cs +++ b/source/Handlebars/Configuration/CompileTimeConfiguration.cs @@ -18,7 +18,6 @@ public class CompileTimeConfiguration public IList Features { get; } = new List { new BuildInHelpersFeatureFactory(), - new ClosureFeatureFactory(), new DefaultCompilerFeatureFactory(), new MissingHelperFeatureFactory() }; diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs index 649f1251..d67508a5 100644 --- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -15,23 +15,28 @@ public sealed class HandlebarsConfiguration : IHandlebarsTemplateRegistrations public IDictionary BlockHelpers { get; } - public IDictionary> RegisteredTemplates { get; } + public IDictionary> RegisteredTemplates { get; } /// public IList HelperResolvers { get; } public IExpressionNameResolver ExpressionNameResolver { get; set; } - public ITextEncoder TextEncoder { get; set; } = new HtmlEncoder(); + public ITextEncoder TextEncoder { get; set; } /// public IFormatProvider FormatProvider { get; set; } = CultureInfo.CurrentCulture; public ViewEngineFileSystem FileSystem { get; set; } + [Obsolete("Use `UnresolvedBindingFormat` instead.")] public string UnresolvedBindingFormatter { get; set; } + public Func UnresolvedBindingFormat { get; set; } + public bool ThrowOnUnresolvedBindingExpression { get; set; } + + public bool NoEscape { get; set; } /// /// The resolver used for unregistered partials. Defaults @@ -57,8 +62,9 @@ public HandlebarsConfiguration() { Helpers = new ObservableDictionary(comparer: StringComparer.OrdinalIgnoreCase); BlockHelpers = new ObservableDictionary(comparer: StringComparer.OrdinalIgnoreCase); - RegisteredTemplates = new ObservableDictionary>(comparer: StringComparer.OrdinalIgnoreCase); + RegisteredTemplates = new ObservableDictionary>(comparer: StringComparer.OrdinalIgnoreCase); HelperResolvers = new ObservableList(); + TextEncoder = new HtmlEncoder(FormatProvider); } } } diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs similarity index 76% rename from source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs rename to source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs index 0b6d3f85..a4f5f13e 100644 --- a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs @@ -5,12 +5,11 @@ using System.Reflection; using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Middlewares; 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 @@ -24,12 +23,26 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) UnderlingConfiguration = configuration; HelperResolvers = new ObservableList(configuration.HelperResolvers); - RegisteredTemplates = new ObservableDictionary>(configuration.RegisteredTemplates); - PathInfoStore = _pathInfoStore = new PathInfoStore(); + RegisteredTemplates = new ObservableDictionary>(configuration.RegisteredTemplates); + PathInfoStore = _pathInfoStore = HandlebarsDotNet.PathInfoStore.Shared; ObjectDescriptorProvider = CreateObjectDescriptorProvider(); AliasProviders = new ObservableList(UnderlingConfiguration.AliasProviders); - - ExpressionMiddleware = new ObservableList(UnderlingConfiguration.CompileTimeConfiguration.ExpressionMiddleware); + UnresolvedBindingFormat = configuration.UnresolvedBindingFormat ?? (undefined => + { + var formatter = UnresolvedBindingFormatter; + if (formatter == null) + { + if(string.IsNullOrEmpty(undefined.Value)) return string.Empty; + formatter = string.Empty; + } + + return string.Format( formatter, undefined.Value ); + }); + + ExpressionMiddlewares = new ObservableList(UnderlingConfiguration.CompileTimeConfiguration.ExpressionMiddleware) + { + new ExpressionOptimizerMiddleware() + }; Features = UnderlingConfiguration.CompileTimeConfiguration.Features .Select(o => o.CreateFeature()) @@ -38,8 +51,6 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) CreateHelpersSubscription(); CreateBlockHelpersSubscription(); - - AliasProviders.Add(new CollectionMemberAliasProvider(this)); } public HandlebarsConfiguration UnderlingConfiguration { get; } @@ -47,32 +58,36 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) public ITextEncoder TextEncoder => UnderlingConfiguration.TextEncoder; public IFormatProvider FormatProvider => UnderlingConfiguration.FormatProvider; public ViewEngineFileSystem FileSystem => UnderlingConfiguration.FileSystem; +#pragma warning disable 618 public string UnresolvedBindingFormatter => UnderlingConfiguration.UnresolvedBindingFormatter; +#pragma warning restore 618 public bool ThrowOnUnresolvedBindingExpression => UnderlingConfiguration.ThrowOnUnresolvedBindingExpression; public IPartialTemplateResolver PartialTemplateResolver => UnderlingConfiguration.PartialTemplateResolver; public IMissingPartialTemplateHandler MissingPartialTemplateHandler => UnderlingConfiguration.MissingPartialTemplateHandler; public Compatibility Compatibility => UnderlingConfiguration.Compatibility; + public bool NoEscape => UnderlingConfiguration.NoEscape; public IObjectDescriptorProvider ObjectDescriptorProvider { get; } - public IList ExpressionMiddleware { get; } + public IList ExpressionMiddlewares { get; } public IList AliasProviders { get; } public IExpressionCompiler ExpressionCompiler { get; set; } public IReadOnlyList Features { get; } public IPathInfoStore PathInfoStore { get; } - public IDictionary> Helpers { get; private set; } - public IDictionary> BlockHelpers { get; private set; } + public Func UnresolvedBindingFormat { get; } + public IDictionary> Helpers { get; private set; } + public IDictionary> BlockHelpers { get; private set; } public IList HelperResolvers { get; } - public IDictionary> RegisteredTemplates { get; } + public IDictionary> RegisteredTemplates { get; } private void CreateHelpersSubscription() { var existingHelpers = UnderlingConfiguration.Helpers.ToDictionary( - o => _pathInfoStore.GetOrAdd($"[{o.Key}]"), + o => new PathInfoLight(_pathInfoStore.GetOrAdd($"[{o.Key}]")), o => new StrongBox(o.Value) ); - Helpers = new ObservableDictionary>(existingHelpers, Compatibility.RelaxedHelperNaming ? PathInfo.PlainPathComparer : PathInfo.PlainPathWithPartsCountComparer); + Helpers = new ObservableDictionary>(existingHelpers, Compatibility.RelaxedHelperNaming ? PathInfoLight.PlainPathComparer : PathInfoLight.PlainPathWithPartsCountComparer); var helpersObserver = new ObserverBuilder>() .OnEvent.ReplacedObservableEvent>( @@ -103,12 +118,11 @@ private void CreateHelpersSubscription() private void CreateBlockHelpersSubscription() { var existingBlockHelpers = UnderlingConfiguration.BlockHelpers.ToDictionary( - o => _pathInfoStore.GetOrAdd($"[{o.Key}]"), + o => (PathInfoLight)_pathInfoStore.GetOrAdd($"[{o.Key}]"), o => new StrongBox(o.Value) ); - BlockHelpers = - new ObservableDictionary>(existingBlockHelpers, Compatibility.RelaxedHelperNaming ? PathInfo.PlainPathComparer : PathInfo.PlainPathWithPartsCountComparer); + BlockHelpers = new ObservableDictionary>(existingBlockHelpers, Compatibility.RelaxedHelperNaming ? PathInfoLight.PlainPathComparer : PathInfoLight.PlainPathWithPartsCountComparer); var blockHelpersObserver = new ObserverBuilder>() .OnEvent.ReplacedObservableEvent>( @@ -143,7 +157,6 @@ private IObjectDescriptorProvider CreateObjectDescriptorProvider() new StringDictionaryObjectDescriptorProvider(), new GenericDictionaryObjectDescriptorProvider(), new DictionaryObjectDescriptor(), - new KeyValuePairObjectDescriptorProvider(), new CollectionObjectDescriptor(objectDescriptorProvider), new EnumerableObjectDescriptor(objectDescriptorProvider), objectDescriptorProvider, diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs index 674b26e5..58b208f6 100644 --- a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -3,7 +3,6 @@ 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; @@ -13,7 +12,7 @@ namespace HandlebarsDotNet { public interface IHandlebarsTemplateRegistrations { - IDictionary> RegisteredTemplates { get; } + IDictionary> RegisteredTemplates { get; } ViewEngineFileSystem FileSystem { get; } } @@ -42,11 +41,11 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat /// IFormatProvider FormatProvider { get; } - /// - /// - /// + [Obsolete("Use `UnresolvedBindingFormat` instead")] string UnresolvedBindingFormatter { get; } + Func UnresolvedBindingFormat { get; } + /// /// /// @@ -65,12 +64,12 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat /// /// /// - IDictionary> Helpers { get; } + IDictionary> Helpers { get; } /// /// /// - IDictionary> BlockHelpers { get; } + IDictionary> BlockHelpers { get; } /// /// @@ -84,7 +83,7 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat IObjectDescriptorProvider ObjectDescriptorProvider { get; } /// - IList ExpressionMiddleware { get; } + IList ExpressionMiddlewares { get; } /// IList AliasProviders { get; } @@ -99,5 +98,7 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat /// IPathInfoStore PathInfoStore { get; } + + bool NoEscape { get; } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/PathInfoStore.cs b/source/Handlebars/Configuration/PathInfoStore.cs index fe20590c..4e960d36 100644 --- a/source/Handlebars/Configuration/PathInfoStore.cs +++ b/source/Handlebars/Configuration/PathInfoStore.cs @@ -1,56 +1,135 @@ -using System.Collections; +using System; using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet { /// /// Provides access to path expressions in the template /// - public interface IPathInfoStore : IReadOnlyDictionary + public interface IPathInfoStore { - /// - /// - /// - /// - /// PathInfo GetOrAdd(string path); } - internal class PathInfoStore : IPathInfoStore + public class PathInfoStore : IPathInfoStore { - private readonly Dictionary _paths = new Dictionary(); + /* + * TODO: migrate to WeakReferences? + */ + + private static readonly Lazy Instance = new Lazy(() => new PathInfoStore()); + + private static readonly Func> ValueFactory = s => + { + return new SafeDeferredValue(s, pathString => + { + return GetPathInfo(pathString); + }); + }; + + public static PathInfoStore Shared => Instance.Value; + + private readonly LookupSlim> _paths = new LookupSlim>(); + + private PathInfoStore(){} public PathInfo GetOrAdd(string path) { - if (_paths.TryGetValue(path, out var pathInfo)) return pathInfo; - - pathInfo = PathResolver.GetPathInfo(path); - _paths.Add(path, pathInfo); + var pathInfo = _paths.GetOrAdd(path, ValueFactory).Value; var trimmedPath = pathInfo.TrimmedPath; - if ((pathInfo.IsBlockHelper || pathInfo.IsInversion) && !_paths.ContainsKey(trimmedPath)) + if (pathInfo.IsBlockHelper || pathInfo.IsInversion) { - _paths.Add(trimmedPath, PathResolver.GetPathInfo(trimmedPath)); + _paths.GetOrAdd(trimmedPath, ValueFactory); } return pathInfo; } + + public static PathInfo GetPathInfo(string path) + { + if (path == "null") + return new PathInfo(false, path, false, null); + + var originalPath = path; + + var isValidHelperLiteral = true; + var isVariable = path.StartsWith("@"); + var isInversion = path.StartsWith("^"); + var isBlockHelper = path.StartsWith("#"); + if (isVariable || isBlockHelper || isInversion) + { + isValidHelperLiteral = isBlockHelper || isInversion; + path = path.Substring(1); + } + + var segments = new List(); + var pathParts = path.Split('/'); + if (pathParts.Length > 1) isValidHelperLiteral = false; + foreach (var segment in pathParts) + { + if (segment == "..") + { + 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; - public IEnumerator> GetEnumerator() => _paths.GetEnumerator(); + segments.Add(new PathSegment(segmentString, chainSegments)); + } + + return new PathInfo(true, originalPath, isValidHelperLiteral, segments.ToArray()); + } - IEnumerator IEnumerable.GetEnumerator() => _paths.GetEnumerator(); + private static IEnumerable GetPathChain(string segmentString) + { + var insideEscapeBlock = false; + var pathChainParts = segmentString.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries); + if (pathChainParts.Length == 0 && segmentString == ".") return new[] {ChainSegment.Create("this")}; - int IReadOnlyCollection>.Count => _paths.Count; + var pathChain = pathChainParts.Aggregate(new List(), (list, next) => + { + if (insideEscapeBlock) + { + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } - bool IReadOnlyDictionary.ContainsKey(string key) => _paths.ContainsKey(key); + list[list.Count - 1] = ChainSegment.Create($"{list[list.Count - 1]}.{next}"); + return list; + } - public bool TryGetValue(string key, out PathInfo value) => _paths.TryGetValue(key, out value); + if (next.StartsWith("[")) + { + insideEscapeBlock = true; + } - public PathInfo this[string key] => _paths[key]; + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } - public IEnumerable Keys => _paths.Keys; + list.Add(ChainSegment.Create(next)); + return list; + }); - public IEnumerable Values => _paths.Values; + return pathChain; + } } } \ No newline at end of file diff --git a/source/Handlebars/DynamicViewModel.cs b/source/Handlebars/DynamicViewModel.cs index 07ef1f39..f09e1711 100644 --- a/source/Handlebars/DynamicViewModel.cs +++ b/source/Handlebars/DynamicViewModel.cs @@ -7,19 +7,17 @@ namespace HandlebarsDotNet { public class DynamicViewModel : DynamicObject { - public object[] Objects { get; set; } - - private static readonly BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance | - BindingFlags.IgnoreCase; + private readonly object[] _objects; + private static readonly BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; public DynamicViewModel(params object[] objects) { - Objects = objects; + _objects = objects; } public override IEnumerable GetDynamicMemberNames() { - return Objects.Select(o => o.GetType()) + return _objects.Select(o => o.GetType()) .SelectMany(t => t.GetMembers(BindingFlags)) .Select(m => m.Name); } @@ -27,21 +25,19 @@ public override IEnumerable GetDynamicMemberNames() public override bool TryGetMember(GetMemberBinder binder, out object result) { result = null; - foreach (var target in Objects) + foreach (var target in _objects) { - var member = target.GetType() - .GetMember(binder.Name, BindingFlags); - + var member = target.GetType().GetMember(binder.Name, BindingFlags); if (member.Length > 0) { if (member[0] is PropertyInfo) { - result = ((PropertyInfo) member[0]).GetValue(target, null); + result = ((PropertyInfo)member[0]).GetValue(target, null); return true; } if (member[0] is FieldInfo) { - result = ((FieldInfo) member[0]).GetValue(target); + result = ((FieldInfo)member[0]).GetValue(target); return true; } } diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index a2e008c8..8928fb2c 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -6,6 +7,12 @@ namespace HandlebarsDotNet { internal static class EnumerableExtensions { + public static bool Any(this IEnumerable builder) + { + var enumerator = builder.GetEnumerator(); + return enumerator.MoveNext(); + } + public static bool IsOneOf(this IEnumerable source) where TExpected : TSource { diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index 94017228..a8f7a35f 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -6,10 +6,7 @@ namespace HandlebarsDotNet.Features { internal class BuildInHelpersFeatureFactory : IFeatureFactory { - public IFeature CreateFeature() - { - return new BuildInHelpersFeature(); - } + public IFeature CreateFeature() => new BuildInHelpersFeature(); } [FeatureOrder(int.MinValue)] @@ -18,8 +15,8 @@ internal class BuildInHelpersFeature : IFeature public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { var pathInfoStore = configuration.PathInfoStore; - configuration.BlockHelpers[pathInfoStore.GetOrAdd("with")] = new StrongBox(new WithBlockHelperDescriptor()); - configuration.BlockHelpers[pathInfoStore.GetOrAdd("*inline")] = new StrongBox(new InlineBlockHelperDescriptor()); + configuration.BlockHelpers[pathInfoStore.GetOrAdd("with")] = new StrongBox(new WithBlockHelperDescriptor(configuration)); + configuration.BlockHelpers[pathInfoStore.GetOrAdd("*inline")] = new StrongBox(new InlineBlockHelperDescriptor(configuration)); configuration.Helpers[pathInfoStore.GetOrAdd("lookup")] = new StrongBox(new LookupReturnHelperDescriptor(configuration)); } diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs deleted file mode 100644 index 89d3ede9..00000000 --- a/source/Handlebars/Features/ClosureFeature.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using Expressions.Shortcuts; - -namespace HandlebarsDotNet.Features -{ - internal class ClosureFeatureFactory : IFeatureFactory - { - public IFeature CreateFeature() - { - return new ClosureFeature(); - } - } - - /// - /// Extracts all items into precompiled closure allowing to compile static delegates - /// - [FeatureOrder(0)] - public class ClosureFeature : IFeature - { - /// - /// Parameter of actual closure - /// - internal ExpressionContainer ClosureInternal { get; } = ExpressionShortcuts.Var("closure"); - - - public ParameterExpression Closure => (ParameterExpression) ClosureInternal.Expression; - - /// - /// Build-time closure store - /// - public TemplateClosure TemplateClosure { get; } = new TemplateClosure(); - - internal LinkedList Children { get; } = new LinkedList(); - - /// - /// Middleware to use in order to apply transformation - /// - public IExpressionMiddleware ExpressionMiddleware { get; } - - - public ClosureFeature() - { - ExpressionMiddleware = new ClosureExpressionMiddleware(TemplateClosure, ClosureInternal); - } - - /// - public void OnCompiling(ICompiledHandlebarsConfiguration configuration) - { - // noting to do here - } - - /// - public void CompilationCompleted() - { - TemplateClosure?.Build(); - - foreach (var child in Children) - { - child.CompilationCompleted(); - } - } - - private class ClosureExpressionMiddleware : IExpressionMiddleware - { - private readonly TemplateClosure _closure; - private readonly ExpressionContainer _closureArg; - - public ClosureExpressionMiddleware(TemplateClosure closure, ExpressionContainer closureArg) - { - _closure = closure; - _closureArg = closureArg; - } - - public Expression Invoke(Expression expression) - { - var closureVisitor = new ClosureVisitor(_closureArg, _closure); - var constantReducer = new ConstantsReducer(); - - expression = closureVisitor.Visit(expression); - return constantReducer.Visit(expression); - } - - private class ClosureVisitor : ExpressionVisitor - { - private readonly TemplateClosure _templateClosure; - private readonly ExpressionContainer _templateClosureArg; - - public ClosureVisitor(ExpressionContainer arg, TemplateClosure templateClosure) - { - _templateClosure = templateClosure; - _templateClosureArg = arg; - } - - protected override Expression VisitLambda(Expression node) - { - var body = Visit(node.Body); - return node.Update(body ?? throw new InvalidOperationException("Cannot create closure"), - node.Parameters); - } - - protected override Expression VisitConstant(ConstantExpression node) - { - switch (node.Value) - { - case null: - case string _: - return node; - - default: - if (node.Type.GetTypeInfo().IsPrimitive) return node; - break; - } - - UnaryExpression unaryExpression; - if (_templateClosure.TryGetKeyByValue(node.Value, out var existingKey)) - { - unaryExpression = - Expression.Convert( - Expression.ArrayIndex(_templateClosureArg, Expression.Constant(existingKey)), - node.Type); - return unaryExpression; - } - - var key = _templateClosure.CurrentIndex; - _templateClosure[key] = node.Value; - var accessor = Expression.ArrayIndex(_templateClosureArg, Expression.Constant(key)); - unaryExpression = Expression.Convert(accessor, node.Type); - return unaryExpression; - } - - protected override Expression VisitMember(MemberExpression node) - { - if (!(node.Expression is ConstantExpression constantExpression)) return base.VisitMember(node); - - switch (node.Member) - { - case PropertyInfo property: - { - var value = property.GetValue(constantExpression.Value); - return VisitConstant(Expression.Constant(value, property.PropertyType)); - } - - case FieldInfo field: - { - var value = field.GetValue(constantExpression.Value); - return VisitConstant(Expression.Constant(value, field.FieldType)); - } - - default: - { - var constant = VisitConstant(constantExpression); - return node.Update(constant); - } - } - } - } - - private class ConstantsReducer : ExpressionVisitor - { - private readonly Dictionary _expressions = new Dictionary(); - - protected override Expression VisitConstant(ConstantExpression node) - { - if(node.Value != null && _expressions.TryGetValue(node.Value, out var storedNode)) - { - return storedNode; - } - - if (node.Value != null) - { - _expressions.Add(node.Value, node); - } - - return node; - } - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index dc72879b..54ffbeb7 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -1,29 +1,22 @@ +using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using Expressions.Shortcuts; -using static Expressions.Shortcuts.ExpressionShortcuts; +using HandlebarsDotNet.Compiler.Middlewares; namespace HandlebarsDotNet.Features { internal class DefaultCompilerFeatureFactory : IFeatureFactory { - public IFeature CreateFeature() - { - return new DefaultCompilerFeature(); - } + public IFeature CreateFeature() => new DefaultCompilerFeature(); } - [FeatureOrder(1)] + [FeatureOrder(2)] internal class DefaultCompilerFeature : IFeature { public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - var templateFeature = configuration.Features - .OfType() - .SingleOrDefault(); - - configuration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); + configuration.ExpressionMiddlewares.Insert(0, new ClosureExpressionMiddleware()); + configuration.ExpressionCompiler = new DefaultExpressionCompiler(configuration); } public void CompilationCompleted() @@ -33,43 +26,21 @@ public void CompilationCompleted() private class DefaultExpressionCompiler : IExpressionCompiler { - private readonly ClosureFeature _closureFeature; - private readonly ICollection _expressionMiddleware; - - public DefaultExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) + private readonly IList _expressionMiddleware; + + public DefaultExpressionCompiler(ICompiledHandlebarsConfiguration configuration) { - _closureFeature = closureFeature; - _expressionMiddleware = configuration.ExpressionMiddleware; + _expressionMiddleware = configuration.ExpressionMiddlewares; } - - public T Compile(Expression expression) where T: class + + public T Compile(Expression expression) where T: class, Delegate { - expression = (Expression) _expressionMiddleware.Aggregate((Expression) expression, (e, m) => m.Invoke(e)); - - var closureFeature = _closureFeature; - - if (closureFeature.TemplateClosure.CurrentIndex == -1) + for (var index = 0; index < _expressionMiddleware.Count; index++) { - closureFeature = new ClosureFeature(); - _closureFeature.Children.AddLast(closureFeature); + expression = _expressionMiddleware[index].Invoke(expression); } - - var templateClosure = closureFeature.TemplateClosure; - var closure = closureFeature.ClosureInternal; - - expression = (Expression) closureFeature.ExpressionMiddleware.Invoke(expression); - - 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 parameterExpressions = new[] { store.Expression }.Concat(outerParameters); - var invocationExpression = Expression.Invoke(Expression.Constant(compiledLambda), parameterExpressions); - var outerLambda = Expression.Lambda(invocationExpression, outerParameters); - return outerLambda.Compile(); + return expression.Compile(); } } } diff --git a/source/Handlebars/Features/TemplateClosure.cs b/source/Handlebars/Features/TemplateClosure.cs deleted file mode 100644 index 430b0e80..00000000 --- a/source/Handlebars/Features/TemplateClosure.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace HandlebarsDotNet.Features -{ - /// - /// Used by to store compiled lambda closure - /// - public sealed class TemplateClosure - { - private Dictionary _objectSet = new Dictionary(); - private List _inner = new List(); - - /// - /// Actual closure storage - /// - public object[] Store { get; private set; } - - internal int CurrentIndex => _inner?.Count ?? -1; - - internal object this[int key] - { - set - { - if(value == null) return; - _inner?.Add(value); - _objectSet?.Add(value, key); - } - } - - internal bool TryGetKeyByValue(object obj, out int key) - { - key = -1; - if (obj != null) return _objectSet?.TryGetValue(obj, out key) ?? false; - - return false; - } - - internal void Build() - { - if(_inner == null) return; - - Store = new object[_inner.Count]; - _inner.CopyTo(Store, 0); - - _inner.Clear(); - _inner = null; - _objectSet?.Clear(); - _objectSet = null; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/FileSystemPartialTemplateResolver.cs b/source/Handlebars/FileSystemPartialTemplateResolver.cs index b5c9baa8..a4c1def5 100644 --- a/source/Handlebars/FileSystemPartialTemplateResolver.cs +++ b/source/Handlebars/FileSystemPartialTemplateResolver.cs @@ -25,9 +25,9 @@ public bool TryRegisterPartial(IHandlebars env, string partialName, string templ var compiled = env .CompileView(partialPath); - handlebarsTemplateRegistrations.RegisteredTemplates.Add(partialName, (writer, o) => + handlebarsTemplateRegistrations.RegisteredTemplates.Add(partialName, (writer, o, data) => { - writer.Write(compiled(o)); + writer.Write(compiled(o, data)); }); return true; diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index 36476ddb..da14a98c 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.IO; +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Helpers.BlockHelpers; @@ -12,14 +13,14 @@ namespace HandlebarsDotNet /// /// /// - public delegate void HandlebarsHelper(TextWriter output, dynamic context, params object[] arguments); + public delegate void HandlebarsHelper(EncodedTextWriter output, object context, Arguments arguments); /// /// InlineHelper: {{#helper arg1 arg2}}, supports value return /// /// /// - public delegate object HandlebarsReturnHelper(dynamic context, params object[] arguments); + public delegate object HandlebarsReturnHelper(object context, Arguments arguments); /// /// BlockHelper: {{#helper}}..{{/helper}} @@ -28,7 +29,7 @@ namespace HandlebarsDotNet /// /// /// - public delegate void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments); + public delegate void HandlebarsBlockHelper(EncodedTextWriter output, HelperOptions options, object context, Arguments arguments); /// /// BlockHelper: {{#helper}}..{{/helper}} @@ -36,7 +37,7 @@ namespace HandlebarsDotNet /// /// /// - public delegate object HandlebarsReturnBlockHelper(HelperOptions options, dynamic context, params object[] arguments); + public delegate object HandlebarsReturnBlockHelper(HelperOptions options, object context, Arguments arguments); public sealed class Handlebars @@ -74,27 +75,27 @@ internal static IHandlebars Create(ICompiledHandlebarsConfiguration configuratio /// /// /// - public static Action Compile(TextReader template) + public static HandlebarsTemplate Compile(TextReader template) { return Instance.Compile(template); } - public static Func Compile(string template) + public static HandlebarsTemplate Compile(string template) { return Instance.Compile(template); } - public static Func CompileView(string templatePath) + public static HandlebarsTemplate CompileView(string templatePath) { return Instance.CompileView(templatePath); } - public static Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + public static HandlebarsTemplate CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) { return Instance.CompileView(templatePath, readerFactoryFactory); } - public static void RegisterTemplate(string templateName, Action template) + public static void RegisterTemplate(string templateName, HandlebarsTemplate template) { Instance.RegisterTemplate(templateName, template); } @@ -166,18 +167,5 @@ public static void RegisterHelper(BlockHelperDescriptorBase 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 f7f4526d..98a2c4a7 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -2,9 +2,8 @@ Handlebars - portable 9822C7B8-7E51-42BC-9A49-72A10491B202 - netstandard1.3;netstandard2.0 + netstandard1.3;netstandard2.0;netstandard2.1 $(TargetFrameworks);net451;net452 2.0.0 HandlebarsDotNet @@ -16,6 +15,12 @@ $(DefineConstants);netstandard + + $(DefineConstants);NET451 + + + $(DefineConstants);NET452 + Rex Morgan @@ -46,19 +51,23 @@ + + + + + - + - \ No newline at end of file diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 4ac954cc..f0f0f88b 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -36,25 +36,25 @@ internal HandlebarsEnvironment(ICompiledHandlebarsConfiguration configuration) internal ICompiledHandlebarsConfiguration CompiledConfiguration { get; } ICompiledHandlebarsConfiguration ICompiledHandlebars.CompiledConfiguration => CompiledConfiguration; - public Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + public HandlebarsTemplate CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) { readerFactoryFactory ??= ViewReaderFactory; return CompileViewInternal(templatePath, readerFactoryFactory); } - public Func CompileView(string templatePath) + public HandlebarsTemplate CompileView(string templatePath) { var view = CompileViewInternal(templatePath, ViewReaderFactory); - return (vm) => + return (vm, data) => { var formatProvider = Configuration?.FormatProvider ?? CompiledConfiguration.FormatProvider; using var writer = ReusableStringWriter.Get(formatProvider); - view(writer, vm); + view(writer, vm, data); return writer.ToString(); }; } - private Action CompileViewInternal(string templatePath, ViewReaderFactory readerFactoryFactory) + private HandlebarsTemplate CompileViewInternal(string templatePath, ViewReaderFactory readerFactoryFactory) { var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration); var createdFeatures = configuration.Features; @@ -70,32 +70,80 @@ private Action CompileViewInternal(string templatePath, View createdFeatures[index].CompilationCompleted(); } - return compiledView; + return (writer, context, data) => + { + if (context is BindingContext bindingContext) + { + var config = bindingContext.Configuration; + using var encodedTextWriter = new EncodedTextWriter(writer, config.TextEncoder, config.UnresolvedBindingFormat, config.NoEscape); + compiledView(encodedTextWriter, bindingContext); + } + else + { + using var newBindingContext = BindingContext.Create(configuration, context, templatePath); + newBindingContext.SetDataObject(data); + + using var encodedTextWriter = new EncodedTextWriter(writer, configuration.TextEncoder, configuration.UnresolvedBindingFormat, configuration.NoEscape); + compiledView(encodedTextWriter, newBindingContext); + } + }; } - public Action Compile(TextReader template) + public HandlebarsTemplate Compile(TextReader template) { var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration); using var reader = new ExtendedStringReader(template); - return HandlebarsCompiler.Compile(reader, configuration); + var compiledTemplate = HandlebarsCompiler.Compile(reader, configuration); + return (writer, context, data) => + { + if (writer is EncodedTextWriterWrapper encodedTextWriterWrapper) + { + var encodedTextWriter = encodedTextWriterWrapper.UnderlyingWriter; + if (context is BindingContext bindingContext) + { + compiledTemplate(encodedTextWriter, bindingContext); + return; + } + + using var newBindingContext = BindingContext.Create(configuration, context); + newBindingContext.SetDataObject(data); + + compiledTemplate(encodedTextWriter, newBindingContext); + } + else + { + if (context is BindingContext bindingContext) + { + var config = bindingContext.Configuration; + using var encodedTextWriter = new EncodedTextWriter(writer, config.TextEncoder, config.UnresolvedBindingFormat, config.NoEscape); + compiledTemplate(encodedTextWriter, bindingContext); + } + else + { + using var newBindingContext = BindingContext.Create(configuration, context); + newBindingContext.SetDataObject(data); + + using var encodedTextWriter = new EncodedTextWriter(writer, configuration.TextEncoder, configuration.UnresolvedBindingFormat, configuration.NoEscape); + compiledTemplate(encodedTextWriter, newBindingContext); + } + } + }; } - public Func Compile(string template) + public HandlebarsTemplate Compile(string template) { - using (var reader = new StringReader(template)) + using var reader = new StringReader(template); + var compiledTemplate = Compile(reader); + return (context, data) => { - var compiledTemplate = Compile(reader); - return context => - { - var formatProvider = Configuration?.FormatProvider ?? CompiledConfiguration?.FormatProvider; - using var writer = ReusableStringWriter.Get(formatProvider); - compiledTemplate(writer, context); - return writer.ToString(); - }; - } + var formatProvider = Configuration?.FormatProvider ?? CompiledConfiguration?.FormatProvider; + using var writer = ReusableStringWriter.Get(formatProvider); + compiledTemplate(writer, context, data); + return writer.ToString(); + }; } - public void RegisterTemplate(string templateName, Action template) + public void RegisterTemplate(string templateName, HandlebarsTemplate template) { var registrations = Configuration ?? (IHandlebarsTemplateRegistrations) CompiledConfiguration; registrations.RegisteredTemplates[templateName] = template; @@ -104,7 +152,7 @@ public void RegisterTemplate(string templateName, Action tem public void RegisterTemplate(string templateName, string template) { using var reader = new StringReader(template); - RegisterTemplate(templateName,Compile(reader)); + RegisterTemplate(templateName, Compile(reader)); } public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) diff --git a/source/Handlebars/HandlebarsExtensions.cs b/source/Handlebars/HandlebarsExtensions.cs index aff3f466..bf1c92bb 100644 --- a/source/Handlebars/HandlebarsExtensions.cs +++ b/source/Handlebars/HandlebarsExtensions.cs @@ -3,7 +3,6 @@ namespace HandlebarsDotNet { - public static class HandlebarsExtensions { /// @@ -11,9 +10,9 @@ public static class HandlebarsExtensions /// /// /// - public static void WriteSafeString(this TextWriter writer, string value) + public static void WriteSafeString(this in EncodedTextWriter writer, string value) { - writer.Write(new SafeString(value)); + writer.Write(value, false); } /// @@ -21,15 +20,15 @@ public static void WriteSafeString(this TextWriter writer, string value) /// /// /// - public static void WriteSafeString(this TextWriter writer, object value) + public static void WriteSafeString(this in EncodedTextWriter writer, object value) { if (value is string str) { - writer.Write(new SafeString(str)); + writer.Write(str, false); return; } - writer.Write(new SafeString(value.ToString())); + writer.Write(value.ToString(), false); } /// @@ -44,28 +43,6 @@ public static HandlebarsConfiguration Configure(this HandlebarsConfiguration con return configuration; } - - public static object This(this HelperOptions options, object context, Func> selector) - { - 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/HandlebarsUtils.cs b/source/Handlebars/HandlebarsUtils.cs index b408e3fd..db1c453f 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -1,7 +1,6 @@ using System; using HandlebarsDotNet.Compiler; using System.Collections; -using System.Linq; namespace HandlebarsDotNet { @@ -54,7 +53,7 @@ public static bool IsFalsyOrEmpty(object value) return true; } - return value is IEnumerable enumerable && !enumerable.OfType().Any(); + return value is IEnumerable enumerable && !enumerable.Any(); } private static bool IsNumber(object value) diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index 9cf07c3c..a1afbbd0 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,78 +1,104 @@ using System; -using System.Collections.Generic; -using System.IO; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; -using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet { /// /// Contains properties accessible withing function /// - public sealed class HelperOptions : IDisposable + public readonly struct HelperOptions { - private static readonly InternalObjectPool Pool = new InternalObjectPool(new Policy()); + private readonly FixedSizeDictionary _extensions; - private readonly Dictionary _extensions; + internal readonly TemplateDelegate OriginalTemplate; + internal readonly TemplateDelegate OriginalInverse; + + public readonly BindingContext Frame; + + public readonly ChainSegment[] BlockParams; - internal static HelperOptions Create(Action template, - Action inverse, + internal HelperOptions( + TemplateDelegate template, + TemplateDelegate inverse, ChainSegment[] blockParamsValues, - BindingContext bindingContext) + BindingContext frame + ) { - var item = Pool.Get(); - - item.OriginalTemplate = template; - item.OriginalInverse = inverse; + _extensions = frame.Extensions; - item.BindingContext = bindingContext; - item.Configuration = bindingContext.Configuration; - item.BlockParams = blockParamsValues; - item.Data = new DataValues(bindingContext); - - return item; + OriginalTemplate = template; + OriginalInverse = inverse; + Frame = frame; + BlockParams = blockParamsValues; } - private HelperOptions() + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Template(in EncodedTextWriter writer, object context) { - _extensions = new Dictionary(7); - Template = (writer, o) => OriginalTemplate(BindingContext, writer, o); - Inverse = (writer, o) => OriginalInverse(BindingContext, writer, o); + if (context is BindingContext bindingContext) + { + OriginalTemplate(writer, bindingContext); + return; + } + + using var frame = Frame.CreateFrame(context); + OriginalTemplate(writer, frame); } - + /// /// BlockHelper body /// - public Action Template { get; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Template(in EncodedTextWriter writer, BindingContext context) + { + OriginalTemplate(writer, context); + } + /// - /// BlockHelper else body + /// BlockHelper body /// - public Action Inverse { get; } - - public ChainSegment[] BlockParams { get; private set; } - - public DataValues Data { get; private set; } - - internal ICompiledHandlebarsConfiguration Configuration { get; private set; } - internal BindingContext BindingContext { get; private set; } - internal Action OriginalTemplate { get; private set; } - internal Action OriginalInverse { get; private set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inverse(in EncodedTextWriter writer, object context) + { + if (context is BindingContext bindingContext) + { + OriginalInverse(writer, bindingContext); + return; + } + + using var frame = Frame.CreateFrame(context); + OriginalInverse(writer, frame); + } - public BindingContext CreateFrame(object value = null) + /// + /// BlockHelper body + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inverse(in EncodedTextWriter writer, BindingContext context) { - return BindingContext.CreateFrame(value); + OriginalInverse(writer, context); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BindingContext CreateFrame(object value = null) => Frame.CreateFrame(value); + /// /// Provides access to dynamic options /// /// public object this[string property] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _extensions.TryGetValue(property, out var value) ? value : null; - internal set => _extensions[property] = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal set => _extensions.AddOrReplace(property, value, out _); } /// @@ -81,27 +107,8 @@ public object this[string property] /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public T GetValue(string property) => (T) this[property]; - - public void Dispose() => Pool.Return(this); - - private class Policy : IInternalObjectPoolPolicy - { - public HelperOptions Create() => new HelperOptions(); - - public bool Return(HelperOptions item) - { - item._extensions.Clear(); - - item.Configuration = null; - item.BindingContext = null; - item.BlockParams = null; - item.OriginalInverse = null; - item.OriginalTemplate = null; - - return true; - } - } } } diff --git a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs index 52b4c538..b4bb4cbc 100644 --- a/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs +++ b/source/Handlebars/Helpers/BlockHelpers/BlockHelperDescriptorBase.cs @@ -1,11 +1,11 @@ -using System.IO; +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Helpers.BlockHelpers { public abstract class BlockHelperDescriptorBase : IHelperDescriptor { - protected BlockHelperDescriptorBase(string name) => Name = PathResolver.GetPathInfo(name); + protected BlockHelperDescriptorBase(string name) => Name = PathInfoStore.Shared.GetOrAdd(name); protected BlockHelperDescriptorBase(PathInfo name) => Name = name; @@ -13,6 +13,6 @@ public abstract class BlockHelperDescriptorBase : IHelperDescriptor public abstract HelperType Type { get; } - public abstract void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments); + public abstract void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments arguments); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs index df39d802..02216b4f 100644 --- a/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/DelegateBlockHelperDescriptor.cs @@ -1,4 +1,3 @@ -using System.IO; using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Helpers.BlockHelpers @@ -15,7 +14,7 @@ public DelegateBlockHelperDescriptor(PathInfo name, HandlebarsBlockHelper helper _helper = helper; } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments 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 index 67ede7b0..ab0f0834 100644 --- a/source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/DelegateReturnBlockHelperDescriptor.cs @@ -16,7 +16,7 @@ public DelegateReturnBlockHelperDescriptor(PathInfo name, HandlebarsReturnBlockH _helper = helper; } - public override object Invoke(HelperOptions options, object context, params object[] arguments) + public override object Invoke(in HelperOptions options, object context, in Arguments arguments) { return _helper(options, context, arguments); } diff --git a/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs index 9d134134..70ef7ed7 100644 --- a/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/InlineBlockHelperDescriptor.cs @@ -1,15 +1,14 @@ -using System.IO; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet.Helpers.BlockHelpers { internal sealed class InlineBlockHelperDescriptor : BlockHelperDescriptor { - public InlineBlockHelperDescriptor() : base("*inline") + public InlineBlockHelperDescriptor(ICompiledHandlebarsConfiguration configuration) : base(configuration.PathInfoStore.GetOrAdd("*inline")) { } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments arguments) { if (arguments.Length != 1) { @@ -32,7 +31,7 @@ public override void Invoke(TextWriter output, HelperOptions options, object con //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)); + bindingContext.InlinePartialTemplates.Add(key, (writer, c) => template(writer, c)); } } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs index 33dea97e..28194e20 100644 --- a/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Structure.Path; @@ -24,8 +23,9 @@ public LateBindBlockHelperDescriptor(PathInfo name, ICompiledHandlebarsConfigura _blockHelperMissing = _configuration.BlockHelpers[pathInfoStore.GetOrAdd("blockHelperMissing")]; } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments arguments) { + // TODO: add cache var helperResolvers = (ObservableList)_configuration.HelperResolvers; if(helperResolvers.Count != 0) { diff --git a/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs b/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs index 2f4f3781..a75dbec1 100644 --- a/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/MissingBlockHelperDescriptor.cs @@ -1,4 +1,3 @@ -using System.IO; using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; @@ -10,14 +9,14 @@ public MissingBlockHelperDescriptor() : base("missingBlockHelper") { } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments 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 bindingContext = options.Frame; var value = PathResolver.ResolvePath(bindingContext, pathInfo); - DeferredSectionBlockHelper.PlainHelper(bindingContext, value, options.OriginalTemplate, options.OriginalInverse); + DeferredSectionBlockHelper.PlainHelper(bindingContext, output, 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 index 86d87c48..3e31d3ec 100644 --- a/source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/ReturnBlockHelperDescriptor.cs @@ -1,4 +1,5 @@ using System.IO; +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Helpers.BlockHelpers @@ -15,9 +16,9 @@ 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 abstract object Invoke(in HelperOptions options, object context, in Arguments arguments); - public sealed override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) => + public sealed override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments 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 index 34e901d7..192763c1 100644 --- a/source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs +++ b/source/Handlebars/Helpers/BlockHelpers/WithBlockHelperDescriptor.cs @@ -1,15 +1,14 @@ -using System.IO; using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Helpers.BlockHelpers { internal sealed class WithBlockHelperDescriptor : BlockHelperDescriptor { - public WithBlockHelperDescriptor() : base("with") + public WithBlockHelperDescriptor(ICompiledHandlebarsConfiguration configuration) : base(configuration.PathInfoStore.GetOrAdd("with")) { } - public override void Invoke(TextWriter output, HelperOptions options, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, in HelperOptions options, object context, in Arguments arguments) { if (arguments.Length != 1) { diff --git a/source/Handlebars/Helpers/DelegateHelperDescriptor.cs b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs index 1e4d4351..ec2dcb95 100644 --- a/source/Handlebars/Helpers/DelegateHelperDescriptor.cs +++ b/source/Handlebars/Helpers/DelegateHelperDescriptor.cs @@ -1,4 +1,5 @@ using System.IO; +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Helpers @@ -13,7 +14,7 @@ public DelegateHelperDescriptor(PathInfo name, HandlebarsHelper helper) : base(n public DelegateHelperDescriptor(string name, HandlebarsHelper helper) : base(name) => _helper = helper; - public override void Invoke(TextWriter output, object context, params object[] arguments) + public override void Invoke(in EncodedTextWriter output, object context, in Arguments 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 580b448a..5b818435 100644 --- a/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/DelegateReturnHelperDescriptor.cs @@ -1,3 +1,4 @@ +using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Helpers @@ -12,7 +13,7 @@ public DelegateReturnHelperDescriptor(PathInfo name, HandlebarsReturnHelper help public DelegateReturnHelperDescriptor(string name, HandlebarsReturnHelper helper) : base(name) => _helper = helper; - public override object Invoke(object context, params object[] arguments) + public override object Invoke(object context, in Arguments arguments) => _helper(context, arguments); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/HelperDescriptor.cs b/source/Handlebars/Helpers/HelperDescriptor.cs index 5d54662e..bb290cc6 100644 --- a/source/Handlebars/Helpers/HelperDescriptor.cs +++ b/source/Handlebars/Helpers/HelperDescriptor.cs @@ -1,4 +1,3 @@ -using System.IO; using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; @@ -16,15 +15,17 @@ 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); + public abstract void Invoke(in EncodedTextWriter output, object context, in Arguments arguments); - internal sealed override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + internal sealed override object ReturnInvoke(BindingContext bindingContext, object context, in Arguments arguments) { - using var writer = ReusableStringWriter.Get(bindingContext.Configuration.FormatProvider); - WriteInvoke(bindingContext, writer, context, arguments); + var configuration = bindingContext.Configuration; + using var writer = ReusableStringWriter.Get(configuration.FormatProvider); + using var encodedTextWriter = new EncodedTextWriter(writer, configuration.TextEncoder, configuration.UnresolvedBindingFormat, configuration.NoEscape); + WriteInvoke(bindingContext, encodedTextWriter, context, arguments); return writer.ToString(); } - internal sealed override void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments) => Invoke(output, context, arguments); + internal sealed override void WriteInvoke(BindingContext bindingContext, in EncodedTextWriter output, object context, in Arguments 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 index 67a6e493..d3f8c245 100644 --- a/source/Handlebars/Helpers/HelperDescriptorBase.cs +++ b/source/Handlebars/Helpers/HelperDescriptorBase.cs @@ -1,4 +1,3 @@ -using System.IO; using HandlebarsDotNet.Compiler; using HandlebarsDotNet.Compiler.Structure.Path; @@ -6,17 +5,17 @@ namespace HandlebarsDotNet.Helpers { public abstract class HelperDescriptorBase : IHelperDescriptor { - protected HelperDescriptorBase(string name) => Name = PathResolver.GetPathInfo(name); + protected HelperDescriptorBase(string name) => Name = PathInfoStore.Shared.GetOrAdd(name); protected HelperDescriptorBase(PathInfo name) => Name = name; public PathInfo Name { get; } public abstract HelperType Type { get; } + + internal abstract object ReturnInvoke(BindingContext bindingContext, object context, in Arguments arguments); - internal abstract object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments); + internal abstract void WriteInvoke(BindingContext bindingContext, in EncodedTextWriter output, object context, in Arguments arguments); - internal abstract void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments); - public override string ToString() => Name; } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/LateBindHelperDescriptor.cs b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs index 8aa4f2c3..ed58391e 100644 --- a/source/Handlebars/Helpers/LateBindHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LateBindHelperDescriptor.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Runtime.CompilerServices; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler; @@ -26,12 +25,13 @@ public LateBindHelperDescriptor(string name, ICompiledHandlebarsConfiguration co _helperMissing = _configuration.Helpers[pathInfoStore.GetOrAdd("helperMissing")]; } - internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + internal override object ReturnInvoke(BindingContext bindingContext, object context, in Arguments arguments) { + // TODO: add cache var helperResolvers = (ObservableList) _configuration.HelperResolvers; if(helperResolvers.Count != 0) { - var targetType = arguments.FirstOrDefault()?.GetType(); + var targetType = arguments.Length > 0 ? arguments[0].GetType() : null; for (var index = 0; index < helperResolvers.Count; index++) { var resolver = helperResolvers[index]; @@ -43,14 +43,12 @@ internal override object ReturnInvoke(BindingContext bindingContext, object cont 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); + + var newArguments = arguments.Add(Name.TrimmedPath); + return _helperMissing.Value.ReturnInvoke(bindingContext, context, newArguments); } - public override object Invoke(object context, params object[] arguments) + public override object Invoke(object context, in Arguments arguments) { throw new NotImplementedException(); } diff --git a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs index 3c9c5f6f..1e5a60a9 100644 --- a/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/LookupReturnHelperDescriptor.cs @@ -11,7 +11,7 @@ public LookupReturnHelperDescriptor(ICompiledHandlebarsConfiguration configurati _configuration = configuration; } - public override object Invoke(object context, params object[] arguments) + public override object Invoke(object context, in Arguments arguments) { if (arguments.Length != 2) { @@ -21,7 +21,7 @@ public override object Invoke(object context, params object[] arguments) var segment = ChainSegment.Create(arguments[1]); return !PathResolver.TryAccessMember(arguments[0], segment, _configuration, out var value) - ? segment.GetUndefinedBindingResult(_configuration) + ? UndefinedBindingResult.Create(segment) : value; } } diff --git a/source/Handlebars/Helpers/MissingHelperDescriptor.cs b/source/Handlebars/Helpers/MissingHelperDescriptor.cs index fdbf5d57..ae5a3bb2 100644 --- a/source/Handlebars/Helpers/MissingHelperDescriptor.cs +++ b/source/Handlebars/Helpers/MissingHelperDescriptor.cs @@ -10,18 +10,18 @@ public MissingHelperDescriptor() : base("helperMissing") { } - internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) + internal override object ReturnInvoke(BindingContext bindingContext, object context, in Arguments arguments) { - var nameArgument = arguments.Last(); + var nameArgument = arguments[arguments.Length - 1]; 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); + var name = PathInfoStore.Shared.GetOrAdd(nameArgument as string ?? nameArgument.ToString()); + return UndefinedBindingResult.Create(name); } - public override object Invoke(object context, params object[] arguments) => throw new NotSupportedException(); + public override object Invoke(object context, in Arguments arguments) => throw new NotSupportedException(); } } \ No newline at end of file diff --git a/source/Handlebars/Helpers/ReturnHelperDescriptor.cs b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs index a9d1e06e..f9b5cb61 100644 --- a/source/Handlebars/Helpers/ReturnHelperDescriptor.cs +++ b/source/Handlebars/Helpers/ReturnHelperDescriptor.cs @@ -16,12 +16,12 @@ protected ReturnHelperDescriptor(string name) : base(name) public sealed override HelperType Type { get; } = HelperType.Return; - public abstract object Invoke(object context, params object[] arguments); + public abstract object Invoke(object context, in Arguments arguments); - internal override object ReturnInvoke(BindingContext bindingContext, object context, object[] arguments) => + internal override object ReturnInvoke(BindingContext bindingContext, object context, in Arguments arguments) => Invoke(context, arguments); - internal sealed override void WriteInvoke(BindingContext bindingContext, TextWriter output, object context, object[] arguments) => + internal sealed override void WriteInvoke(BindingContext bindingContext, in EncodedTextWriter output, object context, in Arguments 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 82f690ed..eee84758 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -1,10 +1,18 @@ -using System; using System.IO; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Helpers.BlockHelpers; namespace HandlebarsDotNet { + public delegate string HandlebarsTemplate(TContext context, TData data = null) + where TData: class + where TContext: class; + + public delegate void HandlebarsTemplate(TWriter writer, TContext context, TData data = null) + where TWriter: TextWriter + where TData: class + where TContext: class; + /// /// /// @@ -15,17 +23,17 @@ public interface IHandlebars /// /// /// - Action Compile(TextReader template); - - Func Compile(string template); + HandlebarsTemplate Compile(TextReader template); + + HandlebarsTemplate Compile(string template); - Func CompileView(string templatePath); + HandlebarsTemplate CompileView(string templatePath); - Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory); + HandlebarsTemplate CompileView(string templatePath, ViewReaderFactory readerFactoryFactory); HandlebarsConfiguration Configuration { get; } - void RegisterTemplate(string templateName, Action template); + void RegisterTemplate(string templateName, HandlebarsTemplate template); void RegisterTemplate(string templateName, string template); diff --git a/source/Handlebars/IMissingPartialTemplateHandler.cs b/source/Handlebars/IMissingPartialTemplateHandler.cs index 60782036..c0eef036 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(ICompiledHandlebarsConfiguration configuration, string partialName, TextWriter textWriter); + void Handle(ICompiledHandlebarsConfiguration configuration, string partialName, in EncodedTextWriter textWriter); } } diff --git a/source/Handlebars/IO/EncodedTextWriter.cs b/source/Handlebars/IO/EncodedTextWriter.cs index 57267535..861dcc92 100644 --- a/source/Handlebars/IO/EncodedTextWriter.cs +++ b/source/Handlebars/IO/EncodedTextWriter.cs @@ -1,100 +1,171 @@ -using System.IO; +using System; +using System.IO; +using System.Runtime.CompilerServices; using System.Text; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { - internal sealed class EncodedTextWriter : TextWriter + public readonly struct EncodedTextWriter : IDisposable { - private static readonly EncodedTextWriterPool Pool = new EncodedTextWriterPool(); - - private ITextEncoder _encoder; - - public bool SuppressEncoding { get; set; } + private readonly TextWriter _underlyingWriter; + private readonly Func _undefinedFormatter; - private EncodedTextWriter() + private readonly TextEncoderWrapper _encoder; + + public bool SuppressEncoding { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !_encoder.Enabled; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _encoder.Enabled = !value; } - - public static EncodedTextWriter From(TextWriter writer, ITextEncoder encoder) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EncodedTextWriter( + TextWriter writer, + ITextEncoder encoder, + Func undefinedFormatter, + bool suppressEncoding = false) { - if (writer is EncodedTextWriter encodedTextWriter) return encodedTextWriter; - - var textWriter = Pool.Get(); - textWriter._encoder = encoder; - textWriter.UnderlyingWriter = writer; + _underlyingWriter = writer; + _undefinedFormatter = undefinedFormatter; - return textWriter; + _encoder = encoder != null + ? TextEncoderWrapper.Create(encoder) + : TextEncoderWrapper.Null; + + SuppressEncoding = suppressEncoding; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TextWriter CreateWrapper() => EncodedTextWriterWrapper.From(this); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(string value, bool encode) { - if(encode && !SuppressEncoding && (_encoder != null)) + if(encode && !SuppressEncoding) { - value = _encoder.Encode(value); + _encoder.Encode(value, _underlyingWriter); + return; } - - UnderlyingWriter.Write(value); - } - - public override void Write(string value) - { - Write(value, true); - } - - public override void Write(char value) - { - Write(value.ToString(), true); + + _underlyingWriter.Write(value); } - public override void Write(object value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(StringBuilder value, bool encode = true) { - if (value == null) + if(encode && !SuppressEncoding) { + _encoder.Encode(value, _underlyingWriter); return; } - if (value is ISafeString safeString) + for (int i = 0; i < value.Length; i++) { - Write(safeString.Value, false); + _underlyingWriter.Write(value[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(string value) + { + if(!SuppressEncoding) + { + _encoder.Encode(value, _underlyingWriter); return; } - - var @string = value as string ?? value.ToString(); - if(string.IsNullOrEmpty(@string)) return; - - Write(@string, true); + + _underlyingWriter.Write(value); } - public TextWriter UnderlyingWriter { get; private set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(string format, params object[] arguments) => Write(string.Format(format, arguments)); - protected override void Dispose(bool disposing) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(char value) { - Pool.Return(this); + if (_encoder.ShouldEncode(value)) + { + Write(value.ToString(), true); + } + else + { + _underlyingWriter.Write(value); + } } - public override Encoding Encoding => UnderlyingWriter.Encoding; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(UndefinedBindingResult undefined) => _underlyingWriter.Write(_undefinedFormatter(undefined)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(int value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(double value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(float value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(bool value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(decimal value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(short value) => _underlyingWriter.Write(value); - private class EncodedTextWriterPool : InternalObjectPool + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(long value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(uint value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ushort value) => _underlyingWriter.Write(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(object value) { - public EncodedTextWriterPool() : base(new Policy()) + switch (value) { - } + case string v: Write(v); return; + case UndefinedBindingResult v: Write(v); return; + case bool v: Write(v); return; + case int v: Write(v); return; + case char v: Write(v); return; + case float v: Write(v); return; + case double v: Write(v); return; + case long v: Write(v); return; + case short v: Write(v); return; + case uint v: Write(v); return; + case ulong v: Write(v); return; + case ushort v: Write(v); return; + case decimal v: Write(v); return; + + default: + var @string = value.ToString(); + if(string.IsNullOrEmpty(@string)) return; - private class Policy : IInternalObjectPoolPolicy - { - public EncodedTextWriter Create() - { - return new EncodedTextWriter(); - } - - public bool Return(EncodedTextWriter obj) - { - obj._encoder = null; - obj.UnderlyingWriter = null; - - return true; - } + Write(@string); + return; } } + + public Encoding Encoding + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _underlyingWriter.Encoding; + } + + public void Dispose() => _encoder.Dispose(); + + public override string ToString() => _underlyingWriter.ToString(); } } \ No newline at end of file diff --git a/source/Handlebars/IO/EncodedTextWriterWrapper.cs b/source/Handlebars/IO/EncodedTextWriterWrapper.cs new file mode 100644 index 00000000..615335e9 --- /dev/null +++ b/source/Handlebars/IO/EncodedTextWriterWrapper.cs @@ -0,0 +1,88 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet +{ + internal sealed class EncodedTextWriterWrapper : TextWriter + { + private static readonly EncodedTextWriterPool Pool = new EncodedTextWriterPool(); + + public EncodedTextWriter UnderlyingWriter { get; private set; } + + public static TextWriter From(EncodedTextWriter encodedTextWriter) + { + var textWriter = Pool.Get(); + textWriter.UnderlyingWriter = encodedTextWriter; + + return textWriter; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(string value, bool encode) => UnderlyingWriter.Write(value, encode); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(StringBuilder value, bool encode) => UnderlyingWriter.Write(value, encode); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(UndefinedBindingResult undefined) => UnderlyingWriter.Write(undefined); + + public override void Write(string value) => UnderlyingWriter.Write(value); + + public override void Write(char value) => UnderlyingWriter.Write(value); + + public override void Write(int value) => UnderlyingWriter.Write(value); + + public override void Write(double value) => UnderlyingWriter.Write(value); + + public override void Write(float value) => UnderlyingWriter.Write(value); + + public override void Write(decimal value) => UnderlyingWriter.Write(value); + + public override void Write(bool value) => UnderlyingWriter.Write(value); + + public override void Write(long value) => UnderlyingWriter.Write(value); + + public override void Write(ulong value) => UnderlyingWriter.Write(value); + + public override void Write(uint value) => UnderlyingWriter.Write(value); + + public override void Write(object value) + { + if (value is StringBuilder builder) + { + UnderlyingWriter.Write(builder); + return; + } + + UnderlyingWriter.Write(value); + } + + public override Encoding Encoding => UnderlyingWriter.Encoding; + + protected override void Dispose(bool disposing) => Pool.Return(this); + + private class EncodedTextWriterPool : InternalObjectPool + { + public EncodedTextWriterPool() : base(new Policy()) + { + } + + private class Policy : IInternalObjectPoolPolicy + { + public EncodedTextWriterWrapper Create() + { + return new EncodedTextWriterWrapper(); + } + + public bool Return(EncodedTextWriterWrapper obj) + { + obj.UnderlyingWriter = default; + + return true; + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/IO/HtmlEncoder.cs b/source/Handlebars/IO/HtmlEncoder.cs index 6edb5145..10d6d56b 100644 --- a/source/Handlebars/IO/HtmlEncoder.cs +++ b/source/Handlebars/IO/HtmlEncoder.cs @@ -1,4 +1,9 @@ -using System.Globalization; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using HandlebarsDotNet.Compiler.Lexer; +using HandlebarsDotNet.StringUtils; namespace HandlebarsDotNet { @@ -8,75 +13,66 @@ namespace HandlebarsDotNet /// public class HtmlEncoder : ITextEncoder { - /// - public string Encode(string text) + public HtmlEncoder(IFormatProvider provider) => FormatProvider = provider; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldEncode(char c) { - if (string.IsNullOrEmpty(text)) - return string.Empty; + return c == '"' + || c == '&' + || c == '>' + || c == '<' + || c > 159; + } + public IFormatProvider FormatProvider { get; } - // Detect if we need to allocate a stringbuilder and new string - for (var i = 0; i < text.Length; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encode(StringBuilder text, TextWriter target) + { + if(text == null || text.Length == 0) return; + + EncodeImpl(new StringBuilderWrapper(text), target); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encode(string text, TextWriter target) + { + if(string.IsNullOrEmpty(text)) return; + + EncodeImpl(new StringWrapper(text), target); + } + + private static void EncodeImpl(T text, TextWriter target) where T: IStringWrapper + { + for (var i = 0; i < text.Count; i++) { - switch (text[i]) + var value = text[i]; + switch (value) { case '"': + target.Write("""); + break; case '&': + target.Write("&"); + break; case '<': + target.Write("<"); + break; case '>': - return ReallyEncode(text, i); + target.Write(">"); + break; + default: - if (text[i] > 159) + if (value > 159) { - return ReallyEncode(text, i); + target.Write("&#"); + target.Write((int)value); + target.Write(";"); } - else - - break; - } - } - - return text; - } - - private static string ReallyEncode(string text, int i) - { - using (var container = StringBuilderPool.Shared.Use()) - { - var sb = container.Value; - sb.Append(text, 0, i); - for (; i < text.Length; i++) - { - switch (text[i]) - { - case '"': - sb.Append("""); - break; - case '&': - sb.Append("&"); - break; - case '<': - sb.Append("<"); - break; - case '>': - sb.Append(">"); - break; - - default: - if (text[i] > 159) - { - sb.Append("&#"); - sb.Append(((int)text[i]).ToString(CultureInfo.InvariantCulture)); - sb.Append(";"); - } - else - sb.Append(text[i]); - - break; - } + else target.Write(value); + break; } - - return sb.ToString(); } } } diff --git a/source/Handlebars/IO/ITextEncoder.cs b/source/Handlebars/IO/ITextEncoder.cs index 41d43f46..c17e7867 100644 --- a/source/Handlebars/IO/ITextEncoder.cs +++ b/source/Handlebars/IO/ITextEncoder.cs @@ -1,10 +1,20 @@ -namespace HandlebarsDotNet +using System; +using System.IO; +using System.Text; + +namespace HandlebarsDotNet { /// /// Encoder used for output encoding. /// public interface ITextEncoder { - string Encode(string value); + IFormatProvider FormatProvider { get; } + + void Encode(StringBuilder text, TextWriter target); + + void Encode(string text, TextWriter target); + + bool ShouldEncode(char c); } } \ No newline at end of file diff --git a/source/Handlebars/IO/TextEncoderWrapper.cs b/source/Handlebars/IO/TextEncoderWrapper.cs new file mode 100644 index 00000000..8e1491cb --- /dev/null +++ b/source/Handlebars/IO/TextEncoderWrapper.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Text; + +namespace HandlebarsDotNet +{ + internal class TextEncoderWrapper : ITextEncoder, IDisposable + { + private static readonly InternalObjectPool Pool + = new InternalObjectPool(new Policy()); + + private ITextEncoder _underlyingEncoder; + private bool _enabled; + + public static TextEncoderWrapper Null { get; } = new TextEncoderWrapper(); + + public static TextEncoderWrapper Create(ITextEncoder encoder) + { + var wrapper = Pool.Get(); + wrapper._underlyingEncoder = encoder; + wrapper.Enabled = encoder != null; + + return wrapper; + } + + public bool Enabled + { + get => _enabled && _underlyingEncoder != null; + set => _enabled = value; + } + + private TextEncoderWrapper() + { + } + + public void Encode(StringBuilder text, TextWriter target) + { + if (!Enabled) return; + + _underlyingEncoder.Encode(text, target); + } + + public void Encode(string text, TextWriter target) + { + if (!Enabled) return; + + _underlyingEncoder.Encode(text, target); + } + + public bool ShouldEncode(char c) + { + return Enabled && _underlyingEncoder.ShouldEncode(c); + } + + public IFormatProvider FormatProvider => _underlyingEncoder.FormatProvider; + + public void Dispose() + { + if(ReferenceEquals(this, Null)) return; + + Pool.Return(this); + } + + private class Policy : IInternalObjectPoolPolicy + { + public TextEncoderWrapper Create() => new TextEncoderWrapper(); + + public bool Return(TextEncoderWrapper item) + { + item._enabled = true; + item._underlyingEncoder = null; + + return true; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs deleted file mode 100644 index 11d93119..00000000 --- a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using HandlebarsDotNet.Compiler.Structure.Path; -using HandlebarsDotNet.MemberAccessors; -using HandlebarsDotNet.Polyfills; - -namespace HandlebarsDotNet.ObjectDescriptors -{ - internal sealed class KeyValuePairObjectDescriptorProvider : IObjectDescriptorProvider - { - 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; - 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]); - - value = (ObjectDescriptor) descriptorCreator.Invoke(null, ArrayEx.Empty()); - return true; - } - - private static ObjectDescriptor CreateDescriptor() - { - return new ObjectDescriptor(typeof(KeyValuePair), new KeyValuePairAccessor(), GetProperties); - } - - private class KeyValuePairAccessor : IMemberAccessor - { - public bool TryGetValue(object instance, ChainSegment memberName, out object value) - { - var keyValuePair = (KeyValuePair) instance; - - if (memberName.IsKey) - { - value = keyValuePair.Key; - return true; - } - - if (memberName.IsValue) - { - value = keyValuePair.Value; - return true; - } - - value = default(TV); - return false; - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectAccessor.cs b/source/Handlebars/ObjectDescriptors/ObjectAccessor.cs new file mode 100644 index 00000000..13e95ff5 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectAccessor.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet +{ + public readonly ref struct ObjectAccessor + { + private readonly object _data; + private readonly ObjectDescriptor _descriptor; + + public ObjectAccessor(object data, ObjectDescriptor descriptor) + { + _data = data; + _descriptor = descriptor; + } + + public IEnumerable Properties => _descriptor + .GetProperties(_descriptor, _data) + .OfType() + .Select(ChainSegment.Create); + + public object this[ChainSegment segment] => + _descriptor.MemberAccessor.TryGetValue(_data, segment, out var value) + ? value + : null; + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs index dfc7ead6..49b3c815 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -1,6 +1,6 @@ using System; using System.Collections; -using System.Collections.Generic; +using System.Runtime.CompilerServices; using HandlebarsDotNet.MemberAccessors; namespace HandlebarsDotNet.ObjectDescriptors @@ -8,10 +8,11 @@ namespace HandlebarsDotNet.ObjectDescriptors /// /// Provides meta-information about /// - public class ObjectDescriptor : IEquatable + public class ObjectDescriptor { public static readonly ObjectDescriptor Empty = new ObjectDescriptor(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ObjectDescriptor Create(object from, ICompiledHandlebarsConfiguration configuration) { if (from == null) return null; @@ -19,6 +20,7 @@ public static ObjectDescriptor Create(object from, ICompiledHandlebarsConfigurat return descriptor; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryCreate(object from, ICompiledHandlebarsConfiguration configuration, out ObjectDescriptor descriptor) { return configuration.ObjectDescriptorProvider.TryGetDescriptor(from.GetType(), out descriptor); diff --git a/source/Handlebars/PathInfoLight.cs b/source/Handlebars/PathInfoLight.cs new file mode 100644 index 00000000..b2d69c52 --- /dev/null +++ b/source/Handlebars/PathInfoLight.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet +{ + public readonly struct PathInfoLight : + IEquatable, + IEquatable + { + private readonly int _comparerTag; + + public readonly PathInfo PathInfo; + + public PathInfoLight(PathInfo pathInfo) + { + PathInfo = pathInfo; + _comparerTag = 0; + } + + private PathInfoLight(PathInfo pathInfo, int comparerTag) + { + PathInfo = pathInfo; + _comparerTag = comparerTag; + } + + internal static IEqualityComparer PlainPathComparer { get; } = new EqualityComparer(false); + + internal static IEqualityComparer PlainPathWithPartsCountComparer { get; } = new EqualityComparer(); + + /// + /// Used for special handling of Relaxed Helper Names + /// + [Pure] + internal PathInfoLight TagComparer() + { + return new PathInfoLight(PathInfo, _comparerTag + 1); + } + + public bool Equals(PathInfoLight other) + { + return _comparerTag == other._comparerTag && Equals(PathInfo, other.PathInfo); + } + + public bool Equals(PathInfo other) + { + return Equals(PathInfo, other); + } + + public override bool Equals(object obj) + { + return obj is PathInfoLight other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (_comparerTag * 397) ^ (PathInfo != null ? PathInfo.GetHashCode() : 0); + } + } + + public static implicit operator PathInfoLight(PathInfo pathInfo) => new PathInfoLight(pathInfo); + + public static implicit operator PathInfo(PathInfoLight pathInfo) => pathInfo.PathInfo; + + private sealed class EqualityComparer : IEqualityComparer + { + private readonly PathInfo.TrimmedPathEqualityComparer _comparer; + + public EqualityComparer(bool countParts = true) + { + _comparer = new PathInfo.TrimmedPathEqualityComparer(countParts); + } + + public bool Equals(PathInfoLight x, PathInfoLight y) + { + return x._comparerTag == y._comparerTag && _comparer.Equals(x.PathInfo, y.PathInfo); + } + + public int GetHashCode(PathInfoLight obj) + { + return _comparer.GetHashCode(obj.PathInfo); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/StringUtils/IStringWrapper.cs b/source/Handlebars/StringUtils/IStringWrapper.cs new file mode 100644 index 00000000..8aaa04cb --- /dev/null +++ b/source/Handlebars/StringUtils/IStringWrapper.cs @@ -0,0 +1,9 @@ +namespace HandlebarsDotNet.StringUtils +{ + internal interface IStringWrapper + { + int Count { get; } + + char this[int index] { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs b/source/Handlebars/StringUtils/StringBuilderEnumerator.cs similarity index 65% rename from source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs rename to source/Handlebars/StringUtils/StringBuilderEnumerator.cs index 68d48f48..7fcc02b1 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs +++ b/source/Handlebars/StringUtils/StringBuilderEnumerator.cs @@ -1,20 +1,21 @@ -using System.Collections; -using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.StringUtils { - internal struct StringBuilderEnumerator : IEnumerator + internal ref struct StringBuilderEnumerator { private readonly StringBuilder _stringBuilder; private int _index; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public StringBuilderEnumerator(StringBuilder stringBuilder) : this() { _stringBuilder = stringBuilder; _index = -1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { if (++_index >= _stringBuilder.Length) return false; @@ -23,14 +24,9 @@ public bool MoveNext() return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() => _index = -1; public char Current { get; private set; } - - object IEnumerator.Current => Current; - - public void Dispose() - { - } } } \ No newline at end of file diff --git a/source/Handlebars/StringUtils/StringBuilderWrapper.cs b/source/Handlebars/StringUtils/StringBuilderWrapper.cs new file mode 100644 index 00000000..4f0919ed --- /dev/null +++ b/source/Handlebars/StringUtils/StringBuilderWrapper.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace HandlebarsDotNet.StringUtils +{ + internal readonly struct StringBuilderWrapper : IStringWrapper + { + private readonly StringBuilder _value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StringBuilderWrapper(StringBuilder value) => _value = value; + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _value.Length; + } + + public char this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _value[index]; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/StringUtils/StringWrapper.cs b/source/Handlebars/StringUtils/StringWrapper.cs new file mode 100644 index 00000000..59f3cc03 --- /dev/null +++ b/source/Handlebars/StringUtils/StringWrapper.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace HandlebarsDotNet.StringUtils +{ + internal readonly struct StringWrapper : IStringWrapper + { + private readonly string _value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StringWrapper(string value) => _value = value; + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _value.Length; + } + + public char this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _value[index]; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/BlockParamsValues.cs b/source/Handlebars/ValueProviders/BlockParamsValues.cs index 87abc610..47b31e5a 100644 --- a/source/Handlebars/ValueProviders/BlockParamsValues.cs +++ b/source/Handlebars/ValueProviders/BlockParamsValues.cs @@ -33,10 +33,10 @@ public void CreateProperty(in int variableIndex, out EntryIndex in var variable = GetVariable(variableIndex); if (ReferenceEquals(variable, null)) { - index = new EntryIndex(-1, 0); + index = new EntryIndex(-1, 0, null); return; } - var value = variable.GetUndefinedBindingResult(_configuration); + var value = UndefinedBindingResult.Create(variable); _values.AddOrReplace(variable, value, out index); } diff --git a/source/Handlebars/ValueProviders/DataValues.cs b/source/Handlebars/ValueProviders/DataValues.cs index d7c82f67..bf9ab7a7 100644 --- a/source/Handlebars/ValueProviders/DataValues.cs +++ b/source/Handlebars/ValueProviders/DataValues.cs @@ -5,7 +5,7 @@ namespace HandlebarsDotNet.ValueProviders { - public readonly struct DataValues + public readonly ref struct DataValues { private readonly BindingContext _context; private readonly EntryIndex[] _wellKnownVariables; @@ -65,7 +65,7 @@ public object this[in EntryIndex entryIndex] [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CreateProperty(ChainSegment variable, out EntryIndex index) { - var value = variable.GetUndefinedBindingResult(Configuration); + var value = UndefinedBindingResult.Create(variable); _data.AddOrReplace(variable, value, out index); if (variable.WellKnownVariable != WellKnownVariable.None) diff --git a/source/Handlebars/ValueProviders/ObjectIteratorValues.cs b/source/Handlebars/ValueProviders/ObjectIteratorValues.cs index 11d13592..1a874157 100644 --- a/source/Handlebars/ValueProviders/ObjectIteratorValues.cs +++ b/source/Handlebars/ValueProviders/ObjectIteratorValues.cs @@ -22,7 +22,7 @@ public ObjectIteratorValues(BindingContext bindingContext) : this() _wellKnownVariables = bindingContext.WellKnownVariables; if (!_supportLastInObjectIterations) { - var undefined = ChainSegment.Last.GetUndefinedBindingResult(configuration); + var undefined = UndefinedBindingResult.Create(ChainSegment.Last); _data.AddOrReplace(ChainSegment.Last, undefined, out _wellKnownVariables[(int) ChainSegment.Last.WellKnownVariable]); } else