diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e078a975..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: csharp -mono: none -dotnet: 2.0.0 -dist: trusty - -sudo: required - -addons: - apt: - sources: - - sourceline: 'deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main' - key_url: 'https://packages.microsoft.com/keys/microsoft.asc' - packages: - - dotnet-hostfxr-1.0.1 - - dotnet-sharedframework-microsoft.netcore.app-1.1.2 - -env: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true - -script: - - dotnet --info - - dotnet restore source - - dotnet build source/Handlebars/Handlebars.csproj --configuration Debug --framework netstandard1.3 - - dotnet build source/Handlebars/Handlebars.csproj --configuration Release --framework netstandard1.3 - - dotnet build source/Handlebars/Handlebars.csproj --configuration Debug --framework netstandard2.0 - - dotnet build source/Handlebars/Handlebars.csproj --configuration Release --framework netstandard2.0 - - dotnet test source/Handlebars.Test/Handlebars.Test.csproj --configuration Debug --framework netcoreapp1.1 - - dotnet test source/Handlebars.Test/Handlebars.Test.csproj --configuration Release --framework netcoreapp1.1 - - dotnet test source/Handlebars.Test/Handlebars.Test.csproj --configuration Debug --framework netcoreapp2.0 - - dotnet test source/Handlebars.Test/Handlebars.Test.csproj --configuration Release --framework netcoreapp2.0 - -notifications: - email: - - rex@rexmorgan.net \ No newline at end of file diff --git a/README.md b/README.md index 5d7221fa..6b13611f 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,42 @@ The animal, Fido, is a dog. The animal, Chewy, is not a dog. */ ``` + +### Compatibility feature toggles + +Compatibility feature toggles defines a set of settings responsible for controlling compilation/rendering behavior. Each of those settings would enable certain feature that would break compatibility with canonical Handlebars. +By default all toggles are set to `false`. + +##### Legend +- Areas + - `Compile-time`: takes affect at the time of template compilation + - `Runtime`: takes affect at the time of template rendering + +#### `RelaxedHelperNaming` +If `true` enables support for Handlebars.Net helper naming rules. +This enables helper names to be not-valid Handlebars identifiers (e.g. `{{ one.two }}`). +Such naming is not supported in Handlebarsjs and would break compatibility. + +##### Areas +- `Compile-time` + +##### Example +```c# +[Fact] +public void HelperWithDotSeparatedName() +{ + var source = "{{ one.two }}"; + var handlebars = Handlebars.Create(); + handlebars.Configuration.Compatibility.RelaxedHelperNaming = true; + handlebars.RegisterHelper("one.two", (context, arguments) => 42); + + var template = handlebars.Compile(source); + var actual = template(null); + + Assert.Equal("42", actual); +} +``` + ## Performance ### Compilation @@ -166,16 +202,16 @@ Compared to rendering, compiling is a fairly intensive process. While both are s Nearly all time spent in rendering is in the routine that resolves values against the model. Different types of objects have different performance characteristics when used as models. #### Model Types -- The absolute fastest model is a dictionary (microseconds), because no reflection is necessary at render time. +- The absolute fastest model is a `IDictionary` (microseconds). - The next fastest is a POCO (typically a few milliseconds for an average-sized template and model), which uses traditional reflection and is fairly fast. - Rendering starts to get slower (into the tens of milliseconds or more) on dynamic objects. - The slowest (up to hundreds of milliseconds or worse) tend to be objects with custom type implementations (such as `ICustomTypeDescriptor`) that are not optimized for heavy reflection. -A frequent performance issue that comes up is JSON.NET's `JObject`, which for reasons we haven't fully researched, has very slow reflection characteristics when used as a model in Handlebars.Net. A simple fix is to just use JSON.NET's built-in ability to deserialize a JSON string to an `ExpandoObject` instead of a `JObject`. This will yield nearly an order of magnitude improvement in render times on average. +~~A frequent performance issue that comes up is JSON.NET's `JObject`, which for reasons we haven't fully researched, has very slow reflection characteristics when used as a model in Handlebars.Net. A simple fix is to just use JSON.NET's built-in ability to deserialize a JSON string to an `ExpandoObject` instead of a `JObject`. This will yield nearly an order of magnitude improvement in render times on average.~~ ## Future roadmap -**[Call for Input on v2](https://github.com/rexm/Handlebars.Net/issues/294)** +TBD ## Contributing diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 218a32b3..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: 1.8.1-ci{build} - -image: Visual Studio 2017 - -build_script: - - cmd: dotnet --info - - cmd: dotnet restore source - - cmd: dotnet build source --configuration Debug - - cmd: dotnet build source --configuration Release - -test_script: - - cmd: set OpenCover=%UserProfile%\.nuget\packages\OpenCover\4.6.519\tools\OpenCover.Console.exe - - cmd: set ReportGenerator=%UserProfile%\.nuget\packages\ReportGenerator\2.5.6\tools\ReportGenerator.exe - - cmd: if not exist TestResults mkdir TestResults - - cmd: '"%OpenCover%" -target:dotnet.exe -targetargs:"test source\Handlebars.Test\Handlebars.Test.csproj --configuration Release --framework net461" -searchdirs:source\Handlebars.Test\bin\Release\net461 -output:TestResults\Handlebars.netframework.report.xml -register:user -filter:+[Handlebars]* -returntargetcode -oldstyle' - - cmd: '"%OpenCover%" -target:dotnet.exe -targetargs:"test source\Handlebars.Test\Handlebars.Test.csproj --configuration Release --framework netcoreapp1.1" -searchdirs:source\Handlebars.Test\bin\Release\netcoreapp1.1 -output:TestResults\Handlebars.netcoreapp.report.xml -register:user -filter:+[Handlebars]* -returntargetcode -oldstyle' - - cmd: '"%OpenCover%" -target:dotnet.exe -targetargs:"test source\Handlebars.Test\Handlebars.Test.csproj --configuration Release --framework netcoreapp2.0" -searchdirs:source\Handlebars.Test\bin\Release\netcoreapp2.0 -output:TestResults\Handlebars.netcoreapp2.report.xml -register:user -filter:+[Handlebars]* -returntargetcode -oldstyle' - - cmd: '"%ReportGenerator%" -reports:TestResults\*.report.xml -targetdir:TestResults\report -reporttypes:Badges;Html' - -artifacts: - - path: 'source\**\*.nupkg' - - path: TestResults diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 42442cd4..1338de34 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -3,19 +3,53 @@ using System.Collections; using System.Collections.Generic; using System.Dynamic; +using System.Linq; +using System.Reflection; using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using HandlebarsDotNet.Features; +using Xunit.Abstractions; namespace HandlebarsDotNet.Test { + public class HandlebarsEnvGenerator : IEnumerable + { + private readonly List _data = new List + { + Handlebars.Create(), + Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), + Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)), + Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => + { + types.Add(typeof(Dictionary)); + types.Add(typeof(Dictionary)); + types.Add(typeof(Dictionary)); + types.Add(typeof(Dictionary)); + })), + }; + + public IEnumerator GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + public class BasicIntegrationTests { - [Fact] - public void BasicPath() + private readonly ITestOutputHelper _testOutputHelper; + + public BasicIntegrationTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Theory] + [ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPath(IHandlebars handlebars) { var source = "Hello, {{name}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" @@ -24,14 +58,15 @@ public void BasicPath() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void EmptyIf() + [Theory] + [ClassData(typeof(HandlebarsEnvGenerator))] + public void EmptyIf(IHandlebars handlebars) { var source = @"{{#if false}} {{else}} {{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { }; @@ -39,16 +74,12 @@ public void EmptyIf() Assert.Equal(string.Empty, result); } - [Fact] - public void BasicPathUnresolvedBindingFormatter() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathUnresolvedBindingFormatter(IHandlebars handlebars) { var source = "Hello, {{foo}}!"; - var config = new HandlebarsConfiguration - { - UnresolvedBindingFormatter = "('{0}' is undefined)" - }; - var handlebars = Handlebars.Create(config); + handlebars.Configuration.UnresolvedBindingFormatter = "('{0}' is undefined)"; var template = handlebars.Compile(source); var data = new @@ -59,35 +90,29 @@ public void BasicPathUnresolvedBindingFormatter() Assert.Equal("Hello, ('foo' is undefined)!", result); } - [Fact] - public void BasicPathThrowOnUnresolvedBindingExpression() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathThrowOnUnresolvedBindingExpression(IHandlebars handlebars) { var source = "Hello, {{foo}}!"; - var config = new HandlebarsConfiguration - { - ThrowOnUnresolvedBindingExpression = true - }; - var handlebars = Handlebars.Create(config); + handlebars.Configuration.ThrowOnUnresolvedBindingExpression = true; var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" }; + Assert.Throws(() => template(data)); } - [Fact] - public void BasicPathThrowOnNestedUnresolvedBindingExpression() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathThrowOnNestedUnresolvedBindingExpression(IHandlebars handlebars) { var source = "Hello, {{foo.bar}}!"; - var config = new HandlebarsConfiguration - { - ThrowOnUnresolvedBindingExpression = true - }; - var handlebars = Handlebars.Create(config); + handlebars.Configuration.ThrowOnUnresolvedBindingExpression = true; + var template = handlebars.Compile(source); var data = new @@ -95,11 +120,12 @@ public void BasicPathThrowOnNestedUnresolvedBindingExpression() foo = (object)null }; var ex = Assert.Throws(() => template(data)); + Assert.Equal("bar is undefined", ex.Message); } - [Fact] - public void BasicPathNoThrowOnNullExpression() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathNoThrowOnNullExpression(IHandlebars handlebars) { var source = @"{{#if foo}} @@ -108,12 +134,7 @@ public void BasicPathNoThrowOnNullExpression() false {{/if}} "; - - var config = new HandlebarsConfiguration - { - ThrowOnUnresolvedBindingExpression = true - }; - var handlebars = Handlebars.Create(config); + handlebars.Configuration.ThrowOnUnresolvedBindingExpression = true; var template = handlebars.Compile(source); var data = new @@ -124,16 +145,12 @@ public void BasicPathNoThrowOnNullExpression() Assert.Contains("false", result); } - [Fact] - public void AssertHandlebarsUndefinedBindingException() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void AssertHandlebarsUndefinedBindingException(IHandlebars handlebars) { var source = "Hello, {{person.firstname}} {{person.lastname}}!"; - var config = new HandlebarsConfiguration - { - ThrowOnUnresolvedBindingExpression = true - }; - var handlebars = Handlebars.Create(config); + handlebars.Configuration.ThrowOnUnresolvedBindingExpression = true; var template = handlebars.Compile(source); var data = new @@ -144,25 +161,16 @@ public void AssertHandlebarsUndefinedBindingException() } }; - try - { - template(data); - } - catch (HandlebarsUndefinedBindingException ex) - { - Assert.Equal("person.lastname", ex.Path); - Assert.Equal("lastname", ex.MissingKey); - return; - } - - Assert.False(true, "Exception is expected."); + var exception = Assert.Throws(() => template(data)); + Assert.Equal("person.lastname", exception.Path); + Assert.Equal("lastname", exception.MissingKey); } - [Fact] - public void BasicPathWhiteSpace() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathWhiteSpace(IHandlebars handlebars) { var source = "Hello, {{ name }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" @@ -171,11 +179,11 @@ public void BasicPathWhiteSpace() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicCurlies() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicCurlies(IHandlebars handlebars) { var source = "Hello, {name}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" @@ -184,11 +192,11 @@ public void BasicCurlies() Assert.Equal("Hello, {name}!", result); } - [Fact] - public void BasicCurliesWithLeadingSlash() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicCurliesWithLeadingSlash(IHandlebars handlebars) { var source = "Hello, \\{name\\}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" @@ -197,11 +205,11 @@ public void BasicCurliesWithLeadingSlash() Assert.Equal("Hello, \\{name\\}!", result); } - [Fact] - public void BasicCurliesWithEscapedLeadingSlash() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicCurliesWithEscapedLeadingSlash(IHandlebars handlebars) { var source = @"Hello, \\{{name}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Handlebars.Net" @@ -210,11 +218,11 @@ public void BasicCurliesWithEscapedLeadingSlash() Assert.Equal(@"Hello, \Handlebars.Net!", result); } - [Fact] - public void BasicPathArray() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathArray(IHandlebars handlebars) { var source = "Hello, {{ names.[1] }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new[] { "Foo", "Handlebars.Net" } @@ -223,11 +231,11 @@ public void BasicPathArray() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathArrayChildPath() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathArrayChildPath(IHandlebars handlebars) { var source = "Hello, {{ names.[1].name }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } @@ -236,11 +244,11 @@ public void BasicPathArrayChildPath() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathArrayNoSquareBracketsChildPath() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathArrayNoSquareBracketsChildPath(IHandlebars handlebars) { var source = "Hello, {{ names.1.name }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } @@ -248,12 +256,25 @@ public void BasicPathArrayNoSquareBracketsChildPath() var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathEnumerableNoSquareBracketsChildPath(IHandlebars handlebars) + { + var source = "Hello, {{ names.1.name }}!"; + var template = handlebars.Compile(source); + var data = new + { + names = new[] { new { name = "skip" }, new { name = "Foo" }, new { name = "Handlebars.Net" } }.Skip(1) + }; + var result = template(data); + Assert.Equal("Hello, Handlebars.Net!", result); + } - [Fact] - public void BasicPathDotBinding() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDotBinding(IHandlebars handlebars) { var source = "{{#nestedObject}}{{.}}{{/nestedObject}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { nestedObject = "A dot goes a long way" @@ -262,11 +283,11 @@ public void BasicPathDotBinding() Assert.Equal("A dot goes a long way", result); } - [Fact] - public void BasicPathRelativeDotBinding() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathRelativeDotBinding(IHandlebars handlebars) { var source = "{{#nestedObject}}{{../.}}{{/nestedObject}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { nestedObject = "Relative dots, yay" @@ -275,11 +296,42 @@ public void BasicPathRelativeDotBinding() Assert.Equal("{ nestedObject = Relative dots, yay }", result); } - [Fact] - public void BasicPropertyOnArray() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPropertyOnArray(IHandlebars handlebars) { var source = "Array is {{ names.Length }} item(s) long"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); + var data = new + { + names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } + }; + var result = template(data); + Assert.Equal("Array is 2 item(s) long", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void AliasedPropertyOnArray(IHandlebars handlebars) + { + var source = "Array is {{ names.count }} item(s) long"; + var template = handlebars.Compile(source); + var data = new + { + names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } + }; + var result = template(data); + Assert.Equal("Array is 2 item(s) long", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void CustomAliasedPropertyOnArray(IHandlebars handlebars) + { + var aliasProvider = new DelegatedMemberAliasProvider() + .AddAlias("myCountAlias", list => list.Count); + + handlebars.Configuration.CompileTimeConfiguration.AliasProviders.Add(aliasProvider); + + var source = "Array is {{ names.myCountAlias }} item(s) long"; + var template = handlebars.Compile(source); var data = new { names = new[] { new { name = "Foo" }, new { name = "Handlebars.Net" } } @@ -287,12 +339,25 @@ public void BasicPropertyOnArray() var result = template(data); Assert.Equal("Array is 2 item(s) long", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void AliasedPropertyOnList(IHandlebars handlebars) + { + var source = "Array is {{ names.Length }} item(s) long"; + var template = handlebars.Compile(source); + var data = new + { + names = new List { new { name = "Foo" }, new { name = "Handlebars.Net" } } + }; + var result = template(data); + Assert.Equal("Array is 2 item(s) long", result); + } - [Fact] - public void BasicIfElse() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicIfElse(IHandlebars handlebars) { var source = "Hello, {{#if basic_bool}}Bob{{else}}Sam{{/if}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var trueData = new { basic_bool = true @@ -307,11 +372,11 @@ public void BasicIfElse() Assert.Equal("Hello, Sam!", resultFalse); } - [Fact] - public void BasicIfElseIf() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicIfElseIf(IHandlebars handlebars) { var source = "{{#if isActive}}active{{else if isInactive}}inactive{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var activeData = new { isActive = true @@ -326,11 +391,11 @@ public void BasicIfElseIf() Assert.Equal("inactive", resultFalse); } - [Fact] - public void BasicIfElseIfElse() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicIfElseIfElse(IHandlebars handlebars) { var source = "{{#if isActive}}active{{else if isInactive}}inactive{{else}}nada{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var activeData = new { isActive = true @@ -350,11 +415,11 @@ public void BasicIfElseIfElse() Assert.Equal("nada", resultElse); } - [Fact] - public void BasicWith() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicWith(IHandlebars handlebars) { var source = "Hello,{{#with person}} my good friend {{name}}{{/with}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { person = new @@ -365,23 +430,56 @@ public void BasicWith() var result = template(data); Assert.Equal("Hello, my good friend Erik!", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void TestSingleLoopDictionary(IHandlebars handlebars) + { + const string source = "{{#Input}}ii={{@index}} {{/Input}}"; + var template = handlebars.Compile(source); + var data = new + { + Input = new List + { + "a", "b", "c" + } + }; + var result = template(data); + Assert.Equal("ii=0 ii=1 ii=2 ", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void WithWithBlockParams(IHandlebars handlebars) + { + var source = "{{#with person as |person|}}{{person.name}} is {{age}} years old{{/with}}."; + var template = handlebars.Compile(source); + var data = new + { + person = new + { + name = "Erik", + age = 42 + } + }; + var result = template(data); + Assert.Equal("Erik is 42 years old.", result); + } - [Fact] - public void BasicWithInversion() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicWithInversion(IHandlebars handlebars) { var source = "Hello, {{#with person}} my good friend{{else}}nevermind{{/with}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); Assert.Equal("Hello, nevermind", template(new { })); Assert.Equal("Hello, nevermind", template(new { person = false })); Assert.Equal("Hello, nevermind", template(new { person = new string[] { } })); } - [Fact] - public void BasicEncoding() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicEncoding(IHandlebars handlebars) { var source = "Hello, {{name}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Bob" @@ -390,11 +488,11 @@ public void BasicEncoding() Assert.Equal("Hello, <b>Bob</b>!", result); } - [Fact] - public void BasicComment() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicComment(IHandlebars handlebars) { var source = "Hello, {{!don't render me!}}{{name}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Carl" @@ -403,11 +501,11 @@ public void BasicComment() Assert.Equal("Hello, Carl!", result); } - [Fact] - public void BasicCommentEscaped() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicCommentEscaped(IHandlebars handlebars) { var source = "Hello, {{!--don't {{render}} me!--}}{{name}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { name = "Carl" @@ -416,11 +514,11 @@ public void BasicCommentEscaped() Assert.Equal("Hello, Carl!", result); } - [Fact] - public void BasicObjectEnumerator() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicObjectEnumerator(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new @@ -433,11 +531,29 @@ public void BasicObjectEnumerator() Assert.Equal("hello world ", result); } - [Fact] - public void BasicObjectEnumeratorWithLast() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicListEnumerator(IHandlebars handlebars) + { + var source = "{{#each enumerateMe}}{{this}} {{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + enumerateMe = new string[] + { + "hello", + "world" + } + }; + var result = template(data); + Assert.Equal("hello world ", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicObjectEnumeratorWithLast(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@last}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); + handlebars.Configuration.Compatibility.SupportLastInObjectIterations = true; var data = new { enumerateMe = new @@ -450,11 +566,11 @@ public void BasicObjectEnumeratorWithLast() Assert.Equal("False True ", result); } - [Fact] - public void BasicObjectEnumeratorWithKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicObjectEnumeratorWithKey(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@key}}: {{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new @@ -466,12 +582,29 @@ public void BasicObjectEnumeratorWithKey() var result = template(data); Assert.Equal("foo: hello bar: world ", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ObjectEnumeratorWithBlockParams(IHandlebars handlebars) + { + var source = "{{#each enumerateMe as |item val|}}{{@item}}: {{@val}} {{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + enumerateMe = new + { + foo = "hello", + bar = "world" + } + }; + var result = template(data); + Assert.Equal("hello: foo world: bar ", result); + } - [Fact] - public void BasicDictionaryEnumerator() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionaryEnumerator(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new Dictionary @@ -483,12 +616,30 @@ public void BasicDictionaryEnumerator() var result = template(data); Assert.Equal("hello world ", result); } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DictionaryEnumeratorWithBlockParams(IHandlebars handlebars) + { + var source = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + enumerateMe = new Dictionary + { + { "foo", "hello"}, + { "bar", "world"} + } + }; + var result = template(data); + Assert.Equal("hello foo world bar ", result); + } - [Fact] - public void DictionaryWithLastEnumerator() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DictionaryWithLastEnumerator(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@last}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); + handlebars.Configuration.Compatibility.SupportLastInObjectIterations = true; var data = new { enumerateMe = new Dictionary @@ -502,11 +653,11 @@ public void DictionaryWithLastEnumerator() Assert.Equal("False False True ", result); } - [Fact] - public void BasicDictionaryEnumeratorWithIntKeys() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionaryEnumeratorWithIntKeys(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new Dictionary @@ -519,11 +670,11 @@ public void BasicDictionaryEnumeratorWithIntKeys() Assert.Equal("hello world ", result); } - [Fact] - public void BasicDictionaryEnumeratorWithKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionaryEnumeratorWithKey(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@key}}: {{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new Dictionary @@ -536,11 +687,11 @@ public void BasicDictionaryEnumeratorWithKey() Assert.Equal("foo: hello bar: world ", result); } - [Fact] - public void BasicDictionaryEnumeratorWithLongKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionaryEnumeratorWithLongKey(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@key}}: {{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new Dictionary @@ -554,11 +705,11 @@ public void BasicDictionaryEnumeratorWithLongKey() } - [Fact] - public void BasicPathDictionaryStringKeyNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryStringKeyNoSquareBrackets(IHandlebars handlebars) { var source = "Hello, {{ names.Foo }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -570,11 +721,11 @@ public void BasicPathDictionaryStringKeyNoSquareBrackets() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathDictionaryStringKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryStringKey(IHandlebars handlebars) { var source = "Hello, {{ names.[Foo] }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -586,11 +737,11 @@ public void BasicPathDictionaryStringKey() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathDictionaryIntKeyNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryIntKeyNoSquareBrackets(IHandlebars handlebars) { var source = "Hello, {{ names.42 }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -602,11 +753,11 @@ public void BasicPathDictionaryIntKeyNoSquareBrackets() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathDictionaryLongKeyNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryLongKeyNoSquareBrackets(IHandlebars handlebars) { var source = "Hello, {{ names.42 }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -618,11 +769,11 @@ public void BasicPathDictionaryLongKeyNoSquareBrackets() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathDictionaryIntKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryIntKey(IHandlebars handlebars) { var source = "Hello, {{ names.[42] }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -634,11 +785,11 @@ public void BasicPathDictionaryIntKey() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathDictionaryLongKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathDictionaryLongKey(IHandlebars handlebars) { var source = "Hello, {{ names.[42] }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { names = new Dictionary @@ -650,33 +801,33 @@ public void BasicPathDictionaryLongKey() Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathExpandoObjectIntKeyRoot() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathExpandoObjectIntKeyRoot(IHandlebars handlebars) { var source = "Hello, {{ [42].name }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = JsonConvert.DeserializeObject("{ 42 : { \"name\": \"Handlebars.Net\" } }"); var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void BasicPathExpandoObjectIntKeyArray() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPathExpandoObjectIntKeyArray(IHandlebars handlebars) { var source = "Hello, {{ names.[1].name }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = JsonConvert.DeserializeObject("{ names : [ { \"name\": \"nope!\" }, { \"name\": \"Handlebars.Net\" } ] }"); var result = template(data); Assert.Equal("Hello, Handlebars.Net!", result); } - [Fact] - public void DynamicWithMetadataEnumerator() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DynamicWithMetadataEnumerator(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); dynamic data = new ExpandoObject(); data.enumerateMe = new ExpandoObject(); data.enumerateMe.foo = "hello"; @@ -685,11 +836,11 @@ public void DynamicWithMetadataEnumerator() Assert.Equal("hello world ", result); } - [Fact] - public void DynamicWithMetadataEnumeratorWithKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DynamicWithMetadataEnumeratorWithKey(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{@key}}: {{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); dynamic data = new ExpandoObject(); data.enumerateMe = new ExpandoObject(); data.enumerateMe.foo = "hello"; @@ -698,17 +849,17 @@ public void DynamicWithMetadataEnumeratorWithKey() Assert.Equal("foo: hello bar: world ", result); } - [Fact] - public void BasicHelper() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicHelper(IHandlebars handlebars) { - Handlebars.RegisterHelper("link_to", (writer, context, parameters) => + handlebars.RegisterHelper("link_to", (writer, context, parameters) => { writer.WriteSafeString("" + parameters[1] + ""); }); string source = @"Click here: {{link_to url text}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -720,14 +871,14 @@ public void BasicHelper() Assert.Equal("Click here: Handlebars.Net", result); } - [Fact] - public void BasicHelperPostRegister() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicHelperPostRegister(IHandlebars handlebars) { string source = @"Click here: {{link_to_post_reg url text}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); - Handlebars.RegisterHelper("link_to_post_reg", (writer, context, parameters) => + handlebars.RegisterHelper("link_to_post_reg", (writer, context, parameters) => { writer.WriteSafeString("" + parameters[1] + ""); }); @@ -744,12 +895,12 @@ public void BasicHelperPostRegister() Assert.Equal("Click here: Handlebars.Net", result); } - [Fact] - public void BasicDeferredBlock() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlock(IHandlebars handlebars) { string source = "Hello, {{#person}}{{name}}{{/person}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -763,23 +914,23 @@ public void BasicDeferredBlock() Assert.Equal("Hello, Bill!", result); } - [Fact] - public void BasicDeferredBlockString() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockString(IHandlebars handlebars) { string source = "{{#person}} -{{this}}- {{/person}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { person = "Bill" }); Assert.Equal(" -Bill- ", result); } - [Fact] - public void BasicDeferredBlockWithWhitespace() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockWithWhitespace(IHandlebars handlebars) { string source = "Hello, {{ # person }}{{ name }}{{ / person }}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -793,12 +944,12 @@ public void BasicDeferredBlockWithWhitespace() Assert.Equal("Hello, Bill!", result); } - [Fact] - public void BasicDeferredBlockFalsy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockFalsy(IHandlebars handlebars) { string source = "Hello, {{#person}}{{name}}{{/person}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -809,12 +960,12 @@ public void BasicDeferredBlockFalsy() Assert.Equal("Hello, !", result); } - [Fact] - public void BasicDeferredBlockNull() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockNull(IHandlebars handlebars) { string source = "Hello, {{#person}}{{name}}{{/person}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -825,12 +976,12 @@ public void BasicDeferredBlockNull() Assert.Equal("Hello, !", result); } - [Fact] - public void BasicDeferredBlockEnumerable() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockEnumerable(IHandlebars handlebars) { string source = "Hello, {{#people}}{{this}} {{/people}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -844,12 +995,12 @@ public void BasicDeferredBlockEnumerable() Assert.Equal("Hello, Bill Mary !", result); } - [Fact] - public void BasicDeferredBlockNegated() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockNegated(IHandlebars handlebars) { string source = "Hello, {{^people}}nobody{{/people}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -861,29 +1012,29 @@ public void BasicDeferredBlockNegated() Assert.Equal("Hello, nobody!", result); } - [Fact] - public void BasicDeferredBlockNegatedContext() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockNegatedContext(IHandlebars handlebars) { - var template = Handlebars.Compile("Hello, {{^obj}}{{name}}{{/obj}}!"); + var template = handlebars.Compile("Hello, {{^obj}}{{name}}{{/obj}}!"); Assert.Equal("Hello, nobody!", template(new { name = "nobody" })); Assert.Equal("Hello, nobody!", template(new { name = "nobody", obj = new string[0] })); } - [Fact] - public void BasicDeferredBlockInversion() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockInversion(IHandlebars handlebars) { - var template = Handlebars.Compile("Hello, {{#obj}}somebody{{else}}{{name}}{{/obj}}!"); + var template = handlebars.Compile("Hello, {{#obj}}somebody{{else}}{{name}}{{/obj}}!"); Assert.Equal("Hello, nobody!", template(new { name = "nobody" })); Assert.Equal("Hello, nobody!", template(new { name = "nobody", obj = false })); Assert.Equal("Hello, nobody!", template(new { name = "nobody", obj = new string[0] })); } - [Fact] - public void BasicDeferredBlockNegatedInversion() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDeferredBlockNegatedInversion(IHandlebars handlebars) { - var template = Handlebars.Compile("Hello, {{^obj}}nobody{{else}}{{name}}{{/obj}}!"); + var template = handlebars.Compile("Hello, {{^obj}}nobody{{else}}{{name}}{{/obj}}!"); var array = new[] { @@ -897,12 +1048,12 @@ public void BasicDeferredBlockNegatedInversion() Assert.Equal("Hello, person!", template(new { obj = new { name = "person" } })); } - [Fact] - public void BasicPropertyMissing() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicPropertyMissing(IHandlebars handlebars) { string source = "Hello, {{first}} {{last}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -913,12 +1064,12 @@ public void BasicPropertyMissing() Assert.Equal("Hello, Marc !", result); } - [Fact] - public void BasicNullOrMissingSubProperty() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNullOrMissingSubProperty(IHandlebars handlebars) { string source = "Hello, {{name.first}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -929,12 +1080,12 @@ public void BasicNullOrMissingSubProperty() Assert.Equal("Hello, !", result); } - [Fact] - public void BasicNumericFalsy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNumericFalsy(IHandlebars handlebars) { string source = "Hello, {{#if falsy}}Truthy!{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -945,12 +1096,12 @@ public void BasicNumericFalsy() Assert.Equal("Hello, ", result); } - [Fact] - public void BasicNullFalsy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNullFalsy(IHandlebars handlebars) { string source = "Hello, {{#if falsy}}Truthy!{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -961,12 +1112,12 @@ public void BasicNullFalsy() Assert.Equal("Hello, ", result); } - [Fact] - public void BasicNumericTruthy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNumericTruthy(IHandlebars handlebars) { string source = "Hello, {{#if truthy}}Truthy!{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -977,12 +1128,12 @@ public void BasicNumericTruthy() Assert.Equal("Hello, Truthy!", result); } - [Fact] - public void BasicStringFalsy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicStringFalsy(IHandlebars handlebars) { string source = "Hello, {{#if falsy}}Truthy!{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -993,12 +1144,12 @@ public void BasicStringFalsy() Assert.Equal("Hello, ", result); } - [Fact] - public void BasicEmptyArrayFalsy() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicEmptyArrayFalsy(IHandlebars handlebars) { var source = "{{#if Array}}stuff: {{#each Array}}{{this}}{{/each}}{{/if}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -1010,12 +1161,12 @@ public void BasicEmptyArrayFalsy() Assert.Equal("", result); } - [Fact] - public void BasicTripleStash() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicTripleStash(IHandlebars handlebars) { string source = "Hello, {{{dangerous_value}}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -1026,12 +1177,12 @@ public void BasicTripleStash() Assert.Equal("Hello,
There's HTML here
!", result); } - [Fact] - public void BasicEscape() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicEscape(IHandlebars handlebars) { string source = @"Hello, \{{raw_value}}!"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -1042,15 +1193,15 @@ public void BasicEscape() Assert.Equal(@"Hello, {{raw_value}}!", result); } - [Fact] - public void BasicNumberLiteral() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNumberLiteral(IHandlebars handlebars) { string source = "{{eval 2 3}}"; - Handlebars.RegisterHelper("eval", + handlebars.RegisterHelper("eval", (writer, context, args) => writer.Write("{0} {1}", args[0], args[1])); - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { }; @@ -1058,15 +1209,15 @@ public void BasicNumberLiteral() Assert.Equal("2 3", result); } - [Fact] - public void BasicNullLiteral() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNullLiteral(IHandlebars handlebars) { string source = "{{eval null}}"; - Handlebars.RegisterHelper("eval", + handlebars.RegisterHelper("eval", (writer, context, args) => writer.Write(args[0] == null)); - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { }; @@ -1074,15 +1225,15 @@ public void BasicNullLiteral() Assert.Equal("True", result); } - [Fact] - public void BasicCurlyBracesInLiterals() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicCurlyBracesInLiterals(IHandlebars handlebars) { var source = @"{{verbatim '{{foo}}'}} something {{verbatim '{{bar}}'}}"; - Handlebars.RegisterHelper("verbatim", + handlebars.RegisterHelper("verbatim", (writer, context, args) => writer.Write(args[0])); - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { }; var result = template(data); @@ -1090,12 +1241,12 @@ public void BasicCurlyBracesInLiterals() Assert.Equal("{{foo}} something {{bar}}", result); } - [Fact] - public void BasicRoot() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicRoot(IHandlebars handlebars) { string source = "{{#people}}- {{this}} is member of {{@root.group}}\n{{/people}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -1111,8 +1262,8 @@ public void BasicRoot() Assert.Equal("- Rex is member of Engineering\n- Todd is member of Engineering\n", result); } - [Fact] - public void ImplicitConditionalBlock() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ImplicitConditionalBlock(IHandlebars handlebars) { var template = "{{#home}}Welcome Home{{/home}}{{^home}}Welcome to {{newCity}}{{/home}}"; @@ -1124,35 +1275,27 @@ public void ImplicitConditionalBlock() home = false }; - var compiler = Handlebars.Compile(template); + var compiler = handlebars.Compile(template); var result = compiler.Invoke(data); Assert.Equal("Welcome to New York City", result); } - [Fact] - public void BasicDictionary() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicDictionary(IHandlebars handlebars) { var source = "
UserName: {{userInfo.userName}} Language: {{userInfo.language}}
" + "
body
"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); - var embeded = new Dictionary(); - embeded.Add("userInfo", - new - { - userName = "Ondrej", - language = "Slovak" - }); - embeded.Add("clientSettings", - new - { - width = 120, - height = 80 - }); + var embedded = new Dictionary + { + {"userInfo", new {userName = "Ondrej", language = "Slovak"}}, + {"clientSettings", new {width = 120, height = 80}} + }; - var result = template(embeded); + var result = template(embedded); var expectedResult = "
UserName: Ondrej Language: Slovak
" + "
body
"; @@ -1160,12 +1303,12 @@ public void BasicDictionary() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicHashtable() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicHashtable(IHandlebars handlebars) { var source = "{{dictionary.[key]}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1179,12 +1322,12 @@ public void BasicHashtable() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicHashtableNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicHashtableNoSquareBrackets(IHandlebars handlebars) { var source = "{{dictionary.key}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1198,12 +1341,12 @@ public void BasicHashtableNoSquareBrackets() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicMockIDictionary() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicMockIDictionary(IHandlebars handlebars) { var source = "{{dictionary.[key]}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1215,12 +1358,12 @@ public void BasicMockIDictionary() Assert.Equal(expectedResult, result); } - [Fact] - public void DictionaryWithSpaceInKeyName() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DictionaryWithSpaceInKeyName(IHandlebars handlebars) { var source = "{{dictionary.[my key]}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1232,12 +1375,12 @@ public void DictionaryWithSpaceInKeyName() Assert.Equal(expectedResult, result); } - [Fact] - public void DictionaryWithSpaceInKeyNameAndChildProperty() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DictionaryWithSpaceInKeyNameAndChildProperty(IHandlebars handlebars) { var source = "{{dictionary.[my key].prop1}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1257,12 +1400,12 @@ public void DictionaryWithSpaceInKeyNameAndChildProperty() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicMockIDictionaryNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicMockIDictionaryNoSquareBrackets(IHandlebars handlebars) { var source = "{{dictionary.key}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1274,12 +1417,12 @@ public void BasicMockIDictionaryNoSquareBrackets() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicMockIDictionaryIntKey() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicMockIDictionaryIntKey(IHandlebars handlebars) { var source = "{{dictionary.[42]}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1291,12 +1434,12 @@ public void BasicMockIDictionaryIntKey() Assert.Equal(expectedResult, result); } - [Fact] - public void BasicMockIDictionaryIntKeyNoSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicMockIDictionaryIntKeyNoSquareBrackets(IHandlebars handlebars) { var source = "{{dictionary.42}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1308,13 +1451,13 @@ public void BasicMockIDictionaryIntKeyNoSquareBrackets() Assert.Equal(expectedResult, result); } - [Fact] - public void TestNoWhitespaceBetweenExpressions() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void TestNoWhitespaceBetweenExpressions(IHandlebars handlebars) { var source = @"{{#is ProgramID """"}}no program{{/is}}{{#is ProgramID ""1081""}}some program text{{/is}}"; - Handlebars.RegisterHelper("is", (output, options, context, args) => + handlebars.RegisterHelper("is", (output, options, context, args) => { if (args[0] == args[1]) { @@ -1323,7 +1466,7 @@ public void TestNoWhitespaceBetweenExpressions() }); - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1336,11 +1479,11 @@ public void TestNoWhitespaceBetweenExpressions() Assert.Equal(expectedResult, result); } - [Fact] - public void DictionaryIteration() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void DictionaryIteration(IHandlebars handlebars) { string source = @"{{#ADictionary}}{{@key}},{{value}}{{/ADictionary}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { ADictionary = new Dictionary @@ -1355,11 +1498,11 @@ public void DictionaryIteration() Assert.Equal("key5,14key6,15key7,16key8,17", result); } - [Fact] - public void ObjectEnumeration() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ObjectEnumeration(IHandlebars handlebars) { string source = @"{{#each myObject}}{{#if this.length}}{{@key}}{{#each this}}
  • {{this}}
  • {{/each}}
    {{/if}}{{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { myObject = new @@ -1372,12 +1515,12 @@ public void ObjectEnumeration() Assert.Equal("arr
  • hello
  • world

  • ", result); } - [Fact] - public void NestedDictionaryWithSegmentLiteral() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void NestedDictionaryWithSegmentLiteral(IHandlebars handlebars) { var source = "{{dictionary.[my key].[another key]}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { @@ -1396,11 +1539,10 @@ public void NestedDictionaryWithSegmentLiteral() Assert.Equal(expectedResult, result); } - [Fact] - public void ImplicitIDictionaryImplementationShouldNotThrowNullref() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ImplicitIDictionaryImplementationShouldNotThrowNullref(IHandlebars handlebars) { // Arrange - IHandlebars handlebars = Handlebars.Create(); handlebars.RegisterHelper("foo", (writer, context, arguments) => { }); var compile = handlebars.Compile(@"{{foo bar}}"); var mock = new MockDictionaryImplicitlyImplemented(new Dictionary { { "bar", 1 } }); @@ -1408,72 +1550,285 @@ public void ImplicitIDictionaryImplementationShouldNotThrowNullref() // Act compile.Invoke(mock); } - - [Fact] - public void ShouldBeAbleToHandleFieldContainingDots() + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleFieldContainingDots(IHandlebars handlebars) + { + var source = "Everybody was {{ foo.bar }}-{{ [foo.bar] }} {{ foo.[bar.baz].buz }}!"; + var template = handlebars.Compile(source); + var data = new Dictionary() + { + {"foo.bar", "fu"}, + {"foo", new Dictionary{{ "bar", "kung" }, { "bar.baz", new Dictionary {{ "buz", "fighting" }} }} } + }; + var result = template(data); + Assert.Equal("Everybody was kung-fu fighting!", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleListWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new List {"FOOBAR"}; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleDictionaryWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new Dictionary + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleJObjectsWithNumericalFields(IHandlebars handlebars) + { + var source = "{{ [0] }}"; + var template = handlebars.Compile(source); + var data = new JObject + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets(IHandlebars handlebars) + { + var source = + "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; + var template = handlebars.Compile(source); + var data = new Dictionary + { + {"noBracket", "foo"}, + {"[startsWithBracket", "bar"}, + {"endsWithBracket]", "baz"}, + {"[bothBrackets]", "buz"} + }; + var result = template(data); + Assert.Equal("foo foo bar baz buz", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicReturnFromHelper(IHandlebars Handlebars) { - var source = "Everybody was {{ foo.bar }}-{{ [foo.bar] }} {{ foo.[bar.baz].buz }}!"; + var getData = $"getData{Guid.NewGuid()}"; + Handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); + var source = $"{{{{{getData} 'data'}}}}"; var template = Handlebars.Compile(source); - var data = new Dictionary() + + var result = template(new object()); + Assert.Equal("data", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void CollectionReturnFromHelper(IHandlebars handlebars) + { + var getData = $"getData{Guid.NewGuid()}"; + handlebars.RegisterHelper(getData, (context, arguments) => + { + var data = new Dictionary + { + {"Nils", arguments[0].ToString()}, + {"Yehuda", arguments[1].ToString()} + }; + + return data; + }); + var source = $"{{{{#each ({getData} 'Darmstadt' 'San Francisco')}}}}{{{{@key}}}} lives in {{{{@value}}}}. {{{{/each}}}}"; + var template = handlebars.Compile(source); + + var result = template(new object()); + Assert.Equal("Nils lives in Darmstadt. Yehuda lives in San Francisco. ", result); + } + + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ReturnFromHelperWithSubExpression(IHandlebars handlebars) + { + var formatData = $"formatData{Guid.NewGuid()}"; + handlebars.RegisterHelper(formatData, (writer, context, arguments) => + { + writer.WriteSafeString(arguments[0]); + writer.WriteSafeString(" "); + writer.WriteSafeString(arguments[1]); + }); + + var getData = $"getData{Guid.NewGuid()}"; + handlebars.RegisterHelper(getData, (context, arguments) => + { + return arguments[0]; + }); + + var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; + var template = handlebars.Compile(source); + + var result = template(new object()); + Assert.Equal("data 42", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ReturnFromHelperLateBindWithSubExpression(IHandlebars handlebars) + { + var formatData = $"formatData{Guid.NewGuid()}"; + var getData = $"getData{Guid.NewGuid()}"; + + var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; + var template = handlebars.Compile(source); + + handlebars.RegisterHelper(formatData, (writer, context, arguments) => { - {"foo.bar", "fu"}, - {"foo", new Dictionary{{ "bar", "kung" }, { "bar.baz", new Dictionary {{ "buz", "fighting" }} }} } + writer.WriteSafeString(arguments[0]); + writer.WriteSafeString(" "); + writer.WriteSafeString(arguments[1]); + }); + + handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); + + var result = template(new object()); + Assert.Equal("data 42", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicLookup(IHandlebars handlebars) + { + var source = "{{#each people}}{{.}} lives in {{lookup ../cities @index}} {{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + people = new[]{"Nils", "Yehuda"}, + cities = new[]{"Darmstadt", "San Francisco"} }; + var result = template(data); - Assert.Equal("Everybody was kung-fu fighting!", result); + Assert.Equal("Nils lives in Darmstadt Yehuda lives in San Francisco ", result); } - - [Fact] - public void ShouldBeAbleToHandleListWithNumericalFields() + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void LookupAsSubExpression(IHandlebars handlebars) { - var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); - var data = new List {"FOOBAR"}; + var source = "{{#each persons}}{{name}} lives in {{#with (lookup ../cities [resident])~}}{{name}} ({{country}}){{/with}}{{/each}}"; + var template = handlebars.Compile(source); + var data = new + { + persons = new[] + { + new + { + name = "Nils", + resident = "darmstadt" + }, + new + { + name = "Yehuda", + resident = "san-francisco" + } + }, + cities = new Dictionary + { + ["darmstadt"] = new + { + name = "Darmstadt", + country = "Germany" + }, + ["san-francisco"] = new + { + name = "San Francisco", + country = "USA" + } + } + }; + var result = template(data); - Assert.Equal("FOOBAR", result); + Assert.Equal("Nils lives in Darmstadt (Germany)Yehuda lives in San Francisco (USA)", result); } - [Fact] - public void ShouldBeAbleToHandleDictionaryWithNumericalFields() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + private void StringConditionTest(IHandlebars handlebars) { - var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); - var data = new Dictionary + var expected = "\"correo\": \"correo@gmail.com\""; + var template = "{{#if Email}}\"correo\": \"{{Email}}\"{{else}}\"correo\": \"no hay correo\",{{/if}}"; + var data = new { - {"0", "FOOBAR"}, + Email = "correo@gmail.com" }; - var result = template(data); - Assert.Equal("FOOBAR", result); + + var func = handlebars.Compile(template); + var actual = func(data); + + Assert.Equal(expected, actual); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + private void CustomHelperResolverTest(IHandlebars handlebars) + { + handlebars.Configuration.HelperResolvers.Add(new StringHelperResolver()); + var template = "{{ toLower input }}"; + var func = handlebars.Compile(template); + var data = new { input = "ABC" }; + + var actual = func(data); + + Assert.Equal(data.input.ToLower(), actual); } - [Fact] - public void ShouldBeAbleToHandleJObjectsWithNumericalFields() + [Theory] + [InlineData("[one].two")] + [InlineData("one.[two]")] + [InlineData("[one].[two]")] + [InlineData("one.two")] + public void ReferencingDirectlyVariableWhenHelperRegistered(string helperName) { - var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); - var data = new JObject + foreach (IHandlebars handlebars in new HandlebarsEnvGenerator().Select(o => o[0])) { - {"0", "FOOBAR"}, - }; - var result = template(data); - Assert.Equal("FOOBAR", result); + var source = "{{ ./" + helperName + " }}"; + handlebars.RegisterHelper("one.two", (context, arguments) => 0); + + var template = handlebars.Compile(source); + + var actual = template(new { one = new { two = 42 } }); + + Assert.Equal("42", actual); + } } - [Fact] - public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() + private class StringHelperResolver : IHelperResolver { - var source = - "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; - var template = Handlebars.Compile(source); - var data = new Dictionary + public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) { - {"noBracket", "foo"}, - {"[startsWithBracket", "bar"}, - {"endsWithBracket]", "baz"}, - {"[bothBrackets]", "buz"} - }; - var result = template(data); - Assert.Equal("foo foo bar baz buz", result); + if (targetType == typeof(string)) + { + var method = targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (method == null) + { + helper = null; + return false; + } + + helper = (context, arguments) => method.Invoke(arguments[0], arguments.Skip(1).ToArray()); + return true; + } + + helper = null; + return false; + } + + public bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper) + { + helper = null; + return false; + } } private class MockDictionary : IDictionary @@ -1484,7 +1839,7 @@ public void Add(string key, string value) } public bool ContainsKey(string key) { - return true; + throw new NotImplementedException(); } public bool Remove(string key) { @@ -1492,7 +1847,8 @@ public bool Remove(string key) } public bool TryGetValue(string key, out string value) { - throw new NotImplementedException(); + value = "Hello world!"; + return true; } public string this[string index] { diff --git a/source/Handlebars.Test/DynamicTests.cs b/source/Handlebars.Test/DynamicTests.cs index a1f0a610..aa1e94ac 100644 --- a/source/Handlebars.Test/DynamicTests.cs +++ b/source/Handlebars.Test/DynamicTests.cs @@ -1,4 +1,4 @@ -using Xunit; +using Xunit; using System; using System.Dynamic; using System.Collections.Generic; @@ -76,11 +76,24 @@ public void JsonTestIfFalsyValue() Assert.Equal("Key1Val1Key2Val2", output); } + + [Fact] + public void JsonTestObjects(){ + var model = JObject.Parse("{\"Key1\": \"Val1\", \"Key2\": \"Val2\"}"); + + var source = "{{#each this}}{{@key}}{{@value}}{{/each}}"; + + var template = Handlebars.Compile(source); + + var output = template(model); + + Assert.Equal("Key1Val1Key2Val2", output); + } [Fact] public void JObjectTest() { object nullValue = null; - var model = JObject.FromObject(new { Nested = new { Prop = "Prop" }, Nested2 = nullValue }); + JObject model = JObject.FromObject(new { Nested = new { Prop = "Prop" }, Nested2 = nullValue }); var source = "{{NotExists.Prop}}"; diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 30c55e86..8eb69d95 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -1,4 +1,4 @@ - + full @@ -10,7 +10,7 @@ false false - + 0618;1701 @@ -24,10 +24,11 @@ $(DefineConstants);netFramework - + $(DefineConstants);netcoreapp;netstandard + $(DefineConstants);netcoreapp;netstandard @@ -45,6 +46,7 @@ + @@ -52,16 +54,17 @@ + - + - + diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 38c9dae3..a09cd202 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -1,7 +1,8 @@ -using Xunit; +using Xunit; using System; -using System.Collections; using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Features; namespace HandlebarsDotNet.Test { @@ -29,6 +30,344 @@ public void HelperWithLiteralArguments() Assert.Equal(expected, output); } + [Theory] + [InlineData("one.two")] + [InlineData("[one.two]")] + [InlineData("[one].two")] + [InlineData("one.[two]")] + public void HelperWithDotSeparatedNameWithNoParameters(string helperName) + { + var source = "{{ " + helperName + " }}"; + var handlebars = Handlebars.Create(); + handlebars.Configuration.Compatibility.RelaxedHelperNaming = true; + handlebars.RegisterHelper("one.two", (context, arguments) => 42); + + var template = handlebars.Compile(source); + + var actual = template(null); + + Assert.Equal("42", actual); + } + + [Theory] + [InlineData("one.two")] + [InlineData("[one.two]")] + [InlineData("[one].two")] + [InlineData("one.[two]")] + public void HelperWithDotSeparatedNameWithParameters(string helperName) + { + var source = "{{ " + helperName + " 'a' 'b' }}"; + var handlebars = Handlebars.Create(); + handlebars.Configuration.Compatibility.RelaxedHelperNaming = true; + handlebars.RegisterHelper("one.two", (context, arguments) => "42" + arguments[0] + arguments[1]); + + var template = handlebars.Compile(source); + + var actual = template(null); + + Assert.Equal("42ab", actual); + } + + [Fact] + public void BlockHelperWithBlockParams() + { + Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { + var count = 0; + options.BlockParams((parameters, binder, deps) => + { + binder(parameters.ElementAtOrDefault(0), ctx => ++count); + }); + + foreach(var arg in args) + { + options.Template(writer, arg); + } + }); + + var source = "Here are some things: {{#myHelper 'foo' 'bar' as |counter|}}{{counter}}:{{this}}\n{{/myHelper}}"; + + var template = Handlebars.Compile(source); + + var output = template(new { }); + + var expected = "Here are some things: 1:foo\n2:bar\n"; + + Assert.Equal(expected, output); + } + + [Fact] + public void BlockHelperLateBound() + { + var source = "Here are some things: \n" + + "{{#myHelper 'foo' 'bar' as |counter|}}\n" + + "{{counter}}:{{this}}\n" + + "{{/myHelper}}"; + + var template = Handlebars.Compile(source); + + Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { + var count = 0; + options.BlockParams((parameters, binder, deps) => + binder(parameters.ElementAtOrDefault(0), ctx => ++count)); + + foreach(var arg in args) + { + options.Template(writer, arg); + } + }); + + var output = template(new { }); + + var expected = "Here are some things: \n1:foo\n2:bar\n"; + + Assert.Equal(expected, output); + } + + [Fact] + public void BlockHelperLateBoundConflictsWithValue() + { + var source = "{{#late}}late{{/late}}"; + + var handlebars = Handlebars.Create(); + var template = handlebars.Compile(source); + + handlebars.RegisterHelper("late", (writer, options, context, args) => + { + options.Template(writer, context); + }); + + var output = template(new { late = "should be ignored" }); + + var expected = "late"; + + Assert.Equal(expected, output); + } + + [Fact] + public void BlockHelperLateBoundMissingHelperFallbackToDeferredSection() + { + var source = "{{#late}}late{{/late}}"; + + var handlebars = Handlebars.Create(); + handlebars.Configuration.RegisterMissingHelperHook( + (context, arguments) => "Hook" + ); + var template = handlebars.Compile(source); + + var output = template(new { late = "late" }); + + var expected = "late"; + + Assert.Equal(expected, output); + } + + [Fact] + public void HelperLateBound() + { + var source = "{{lateHelper}}"; + + var template = Handlebars.Compile(source); + + var expected = "late"; + Handlebars.RegisterHelper("lateHelper", (writer, context, arguments) => + { + writer.WriteSafeString(expected); + }); + + var output = template(null); + + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("[$lateHelper]")] + [InlineData("[late.Helper]")] + [InlineData("[@lateHelper]")] + public void HelperEscapedLateBound(string helperName) + { + var handlebars = Handlebars.Create(); + + var source = "{{" + helperName + "}}"; + + var template = handlebars.Compile(source); + + var expected = "late"; + handlebars.RegisterHelper(helperName.Trim('[', ']'), (writer, context, arguments) => + { + writer.WriteSafeString(expected); + }); + + var output = template(null); + + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("{{lateHelper.a}}")] + [InlineData("{{[lateHelper].a}}")] + [InlineData("{{[lateHelper.a]}}")] + public void WrongHelperLiteralLateBound(string source) + { + var handlebars = Handlebars.Create(); + + var template = handlebars.Compile(source); + + handlebars.RegisterHelper("lateHelper", (writer, context, arguments) => + { + writer.WriteSafeString("should not appear"); + }); + + var output = template(null); + + Assert.Equal(string.Empty, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingHelperHook(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing helper: {0}"; + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => + { + var name = arguments.Last().ToString(); + return string.Format(format, name.Trim('[', ']')); + }); + + var source = "{{"+ helperName +"}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Fact] + public void MissingHelperHookViaFeatureAndMethod() + { + var expected = "Hook"; + var handlebars = Handlebars.Create(); + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => expected + ); + + handlebars.RegisterHelper("helperMissing", + (context, arguments) => "Should be ignored" + ); + + var source = "{{missing}}"; + var template = handlebars.Compile(source); + + var output = template(null); + + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingHelperHookViaHelperRegistration(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing helper: {0}"; + handlebars.RegisterHelper("helperMissing", (context, arguments) => + { + var name = arguments.Last().ToString(); + return string.Format(format, name.Trim('[', ']')); + }); + + var source = "{{"+ helperName +"}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingBlockHelperHook(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing block helper: {0}"; + handlebars.Configuration + .RegisterMissingHelperHook( + blockHelperMissing: (writer, options, context, arguments) => + { + var name = options.GetValue("name"); + writer.WriteSafeString(string.Format(format, name.Trim('[', ']'))); + }); + + var source = "{{#"+ helperName +"}}should not appear{{/" + helperName + "}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingBlockHelperHookViaHelperRegistration(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing block helper: {0}"; + handlebars.RegisterHelper("blockHelperMissing", (writer, options, context, arguments) => + { + var name = options.GetValue("name"); + writer.WriteSafeString(string.Format(format, name.Trim('[', ']'))); + }); + + var source = "{{#"+ helperName +"}}should not appear{{/" + helperName + "}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Fact] + public void MissingHelperHookWhenVariableExists() + { + var handlebars = Handlebars.Create(); + var expected = "Variable"; + + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => "Hook" + ); + + var source = "{{missing}}"; + + var template = Handlebars.Compile(source); + + var output = template(new { missing = "Variable" }); + + Assert.Equal(expected, output); + } + [Fact] public void HelperWithLiteralArgumentsWithQuotes() { @@ -339,7 +678,8 @@ public void HelperWithNumericArguments() [Fact] public void HelperWithHashArgument() { - Handlebars.RegisterHelper("myHelper", (writer, context, args) => { + var h = Handlebars.Create(); + h.RegisterHelper("myHelper", (writer, context, args) => { var hash = args[2] as Dictionary; foreach(var item in hash) { @@ -349,7 +689,7 @@ public void HelperWithHashArgument() var source = "Here are some things:{{myHelper 'foo' 'bar' item1='val1' item2='val2'}}"; - var template = Handlebars.Compile(source); + var template = h.Compile(source); var output = template(new { }); diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs new file mode 100644 index 00000000..985ee853 --- /dev/null +++ b/source/Handlebars.Test/IssueTests.cs @@ -0,0 +1,109 @@ +using System.Dynamic; +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class IssueTests + { + // Issue https://github.com/zjklee/Handlebars.CSharp/issues/7 + [Fact] + public void ValueVariableShouldNotBeAccessibleFromContext() + { + var handlebars = Handlebars.Create(); + var render = handlebars.Compile("{{value}}"); + var output = render(new + { + anotherValue = "Test" + }); + + Assert.Equal("", output); + } + + // Issue https://github.com/rexm/Handlebars.Net/issues/351 + [Fact] + public void PerhapsNull() + { + var handlebars = Handlebars.Create(); + var render = handlebars.Compile("{{#if PerhapsNull}}It's not null{{else}}It's null{{/if}}"); + dynamic data = new ExpandoObject(); + data.PerhapsNull = null; + + var actual = render(data); + Assert.Equal("It's null", actual); + } + + // Issue https://github.com/rexm/Handlebars.Net/issues/350 + // the helper has priority + // https://handlebarsjs.com/guide/expressions.html#disambiguating-helpers-calls-and-property-lookup + [Fact] + public void HelperWithSameNameVariable() + { + var handlebars = Handlebars.Create(); + var expected = "Helper"; + handlebars.RegisterHelper("foo", (context, arguments) => expected); + + var template = handlebars.Compile("{{foo}}"); + var data = new {foo = "Variable"}; + var actual = template(data); + Assert.Equal(expected, actual); + } + + // Issue https://github.com/rexm/Handlebars.Net/issues/350 + [Fact] + public void LateBoundHelperWithSameNameVariable() + { + var handlebars = Handlebars.Create(); + var template = handlebars.Compile("{{amoeba}}"); + + Assert.Equal("Variable", template(new {amoeba = "Variable"})); + + handlebars.RegisterHelper("amoeba", (writer, context, arguments) => { writer.Write("Helper"); }); + + Assert.Equal("Helper", template(new {amoeba = "Variable"})); + } + + // Issue https://github.com/rexm/Handlebars.Net/issues/350 + [Fact] + public void LateBoundHelperWithSameNameVariablePath() + { + var handlebars = Handlebars.Create(); + var expected = "Variable"; + var template = handlebars.Compile("{{amoeba.a}}"); + var data = new {amoeba = new {a = expected}}; + + var actual = template(data); + Assert.Equal(expected, actual); + + handlebars.RegisterHelper("amoeba", (context, arguments) => "Helper"); + + actual = template(data); + Assert.Equal(expected, actual); + } + + // Issue https://github.com/rexm/Handlebars.Net/issues/354 + [Fact] + public void BlockHelperWithInversion() + { + string source = "{{^test input}}empty{{else}}not empty{{/test}}"; + + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("test", (output, options, context, arguments) => + { + if (HandlebarsUtils.IsTruthy(arguments[0])) + { + options.Template(output, context); + } + else + { + options.Inverse(output, context); + } + }); + + var template = handlebars.Compile(source); + + Assert.Equal("empty", template(null)); + Assert.Equal("empty", template(new { otherInput = 1 })); + Assert.Equal("not empty", template(new { input = 1 })); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/IteratorTests.cs b/source/Handlebars.Test/IteratorTests.cs index a6ad6a20..e5d43f55 100644 --- a/source/Handlebars.Test/IteratorTests.cs +++ b/source/Handlebars.Test/IteratorTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Xunit; namespace HandlebarsDotNet.Test @@ -9,7 +10,7 @@ public class IteratorTests public void BasicIterator() { var source = "Hello,{{#each people}}\n- {{name}}{{/each}}"; - var template = Handlebars.Compile(source); + var template = Handlebars.Create().Compile(source); var data = new { people = new []{ new { @@ -86,7 +87,7 @@ public void WithParentIndex() {{/each}} {{/each}} {{/each}}"; - var template = Handlebars.Compile( source ); + var template = Handlebars.Create().Compile( source ); var data = new { level1 = new[]{ @@ -276,6 +277,19 @@ public void NullSequence() var result = template(data); Assert.Equal("Hello, (no one listed)", result); } + + [Fact] + public void EnumerableIterator() + { + var source = "{{#each people}}{{.}}{{@index}}{{/each}}"; + var template = Handlebars.Compile(source); + var data = new + { + people = Enumerable.Range(0, 3).Select(x => x.ToString()) + }; + var result = template(data); + Assert.Equal("001122", result); + } } } diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs index 0454e6ae..801f2800 100644 --- a/source/Handlebars.Test/PartialTests.cs +++ b/source/Handlebars.Test/PartialTests.cs @@ -188,15 +188,15 @@ public void BasicPartialWithSubExpressionParameters() { string source = "Hello, {{>person first=(_ first arg1=(_ \"value\")) last=(_ last)}}!"; - Handlebars.RegisterHelper("_", (output, context, arguments) => - { - output.Write(arguments[0].ToString()); - - if (arguments.Length > 1) - { - var hash = arguments[1] as Dictionary; - output.Write(hash["arg1"]); - } + Handlebars.RegisterHelper("_", (output, context, arguments) => + { + output.Write(arguments[0].ToString()); + + if (arguments.Length > 1) + { + var hash = arguments[1] as Dictionary; + output.Write(hash["arg1"]); + } }); var template = Handlebars.Compile(source); @@ -597,12 +597,12 @@ public void TemplateWithSpecialNamedPartial() Assert.Equal("Referenced partial name @partial-block could not be resolved", ex.Message); } - public class TestMissingPartialTemplateHandler : IMissingPartialTemplateHandler - { - public void Handle(HandlebarsConfiguration configuration, string partialName, TextWriter textWriter) - { - textWriter.Write($"Partial Not Found: {partialName}"); - } + public class TestMissingPartialTemplateHandler : IMissingPartialTemplateHandler + { + public void Handle(HandlebarsConfiguration configuration, string partialName, TextWriter textWriter) + { + textWriter.Write($"Partial Not Found: {partialName}"); + } } [Fact] @@ -610,9 +610,9 @@ public void MissingPartialTemplateHandler() { var source = "Missing template should not throw exception: {{> missing }}"; - var handlebars = Handlebars.Create(new HandlebarsConfiguration - { - MissingPartialTemplateHandler = new TestMissingPartialTemplateHandler() + var handlebars = Handlebars.Create(new HandlebarsConfiguration + { + MissingPartialTemplateHandler = new TestMissingPartialTemplateHandler() }); var template = handlebars.Compile(source); diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 0200373c..63571124 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -1,8 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using HandlebarsDotNet; +using System.IO; +using System.Text; using Xunit; namespace HandlebarsDotNet.Test.ViewEngine @@ -21,13 +21,36 @@ public void CanLoadAViewWithALayout() }; //When a viewengine renders that view - var handleBars = Handlebars.Create(new HandlebarsConfiguration() {FileSystem = files}); - var renderView = handleBars.CompileView("views\\someview.hbs"); + var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); + var renderView = handleBars.CompileView ("views\\someview.hbs"); var output = renderView(null); - + + //Then the correct output should be rendered + Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output); + } + [Fact] + public void CanLoadAWriterViewWithALayout() + { + //Given a layout in a subfolder + var files = new FakeFileSystem() + { + {"views\\somelayout.hbs", "layout start\r\n{{{body}}}\r\nlayout end"}, + //And a view in the same folder which uses that layout + { "views\\someview.hbs", "{{!< somelayout}}This is the body"} + }; + + //When a viewengine renders that view + var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); + var renderView = handleBars.CompileView("views\\someview.hbs", null); + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + renderView(writer, null); + var output = sb.ToString(); + //Then the correct output should be rendered Assert.Equal("layout start\r\nThis is the body\r\nlayout end", output); } + [Fact] public void CanLoadAViewWithALayoutInTheRoot() { diff --git a/source/Handlebars.Test/WhitespaceTests.cs b/source/Handlebars.Test/WhitespaceTests.cs index b35def3d..725cf3dd 100644 --- a/source/Handlebars.Test/WhitespaceTests.cs +++ b/source/Handlebars.Test/WhitespaceTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using Xunit; namespace HandlebarsDotNet.Test @@ -123,14 +123,14 @@ public void StandaloneSection() [Fact] public void StandaloneInvertedSection() { - var source = " {{^some}}\n{{none}}\n{{else}}\n{{none}}\n{{/some}} "; + var source = " {{^some}}{{none}}{{else}}{{none}}{{/some}} "; var template = _handlebars.Compile(source); var data = new {none = "No people"}; var result = template(data); - Assert.Equal("No people\n", result); + Assert.Equal(" No people ", result); } [Fact] diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 969e6cb0..0224c9d9 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26403.3 @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" ProjectSection(SolutionItems) = preProject ..\README.md = ..\README.md + Directory.Build.props = Directory.Build.props EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{417E2E51-2DD2-4045-84E5-BA66484E957B}" diff --git a/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs b/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs new file mode 100644 index 00000000..5248d982 --- /dev/null +++ b/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs @@ -0,0 +1,27 @@ +namespace HandlebarsDotNet.Adapters +{ + // Will be removed in next iterations + internal class HelperToReturnHelperAdapter + { + private readonly HandlebarsHelper _helper; + private readonly HandlebarsReturnHelper _delegate; + + public HelperToReturnHelperAdapter(HandlebarsHelper helper) + { + _helper = helper; + _delegate = (context, arguments) => + { + using (var writer = new PolledStringWriter()) + { + _helper(writer, context, arguments); + return writer.ToString(); + } + }; + } + + public static implicit operator HandlebarsReturnHelper(HelperToReturnHelperAdapter adapter) + { + return adapter._delegate; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaEnricher.cs b/source/Handlebars/Adapters/LambdaEnricher.cs new file mode 100644 index 00000000..9e9467dd --- /dev/null +++ b/source/Handlebars/Adapters/LambdaEnricher.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Adapters +{ + internal class LambdaEnricher + { + private readonly Action _direct; + private readonly Action _inverse; + + public LambdaEnricher(Action direct, Action inverse) + { + _direct = direct; + _inverse = inverse; + + Direct = (context, writer, arg) => _direct(writer, arg); + Inverse = (context, writer, arg) => _inverse(writer, arg); + } + + public readonly Action Direct; + public readonly Action Inverse; + } +} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaReducer.cs b/source/Handlebars/Adapters/LambdaReducer.cs new file mode 100644 index 00000000..9d82c093 --- /dev/null +++ b/source/Handlebars/Adapters/LambdaReducer.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Adapters +{ + // Will be removed in next iterations + internal class LambdaReducer + { + private readonly BindingContext _context; + private readonly Action _direct; + private readonly Action _inverse; + + public LambdaReducer(BindingContext context, Action direct, Action inverse) + { + _context = context; + _direct = direct; + _inverse = inverse; + + Direct = (writer, arg) => _direct(_context, writer, arg); + Inverse = (writer, arg) => _inverse(_context, writer, arg); + } + + public readonly Action Direct; + public readonly Action Inverse; + } +} \ No newline at end of file diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs deleted file mode 100644 index 046ab415..00000000 --- a/source/Handlebars/BuiltinHelpers.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Collections.Generic; -using HandlebarsDotNet.Compiler; - -namespace HandlebarsDotNet -{ - internal static class BuiltinHelpers - { - [Description("with")] - public static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) - { - if (arguments.Length != 1) - { - throw new HandlebarsException("{{with}} helper must have exactly one argument"); - } - - if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) - { - options.Template(output, arguments[0]); - } - else - { - options.Inverse(output, context); - } - } - - [Description("*inline")] - public static void Inline(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) - { - if (arguments.Length != 1) - { - throw new HandlebarsException("{{*inline}} helper must have exactly one argument"); - } - - //This helper needs the "context" var to be the complete BindingContext as opposed to just the - //data { firstName: "todd" }. The full BindingContext is needed for registering the partial templates. - //This magic happens in BlockHelperFunctionbinder.VisitBlockHelperExpression - - if (context as BindingContext == null) - { - throw new HandlebarsException("{{*inline}} helper must receiving the full BindingContext"); - } - - var key = arguments[0] as string; - - //Inline partials cannot use the Handlebars.RegisterTemplate method - //because it is static and therefore app-wide. To prevent collisions - //this helper will add the compiled partial to a dicionary - //that is passed around in the context without fear of collisions. - context.InlinePartialTemplates.Add(key, options.Template); - } - - public static IEnumerable> Helpers - { - get - { - return GetHelpers(); - } - } - - public static IEnumerable> BlockHelpers - { - get - { - return GetHelpers(); - } - } - - private static IEnumerable> GetHelpers() - { - var builtInHelpersType = typeof(BuiltinHelpers); - foreach (var method in builtInHelpersType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public)) - { - Delegate possibleDelegate; - try - { -#if netstandard - possibleDelegate = method.CreateDelegate(typeof(T)); -#else - possibleDelegate = Delegate.CreateDelegate(typeof(T), method); -#endif - } - catch - { - possibleDelegate = null; - } - if (possibleDelegate != null) - { -#if netstandard - yield return new KeyValuePair( - method.GetCustomAttribute().Description, - (T)(object)possibleDelegate); -#else - yield return new KeyValuePair( - ((DescriptionAttribute)Attribute.GetCustomAttribute(method, typeof(DescriptionAttribute))).Description, - (T)(object)possibleDelegate); -#endif - } - } - } - } -} - diff --git a/source/Handlebars/Collections/CascadeCollection.cs b/source/Handlebars/Collections/CascadeCollection.cs new file mode 100644 index 00000000..d0518e3d --- /dev/null +++ b/source/Handlebars/Collections/CascadeCollection.cs @@ -0,0 +1,68 @@ +using System.Collections; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + // Will be removed in next iterations + internal class CascadeCollection : ICollection + { + private readonly ICollection _outer; + private readonly ICollection _inner = new List(); + + public CascadeCollection(ICollection outer) + { + _outer = outer; + } + + public IEnumerator GetEnumerator() + { + foreach (var value in _outer) + { + yield return value; + } + + foreach (var value in _inner) + { + yield return value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(T item) + { + _inner.Add(item); + } + + public void Clear() + { + _inner.Clear(); + } + + public bool Contains(T item) + { + return _inner.Contains(item) || _outer.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + foreach (var value in this) + { + array[arrayIndex] = value; + arrayIndex++; + } + } + + public bool Remove(T item) + { + return _inner.Remove(item); + } + + public int Count => _outer.Count + _inner.Count; + + public bool IsReadOnly => _inner.IsReadOnly; + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/CascadeDictionary.cs b/source/Handlebars/Collections/CascadeDictionary.cs new file mode 100644 index 00000000..206c5898 --- /dev/null +++ b/source/Handlebars/Collections/CascadeDictionary.cs @@ -0,0 +1,137 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.Collections +{ + internal class CascadeDictionary : IDictionary + { + private readonly IDictionary _outer; + private readonly IDictionary _inner; + + public CascadeDictionary(IDictionary outer, IEqualityComparer comparer = null) + { + _outer = outer; + _inner = new Dictionary(comparer); + } + + public IEnumerator> GetEnumerator() + { + foreach (var value in _outer) + { + if (_inner.TryGetValue(value.Key, out var innerValue)) + { + yield return new KeyValuePair(value.Key, innerValue); + } + else + { + yield return value; + } + } + + foreach (var value in _inner) + { + if (_outer.ContainsKey(value.Key)) continue; + + yield return value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(KeyValuePair item) + { + _inner.Add(item); + } + + public void Clear() + { + _inner.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _inner.Contains(item) || _outer.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var value in _outer) + { + if (_inner.TryGetValue(value.Key, out var innerValue)) + { + array[arrayIndex] = new KeyValuePair(value.Key, innerValue); + arrayIndex++; + } + else + { + array[arrayIndex] = value; + arrayIndex++; + } + } + + foreach (var value in _inner) + { + if (_outer.ContainsKey(value.Key)) continue; + + array[arrayIndex] = value; + arrayIndex++; + } + } + + public bool Remove(KeyValuePair item) + { + return _inner.Remove(item); + } + + public int Count + { + get + { + var count = _outer.Count; + foreach (var value in _inner) + { + if (_outer.ContainsKey(value.Key)) continue; + count++; + } + + return count; + } + } + + public bool IsReadOnly => _inner.IsReadOnly; + + public bool ContainsKey(TKey key) + { + return _inner.ContainsKey(key) || _outer.ContainsKey(key); + } + + public void Add(TKey key, TValue value) + { + _inner.Add(key, value); + } + + public bool Remove(TKey key) + { + return _inner.Remove(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return _inner.TryGetValue(key, out value) || _outer.TryGetValue(key, out value); + } + + public TValue this[TKey key] + { + get => !_inner.TryGetValue(key, out var value) ? _outer[key] : value; + set => _inner[key] = value; + } + + public ICollection Keys => this.Select(o => o.Key).ToArray(); + + public ICollection Values => this.Select(o => o.Value).ToArray(); + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/DeferredValue.cs b/source/Handlebars/Collections/DeferredValue.cs new file mode 100644 index 00000000..fed3e601 --- /dev/null +++ b/source/Handlebars/Collections/DeferredValue.cs @@ -0,0 +1,31 @@ +using System; + +namespace HandlebarsDotNet.Collections +{ + internal class DeferredValue + { + private readonly TState _state; + private readonly Func _factory; + + private T _value; + private bool _isValueCreated; + + public DeferredValue(TState state, Func factory) + { + _state = state; + _factory = factory; + } + + public T Value + { + get + { + if (_isValueCreated) return _value; + + _value = _factory(_state); + _isValueCreated = true; + return _value; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/DisposableContainer.cs b/source/Handlebars/Collections/DisposableContainer.cs new file mode 100644 index 00000000..5d0e795b --- /dev/null +++ b/source/Handlebars/Collections/DisposableContainer.cs @@ -0,0 +1,21 @@ +using System; + +namespace HandlebarsDotNet +{ + internal struct DisposableContainer : IDisposable + { + private readonly Action _onDispose; + public readonly T Value; + + public DisposableContainer(T value, Action onDispose) + { + _onDispose = onDispose; + Value = value; + } + + public void Dispose() + { + _onDispose(Value); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/ExtendedEnumerable.cs b/source/Handlebars/Collections/ExtendedEnumerable.cs similarity index 59% rename from source/Handlebars/Compiler/Translation/Expression/ExtendedEnumerable.cs rename to source/Handlebars/Collections/ExtendedEnumerable.cs index c4749f30..cc388627 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ExtendedEnumerable.cs +++ b/source/Handlebars/Collections/ExtendedEnumerable.cs @@ -1,38 +1,31 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace HandlebarsDotNet.Compiler +namespace HandlebarsDotNet.Collections { /// /// Wraps and provide additional information about the iteration via /// - internal sealed class ExtendedEnumerable : IEnumerable> + internal sealed class ExtendedEnumerable { - private readonly IEnumerable _enumerable; + private Enumerator _enumerator; public ExtendedEnumerable(IEnumerable enumerable) { - _enumerable = enumerable; + _enumerator = new Enumerator(enumerable.GetEnumerator()); } - public IEnumerator> GetEnumerator() + public ref Enumerator GetEnumerator() { - return new Enumerator(_enumerable.GetEnumerator()); + return ref _enumerator; } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private sealed class Enumerator : IEnumerator> + internal struct Enumerator { private readonly IEnumerator _enumerator; private Container _next; private int _index; - public Enumerator(IEnumerator enumerator) + public Enumerator(IEnumerator enumerator) : this() { _enumerator = enumerator; PerformIteration(); @@ -49,28 +42,13 @@ public bool MoveNext() return true; } - public void Reset() - { - _next = null; - Current = null; - _enumerator.Reset(); - PerformIteration(); - } - - object IEnumerator.Current => Current; - - public void Dispose() - { - (_enumerator as IDisposable)?.Dispose(); - } - private void PerformIteration() { if (!_enumerator.MoveNext()) { Current = _next != null ? new EnumeratorValue(_next.Value, _index++, true) - : null; + : EnumeratorValue.Empty; _next = null; return; @@ -85,21 +63,23 @@ private void PerformIteration() Current = new EnumeratorValue(_next.Value, _index++, false); _next.Value = (T) _enumerator.Current; } - - private class Container - { - public TValue Value { get; set; } + } + + private class Container + { + public TValue Value { get; set; } - public Container(TValue value) - { - Value = value; - } + public Container(TValue value) + { + Value = value; } } } - - internal class EnumeratorValue + + internal struct EnumeratorValue { + public static readonly EnumeratorValue Empty = new EnumeratorValue(); + public EnumeratorValue(T value, int index, bool isLast) { Value = value; diff --git a/source/Handlebars/Collections/HashSetSlim.cs b/source/Handlebars/Collections/HashSetSlim.cs new file mode 100644 index 00000000..37d7f56f --- /dev/null +++ b/source/Handlebars/Collections/HashSetSlim.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading; + +namespace HandlebarsDotNet.Collections +{ + // Will be removed in next iterations + internal sealed class HashSetSlim + { + private HashSet _inner; + private readonly IEqualityComparer _comparer; + + public HashSetSlim(IEqualityComparer comparer = null) + { + _comparer = comparer ?? EqualityComparer.Default; + _inner = new HashSet(_comparer); + } + + public bool Contains(TKey key) + { + return _inner.Contains(key); + } + + public void Add(TKey key) + { + var copy = new HashSet(_inner, _comparer) + { + key + }; + + Interlocked.CompareExchange(ref _inner, copy, _inner); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/HashedCollection.cs b/source/Handlebars/Collections/HashedCollection.cs new file mode 100644 index 00000000..0dc0fb65 --- /dev/null +++ b/source/Handlebars/Collections/HashedCollection.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal class HashedCollection : IReadOnlyList, ICollection where T:class + { + private readonly List _list = new List(); + private readonly Dictionary _mapping = new Dictionary(); + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _list.Count; i++) + { + yield return _list[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(T item) + { + if(item == null) throw new ArgumentNullException(nameof(item)); + + if(_mapping.ContainsKey(item)) return; + _mapping.Add(item, _list.Count); + _list.Add(item); + } + + public void Clear() + { + _list.Clear(); + _mapping.Clear(); + } + + public bool Contains(T item) + { + if(item == null) throw new ArgumentNullException(nameof(item)); + + return _mapping.ContainsKey(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + if(item == null) throw new ArgumentNullException(nameof(item)); + + if (!_mapping.TryGetValue(item, out var index)) return false; + _list.RemoveAt(index); + _mapping.Remove(item); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly { get; } = false; + + public T this[int index] => _list[index]; + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/LookupSlim.cs b/source/Handlebars/Collections/LookupSlim.cs new file mode 100644 index 00000000..35972c48 --- /dev/null +++ b/source/Handlebars/Collections/LookupSlim.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace HandlebarsDotNet.Collections +{ + internal sealed class LookupSlim + { + private Dictionary _inner; + private readonly IEqualityComparer _comparer; + + public LookupSlim(IEqualityComparer comparer = null) + { + _comparer = comparer ?? EqualityComparer.Default; + _inner = new Dictionary(_comparer); + } + + public bool ContainsKey(TKey key) + { + return _inner.ContainsKey(key); + } + + public TValue GetOrAdd(TKey key, Func valueFactory) + { + return !_inner.TryGetValue(key, out var value) + ? Write(key, valueFactory(key)) + : value; + } + + public TValue GetOrAdd(TKey key, Func valueFactory, TState state) + { + return !_inner.TryGetValue(key, out var value) + ? Write(key, valueFactory(key, state)) + : value; + } + + public bool TryGetValue(TKey key, out TValue value) + { + return _inner.TryGetValue(key, out value); + } + + private TValue Write(TKey key, TValue value) + { + var copy = new Dictionary(_inner, _comparer) + { + [key] = value + }; + + Interlocked.CompareExchange(ref _inner, copy, _inner); + + return value; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/ObjectPool.cs b/source/Handlebars/Collections/ObjectPool.cs new file mode 100644 index 00000000..ca55c043 --- /dev/null +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.ObjectPool; + +namespace HandlebarsDotNet +{ + internal static class ObjectPoolExtensions + { + public static DisposableContainer Use(this ObjectPool objectPool) where T : class + { + return new DisposableContainer(objectPool.Get(), objectPool.Return); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/StringBuilderPool.cs b/source/Handlebars/Collections/StringBuilderPool.cs new file mode 100644 index 00000000..42b03caf --- /dev/null +++ b/source/Handlebars/Collections/StringBuilderPool.cs @@ -0,0 +1,18 @@ +using System; +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace HandlebarsDotNet +{ + internal class StringBuilderPool : DefaultObjectPool + { + private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); + + public static StringBuilderPool Shared => Lazy.Value; + + public StringBuilderPool(int initialCapacity = 16) + : base(new StringBuilderPooledObjectPolicy{ InitialCapacity = initialCapacity }) + { + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/CompilationContext.cs b/source/Handlebars/Compiler/CompilationContext.cs index 83733c06..4c67314c 100644 --- a/source/Handlebars/Compiler/CompilationContext.cs +++ b/source/Handlebars/Compiler/CompilationContext.cs @@ -1,31 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { - internal class CompilationContext + internal sealed class CompilationContext { - private readonly HandlebarsConfiguration _configuration; - private readonly ParameterExpression _bindingContext; - - public CompilationContext(HandlebarsConfiguration configuration) + public CompilationContext(InternalHandlebarsConfiguration configuration) { - _configuration = configuration; - _bindingContext = Expression.Variable(typeof(BindingContext), "context"); + Configuration = configuration; + BindingContext = Expression.Variable(typeof(BindingContext), "context"); } - public virtual HandlebarsConfiguration Configuration - { - get { return _configuration; } - } + public InternalHandlebarsConfiguration Configuration { get; } - public virtual ParameterExpression BindingContext - { - get { return _bindingContext; } - } + public ParameterExpression BindingContext { get; } } } diff --git a/source/Handlebars/Compiler/ExpressionBuilder.cs b/source/Handlebars/Compiler/ExpressionBuilder.cs index 02f2dddd..624d6916 100644 --- a/source/Handlebars/Compiler/ExpressionBuilder.cs +++ b/source/Handlebars/Compiler/ExpressionBuilder.cs @@ -6,9 +6,9 @@ namespace HandlebarsDotNet.Compiler { internal class ExpressionBuilder { - private readonly HandlebarsConfiguration _configuration; + private readonly InternalHandlebarsConfiguration _configuration; - public ExpressionBuilder(HandlebarsConfiguration configuration) + public ExpressionBuilder(InternalHandlebarsConfiguration configuration) { _configuration = configuration; } @@ -21,6 +21,7 @@ public IEnumerable ConvertTokensToExpressions(IEnumerable to tokens = LiteralConverter.Convert(tokens); tokens = HashParameterConverter.Convert(tokens); tokens = PathConverter.Convert(tokens); + tokens = BlockParamsConverter.Convert(tokens); tokens = SubExpressionConverter.Convert(tokens); tokens = HashParametersAccumulator.Accumulate(tokens); tokens = PartialConverter.Convert(tokens); diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index 7b71b703..f1f95d9f 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -6,62 +6,77 @@ namespace HandlebarsDotNet.Compiler { - internal class FunctionBuilder + internal static class FunctionBuilder { - private readonly HandlebarsConfiguration _configuration; - private static readonly Expression _emptyLambda = + 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) => {}; - public FunctionBuilder(HandlebarsConfiguration configuration) + public static Expression Reduce(Expression expression, CompilationContext context) { - _configuration = configuration; + expression = new CommentVisitor().Visit(expression); + expression = new UnencodedStatementVisitor(context).Visit(expression); + expression = new PartialBinder(context).Visit(expression); + expression = new StaticReplacer(context).Visit(expression); + expression = new IteratorBinder(context).Visit(expression); + expression = new BlockHelperFunctionBinder(context).Visit(expression); + expression = new HelperFunctionBinder(context).Visit(expression); + expression = new BoolishConverter(context).Visit(expression); + expression = new PathBinder(context).Visit(expression); + expression = new SubExpressionVisitor(context).Visit(expression); + expression = new HashParameterBinder().Visit(expression); + + return expression; } - public Expression Compile(IEnumerable expressions, Expression parentContext, string templatePath = null) + public static Action CompileCore(IEnumerable expressions, InternalHandlebarsConfiguration configuration, string templatePath = null) { try { - if (expressions.Any() == false) + if (!expressions.Any()) { - return _emptyLambda; + return EmptyLambdaWithContext; } - if (expressions.IsOneOf() == true) + if (expressions.IsOneOf()) { - return _emptyLambda; + return EmptyLambdaWithContext; } - var compilationContext = new CompilationContext(_configuration); - var expression = CreateExpressionBlock(expressions); - expression = CommentVisitor.Visit(expression, compilationContext); - expression = UnencodedStatementVisitor.Visit(expression, compilationContext); - expression = PartialBinder.Bind(expression, compilationContext); - expression = StaticReplacer.Replace(expression, compilationContext); - expression = IteratorBinder.Bind(expression, compilationContext); - expression = BlockHelperFunctionBinder.Bind(expression, compilationContext); - expression = DeferredSectionVisitor.Bind(expression, compilationContext); - expression = HelperFunctionBinder.Bind(expression, compilationContext); - expression = BoolishConverter.Convert(expression, compilationContext); - expression = PathBinder.Bind(expression, compilationContext); - expression = SubExpressionVisitor.Visit(expression, compilationContext); - expression = HashParameterBinder.Bind(expression, compilationContext); - expression = ContextBinder.Bind(expression, compilationContext, parentContext, templatePath); - return expression; + + var context = new CompilationContext(configuration); + var expression = (Expression) Expression.Block(expressions); + expression = Reduce(expression, context); + + var lambda = ContextBinder.Bind(context, expression, templatePath); + return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(lambda); } catch (Exception ex) { throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); } } - - public Action Compile(IEnumerable expressions, string templatePath = null) + + public static Expression> CompileCore(IEnumerable expressions, Expression parentContext, InternalHandlebarsConfiguration configuration, string templatePath = null) { try { + if (!expressions.Any()) + { + return EmptyLambda; + } + if (expressions.IsOneOf()) + { + return EmptyLambda; + } + + var context = new CompilationContext(configuration); + var expression = (Expression) Expression.Block(expressions); + expression = Reduce(expression, context); - var expression = Compile(expressions, null, templatePath); - return ((Expression>)expression).Compile(); + return ContextBinder.Bind(context, expression, parentContext, templatePath); } catch (Exception ex) { @@ -69,10 +84,18 @@ public Expression Compile(IEnumerable expressions, Expression parent } } - - private Expression CreateExpressionBlock(IEnumerable expressions) + public static Action Compile(IEnumerable expressions, InternalHandlebarsConfiguration configuration, string templatePath = null) { - return Expression.Block(expressions); + try + { + var expression = CompileCore(expressions, null, configuration, templatePath); + + return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(expression); + } + catch (Exception ex) + { + throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); + } } } } diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index f6debb81..5123a789 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -4,64 +4,77 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Text; using HandlebarsDotNet.Compiler.Lexer; namespace HandlebarsDotNet.Compiler { internal class HandlebarsCompiler { - private Tokenizer _tokenizer; - private FunctionBuilder _functionBuilder; - private ExpressionBuilder _expressionBuilder; - private HandlebarsConfiguration _configuration; + private readonly HandlebarsConfiguration _configuration; public HandlebarsCompiler(HandlebarsConfiguration configuration) { _configuration = configuration; - _tokenizer = new Tokenizer(configuration); - _expressionBuilder = new ExpressionBuilder(configuration); - _functionBuilder = new FunctionBuilder(configuration); } - public Action Compile(TextReader source) + public Action Compile(ExtendedStringReader source) { - var tokens = _tokenizer.Tokenize(source).ToList(); - var expressions = _expressionBuilder.ConvertTokensToExpressions(tokens); - return _functionBuilder.Compile(expressions); + var configuration = new InternalHandlebarsConfiguration(_configuration); + var createdFeatures = configuration.Features; + for (var index = 0; index < createdFeatures.Count; index++) + { + createdFeatures[index].OnCompiling(configuration); + } + + var expressionBuilder = new ExpressionBuilder(configuration); + var tokens = Tokenizer.Tokenize(source).ToList(); + var expressions = expressionBuilder.ConvertTokensToExpressions(tokens); + var action = FunctionBuilder.Compile(expressions, configuration); + + for (var index = 0; index < createdFeatures.Count; index++) + { + createdFeatures[index].CompilationCompleted(); + } + + return action; } - internal Action CompileView(string templatePath) + internal Action CompileView(ViewReaderFactory readerFactoryFactory, string templatePath, InternalHandlebarsConfiguration configuration) { - var fs = _configuration.FileSystem; - if (fs == null) throw new InvalidOperationException("Cannot compile view when configuration.FileSystem is not set"); - var template = fs.GetFileContent(templatePath); - if (template == null) throw new InvalidOperationException("Cannot find template at '" + templatePath + "'"); - IEnumerable tokens = null; - using (var sr = new StringReader(template)) + IEnumerable tokens; + using (var sr = readerFactoryFactory(configuration, templatePath)) { - tokens = _tokenizer.Tokenize(sr).ToList(); + using (var reader = new ExtendedStringReader(sr)) + { + tokens = Tokenizer.Tokenize(reader).ToList(); + } } + var layoutToken = tokens.OfType().SingleOrDefault(); - var expressions = _expressionBuilder.ConvertTokensToExpressions(tokens); - var compiledView = _functionBuilder.Compile(expressions, templatePath); + var expressionBuilder = new ExpressionBuilder(configuration); + var expressions = expressionBuilder.ConvertTokensToExpressions(tokens); + var compiledView = FunctionBuilder.Compile(expressions, configuration, templatePath); 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 + "'"); + if (layoutPath == null) + throw new InvalidOperationException("Cannot find layout '" + layoutPath + "' for template '" + + templatePath + "'"); - var compiledLayout = CompileView(layoutPath); + var compiledLayout = CompileView(readerFactoryFactory, layoutPath, configuration); return (tw, vm) => { - var sb = new StringBuilder(); - using (var innerWriter = new StringWriter(sb)) + string inner; + using (var innerWriter = new PolledStringWriter(configuration.FormatProvider)) { compiledView(innerWriter, vm); + inner = innerWriter.ToString(); } - var inner = sb.ToString(); - compiledLayout(tw, new DynamicViewModel(new object[] { new { body = inner }, vm })); + + compiledLayout(tw, new DynamicViewModel(new[] {new {body = inner}, vm})); }; } diff --git a/source/Handlebars/Compiler/HandlebarsCompilerException.cs b/source/Handlebars/Compiler/HandlebarsCompilerException.cs index ce3f3c54..ec0c5f71 100644 --- a/source/Handlebars/Compiler/HandlebarsCompilerException.cs +++ b/source/Handlebars/Compiler/HandlebarsCompilerException.cs @@ -2,15 +2,32 @@ namespace HandlebarsDotNet { + /// + /// Represents exceptions occured at compile time + /// public class HandlebarsCompilerException : HandlebarsException { + /// + /// + /// + /// public HandlebarsCompilerException(string message) - : base(message) + : this(message, null, null) { } - + + internal HandlebarsCompilerException(string message, IReaderContext context = null) + : this(message, null, context) + { + } + public HandlebarsCompilerException(string message, Exception innerException) - : base(message, innerException) + : base(message, innerException, null) + { + } + + internal HandlebarsCompilerException(string message, Exception innerException, IReaderContext context = null) + : base(message, innerException, context) { } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 114c4586..6820cc52 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -17,18 +17,15 @@ public static BlockAccumulatorContext Create(Expression item, HandlebarsConfigur { context = new PartialBlockAccumulatorContext(item); } - else if (IsBlockHelper(item, configuration)) - { - context = new BlockHelperAccumulatorContext(item); - } else if (IsIteratorBlock(item)) { context = new IteratorBlockAccumulatorContext(item); } - else if (IsDeferredBlock(item)) + else if (IsBlockHelper(item, configuration)) { - context = new DeferredBlockAccumulatorContext(item); + context = new BlockHelperAccumulatorContext(item); } + return context; } @@ -62,8 +59,8 @@ private static bool IsBlockHelper(Expression item, HandlebarsConfiguration confi if (item is HelperExpression hitem) { var helperName = hitem.HelperName; - return !configuration.Helpers.ContainsKey(helperName) && - configuration.BlockHelpers.ContainsKey(helperName.Replace("#", "")); + return hitem.IsBlock || !(configuration.Helpers.ContainsKey(helperName) || configuration.ReturnHelpers.ContainsKey(helperName)) && + configuration.BlockHelpers.ContainsKey(helperName.Replace("#", "").Replace("^", "")); } return false; } @@ -71,42 +68,33 @@ private static bool IsBlockHelper(Expression item, HandlebarsConfiguration confi private static bool IsIteratorBlock(Expression item) { item = UnwrapStatement(item); - return (item is HelperExpression) && new[] { "#each" }.Contains(((HelperExpression)item).HelperName); - } - - private static bool IsDeferredBlock(Expression item) - { - item = UnwrapStatement(item); - return (item is PathExpression) && (((PathExpression)item).Path.StartsWith("#") || ((PathExpression)item).Path.StartsWith("^")); + return item is HelperExpression expression && "#each".Equals(expression.HelperName, StringComparison.OrdinalIgnoreCase); } private static bool IsPartialBlock (Expression item) { item = UnwrapStatement (item); - if (item is PathExpression) + switch (item) { - return ((PathExpression)item).Path.StartsWith("#>"); - } - else if (item is HelperExpression) - { - return ((HelperExpression)item).HelperName.StartsWith("#>"); - } - else - { - return false; + case PathExpression expression: + return expression.Path.StartsWith("#>"); + + case HelperExpression helperExpression: + return helperExpression.HelperName.StartsWith("#>"); + + default: + return false; } } protected static Expression UnwrapStatement(Expression item) { - if (item is StatementExpression) - { - return ((StatementExpression)item).Body; - } - else + if (item is StatementExpression expression) { - return item; + return expression.Body; } + + return item; } protected BlockAccumulatorContext(Expression startingNode) diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs index 4c16b38a..bc072b1d 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Linq.Expressions; using System.Collections.Generic; @@ -37,7 +36,7 @@ public override void HandleElement(Expression item) } else { - _body.Add((Expression)item); + _body.Add(item); } } @@ -55,8 +54,11 @@ public override bool IsClosingElement(Expression item) private bool IsClosingNode(Expression item) { - var helperName = _startingNode.HelperName.Replace("#", "").Replace("*", ""); - return item is PathExpression && ((PathExpression)item).Path == "/" + helperName; + var helperName = _startingNode.HelperName + .Replace("#", string.Empty) + .Replace("^", string.Empty) + .Replace("*", string.Empty); + return item is PathExpression expression && expression.Path == "/" + helperName; } public override Expression GetAccumulatedBlock() @@ -73,7 +75,8 @@ public override Expression GetAccumulatedBlock() var resultExpr = HandlebarsExpression.BlockHelper( _startingNode.HelperName, - _startingNode.Arguments, + _startingNode.Arguments.Where(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), _accumulatedBody, _accumulatedInversion, _startingNode.IsRaw); diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs index d16207c4..43446506 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq.Expressions; using System.Collections.Generic; using System.Linq; @@ -7,9 +7,12 @@ namespace HandlebarsDotNet.Compiler { internal class ConditionalBlockAccumulatorContext : BlockAccumulatorContext { + private static readonly HashSet ValidHelperNames = new HashSet { "if", "unless" }; + private readonly List _conditionalBlock = new List(); private Expression _currentCondition; private List _bodyBuffer = new List(); + public string BlockName { get; } public ConditionalBlockAccumulatorContext(Expression startingNode) @@ -17,7 +20,7 @@ public ConditionalBlockAccumulatorContext(Expression startingNode) { startingNode = UnwrapStatement(startingNode); BlockName = ((HelperExpression)startingNode).HelperName.Replace("#", ""); - if (new [] { "if", "unless" }.Contains(BlockName) == false) + if (!ValidHelperNames.Contains(BlockName)) { throw new HandlebarsCompilerException(string.Format( "Tried to convert {0} expression to conditional block", BlockName)); @@ -106,32 +109,17 @@ private Expression GetElseIfTestExpression(Expression item) private bool IsClosingNode(Expression item) { item = UnwrapStatement(item); - return item is PathExpression && ((PathExpression)item).Path == "/" + BlockName; - } - - private static IEnumerable UnwrapBlockExpression(IEnumerable body) - { - if (body.IsOneOf()) - { - body = body.OfType().First().Expressions; - } - return body; + return item is PathExpression expression && expression.Path == "/" + BlockName; } private static Expression SinglifyExpressions(IEnumerable expressions) { - if (expressions.Count() > 1) + if (expressions.IsMultiple()) { return Expression.Block(expressions); } - else if(expressions.Count() == 0) - { - return Expression.Empty(); - } - else - { - return expressions.Single(); - } + + return expressions.SingleOrDefault() ?? Expression.Empty(); } } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/DeferredBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/DeferredBlockAccumulatorContext.cs deleted file mode 100644 index b2e68bf1..00000000 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/DeferredBlockAccumulatorContext.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Linq; -using System.Linq.Expressions; -using System.Collections.Generic; - -namespace HandlebarsDotNet.Compiler -{ - internal class DeferredBlockAccumulatorContext : BlockAccumulatorContext - { - private readonly PathExpression _startingNode; - private List _body = new List(); - private BlockExpression _accumulatedBody; - private BlockExpression _accumulatedInversion; - - - public DeferredBlockAccumulatorContext(Expression startingNode) - : base(startingNode) - { - startingNode = UnwrapStatement(startingNode); - _startingNode = (PathExpression)startingNode; - } - - public override Expression GetAccumulatedBlock() - { - if (_accumulatedBody == null) - { - _accumulatedBody = Expression.Block(_body); - _accumulatedInversion = Expression.Block(Expression.Empty()); - } - else if (_accumulatedInversion == null && _body.Any()) - { - _accumulatedInversion = Expression.Block(_body); - } - else - { - _accumulatedInversion = Expression.Block(Expression.Empty()); - } - - return HandlebarsExpression.DeferredSection( - _startingNode, - _accumulatedBody, - _accumulatedInversion); - } - - public override void HandleElement(Expression item) - { - if (IsInversionBlock(item)) - { - _accumulatedBody = Expression.Block(_body); - _body = new List(); - } - else - { - _body.Add(item); - } - } - - public override bool IsClosingElement(Expression item) - { - item = UnwrapStatement(item); - var blockName = _startingNode.Path.Replace("#", "").Replace("^", ""); - return item is PathExpression && ((PathExpression)item).Path == "/" + blockName; - } - - private bool IsInversionBlock(Expression item) - { - item = UnwrapStatement(item); - return item is HelperExpression && ((HelperExpression)item).HelperName == "else"; - } - } -} - diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs index b88f8ff8..f615516a 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Linq.Expressions; using System.Collections.Generic; @@ -25,7 +24,8 @@ public override void HandleElement(Expression item) if (IsElseBlock(item)) { _accumulatedExpression = HandlebarsExpression.Iterator( - _startingNode.Arguments.Single(), + _startingNode.Arguments.Single(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), Expression.Block(_body)); _body = new List(); } @@ -44,13 +44,15 @@ public override bool IsClosingElement(Expression item) if (_accumulatedExpression == null) { _accumulatedExpression = HandlebarsExpression.Iterator( - _startingNode.Arguments.Single(), + _startingNode.Arguments.Single(o => o.NodeType != (ExpressionType)HandlebarsExpressionType.BlockParamsExpression), + _startingNode.Arguments.OfType().SingleOrDefault() ?? BlockParamsExpression.Empty(), Expression.Block(bodyStatements)); } else { _accumulatedExpression = HandlebarsExpression.Iterator( ((IteratorExpression)_accumulatedExpression).Sequence, + ((IteratorExpression)_accumulatedExpression).BlockParams, ((IteratorExpression)_accumulatedExpression).Template, Expression.Block(bodyStatements)); } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs new file mode 100644 index 00000000..3ef76923 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Compiler.Lexer; + +namespace HandlebarsDotNet.Compiler +{ + internal class BlockParamsConverter : TokenConverter + { + public static IEnumerable Convert(IEnumerable sequence) + { + return new BlockParamsConverter().ConvertTokens(sequence); + } + + private BlockParamsConverter() + { + } + + public override IEnumerable ConvertTokens(IEnumerable sequence) + { + var result = new List(); + bool foundBlockParams = false; + foreach (var item in sequence) + { + if (item is BlockParameterToken blockParameterToken) + { + if(foundBlockParams) throw new HandlebarsCompilerException("multiple blockParams expressions are not supported"); + + foundBlockParams = true; + if(!(result.Last() is PathExpression pathExpression)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + if(!string.Equals("as", pathExpression.Path, StringComparison.OrdinalIgnoreCase)) throw new HandlebarsCompilerException("blockParams definition has incorrect syntax"); + + result[result.Count - 1] = HandlebarsExpression.BlockParams(pathExpression.Path, blockParameterToken.Value); + } + else + { + result.Add(item); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Converter/CommentAndLayoutConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/CommentAndLayoutConverter.cs index 0d55a0b5..589fe13b 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/CommentAndLayoutConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/CommentAndLayoutConverter.cs @@ -22,19 +22,17 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) private static object Convert(object item) { - var commentToken = item as CommentToken; - if (commentToken != null) + if (item is CommentToken commentToken) { return HandlebarsExpression.Comment(commentToken.Value); } - else if (item is LayoutToken) + + if (item is LayoutToken) { return HandlebarsExpression.Comment(null); } - else - { - return item; - } + + return item; } } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs index 3a651a08..8cdcb8ae 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using HandlebarsDotNet.Compiler.Lexer; @@ -34,11 +34,10 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) var possibleBody = GetNext(enumerator); if (!(possibleBody is Expression)) { - throw new HandlebarsCompilerException(String.Format("Token '{0}' could not be converted to an expression", possibleBody)); + throw new HandlebarsCompilerException($"Token '{possibleBody}' could not be converted to an expression"); } - var endExpression = GetNext(enumerator) as EndExpressionToken; - if (endExpression == null) + if (!(GetNext(enumerator) is EndExpressionToken endExpression)) { throw new HandlebarsCompilerException("Handlebars statement was not reduced to a single expression"); } diff --git a/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs index c447425d..25c31ce7 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq; -using System.Linq.Expressions; - +using System.Linq.Expressions; + namespace HandlebarsDotNet.Compiler { internal class HashParameterConverter : TokenConverter @@ -24,19 +24,19 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (item is WordExpressionToken word) { item = GetNext(enumerator); - if (item is AssignmentToken) - { - yield return HandlebarsExpression.HashParameterAssignmentExpression(word.Value); - item = GetNext(enumerator); - } - else - { - yield return word; - } + if (item is AssignmentToken) + { + yield return HandlebarsExpression.HashParameterAssignmentExpression(word.Value); + item = GetNext(enumerator); + } + else + { + yield return word; + } } yield return item; - } + } } private static object GetNext(IEnumerator enumerator) diff --git a/source/Handlebars/Compiler/Lexer/Converter/HashParametersAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/HashParametersAccumulator.cs index add4a6bc..491f870f 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HashParametersAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HashParametersAccumulator.cs @@ -1,26 +1,25 @@ -using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Lexer; -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; -namespace HandlebarsDotNet.Compiler -{ - internal class HashParametersAccumulator : TokenConverter - { - public static IEnumerable Accumulate(IEnumerable sequence) - { - return new HashParametersAccumulator().ConvertTokens(sequence).ToList(); - } - - private HashParametersAccumulator() { } - - public override IEnumerable ConvertTokens(IEnumerable sequence) - { - var enumerator = sequence.GetEnumerator(); - while (enumerator.MoveNext()) - { - var item = enumerator.Current; - +namespace HandlebarsDotNet.Compiler +{ + internal class HashParametersAccumulator : TokenConverter + { + public static IEnumerable Accumulate(IEnumerable sequence) + { + return new HashParametersAccumulator().ConvertTokens(sequence).ToList(); + } + + private HashParametersAccumulator() { } + + public override IEnumerable ConvertTokens(IEnumerable sequence) + { + var enumerator = sequence.GetEnumerator(); + while (enumerator.MoveNext()) + { + var item = enumerator.Current; + if (item is HashParameterAssignmentExpression parameterAssignment) { bool moveNext; @@ -37,12 +36,12 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) } item = enumerator.Current; - } - - yield return item is Expression expression ? Visit(expression) : item; + } + + yield return item is Expression expression ? Visit(expression) : item; } - } - + } + Dictionary AccumulateParameters(IEnumerator enumerator, out bool moveNext) { moveNext = true; @@ -70,8 +69,8 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) } return parameters; - } - + } + Expression Visit(Expression expression) { if (expression is HelperExpression helperExpression) @@ -80,30 +79,32 @@ Expression Visit(Expression expression) var arguments = ConvertTokens(originalArguments) .Cast() .ToArray(); + if (!arguments.SequenceEqual(originalArguments)) { return HandlebarsExpression.Helper( helperExpression.HelperName, + helperExpression.IsBlock, arguments, helperExpression.IsRaw); } } if (expression is SubExpressionExpression subExpression) { - Expression childExpression = Visit(subExpression.Expression); + var childExpression = Visit(subExpression.Expression); if (childExpression != subExpression.Expression) { return HandlebarsExpression.SubExpression(childExpression); } } return expression; - } - - private static object GetNext(IEnumerator enumerator) - { - enumerator.MoveNext(); - return enumerator.Current; - } - } -} - + } + + private static object GetNext(IEnumerator enumerator) + { + enumerator.MoveNext(); + return enumerator.Current; + } + } +} + diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs index 18fee4bf..cfc3d8e7 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Collections.Generic; using System.Linq; using HandlebarsDotNet.Compiler.Lexer; @@ -23,37 +22,43 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (enumerator.MoveNext()) { var item = enumerator.Current; - if (item is HelperExpression) + switch (item) { - var helper = item as HelperExpression; - var helperArguments = AccumulateArguments(enumerator); - yield return HandlebarsExpression.Helper( - helper.HelperName, - helperArguments, - helper.IsRaw); - yield return enumerator.Current; - } - else if (item is PathExpression) - { - var helperArguments = AccumulateArguments(enumerator); - if (helperArguments.Count > 0) + case HelperExpression helper: { - var path = item as PathExpression; + var helperArguments = AccumulateArguments(enumerator); yield return HandlebarsExpression.Helper( - path.Path, + helper.HelperName, + helper.IsBlock, helperArguments, - (enumerator.Current as EndExpressionToken).IsRaw); + helper.IsRaw); yield return enumerator.Current; + break; } - else + case PathExpression path: { - yield return item; - yield return enumerator.Current; + var helperArguments = AccumulateArguments(enumerator); + if (helperArguments.Count > 0) + { + yield return HandlebarsExpression.Helper( + path.Path, + false, + helperArguments, + ((EndExpressionToken) enumerator.Current)?.IsRaw ?? false); + yield return enumerator.Current; + } + else + { + yield return path; + yield return enumerator.Current; + } + + break; } - } - else - { - yield return item; + + default: + yield return item; + break; } } } @@ -61,12 +66,12 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) private static List AccumulateArguments(IEnumerator enumerator) { var item = GetNext(enumerator); - List helperArguments = new List(); - while ((item is EndExpressionToken) == false) + var helperArguments = new List(); + while (!(item is EndExpressionToken)) { - if ((item is Expression) == false) + if (!(item is Expression)) { - throw new HandlebarsCompilerException(string.Format("Token '{0}' could not be converted to an expression", item)); + throw new HandlebarsCompilerException($"Token '{item}' could not be converted to an expression"); } helperArguments.Add((Expression)item); item = GetNext(enumerator); diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs index 6bb21e9e..f2deaee3 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs @@ -8,18 +8,21 @@ namespace HandlebarsDotNet.Compiler { internal class HelperConverter : TokenConverter { - private static readonly string[] builtInHelpers = new [] { "else", "each" }; + private static readonly HashSet BuiltInHelpers = new HashSet + { + "else", "each" + }; public static IEnumerable Convert( IEnumerable sequence, - HandlebarsConfiguration configuration) + InternalHandlebarsConfiguration configuration) { return new HelperConverter(configuration).ConvertTokens(sequence).ToList(); } - private readonly HandlebarsConfiguration _configuration; + private readonly InternalHandlebarsConfiguration _configuration; - private HelperConverter(HandlebarsConfiguration configuration) + private HelperConverter(InternalHandlebarsConfiguration configuration) { _configuration = configuration; } @@ -30,53 +33,75 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (enumerator.MoveNext()) { var item = enumerator.Current; - if (item is StartExpressionToken) + if (!(item is StartExpressionToken token)) { - var isRaw = ((StartExpressionToken)item).IsRaw; yield return item; - item = GetNext(enumerator); - if (item is Expression) - { + continue; + } + + var isRaw = token.IsRaw; + yield return token; + item = GetNext(enumerator); + switch (item) + { + case Expression _: yield return item; continue; - } - if (item is WordExpressionToken word) + case WordExpressionToken word when IsRegisteredHelperName(word.Value): + yield return HandlebarsExpression.Helper(word.Value, false, isRaw, word.Context); + break; + case WordExpressionToken word when IsRegisteredBlockHelperName(word.Value, isRaw): { - if (IsRegisteredHelperName(word.Value)) - { - yield return HandlebarsExpression.Helper(word.Value); - } - else if (IsRegisteredBlockHelperName(word.Value, isRaw)) - { - yield return HandlebarsExpression.Helper(word.Value, isRaw); - } - else - { - yield return item; - } + yield return HandlebarsExpression.Helper(word.Value, true, isRaw, word.Context); + break; } - else + case WordExpressionToken word when IsUnregisteredBlockHelperName(word.Value, isRaw, sequence): { - yield return item; + var expression = HandlebarsExpression.Helper(word.Value, true, isRaw, word.Context); + expression.IsBlock = true; + yield return expression; + break; } - } - else - { - yield return item; + default: + yield return item; + break; } } } private bool IsRegisteredHelperName(string name) { - return _configuration.Helpers.ContainsKey(name) || builtInHelpers.Contains(name); + var pathInfo = _configuration.PathInfoStore.GetOrAdd(name); + if (!pathInfo.IsValidHelperLiteral && !_configuration.Compatibility.RelaxedHelperNaming) return false; + if (pathInfo.IsBlockHelper || pathInfo.IsInversion || pathInfo.IsBlockClose) return false; + name = pathInfo.TrimmedPath; + + return _configuration.Helpers.ContainsKey(name) || _configuration.ReturnHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); } private bool IsRegisteredBlockHelperName(string name, bool isRaw) { - if (!isRaw && name[0] != '#') return false; - name = name.Replace("#", ""); - return _configuration.BlockHelpers.ContainsKey(name) || builtInHelpers.Contains(name); + var pathInfo = _configuration.PathInfoStore.GetOrAdd(name); + if (!pathInfo.IsValidHelperLiteral && !_configuration.Compatibility.RelaxedHelperNaming) return false; + if (!isRaw && !(pathInfo.IsBlockHelper || pathInfo.IsInversion)) return false; + if (pathInfo.IsBlockClose) return false; + + name = pathInfo.TrimmedPath; + + return _configuration.BlockHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); + } + + private bool IsUnregisteredBlockHelperName(string name, bool isRaw, IEnumerable sequence) + { + var pathInfo = _configuration.PathInfoStore.GetOrAdd(name); + if (!pathInfo.IsValidHelperLiteral && !_configuration.Compatibility.RelaxedHelperNaming) return false; + + if (!isRaw && !(pathInfo.IsBlockHelper || pathInfo.IsInversion)) return false; + name = name.Substring(1); + + var expectedBlockName = $"/{name}"; + return sequence.OfType().Any(o => + string.Equals(o.Value, expectedBlockName, StringComparison.OrdinalIgnoreCase)); } private static object GetNext(IEnumerator enumerator) diff --git a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs index f9318f9f..93ba0c96 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq.Expressions; @@ -20,30 +20,27 @@ private LiteralConverter() public override IEnumerable ConvertTokens(IEnumerable sequence) { foreach (var item in sequence) - { - bool boolValue; - int intValue; + { + var result = item; - object result = item; - - if (item is LiteralExpressionToken literalExpression) + switch (item) { - result = Expression.Convert(Expression.Constant(literalExpression.Value), typeof(object)); - - if (!literalExpression.IsDelimitedLiteral) - { - if (int.TryParse(literalExpression.Value, out intValue)) - { - result = Expression.Convert(Expression.Constant(intValue), typeof(object)); - } + case LiteralExpressionToken literalExpression: + { + result = Expression.Convert(Expression.Constant(literalExpression.Value), typeof(object)); + if (!literalExpression.IsDelimitedLiteral && int.TryParse(literalExpression.Value, out var intValue)) + { + result = Expression.Convert(Expression.Constant(intValue), typeof(object)); + } + + break; } + + case WordExpressionToken wordExpression when bool.TryParse(wordExpression.Value, out var boolValue): + result = Expression.Convert(Expression.Constant(boolValue), typeof(object)); + break; } - else if (item is WordExpressionToken wordExpression - && bool.TryParse(wordExpression.Value, out boolValue)) - { - result = Expression.Convert(Expression.Constant(boolValue), typeof(object)); - } - + yield return result; } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs index 1d1691cc..686d60ef 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs @@ -22,52 +22,46 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (enumerator.MoveNext()) { var item = enumerator.Current; - if (item is PartialToken) + if (!(item is PartialToken)) { - var arguments = AccumulateArguments(enumerator); - if (arguments.Count == 0) - { - throw new HandlebarsCompilerException("A partial must have a name"); - } + yield return item; + continue; + } - var partialName = arguments[0]; + var arguments = AccumulateArguments(enumerator); + if (arguments.Count == 0) throw new HandlebarsCompilerException("A partial must have a name"); - if (partialName is PathExpression) - { - partialName = Expression.Constant(((PathExpression)partialName).Path); - } + var partialName = arguments[0]; - if (arguments.Count == 1) - { - yield return HandlebarsExpression.Partial(partialName); - } - else if (arguments.Count == 2) - { - yield return HandlebarsExpression.Partial(partialName, arguments [1]); - } - else - { - throw new HandlebarsCompilerException("A partial can only accept 0 or 1 arguments"); - } - yield return enumerator.Current; + if (partialName is PathExpression expression) + { + partialName = Expression.Constant(expression.Path); } - else + + switch (arguments.Count) { - yield return item; + case 1: + yield return HandlebarsExpression.Partial(partialName); + break; + case 2: + yield return HandlebarsExpression.Partial(partialName, arguments[1]); + break; + default: + throw new HandlebarsCompilerException("A partial can only accept 0 or 1 arguments"); } + + yield return enumerator.Current; } } private static List AccumulateArguments(IEnumerator enumerator) { var item = GetNext(enumerator); - List helperArguments = new List(); - while ((item is EndExpressionToken) == false) + var helperArguments = new List(); + while (!(item is EndExpressionToken)) { - if ((item is Expression) == false) - { - throw new HandlebarsCompilerException(string.Format("Token '{0}' could not be converted to an expression", item)); - } + if (!(item is Expression)) throw new HandlebarsCompilerException($"Token '{item}' could not be converted to an expression"); + helperArguments.Add((Expression)item); item = GetNext(enumerator); } diff --git a/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs index d3a6b919..8e263273 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq.Expressions; @@ -21,9 +21,9 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) { foreach (var item in sequence) { - if (item is WordExpressionToken) + if (item is WordExpressionToken wordExpressionToken) { - yield return HandlebarsExpression.Path(((WordExpressionToken)item).Value); + yield return HandlebarsExpression.Path(wordExpressionToken.Value); } else { diff --git a/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs index 937ced02..b5d98583 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs @@ -1,4 +1,4 @@ -using HandlebarsDotNet.Compiler.Lexer; +using HandlebarsDotNet.Compiler.Lexer; using System.Collections.Generic; using System.Linq; using System.Text; @@ -86,36 +86,39 @@ private IEnumerable CollectParameters(IEnumerator enumerator, st private IEnumerable CollectBody(IEnumerator enumerator, string rawHelperName) { - var buffer = new StringBuilder(); - object precedingItem = null; - - while (enumerator.MoveNext()) + using (var container = StringBuilderPool.Shared.Use()) { - var item = enumerator.Current; + var buffer = container.Value; + object precedingItem = null; - if (item is StartExpressionToken startExpressionToken) + while (enumerator.MoveNext()) { - item = GetNext(enumerator); - if (IsClosingTag(startExpressionToken, item, rawHelperName)) + var item = enumerator.Current; + + if (item is StartExpressionToken startExpressionToken) { - yield return Token.Static(buffer.ToString()); - yield return startExpressionToken; - yield return item; - yield break; + item = GetNext(enumerator); + if (IsClosingTag(startExpressionToken, item, rawHelperName)) + { + yield return Token.Static(buffer.ToString()); + yield return startExpressionToken; + yield return item; + yield break; + } + + buffer.Append(Stringify(startExpressionToken, precedingItem)); + buffer.Append(Stringify(item, startExpressionToken)); + } + else + { + buffer.Append(Stringify(item, precedingItem)); } - buffer.Append(Stringify(startExpressionToken, precedingItem)); - buffer.Append(Stringify(item, startExpressionToken)); - } - else - { - buffer.Append(Stringify(item, precedingItem)); + precedingItem = item; } - precedingItem = item; + throw new HandlebarsCompilerException($"Reached end of template before raw block helper expression '{rawHelperName}' was closed"); } - - throw new HandlebarsCompilerException($"Reached end of template before raw block helper expression '{rawHelperName}' was closed"); } private bool IsClosingTag(StartExpressionToken startExpressionToken, object item, string helperName) diff --git a/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs index ff990bf0..bb2ba302 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq; @@ -20,20 +20,15 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) { foreach (var item in sequence) { - if (item is StaticToken) + if (!(item is StaticToken staticToken)) { - if (((StaticToken)item).Value != string.Empty) - { - yield return HandlebarsExpression.Static(((StaticToken)item).Value); - } - else - { - continue; - } + yield return item; + continue; } - else + + if (staticToken.Value != string.Empty) { - yield return item; + yield return HandlebarsExpression.Static(staticToken.Value); } } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs index 30b4e02e..ef98c990 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -46,6 +46,7 @@ private static SubExpressionExpression BuildSubExpression(IEnumerator en return HandlebarsExpression.SubExpression( HandlebarsExpression.Helper( path.Path, + false, helperArguments)); } @@ -53,15 +54,15 @@ private static IEnumerable AccumulateSubExpression(IEnumerator helperArguments = new List(); - while ((item is EndSubExpressionToken) == false) + while (!(item is EndSubExpressionToken)) { if (item is StartSubExpressionToken) { item = BuildSubExpression(enumerator); } - else if ((item is Expression) == false) + else if (!(item is Expression)) { - throw new HandlebarsCompilerException(string.Format("Token '{0}' could not be converted to an expression", item)); + throw new HandlebarsCompilerException($"Token '{item}' could not be converted to an expression"); } helperArguments.Add((Expression)item); item = GetNext(enumerator); diff --git a/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs b/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs index 24a55282..d56a4112 100644 --- a/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs +++ b/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs @@ -2,15 +2,28 @@ namespace HandlebarsDotNet { + /// + /// Represents exceptions occured at template parsing stage + /// public class HandlebarsParserException : HandlebarsException { public HandlebarsParserException(string message) - : base(message) + : this(message, null, null) { } - + + internal HandlebarsParserException(string message, IReaderContext context = null) + : this(message, null, context) + { + } + public HandlebarsParserException(string message, Exception innerException) - : base(message, innerException) + : base(message, innerException, null) + { + } + + internal HandlebarsParserException(string message, Exception innerException, IReaderContext context = null) + : base(message, innerException, context) { } } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs new file mode 100644 index 00000000..dd238657 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -0,0 +1,38 @@ +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParamsParser : Parser + { + public override Token Parse(ExtendedStringReader reader) + { + var context = reader.GetContext(); + var buffer = AccumulateWord(reader); + return !string.IsNullOrEmpty(buffer) + ? Token.BlockParams(buffer, context) + : null; + } + + private static string AccumulateWord(ExtendedStringReader reader) + { + using(var container = StringBuilderPool.Shared.Use()) + { + var buffer = container.Value; + + if (reader.Peek() != '|') return null; + + reader.Read(); + + while (reader.Peek() != '|') + { + buffer.Append((char) reader.Read()); + } + + reader.Read(); + + var accumulateWord = buffer.ToString().Trim(); + if(string.IsNullOrEmpty(accumulateWord)) throw new HandlebarsParserException($"BlockParams expression is not valid", reader.GetContext()); + + return accumulateWord; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs index 5b9ca63a..d9bba3db 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs @@ -1,57 +1,61 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; +using System.Collections.Generic; namespace HandlebarsDotNet.Compiler.Lexer { internal class BlockWordParser : Parser { - private const string validBlockWordStartCharacters = "#^/"; + private static readonly HashSet ValidBlockWordStartCharacters = new HashSet + { + '#', '^', '/' + }; - public override Token Parse(TextReader reader) + public override Token Parse(ExtendedStringReader reader) { - WordExpressionToken token = null; - if (IsBlockWord(reader)) - { - var buffer = AccumulateBlockWord(reader); - token = Token.Word(buffer); - } + if (!IsBlockWord(reader)) return null; + + var context = reader.GetContext(); + var buffer = AccumulateBlockWord(reader); + var token = Token.Word(buffer, context); return token; } - private bool IsBlockWord(TextReader reader) + private static bool IsBlockWord(ExtendedStringReader reader) { var peek = (char)reader.Peek(); - return validBlockWordStartCharacters.Contains(peek.ToString()); + return ValidBlockWordStartCharacters.Contains(peek); } - private string AccumulateBlockWord(TextReader reader) + private static string AccumulateBlockWord(ExtendedStringReader reader) { - StringBuilder buffer = new StringBuilder(); - buffer.Append((char)reader.Read()); - while(char.IsWhiteSpace((char)reader.Peek())) + using(var container = StringBuilderPool.Shared.Use()) { - reader.Read(); - } - while(true) - { - var peek = (char)reader.Peek(); - if (peek == '}' || peek == '~' || char.IsWhiteSpace(peek)) - { - break; - } - var node = reader.Read(); - if (node == -1) + var buffer = container.Value; + buffer.Append((char)reader.Read()); + while(char.IsWhiteSpace((char)reader.Peek())) { - throw new InvalidOperationException("Reached end of template before the expression was closed."); + reader.Read(); } - else + + while(true) { - buffer.Append((char)node); + var peek = (char)reader.Peek(); + if (peek == '}' || peek == '~' || char.IsWhiteSpace(peek)) + { + break; + } + var node = reader.Read(); + if (node == -1) + { + throw new HandlebarsParserException("Reached end of template before the expression was closed.", reader.GetContext()); + } + else + { + buffer.Append((char)node); + } } + + return buffer.ToString(); } - return buffer.ToString(); } } } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs index b31e84eb..039182df 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs @@ -1,90 +1,88 @@ -using System; -using System.IO; -using System.Text; +using System.Text; namespace HandlebarsDotNet.Compiler.Lexer { internal class CommentParser : Parser { - public override Token Parse(TextReader reader) + public override Token Parse(ExtendedStringReader reader) { + if (!IsComment(reader)) return null; + Token token = null; - if (IsComment(reader)) + var buffer = AccumulateComment(reader).Trim(); + if (buffer.StartsWith("<")) //syntax for layout is {{') { - token = Token.Partial(); + token = Token.Partial(reader.GetContext()); } return token; } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs b/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs new file mode 100644 index 00000000..68d48f48 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal struct StringBuilderEnumerator : IEnumerator + { + private readonly StringBuilder _stringBuilder; + private int _index; + + public StringBuilderEnumerator(StringBuilder stringBuilder) : this() + { + _stringBuilder = stringBuilder; + _index = -1; + } + + public bool MoveNext() + { + if (++_index >= _stringBuilder.Length) return false; + + Current = _stringBuilder[_index]; + return true; + } + + 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/Compiler/Lexer/Parsers/WordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs index 42190901..f6187c07 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs @@ -1,78 +1,106 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; - -namespace HandlebarsDotNet.Compiler.Lexer -{ - internal class WordParser : Parser - { - private const string validWordStartCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]"; - - public override Token Parse(TextReader reader) - { - if (IsWord(reader)) - { - var buffer = AccumulateWord(reader); - - return Token.Word(buffer); - } - return null; - } - - private bool IsWord(TextReader reader) - { - var peek = (char)reader.Peek(); - return validWordStartCharacters.Contains(peek.ToString()); - } - - private string AccumulateWord(TextReader reader) - { - StringBuilder buffer = new StringBuilder(); - - var inString = false; - - while (true) - { - if (!inString) - { - var peek = (char)reader.Peek(); - - if (peek == '}' || peek == '~' || peek == ')' || peek == '=' || (char.IsWhiteSpace(peek) && CanBreakAtSpace(buffer.ToString()))) - { - break; - } - } - - var node = reader.Read(); - - if (node == -1) - { - throw new InvalidOperationException("Reached end of template before the expression was closed."); - } - - if (node == '\'' || node == '"') - { - inString = !inString; - } - - buffer.Append((char)node); - } - - if (buffer.ToString().Contains("[") && !buffer.ToString().Contains("]")) - { - throw new HandlebarsCompilerException("Expression is missing a closing ]."); - } - - return buffer.ToString().Trim(); - } - - private bool CanBreakAtSpace(string buffer) - { - var bufferQueryable = buffer.OfType(); - return (!buffer.Contains("[") || (bufferQueryable.Count(x => x == '[') == bufferQueryable.Count(x => x == ']'))); - } - - } -} - +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class WordParser : Parser + { + private const string ValidWordStartCharactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]"; + private static readonly HashSet ValidWordStartCharacters = new HashSet(); + + static WordParser() + { + for (var index = 0; index < ValidWordStartCharactersString.Length; index++) + { + ValidWordStartCharacters.Add(ValidWordStartCharactersString[index]); + } + } + + public override Token Parse(ExtendedStringReader reader) + { + var context = reader.GetContext(); + if (IsWord(reader)) + { + var buffer = AccumulateWord(reader); + + return Token.Word(buffer, context); + } + return null; + } + + private static bool IsWord(ExtendedStringReader reader) + { + var peek = reader.Peek(); + return ValidWordStartCharacters.Contains((char) peek); + } + + private static string AccumulateWord(ExtendedStringReader reader) + { + using(var container = StringBuilderPool.Shared.Use()) + { + var buffer = container.Value; + + var inString = false; + + while (true) + { + if (!inString) + { + var peek = (char) reader.Peek(); + + if (peek == '}' || peek == '~' || peek == ')' || peek == '=' || char.IsWhiteSpace(peek) && CanBreakAtSpace(buffer)) + { + break; + } + } + + var node = reader.Read(); + + if (node == -1) + { + throw new HandlebarsParserException("Reached end of template before the expression was closed.", reader.GetContext()); + } + + if (node == '\'' || node == '"') + { + inString = !inString; + } + + buffer.Append((char)node); + } + + return buffer.ToString().Trim(); + } + } + + private static bool CanBreakAtSpace(StringBuilder buffer) + { + CalculateBraces(buffer, out var left, out var right); + + return left == 0 || left == right; + } + + private static void CalculateBraces(StringBuilder buffer, out int left, out int right) + { + left = 0; + right = 0; + + var enumerator = new StringBuilderEnumerator(buffer); + while (enumerator.MoveNext()) + { + switch (enumerator.Current) + { + case ']': + right++; + break; + + case '[': + left++; + break; + } + } + } + } +} + diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index 7326dd8e..c0ac1539 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -1,187 +1,180 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace HandlebarsDotNet.Compiler.Lexer -{ - internal class Tokenizer - { - private readonly HandlebarsConfiguration _configuration; - - private static Parser _wordParser = new WordParser(); - private static Parser _literalParser = new LiteralParser(); - private static Parser _commentParser = new CommentParser(); - private static Parser _partialParser = new PartialParser(); - private static Parser _blockWordParser = new BlockWordParser(); - //TODO: structure parser - - public Tokenizer(HandlebarsConfiguration configuration) - { - _configuration = configuration; - } - - public IEnumerable Tokenize(TextReader source) - { - try - { - return Parse(source); - } - catch (Exception ex) - { - throw new HandlebarsParserException("An unhandled exception occurred while trying to compile the template", ex); - } - } - - private IEnumerable Parse(TextReader source) - { - bool inExpression = false; - bool trimWhitespace = false; - var buffer = new StringBuilder(); - var node = source.Read(); - while (true) - { - if (node == -1) - { - if (buffer.Length > 0) - { - if (inExpression) - { - throw new InvalidOperationException("Reached end of template before expression was closed"); - } - else - { - yield return Token.Static(buffer.ToString()); - } - } - break; - } - if (inExpression) - { - if ((char)node == '(') - { - yield return Token.StartSubExpression(); - } - - Token token = null; - token = token ?? _wordParser.Parse(source); - token = token ?? _literalParser.Parse(source); - token = token ?? _commentParser.Parse(source); - token = token ?? _partialParser.Parse(source); - token = token ?? _blockWordParser.Parse(source); - - if (token != null) - { +using System; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal static class Tokenizer + { + private static readonly Parser WordParser = new WordParser(); + private static readonly Parser LiteralParser = new LiteralParser(); + private static readonly Parser CommentParser = new CommentParser(); + private static readonly Parser PartialParser = new PartialParser(); + private static readonly Parser BlockWordParser = new BlockWordParser(); + private static readonly Parser BlockParamsParser = new BlockParamsParser(); + //TODO: structure parser + + public static IEnumerable Tokenize(ExtendedStringReader source) + { + try + { + return Parse(source); + } + catch (Exception ex) + { + throw new HandlebarsParserException("An unhandled exception occurred while trying to compile the template", ex); + } + } + + private static IEnumerable Parse(ExtendedStringReader source) + { + bool inExpression = false; + bool trimWhitespace = false; + using var container = StringBuilderPool.Shared.Use(); + var buffer = container.Value; + + var node = source.Read(); + while (true) + { + if (node == -1) + { + if (buffer.Length > 0) + { + if (inExpression) + { + throw new InvalidOperationException("Reached end of template before expression was closed"); + } + else + { + yield return Token.Static(buffer.ToString(), source.GetContext()); + } + } + break; + } + if (inExpression) + { + if ((char)node == '(') + { + yield return Token.StartSubExpression(); + } + + var token = WordParser.Parse(source); + token ??= LiteralParser.Parse(source); + token ??= CommentParser.Parse(source); + token ??= PartialParser.Parse(source); + token ??= BlockWordParser.Parse(source); + token ??= BlockParamsParser.Parse(source); + + if (token != null) + { yield return token; if ((char)source.Peek() == '=') { source.Read(); - yield return Token.Assignment(); + yield return Token.Assignment(source.GetContext()); continue; } } - if ((char)node == '}' && (char)source.Read() == '}') - { - bool escaped = true; - bool raw = false; - if ((char)source.Peek() == '}') - { - node = source.Read(); - escaped = false; + if ((char)node == '}' && (char)source.Read() == '}') + { + bool escaped = true; + bool raw = false; + if ((char)source.Peek() == '}') + { + source.Read(); + escaped = false; } if ((char)source.Peek() == '}') + { + source.Read(); + raw = true; + } + node = source.Read(); + yield return Token.EndExpression(escaped, trimWhitespace, raw, source.GetContext()); + inExpression = false; + } + else if ((char)node == ')') + { + node = source.Read(); + yield return Token.EndSubExpression(source.GetContext()); + } + else if (char.IsWhiteSpace((char)node) || char.IsWhiteSpace((char)source.Peek())) + { + node = source.Read(); + } + else if ((char)node == '~') + { + node = source.Read(); + trimWhitespace = true; + } + else + { + if (token == null) + { + + throw new HandlebarsParserException("Reached unparseable token in expression: " + source.ReadLine(), source.GetContext()); + } + node = source.Read(); + } + } + else + { + if ((char)node == '\\' && (char)source.Peek() == '\\') + { + source.Read(); + buffer.Append('\\'); + node = source.Read(); + } + else if ((char)node == '\\' && (char)source.Peek() == '{') + { + source.Read(); + if ((char)source.Peek() == '{') + { + source.Read(); + buffer.Append('{', 2); + } + else + { + buffer.Append("\\{"); + } + node = source.Read(); + } + else if ((char)node == '{' && (char)source.Peek() == '{') + { + bool escaped = true; + bool raw = false; + trimWhitespace = false; + node = source.Read(); + if ((char)source.Peek() == '{') + { + node = source.Read(); + escaped = false; + } + if ((char)source.Peek() == '{') { node = source.Read(); raw = true; - } - node = source.Read(); - yield return Token.EndExpression(escaped, trimWhitespace, raw); - inExpression = false; - } - else if ((char)node == ')') - { - node = source.Read(); - yield return Token.EndSubExpression(); - } - else if (char.IsWhiteSpace((char)node) || char.IsWhiteSpace((char)source.Peek())) - { - node = source.Read(); - } - else if ((char)node == '~') - { - node = source.Read(); - trimWhitespace = true; - } - else - { - if (token == null) - { - - throw new HandlebarsParserException("Reached unparseable token in expression: " + source.ReadLine()); - } - node = source.Read(); - } - } - else - { - if ((char)node == '\\' && (char)source.Peek() == '\\') - { - source.Read(); - buffer.Append('\\'); - node = source.Read(); - } - else if ((char)node == '\\' && (char)source.Peek() == '{') - { - source.Read(); - if ((char)source.Peek() == '{') - { - source.Read(); - buffer.Append('{', 2); - } - else - { - buffer.Append("\\{"); - } - node = source.Read(); - } - else if ((char)node == '{' && (char)source.Peek() == '{') - { - bool escaped = true; - bool raw = false; - trimWhitespace = false; - node = source.Read(); - if ((char)source.Peek() == '{') - { - node = source.Read(); - escaped = false; - } - if ((char)source.Peek() == '{') - { - node = source.Read(); - raw = true; - } - if ((char)source.Peek() == '~') - { - source.Read(); - node = source.Peek(); - trimWhitespace = true; - } - yield return Token.Static(buffer.ToString()); - yield return Token.StartExpression(escaped, trimWhitespace, raw); - trimWhitespace = false; - buffer = new StringBuilder(); - inExpression = true; - } - else - { - buffer.Append((char)node); - node = source.Read(); - } - } - } - } - } -} - + } + if ((char)source.Peek() == '~') + { + source.Read(); + node = source.Peek(); + trimWhitespace = true; + } + yield return Token.Static(buffer.ToString(), source.GetContext()); + yield return Token.StartExpression(escaped, trimWhitespace, raw, source.GetContext()); + trimWhitespace = false; + buffer.Clear(); + inExpression = true; + } + else + { + buffer.Append((char)node); + node = source.Read(); + } + } + } + } + } +} + diff --git a/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs index d7255d1e..5328ef2f 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs @@ -1,18 +1,17 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer -{ - internal class AssignmentToken : Token - { - public override TokenType Type - { - get { return TokenType.Assignment; } - } - - public override string Value +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class AssignmentToken : Token + { + public AssignmentToken(IReaderContext context) { - get { return "="; } - } - } -} - + Context = context; + } + + public IReaderContext Context { get; } + + public override TokenType Type => TokenType.Assignment; + + public override string Value => "="; + } +} + diff --git a/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs new file mode 100644 index 00000000..05bd0a52 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs @@ -0,0 +1,16 @@ +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParameterToken : Token + { + public BlockParameterToken(string value, IReaderContext context = null) + { + Value = value; + Context = context; + } + + public override TokenType Type => TokenType.BlockParams; + + public override string Value { get; } + public IReaderContext Context { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokens/EndExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/EndExpressionToken.cs index 61a1e7f5..3d8524a8 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/EndExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/EndExpressionToken.cs @@ -1,49 +1,25 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class EndExpressionToken : ExpressionScopeToken { - private readonly bool _isEscaped; - private readonly bool _trimWhitespace; - private readonly bool _isRaw; - - public EndExpressionToken(bool isEscaped, bool trimWhitespace, bool isRaw) + public EndExpressionToken(bool isEscaped, bool trimWhitespace, bool isRaw, IReaderContext context) { - _isEscaped = isEscaped; - _trimWhitespace = trimWhitespace; - _isRaw = isRaw; + IsEscaped = isEscaped; + TrimTrailingWhitespace = trimWhitespace; + IsRaw = isRaw; + Context = context; } - public bool IsEscaped - { - get { return _isEscaped; } - } + public bool IsEscaped { get; } - public bool TrimTrailingWhitespace - { - get { return _trimWhitespace; } - } + public bool TrimTrailingWhitespace { get; } - public bool IsRaw - { - get { return _isRaw; } - } + public bool IsRaw { get; } + public IReaderContext Context { get; } - public override string Value - { - get { return IsRaw ? "}}}}" : IsEscaped ? "}}" : "}}}"; } - } + public override string Value => IsRaw ? "}}}}" : IsEscaped ? "}}" : "}}}"; - public override TokenType Type - { - get { return TokenType.EndExpression; } - } - - public override string ToString() - { - return this.Value; - } + public override TokenType Type => TokenType.EndExpression; } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/EndSubExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/EndSubExpressionToken.cs index 6e7d665b..dbbf520a 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/EndSubExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/EndSubExpressionToken.cs @@ -1,27 +1,17 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class EndSubExpressionToken : ExpressionScopeToken { - public EndSubExpressionToken() - { - } + public IReaderContext Context { get; } - public override string Value + public EndSubExpressionToken(IReaderContext context) { - get { return ")"; } + Context = context; } - public override TokenType Type - { - get { return TokenType.EndSubExpression; } - } + public override string Value { get; } = ")"; - public override string ToString() - { - return this.Value; - } + public override TokenType Type => TokenType.EndSubExpression; } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/LiteralExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/LiteralExpressionToken.cs index 2b9140f3..365849ca 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/LiteralExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/LiteralExpressionToken.cs @@ -1,37 +1,23 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class LiteralExpressionToken : ExpressionToken { - private readonly string _value; - private readonly string _delimiter; - - public LiteralExpressionToken(string value, string delimiter = null) + public LiteralExpressionToken(string value, string delimiter = null, IReaderContext context = null) { - _value = value; - _delimiter = delimiter; + Context = context; + Value = value; + Delimiter = delimiter; } - public bool IsDelimitedLiteral - { - get { return _delimiter != null; } - } + public IReaderContext Context { get; } + + public bool IsDelimitedLiteral => Delimiter != null; - public string Delimiter - { - get { return _delimiter; } - } + public string Delimiter { get; } - public override TokenType Type - { - get { return TokenType.Literal; } - } + public override TokenType Type => TokenType.Literal; - public override string Value - { - get { return _value; } - } + public override string Value { get; } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/PartialToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/PartialToken.cs index bf565403..1c7a1055 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/PartialToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/PartialToken.cs @@ -2,20 +2,16 @@ { internal class PartialToken : Token { - public override TokenType Type + public PartialToken(IReaderContext context = null) { - get { return TokenType.Partial; } + Context = context; } - public override string Value - { - get { return ">"; } - } + public IReaderContext Context { get; } + + public override TokenType Type => TokenType.Partial; - public override string ToString() - { - return Value; - } + public override string Value => ">"; } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/StartExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/StartExpressionToken.cs index 89051742..aaabe8ae 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/StartExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/StartExpressionToken.cs @@ -1,49 +1,26 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class StartExpressionToken : ExpressionScopeToken { - private readonly bool _isEscaped; - private readonly bool _trimWhitespace; - private readonly bool _isRaw; - - public StartExpressionToken(bool isEscaped, bool trimWhitespace, bool isRaw) + public StartExpressionToken(bool isEscaped, bool trimWhitespace, bool isRaw, IReaderContext context) { - _isEscaped = isEscaped; - _trimWhitespace = trimWhitespace; - _isRaw = isRaw; + Context = context; + IsEscaped = isEscaped; + TrimPreceedingWhitespace = trimWhitespace; + IsRaw = isRaw; } - public bool IsEscaped - { - get { return _isEscaped; } - } + public IReaderContext Context { get; } + + public bool IsEscaped { get; } - public bool TrimPreceedingWhitespace - { - get { return _trimWhitespace; } - } + public bool TrimPreceedingWhitespace { get; } - public bool IsRaw - { - get { return _isRaw; } - } + public bool IsRaw { get; } - public override string Value - { - get { return IsRaw ? "{{{{" : IsEscaped ? "{{" : "{{{"; } - } + public override string Value => IsRaw ? "{{{{" : IsEscaped ? "{{" : "{{{"; - public override TokenType Type - { - get { return TokenType.StartExpression; } - } - - public override string ToString() - { - return this.Value; - } + public override TokenType Type => TokenType.StartExpression; } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/StartSubExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/StartSubExpressionToken.cs index 0ed0bc17..610c4124 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/StartSubExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/StartSubExpressionToken.cs @@ -1,27 +1,10 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class StartSubExpressionToken : ExpressionScopeToken { - public StartSubExpressionToken() - { - } - - public override string Value - { - get { return "("; } - } - - public override TokenType Type - { - get { return TokenType.StartSubExpression; } - } + public override string Value { get; } = "("; - public override string ToString() - { - return this.Value; - } + public override TokenType Type => TokenType.StartSubExpression; } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/StaticToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/StaticToken.cs index 86751eed..820d37a7 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/StaticToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/StaticToken.cs @@ -1,41 +1,31 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class StaticToken : Token { - private readonly string _value; - private readonly string _original; + public IReaderContext Context { get; } - private StaticToken(string value, string original) + private StaticToken(string value, string original, IReaderContext context = null) { - _value = value; - _original = original; + Value = value; + Original = original; + Context = context; } - internal StaticToken(string value) + internal StaticToken(string value, IReaderContext context = null) : this(value, value) { + Context = context; } - public override TokenType Type - { - get { return TokenType.Static; } - } + public override TokenType Type => TokenType.Static; - public override string Value - { - get { return _value; } - } + public override string Value { get; } - public string Original - { - get { return _original; } - } + public string Original { get; } public StaticToken GetModifiedToken(string value) { - return new StaticToken(value, _original); + return new StaticToken(value, Original, Context); } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokens/Token.cs b/source/Handlebars/Compiler/Lexer/Tokens/Token.cs index 2acd5a3f..993e7252 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/Token.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/Token.cs @@ -1,5 +1,3 @@ -using System; - namespace HandlebarsDotNet.Compiler.Lexer { internal abstract class Token @@ -8,29 +6,34 @@ internal abstract class Token public abstract string Value { get; } - public static StaticToken Static(string value) + public sealed override string ToString() + { + return Value; + } + + public static StaticToken Static(string value, IReaderContext context = null) { - return new StaticToken(value); + return new StaticToken(value, context); } - public static LiteralExpressionToken Literal(string value, string delimiter = null) + public static LiteralExpressionToken Literal(string value, string delimiter = null, IReaderContext context = null) { - return new LiteralExpressionToken(value, delimiter); + return new LiteralExpressionToken(value, delimiter, context); } - public static WordExpressionToken Word(string word) + public static WordExpressionToken Word(string word, IReaderContext context = null) { - return new WordExpressionToken(word); + return new WordExpressionToken(word, context); } - public static StartExpressionToken StartExpression(bool isEscaped, bool trimWhitespace, bool isRaw) + public static StartExpressionToken StartExpression(bool isEscaped, bool trimWhitespace, bool isRaw, IReaderContext context = null) { - return new StartExpressionToken(isEscaped, trimWhitespace, isRaw); + return new StartExpressionToken(isEscaped, trimWhitespace, isRaw, context); } - public static EndExpressionToken EndExpression(bool isEscaped, bool trimWhitespace, bool isRaw) + public static EndExpressionToken EndExpression(bool isEscaped, bool trimWhitespace, bool isRaw, IReaderContext context = null) { - return new EndExpressionToken(isEscaped, trimWhitespace, isRaw); + return new EndExpressionToken(isEscaped, trimWhitespace, isRaw, context); } public static CommentToken Comment(string comment) @@ -38,9 +41,9 @@ public static CommentToken Comment(string comment) return new CommentToken(comment); } - public static PartialToken Partial() + public static PartialToken Partial(IReaderContext context = null) { - return new PartialToken(); + return new PartialToken(context); } public static LayoutToken Layout(string layout) @@ -53,14 +56,19 @@ public static StartSubExpressionToken StartSubExpression() return new StartSubExpressionToken(); } - public static EndSubExpressionToken EndSubExpression() + public static EndSubExpressionToken EndSubExpression(IReaderContext context) { - return new EndSubExpressionToken(); + return new EndSubExpressionToken(context); } - public static AssignmentToken Assignment() + public static AssignmentToken Assignment(IReaderContext context) + { + return new AssignmentToken(context); + } + + public static BlockParameterToken BlockParams(string blockParams, IReaderContext context) { - return new AssignmentToken(); + return new BlockParameterToken(blockParams, context); } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs index e325e1ba..34add212 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs @@ -1,5 +1,3 @@ -using System; - namespace HandlebarsDotNet.Compiler.Lexer { internal enum TokenType @@ -15,7 +13,8 @@ internal enum TokenType Layout = 8, StartSubExpression = 9, EndSubExpression = 10, - Assignment = 11 + Assignment = 11, + BlockParams = 12 } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/WordExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/WordExpressionToken.cs index 79703543..1c64ec41 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/WordExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/WordExpressionToken.cs @@ -1,25 +1,17 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class WordExpressionToken : ExpressionToken { - private readonly string _word; - - public WordExpressionToken(string word) + public WordExpressionToken(string word, IReaderContext context = null) { - _word = word; + Value = word; + Context = context; } - public override TokenType Type - { - get { return TokenType.Word; } - } + public override TokenType Type => TokenType.Word; - public override string Value - { - get { return _word; } - } + public override string Value { get; } + public IReaderContext Context { get; } } } diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 849e3032..f5ceb833 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -2,167 +2,230 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.Compiler { - internal class BindingContext + internal sealed class BindingContext : IDisposable { - private readonly object _value; - private readonly BindingContext _parent; + private static readonly BindingContextPool Pool = new BindingContextPool(); + + private readonly HashedCollection _valueProviders = new HashedCollection(); - public string TemplatePath { get; private set; } - - public EncodedTextWriter TextWriter { get; private set; } - - public IDictionary> InlinePartialTemplates { get; private set; } - - public Action PartialBlockTemplate { get; private set; } - - public bool SuppressEncoding + public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath, + IDictionary> inlinePartialTemplates) { - get { return TextWriter.SuppressEncoding; } - set { TextWriter.SuppressEncoding = value; } + return Pool.CreateContext(configuration, value, writer, parent, templatePath, null, inlinePartialTemplates); + } + + public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath, + Action partialBlockTemplate, + IDictionary> inlinePartialTemplates) + { + return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate, inlinePartialTemplates); } - public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : - this(value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } - - public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) : - this(value, writer, parent, templatePath, partialBlockTemplate, null, inlinePartialTemplates) { } - - public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) + private BindingContext() { - TemplatePath = parent != null ? (parent.TemplatePath ?? templatePath) : templatePath; - TextWriter = writer; - _value = value; - _parent = parent; + RegisterValueProvider(new BindingContextValueProvider(this)); + } + private void Initialize() + { + Root = ParentContext?.Root ?? this; + TemplatePath = (ParentContext != null ? ParentContext.TemplatePath : TemplatePath) ?? TemplatePath; + //Inline partials cannot use the Handlebars.RegisteredTemplate method //because it pollutes the static dictionary and creates collisions //where the same partial name might exist in multiple templates. //To avoid collisions, pass around a dictionary of compiled partials //in the context - if (parent != null) + if (ParentContext != null) { - InlinePartialTemplates = parent.InlinePartialTemplates; + InlinePartialTemplates = ParentContext.InlinePartialTemplates; - if (value is HashParameterDictionary dictionary) { + if (Value is HashParameterDictionary dictionary) { // Populate value with parent context - foreach (var item in GetContextDictionary(parent.Value)) { + foreach (var item in GetContextDictionary(ParentContext.Value)) { if (!dictionary.ContainsKey(item.Key)) dictionary[item.Key] = item.Value; } } } - else if (current != null) - { - InlinePartialTemplates = current.InlinePartialTemplates; - } - else if (inlinePartialTemplates != null) - { - InlinePartialTemplates = inlinePartialTemplates; - } else { InlinePartialTemplates = new Dictionary>(StringComparer.OrdinalIgnoreCase); } - - PartialBlockTemplate = partialBlockTemplate; } - public virtual object Value + public string TemplatePath { get; private set; } + + public InternalHandlebarsConfiguration Configuration { get; private set; } + + public EncodedTextWriter TextWriter { get; private set; } + + public IDictionary> InlinePartialTemplates { get; private set; } + + public Action PartialBlockTemplate { get; private set; } + + public bool SuppressEncoding { - get { return _value; } + get => TextWriter.SuppressEncoding; + set => TextWriter.SuppressEncoding = value; } - public virtual BindingContext ParentContext + public object Value { get; private set; } + + public BindingContext ParentContext { get; private set; } + + public object Root { get; private set; } + + public void RegisterValueProvider(IValueProvider valueProvider) + { + if(valueProvider == null) throw new ArgumentNullException(nameof(valueProvider)); + + _valueProviders.Add(valueProvider); + } + + public void UnregisterValueProvider(IValueProvider valueProvider) { - get { return _parent; } + _valueProviders.Remove(valueProvider); } - public virtual object Root + public bool TryGetContextVariable(ref ChainSegment segment, out object value) { - get + // accessing value providers in reverse order as it gives more probability of hit + for (var index = _valueProviders.Count - 1; index >= 0; index--) { - var currentContext = this; - while (currentContext.ParentContext != null) - { - currentContext = currentContext.ParentContext; - } - return currentContext.Value; + if (_valueProviders[index].TryGetValue(ref segment, out value)) return true; } - } - public virtual object GetContextVariable(string variableName) + value = null; + return false; + } + + public bool TryGetVariable(ref ChainSegment segment, out object value, bool searchContext = false) { - var target = this; + // accessing value providers in reverse order as it gives more probability of hit + for (var index = _valueProviders.Count - 1; index >= 0; index--) + { + var valueProvider = _valueProviders[index]; + if(!valueProvider.SupportedValueTypes.HasFlag(ValueTypes.All) && !searchContext) continue; + + if (valueProvider.TryGetValue(ref segment, out value)) return true; + } - return GetContextVariable(variableName, target) - ?? GetContextVariable(variableName, target.Value); + value = null; + return ParentContext?.TryGetVariable(ref segment, out value, searchContext) ?? false; } - private object GetContextVariable(string variableName, object target) + private static IDictionary GetContextDictionary(object target) { - object returnValue; - variableName = variableName.TrimStart('@'); - var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (member.Length > 0) + var contextDictionary = new Dictionary(); + + switch (target) { - if (member[0] is PropertyInfo) - { - returnValue = ((PropertyInfo)member[0]).GetValue(target, null); - } - else if (member[0] is FieldInfo) + case null: + return contextDictionary; + + case IDictionary dictionary: { - returnValue = ((FieldInfo)member[0]).GetValue(target); + foreach (var item in dictionary) + { + contextDictionary[item.Key] = item.Value; + } + + break; } - else + default: { - throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); + var type = target.GetType(); + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + contextDictionary[field.Name] = field.GetValue(target); + } + + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (var property in properties) + { + if (property.GetIndexParameters().Length == 0) + { + contextDictionary[property.Name] = property.GetValue(target); + } + } + + break; } } - else if (_parent != null) + + return contextDictionary; + } + + public BindingContext CreateChildContext(object value, Action partialBlockTemplate = null) + { + return Create(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); + } + + public void Dispose() + { + Pool.Return(this); + } + + private class BindingContextPool : DefaultObjectPool + { + public BindingContextPool() : base(new BindingContextPolicy()) { - returnValue = _parent.GetContextVariable(variableName); } - else + + public BindingContext CreateContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) { - returnValue = null; + var context = Get(); + context.Configuration = configuration; + context.Value = value; + context.TextWriter = writer; + context.ParentContext = parent; + context.TemplatePath = templatePath; + context.InlinePartialTemplates = inlinePartialTemplates; + context.PartialBlockTemplate = partialBlockTemplate; + + context.Initialize(); + + return context; } - return returnValue; - } - - private IDictionary GetContextDictionary(object target) - { - var dict = new Dictionary(); - - if (target == null) - return dict; - - if (target is IDictionary dictionary) { - foreach (var item in dictionary) - dict[item.Key] = item.Value; - } else { - var type = target.GetType(); - - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - foreach (var field in fields) { - dict[field.Name] = field.GetValue(target); + + private class BindingContextPolicy : IPooledObjectPolicy + { + public BindingContext Create() + { + return new BindingContext(); } - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - foreach (var property in properties) { - if (property.GetIndexParameters().Length == 0) - dict[property.Name] = property.GetValue(target); + public bool Return(BindingContext item) + { + item.Root = null; + item.Value = null; + item.ParentContext = null; + item.TemplatePath = null; + item.TextWriter = null; + item.InlinePartialTemplates = null; + item.PartialBlockTemplate = null; + + var valueProviders = item._valueProviders; + for (var index = valueProviders.Count - 1; index >= 1; index--) + { + valueProviders.Remove(valueProviders[index]); + } + + return true; } } - - return dict; - } - - public virtual BindingContext CreateChildContext(object value, Action partialBlockTemplate) - { - return new BindingContext(value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); } } } diff --git a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs index 5273bc5b..c50b31d4 100644 --- a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs @@ -1,40 +1,42 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Collections.Generic; namespace HandlebarsDotNet.Compiler { internal class BlockHelperExpression : HelperExpression { - private readonly Expression _body; - private readonly Expression _inversion; - public BlockHelperExpression( string helperName, IEnumerable arguments, Expression body, Expression inversion, bool isRaw = false) - : base(helperName, arguments, isRaw) + : this(helperName, arguments, BlockParamsExpression.Empty(), body, inversion, isRaw) { - _body = body; - _inversion = inversion; } - - public Expression Body + + public BlockHelperExpression( + string helperName, + IEnumerable arguments, + BlockParamsExpression blockParams, + Expression body, + Expression inversion, + bool isRaw = false) + : base(helperName, true, arguments, isRaw) { - get { return _body; } + Body = body; + Inversion = inversion; + BlockParams = blockParams; + IsBlock = true; } - public Expression Inversion - { - get { return _inversion; } - } + public Expression Body { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.BlockExpression; } - } + public Expression Inversion { get; } + + public new BlockParamsExpression BlockParams { get; } + + public override ExpressionType NodeType => (ExpressionType) HandlebarsExpressionType.BlockExpression; } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs new file mode 100644 index 00000000..a0a3f5a9 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.Compiler +{ + internal class BlockParamsExpression : HandlebarsExpression + { + public new static BlockParamsExpression Empty() => new BlockParamsExpression(null); + + private readonly BlockParam _blockParam; + + private BlockParamsExpression(BlockParam blockParam) + { + _blockParam = blockParam; + } + + public BlockParamsExpression(string action, string blockParams) + :this(new BlockParam + { + Action = action, + Parameters = blockParams.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries) + }) + { + } + + public override ExpressionType NodeType { get; } = (ExpressionType)HandlebarsExpressionType.BlockParamsExpression; + + public override Type Type { get; } = typeof(BlockParam); + + protected override Expression Accept(ExpressionVisitor visitor) + { + return visitor.Visit(Constant(_blockParam, typeof(BlockParam))); + } + } + + internal class BlockParam + { + public string Action { get; set; } + public string[] Parameters { get; set; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs new file mode 100644 index 00000000..fd2e7de4 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; +using Microsoft.Extensions.ObjectPool; + +namespace HandlebarsDotNet.Compiler +{ + /* + * Is going to be changed in next iterations + */ + + /// + /// Configures BlockParameters for current BlockHelper + /// + /// Parameters passed to BlockParams. + /// Function that perform binding of parameter to . + /// Dependencies of current configuration. Used to omit closure creation. + public delegate void ConfigureBlockParams(string[] parameters, ValueBinder valueBinder, object[] dependencies); + + /// + /// Function that perform binding of parameter to . + /// + /// Variable name that would be added to the . + /// Variable value provider that would be invoked when is requested. + /// Context for the binding. + public delegate void ValueBinder(string variableName, Func valueProvider, object context = null); + + /// + internal class BlockParamsValueProvider : IValueProvider + { + private static readonly string[] EmptyParameters = new string[0]; + + private static readonly BlockParamsValueProviderPool Pool = new BlockParamsValueProviderPool(); + + private readonly Dictionary>> _accessors; + + private BlockParam _params; + private Action> _invoker; + + public static BlockParamsValueProvider Create(BindingContext context, object @params) + { + var blockParamsValueProvider = Pool.Get(); + + blockParamsValueProvider._params = @params as BlockParam; + blockParamsValueProvider._invoker = action => action(context); + + return blockParamsValueProvider; + } + + private BlockParamsValueProvider() + { + _accessors = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + } + + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context | ValueTypes.All; + + /// + /// Configures behavior of BlockParams. + /// + public void Configure(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies) + { + var parameters = _params?.Parameters ?? EmptyParameters; + void BlockParamsAction(BindingContext context) + { + void ValueBinder(string name, Func value, object ctx) + { + if (!string.IsNullOrEmpty(name)) _accessors[name] = new KeyValuePair>(ctx, value); + } + + blockParamsConfiguration.Invoke(parameters, ValueBinder, dependencies); + } + + _invoker(BlockParamsAction); + } + + public bool TryGetValue(ref ChainSegment segment, out object value) + { + if (_accessors.TryGetValue(segment.LowerInvariant, out var provider)) + { + value = provider.Value(provider.Key); + return true; + } + + value = null; + return false; + } + + public void Dispose() + { + Pool.Return(this); + } + + private class BlockParamsValueProviderPool : DefaultObjectPool + { + public BlockParamsValueProviderPool() : base(new BlockParamsValueProviderPolicy()) + { + } + + private class BlockParamsValueProviderPolicy : IPooledObjectPolicy + { + public BlockParamsValueProvider Create() + { + return new BlockParamsValueProvider(); + } + + public bool Return(BlockParamsValueProvider item) + { + item._accessors.Clear(); + item._invoker = null; + item._params = null; + + return true; + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BoolishExpression.cs b/source/Handlebars/Compiler/Structure/BoolishExpression.cs index 816e6b6c..285211a5 100644 --- a/source/Handlebars/Compiler/Structure/BoolishExpression.cs +++ b/source/Handlebars/Compiler/Structure/BoolishExpression.cs @@ -5,27 +5,16 @@ namespace HandlebarsDotNet.Compiler { internal class BoolishExpression : HandlebarsExpression { - private readonly Expression _condition; - public BoolishExpression(Expression condition) { - _condition = condition; + Condition = condition; } - public Expression Condition - { - get { return _condition; } - } + public new Expression Condition { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.BoolishExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.BoolishExpression; - public override Type Type - { - get { return typeof(bool); } - } + public override Type Type => typeof(bool); } } diff --git a/source/Handlebars/Compiler/Structure/CommentExpression.cs b/source/Handlebars/Compiler/Structure/CommentExpression.cs index 06b02d24..c687843a 100644 --- a/source/Handlebars/Compiler/Structure/CommentExpression.cs +++ b/source/Handlebars/Compiler/Structure/CommentExpression.cs @@ -1,25 +1,16 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { internal class CommentExpression : HandlebarsExpression { - public string Value { get; private set; } - - public override ExpressionType NodeType - { - get { return (ExpressionType) HandlebarsExpressionType.CommentExpression; } - } - - public override Type Type - { - get { return typeof (void); } - } - public CommentExpression(string value) { Value = value; } + + public string Value { get; } + + public override ExpressionType NodeType => (ExpressionType) HandlebarsExpressionType.CommentExpression; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs b/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs deleted file mode 100644 index dfaf6d17..00000000 --- a/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace HandlebarsDotNet.Compiler -{ - internal class DeferredSectionExpression : HandlebarsExpression - { - public DeferredSectionExpression( - PathExpression path, - BlockExpression body, - BlockExpression inversion) - { - Path = path; - Body = body; - Inversion = inversion; - } - - public BlockExpression Body { get; private set; } - - public BlockExpression Inversion { get; private set; } - - public PathExpression Path { get; private set; } - - public override Type Type - { - get { return typeof(void); } - } - - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.DeferredSection; } - } - } -} - diff --git a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs index 38326a89..7c50ff3a 100644 --- a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs +++ b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; using System.Collections.Generic; @@ -11,41 +12,51 @@ internal enum HandlebarsExpressionType HelperExpression = 6003, PathExpression = 6004, IteratorExpression = 6005, - DeferredSection = 6006, PartialExpression = 6007, BoolishExpression = 6008, SubExpression = 6009, HashParameterAssignmentExpression = 6010, HashParametersExpression = 6011, - CommentExpression = 6012 + CommentExpression = 6012, + BlockParamsExpression = 6013 } internal abstract class HandlebarsExpression : Expression { - public static HelperExpression Helper(string helperName, IEnumerable arguments, bool isRaw = false) + public override Type Type => GetType(); + + public override bool CanReduce { get; } = false; + + public static HelperExpression Helper(string helperName, bool isBlock, IEnumerable arguments, bool isRaw = false) { - return new HelperExpression(helperName, arguments, isRaw); + return new HelperExpression(helperName, isBlock, arguments, isRaw); } - public static HelperExpression Helper(string helperName, bool isRaw = false) + public static HelperExpression Helper(string helperName, bool isBlock, bool isRaw = false, IReaderContext context = null) { - return new HelperExpression(helperName, isRaw); + return new HelperExpression(helperName, isBlock, isRaw, context); } public static BlockHelperExpression BlockHelper( string helperName, IEnumerable arguments, + BlockParamsExpression blockParams, Expression body, Expression inversion, bool isRaw = false) { - return new BlockHelperExpression(helperName, arguments, body, inversion, isRaw); + return new BlockHelperExpression(helperName, arguments, blockParams, body, inversion, isRaw); } public static PathExpression Path(string path) { return new PathExpression(path); } + + public static BlockParamsExpression BlockParams(string action, string blockParams) + { + return new BlockParamsExpression(action, blockParams); + } public static StaticExpression Static(string value) { @@ -59,25 +70,19 @@ public static StatementExpression Statement(Expression body, bool isEscaped, boo public static IteratorExpression Iterator( Expression sequence, + BlockParamsExpression blockParams, Expression template) { - return new IteratorExpression(sequence, template); + return new IteratorExpression(sequence, blockParams, template, Empty()); } public static IteratorExpression Iterator( Expression sequence, + BlockParamsExpression blockParams, Expression template, Expression ifEmpty) { - return new IteratorExpression(sequence, template, ifEmpty); - } - - public static DeferredSectionExpression DeferredSection( - PathExpression path, - BlockExpression body, - BlockExpression inversion) - { - return new DeferredSectionExpression(path, body, inversion); + return new IteratorExpression(sequence, blockParams, template, ifEmpty); } public static PartialExpression Partial(Expression partialName) diff --git a/source/Handlebars/Compiler/Structure/HashParametersExpression.cs b/source/Handlebars/Compiler/Structure/HashParametersExpression.cs index e4784148..bb4aab1e 100644 --- a/source/Handlebars/Compiler/Structure/HashParametersExpression.cs +++ b/source/Handlebars/Compiler/Structure/HashParametersExpression.cs @@ -6,22 +6,16 @@ namespace HandlebarsDotNet.Compiler { internal class HashParametersExpression : HandlebarsExpression { - public Dictionary Parameters { get; set; } + public Dictionary Parameters { get; } public HashParametersExpression(Dictionary parameters) { Parameters = parameters; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.HashParametersExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HashParametersExpression; - public override Type Type - { - get { return typeof(object); } - } + public override Type Type => typeof(HashParameterDictionary); } } diff --git a/source/Handlebars/Compiler/Structure/HelperExpression.cs b/source/Handlebars/Compiler/Structure/HelperExpression.cs index 5ecf0f42..b18423bb 100644 --- a/source/Handlebars/Compiler/Structure/HelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/HelperExpression.cs @@ -7,47 +7,35 @@ namespace HandlebarsDotNet.Compiler { internal class HelperExpression : HandlebarsExpression { - private readonly IEnumerable _arguments; - private readonly string _helperName; - private readonly bool _isRaw; - - public HelperExpression(string helperName, IEnumerable arguments, bool isRaw = false) - : this(helperName, isRaw) + public HelperExpression(string helperName, bool isBlock, IEnumerable arguments, bool isRaw = false, IReaderContext context = null) + : this(helperName, isBlock, isRaw) { - _arguments = arguments; + Arguments = arguments; + Context = context; + IsBlock = isBlock; } - public HelperExpression(string helperName, bool isRaw = false) + public HelperExpression(string helperName, bool isBlock, bool isRaw = false, IReaderContext context = null) { - _helperName = helperName; - _isRaw = isRaw; - _arguments = Enumerable.Empty(); + HelperName = helperName; + IsRaw = isRaw; + Arguments = Enumerable.Empty(); + Context = context; + IsBlock = isBlock; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.HelperExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HelperExpression; - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => typeof(void); - public string HelperName - { - get { return _helperName; } - } + public string HelperName { get; } - public bool IsRaw - { - get { return _isRaw; } - } + public bool IsRaw { get; } - public IEnumerable Arguments - { - get { return _arguments; } - } + public bool IsBlock { get; set; } + + public IEnumerable Arguments { get; } + public IReaderContext Context { get; } } } diff --git a/source/Handlebars/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index b42498aa..a67622b6 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -1,51 +1,28 @@ using System; +using System.Linq; using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { - internal class IteratorExpression : HandlebarsExpression + internal class IteratorExpression : BlockHelperExpression { - private readonly Expression _sequence; - private readonly Expression _template; - private readonly Expression _ifEmpty; - - - public IteratorExpression(Expression sequence, Expression template) - : this(sequence, template, Expression.Empty()) + public IteratorExpression(Expression sequence, BlockParamsExpression blockParams, Expression template, Expression ifEmpty) + :base("each", Enumerable.Empty(), blockParams, template, ifEmpty, false) { + Sequence = sequence; + Template = template; + IfEmpty = ifEmpty; } - public IteratorExpression(Expression sequence, Expression template, Expression ifEmpty) - { - _sequence = sequence; - _template = template; - _ifEmpty = ifEmpty; - } + public Expression Sequence { get; } - public Expression Sequence - { - get { return _sequence; } - } + public Expression Template { get; } - public Expression Template - { - get { return _template; } - } + public Expression IfEmpty { get; } - public Expression IfEmpty - { - get { return _ifEmpty; } - } + public override Type Type => typeof(void); - public override Type Type - { - get { return typeof(void); } - } - - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.IteratorExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.IteratorExpression; } } diff --git a/source/Handlebars/Compiler/Structure/PartialExpression.cs b/source/Handlebars/Compiler/Structure/PartialExpression.cs index c3beb096..bb25eda6 100644 --- a/source/Handlebars/Compiler/Structure/PartialExpression.cs +++ b/source/Handlebars/Compiler/Structure/PartialExpression.cs @@ -1,46 +1,23 @@ -using System; -using HandlebarsDotNet.Compiler; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { internal class PartialExpression : HandlebarsExpression { - private readonly Expression _partialName; - private readonly Expression _argument; - private readonly Expression _fallback; - public PartialExpression(Expression partialName, Expression argument, Expression fallback) { - _partialName = partialName; - _argument = argument; - _fallback = fallback; - } - - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.PartialExpression; } + PartialName = partialName; + Argument = argument; + Fallback = fallback; } - public override Type Type - { - get { return typeof(void); } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PartialExpression; - public Expression PartialName - { - get { return _partialName; } - } + public Expression PartialName { get; } - public Expression Argument - { - get { return _argument; } - } + public Expression Argument { get; } - public Expression Fallback - { - get { return _fallback; } - } + public Expression Fallback { get; } } } diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs new file mode 100644 index 00000000..05f9b6f9 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -0,0 +1,76 @@ +using System; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + /// + /// Represents parts of single separated with dots. + /// + public struct ChainSegment : IEquatable + { + private readonly string _value; + + internal readonly string LowerInvariant; + + public ChainSegment(string value) + { + var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); + var segmentTrimmedValue = TrimSquareBrackets(segmentValue).Intern(); + + IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); + _value = segmentValue; + IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"); + TrimmedValue = segmentTrimmedValue; + LowerInvariant = segmentTrimmedValue.ToLowerInvariant().Intern(); + } + + /// + /// Value with trimmed '[' and ']' + /// + public readonly string TrimmedValue; + + /// + /// Indicates whether is part of @ variable + /// + public readonly bool IsVariable; + + /// + /// Indicates whether is this or . + /// + public readonly bool IsThis; + + /// + /// Returns string representation of current + /// + public override string ToString() => _value; + + /// + public bool Equals(ChainSegment other) => _value == other._value; + + /// + public override bool Equals(object obj) => obj is ChainSegment other && Equals(other); + + /// + public override int GetHashCode() => _value != null ? _value.GetHashCode() : 0; + + /// + public static bool operator ==(ChainSegment a, ChainSegment b) => a.Equals(b); + + /// + public static bool operator !=(ChainSegment a, ChainSegment b) => !a.Equals(b); + + /// + public static implicit operator string(ChainSegment segment) => segment._value; + + internal static string TrimSquareBrackets(string key) + { + //Only trim a single layer of brackets. + if (key.StartsWith("[") && key.EndsWith("]")) + { + return key.Substring(1, key.Length - 2); + } + + return key; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs new file mode 100644 index 00000000..c5a6bb7a --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + internal delegate object ProcessSegment(ref PathInfo pathInfo, ref BindingContext context, object instance, HashParameterDictionary hashParameters); + + /// + /// Represents path expression + /// + public struct PathInfo : IEquatable + { + private readonly string _path; + + internal readonly ProcessSegment ProcessSegment; + internal readonly bool IsValidHelperLiteral; + internal readonly bool HasValue; + internal readonly bool IsThis; + + internal PathInfo( + bool hasValue, + string path, + bool isValidHelperLiteral, + PathSegment[] segments, + ProcessSegment processSegment + ) + { + IsValidHelperLiteral = isValidHelperLiteral; + HasValue = hasValue; + _path = path; + + IsVariable = path.StartsWith("@"); + IsInversion = path.StartsWith("^"); + IsBlockHelper = path.StartsWith("#"); + IsBlockClose = path.StartsWith("/"); + + Segments = segments; + ProcessSegment = processSegment; + TrimmedPath = string.Join(".", Segments?.SelectMany(o => o.PathChain).Select(o => o.TrimmedValue) ?? ArrayEx.Empty()); + IsThis = string.Equals(path, "this", StringComparison.OrdinalIgnoreCase) || path == "." || TrimmedPath.StartsWith("this.", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Indicates whether is part of @ variable + /// + public readonly bool IsVariable; + + /// + public readonly PathSegment[] Segments; + + internal readonly string TrimmedPath; + internal readonly bool IsInversion; + internal readonly bool IsBlockHelper; + internal readonly bool IsBlockClose; + + /// + public bool Equals(PathInfo other) + { + return HasValue == other.HasValue + && IsVariable == other.IsVariable + && _path == other._path; + } + + /// + public override bool Equals(object obj) => obj is PathInfo other && Equals(other); + + /// + public override int GetHashCode() => _path.GetHashCode(); + + /// + /// Returns string representation of current + /// + public override string ToString() => _path; + + /// + public static implicit operator string(PathInfo pathInfo) => pathInfo._path; + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs new file mode 100644 index 00000000..da0e9d60 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.ObjectDescriptors; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + // A lot is going to be changed here in next iterations + internal static partial class PathResolver + { + public static PathInfo GetPathInfo(string path) + { + if (path == "null") + return new PathInfo(false, path, false, null, 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 == "..") + { + segments.Add(new PathSegment(segment, ArrayEx.Empty(), true, null)); + continue; + } + + var segmentString = isVariable ? "@" + segment : segment; + var chainSegments = GetPathChain(segmentString).ToArray(); + if (chainSegments.Length > 1) isValidHelperLiteral = false; + ProcessPathChain chainDelegate; + switch (chainSegments.Length) + { + case 1: chainDelegate = ProcessPathChain_1; break; + case 2: chainDelegate = ProcessPathChain_2; break; + case 3: chainDelegate = ProcessPathChain_3; break; + case 4: chainDelegate = ProcessPathChain_4; break; + case 5: chainDelegate = ProcessPathChain_5; break; + default: chainDelegate = ProcessPathChain_Generic; break; + + } + + segments.Add(new PathSegment(segmentString, chainSegments, false, chainDelegate)); + } + + ProcessSegment @delegate; + switch (segments.Count) + { + case 1: @delegate = ProcessSegment_1; break; + case 2: @delegate = ProcessSegment_2; break; + case 3: @delegate = ProcessSegment_3; break; + case 4: @delegate = ProcessSegment_4; break; + case 5: @delegate = ProcessSegment_5; break; + default: @delegate = ProcessSegment_Generic; break; + } + + return new PathInfo(true, originalPath, isValidHelperLiteral, segments.ToArray(), @delegate); + } + + + //TODO: make path resolution logic smarter + public static object ResolvePath(BindingContext context, ref PathInfo pathInfo) + { + if (!pathInfo.HasValue) + return null; + + var instance = context.Value; + var hashParameters = instance as HashParameterDictionary; + + return pathInfo.ProcessSegment(ref pathInfo, ref context, instance, hashParameters); + } + + private static bool TryProcessSegment( + ref PathInfo pathInfo, + ref PathSegment segment, + ref BindingContext context, + ref object instance, + HashParameterDictionary hashParameters + ) + { + if (segment.IsJumpUp) return TryProcessJumpSegment(ref pathInfo, ref instance, ref context); + + instance = segment.ProcessPathChain(context, hashParameters, ref pathInfo, ref segment, instance); + return !(instance is UndefinedBindingResult); + } + + private static bool TryProcessJumpSegment( + ref PathInfo pathInfo, + ref object instance, + ref BindingContext context + ) + { + context = context.ParentContext; + if (context == null) + { + if (pathInfo.IsVariable) + { + instance = string.Empty; + return false; + } + + throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); + } + + instance = context.Value; + return true; + } + + private static object ProcessChainSegment( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref ChainSegment chainSegment, + object instance + ) + { + instance = ResolveValue(context, instance, ref chainSegment); + + if (!(instance is UndefinedBindingResult)) + return instance; + + if (hashParameters == null || hashParameters.ContainsKey(chainSegment) || + context.ParentContext == null) + { + if (context.Configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo, (instance as UndefinedBindingResult).Value); + return instance; + } + + instance = ResolveValue(context.ParentContext, context.ParentContext.Value, ref chainSegment); + if (!(instance is UndefinedBindingResult result)) return instance; + + if (context.Configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo, result.Value); + return result; + } + + private static IEnumerable GetPathChain(string segmentString) + { + var insideEscapeBlock = false; + var pathChainParts = segmentString.Split(new[]{'.'}, StringSplitOptions.RemoveEmptyEntries); + if (pathChainParts.Length == 0 && segmentString == ".") return new[] { new ChainSegment("this") }; + + var pathChain = pathChainParts.Aggregate(new List(), (list, next) => + { + if (insideEscapeBlock) + { + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } + + list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].ToString()}.{next}"); + return list; + } + + if (next.StartsWith("[")) + { + insideEscapeBlock = true; + } + + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } + + list.Add(new ChainSegment(next)); + return list; + }); + + return pathChain; + } + + private static object ResolveValue(BindingContext context, object instance, ref ChainSegment chainSegment) + { + object resolvedValue; + if (chainSegment.IsVariable) + { + return !context.TryGetContextVariable(ref chainSegment, out resolvedValue) + ? new UndefinedBindingResult(chainSegment, context.Configuration) + : resolvedValue; + } + + if (chainSegment.IsThis) return instance; + + if (TryAccessMember(instance, ref chainSegment, context.Configuration, out resolvedValue) + || context.TryGetVariable(ref chainSegment, out resolvedValue)) + { + return resolvedValue; + } + + if (chainSegment.LowerInvariant == "value" && context.TryGetVariable(ref chainSegment, out resolvedValue, true)) + { + return resolvedValue; + } + + return new UndefinedBindingResult(chainSegment, context.Configuration); + } + + public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) + { + if (instance == null) + { + value = new UndefinedBindingResult(chainSegment, configuration); + return false; + } + + var memberName = chainSegment.ToString(); + var instanceType = instance.GetType(); + memberName = TryResolveMemberName(instance, memberName, configuration, out var result) + ? ChainSegment.TrimSquareBrackets(result).Intern() + : chainSegment.TrimmedValue; + + if (!configuration.ObjectDescriptorProvider.CanHandleType(instanceType)) + { + value = new UndefinedBindingResult(memberName, configuration); + return false; + } + + if (!configuration.ObjectDescriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) + { + value = new UndefinedBindingResult(memberName, configuration); + return false; + } + + return descriptor.MemberAccessor.TryGetValue(instance, instanceType, memberName, out value); + } + + private static bool TryResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration, out string value) + { + var resolver = configuration.ExpressionNameResolver; + if (resolver == null) + { + value = null; + return false; + } + + value = resolver.ResolveExpressionName(instance, memberName); + return true; + } + } + + internal static partial class PathResolver + { + private static object ProcessSegment_1( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters); + return instance; + } + + private static object ProcessSegment_2( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters); + return instance; + } + + private static object ProcessSegment_3( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters); + return instance; + } + + private static object ProcessSegment_4( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[3], ref context, ref instance, hashParameters); + return instance; + } + + private static object ProcessSegment_5( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + _ = TryProcessSegment(ref pathInfo, ref pathInfo.Segments[0], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[1], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[2], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[3], ref context, ref instance, hashParameters) && + TryProcessSegment(ref pathInfo, ref pathInfo.Segments[4], ref context, ref instance, hashParameters); + return instance; + } + + private static object ProcessSegment_Generic( + ref PathInfo pathInfo, + ref BindingContext context, + object instance, + HashParameterDictionary hashParameters + ) + { + for (var segmentIndex = 0; segmentIndex < pathInfo.Segments.Length; segmentIndex++) + { + if (!TryProcessSegment( + ref pathInfo, + ref pathInfo.Segments[segmentIndex], + ref context, + ref instance, + hashParameters) + ) + { + return instance; + } + } + + return instance; + } + + private static object ProcessPathChain_1( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); + } + private static object ProcessPathChain_2( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); + return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); + } + + private static object ProcessPathChain_3( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); + return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); + } + + private static object ProcessPathChain_4( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); + return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[3], instance); + } + + private static object ProcessPathChain_5( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[0], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[1], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[2], instance); + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[3], instance); + return ProcessChainSegment(context, hashParameters, ref pathInfo, ref segment.PathChain[4], instance); + } + + private static object ProcessPathChain_Generic( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref PathSegment segment, + object instance + ) + { + for (var pathChainIndex = 0; pathChainIndex < segment.PathChain.Length; pathChainIndex++) + { + ref var chainSegment = ref segment.PathChain[pathChainIndex]; + instance = ProcessChainSegment(context, hashParameters, ref pathInfo, ref chainSegment, instance); + } + + return instance; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs new file mode 100644 index 00000000..fed4e09b --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs @@ -0,0 +1,49 @@ +using System; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + internal delegate object ProcessPathChain(BindingContext context, HashParameterDictionary hashParameters, ref PathInfo pathInfo, ref PathSegment segment, object instance); + + /// + /// Represents parts of single separated with '/'. + /// + public struct PathSegment : IEquatable + { + private readonly string _segment; + + internal readonly ProcessPathChain ProcessPathChain; + internal readonly bool IsJumpUp; + + internal PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, ProcessPathChain processPathChain) + { + _segment = segment; + IsJumpUp = isJumpUp; + PathChain = chain; + ProcessPathChain = processPathChain; + } + + /// + public readonly ChainSegment[] PathChain; + + /// + /// Returns string representation of current + /// + /// + public override string ToString() => _segment; + + /// + public bool Equals(PathSegment other) => _segment == other._segment; + + /// + public override bool Equals(object obj) => obj is PathSegment other && Equals(other); + + /// + public override int GetHashCode() => _segment != null ? _segment.GetHashCode() : 0; + + /// + public static bool operator ==(PathSegment a, PathSegment b) => a.Equals(b); + + /// + public static bool operator !=(PathSegment a, PathSegment b) => !a.Equals(b); + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/PathExpression.cs b/source/Handlebars/Compiler/Structure/PathExpression.cs index af760d72..be5cc0cc 100644 --- a/source/Handlebars/Compiler/Structure/PathExpression.cs +++ b/source/Handlebars/Compiler/Structure/PathExpression.cs @@ -1,31 +1,29 @@ using System; using System.Linq.Expressions; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { internal class PathExpression : HandlebarsExpression { - private readonly string _path; - - public PathExpression(string path) + public enum ResolutionContext { - _path = path; + None, + Parameter } - - public string Path + + public PathExpression(string path) { - get { return _path; } + Path = path; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.PathExpression; } - } + public new string Path { get; } + + public ResolutionContext Context { get; set; } + + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PathExpression; - public override Type Type - { - get { return typeof(object); } - } + public override Type Type => typeof(PathInfo); } } diff --git a/source/Handlebars/Compiler/Structure/StatementExpression.cs b/source/Handlebars/Compiler/Structure/StatementExpression.cs index c95d8af2..4f4fc185 100644 --- a/source/Handlebars/Compiler/Structure/StatementExpression.cs +++ b/source/Handlebars/Compiler/Structure/StatementExpression.cs @@ -13,22 +13,16 @@ public StatementExpression(Expression body, bool isEscaped, bool trimBefore, boo TrimAfter = trimAfter; } - public Expression Body { get; private set; } + public Expression Body { get; } - public bool IsEscaped { get; private set; } + public bool IsEscaped { get; } - public bool TrimBefore { get; private set; } + public bool TrimBefore { get; } - public bool TrimAfter { get; private set; } + public bool TrimAfter { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.StatementExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.StatementExpression; - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => Body.Type; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index 42de7b06..69c1b9d1 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -1,5 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { @@ -7,15 +7,21 @@ namespace HandlebarsDotNet.Compiler internal class UndefinedBindingResult { public readonly string Value; - private readonly HandlebarsConfiguration _configuration; + private readonly ICompiledHandlebarsConfiguration _configuration; - public UndefinedBindingResult(string value, HandlebarsConfiguration configuration) + public UndefinedBindingResult(string value, ICompiledHandlebarsConfiguration configuration) + { + Value = value; + _configuration = configuration; + } + + public UndefinedBindingResult(ChainSegment value, ICompiledHandlebarsConfiguration configuration) { Value = value; _configuration = configuration; } - public override string ToString() + public override string ToString() { var formatter = _configuration.UnresolvedBindingFormatter ?? string.Empty; return string.Format( formatter, Value ); diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 46a44b20..71d17033 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,83 +1,208 @@ -using System.Linq.Expressions; -using System.Reflection; +using System; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using Expressions.Shortcuts; +using HandlebarsDotNet.Adapters; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class BlockHelperFunctionBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) + private CompilationContext CompilationContext { get; } + + public BlockHelperFunctionBinder(CompilationContext compilationContext) { - return new BlockHelperFunctionBinder(context).Visit(expr); + CompilationContext = compilationContext; } - - private BlockHelperFunctionBinder(CompilationContext context) - : base(context) + + protected override Expression VisitStatementExpression(StatementExpression sex) { + return sex.Body is BlockHelperExpression ? Visit(sex.Body) : sex; } - protected override Expression VisitStatementExpression(StatementExpression sex) + protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) { - if (sex.Body is BlockHelperExpression) + var isInlinePartial = bhex.HelperName == "#*inline"; + + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(bhex.HelperName); + var context = Arg(CompilationContext.BindingContext); + var bindingContext = isInlinePartial + ? context.Cast() + : context.Property(o => o.Value); + + var readerContext = Arg(bhex.Context); + var body = FunctionBuilder.CompileCore(((BlockExpression) bhex.Body).Expressions, CompilationContext.Configuration); + var inverse = FunctionBuilder.CompileCore(((BlockExpression) bhex.Inversion).Expressions, CompilationContext.Configuration); + var helperName = pathInfo.TrimmedPath; + var helperPrefix = bhex.IsRaw || pathInfo.IsBlockHelper ? '#' : '^'; + var textWriter = context.Property(o => o.TextWriter); + var args = bhex.Arguments + .ApplyOn((PathExpression pex) => pex.Context = PathExpression.ResolutionContext.Parameter) + .Select(o => FunctionBuilder.Reduce(o, CompilationContext)); + + var arguments = Array(args); + var configuration = Arg(CompilationContext.Configuration); + + var reducerNew = New(() => new LambdaReducer(context, body, inverse)); + var reducer = Var(); + + var blockParamsProvider = Var(); + var blockParamsExpression = Call( + () => BlockParamsValueProvider.Create(context, Arg(bhex.BlockParams)) + ); + + var helperOptions = CreateHelperOptions(bhex, helperPrefix, reducer, blockParamsProvider, configuration, context); + + var blockHelpers = CompilationContext.Configuration.BlockHelpers; + if (blockHelpers.TryGetValue(helperName, out var helper)) { - return Visit(sex.Body); + return Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(Try() + .Body(Call( + () => helper(textWriter, helperOptions, bindingContext, arguments) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); } - else + + foreach (var resolver in CompilationContext.Configuration.HelperResolvers) { - return sex; + if (!resolver.TryResolveBlockHelper(helperName, out helper)) continue; + + return Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(Try() + .Body(Call( + () => helper(textWriter, helperOptions, bindingContext, arguments) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); } + + return Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(Try() + .Body(Call( + () => LateBoundCall( + helperName, + helperPrefix, + context, + (IReaderContext) readerContext, + textWriter, helperOptions, + body, + inverse, + bindingContext, + self, + arguments + ) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); } - protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) + private static ExpressionContainer CreateHelperOptions( + BlockHelperExpression bhex, + char helperPrefix, + ExpressionContainer reducer, + ExpressionContainer blockParamsProvider, + ExpressionContainer configuration, + ExpressionContainer context) { - var isInlinePartial = bhex.HelperName == "#*inline"; + ExpressionContainer helperOptions; + switch (helperPrefix) + { + case '#': + helperOptions = New( + () => new HelperOptions( + reducer.Member(o => o.Direct), + reducer.Member(o => o.Inverse), + blockParamsProvider, + configuration, + context) + ); + break; - var fb = new FunctionBuilder(CompilationContext.Configuration); + case '^': + helperOptions = New( + () => new HelperOptions( + reducer.Member(o => o.Inverse), + reducer.Member(o => o.Direct), + blockParamsProvider, + configuration, + context) + ); + break; + default: + throw new HandlebarsCompilerException($"Helper {bhex.HelperName} referenced with unsupported prefix", bhex.Context); + } - var bindingContext = isInlinePartial ? (Expression)CompilationContext.BindingContext : - Expression.Property( - CompilationContext.BindingContext, - typeof(BindingContext).GetProperty("Value")); + return helperOptions; + } - var body = fb.Compile(((BlockExpression)bhex.Body).Expressions, CompilationContext.BindingContext); - var inversion = fb.Compile(((BlockExpression)bhex.Inversion).Expressions, CompilationContext.BindingContext); - var helper = CompilationContext.Configuration.BlockHelpers[bhex.HelperName.Replace("#", "")]; - var arguments = new Expression[] + private static void LateBoundCall( + string helperName, + char helperPrefix, + BindingContext bindingContext, + IReaderContext readerContext, + TextWriter output, + HelperOptions options, + Action body, + Action inverse, + dynamic context, + BlockParamsValueProvider blockParamsValueProvider, + params object[] arguments + ) + { + try { - Expression.Property( - CompilationContext.BindingContext, - typeof(BindingContext).GetProperty("TextWriter")), - Expression.New( - typeof(HelperOptions).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0], - body, - inversion), - //this next arg is usually data, like { first: "Marc" } - //but for inline partials this is the complete BindingContext. - bindingContext, - Expression.NewArrayInit(typeof(object), bhex.Arguments) - }; + if (bindingContext.Configuration.BlockHelpers.TryGetValue(helperName, out var helper)) + { + helper(output, options, context, arguments); + return; + } + + foreach (var resolver in bindingContext.Configuration.HelperResolvers) + { + if (!resolver.TryResolveBlockHelper(helperName, out helper)) continue; + + helper(output, options, context, arguments); + + return; + } - - if (helper.Target != null) - { - return Expression.Call( - Expression.Constant(helper.Target), -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); + if(arguments.Length > 0) throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. BlockHelper '{helperName}'", readerContext); + + var pathInfo = bindingContext.Configuration.PathInfoStore.GetOrAdd(helperName); + var value = PathResolver.ResolvePath(bindingContext, ref pathInfo); + DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); } - else + catch(Exception e) when(!(e is HandlebarsException)) { - return Expression.Call( -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); + throw new HandlebarsRuntimeException($"Error occured while executing `{helperName}.`", e, readerContext); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs index 6ef6379b..a00ccb0b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -1,32 +1,23 @@ -using System; -using System.Linq; -using HandlebarsDotNet.Compiler; using System.Linq.Expressions; -using System.Reflection; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { internal class BoolishConverter : HandlebarsExpressionVisitor { - public static Expression Convert(Expression expr, CompilationContext context) - { - return new BoolishConverter(context).Visit(expr); - } + private readonly CompilationContext _compilationContext; - private BoolishConverter(CompilationContext context) - : base(context) + public BoolishConverter(CompilationContext compilationContext) { + _compilationContext = compilationContext; } - + protected override Expression VisitBoolishExpression(BoolishExpression bex) { - return Expression.Call( -#if netstandard - new Func(HandlebarsUtils.IsTruthyOrNonEmpty).GetMethodInfo(), -#else - new Func(HandlebarsUtils.IsTruthyOrNonEmpty).Method, -#endif - Visit(bex.Condition)); + var condition = Visit(bex.Condition); + condition = FunctionBuilder.Reduce(condition, _compilationContext); + var @object = ExpressionShortcuts.Arg(condition); + return ExpressionShortcuts.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(@object)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/CommentVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/CommentVisitor.cs index 44fde9d5..ec2d3eab 100644 --- a/source/Handlebars/Compiler/Translation/Expression/CommentVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/CommentVisitor.cs @@ -4,13 +4,7 @@ namespace HandlebarsDotNet.Compiler { internal class CommentVisitor : HandlebarsExpressionVisitor { - public static Expression Visit(Expression expr, CompilationContext compilationContext) - { - return new CommentVisitor(compilationContext).Visit(expr); - } - - private CommentVisitor(CompilationContext compilationContext) - : base(compilationContext) + public CommentVisitor() { } diff --git a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs index 6c63e477..877d6982 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -1,63 +1,101 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Linq.Expressions; -#if netstandard -using System.Reflection; -#endif +using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class ContextBinder : HandlebarsExpressionVisitor { private ContextBinder() - : base(null) { } - public static Expression Bind(Expression body, CompilationContext context, Expression parentContext, string templatePath) + public static Expression> Bind(CompilationContext context, Expression body, Expression parentContext, string templatePath) { - var writerParameter = Expression.Parameter(typeof(TextWriter), "buffer"); - var objectParameter = Expression.Parameter(typeof(object), "data"); - if (parentContext == null) - { - parentContext = Expression.Constant(null, typeof(BindingContext)); - } - var inlinePartialsParameter = Expression.Constant(null, typeof(IDictionary>)); + var configuration = Arg(context.Configuration); + + var writerParameter = Parameter("buffer"); + var objectParameter = Parameter("data"); + + var bindingContext = Arg(context.BindingContext); + var inlinePartialsParameter = Null>>(); + var textEncoder = configuration.Property(o => o.TextEncoder); + var encodedWriterExpression = Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); + var parentContextArg = Arg(parentContext); + + var newBindingContext = Call( + () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) + ); - var encodedWriterExpression = ResolveEncodedWriter(writerParameter, context.Configuration.TextEncoder); - var templatePathExpression = Expression.Constant(templatePath, typeof(string)); - var newBindingContext = Expression.New( - typeof(BindingContext).GetConstructor( - new[] { typeof(object), typeof(EncodedTextWriter), typeof(BindingContext), typeof(string), typeof(IDictionary>) }), - new[] { objectParameter, encodedWriterExpression, parentContext, templatePathExpression, inlinePartialsParameter }); - return Expression.Lambda>( - Expression.Block( - new[] { context.BindingContext }, - new Expression[] + var shouldDispose = Var("shouldDispose"); + + Expression blockBuilder = Block() + .Parameter(bindingContext) + .Parameter(shouldDispose) + .Line(Condition() + .If(objectParameter.Is()) + .Then(bindingContext.Assign(objectParameter.As())) + .Else(block => { - Expression.IfThenElse( - Expression.TypeIs(objectParameter, typeof(BindingContext)), - Expression.Assign(context.BindingContext, Expression.TypeAs(objectParameter, typeof(BindingContext))), - Expression.Assign(context.BindingContext, newBindingContext)) - }.Concat( - ((BlockExpression)body).Expressions - )), - new[] { writerParameter, objectParameter }); - } + block.Line(shouldDispose.Assign(true)); + block.Line(bindingContext.Assign(newBindingContext)); + }) + ) + .Line(Try() + .Body(block => block.Lines(((BlockExpression) body).Expressions)) + .Finally(Condition() + .If(shouldDispose) + .Then(bindingContext.Call(o => o.Dispose())) + ) + ); - private static Expression ResolveEncodedWriter(ParameterExpression writerParameter, ITextEncoder textEncoder) + return Expression.Lambda>(blockBuilder, (ParameterExpression) writerParameter.Expression, (ParameterExpression) objectParameter.Expression); + } + + public static Expression> Bind(CompilationContext context, Expression body, string templatePath) { - var outputEncoderExpression = Expression.Constant(textEncoder, typeof(ITextEncoder)); + var configuration = Arg(context.Configuration); + + var writerParameter = Parameter("buffer"); + var objectParameter = Parameter("data"); + + var bindingContext = Arg(context.BindingContext); + var inlinePartialsParameter = Null>>(); + var textEncoder = configuration.Property(o => o.TextEncoder); + var encodedWriterExpression = Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); + var parentContextArg = Var("parentContext"); + + var newBindingContext = Call( + () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) + ); + + var shouldDispose = Var("shouldDispose"); -#if netstandard - var encodedWriterFromMethod = typeof(EncodedTextWriter).GetRuntimeMethod("From", new[] { typeof(TextWriter), typeof(ITextEncoder) }); -#else - var encodedWriterFromMethod = typeof(EncodedTextWriter).GetMethod("From", new[] { typeof(TextWriter), typeof(ITextEncoder) }); -#endif - - return Expression.Call(encodedWriterFromMethod, writerParameter, outputEncoderExpression); + Expression blockBuilder = Block() + .Parameter(bindingContext) + .Parameter(shouldDispose) + .Line(Condition() + .If(objectParameter.Is()) + .Then(bindingContext.Assign(objectParameter.As()) + ) + .Else(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(bindingContext.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 new file mode 100644 index 00000000..e432a82d --- /dev/null +++ b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections; +using System.IO; + +namespace HandlebarsDotNet.Compiler +{ + internal static class DeferredSectionBlockHelper + { + public static void Helper(BindingContext context, char prefix, object value, + Action body, Action inverse, + BlockParamsValueProvider blockParamsValueProvider) + { + if (prefix == '#') + { + RenderSection(value, context, body, inverse, blockParamsValueProvider); + } + else + { + RenderSection(value, context, inverse, body, blockParamsValueProvider); + } + } + + private static void RenderSection( + object value, + BindingContext context, + Action body, + Action inversion, + BlockParamsValueProvider blockParamsValueProvider + ) + { + switch (value) + { + case bool boolValue when boolValue: + body(context, context.TextWriter, context); + return; + + case null: + case object _ when HandlebarsUtils.IsFalsyOrEmpty(value): + inversion(context, context.TextWriter, context); + return; + + case string _: + body(context, context.TextWriter, value); + return; + + case IEnumerable enumerable: + Iterator.Iterate(context, blockParamsValueProvider, enumerable, body, inversion); + break; + + default: + body(context, context.TextWriter, value); + break; + } + } + } +} + diff --git a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs deleted file mode 100644 index ba806f79..00000000 --- a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Collections; -using System.Linq; -using System.IO; -using System.Reflection; - -namespace HandlebarsDotNet.Compiler -{ - internal class DeferredSectionVisitor : HandlebarsExpressionVisitor - { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new DeferredSectionVisitor(context).Visit(expr); - } - - private DeferredSectionVisitor(CompilationContext context) - : base(context) - { - } - - protected override Expression VisitDeferredSectionExpression(DeferredSectionExpression dsex) - { -#if netstandard - var method = new Action, Action>(RenderSection).GetMethodInfo(); -#else - var method = new Action, Action>(RenderSection).Method; -#endif - Expression path = HandlebarsExpression.Path(dsex.Path.Path.Substring(1)); - Expression context = CompilationContext.BindingContext; - Expression[] templates = GetDeferredSectionTemplates(dsex); - - return Expression.Call(method, new[] {path, context}.Concat(templates)); - - } - - private Expression[] GetDeferredSectionTemplates(DeferredSectionExpression dsex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - var body = fb.Compile(dsex.Body.Expressions, CompilationContext.BindingContext); - var inversion = fb.Compile(dsex.Inversion.Expressions, CompilationContext.BindingContext); - - var sectionPrefix = dsex.Path.Path.Substring(0, 1); - - switch (sectionPrefix) - { - case "#": - return new[] {body, inversion}; - case "^": - return new[] {inversion, body}; - default: - throw new HandlebarsCompilerException("Tried to compile a section expression that did not begin with # or ^"); - } - } - - private static void RenderSection(object value, BindingContext context, Action body, Action inversion) - { - var boolValue = value as bool?; - var enumerable = value as IEnumerable; - - if (boolValue == true) - { - body(context.TextWriter, context); - } - else if (boolValue == false) - { - inversion(context.TextWriter, context); - } - else if (HandlebarsUtils.IsFalsyOrEmpty(value)) - { - inversion(context.TextWriter, context); - } - else if (value is string) - { - body(context.TextWriter, value); - } - else if (enumerable != null) - { - foreach (var item in enumerable) - { - body(context.TextWriter, item); - } - } - else - { - body(context.TextWriter, value); - } - } - } -} - diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs index 41d70b78..77139d03 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -1,181 +1,154 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; - -namespace HandlebarsDotNet.Compiler -{ - internal abstract class HandlebarsExpressionVisitor : ExpressionVisitor - { - private readonly CompilationContext _compilationContext; - - protected HandlebarsExpressionVisitor(CompilationContext compilationContext) - { - _compilationContext = compilationContext; - } - - protected virtual CompilationContext CompilationContext - { - get { return _compilationContext; } - } - - public override Expression Visit(Expression exp) - { - if (exp == null) - { - return null; - } - switch ((HandlebarsExpressionType)exp.NodeType) - { - case HandlebarsExpressionType.StatementExpression: - return VisitStatementExpression((StatementExpression)exp); - case HandlebarsExpressionType.StaticExpression: - return VisitStaticExpression((StaticExpression)exp); - case HandlebarsExpressionType.HelperExpression: - return VisitHelperExpression((HelperExpression)exp); - case HandlebarsExpressionType.BlockExpression: - return VisitBlockHelperExpression((BlockHelperExpression)exp); - case HandlebarsExpressionType.HashParameterAssignmentExpression: - return exp; - case HandlebarsExpressionType.HashParametersExpression: - return VisitHashParametersExpression((HashParametersExpression)exp); - case HandlebarsExpressionType.PathExpression: - return VisitPathExpression((PathExpression)exp); - case HandlebarsExpressionType.IteratorExpression: - return VisitIteratorExpression((IteratorExpression)exp); - case HandlebarsExpressionType.DeferredSection: - return VisitDeferredSectionExpression((DeferredSectionExpression)exp); - case HandlebarsExpressionType.PartialExpression: - return VisitPartialExpression((PartialExpression)exp); - case HandlebarsExpressionType.BoolishExpression: - return VisitBoolishExpression((BoolishExpression)exp); - case HandlebarsExpressionType.SubExpression: - return VisitSubExpression((SubExpressionExpression)exp); - default: - return base.Visit(exp); - } - } - - protected virtual Expression VisitStatementExpression(StatementExpression sex) - { - Expression body = Visit(sex.Body); +using System.Linq.Expressions; + +namespace HandlebarsDotNet.Compiler +{ + internal abstract class HandlebarsExpressionVisitor : ExpressionVisitor + { + public override Expression Visit(Expression exp) + { + if (exp == null) + { + return null; + } + switch ((HandlebarsExpressionType)exp.NodeType) + { + case HandlebarsExpressionType.StatementExpression: + return VisitStatementExpression((StatementExpression)exp); + case HandlebarsExpressionType.StaticExpression: + return VisitStaticExpression((StaticExpression)exp); + case HandlebarsExpressionType.HelperExpression: + return VisitHelperExpression((HelperExpression)exp); + case HandlebarsExpressionType.BlockExpression: + return VisitBlockHelperExpression((BlockHelperExpression)exp); + case HandlebarsExpressionType.HashParameterAssignmentExpression: + return exp; + case HandlebarsExpressionType.HashParametersExpression: + return VisitHashParametersExpression((HashParametersExpression)exp); + case HandlebarsExpressionType.PathExpression: + return VisitPathExpression((PathExpression)exp); + case HandlebarsExpressionType.IteratorExpression: + return VisitIteratorExpression((IteratorExpression)exp); + case HandlebarsExpressionType.PartialExpression: + return VisitPartialExpression((PartialExpression)exp); + case HandlebarsExpressionType.BoolishExpression: + return VisitBoolishExpression((BoolishExpression)exp); + case HandlebarsExpressionType.SubExpression: + return VisitSubExpression((SubExpressionExpression)exp); + default: + return base.Visit(exp); + } + } + + protected virtual Expression VisitStatementExpression(StatementExpression sex) + { + Expression body = Visit(sex.Body); if (body != sex.Body) { return HandlebarsExpression.Statement(body, sex.IsEscaped, sex.TrimBefore, sex.TrimAfter); - } - return sex; - } - - protected virtual Expression VisitPathExpression(PathExpression pex) - { - return pex; - } - - protected virtual Expression VisitHelperExpression(HelperExpression hex) + } + return sex; + } + + protected virtual Expression VisitPathExpression(PathExpression pex) + { + return pex; + } + + protected virtual Expression VisitHelperExpression(HelperExpression hex) { var arguments = VisitExpressionList(hex.Arguments); if (arguments != hex.Arguments) { - return HandlebarsExpression.Helper(hex.HelperName, arguments, hex.IsRaw); - } - return hex; - } - - protected virtual Expression VisitBlockHelperExpression(BlockHelperExpression bhex) + return HandlebarsExpression.Helper(hex.HelperName, hex.IsBlock, arguments, hex.IsRaw); + } + return hex; + } + + protected virtual Expression VisitBlockHelperExpression(BlockHelperExpression bhex) { - var arguments = VisitExpressionList(bhex.Arguments); + var arguments = VisitExpressionList(bhex.Arguments); // Don't visit Body/Inversion - they will be compiled separately if (arguments != bhex.Arguments) { - return HandlebarsExpression.BlockHelper(bhex.HelperName, arguments, bhex.Body, bhex.Inversion, bhex.IsRaw); - } - return bhex; - } - - protected virtual Expression VisitStaticExpression(StaticExpression stex) - { - return stex; - } - - protected virtual Expression VisitIteratorExpression(IteratorExpression iex) - { - Expression sequence = Visit(iex.Sequence); - // Don't visit Template/IfEmpty - they will be compiled separately - + return HandlebarsExpression.BlockHelper(bhex.HelperName, arguments, bhex.BlockParams, bhex.Body, bhex.Inversion, bhex.IsRaw); + } + return bhex; + } + + protected virtual Expression VisitStaticExpression(StaticExpression stex) + { + return stex; + } + + protected virtual Expression VisitIteratorExpression(IteratorExpression iex) + { + Expression sequence = Visit(iex.Sequence); + // Don't visit Template/IfEmpty - they will be compiled separately + if (sequence != iex.Sequence) { - return HandlebarsExpression.Iterator(sequence, iex.Template, iex.IfEmpty); - } - return iex; - } - - protected virtual Expression VisitDeferredSectionExpression(DeferredSectionExpression dsex) - { - PathExpression path = (PathExpression)Visit(dsex.Path); - // Don't visit Body/Inversion - they will be compiled separately - - if (path != dsex.Path) - { - return HandlebarsExpression.DeferredSection(path, dsex.Body, dsex.Inversion); - } - return dsex; - } - - protected virtual Expression VisitPartialExpression(PartialExpression pex) - { - Expression partialName = Visit(pex.PartialName); - Expression argument = Visit(pex.Argument); - // Don't visit Fallback - it will be compiled separately - - if (partialName != pex.PartialName + return HandlebarsExpression.Iterator(sequence, iex.BlockParams, iex.Template, iex.IfEmpty); + } + return iex; + } + + protected virtual Expression VisitPartialExpression(PartialExpression pex) + { + Expression partialName = Visit(pex.PartialName); + Expression argument = Visit(pex.Argument); + // Don't visit Fallback - it will be compiled separately + + if (partialName != pex.PartialName || argument != pex.Argument) { return HandlebarsExpression.Partial(partialName, argument, pex.Fallback); - } - return pex; - } - - protected virtual Expression VisitBoolishExpression(BoolishExpression bex) - { - Expression condition = Visit(bex.Condition); + } + return pex; + } + + protected virtual Expression VisitBoolishExpression(BoolishExpression bex) + { + Expression condition = Visit(bex.Condition); if (condition != bex.Condition) { return HandlebarsExpression.Boolish(condition); - } - return bex; - } - - protected virtual Expression VisitSubExpression(SubExpressionExpression subex) - { - Expression expression = Visit(subex.Expression); + } + return bex; + } + + protected virtual Expression VisitSubExpression(SubExpressionExpression subex) + { + Expression expression = Visit(subex.Expression); if (expression != subex.Expression) { return HandlebarsExpression.SubExpression(expression); - } - return subex; - } - - protected virtual Expression VisitHashParametersExpression(HashParametersExpression hpex) - { - var parameters = new Dictionary(); - bool parametersChanged = false; - foreach (string key in hpex.Parameters.Keys) - { - Expression value = Visit(hpex.Parameters[key]); - parameters.Add(key, value); - if (value != hpex.Parameters[key]) - { - parametersChanged = true; - } - } - if (parametersChanged) - { - return HandlebarsExpression.HashParametersExpression(parameters); - } - return hpex; - } - + } + return subex; + } + + protected virtual Expression VisitHashParametersExpression(HashParametersExpression hpex) + { + var parameters = new Dictionary(); + bool parametersChanged = false; + foreach (string key in hpex.Parameters.Keys) + { + Expression value = Visit(hpex.Parameters[key]); + parameters.Add(key, value); + if (value != hpex.Parameters[key]) + { + parametersChanged = true; + } + } + if (parametersChanged) + { + return HandlebarsExpression.HashParametersExpression(parameters); + } + return hpex; + } + IEnumerable VisitExpressionList(IEnumerable original) { if (original == null) @@ -205,7 +178,7 @@ IEnumerable VisitExpressionList(IEnumerable original) if (list != null) return list.ToArray(); return original; - } - } -} - + } + } +} + diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs index 77a89a0c..09a4f92f 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs @@ -1,23 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; - -namespace HandlebarsDotNet.Compiler -{ - internal class HashParameterBinder : HandlebarsExpressionVisitor - { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new HashParameterBinder(context).Visit(expr); - } - - private HashParameterBinder(CompilationContext context) - : base(context) - { - } - - protected override Expression VisitHashParametersExpression(HashParametersExpression hpex) +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace HandlebarsDotNet.Compiler +{ + internal class HashParameterBinder : HandlebarsExpressionVisitor + { + protected override Expression VisitHashParametersExpression(HashParametersExpression hpex) { var addMethod = typeof(HashParameterDictionary).GetMethod("Add", new[] { typeof(string), typeof(object) }); @@ -33,8 +23,8 @@ protected override Expression VisitHashParametersExpression(HashParametersExpres return Expression.ListInit( Expression.New(typeof(HashParameterDictionary).GetConstructor(new Type[0])), - elementInits); - } - } -} - + elementInits); + } + } +} + diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs index eb764ea0..77c09999 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs @@ -1,6 +1,14 @@ +using System; using System.Collections.Generic; namespace HandlebarsDotNet.Compiler { - public class HashParameterDictionary : Dictionary { } + public class HashParameterDictionary : Dictionary + { + public HashParameterDictionary() + :base(StringComparer.OrdinalIgnoreCase) + { + + } + } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 1205ea9e..e7169619 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,109 +1,119 @@ -using System; using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using System.Collections.Generic; +using System.IO; +using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class HelperFunctionBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new HelperFunctionBinder(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private HelperFunctionBinder(CompilationContext context) - : base(context) + public HelperFunctionBinder(CompilationContext compilationContext) { + CompilationContext = compilationContext; } - + protected override Expression VisitStatementExpression(StatementExpression sex) { - if (sex.Body is HelperExpression) + return sex.Body is HelperExpression ? Visit(sex.Body) : sex; + } + + protected override Expression VisitHelperExpression(HelperExpression hex) + { + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(hex.HelperName); + if(!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming) return Expression.Empty(); + + var readerContext = Arg(hex.Context); + var helperName = pathInfo.TrimmedPath; + var bindingContext = Arg(CompilationContext.BindingContext); + var contextValue = bindingContext.Property(o => o.Value); + 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 args = Array(arguments); + + var configuration = CompilationContext.Configuration; + if (configuration.Helpers.TryGetValue(helperName, out var helper)) + { + return Call(() => helper(textWriter, contextValue, args)); + } + + if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { - return Visit(sex.Body); + return Call(() => + CaptureResult(textWriter, Call(() => returnHelper(contextValue, args))) + ); } - else + + foreach (var resolver in configuration.HelperResolvers) { - return sex; + if (resolver.TryResolveReturnHelper(helperName, typeof(object), out var resolvedHelper)) + { + return Call(() => + CaptureResult(textWriter, Call(() => resolvedHelper(contextValue, args))) + ); + } } + + return Call(() => + CaptureResult(textWriter, Call(() => + LateBindHelperExpression(bindingContext, helperName, args, (IReaderContext) readerContext) + )) + ); } - protected override Expression VisitHelperExpression(HelperExpression hex) + // will be significantly improved in next iterations + public static ResultHolder TryLateBindHelperExpression(BindingContext context, string helperName, object[] arguments) { - if (CompilationContext.Configuration.Helpers.ContainsKey(hex.HelperName)) + var configuration = context.Configuration; + if (configuration.Helpers.TryGetValue(helperName, out var helper)) { - var helper = CompilationContext.Configuration.Helpers[hex.HelperName]; - var arguments = new Expression[] - { - Expression.Property( - CompilationContext.BindingContext, -#if netstandard - typeof(BindingContext).GetRuntimeProperty("TextWriter")), -#else - typeof(BindingContext).GetProperty("TextWriter")), -#endif - Expression.Property( - CompilationContext.BindingContext, -#if netstandard - typeof(BindingContext).GetRuntimeProperty("Value")), -#else - typeof(BindingContext).GetProperty("Value")), -#endif - Expression.NewArrayInit(typeof(object), hex.Arguments.Select(a => Visit(a))) - }; - if (helper.Target != null) - { - return Expression.Call( - Expression.Constant(helper.Target), -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); - } - else + using (var write = new PolledStringWriter(configuration.FormatProvider)) { - return Expression.Call( -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); + helper(write, context.Value, arguments); + var result = write.ToString(); + return new ResultHolder(true, result); } } - else + + if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { - return Expression.Call( - Expression.Constant(this), -#if netstandard - new Action>(LateBindHelperExpression).GetMethodInfo(), -#else - new Action>(LateBindHelperExpression).Method, -#endif - CompilationContext.BindingContext, - Expression.Constant(hex.HelperName), - Expression.NewArrayInit(typeof(object), hex.Arguments)); + var result = returnHelper(context.Value, arguments); + return new ResultHolder(true, result); } - } - - private void LateBindHelperExpression( - BindingContext context, - string helperName, - IEnumerable arguments) - { - if (CompilationContext.Configuration.Helpers.ContainsKey(helperName)) + + var targetType = arguments.FirstOrDefault()?.GetType(); + foreach (var resolver in configuration.HelperResolvers) { - var helper = CompilationContext.Configuration.Helpers[helperName]; - helper(context.TextWriter, context.Value, arguments.ToArray()); + if (!resolver.TryResolveReturnHelper(helperName, targetType, out returnHelper)) continue; + + var result = returnHelper(context.Value, arguments); + return new ResultHolder(true, result); } - else + + return new ResultHolder(false, null); + } + + private static object LateBindHelperExpression(BindingContext context, string helperName, object[] arguments, + IReaderContext readerContext) + { + var result = TryLateBindHelperExpression(context, helperName, arguments); + if (result.Success) { - throw new HandlebarsRuntimeException(string.Format("Template references a helper that is not registered. Could not find helper '{0}'", helperName)); + return result.Value; } + + throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. Helper '{helperName}'", readerContext); + } + + private static object CaptureResult(TextWriter writer, object result) + { + writer?.WriteSafeString(result); + return result; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index cdbebfd5..2c5dfc74 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -4,345 +4,260 @@ using System.IO; using System.Collections; using System.Collections.Generic; -using System.Dynamic; -using System.Reflection; +using Expressions.Shortcuts; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.ObjectDescriptors; +using HandlebarsDotNet.Polyfills; +using HandlebarsDotNet.ValueProviders; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class IteratorBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new IteratorBinder(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private IteratorBinder(CompilationContext context) - : base(context) + public IteratorBinder(CompilationContext compilationContext) { + CompilationContext = compilationContext; } - + protected override Expression VisitIteratorExpression(IteratorExpression iex) { - var iteratorBindingContext = Expression.Variable(typeof(BindingContext), "context"); - return Expression.Block( - new ParameterExpression[] - { - iteratorBindingContext - }, - Expression.IfThenElse( - Expression.TypeIs(iex.Sequence, typeof(IEnumerable)), - Expression.IfThenElse( -#if netstandard - Expression.Call(new Func(IsNonListDynamic).GetMethodInfo(), new[] { iex.Sequence }), -#else - Expression.Call(new Func(IsNonListDynamic).Method, new[] { iex.Sequence }), -#endif - GetDynamicIterator(iteratorBindingContext, iex), - Expression.IfThenElse( -#if netstandard - Expression.Call(new Func(IsGenericDictionary).GetMethodInfo(), new[] { iex.Sequence }), -#else - Expression.Call(new Func(IsGenericDictionary).Method, new[] { iex.Sequence }), -#endif - GetDictionaryIterator(iteratorBindingContext, iex), - GetEnumerableIterator(iteratorBindingContext, iex))), - GetObjectIterator(iteratorBindingContext, iex)) - ); - } + var context = Arg(CompilationContext.BindingContext); + var sequence = Var("sequence"); + + var template = FunctionBuilder.CompileCore(new[] {iex.Template}, CompilationContext.Configuration); + var ifEmpty = FunctionBuilder.CompileCore(new[] {iex.IfEmpty}, CompilationContext.Configuration); - private Expression GetEnumerableIterator(Expression contextParameter, IteratorExpression iex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(IteratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( -#if netstandard - new Action, Action>(Iterate).GetMethodInfo(), -#else - new Action, Action>(Iterate).Method, -#endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(IteratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); - } - - private Expression GetObjectIterator(Expression contextParameter, IteratorExpression iex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( -#if netstandard - new Action, Action>(Iterate).GetMethodInfo(), -#else - new Action, Action>(Iterate).Method, -#endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - iex.Sequence, - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); - } - - private Expression GetDictionaryIterator(Expression contextParameter, IteratorExpression iex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( -#if netstandard - new Action, Action>(Iterate).GetMethodInfo(), -#else - new Action, Action>(Iterate).Method, -#endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); - } + if (iex.Sequence is PathExpression pathExpression) + { + pathExpression.Context = PathExpression.ResolutionContext.Parameter; + } + + var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext)); + var blockParams = Arg(iex.BlockParams); + var blockParamsProvider = Call(() => BlockParamsValueProvider.Create(context, blockParams)); + var blockParamsProviderVar = Var(); - private Expression GetDynamicIterator(Expression contextParameter, IteratorExpression iex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Block( - Expression.Assign(contextParameter, - Expression.New( - typeof(ObjectEnumeratorBindingContext).GetConstructor(new[] { typeof(BindingContext) }), - new Expression[] { CompilationContext.BindingContext })), - Expression.Call( -#if netstandard - new Action, Action>(Iterate).GetMethodInfo(), -#else - new Action, Action>(Iterate).Method, -#endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - Expression.Convert(iex.Sequence, typeof(IDynamicMetaObjectProvider)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + return Block() + .Parameter(sequence, compiledSequence) + .Parameter(blockParamsProviderVar, blockParamsProvider) + .Line(blockParamsProviderVar.Using((self, builder) => + { + builder + .Line(Call(() => + Iterator.Iterate(context, self, sequence, template, ifEmpty) + )); + })); } + } - private static bool IsNonListDynamic(object target) + internal static class Iterator + { + private static readonly ConfigureBlockParams BlockParamsEnumerableConfiguration = (parameters, binder, deps) => { - var interfaces = target.GetType().GetInterfaces(); - return interfaces.Contains(typeof(IDynamicMetaObjectProvider)) - && ((IDynamicMetaObjectProvider)target).GetMetaObject(Expression.Constant(target)).GetDynamicMemberNames().Any(); - } + binder(parameters.ElementAtOrDefault(0), ctx => ctx.As().Value, deps[0]); + binder(parameters.ElementAtOrDefault(1), ctx => ctx.As().Index, deps[0]); + }; - private static bool IsGenericDictionary(object target) + private static readonly ConfigureBlockParams BlockParamsObjectEnumeratorConfiguration = (parameters, binder, deps) => { - return - target.GetType() -#if netstandard - .GetInterfaces() - .Where(i => i.GetTypeInfo().IsGenericType) + binder(parameters.ElementAtOrDefault(0), ctx => ctx.As().Value, deps[0]); + binder(parameters.ElementAtOrDefault(1), ctx => ctx.As().Key, deps[0]); + }; -#else - .GetInterfaces() - .Where(i => i.IsGenericType) -#endif - .Any(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); - } - - private static void Iterate( - ObjectEnumeratorBindingContext context, + public static void Iterate(BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, object target, - Action template, - Action ifEmpty) + Action template, + Action ifEmpty) { - if (HandlebarsUtils.IsTruthy(target)) + if (!HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; - var targetType = target.GetType(); - var properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public).OfType(); - var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.Instance); - foreach (var enumerableValue in new ExtendedEnumerable(properties.Concat(fields))) - { - var member = enumerableValue.Value; - context.Key = member.Name; - var value = AccessMember(target, member); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + ifEmpty(context, context.TextWriter, context.Value); + return; + } - template(context.TextWriter, value); - } + var targetType = target.GetType(); + if (!(context.Configuration.ObjectDescriptorProvider.CanHandleType(targetType) && + context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor))) + { + ifEmpty(context, context.TextWriter, context.Value); + return; + } - if (context.Index == 0) + if (!descriptor.ShouldEnumerate) + { + var properties = descriptor.GetProperties(descriptor, target); + if (properties is IList propertiesList) { - ifEmpty(context.TextWriter, context.Value); + IterateObjectWithStaticProperties(context, descriptor, blockParamsValueProvider, target, propertiesList, targetType, template, ifEmpty); + return; } + + IterateObject(context, descriptor, blockParamsValueProvider, target, properties, targetType, template, ifEmpty); + return; } - else + + if (target is IList list) { - ifEmpty(context.TextWriter, context.Value); + IterateList(context, blockParamsValueProvider, list, template, ifEmpty); + return; } - } - private static void Iterate( - ObjectEnumeratorBindingContext context, - IEnumerable target, - Action template, - Action ifEmpty) + IterateEnumerable(context, blockParamsValueProvider, (IEnumerable) target, template, ifEmpty); + } + + private static void IterateObject(BindingContext context, + ObjectDescriptor descriptor, + BlockParamsValueProvider blockParamsValueProvider, + object target, + IEnumerable properties, + Type targetType, + Action template, + Action ifEmpty) { - if (HandlebarsUtils.IsTruthy(target)) + using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - context.Index = 0; - var targetType = target.GetType(); -#if netstandard - var keysProperty = targetType.GetRuntimeProperty("Keys"); -#else - var keysProperty = targetType.GetProperty("Keys"); -#endif - if (keysProperty != null) + blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); + + iterator.Index = 0; + var accessor = descriptor.MemberAccessor; + var enumerable = new ExtendedEnumerable(properties); + bool enumerated = false; + + foreach (var enumerableValue in enumerable) { - var keys = keysProperty.GetGetMethod().Invoke(target, null) as IEnumerable; - if (keys != null) - { - var getItemMethodInfo = targetType.GetMethod("get_Item"); - var parameters = new object[1]; - foreach (var enumerableValue in new ExtendedEnumerable(keys)) - { - var key = parameters[0] = enumerableValue.Value; - context.Key = key.ToString(); - var value = getItemMethodInfo.Invoke(target, parameters); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + enumerated = true; + iterator.Key = enumerableValue.Value.ToString(); + var key = iterator.Key.Intern(); + iterator.Value = accessor.TryGetValue(target, targetType, key, out var value) ? value : null; + iterator.First = enumerableValue.IsFirst; + iterator.Last = enumerableValue.IsLast; + iterator.Index = enumerableValue.Index; - template(context.TextWriter, value); - } + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(blockParamsValueProvider); + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); } } - if (context.Index == 0) + + if (iterator.Index == 0 && !enumerated) { - ifEmpty(context.TextWriter, context.Value); + ifEmpty(context, context.TextWriter, context.Value); } } - else - { - ifEmpty(context.TextWriter, context.Value); - } } - - private static void Iterate( - ObjectEnumeratorBindingContext context, - IDynamicMetaObjectProvider target, - Action template, - Action ifEmpty) + + private static void IterateObjectWithStaticProperties(BindingContext context, + ObjectDescriptor descriptor, + BlockParamsValueProvider blockParamsValueProvider, + object target, + IList properties, + Type targetType, + Action template, + Action ifEmpty) { - if (HandlebarsUtils.IsTruthy(target)) + using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - context.Index = 0; - var meta = target.GetMetaObject(Expression.Constant(target)); - foreach (var enumerableValue in new ExtendedEnumerable(meta.GetDynamicMemberNames())) + blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); + + var accessor = descriptor.MemberAccessor; + + var count = properties.Count; + for (iterator.Index = 0; iterator.Index < count; iterator.Index++) { - var name = enumerableValue.Value; - context.Key = name; - var value = GetProperty(target, name); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + iterator.Key = properties[iterator.Index].ToString(); + iterator.Value = accessor.TryGetValue(target, targetType, iterator.Key, out var value) ? value : null; + iterator.First = iterator.Index == 0; + iterator.Last = iterator.Index == count - 1; - template(context.TextWriter, value); + using (var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(blockParamsValueProvider); + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + } } - if (context.Index == 0) + if (iterator.Index == 0) { - ifEmpty(context.TextWriter, context.Value); + ifEmpty(context, context.TextWriter, context.Value); } } - else - { - ifEmpty(context.TextWriter, context.Value); - } } - - private static void Iterate( - IteratorBindingContext context, - IEnumerable sequence, - Action template, - Action ifEmpty) + + private static void IterateList(BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, + IList target, + Action template, + Action ifEmpty) { - context.Index = 0; - foreach (var enumeratorValue in new ExtendedEnumerable(sequence)) + using (var iterator = IteratorValueProvider.Create()) { - var item = enumeratorValue.Value; - context.First = enumeratorValue.IsFirst; - context.Last = enumeratorValue.IsLast; - context.Index = enumeratorValue.Index; + blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); - template(context.TextWriter, item); - } - - if (context.Index == 0) - { - ifEmpty(context.TextWriter, context.Value); - } - } + var count = target.Count; + for (iterator.Index = 0; iterator.Index < count; iterator.Index++) + { + iterator.Value = target[iterator.Index]; + iterator.First = iterator.Index == 0; + iterator.Last = iterator.Index == count - 1; - private static object GetProperty(object target, string name) - { - var site = System.Runtime.CompilerServices.CallSite>.Create( - Microsoft.CSharp.RuntimeBinder.Binder.GetMember(0, name, target.GetType(), new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(0, null) })); - return site.Target(site, target); - } + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(blockParamsValueProvider); + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + } + } - private class IteratorBindingContext : BindingContext - { - public IteratorBindingContext(BindingContext context) - : base(context.Value, context.TextWriter, context.ParentContext, context.TemplatePath, context.InlinePartialTemplates) - { + if (iterator.Index == 0) + { + ifEmpty(context, context.TextWriter, context.Value); + } } - - public int Index { get; set; } - - public bool First { get; set; } - - public bool Last { get; set; } } - - private class ObjectEnumeratorBindingContext : IteratorBindingContext + + private static void IterateEnumerable(BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, + IEnumerable target, + Action template, + Action ifEmpty) { - public ObjectEnumeratorBindingContext(BindingContext context) - : base(context) + using (var iterator = IteratorValueProvider.Create()) { - } + blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); - public string Key { get; set; } - } + iterator.Index = 0; + var enumerable = new ExtendedEnumerable(target); + bool enumerated = false; + + foreach (var enumerableValue in enumerable) + { + enumerated = true; + iterator.Value = enumerableValue.Value; + iterator.First = enumerableValue.IsFirst; + iterator.Last = enumerableValue.IsLast; - private static object AccessMember(object instance, MemberInfo member) - { - if (member is PropertyInfo) - { - return ((PropertyInfo)member).GetValue(instance, null); - } - if (member is FieldInfo) - { - return ((FieldInfo)member).GetValue(instance); + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(blockParamsValueProvider); + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + } + + iterator.Index++; + } + + if (iterator.Index == 0 && !enumerated) + { + ifEmpty(context, context.TextWriter, context.Value); + } } - throw new InvalidOperationException("Requested member was not a field or property"); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index d7a1aae1..0fdd85c3 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -1,139 +1,106 @@ -using System; -using System.IO; -using System.Linq.Expressions; -using System.Reflection; - -namespace HandlebarsDotNet.Compiler -{ - internal class PartialBinder : HandlebarsExpressionVisitor - { - private static string SpecialPartialBlockName = "@partial-block"; - - public static Expression Bind(Expression expr, CompilationContext context) - { - return new PartialBinder(context).Visit(expr); - } - - private PartialBinder(CompilationContext context) - : base(context) - { - } - - protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) +using System; +using System.Linq.Expressions; +using Expressions.Shortcuts; + +namespace HandlebarsDotNet.Compiler +{ + internal class PartialBinder : HandlebarsExpressionVisitor + { + private static string SpecialPartialBlockName = "@partial-block"; + + private CompilationContext CompilationContext { get; } + + public PartialBinder(CompilationContext compilationContext) { - return bhex; - } - - protected override Expression VisitStatementExpression(StatementExpression sex) - { - if (sex.Body is PartialExpression) - { - return Visit(sex.Body); - } - else - { - return sex; - } - } - - protected override Expression VisitPartialExpression(PartialExpression pex) - { - Expression bindingContext = CompilationContext.BindingContext; - - var fb = new FunctionBuilder(CompilationContext.Configuration); - var partialBlockTemplate = pex.Fallback == null ? null : fb.Compile(new[] {pex.Fallback}, null, null); - - if (pex.Argument != null || partialBlockTemplate != null) - { - bindingContext = Expression.Call( - bindingContext, - typeof(BindingContext).GetMethod("CreateChildContext"), - pex.Argument ?? Expression.Constant(null), - partialBlockTemplate ?? Expression.Constant(null, typeof(Action))); - } - - var partialInvocation = Expression.Call( -#if netstandard - new Action(InvokePartialWithFallback).GetMethodInfo(), -#else - new Action(InvokePartialWithFallback).Method, -#endif - Expression.Convert(pex.PartialName, typeof(string)), - bindingContext, - Expression.Constant(CompilationContext.Configuration)); - - return partialInvocation; - } - - private static void InvokePartialWithFallback( - string partialName, - BindingContext context, - HandlebarsConfiguration configuration) - { - if (!InvokePartial(partialName, context, configuration)) - { + CompilationContext = compilationContext; + } + + protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) => bhex; + + protected override Expression VisitStatementExpression(StatementExpression sex) => sex.Body is PartialExpression ? Visit(sex.Body) : sex; + + protected override Expression VisitPartialExpression(PartialExpression pex) + { + var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var partialBlockTemplate = pex.Fallback != null + ? FunctionBuilder.CompileCore(new[] { pex.Fallback }, null, CompilationContext.Configuration) + : null; + + if (pex.Argument != null || partialBlockTemplate != null) + { + var value = ExpressionShortcuts.Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext)); + var partialTemplate = ExpressionShortcuts.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, configuration) + ); + } + + private static void InvokePartialWithFallback( + string partialName, + BindingContext context, + HandlebarsConfiguration configuration) + { + if (InvokePartial(partialName, context, 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); + return; + } + + context.PartialBlockTemplate(context.TextWriter, context); + } + + private static bool InvokePartial( + string partialName, + BindingContext context, + HandlebarsConfiguration configuration) + { + if (partialName.Equals(SpecialPartialBlockName)) + { if (context.PartialBlockTemplate == null) - { - if (configuration.MissingPartialTemplateHandler != null) - { - configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); - return; - } - else - { - throw new HandlebarsRuntimeException(string.Format("Referenced partial name {0} could not be resolved", partialName)); - } - } - - context.PartialBlockTemplate(context.TextWriter, context); - } - } - - private static bool InvokePartial( - string partialName, - BindingContext context, - HandlebarsConfiguration configuration) - { - if (partialName.Equals(SpecialPartialBlockName)) - { - if (context.PartialBlockTemplate == null) - { - return false; - } - - context.PartialBlockTemplate(context.TextWriter, context); - return true; - } - - //if we have an inline partial, skip the file system and RegisteredTemplates collection - if (context.InlinePartialTemplates.ContainsKey(partialName)) - { - context.InlinePartialTemplates[partialName](context.TextWriter, context); - return true; - } - - // Partial is not found, so call the resolver and attempt to load it. - if (configuration.RegisteredTemplates.ContainsKey(partialName) == false) - { + { + return false; + } + + context.PartialBlockTemplate(context.TextWriter, context); + return true; + } + + //if we have an inline partial, skip the file system and RegisteredTemplates collection + if (context.InlinePartialTemplates.ContainsKey(partialName)) + { + context.InlinePartialTemplates[partialName](context.TextWriter, context); + return true; + } + + // Partial is not found, so call the resolver and attempt to load it. + if (!configuration.RegisteredTemplates.ContainsKey(partialName)) + { if (configuration.PartialTemplateResolver == null - || configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath) == false) + || !configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath)) { // Template not found. return false; - } - } - - try - { - configuration.RegisteredTemplates[partialName](context.TextWriter, context); - return true; - } - catch (Exception exception) - { - throw new HandlebarsRuntimeException( - $"Runtime error while rendering partial '{partialName}', see inner exception for more information", - exception); - } - } - } -} + } + } + + try + { + configuration.RegisteredTemplates[partialName](context.TextWriter, context); + return true; + } + catch (Exception exception) + { + throw new HandlebarsRuntimeException($"Runtime error while rendering partial '{partialName}', see inner exception for more information", exception); + } + } + } +} diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 41a6c893..ce0fb05b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,392 +1,52 @@ -using System; -using System.Collections; -using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using System.IO; -using System.Dynamic; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Globalization; +using Expressions.Shortcuts; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Polyfills; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class PathBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new PathBinder(context).Visit(expr); - } - - private PathBinder(CompilationContext context) - : base(context) - { - } + private CompilationContext CompilationContext { get; } - protected override Expression VisitStatementExpression(StatementExpression sex) + public PathBinder(CompilationContext compilationContext) { - if (sex.Body is PathExpression) - { -#if netstandard - var writeMethod = typeof(TextWriter).GetRuntimeMethod("Write", new [] { typeof(object) }); -#else - var writeMethod = typeof(TextWriter).GetMethod("Write", new[] { typeof(object) }); -#endif - return Expression.Call( - Expression.Property( - CompilationContext.BindingContext, - "TextWriter"), - writeMethod, Visit(sex.Body)); - } - else - { - return Visit(sex.Body); - } + CompilationContext = compilationContext; } - - protected override Expression VisitPathExpression(PathExpression pex) - { - return Expression.Call( - Expression.Constant(this), -#if netstandard - new Func(ResolvePath).GetMethodInfo(), -#else - new Func(ResolvePath).Method, -#endif - CompilationContext.BindingContext, - Expression.Constant(pex.Path)); - } - - //TODO: make path resolution logic smarter - private object ResolvePath(BindingContext context, string path) - { - if (path == "null") - return null; - - var containsVariable = path.StartsWith("@"); - if (containsVariable) - { - path = path.Substring(1); - if (path.Contains("..")) - { - context = context.ParentContext; - } - } - - var instance = context.Value; - var hashParameters = instance as HashParameterDictionary; - - foreach (var segment in path.Split('/')) - { - if (segment == "..") - { - context = context.ParentContext; - if (context == null) - { - if (containsVariable) return string.Empty; - - throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); - } - instance = context.Value; - } - else - { - var segmentString = containsVariable ? "@" + segment : segment; - var insideEscapeBlock = false; - var pathChain = segmentString.Split('.').Aggregate(new List(), (list, next) => - { - if (insideEscapeBlock) - { - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list[list.Count - 1] = list[list.Count - 1] + "." + next; - return list; - } - else - { - if (next.StartsWith("[")) - { - insideEscapeBlock = true; - } - - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list.Add(next); - return list; - } - }); - - foreach (var memberName in pathChain) - { - instance = ResolveValue(context, instance, memberName); - - if (!(instance is UndefinedBindingResult)) - continue; - - if (hashParameters == null || hashParameters.ContainsKey(memberName) || context.ParentContext == null) - { - if (CompilationContext.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(path, (instance as UndefinedBindingResult).Value); - return instance; - } - - instance = ResolveValue(context.ParentContext, context.ParentContext.Value, memberName); - if (instance is UndefinedBindingResult) - { - if (CompilationContext.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(path, (instance as UndefinedBindingResult).Value); - return instance; - } - } - } - } - return instance; - } - - private object ResolveValue(BindingContext context, object instance, string segment) - { - object resolvedValue = new UndefinedBindingResult(segment, CompilationContext.Configuration); - if (segment.StartsWith("@")) - { - var contextValue = context.GetContextVariable(segment.Substring(1)); - if (contextValue != null) - { - resolvedValue = contextValue; - } - } - else if (segment == "this" || segment == string.Empty) - { - resolvedValue = instance; - } - else - { - resolvedValue = AccessMember(instance, segment); - } - return resolvedValue; - } - - private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.None); - private object AccessMember(object instance, string memberName) + protected override Expression VisitStatementExpression(StatementExpression sex) { - if (instance == null) - return new UndefinedBindingResult(memberName, CompilationContext.Configuration); - - var resolvedMemberName = ResolveMemberName(instance, memberName); - var instanceType = instance.GetType(); - - // Give preference to a string index getter if one exists - var stringIndexPropertyGetter = GetStringIndexPropertyGetter(instanceType); - if (stringIndexPropertyGetter != null) - { - string key = TrimSquareBrackets(resolvedMemberName); // Ensure square brackets removed - try - { - return stringIndexPropertyGetter.Invoke(instance, new object[] {key}); - } - catch - { - return new UndefinedBindingResult(key, CompilationContext.Configuration); - } - } - - var enumerable = instance as IEnumerable; - if (enumerable != null) - { - var match = IndexRegex.Match(memberName); - if (match.Success) - { - int index; - if (match.Groups["index"].Success == false || - int.TryParse(match.Groups["index"].Value, out index) == false) - { - return new UndefinedBindingResult(memberName, CompilationContext.Configuration); - } - - var result = enumerable.ElementAtOrDefault(index); - - return result ?? new UndefinedBindingResult(memberName, CompilationContext.Configuration); - } - } + if (!(sex.Body is PathExpression)) return Visit(sex.Body); - //crude handling for dynamic objects that don't have metadata - if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(instanceType)) - { - try - { - var key = TrimSquareBrackets(resolvedMemberName); // Ensure square brackets removed - var result = GetProperty(instance, key); - if (result == null) - return new UndefinedBindingResult(key, CompilationContext.Configuration); - - return result; - } - catch - { - return new UndefinedBindingResult(resolvedMemberName, CompilationContext.Configuration); - } - } - - - // Check if the instance is IDictionary<,> - var iDictInstance = FirstGenericDictionaryTypeInstance(instanceType); - if (iDictInstance != null) - { - var genericArgs = iDictInstance.GetGenericArguments(); - object key = TrimSquareBrackets(resolvedMemberName); // Ensure square brackets removed - if (genericArgs.Length > 0) - { - // Dictionary key type isn't a string, so attempt to convert. - if (genericArgs[0] != typeof(string)) - { - try - { - key = Convert.ChangeType(key, genericArgs[0], CultureInfo.CurrentCulture); - } - catch (Exception) - { - // Can't convert to key type. - return new UndefinedBindingResult(resolvedMemberName, CompilationContext.Configuration); - } - } - } - - var containsKeyMethod = GetDictionaryMethod(instanceType, "ContainsKey"); - - if (containsKeyMethod == null) - { - throw new MethodAccessException("Method ContainsKey not found"); - } - - if ((bool)containsKeyMethod.Invoke(instance, new[] { key })) - { - var itemProperty = GetDictionaryMethod(instanceType, "get_Item"); - if (itemProperty == null) - { - throw new MethodAccessException("Property Item not found"); - } - - return itemProperty.Invoke(instance, new[] { key }); - } - else - { - // Key doesn't exist. - return new UndefinedBindingResult(resolvedMemberName, CompilationContext.Configuration); - } - } - // Check if the instance is IDictionary (ie, System.Collections.Hashtable) - if (typeof(IDictionary).IsAssignableFrom(instanceType)) - { - var key = TrimSquareBrackets(resolvedMemberName); // Ensure square brackets removed - // Only string keys supported - indexer takes an object, but no nice - // way to check if the hashtable check if it should be a different type. - return ((IDictionary)instance)[key]; - } - - var members = instanceType.GetMember(resolvedMemberName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - MemberInfo preferredMember; - if (members.Length == 0) - { - return new UndefinedBindingResult(resolvedMemberName, CompilationContext.Configuration); - } - else if (members.Length > 1) - { - preferredMember = members.FirstOrDefault(m => m.Name == resolvedMemberName) ?? members[0]; - } - else - { - preferredMember = members[0]; - } - - var propertyInfo = preferredMember as PropertyInfo; - if (propertyInfo != null) - { - var propertyValue = propertyInfo.GetValue(instance, null); - return propertyValue; - } - if (preferredMember is FieldInfo) - { - var fieldValue = ((FieldInfo)preferredMember).GetValue(instance); - return fieldValue; - } - return new UndefinedBindingResult(resolvedMemberName, CompilationContext.Configuration); + var context = Arg(CompilationContext.BindingContext); + var value = Arg(Visit(sex.Body)); + return context.Call(o => o.TextWriter.Write(value)); } - //Only trim a single layer of brackets. - private static string TrimSquareBrackets(string key) - { - if (key.StartsWith("[") && key.EndsWith("]")) - { - return key.Substring(1, key.Length - 2); - } - - return key; - } - - private static MethodInfo GetStringIndexPropertyGetter(Type type) + protected override Expression VisitPathExpression(PathExpression pex) { - return type - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(prop => prop.Name == "Item" && prop.CanRead) - .SingleOrDefault(prop => - { - var indexParams = prop.GetIndexParameters(); - if (indexParams.Length == 1 && indexParams.Single().ParameterType == typeof(string)) - { - return true; - } + var context = Arg(CompilationContext.BindingContext); + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(pex.Path); - return false; - })?.GetMethod; - } + var resolvePath = Call(() => PathResolver.ResolvePath(context, ref pathInfo)); + if (pex.Context == PathExpression.ResolutionContext.Parameter) return resolvePath; + if (!pathInfo.IsValidHelperLiteral && !CompilationContext.Configuration.Compatibility.RelaxedHelperNaming || pathInfo.IsThis) return resolvePath; - private static MethodInfo GetDictionaryMethod(Type instanceType, string methodName) - { - var methodInfo = instanceType.GetMethod(methodName); - - if (methodInfo == null) - { - // Support implicit interface impl. - methodInfo = instanceType.GetTypeInfo().DeclaredMethods.FirstOrDefault(m => m.IsPrivate && m.Name.StartsWith("System.Collections.Generic.IDictionary") && m.Name.EndsWith(methodName)); - } - - return methodInfo; - } + var helperName = pathInfo.TrimmedPath; + var tryBoundHelper = Call(() => + HelperFunctionBinder.TryLateBindHelperExpression(context, helperName, ArrayEx.Empty()) + ); - static Type FirstGenericDictionaryTypeInstance(Type instanceType) - { - return instanceType.GetInterfaces() - .FirstOrDefault(i => -#if netstandard - i.GetTypeInfo().IsGenericType -#else - i.IsGenericType -#endif - && - ( - i.GetGenericTypeDefinition() == typeof(IDictionary<,>) - ) + return Block() + .Parameter(out var result, tryBoundHelper) + .Line(Condition() + .If(result.Member(o => o.Success)) + .Then(result.Member(o => o.Value)) + .Else(resolvePath) ); } - - private static object GetProperty(object target, string name) - { - var site = System.Runtime.CompilerServices.CallSite>.Create(Microsoft.CSharp.RuntimeBinder.Binder.GetMember(0, name, target.GetType(), new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(0, null) })); - return site.Target(site, target); - } - - private string ResolveMemberName(object instance, string memberName) - { - var resolver = CompilationContext.Configuration.ExpressionNameResolver; - return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; - } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs index a8a07ac8..c9c0155d 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -1,31 +1,23 @@ -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; +using System.Linq.Expressions; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { internal class StaticReplacer : HandlebarsExpressionVisitor { - public static Expression Replace(Expression expr, CompilationContext context) - { - return new StaticReplacer(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private StaticReplacer(CompilationContext context) - : base(context) + public StaticReplacer(CompilationContext compilationContext) { + CompilationContext = compilationContext; } - + protected override Expression VisitStaticExpression(StaticExpression stex) { - var encodedTextWriter = Expression.Property(CompilationContext.BindingContext, "TextWriter"); -#if netstandard - var writeMethod = typeof(EncodedTextWriter).GetRuntimeMethod("Write", new [] { typeof(string), typeof(bool) }); -#else - var writeMethod = typeof(EncodedTextWriter).GetMethod("Write", new [] { typeof(string), typeof(bool) }); -#endif - - return Expression.Call(encodedTextWriter, writeMethod, Expression.Constant(stex.Value), Expression.Constant(false)); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var value = ExpressionShortcuts.Arg(stex.Value); + + return context.Call(o => o.TextWriter.Write(value, false)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs old mode 100755 new mode 100644 index b7e6ac88..af51405b --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,84 +1,109 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.IO; -using System.Text; +using System.Linq; using System.Reflection; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { + // will be significantly improved in next iterations internal class SubExpressionVisitor : HandlebarsExpressionVisitor { - public static Expression Visit(Expression expr, CompilationContext context) - { - return new SubExpressionVisitor(context).Visit(expr); - } + private readonly IExpressionCompiler _expressionCompiler; + private CompilationContext CompilationContext { get; } - private SubExpressionVisitor(CompilationContext context) - : base(context) + public SubExpressionVisitor(CompilationContext compilationContext) { + CompilationContext = compilationContext; + + _expressionCompiler = CompilationContext.Configuration.CompileTimeConfiguration.ExpressionCompiler; } - + protected override Expression VisitSubExpression(SubExpressionExpression subex) { - var helperCall = subex.Expression as MethodCallExpression; - if (helperCall == null) + switch (subex.Expression) { - throw new HandlebarsCompilerException("Sub-expression does not contain a converted MethodCall expression"); + case InvocationExpression invocationExpression: + return HandleInvocationExpression(invocationExpression); + + case MethodCallExpression callExpression: + return HandleMethodCallExpression(callExpression); + + default: + var expression = FunctionBuilder.Reduce(subex.Expression, CompilationContext); + if (expression is MethodCallExpression lateBoundCall) + return HandleMethodCallExpression(lateBoundCall); + + throw new HandlebarsCompilerException("Sub-expression does not contain a converted MethodCall expression"); } - HandlebarsHelper helper = GetHelperDelegateFromMethodCallExpression(helperCall); - return Expression.Call( -#if netstandard - new Func(CaptureTextWriterOutputFromHelper).GetMethodInfo(), -#else - new Func(CaptureTextWriterOutputFromHelper).Method, -#endif - Expression.Constant(helper), - Visit(helperCall.Arguments[1]), - Visit(helperCall.Arguments[2])); } - private static HandlebarsHelper GetHelperDelegateFromMethodCallExpression(MethodCallExpression helperCall) + private Expression HandleMethodCallExpression(MethodCallExpression helperCall) { - object target = helperCall.Object; - HandlebarsHelper helper; - if (target != null) + if (helperCall.Type != typeof(void)) { - if (target is ConstantExpression) - { - target = ((ConstantExpression)target).Value; - } - else - { - throw new NotSupportedException("Helper method instance target must be reduced to a ConstantExpression"); - } -#if netstandard - helper = (HandlebarsHelper)helperCall.Method.CreateDelegate(typeof(HandlebarsHelper), target); -#else - helper = (HandlebarsHelper)Delegate.CreateDelegate(typeof(HandlebarsHelper), target, helperCall.Method); -#endif + return helperCall.Update(helperCall.Object, + ReplaceValuesOf(helperCall.Arguments, ExpressionShortcuts.Null()).Select(Visit) + ); } - else - { -#if netstandard - helper = (HandlebarsHelper)helperCall.Method.CreateDelegate(typeof(HandlebarsHelper)); -#else - helper = (HandlebarsHelper)Delegate.CreateDelegate(typeof(HandlebarsHelper), helperCall.Method); -#endif - } - return helper; + + var context = ExpressionShortcuts.Var(); + var writer = ExpressionShortcuts.Var(); + helperCall = helperCall.Update(ExpressionUtils.ReplaceParameters(helperCall.Object, context), + ExpressionUtils.ReplaceParameters( + ReplaceValuesOf(helperCall.Arguments, writer), new Expression[] { context } + ).Select(Visit) + ); + + var formatProvider = ExpressionShortcuts.Arg(CompilationContext.Configuration.FormatProvider); + var block = ExpressionShortcuts.Block() + .Parameter(writer, ExpressionShortcuts.New(() => new PolledStringWriter((IFormatProvider) formatProvider))) + .Line(writer.Using((o, body) => + body.Line(helperCall) + .Line(o.Call(x => (object) x.ToString())) + )); + + var continuation = _expressionCompiler.Compile(Expression.Lambda>(block, (ParameterExpression) context)); + return ExpressionShortcuts.Arg(Expression.Invoke(Expression.Constant(continuation), CompilationContext.BindingContext)); + } + + private static IEnumerable ReplaceValuesOf(IEnumerable instance, Expression newValue) + { + var targetType = typeof(T); + return instance.Select(value => targetType.IsAssignableFrom(value.Type) + ? newValue + : value); } - private static string CaptureTextWriterOutputFromHelper( - HandlebarsHelper helper, - object context, - object[] arguments) + private Expression HandleInvocationExpression(InvocationExpression invocation) { - var builder = new StringBuilder(); - using (var writer = new StringWriter(builder)) + if (invocation.Type != typeof(void)) { - helper(writer, context, arguments); + return invocation.Update(invocation.Expression, + ReplaceValuesOf(invocation.Arguments, ExpressionShortcuts.Null()).Select(Visit) + ); } - return builder.ToString(); + + var context = ExpressionShortcuts.Var(); + var writer = ExpressionShortcuts.Var(); + invocation = invocation.Update(ExpressionUtils.ReplaceParameters(invocation.Expression, context), + ExpressionUtils.ReplaceParameters( + ReplaceValuesOf(invocation.Arguments, writer), new Expression[]{ context } + ).Select(Visit) + ); + + var formatProvider = ExpressionShortcuts.Arg(CompilationContext.Configuration.FormatProvider); + var block = ExpressionShortcuts.Block() + .Parameter(writer, ExpressionShortcuts.New(() => new PolledStringWriter((IFormatProvider) formatProvider))) + .Line(writer.Using((o, body) => + body.Line(invocation) + .Line(o.Call(x => (object) x.ToString())) + )); + + var continuation = _expressionCompiler.Compile(Expression.Lambda>(block, (ParameterExpression) context)); + return ExpressionShortcuts.Arg(Expression.Invoke(Expression.Constant(continuation), CompilationContext.BindingContext)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs index 9dd75438..a247d0c3 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -1,39 +1,32 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { internal class UnencodedStatementVisitor : HandlebarsExpressionVisitor { - public static Expression Visit(Expression expr, CompilationContext context) - { - return new UnencodedStatementVisitor(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private UnencodedStatementVisitor(CompilationContext context) - : base(context) + public UnencodedStatementVisitor(CompilationContext compilationContext) { + CompilationContext = compilationContext; } protected override Expression VisitStatementExpression(StatementExpression sex) { - if (sex.IsEscaped == false) + if (!sex.IsEscaped) { - return Expression.Block( - typeof(void), - Expression.Assign( - Expression.Property(CompilationContext.BindingContext, "SuppressEncoding"), - Expression.Constant(true)), - sex, - Expression.Assign( - Expression.Property(CompilationContext.BindingContext, "SuppressEncoding"), - Expression.Constant(false)), - Expression.Empty()); - } - else - { - return sex; + var context = Arg(CompilationContext.BindingContext); + var suppressEncoding = context.Property(o => o.SuppressEncoding); + + return Block(typeof(void)) + .Line(suppressEncoding.Assign(true)) + .Line(sex) + .Line(suppressEncoding.Assign(false)) + .Line(Expression.Empty()); } + + return sex; } } } diff --git a/source/Handlebars/Compiler/Translation/ResultHolder.cs b/source/Handlebars/Compiler/Translation/ResultHolder.cs new file mode 100644 index 00000000..aa805d90 --- /dev/null +++ b/source/Handlebars/Compiler/Translation/ResultHolder.cs @@ -0,0 +1,15 @@ +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/Compatibility.cs b/source/Handlebars/Configuration/Compatibility.cs new file mode 100644 index 00000000..0d2f1519 --- /dev/null +++ b/source/Handlebars/Configuration/Compatibility.cs @@ -0,0 +1,20 @@ +namespace HandlebarsDotNet +{ + /// + /// Contains feature flags that breaks compatibility with Handlebarsjs. + /// + public class Compatibility + { + /// + /// If enables support for @last in object properties iterations. Not supported in Handlebarsjs. + /// + public bool SupportLastInObjectIterations { get; set; } = false; + + /// + /// If enables support for Handlebars.Net helper naming rules. + /// This enables helper names to be not-valid Handlebars identifiers (e.g. {{ one.two }}) + /// Such naming is not supported in Handlebarsjs and would break compatibility. + /// + public bool RelaxedHelperNaming { get; set; } = false; + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/CompileTimeConfiguration.cs b/source/Handlebars/Configuration/CompileTimeConfiguration.cs new file mode 100644 index 00000000..7d458455 --- /dev/null +++ b/source/Handlebars/Configuration/CompileTimeConfiguration.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using HandlebarsDotNet.Features; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet +{ + /// + /// Contains compile-time affective configuration. Changing values after template compilation would take no affect. + /// + public class CompileTimeConfiguration + { + /// + public IList ObjectDescriptorProviders { get; } = new List(); + + + public IList ExpressionMiddleware { get; internal set; } = new List(); + + /// + public IList Features { get; internal set; } = new List + { + new BuildInHelpersFeatureFactory(), + new ClosureFeatureFactory(), + new DefaultCompilerFeatureFactory(), + new MissingHelperFeatureFactory() + }; + + /// + public IList AliasProviders { get; internal set; } = new List(); + + /// + /// Defines whether Handlebars uses aggressive caching to achieve better performance. by default. + /// + public bool UseAggressiveCaching { get; set; } = true; + + /// + /// The compiler used to compile + /// + public IExpressionCompiler ExpressionCompiler { get; set; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs new file mode 100644 index 00000000..9dcf9836 --- /dev/null +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -0,0 +1,75 @@ +using HandlebarsDotNet.Compiler.Resolvers; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using HandlebarsDotNet.Helpers; + +namespace HandlebarsDotNet +{ + + public class HandlebarsConfiguration + { + + public IDictionary Helpers { get; protected set; } + + + public IDictionary ReturnHelpers { get; protected set; } + + + public IDictionary BlockHelpers { get; protected set; } + + + public IDictionary> RegisteredTemplates { get; protected set; } + + /// + public ICollection HelperResolvers { get; protected set; } + + + public virtual IExpressionNameResolver ExpressionNameResolver { get; set; } + + + public virtual ITextEncoder TextEncoder { get; set; } = new HtmlEncoder(); + + /// + public virtual IFormatProvider FormatProvider { get; set; } = CultureInfo.CurrentCulture; + + + public virtual ViewEngineFileSystem FileSystem { get; set; } + + + public virtual string UnresolvedBindingFormatter { get; set; } + + + public virtual bool ThrowOnUnresolvedBindingExpression { get; set; } + + /// + /// The resolver used for unregistered partials. Defaults + /// to the . + /// + public virtual IPartialTemplateResolver PartialTemplateResolver { get; set; } = new FileSystemPartialTemplateResolver(); + + /// + /// The handler called when a partial template cannot be found. + /// + public virtual IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; } + + /// + public virtual Compatibility Compatibility { get; set; } = new Compatibility(); + + /// + public virtual CompileTimeConfiguration CompileTimeConfiguration { get; } = new CompileTimeConfiguration(); + + + public HandlebarsConfiguration() + { + Helpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + ReturnHelpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + BlockHelpers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + RegisteredTemplates = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + HelperResolvers = new List(); + } + } +} + diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs new file mode 100644 index 00000000..a889ae11 --- /dev/null +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using HandlebarsDotNet.Compiler.Resolvers; +using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet +{ + + public interface ICompiledHandlebarsConfiguration + { + + IExpressionNameResolver ExpressionNameResolver { get; } + + + ITextEncoder TextEncoder { get; } + + + IFormatProvider FormatProvider { get; } + + + ViewEngineFileSystem FileSystem { get; } + + + string UnresolvedBindingFormatter { get; } + + + bool ThrowOnUnresolvedBindingExpression { get; } + + + IPartialTemplateResolver PartialTemplateResolver { get; } + + + IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; } + + + IDictionary Helpers { get; } + + + IDictionary ReturnHelpers { get; } + + + IDictionary BlockHelpers { get; } + + + ICollection HelperResolvers { get; } + + + IDictionary> RegisteredTemplates { get; } + + /// + Compatibility Compatibility { get; } + + /// + IObjectDescriptorProvider ObjectDescriptorProvider { get; } + + /// + ICollection ExpressionMiddleware { get; } + + /// + ICollection AliasProviders { get; } + + /// + IExpressionCompiler ExpressionCompiler { get; set; } + + /// + bool UseAggressiveCaching { get; set; } + + /// + /// List of associated s + /// + IReadOnlyList Features { get; } + + /// + IReadOnlyPathInfoStore PathInfoStore { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs new file mode 100644 index 00000000..4f79b057 --- /dev/null +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Resolvers; +using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet +{ + internal sealed class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompiledHandlebarsConfiguration + { + private readonly HandlebarsConfiguration _configuration; + + public override IExpressionNameResolver ExpressionNameResolver => _configuration.ExpressionNameResolver; + public override ITextEncoder TextEncoder => _configuration.TextEncoder; + public override IFormatProvider FormatProvider => _configuration.FormatProvider; + public override ViewEngineFileSystem FileSystem => _configuration.FileSystem; + public override string UnresolvedBindingFormatter => _configuration.UnresolvedBindingFormatter; + public override bool ThrowOnUnresolvedBindingExpression => _configuration.ThrowOnUnresolvedBindingExpression; + public override IPartialTemplateResolver PartialTemplateResolver => _configuration.PartialTemplateResolver; + public override IMissingPartialTemplateHandler MissingPartialTemplateHandler => _configuration.MissingPartialTemplateHandler; + public override Compatibility Compatibility => _configuration.Compatibility; + public override CompileTimeConfiguration CompileTimeConfiguration { get; } + + public List Features { get; } + public IObjectDescriptorProvider ObjectDescriptorProvider { get; } + public ICollection ExpressionMiddleware => CompileTimeConfiguration.ExpressionMiddleware; + public ICollection AliasProviders => CompileTimeConfiguration.AliasProviders; + public IExpressionCompiler ExpressionCompiler + { + get => CompileTimeConfiguration.ExpressionCompiler; + set => CompileTimeConfiguration.ExpressionCompiler = value; + } + + public bool UseAggressiveCaching + { + get => CompileTimeConfiguration.UseAggressiveCaching; + set => CompileTimeConfiguration.UseAggressiveCaching = value; + } + IReadOnlyList ICompiledHandlebarsConfiguration.Features => Features; + + public PathInfoStore PathInfoStore { get; } + + IReadOnlyPathInfoStore ICompiledHandlebarsConfiguration.PathInfoStore => PathInfoStore; + + internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) + { + _configuration = configuration; + + Helpers = new CascadeDictionary(configuration.Helpers, StringComparer.OrdinalIgnoreCase); + ReturnHelpers = new CascadeDictionary(configuration.ReturnHelpers, StringComparer.OrdinalIgnoreCase); + BlockHelpers = new CascadeDictionary(configuration.BlockHelpers, StringComparer.OrdinalIgnoreCase); + RegisteredTemplates = new CascadeDictionary>(configuration.RegisteredTemplates, StringComparer.OrdinalIgnoreCase); + HelperResolvers = new CascadeCollection(configuration.HelperResolvers); + PathInfoStore = new PathInfoStore(); + + CompileTimeConfiguration = new CompileTimeConfiguration + { + UseAggressiveCaching = _configuration.CompileTimeConfiguration.UseAggressiveCaching, + ExpressionCompiler = _configuration.CompileTimeConfiguration.ExpressionCompiler, + ExpressionMiddleware = new List(configuration.CompileTimeConfiguration.ExpressionMiddleware), + Features = new List(configuration.CompileTimeConfiguration.Features), + AliasProviders = new List(configuration.CompileTimeConfiguration.AliasProviders) + { + new CollectionMemberAliasProvider(this) // will not be registered by default in next iterations + } + }; + + var objectDescriptorProvider = new ObjectDescriptorProvider(this); + var listObjectDescriptor = new CollectionObjectDescriptor(objectDescriptorProvider); + var providers = new List(configuration.CompileTimeConfiguration.ObjectDescriptorProviders) + { + new ContextObjectDescriptor(), + new StringDictionaryObjectDescriptorProvider(), + new GenericDictionaryObjectDescriptorProvider(), + new DictionaryObjectDescriptor(), + listObjectDescriptor, + new EnumerableObjectDescriptor(listObjectDescriptor), + new KeyValuePairObjectDescriptorProvider(), + objectDescriptorProvider, + new DynamicObjectDescriptor() + }; + + ObjectDescriptorProvider = new ObjectDescriptorFactory(providers); + + Features = CompileTimeConfiguration + .Features.Select(o => o.CreateFeature()) + .OrderBy(o => o.GetType().GetTypeInfo().GetCustomAttribute()?.Order ?? 100) + .ToList(); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/PathInfoStore.cs b/source/Handlebars/Configuration/PathInfoStore.cs new file mode 100644 index 00000000..ce2aeed0 --- /dev/null +++ b/source/Handlebars/Configuration/PathInfoStore.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet +{ + /// + /// Provides access to path expressions in the template + /// + public interface IReadOnlyPathInfoStore : IReadOnlyDictionary + { + } + + internal class PathInfoStore : IReadOnlyPathInfoStore + { + private readonly Dictionary _paths = new Dictionary(); + + public PathInfo GetOrAdd(string path) + { + if (_paths.TryGetValue(path, out var pathInfo)) return pathInfo; + + pathInfo = PathResolver.GetPathInfo(path); + _paths.Add(path, pathInfo); + + return pathInfo; + } + + public IEnumerator> GetEnumerator() => _paths.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _paths.GetEnumerator(); + + int IReadOnlyCollection>.Count => _paths.Count; + + bool IReadOnlyDictionary.ContainsKey(string key) => _paths.ContainsKey(key); + + public bool TryGetValue(string key, out PathInfo value) => _paths.TryGetValue(key, out value); + + public PathInfo this[string key] => _paths[key]; + + public IEnumerable Keys => _paths.Keys; + + public IEnumerable Values => _paths.Values; + } +} \ No newline at end of file diff --git a/source/Handlebars/DescriptionAttribute.cs b/source/Handlebars/DescriptionAttribute.cs deleted file mode 100644 index 69e77689..00000000 --- a/source/Handlebars/DescriptionAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace HandlebarsDotNet -{ - public class DescriptionAttribute : Attribute - { - public string Description { get; set; } - - public DescriptionAttribute(string description) - { - Description = description; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index a161d0e4..82419f04 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -1,9 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.IO; namespace HandlebarsDotNet { @@ -12,21 +8,28 @@ internal static class EnumerableExtensions public static bool IsOneOf(this IEnumerable source) where TExpected : TSource { - var enumerator = source.GetEnumerator(); + using var enumerator = source.GetEnumerator(); enumerator.MoveNext(); - return (enumerator.Current is TExpected) && (enumerator.MoveNext() == false); + return enumerator.Current is TExpected && !enumerator.MoveNext(); + } + + public static bool IsMultiple(this IEnumerable source) + { + using var enumerator = source.GetEnumerator(); + var hasNext = enumerator.MoveNext(); + hasNext = hasNext && enumerator.MoveNext(); + return hasNext; } - public static void AddOrUpdate(this IDictionary dictionary, TKey key, TValue value) + public static IEnumerable ApplyOn(this IEnumerable source, Action mutator) + where T: class + where TV : T { - if (dictionary.ContainsKey(key)) - { - dictionary[key] = value; - } - else + foreach (var item in source) { - dictionary.Add(key, value); - } + if(item is TV typed) mutator(typed); + yield return item; + } } } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs new file mode 100644 index 00000000..15e8fa2f --- /dev/null +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -0,0 +1,99 @@ +using System.IO; +using System.Linq; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.Features +{ + internal class BuildInHelpersFeatureFactory : IFeatureFactory + { + public IFeature CreateFeature() + { + return new BuildInHelpersFeature(); + } + } + + [FeatureOrder(int.MinValue)] + internal class BuildInHelpersFeature : IFeature + { + private ICompiledHandlebarsConfiguration _configuration; + + private static readonly ConfigureBlockParams WithBlockParamsConfiguration = (parameters, binder, deps) => + { + binder(parameters.ElementAtOrDefault(0), ctx => ctx, deps[0]); + }; + + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) + { + _configuration = configuration; + + configuration.BlockHelpers["with"] = With; + configuration.BlockHelpers["*inline"] = Inline; + + configuration.ReturnHelpers["lookup"] = Lookup; + } + + public void CompilationCompleted() + { + // noting to do here + } + + private static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{with}} helper must have exactly one argument"); + } + + options.BlockParams(WithBlockParamsConfiguration, arguments[0]); + + if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) + { + options.Template(output, arguments[0]); + } + else + { + options.Inverse(output, context); + } + } + + private object Lookup(dynamic context, params object[] arguments) + { + if (arguments.Length != 2) + { + throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); + } + + var memberName = arguments[1].ToString(); + var segment = new ChainSegment(memberName); + return !PathResolver.TryAccessMember(arguments[0], ref segment, _configuration, out var value) + ? new UndefinedBindingResult(memberName, _configuration) + : value; + } + + private static void Inline(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{*inline}} helper must have exactly one argument"); + } + + //This helper needs the "context" var to be the complete BindingContext as opposed to just the + //data { firstName: "todd" }. The full BindingContext is needed for registering the partial templates. + //This magic happens in BlockHelperFunctionBinder.VisitBlockHelperExpression + + if (!(context is BindingContext)) + { + throw new HandlebarsException("{{*inline}} helper must receiving the full BindingContext"); + } + + var key = arguments[0] as string; + + //Inline partials cannot use the Handlebars.RegisterTemplate method + //because it is static and therefore app-wide. To prevent collisions + //this helper will add the compiled partial to a dicionary + //that is passed around in the context without fear of collisions. + context.InlinePartialTemplates.Add(key, options.Template); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs new file mode 100644 index 00000000..3682da66 --- /dev/null +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -0,0 +1,175 @@ +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(); + + /// + /// 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(); + } + + 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 new file mode 100644 index 00000000..fde35512 --- /dev/null +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; + +namespace HandlebarsDotNet.Features +{ + internal class DefaultCompilerFeatureFactory : IFeatureFactory + { + public IFeature CreateFeature() + { + return new DefaultCompilerFeature(); + } + } + + [FeatureOrder(1)] + internal class DefaultCompilerFeature : IFeature + { + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) + { + var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features + .OfType().SingleOrDefault(); + + configuration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); + } + + public void CompilationCompleted() + { + // noting to do here + } + + private class DefaultExpressionCompiler : IExpressionCompiler + { + private readonly ClosureFeature _closureFeature; + private readonly TemplateClosure _templateClosure; + private readonly ExpressionContainer _closure; + private readonly ICollection _expressionMiddleware; + + public DefaultExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) + { + _closureFeature = closureFeature; + _templateClosure = closureFeature?.TemplateClosure; + _closure = closureFeature?.ClosureInternal; + _expressionMiddleware = configuration.ExpressionMiddleware; + } + + public T Compile(Expression expression) where T: class + { + expression = (Expression) _expressionMiddleware.Aggregate((Expression) expression, (e, m) => m.Invoke(e)); + + if (_closureFeature == null) + { + return expression.Compile(); + } + + 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(); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/FeatureOrderAttribute.cs b/source/Handlebars/Features/FeatureOrderAttribute.cs new file mode 100644 index 00000000..47d0e8f3 --- /dev/null +++ b/source/Handlebars/Features/FeatureOrderAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace HandlebarsDotNet.Features +{ + internal class FeatureOrderAttribute : Attribute + { + public int Order { get; } + + public FeatureOrderAttribute(int order) + { + Order = order; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/IFeature.cs b/source/Handlebars/Features/IFeature.cs new file mode 100644 index 00000000..dee2b762 --- /dev/null +++ b/source/Handlebars/Features/IFeature.cs @@ -0,0 +1,19 @@ +namespace HandlebarsDotNet.Features +{ + /// + /// Feature allows to attach a behaviour on per-template basis by modifying template bound + /// + public interface IFeature + { + /// + /// Executes before any template parsing/compiling activity + /// + /// + void OnCompiling(ICompiledHandlebarsConfiguration configuration); + + /// + /// Executes after template is compiled + /// + void CompilationCompleted(); + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/IFeatureFactory.cs b/source/Handlebars/Features/IFeatureFactory.cs new file mode 100644 index 00000000..02614fc1 --- /dev/null +++ b/source/Handlebars/Features/IFeatureFactory.cs @@ -0,0 +1,12 @@ +namespace HandlebarsDotNet.Features +{ + + public interface IFeatureFactory + { + /// + /// Creates new for each template + /// + /// + IFeature CreateFeature(); + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/MissingHelperFeature.cs b/source/Handlebars/Features/MissingHelperFeature.cs new file mode 100644 index 00000000..7270d478 --- /dev/null +++ b/source/Handlebars/Features/MissingHelperFeature.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using HandlebarsDotNet.Adapters; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers; + +namespace HandlebarsDotNet.Features +{ + public static class MissingHelperFeatureExtension + { + /// + /// Allows to intercept calls to missing helpers. + /// For Handlebarsjs docs see: https://handlebarsjs.com/guide/hooks.html#helpermissing + /// + /// + /// Delegate that returns interceptor for and + /// Delegate that returns interceptor for + /// + public static HandlebarsConfiguration RegisterMissingHelperHook( + this HandlebarsConfiguration configuration, + HandlebarsReturnHelper helperMissing = null, + HandlebarsBlockHelper blockHelperMissing = null + ) + { + var feature = new MissingHelperFeatureFactory(helperMissing, blockHelperMissing); + configuration.CompileTimeConfiguration.Features.Add(feature); + + return configuration; + } + } + + internal class MissingHelperFeatureFactory : IFeatureFactory + { + private readonly HandlebarsReturnHelper _returnHelper; + private readonly HandlebarsBlockHelper _blockHelper; + + public MissingHelperFeatureFactory( + HandlebarsReturnHelper returnHelper = null, + HandlebarsBlockHelper blockHelper = null + ) + { + _returnHelper = returnHelper; + _blockHelper = blockHelper; + } + + public IFeature CreateFeature() => new MissingHelperFeature(_returnHelper, _blockHelper); + } + + [FeatureOrder(int.MaxValue)] + internal class MissingHelperFeature : IFeature, IHelperResolver + { + private ICompiledHandlebarsConfiguration _configuration; + private HandlebarsReturnHelper _returnHelper; + private HandlebarsBlockHelper _blockHelper; + + public MissingHelperFeature( + HandlebarsReturnHelper returnHelper, + HandlebarsBlockHelper blockHelper + ) + { + _returnHelper = returnHelper; + _blockHelper = blockHelper; + } + + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) => _configuration = configuration; + + public void CompilationCompleted() + { + var existingFeatureRegistrations = _configuration + .HelperResolvers + .OfType() + .ToList(); + + if (existingFeatureRegistrations.Any()) + { + existingFeatureRegistrations.ForEach(o => _configuration.HelperResolvers.Remove(o)); + } + + ResolveHelpersRegistrations(); + + _configuration.HelperResolvers.Add(this); + } + + public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) + { + if (_returnHelper == null) + { + _configuration.ReturnHelpers.TryGetValue("helperMissing", out _returnHelper); + } + + if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var simpleHelper)) + { + _returnHelper = new HelperToReturnHelperAdapter(simpleHelper); + } + + if (_returnHelper == null) + { + helper = null; + return false; + } + + helper = (context, arguments) => + { + var instance = (object) context; + var chainSegment = new ChainSegment(name); + if (PathResolver.TryAccessMember(instance, ref chainSegment, _configuration, out var value)) + return value; + + var newArguments = new object[arguments.Length + 1]; + Array.Copy(arguments, newArguments, arguments.Length); + newArguments[arguments.Length] = name; + + return _returnHelper(context, newArguments); + }; + + return true; + } + + public bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper) + { + if (_blockHelper == null) + { + _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out _blockHelper); + } + + if (_blockHelper == null) + { + helper = null; + return false; + } + + helper = (output, options, context, arguments) => + { + options["name"] = name; + _blockHelper(output, options, context, arguments); + }; + + return true; + } + + private void ResolveHelpersRegistrations() + { + if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var helperMissing)) + { + _returnHelper = new HelperToReturnHelperAdapter(helperMissing); + } + + if (_returnHelper == null && + _configuration.ReturnHelpers.TryGetValue("helperMissing", out var returnHelperMissing)) + { + _returnHelper = returnHelperMissing; + } + + if (_blockHelper == null && + _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out var blockHelperMissing)) + { + _blockHelper = blockHelperMissing; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/TemplateClosure.cs b/source/Handlebars/Features/TemplateClosure.cs new file mode 100644 index 00000000..430b0e80 --- /dev/null +++ b/source/Handlebars/Features/TemplateClosure.cs @@ -0,0 +1,52 @@ +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/Features/WarmUpFeature.cs b/source/Handlebars/Features/WarmUpFeature.cs new file mode 100644 index 00000000..05b624ca --- /dev/null +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Features +{ + public static class WarmUpFeatureExtensions + { + /// + /// Allows to warm-up internal caches for specific types + /// + public static HandlebarsConfiguration UseWarmUp(this HandlebarsConfiguration configuration, Action> configure) + { + var types = new HashSet(); + + configure(types); + + configuration.CompileTimeConfiguration.Features.Add(new WarmUpFeatureFactory(types)); + + return configuration; + } + } + + internal class WarmUpFeature : IFeature + { + private readonly HashSet _types; + + public WarmUpFeature(HashSet types) => _types = types; + + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) + { + var descriptorProvider = configuration.ObjectDescriptorProvider; + foreach (var type in _types) + { + if(!descriptorProvider.CanHandleType(type)) continue; + descriptorProvider.TryGetDescriptor(type, out _); + } + } + + public void CompilationCompleted() + { + // noting to do here + } + } + + internal class WarmUpFeatureFactory : IFeatureFactory + { + private readonly HashSet _types; + + public WarmUpFeatureFactory(HashSet types) => _types = types; + + public IFeature CreateFeature() => new WarmUpFeature(_types); + } +} \ No newline at end of file diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index e59108ce..ab2a4e0d 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -3,27 +3,54 @@ namespace HandlebarsDotNet { + /// + /// InlineHelper: {{#helper arg1 arg2}} + /// + /// + /// + /// public delegate void HandlebarsHelper(TextWriter output, dynamic context, params object[] arguments); + + /// + /// InlineHelper: {{#helper arg1 arg2}}, supports value return + /// + /// + /// + public delegate object HandlebarsReturnHelper(dynamic context, params object[] arguments); + + /// + /// BlockHelper: {{#helper}}..{{/helper}} + /// + /// + /// + /// + /// public delegate void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments); - public sealed partial class Handlebars + + public sealed class Handlebars { // Lazy-load Handlebars environment to ensure thread safety. See Jon Skeet's excellent article on this for more info. http://csharpindepth.com/Articles/General/Singleton.aspx - private static readonly Lazy lazy = new Lazy(() => new HandlebarsEnvironment(new HandlebarsConfiguration())); + private static readonly Lazy Lazy = new Lazy(() => new HandlebarsEnvironment(new HandlebarsConfiguration())); - private static IHandlebars Instance { get { return lazy.Value; } } + private static IHandlebars Instance => Lazy.Value; + /// + /// Creates standalone instance of environment + /// + /// + /// public static IHandlebars Create(HandlebarsConfiguration configuration = null) { configuration = configuration ?? new HandlebarsConfiguration(); return new HandlebarsEnvironment(configuration); } - + public static Action Compile(TextReader template) { return Instance.Compile(template); } - + public static Func Compile(string template) { return Instance.Compile(template); @@ -33,33 +60,55 @@ public static IHandlebars Create(HandlebarsConfiguration configuration = null) { return Instance.CompileView(templatePath); } - + + public static Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + { + return Instance.CompileView(templatePath, readerFactoryFactory); + } + public static void RegisterTemplate(string templateName, Action template) { Instance.RegisterTemplate(templateName, template); } - + public static void RegisterTemplate(string templateName, string template) { Instance.RegisterTemplate(templateName, template); } + /// + /// Registers new + /// + /// + /// public static void RegisterHelper(string helperName, HandlebarsHelper helperFunction) { Instance.RegisterHelper(helperName, helperFunction); } - - public static void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) + + /// + /// Registers new + /// + /// + /// + public static void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) { Instance.RegisterHelper(helperName, helperFunction); } /// - /// Expose the configuration on order to have access in all Helpers and Templates. + /// Registers new /// - public static HandlebarsConfiguration Configuration + /// + /// + public static void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { - get { return Instance.Configuration; } + Instance.RegisterHelper(helperName, helperFunction); } + + /// + /// Expose the configuration in order to have access in all Helpers and Templates. + /// + public static HandlebarsConfiguration Configuration => Instance.Configuration; } } \ No newline at end of file diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 89de3a07..a0506f97 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -1,13 +1,13 @@ - + + Handlebars portable 9822C7B8-7E51-42BC-9A49-72A10491B202 netstandard1.3;netstandard2.0 - $(TargetFrameworks);net452 - 1.11.4 + $(TargetFrameworks);net451;net452 + 2.0.0 8 - true @@ -21,28 +21,42 @@ Rex Morgan Copyright © 2014-2020 Rex Morgan Blistering-fast Handlebars.js templates in your .NET application. - https://raw.githubusercontent.com/Handlebars-Net/Handlebars.Net/master/hbnet-icon.png + hbnet-icon.png Handlebars.Net - https://opensource.org/licenses/mit - https://github.com/rexm/Handlebars.Net - https://github.com/Handlebars-Net/Handlebars.Net/releases/tag/$(Version) + https://github.com/Handlebars-Net handlebars;mustache;templating;engine;compiler git https://github.com/Handlebars-Net/Handlebars.Net + https://github.com/Handlebars-Net/Handlebars.Net/releases/tag/$(Version) true - + + + false + true + . + + + + + - - + + + + - - + + + + + + - + \ No newline at end of file diff --git a/source/Handlebars/HandlebarsConfiguration.cs b/source/Handlebars/HandlebarsConfiguration.cs deleted file mode 100644 index d0910beb..00000000 --- a/source/Handlebars/HandlebarsConfiguration.cs +++ /dev/null @@ -1,48 +0,0 @@ -using HandlebarsDotNet.Compiler.Resolvers; -using System; -using System.Collections.Generic; -using System.IO; - -namespace HandlebarsDotNet -{ - public class HandlebarsConfiguration - { - public IDictionary Helpers { get; private set; } - - public IDictionary BlockHelpers { get; private set; } - - public IDictionary> RegisteredTemplates { get; private set; } - - public IExpressionNameResolver ExpressionNameResolver { get; set; } - - public ITextEncoder TextEncoder { get; set; } - - public ViewEngineFileSystem FileSystem { get; set; } - - public string UnresolvedBindingFormatter { get; set; } - - public bool ThrowOnUnresolvedBindingExpression { get; set; } - - /// - /// The resolver used for unregistered partials. Defaults - /// to the . - /// - public IPartialTemplateResolver PartialTemplateResolver { get; set; } - - /// - /// The handler called when a partial template cannot be found. - /// - public IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; } - - public HandlebarsConfiguration() - { - this.Helpers = new Dictionary(StringComparer.OrdinalIgnoreCase); - this.BlockHelpers = new Dictionary(StringComparer.OrdinalIgnoreCase); - this.PartialTemplateResolver = new FileSystemPartialTemplateResolver(); - this.RegisteredTemplates = new Dictionary>(StringComparer.OrdinalIgnoreCase); - this.TextEncoder = new HtmlEncoder(); - this.ThrowOnUnresolvedBindingExpression = false; - } - } -} - diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 53273882..75c31df3 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -1,116 +1,123 @@ using System; using System.IO; -using System.Text; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { - public partial class Handlebars + public delegate TextReader ViewReaderFactory(ICompiledHandlebarsConfiguration configuration, string templatePath); + + internal class HandlebarsEnvironment : IHandlebars { - private class HandlebarsEnvironment : IHandlebars + private static readonly ViewReaderFactory ViewReaderFactory = (configuration, path) => { - private readonly HandlebarsConfiguration _configuration; - private readonly HandlebarsCompiler _compiler; + var fs = configuration.FileSystem; + if (fs == null) + throw new InvalidOperationException("Cannot compile view when configuration.FileSystem is not set"); + var template = fs.GetFileContent(path); + if (template == null) + throw new InvalidOperationException("Cannot find template at '" + path + "'"); + + return new StringReader(template); + }; + + public HandlebarsEnvironment(HandlebarsConfiguration configuration) + { + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } - public HandlebarsEnvironment(HandlebarsConfiguration configuration) + public Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + { + readerFactoryFactory = readerFactoryFactory ?? ViewReaderFactory; + return CompileViewInternal(templatePath, readerFactoryFactory); + } + + public Func CompileView(string templatePath) + { + var view = CompileViewInternal(templatePath, ViewReaderFactory); + return (vm) => { - if (configuration == null) + using (var writer = new PolledStringWriter(Configuration.FormatProvider)) { - throw new ArgumentNullException("configuration"); + view(writer, vm); + return writer.ToString(); } + }; + } - _configuration = configuration; - _compiler = new HandlebarsCompiler(_configuration); - RegisterBuiltinHelpers(); - } - - public Func CompileView(string templatePath) + private Action CompileViewInternal(string templatePath, ViewReaderFactory readerFactoryFactory) + { + var configuration = new InternalHandlebarsConfiguration(Configuration); + var createdFeatures = configuration.Features; + for (var index = 0; index < createdFeatures.Count; index++) { - var compiledView = _compiler.CompileView(templatePath); - return (vm) => - { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) - { - compiledView(tw, vm); - } - return sb.ToString(); - }; + createdFeatures[index].OnCompiling(configuration); } - public HandlebarsConfiguration Configuration + var compiler = new HandlebarsCompiler(configuration); + var compiledView = compiler.CompileView(readerFactoryFactory, templatePath, configuration); + + for (var index = 0; index < createdFeatures.Count; index++) { - get - { - return this._configuration; - } + createdFeatures[index].CompilationCompleted(); } - public Action Compile(TextReader template) - { - return _compiler.Compile(template); - } + return compiledView; + } - public Func Compile(string template) - { - using (var reader = new StringReader(template)) - { - var compiledTemplate = Compile(reader); - return context => - { - var builder = new StringBuilder(); - using (var writer = new StringWriter(builder)) - { - compiledTemplate(writer, context); - } - return builder.ToString(); - }; - } - } + public HandlebarsConfiguration Configuration { get; } - public void RegisterTemplate(string templateName, Action template) + public Action Compile(TextReader template) + { + using (var reader = new ExtendedStringReader(template)) { - lock (_configuration) - { - _configuration.RegisteredTemplates.AddOrUpdate(templateName, template); - } + var compiler = new HandlebarsCompiler(Configuration); + return compiler.Compile(reader); } + } - public void RegisterTemplate(string templateName, string template) + public Func Compile(string template) + { + using (var reader = new StringReader(template)) { - using (var reader = new StringReader(template)) + var compiledTemplate = Compile(reader); + return context => { - RegisterTemplate(templateName, Compile(reader)); - } + using (var writer = new PolledStringWriter(Configuration.FormatProvider)) + { + compiledTemplate(writer, context); + return writer.ToString(); + } + }; } + } - public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) - { - lock (_configuration) - { - _configuration.Helpers.AddOrUpdate(helperName, helperFunction); - } - } + public void RegisterTemplate(string templateName, Action template) + { + Configuration.RegisteredTemplates[templateName] = template; + } - public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) + public void RegisterTemplate(string templateName, string template) + { + using (var reader = new StringReader(template)) { - lock (_configuration) - { - _configuration.BlockHelpers.AddOrUpdate(helperName, helperFunction); - } + RegisterTemplate(templateName, Compile(reader)); } + } - private void RegisterBuiltinHelpers() - { - foreach (var helperDefinition in BuiltinHelpers.Helpers) - { - RegisterHelper(helperDefinition.Key, helperDefinition.Value); - } - foreach (var helperDefinition in BuiltinHelpers.BlockHelpers) - { - RegisterHelper(helperDefinition.Key, helperDefinition.Value); - } - } + public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) + { + Configuration.Helpers[helperName] = helperFunction; + } + + public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + { + Configuration.ReturnHelpers[helperName] = helperFunction; + } + + public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) + { + Configuration.BlockHelpers[helperName] = helperFunction; } } } diff --git a/source/Handlebars/HandlebarsException.cs b/source/Handlebars/HandlebarsException.cs index 2e24dfb8..3d0b8e14 100644 --- a/source/Handlebars/HandlebarsException.cs +++ b/source/Handlebars/HandlebarsException.cs @@ -2,17 +2,37 @@ namespace HandlebarsDotNet { + /// + /// General Handlebars exception + /// public class HandlebarsException : Exception { public HandlebarsException(string message) - : base(message) + : this(message, null, null) { } - + + internal HandlebarsException(string message, IReaderContext context = null) + : this(message, null, context) + { + } + public HandlebarsException(string message, Exception innerException) : base(message, innerException) { } + + internal HandlebarsException(string message, Exception innerException, IReaderContext context = null) + : base(FormatMessage(message, context), innerException) + { + } + + private static string FormatMessage(string message, IReaderContext context) + { + if (context == null) return message; + + return $"{message}\n\nOccured at: {context.LineNumber}:{context.CharNumber}"; + } } } diff --git a/source/Handlebars/HandlebarsExtensions.cs b/source/Handlebars/HandlebarsExtensions.cs index 5a21eec5..cb3f2a44 100644 --- a/source/Handlebars/HandlebarsExtensions.cs +++ b/source/Handlebars/HandlebarsExtensions.cs @@ -1,20 +1,45 @@ using System; +using System.Diagnostics; using System.IO; namespace HandlebarsDotNet { + public static class HandlebarsExtensions { + /// + /// Writes an encoded string using + /// + /// + /// public static void WriteSafeString(this TextWriter writer, string value) { writer.Write(new SafeString(value)); } + /// + /// Writes an encoded string using + /// + /// + /// public static void WriteSafeString(this TextWriter writer, object value) { writer.WriteSafeString(value.ToString()); } + + /// + /// Allows configuration manipulations + /// + /// + /// + /// + public static HandlebarsConfiguration Configure(this HandlebarsConfiguration configuration, Action config) + { + config(configuration); + return configuration; + } + private class SafeString : ISafeString { private readonly string _value; @@ -30,5 +55,10 @@ public override string ToString() } } } + + + public interface ISafeString + { + } } diff --git a/source/Handlebars/HandlebarsRuntimeException.cs b/source/Handlebars/HandlebarsRuntimeException.cs index ea85cc33..f331a119 100644 --- a/source/Handlebars/HandlebarsRuntimeException.cs +++ b/source/Handlebars/HandlebarsRuntimeException.cs @@ -1,20 +1,29 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace HandlebarsDotNet { + /// + /// Represents errors occured in Handlebar's runtime + /// public class HandlebarsRuntimeException : HandlebarsException { public HandlebarsRuntimeException(string message) - : base(message) + : this(message, null, null) { } - + + internal HandlebarsRuntimeException(string message, IReaderContext context = null) + : this(message, null, context) + { + } + public HandlebarsRuntimeException(string message, Exception innerException) - : base(message, innerException) + : base(message, innerException, null) + { + } + + internal HandlebarsRuntimeException(string message, Exception innerException, IReaderContext context = null) + : base(message, innerException, context) { } } diff --git a/source/Handlebars/HandlebarsUtils.cs b/source/Handlebars/HandlebarsUtils.cs index 40ba1f50..b408e3fd 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -7,64 +7,54 @@ namespace HandlebarsDotNet { public static class HandlebarsUtils { + /// + /// Implementation of JS's `==` + /// + /// + /// public static bool IsTruthy(object value) { return !IsFalsy(value); } - - public static bool IsUndefinedBindingResult(object value) - { - return value is UndefinedBindingResult; - } + /// + /// Implementation of JS's `!=` + /// + /// + /// public static bool IsFalsy(object value) { - if (value is UndefinedBindingResult) - { - return true; - } - if (value == null) - { - return true; - } - else if (value is bool) - { - return !(bool)value; - } - else if (value is string) + switch (value) { - if ((string)value == "") - { + case UndefinedBindingResult _: + case null: return true; - } - else - { - return false; - } + case bool b: + return !b; + case string s: + return s == string.Empty; } - else if (IsNumber(value)) + + if (IsNumber(value)) { - return !System.Convert.ToBoolean(value); + return !Convert.ToBoolean(value); } return false; } - + public static bool IsTruthyOrNonEmpty(object value) { return !IsFalsyOrEmpty(value); } - + public static bool IsFalsyOrEmpty(object value) { if(IsFalsy(value)) { return true; } - else if (value is IEnumerable && ((IEnumerable)value).OfType().Any() == false) - { - return true; - } - return false; + + return value is IEnumerable enumerable && !enumerable.OfType().Any(); } private static bool IsNumber(object value) diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index ab9efb40..fa10326e 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,30 +1,107 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { - public sealed class HelperOptions + + public delegate void BlockParamsConfiguration(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies); + + /// + /// Contains properties accessible withing function + /// + public sealed class HelperOptions : IReadOnlyDictionary { - private readonly Action _template; - private readonly Action _inverse; - + private readonly Dictionary _extensions; + internal HelperOptions( Action template, - Action inverse) + Action inverse, + BlockParamsValueProvider blockParamsValueProvider, + InternalHandlebarsConfiguration configuration, + BindingContext bindingContext) + { + Template = template; + Inverse = inverse; + BlockParams = blockParamsValueProvider.Configure; + + BindingContext = bindingContext; + Configuration = configuration; + + _extensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [nameof(Template)] = Template, + [nameof(Inverse)] = Inverse, + [nameof(BlockParams)] = BlockParams + }; + } + + /// + /// BlockHelper body + /// + public Action Template { get; } + + /// + /// BlockHelper else body + /// + public Action Inverse { get; } + + /// + public BlockParamsConfiguration BlockParams { get; } + + /// + internal InternalHandlebarsConfiguration Configuration { get; } + + internal BindingContext BindingContext { get; } + + bool IReadOnlyDictionary.ContainsKey(string key) + { + return _extensions.ContainsKey(key); + } + + bool IReadOnlyDictionary.TryGetValue(string key, out object value) { - _template = template; - _inverse = inverse; + return _extensions.TryGetValue(key, out value); } - public Action Template + /// + /// Provides access to dynamic data entries + /// + /// + public object this[string property] { - get { return _template; } + get => _extensions.TryGetValue(property, out var value) ? value : null; + internal set => _extensions[property] = value; } - public Action Inverse + /// + /// Provides access to dynamic data entries in a typed manner + /// + /// + /// + /// + public T GetValue(string property) { - get { return _inverse; } + return (T) this[property]; } + + IEnumerable IReadOnlyDictionary.Keys => _extensions.Keys; + + IEnumerable IReadOnlyDictionary.Values => _extensions.Values; + + IEnumerator> IEnumerable>.GetEnumerator() + { + return _extensions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) _extensions).GetEnumerator(); + } + + int IReadOnlyCollection>.Count => _extensions.Count; } } diff --git a/source/Handlebars/Helpers/IHelperResolver.cs b/source/Handlebars/Helpers/IHelperResolver.cs new file mode 100644 index 00000000..57388af4 --- /dev/null +++ b/source/Handlebars/Helpers/IHelperResolver.cs @@ -0,0 +1,27 @@ +using System; + +namespace HandlebarsDotNet.Helpers +{ + /// + /// Allows to provide helpers on-demand + /// + public interface IHelperResolver + { + /// + /// Resolves + /// + /// + /// + /// + /// + bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper); + + /// + /// Resolves + /// + /// + /// + /// + bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper); + } +} \ No newline at end of file diff --git a/source/Handlebars/HtmlEncoder.cs b/source/Handlebars/HtmlEncoder.cs deleted file mode 100644 index af0bda1f..00000000 --- a/source/Handlebars/HtmlEncoder.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace HandlebarsDotNet -{ - public class HtmlEncoder : ITextEncoder - { - public string Encode(string text) - { - if (string.IsNullOrEmpty(text)) - return String.Empty; - - - // Detect if we need to allocate a stringbuilder and new string - for (var i = 0; i < text.Length; i++) - { - switch (text[i]) - { - case '"': - case '&': - case '<': - case '>': - return ReallyEncode(text, i); - default: - if (text[i] > 159) - { - return ReallyEncode(text, i); - } - else - - break; - } - } - - return text; - } - - private static string ReallyEncode(string text, int i) - { - var sb = new StringBuilder(text.Length + 5); - 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; - } - } - - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/IExpressionCompiler.cs b/source/Handlebars/IExpressionCompiler.cs new file mode 100644 index 00000000..e557dbfc --- /dev/null +++ b/source/Handlebars/IExpressionCompiler.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq.Expressions; + +namespace HandlebarsDotNet +{ + /// + /// Executes compilation of lambda to actual + /// + public interface IExpressionCompiler + { + T Compile(Expression expression) where T: class; + } +} \ No newline at end of file diff --git a/source/Handlebars/IExpressionMiddleware.cs b/source/Handlebars/IExpressionMiddleware.cs new file mode 100644 index 00000000..0f3b7640 --- /dev/null +++ b/source/Handlebars/IExpressionMiddleware.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; + +namespace HandlebarsDotNet +{ + /// + /// Allows to modify expression before lambda compilation. Should be executed as part of . + /// + public interface IExpressionMiddleware + { + Expression Invoke(Expression expression); + } +} \ No newline at end of file diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 61140ea4..7200bb85 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -1,26 +1,36 @@ using System; using System.IO; -using HandlebarsDotNet.Compiler; -using System.Text; namespace HandlebarsDotNet { + /// + /// + /// public interface IHandlebars { + /// + /// + /// + /// + /// Action Compile(TextReader template); - + Func Compile(string template); - + Func CompileView(string templatePath); - + + Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory); + HandlebarsConfiguration Configuration { get; } - + void RegisterTemplate(string templateName, Action template); - + void RegisterTemplate(string templateName, string template); - + void RegisterHelper(string helperName, HandlebarsHelper helperFunction); - + + void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction); + void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction); } } diff --git a/source/Handlebars/EncodedTextWriter.cs b/source/Handlebars/IO/EncodedTextWriter.cs similarity index 72% rename from source/Handlebars/EncodedTextWriter.cs rename to source/Handlebars/IO/EncodedTextWriter.cs index 40b67c9f..52322967 100644 --- a/source/Handlebars/EncodedTextWriter.cs +++ b/source/Handlebars/IO/EncodedTextWriter.cs @@ -5,14 +5,13 @@ namespace HandlebarsDotNet { internal class EncodedTextWriter : TextWriter { - private readonly TextWriter _underlyingWriter; private readonly ITextEncoder _encoder; public bool SuppressEncoding { get; set; } - public EncodedTextWriter(TextWriter writer, ITextEncoder encoder) + private EncodedTextWriter(TextWriter writer, ITextEncoder encoder) { - _underlyingWriter = writer; + UnderlyingWriter = writer; _encoder = encoder; } @@ -30,7 +29,7 @@ public void Write(string value, bool encode) value = _encoder.Encode(value); } - _underlyingWriter.Write(value); + UnderlyingWriter.Write(value); } public override void Write(string value) @@ -54,14 +53,8 @@ public override void Write(object value) Write(value.ToString(), encode); } - public TextWriter UnderlyingWriter - { - get { return _underlyingWriter; } - } + public TextWriter UnderlyingWriter { get; } - public override Encoding Encoding - { - get { return _underlyingWriter.Encoding; } - } + public override Encoding Encoding => UnderlyingWriter.Encoding; } } \ No newline at end of file diff --git a/source/Handlebars/IO/ExtendedStringReader.cs b/source/Handlebars/IO/ExtendedStringReader.cs new file mode 100644 index 00000000..f48fac49 --- /dev/null +++ b/source/Handlebars/IO/ExtendedStringReader.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; + +namespace HandlebarsDotNet +{ + internal sealed class ExtendedStringReader : TextReader + { + private int _linePos; + private int _charPos; + private int _matched; + + public ExtendedStringReader(TextReader reader) + { + _inner = reader; + } + + private readonly TextReader _inner; + + public override int Peek() + { + return _inner.Peek(); + } + + public override int Read() + { + var c = _inner.Read(); + if (c >= 0) AdvancePosition((char) c); + return c; + } + + private void AdvancePosition(char c) + { + if (Environment.NewLine[_matched] == c) + { + _matched++; + if (_matched != Environment.NewLine.Length) return; + + _linePos++; + _charPos = 0; + _matched = 0; + + return; + } + + _matched = 0; + _charPos++; + } + + public IReaderContext GetContext() + { + return new ReaderContext + { + LineNumber = _linePos, + CharNumber = _charPos + }; + } + + private class ReaderContext : IReaderContext + { + public int LineNumber { get; set; } + + public int CharNumber { get; set; } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/IO/HtmlEncoder.cs b/source/Handlebars/IO/HtmlEncoder.cs new file mode 100644 index 00000000..6edb5145 --- /dev/null +++ b/source/Handlebars/IO/HtmlEncoder.cs @@ -0,0 +1,83 @@ +using System.Globalization; + +namespace HandlebarsDotNet +{ + /// + /// + /// Produces HTML safe output. + /// + public class HtmlEncoder : ITextEncoder + { + /// + public string Encode(string text) + { + if (string.IsNullOrEmpty(text)) + return string.Empty; + + + // Detect if we need to allocate a stringbuilder and new string + for (var i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case '"': + case '&': + case '<': + case '>': + return ReallyEncode(text, i); + default: + if (text[i] > 159) + { + return ReallyEncode(text, i); + } + 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; + } + } + + return sb.ToString(); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/IO/IReaderContext.cs b/source/Handlebars/IO/IReaderContext.cs new file mode 100644 index 00000000..72d269d2 --- /dev/null +++ b/source/Handlebars/IO/IReaderContext.cs @@ -0,0 +1,8 @@ +namespace HandlebarsDotNet +{ + internal interface IReaderContext + { + int LineNumber { get; set; } + int CharNumber { get; set; } + } +} \ No newline at end of file diff --git a/source/Handlebars/ITextEncoder.cs b/source/Handlebars/IO/ITextEncoder.cs similarity index 58% rename from source/Handlebars/ITextEncoder.cs rename to source/Handlebars/IO/ITextEncoder.cs index 953196b2..41d43f46 100644 --- a/source/Handlebars/ITextEncoder.cs +++ b/source/Handlebars/IO/ITextEncoder.cs @@ -1,5 +1,8 @@ namespace HandlebarsDotNet { + /// + /// Encoder used for output encoding. + /// public interface ITextEncoder { string Encode(string value); diff --git a/source/Handlebars/IO/PolledStringWriter.cs b/source/Handlebars/IO/PolledStringWriter.cs new file mode 100644 index 00000000..7f34fc1b --- /dev/null +++ b/source/Handlebars/IO/PolledStringWriter.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace HandlebarsDotNet +{ + internal class PolledStringWriter : StringWriter + { + public PolledStringWriter() : base(StringBuilderPool.Shared.Get()) + { + + } + + public PolledStringWriter(IFormatProvider formatProvider) : base(StringBuilderPool.Shared.Get(), formatProvider) + { + } + + protected override void Dispose(bool disposing) + { + StringBuilderPool.Shared.Return(base.GetStringBuilder()); + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ISafeString.cs b/source/Handlebars/ISafeString.cs deleted file mode 100644 index 51016df9..00000000 --- a/source/Handlebars/ISafeString.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace HandlebarsDotNet -{ - public interface ISafeString - { - } -} - diff --git a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs new file mode 100644 index 00000000..a48d9032 --- /dev/null +++ b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs @@ -0,0 +1,16 @@ +using System; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class ContextMemberAccessor : IMemberAccessor + { + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + var bindingContext = (BindingContext) instance; + var segment = new ChainSegment(memberName); + return bindingContext.TryGetContextVariable(ref segment, out value); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs b/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs new file mode 100644 index 00000000..1edd3503 --- /dev/null +++ b/source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class DictionaryMemberAccessor : IMemberAccessor + { + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + value = null; + // Check if the instance is IDictionary (ie, System.Collections.Hashtable) + // Only string keys supported - indexer takes an object, but no nice + // way to check if the hashtable check if it should be a different type. + var dictionary = (IDictionary) instance; + value = dictionary[memberName]; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs b/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs new file mode 100644 index 00000000..148e1f1c --- /dev/null +++ b/source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs @@ -0,0 +1,31 @@ +using System; +using System.Dynamic; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class DynamicMemberAccessor : IMemberAccessor + { + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + value = null; + //crude handling for dynamic objects that don't have metadata + var metaObjectProvider = (IDynamicMetaObjectProvider) instance; + + try + { + value = GetProperty(metaObjectProvider, memberName); + return value != null; + } + catch + { + return false; + } + } + + private static object GetProperty(object target, string name) + { + var site = System.Runtime.CompilerServices.CallSite>.Create(Microsoft.CSharp.RuntimeBinder.Binder.GetMember(0, name, target.GetType(), new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(0, null) })); + return site.Target(site, target); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs new file mode 100644 index 00000000..85d1ad79 --- /dev/null +++ b/source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Linq; +using System.Text.RegularExpressions; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class EnumerableMemberAccessor : IMemberAccessor + { + private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); + + public bool TryGetValue(object instance, Type type, string memberName, out object value) + { + value = null; + + var match = IndexRegex.Match(memberName); + if (!match.Success) return false; + const string indexGroupName = "index"; + if (!match.Groups[indexGroupName].Success || !int.TryParse(match.Groups[indexGroupName].Value, out var index)) return false; + + switch (instance) + { + case IList list: + value = list[index]; + return true; + + case IEnumerable enumerable: + value = enumerable.Cast().ElementAtOrDefault(index); + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/IMemberAccessor.cs b/source/Handlebars/MemberAccessors/IMemberAccessor.cs new file mode 100644 index 00000000..b74fe239 --- /dev/null +++ b/source/Handlebars/MemberAccessors/IMemberAccessor.cs @@ -0,0 +1,20 @@ +using System; + +namespace HandlebarsDotNet.MemberAccessors +{ + /// + /// Describes mechanism to access members of object + /// + public interface IMemberAccessor + { + /// + /// Describes mechanism to access members of an object. Returns if operation is successful and contains data, otherwise returns + /// + /// + /// + /// + /// + /// + bool TryGetValue(object instance, Type instanceType, string memberName, out object value); + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs new file mode 100644 index 00000000..89b01683 --- /dev/null +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using HandlebarsDotNet.Collections; + +namespace HandlebarsDotNet.MemberAccessors +{ + internal class ReflectionMemberAccessor : IMemberAccessor + { + private readonly InternalHandlebarsConfiguration _configuration; + private readonly IMemberAccessor _inner; + + public ReflectionMemberAccessor(InternalHandlebarsConfiguration configuration) + { + _configuration = configuration; + _inner = configuration.CompileTimeConfiguration.UseAggressiveCaching + ? (IMemberAccessor) new MemberAccessor() // will be removed in next iterations + : (IMemberAccessor) new MemberAccessor(); + } + + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + if (_inner.TryGetValue(instance, instanceType, memberName, out value)) + { + return true; + } + + var aliasProviders = _configuration.CompileTimeConfiguration.AliasProviders; + for (var index = 0; index < aliasProviders.Count; index++) + { + if (aliasProviders[index].TryGetMemberByAlias(instance, instanceType, memberName, out value)) + return true; + } + + value = null; + return false; + } + + private abstract class ObjectTypeDescriptor + { + protected readonly LookupSlim, Func>> + Accessors = new LookupSlim, Func>>(); + + protected Type Type { get; } + + public ObjectTypeDescriptor(Type type) + { + Type = type; + } + + public abstract Func GetOrCreateAccessor(string name); + } + + private class MemberAccessor : IMemberAccessor + where T : ObjectTypeDescriptor + { + private readonly LookupSlim> _descriptors = + new LookupSlim>(); + + private static readonly Func> ValueFactory = + key => new DeferredValue(key, type => (T) Activator.CreateInstance(typeof(T), type)); + + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) + { + deferredValue = _descriptors.GetOrAdd(instanceType, ValueFactory); + } + + var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); + value = accessor?.Invoke(instance); + return accessor != null; + } + } + + private sealed class RawObjectTypeDescriptor : ObjectTypeDescriptor + { + private static readonly MethodInfo CreateGetDelegateMethodInfo = typeof(RawObjectTypeDescriptor) + .GetMethod(nameof(CreateGetDelegate), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly Func, Func> ValueGetterFactory = o => GetValueGetter(o.Key, o.Value); + + private static readonly Func, Func>> + ValueFactory = (key, state) => new DeferredValue, Func>(new KeyValuePair(key, state), ValueGetterFactory); + + public RawObjectTypeDescriptor(Type type) : base(type) + { + } + + public override Func GetOrCreateAccessor(string name) + { + return Accessors.TryGetValue(name, out var deferredValue) + ? deferredValue.Value + : Accessors.GetOrAdd(name, ValueFactory, Type).Value; + } + + private static Func GetValueGetter(string name, Type type) + { + var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => + o.GetIndexParameters().Length == 0 && + string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (property != null) + { + return (Func) CreateGetDelegateMethodInfo + .MakeGenericMethod(type, property.PropertyType) + .Invoke(null, new[] {property}); + } + + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + if (field != null) + { + return o => field.GetValue(o); + } + + return null; + } + + private static Func CreateGetDelegate(PropertyInfo property) + { + var @delegate = (Func) property.GetMethod.CreateDelegate(typeof(Func)); + return o => (object) @delegate((T) o); + } + } + + private sealed class CompiledObjectTypeDescriptor : ObjectTypeDescriptor + { + private static readonly Func, Func> ValueGetterFactory = + o => GetValueGetter(o.Key, o.Value); + + private static readonly Func, Func>> + ValueFactory = (key, state) => new DeferredValue, Func>(new KeyValuePair(key, state), ValueGetterFactory); + + public CompiledObjectTypeDescriptor(Type type) : base(type) + { + } + + public override Func GetOrCreateAccessor(string name) + { + return Accessors.TryGetValue(name, out var deferredValue) + ? deferredValue.Value + : Accessors.GetOrAdd(name, ValueFactory, Type).Value; + } + + private static Func GetValueGetter(string name, Type type) + { + var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => + o.GetIndexParameters().Length == 0 && + string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (property != null) + { + var instance = Expression.Parameter(typeof(object), "i"); + var memberExpression = Expression.Property(Expression.Convert(instance, type), name); + var convert = Expression.TypeAs(memberExpression, typeof(object)); + + return (Func) Expression.Lambda(convert, instance).Compile(); + } + + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (field != null) + { + var instance = Expression.Parameter(typeof(object), "i"); + var memberExpression = Expression.Field(Expression.Convert(instance, type), name); + var convert = Expression.TypeAs(memberExpression, typeof(object)); + + return (Func) Expression.Lambda(convert, instance).Compile(); + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs new file mode 100644 index 00000000..3b5d4f45 --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections; +using System.Linq; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet +{ + internal class CollectionMemberAliasProvider : IMemberAliasProvider + { + private readonly InternalHandlebarsConfiguration _configuration; + + public CollectionMemberAliasProvider(InternalHandlebarsConfiguration configuration) + { + _configuration = configuration; + } + + public bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) + { + var segment = new ChainSegment(memberAlias); + switch (instance) + { + case Array array: + switch (segment.LowerInvariant) + { + case "count": + value = array.Length; + return true; + + default: + value = null; + return false; + } + + case ICollection array: + switch (segment.LowerInvariant) + { + case "length": + value = array.Count; + return true; + + default: + value = null; + return false; + } + + case IEnumerable enumerable: + if (!_configuration.ObjectDescriptorProvider.TryGetDescriptor(targetType, out var descriptor)) + { + value = null; + return false; + } + + var properties = descriptor.GetProperties(descriptor, enumerable); + var property = properties.FirstOrDefault(o => + { + var name = o.ToString().ToLowerInvariant(); + return name.Equals("length") || name.Equals("count"); + }); + + if (property != null && descriptor.MemberAccessor.TryGetValue(enumerable, targetType, property.ToString(), out value)) return true; + + value = null; + return false; + + default: + value = null; + return false; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs new file mode 100644 index 00000000..30ccf332 --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace HandlebarsDotNet +{ + /// + /// Provides simple interface for adding member aliases + /// + public class DelegatedMemberAliasProvider : IMemberAliasProvider + { + private readonly Dictionary>> _aliases + = new Dictionary>>(); + + public DelegatedMemberAliasProvider AddAlias(Type type, string alias, Func accessor) + { + if (!_aliases.TryGetValue(type, out var aliases)) + { + aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _aliases.Add(type, aliases); + } + + aliases.Add(alias, accessor); + + return this; + } + + public DelegatedMemberAliasProvider AddAlias(string alias, Func accessor) + { + AddAlias(typeof(T), alias, o => accessor((T) o)); + + return this; + } + + bool IMemberAliasProvider.TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) + { + if (_aliases.TryGetValue(targetType, out var aliases)) + { + if (aliases.TryGetValue(memberAlias, out var accessor)) + { + value = accessor(instance); + return true; + } + } + + aliases = _aliases.FirstOrDefault(o => o.Key.IsAssignableFrom(targetType)).Value; + if (aliases != null) + { + if (aliases.TryGetValue(memberAlias, out var accessor)) + { + value = accessor(instance); + return true; + } + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs new file mode 100644 index 00000000..382e645d --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs @@ -0,0 +1,12 @@ +using System; + +namespace HandlebarsDotNet +{ + /// + /// Allows to redirect member access to a different member + /// + public interface IMemberAliasProvider + { + bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value); + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs new file mode 100644 index 00000000..ebdf9a87 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class CollectionObjectDescriptor : IObjectDescriptorProvider + { + private readonly ObjectDescriptorProvider _objectDescriptorProvider; + + public CollectionObjectDescriptor(ObjectDescriptorProvider objectDescriptorProvider) + { + _objectDescriptorProvider = objectDescriptorProvider; + } + + public bool CanHandleType(Type type) + { + return typeof(ICollection).IsAssignableFrom(type) && _objectDescriptorProvider.CanHandleType(type); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + if (!_objectDescriptorProvider.TryGetDescriptor(type, out value)) return false; + + var mergedMemberAccessor = new MergedMemberAccessor(new EnumerableMemberAccessor(), value.MemberAccessor); + value = new ObjectDescriptor( + value.DescribedType, + mergedMemberAccessor, + value.GetProperties, + true + ); + + return true; + + } + } + + internal class MergedMemberAccessor : IMemberAccessor + { + private readonly IMemberAccessor[] _accessors; + + public MergedMemberAccessor(params IMemberAccessor[] accessors) + { + _accessors = accessors; + } + + public bool TryGetValue(object instance, Type type, string memberName, out object value) + { + for (var index = 0; index < _accessors.Length; index++) + { + if (_accessors[index].TryGetValue(instance, type, memberName, out value)) return true; + } + + value = default(object); + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs new file mode 100644 index 00000000..03d4d666 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class ContextObjectDescriptor : IObjectDescriptorProvider + { + private static readonly Type BindingContextType = typeof(BindingContext); + private static readonly string[] Properties = { "root", "parent" }; + private static readonly Func> PropertiesDelegate = (descriptor, o) => Properties; + + private static readonly ObjectDescriptor Descriptor = + new ObjectDescriptor(BindingContextType, new ContextMemberAccessor(), PropertiesDelegate); + + public bool CanHandleType(Type type) + { + return type == BindingContextType; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = Descriptor; + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs new file mode 100644 index 00000000..4e80caa3 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class DictionaryObjectDescriptor : IObjectDescriptorProvider + { + private static readonly DictionaryMemberAccessor DictionaryMemberAccessor = new DictionaryMemberAccessor(); + + private static readonly Func> GetProperties = (descriptor, arg) => + { + return Enumerate((IDictionary) arg); + + IEnumerable Enumerate(IDictionary dictionary) + { + foreach (var key in dictionary.Keys) yield return key; + } + }; + + public bool CanHandleType(Type type) + { + return typeof(IDictionary).IsAssignableFrom(type); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = new ObjectDescriptor(type, DictionaryMemberAccessor, GetProperties); + + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs new file mode 100644 index 00000000..484db2b4 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq.Expressions; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class DynamicObjectDescriptor : IObjectDescriptorProvider + { + private static readonly DynamicMemberAccessor DynamicMemberAccessor = new DynamicMemberAccessor(); + private static readonly Func> GetProperties = (descriptor, o) => ((IDynamicMetaObjectProvider) o).GetMetaObject(Expression.Constant(o)).GetDynamicMemberNames(); + + public bool CanHandleType(Type type) + { + return typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = new ObjectDescriptor(type, DynamicMemberAccessor, GetProperties); + + return true; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs new file mode 100644 index 00000000..f09bb06f --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Reflection; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class EnumerableObjectDescriptor : IObjectDescriptorProvider + { + private readonly CollectionObjectDescriptor _collectionObjectDescriptor; + + public EnumerableObjectDescriptor(CollectionObjectDescriptor collectionObjectDescriptor) + { + _collectionObjectDescriptor = collectionObjectDescriptor; + } + + public bool CanHandleType(Type type) + { + return typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + return _collectionObjectDescriptor.TryGetDescriptor(type, out value); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs new file mode 100644 index 00000000..461d29c2 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal sealed class GenericDictionaryObjectDescriptorProvider : IObjectDescriptorProvider + { + private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); + + private readonly LookupSlim> _typeCache = new LookupSlim>(); + + public bool CanHandleType(Type type) + { + var deferredValue = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); + return deferredValue.Value != null; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + var interfaceType = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; + if (interfaceType == null) + { + value = ObjectDescriptor.Empty; + return false; + } + + var descriptorCreator = CreateDescriptorMethodInfo + .MakeGenericMethod(interfaceType.GetGenericArguments()); + + value = (ObjectDescriptor) descriptorCreator.Invoke(null, ArrayEx.Empty()); + return true; + } + + private static readonly Func> InterfaceTypeValueFactory = + key => new DeferredValue(key, type => + { + return type.GetInterfaces() + .Where(i => i.GetTypeInfo().IsGenericType) + .Where(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + .FirstOrDefault(i => + TypeDescriptor.GetConverter(i.GetGenericArguments()[0]).CanConvertFrom(typeof(string)) + ); + }); + + private static ObjectDescriptor CreateDescriptor() + { + IEnumerable Enumerate(IDictionary o) + { + foreach (var key in o.Keys) yield return key; + } + + return new ObjectDescriptor( + typeof(IDictionary), + new DictionaryAccessor(), + (descriptor, o) => Enumerate((IDictionary) o) + ); + } + + private class DictionaryAccessor : IMemberAccessor + { + private static readonly TypeConverter TypeConverter = TypeDescriptor.GetConverter(typeof(T)); + + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + var key = (T) TypeConverter.ConvertFromString(memberName); + var dictionary = (IDictionary) instance; + if (dictionary.TryGetValue(key, out var v)) + { + value = v; + return true; + } + + value = default(TV); + return false; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs new file mode 100644 index 00000000..e29dd458 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs @@ -0,0 +1,24 @@ +using System; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + /// + /// Facade for + /// + public interface IObjectDescriptorProvider + { + /// + /// Lightweight method to check whether descriptor can be created + /// + /// + /// + bool CanHandleType(Type type); + + /// + /// Tries to create for . Methods is guarantied to be called if return . + /// + /// + /// + bool TryGetDescriptor(Type type, out ObjectDescriptor value); + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs new file mode 100644 index 00000000..2ce84577 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +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; + + public bool CanHandleType(Type type) + { + return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + 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, Type instanceType, string memberName, out object value) + { + var keyValuePair = (KeyValuePair) instance; + switch (memberName.ToLowerInvariant()) + { + case "key": + value = keyValuePair.Key; + return true; + + case "value": + value = keyValuePair.Value; + return true; + + default: + value = default(TV); + return false; + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs new file mode 100644 index 00000000..998d5920 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + /// + /// Provides meta-information about + /// + public class ObjectDescriptor : IEquatable + { + + public static readonly ObjectDescriptor Empty = new ObjectDescriptor(); + + private readonly bool _isNotEmpty; + + /// + /// + /// + /// Returns type described by this instance of + /// associated with the + /// Factory enabling receiving properties of specific instance + /// Specifies whether the type should be treated as + /// + public ObjectDescriptor( + Type describedType, + IMemberAccessor memberAccessor, + Func> getProperties, + bool shouldEnumerate = false, + params object[] dependencies + ) + { + DescribedType = describedType; + GetProperties = getProperties; + MemberAccessor = memberAccessor; + ShouldEnumerate = shouldEnumerate; + Dependencies = dependencies; + + _isNotEmpty = true; + } + + private ObjectDescriptor(){ } + + /// + /// Specifies whether the type should be treated as + /// + public readonly bool ShouldEnumerate; + + /// + /// Contains dependencies for delegate + /// + public readonly object[] Dependencies; + + /// + /// Returns type described by this instance of + /// + public readonly Type DescribedType; + + /// + /// Factory enabling receiving properties of specific instance + /// + public readonly Func> GetProperties; + + /// + /// associated with the + /// + public readonly IMemberAccessor MemberAccessor; + + /// + public bool Equals(ObjectDescriptor other) + { + return _isNotEmpty == other?._isNotEmpty && DescribedType == other.DescribedType; + } + + /// + public override bool Equals(object obj) + { + return obj is ObjectDescriptor other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (_isNotEmpty.GetHashCode() * 397) ^ (DescribedType?.GetHashCode() ?? 0); + } + } + + /// + public static bool operator ==(ObjectDescriptor a, ObjectDescriptor b) + { + return Equals(a, b); + } + + /// + public static bool operator !=(ObjectDescriptor a, ObjectDescriptor b) + { + return !Equals(a, b); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs new file mode 100644 index 00000000..41ef1855 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Collections; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class ObjectDescriptorFactory : IObjectDescriptorProvider + { + private readonly IList _providers; + private readonly HashSetSlim _descriptorsNegativeCache = new HashSetSlim(); + private readonly LookupSlim> _descriptorsCache = new LookupSlim>(); + + private static readonly Func, DeferredValue> ValueFactory = (key, providers) => new DeferredValue(key, t => + { + for (var index = 0; index < providers.Count; index++) + { + var descriptorProvider = providers[index]; + if (!descriptorProvider.CanHandleType(t)) continue; + if (!descriptorProvider.TryGetDescriptor(t, out var descriptor)) continue; + + return descriptor; + } + + return ObjectDescriptor.Empty; + }); + + public ObjectDescriptorFactory(IList providers) + { + _providers = providers; + } + + public bool CanHandleType(Type type) + { + if (_descriptorsNegativeCache.Contains(type)) return false; + if (_descriptorsCache.TryGetValue(type, out var deferredValue) && !ReferenceEquals(deferredValue.Value, ObjectDescriptor.Empty)) return true; + + deferredValue = _descriptorsCache.GetOrAdd(type, ValueFactory, _providers); + return !ReferenceEquals(deferredValue.Value, ObjectDescriptor.Empty); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + if (_descriptorsCache.TryGetValue(type, out var deferredValue)) + { + value = deferredValue.Value; + return true; + } + + value = ObjectDescriptor.Empty; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs new file mode 100644 index 00000000..9446f667 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -0,0 +1,49 @@ +using System; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class ObjectDescriptorProvider : IObjectDescriptorProvider + { + private readonly Type _dynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); + private readonly LookupSlim> _membersCache = new LookupSlim>(); + private readonly ReflectionMemberAccessor _reflectionMemberAccessor; + + public ObjectDescriptorProvider(InternalHandlebarsConfiguration configuration) + { + _reflectionMemberAccessor = new ReflectionMemberAccessor(configuration); + } + + public bool CanHandleType(Type type) + { + return !_dynamicMetaObjectProviderType.IsAssignableFrom(type) && type != typeof(string); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = new ObjectDescriptor(type, _reflectionMemberAccessor, (descriptor, o) => + { + var cache = (LookupSlim>) descriptor.Dependencies[0]; + return cache.GetOrAdd(descriptor.DescribedType, DescriptorValueFactory).Value; + }, dependencies: _membersCache); + + return true; + } + + private static readonly Func> DescriptorValueFactory = + key => + { + return new DeferredValue(key, type => + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(o => o.CanRead && o.GetIndexParameters().Length == 0); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + return properties.Cast().Concat(fields).Select(o => o.Name).ToArray(); + }); + }; + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs new file mode 100644 index 00000000..b1f591c7 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal sealed class StringDictionaryObjectDescriptorProvider : IObjectDescriptorProvider + { + private static readonly object[] EmptyArray = ArrayEx.Empty(); + private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(StringDictionaryObjectDescriptorProvider).GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); + + private readonly LookupSlim> _typeCache = new LookupSlim>(); + + public bool CanHandleType(Type type) + { + return _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value != null; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + var interfaceType = _typeCache.TryGetValue(type, out var deferredValue) + ? deferredValue.Value + : _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; + + if (interfaceType == null) + { + value = ObjectDescriptor.Empty; + return false; + } + + var descriptorCreator = CreateDescriptorMethodInfo + .MakeGenericMethod(interfaceType.GetGenericArguments()[1]); + + value = (ObjectDescriptor) descriptorCreator.Invoke(null, EmptyArray); + return true; + } + + private static readonly Func> InterfaceTypeValueFactory = + key => new DeferredValue(key, type => + { + return type.GetInterfaces() + .FirstOrDefault(i => + i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && + i.GetGenericArguments()[0] == typeof(string)); + }); + + private static ObjectDescriptor CreateDescriptor() + { + return new ObjectDescriptor( + typeof(IDictionary), + new DictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys + ); + } + + private class DictionaryAccessor : IMemberAccessor + { + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + var dictionary = (IDictionary) instance; + if (dictionary.TryGetValue(memberName, out var v)) + { + value = v; + return true; + } + + value = default(TV); + return false; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectExtensions.cs b/source/Handlebars/ObjectExtensions.cs new file mode 100644 index 00000000..98a5db1f --- /dev/null +++ b/source/Handlebars/ObjectExtensions.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace HandlebarsDotNet +{ + internal static class ObjectExtensions + { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T As(this object source) => (T) source; + } + + internal static class ReadWriteLockExtensions + { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DisposableContainer UseRead(this ReaderWriterLockSlim @lock) + { + @lock.EnterReadLock(); + return new DisposableContainer(@lock, self => self.ExitReadLock()); + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DisposableContainer UseWrite(this ReaderWriterLockSlim @lock) + { + @lock.EnterWriteLock(); + return new DisposableContainer(@lock, self => self.ExitWriteLock()); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Polyfills/ArrayEx.cs b/source/Handlebars/Polyfills/ArrayEx.cs new file mode 100644 index 00000000..40dda581 --- /dev/null +++ b/source/Handlebars/Polyfills/ArrayEx.cs @@ -0,0 +1,16 @@ +using System; + +namespace HandlebarsDotNet.Polyfills +{ + internal static class ArrayEx + { + public static T[] Empty() + { +#if !netstandard + return new T[0]; +#else + return Array.Empty(); +#endif + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Polyfills/StringExtensions.cs b/source/Handlebars/Polyfills/StringExtensions.cs new file mode 100644 index 00000000..bd462fc9 --- /dev/null +++ b/source/Handlebars/Polyfills/StringExtensions.cs @@ -0,0 +1,15 @@ +namespace HandlebarsDotNet.Polyfills +{ + internal static class StringExtensions + { + public static string Intern(this string str) + { + if (string.IsNullOrEmpty(str)) return str; +#if netstandard1_3 + return str; +#else + return string.Intern(str); +#endif + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs new file mode 100644 index 00000000..518bc99c --- /dev/null +++ b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs @@ -0,0 +1,57 @@ +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + internal class BindingContextValueProvider : IValueProvider + { + private readonly BindingContext _context; + + public BindingContextValueProvider(BindingContext context) + { + _context = context; + } + + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; + + public bool TryGetValue(ref ChainSegment segment, out object value) + { + switch (segment.LowerInvariant) + { + case "root": + value = _context.Root; + return true; + + case "parent": + value = _context.ParentContext; + return true; + + default: + return TryGetContextVariable(_context.Value, ref segment, out value); + } + } + + private bool TryGetContextVariable(object instance, ref ChainSegment segment, out object value) + { + value = null; + if (instance == null) return false; + + var instanceType = instance.GetType(); + var descriptorProvider = _context.Configuration.ObjectDescriptorProvider; + if( + descriptorProvider.CanHandleType(instanceType) && + descriptorProvider.TryGetDescriptor(instanceType, out var descriptor) && + descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment, out value) + ) + { + return true; + } + + return _context.ParentContext?.TryGetContextVariable(ref segment, out value) ?? false; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/IValueProvider.cs b/source/Handlebars/ValueProviders/IValueProvider.cs new file mode 100644 index 00000000..03ee9839 --- /dev/null +++ b/source/Handlebars/ValueProviders/IValueProvider.cs @@ -0,0 +1,18 @@ +using System; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + [Flags] + internal enum ValueTypes + { + Context = 1, + All = 2 + } + + internal interface IValueProvider : IDisposable + { + ValueTypes SupportedValueTypes { get; } + bool TryGetValue(ref ChainSegment segment, out object value); + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/IteratorValueProvider.cs b/source/Handlebars/ValueProviders/IteratorValueProvider.cs new file mode 100644 index 00000000..ed5b24bb --- /dev/null +++ b/source/Handlebars/ValueProviders/IteratorValueProvider.cs @@ -0,0 +1,78 @@ +using HandlebarsDotNet.Compiler.Structure.Path; +using Microsoft.Extensions.ObjectPool; + +namespace HandlebarsDotNet.ValueProviders +{ + internal class IteratorValueProvider : IValueProvider + { + private static readonly IteratorValueProviderPool Pool = new IteratorValueProviderPool(); + + public static IteratorValueProvider Create() + { + return Pool.Get(); + } + + public object Value { get; set; } + + public int Index { get; set; } + + public bool First { get; set; } + + public bool Last { get; set; } + + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; + + public virtual bool TryGetValue(ref ChainSegment segment, out object value) + { + switch (segment.LowerInvariant) + { + case "index": + value = Index; + return true; + case "first": + value = First; + return true; + case "last": + value = Last; + return true; + case "value": + value = Value; + return true; + + default: + value = null; + return false; + } + } + + public virtual void Dispose() + { + Pool.Return(this); + } + + private class IteratorValueProviderPool : DefaultObjectPool + { + public IteratorValueProviderPool() : base(new IteratorValueProviderPolicy()) + { + } + + private class IteratorValueProviderPolicy : IPooledObjectPolicy + { + IteratorValueProvider IPooledObjectPolicy.Create() + { + return new IteratorValueProvider(); + } + + bool IPooledObjectPolicy.Return(IteratorValueProvider item) + { + item.First = true; + item.Last = false; + item.Index = 0; + item.Value = null; + + return true; + } + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs new file mode 100644 index 00000000..c62d5c07 --- /dev/null +++ b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs @@ -0,0 +1,70 @@ +using HandlebarsDotNet.Compiler.Structure.Path; +using Microsoft.Extensions.ObjectPool; + +namespace HandlebarsDotNet.ValueProviders +{ + internal class ObjectEnumeratorValueProvider : IteratorValueProvider + { + private HandlebarsConfiguration _configuration; + + private static readonly ObjectEnumeratorValueProviderPool Pool = new ObjectEnumeratorValueProviderPool(); + + public static ObjectEnumeratorValueProvider Create(HandlebarsConfiguration configuration) + { + var provider = Pool.Get(); + provider._configuration = configuration; + return provider; + } + + public string Key { get; set; } + + public override bool TryGetValue(ref ChainSegment segment, out object value) + { + switch (segment.LowerInvariant) + { + case "key": + value = Key; + return true; + + case "last" when !_configuration.Compatibility.SupportLastInObjectIterations: + value = null; + return true; + + default: + return base.TryGetValue(ref segment, out value); + } + } + + public override void Dispose() + { + Pool.Return(this); + } + + private class ObjectEnumeratorValueProviderPool : DefaultObjectPool + { + public ObjectEnumeratorValueProviderPool() : base(new ObjectEnumeratorValueProviderPolicy()) + { + } + + private class ObjectEnumeratorValueProviderPolicy : IPooledObjectPolicy + { + ObjectEnumeratorValueProvider IPooledObjectPolicy.Create() + { + return new ObjectEnumeratorValueProvider(); + } + + bool IPooledObjectPolicy.Return(ObjectEnumeratorValueProvider item) + { + item.First = true; + item.Last = false; + item.Index = 0; + item.Value = null; + item.Key = null; + item._configuration = null; + + return true; + } + } + } + } +} \ No newline at end of file