From 47a2b912d6b2ad62be01dd23d66d091c90e2e07b Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 15 Mar 2020 10:55:08 -0700 Subject: [PATCH 01/53] Create pull_request.yml --- .github/workflows/pull_request.yml | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..1962ea20 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,54 @@ +name: Pull Request + +on: + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Build + working-directory: ./source + run: dotnet build -c Release ./Handlebars/Handlebars.csproj + + test: + runs-on: windows-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj --logger:trx + + sonar-pr: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Sonarscanner for dotnet + uses: Secbyte/dotnet-sonarscanner@v2.2 + with: + buildCommand: dotnet build source/Handlebars/Handlebars.csproj -f netstandard2.0 + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + projectKey: zjklee_handlebars.csharp + projectName: handlebars.csharp + sonarOrganisation: zjklee + beginArguments: > + /d:sonar.verbose="true" + /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' + /d:sonar.pullrequest.key=${{ github.event.number }} + /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} + /d:sonar.pullrequest.base=${{ github.event.pull_request.base.ref }} + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 54d3f1ed32484b4bd5451be83be532d35c94beda Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 15 Mar 2020 11:15:00 -0700 Subject: [PATCH 02/53] Create ci.yml --- .github/workflows/ci.yml | 73 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f27dfff4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + branches: [ master ] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Build Main + working-directory: ./source + run: dotnet build Handlebars.sln -c Release + + test: + name: Test + runs-on: windows-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test Handlebars.sln --logger:trx + + sonar-ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Sonarscanner for dotnet + uses: Secbyte/dotnet-sonarscanner@v2.2 + with: + buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + projectKey: zjklee_handlebars.csharp + projectName: handlebars.csharp + sonarOrganisation: zjklee + beginArguments: > + /d:sonar.verbose="true" + /d:sonar.cs.opencover.reportsPaths='"*.opencover.xml"' + /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' + + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + publish: + needs: [build,test,sonar-ci] + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.100 + + - name: publish + working-directory: ./source + run: dotnet pack Handlebars.Code.sln -c Release /p:version=1.0.0-beta-${{ github.run_number }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json From 8311487ac4d37086bc78f656abef040804c7495c Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 15 Mar 2020 11:16:07 -0700 Subject: [PATCH 03/53] Create release.yml --- .github/workflows/release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ce4a5237 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Release + +on: + release: + types: [published] + +jobs: + publish: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.100 + + - name: publish + working-directory: ./source + run: dotnet pack Handlebars.Code.sln -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json From 8eae4682e5b07db68c383235742e8d29b2779b27 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 15 Mar 2020 11:17:00 -0700 Subject: [PATCH 04/53] Update pull_request.yml --- .github/workflows/pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1962ea20..43881793 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: dotnet-version: 3.1.102 - name: Build working-directory: ./source - run: dotnet build -c Release ./Handlebars/Handlebars.csproj + run: dotnet build Handlebars.Code.sln -c Release test: runs-on: windows-latest @@ -38,7 +38,7 @@ jobs: - name: Sonarscanner for dotnet uses: Secbyte/dotnet-sonarscanner@v2.2 with: - buildCommand: dotnet build source/Handlebars/Handlebars.csproj -f netstandard2.0 + buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp From 942420f17a1747cd9bfbae4cd5adde6fe020b051 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 15:25:34 -0700 Subject: [PATCH 05/53] Add `Handlebars.Code.sln` --- source/Handlebars.Code.sln | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 source/Handlebars.Code.sln diff --git a/source/Handlebars.Code.sln b/source/Handlebars.Code.sln new file mode 100644 index 00000000..1a5ef6e5 --- /dev/null +++ b/source/Handlebars.Code.sln @@ -0,0 +1,66 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.3 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars", "Handlebars\Handlebars.csproj", "{A09CFF95-B671-48FE-96A8-D3CBDC110B75}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" + ProjectSection(SolutionItems) = preProject + ..\README.md = ..\README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.inheritsSet = VisualStudio + $1.inheritsScope = text/plain + $1.scope = text/x-csharp + $0.CSharpFormattingPolicy = $2 + $2.IndentSwitchBody = True + $2.IndentBlocksInsideExpressions = True + $2.AnonymousMethodBraceStyle = NextLine + $2.PropertyBraceStyle = NextLine + $2.PropertyGetBraceStyle = NextLine + $2.PropertySetBraceStyle = NextLine + $2.EventBraceStyle = NextLine + $2.EventAddBraceStyle = NextLine + $2.EventRemoveBraceStyle = NextLine + $2.StatementBraceStyle = NextLine + $2.ElseNewLinePlacement = NewLine + $2.CatchNewLinePlacement = NewLine + $2.FinallyNewLinePlacement = NewLine + $2.WhileNewLinePlacement = DoNotCare + $2.ArrayInitializerWrapping = DoNotChange + $2.ArrayInitializerBraceStyle = NextLine + $2.BeforeMethodDeclarationParentheses = False + $2.BeforeMethodCallParentheses = False + $2.BeforeConstructorDeclarationParentheses = False + $2.NewLineBeforeConstructorInitializerColon = NewLine + $2.NewLineAfterConstructorInitializerColon = SameLine + $2.BeforeDelegateDeclarationParentheses = False + $2.NewParentheses = False + $2.SpacesBeforeBrackets = False + $2.inheritsSet = Mono + $2.inheritsScope = text/x-csharp + $2.scope = text/x-csharp + $0.DotNetNamingPolicy = $3 + $3.DirectoryNamespaceAssociation = None + $3.ResourceNamePolicy = FileFormatDefault + version = 1.0.0 + EndGlobalSection +EndGlobal From d7e3c112ad75bebd3f828ac8ea6d4df9437e0404 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 15:43:43 -0700 Subject: [PATCH 06/53] Update pull_request.yml --- .github/workflows/pull_request.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 43881793..45e4d681 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,6 +26,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.102 + - name: Setup dotnet (legacy) + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 1.1.2 - name: Test working-directory: ./source run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj --logger:trx From cfea4b0d490e00d66cdf72b791d14ac226df4049 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 15:47:46 -0700 Subject: [PATCH 07/53] Update pull_request.yml --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 45e4d681..a32881ef 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -29,7 +29,7 @@ jobs: - name: Setup dotnet (legacy) uses: actions/setup-dotnet@v1 with: - dotnet-version: 1.1.2 + dotnet-version: 1.1.14 - name: Test working-directory: ./source run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj --logger:trx From 53e01096c52e4ff41d3d8e3bf65baf83c0e80745 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 15:55:13 -0700 Subject: [PATCH 08/53] Add netcoreapp3.1 --- source/Handlebars.Test/Handlebars.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index f9157cfe..e9c73b82 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - net461;netcoreapp1.1;netcoreapp2.0 + net461;netcoreapp1.1;netcoreapp2.0;netcoreapp3.1 From c8f5ccccb41baa0e104bb4a6f8a99f4a777c3ac4 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 15:56:01 -0700 Subject: [PATCH 09/53] Update pull_request.yml --- .github/workflows/pull_request.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a32881ef..bf218810 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,13 +26,9 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.102 - - name: Setup dotnet (legacy) - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 1.1.14 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx sonar-pr: runs-on: ubuntu-latest From b8b548b047875d615579ad88d2d83899d1150d88 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 16 Mar 2020 16:12:48 -0700 Subject: [PATCH 10/53] Fix tests --- source/Handlebars.Test/Handlebars.Test.csproj | 3 +++ source/Handlebars.Test/WhitespaceTests.cs | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index e9c73b82..662482e0 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -11,6 +11,9 @@ $(DefineConstants);netstandard + + $(DefineConstants);netstandard + diff --git a/source/Handlebars.Test/WhitespaceTests.cs b/source/Handlebars.Test/WhitespaceTests.cs index cf73457d..ca64a623 100644 --- a/source/Handlebars.Test/WhitespaceTests.cs +++ b/source/Handlebars.Test/WhitespaceTests.cs @@ -54,7 +54,8 @@ public void ComplexTest() {{~/if~}} {{~/each}}"; - var template = Handlebars.Compile(source); + var h = Handlebars.Create(); + var template = h.Compile(source); var data = new { nav = new [] { new { @@ -77,7 +78,8 @@ public void ComplexTest() public void StandaloneEach() { var source = "Links:\n {{#each nav}}\n \n {{#if test}}\n {{title}}\n {{else}}\n Empty\n {{/if}}\n \n {{/each}}"; - var template = Handlebars.Compile(source); + var h = Handlebars.Create(); + var template = h.Compile(source); var data = new { nav = new[] From e367f9e8e1e39839e629226baebad17eef8161de Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 17 Mar 2020 08:17:17 -0700 Subject: [PATCH 11/53] Update ci.yml --- .github/workflows/ci.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f27dfff4..9166b109 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: dotnet-version: 3.1.102 - name: Test working-directory: ./source - run: dotnet test Handlebars.sln --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx sonar-ci: runs-on: ubuntu-latest @@ -62,12 +62,16 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 - + with: + fetch-depth: 0 - name: Setup dotnet uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.100 - + - name: Fetch + run: git fetch --depth 500 + - name: Determine version + run: echo "::set-env name=VERSION::$(git describe --tags --abbrev=0)" - name: publish working-directory: ./source - run: dotnet pack Handlebars.Code.sln -c Release /p:version=1.0.0-beta-${{ github.run_number }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + run: dotnet pack Handlebars.Code.sln -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json From 2f09a18a56613878b84621e3f558ebdd671a20d2 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 7 Jan 2020 12:35:36 -0800 Subject: [PATCH 12/53] Support for BlockParams --- .../Handlebars.Test/BasicIntegrationTests.cs | 140 ++++++- source/Handlebars.Test/HelperTests.cs | 27 ++ source/Handlebars/BuiltinHelpers.cs | 8 +- .../Handlebars/Compiler/ExpressionBuilder.cs | 1 + .../BlockHelperAccumulatorContext.cs | 3 +- .../IteratorBlockAccumulatorContext.cs | 7 +- .../Lexer/Converter/BlockParamsConverter.cs | 44 +++ .../Lexer/Parsers/BlockParamsParser.cs | 39 ++ source/Handlebars/Compiler/Lexer/Tokenizer.cs | 50 +-- .../Lexer/Tokens/BlockParameterToken.cs | 19 + .../Handlebars/Compiler/Lexer/Tokens/Token.cs | 5 + .../Compiler/Lexer/Tokens/TokenType.cs | 3 +- .../Compiler/Structure/BindingContext.cs | 109 +++--- .../Structure/BindingContextValueProvider.cs | 39 ++ .../Structure/BlockHelperExpression.cs | 21 ++ .../Structure/BlockParamsExpression.cs | 43 +++ .../Structure/BlockParamsValueProvider.cs | 77 ++++ .../Structure/HandlebarsExpression.cs | 17 +- .../Compiler/Structure/IValueProvider.cs | 8 + .../Compiler/Structure/IteratorExpression.cs | 11 +- .../Compiler/Structure/PathResolver.cs | 342 +++++++++++++++++ .../Expression/BlockHelperFunctionBinder.cs | 14 +- .../Expression/HandlebarsExpressionVisitor.cs | 116 +++--- .../Translation/Expression/IteratorBinder.cs | 330 ++++++++++------- .../Translation/Expression/PartialBinder.cs | 36 +- .../Translation/Expression/PathBinder.cs | 346 +----------------- source/Handlebars/HelperOptions.cs | 25 +- 27 files changed, 1194 insertions(+), 686 deletions(-) create mode 100644 source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs create mode 100644 source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs create mode 100644 source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs create mode 100644 source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/BlockParamsExpression.cs create mode 100644 source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/IValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/PathResolver.cs diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 42442cd4..87d703fc 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -144,18 +144,9 @@ 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] @@ -365,6 +356,23 @@ public void BasicWith() var result = template(data); Assert.Equal("Hello, my good friend Erik!", result); } + + [Fact] + public void WithWithBlockParams() + { + 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() @@ -466,6 +474,23 @@ public void BasicObjectEnumeratorWithKey() var result = template(data); Assert.Equal("foo: hello bar: world ", result); } + + [Fact] + public void ObjectEnumeratorWithBlockParams() + { + 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("foo: hello bar: world ", result); + } [Fact] public void BasicDictionaryEnumerator() @@ -484,6 +509,23 @@ public void BasicDictionaryEnumerator() Assert.Equal("hello world ", result); } + [Fact] + public void DictionaryEnumeratorWithBlockParams() + { + 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("foo hello bar world ", result); + } + [Fact] public void DictionaryWithLastEnumerator() { @@ -1408,6 +1450,73 @@ public void ImplicitIDictionaryImplementationShouldNotThrowNullref() // Act compile.Invoke(mock); } + + [Fact] + public void ShouldBeAbleToHandleFieldContainingDots() + { + 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); + } + + [Fact] + public void ShouldBeAbleToHandleListWithNumericalFields() + { + var source = "{{ [0] }}"; + var template = Handlebars.Compile(source); + var data = new List {"FOOBAR"}; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Fact] + public void ShouldBeAbleToHandleDictionaryWithNumericalFields() + { + var source = "{{ [0] }}"; + var template = Handlebars.Compile(source); + var data = new Dictionary + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Fact] + public void ShouldBeAbleToHandleJObjectsWithNumericalFields() + { + var source = "{{ [0] }}"; + var template = Handlebars.Compile(source); + var data = new JObject + { + {"0", "FOOBAR"}, + }; + var result = template(data); + Assert.Equal("FOOBAR", result); + } + + [Fact] + public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() + { + 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); + } [Fact] public void ShouldBeAbleToHandleFieldContainingDots() @@ -1484,7 +1593,7 @@ public void Add(string key, string value) } public bool ContainsKey(string key) { - return true; + throw new NotImplementedException(); } public bool Remove(string key) { @@ -1492,13 +1601,14 @@ 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] { get { - return "Hello world!"; + throw new NotImplementedException(); } set { diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 38c9dae3..9f612693 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -2,6 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet.Test { @@ -28,6 +30,31 @@ public void HelperWithLiteralArguments() Assert.Equal(expected, output); } + + [Fact] + public void BlockHelperWithBlockParams() + { + Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { + var count = 0; + options.BlockParams((parameters, binder) => + binder(parameters.Keys.First(), () => ++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 HelperWithLiteralArgumentsWithQuotes() diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs index 046ab415..8cf1594b 100644 --- a/source/Handlebars/BuiltinHelpers.cs +++ b/source/Handlebars/BuiltinHelpers.cs @@ -1,12 +1,13 @@ -using System; +using System; using System.IO; using System.Reflection; using System.Collections.Generic; +using System.Linq; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { - internal static class BuiltinHelpers + internal class BuiltinHelpers { [Description("with")] public static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) @@ -16,6 +17,9 @@ public static void With(TextWriter output, HelperOptions options, dynamic contex throw new HandlebarsException("{{with}} helper must have exactly one argument"); } + options.BlockParams((parameters, binder) => + binder(parameters.Keys.First(), () => arguments[0])); + if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) { options.Template(output, arguments[0]); diff --git a/source/Handlebars/Compiler/ExpressionBuilder.cs b/source/Handlebars/Compiler/ExpressionBuilder.cs index 02f2dddd..732638b2 100644 --- a/source/Handlebars/Compiler/ExpressionBuilder.cs +++ b/source/Handlebars/Compiler/ExpressionBuilder.cs @@ -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/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs index 4c16b38a..f6cb0f20 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs @@ -73,7 +73,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/IteratorBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs index b88f8ff8..89c57a23 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/IteratorBlockAccumulatorContext.cs @@ -25,7 +25,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 +45,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..b654ac38 --- /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).ToList(); + } + + 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/Parsers/BlockParamsParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs new file mode 100644 index 00000000..cdd08b8a --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParamsParser : Parser + { + public override Token Parse(TextReader reader) + { + var buffer = AccumulateWord(reader); + return !string.IsNullOrEmpty(buffer) + ? Token.BlockParams(buffer) + : null; + } + + private static string AccumulateWord(TextReader reader) + { + var buffer = new StringBuilder(); + + 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"); + + return accumulateWord; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index 7326dd8e..2dafffa8 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -10,11 +10,12 @@ 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(); + 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 Tokenizer(HandlebarsConfiguration configuration) @@ -65,23 +66,24 @@ private IEnumerable Parse(TextReader source) } 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); + 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); + token = token ?? BlockParamsParser.Parse(source); if (token != null) { - yield return token; - - if ((char)source.Peek() == '=') - { - source.Read(); - yield return Token.Assignment(); - continue; - } - } + yield return token; + + if ((char)source.Peek() == '=') + { + source.Read(); + yield return Token.Assignment(); + continue; + } + } if ((char)node == '}' && (char)source.Read() == '}') { bool escaped = true; @@ -90,11 +92,11 @@ private IEnumerable Parse(TextReader source) { node = source.Read(); escaped = false; - } - if ((char)source.Peek() == '}') - { - node = source.Read(); - raw = true; + } + if ((char)source.Peek() == '}') + { + node = source.Read(); + raw = true; } node = source.Read(); yield return Token.EndExpression(escaped, trimWhitespace, raw); diff --git a/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs new file mode 100644 index 00000000..7572a499 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs @@ -0,0 +1,19 @@ +using System; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class BlockParameterToken : Token + { + public BlockParameterToken(string value) + { + Value = value; + } + + public override TokenType Type + { + get { return TokenType.BlockParams; } + } + + public override string Value { get; } + } +} \ 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..56e0b6b3 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/Token.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/Token.cs @@ -62,5 +62,10 @@ public static AssignmentToken Assignment() { return new AssignmentToken(); } + + public static BlockParameterToken BlockParams(string blockParams) + { + return new BlockParameterToken(blockParams); + } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs index e325e1ba..2e93156d 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs @@ -15,7 +15,8 @@ internal enum TokenType Layout = 8, StartSubExpression = 9, EndSubExpression = 10, - Assignment = 11 + Assignment = 11, + BlockParams = 12 } } diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 849e3032..b8baa8c7 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -7,23 +7,8 @@ namespace HandlebarsDotNet.Compiler { internal class BindingContext { - private readonly object _value; - private readonly BindingContext _parent; - - 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 - { - get { return TextWriter.SuppressEncoding; } - set { TextWriter.SuppressEncoding = value; } - } - + private readonly List _valueProviders = new List(); + public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : this(value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } @@ -32,10 +17,12 @@ public BindingContext(object value, EncodedTextWriter writer, BindingContext par public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) { + RegisterValueProvider(new BindingContextValueProvider(this)); + TemplatePath = parent != null ? (parent.TemplatePath ?? templatePath) : templatePath; TextWriter = writer; - _value = value; - _parent = parent; + Value = value; + ParentContext = parent; //Inline partials cannot use the Handlebars.RegisteredTemplate method //because it pollutes the static dictionary and creates collisions @@ -70,69 +57,59 @@ public BindingContext(object value, EncodedTextWriter writer, BindingContext par PartialBlockTemplate = partialBlockTemplate; } - public virtual object Value - { - get { return _value; } - } + public string TemplatePath { get; } + + public EncodedTextWriter TextWriter { get; } - public virtual BindingContext ParentContext + public IDictionary> InlinePartialTemplates { get; } + + public Action PartialBlockTemplate { get; } + + public bool SuppressEncoding { - get { return _parent; } + get => TextWriter.SuppressEncoding; + set => TextWriter.SuppressEncoding = value; } + + public virtual object Value { get; } + + public virtual BindingContext ParentContext { get; } - public virtual object Root + public virtual object Root => ParentContext?.Root ?? this; + + public void RegisterValueProvider(IValueProvider valueProvider) { - get - { - var currentContext = this; - while (currentContext.ParentContext != null) - { - currentContext = currentContext.ParentContext; - } - return currentContext.Value; - } + if(valueProvider == null) throw new ArgumentNullException(nameof(valueProvider)); + + _valueProviders.Add(valueProvider); } public virtual object GetContextVariable(string variableName) { - 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--) + { + if (_valueProviders[index].TryGetValue(variableName, out var value)) return value; + } - return GetContextVariable(variableName, target) - ?? GetContextVariable(variableName, target.Value); + return null; } - - private object GetContextVariable(string variableName, object target) + + public virtual object GetVariable(string variableName) { - object returnValue; - variableName = variableName.TrimStart('@'); - var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (member.Length > 0) - { - if (member[0] is PropertyInfo) - { - returnValue = ((PropertyInfo)member[0]).GetValue(target, null); - } - else if (member[0] is FieldInfo) - { - returnValue = ((FieldInfo)member[0]).GetValue(target); - } - else - { - throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); - } - } - else if (_parent != null) + // accessing value providers in reverse order as it gives more probability of hit + for (var index = _valueProviders.Count - 1; index >= 0; index--) { - returnValue = _parent.GetContextVariable(variableName); + var valueProvider = _valueProviders[index]; + if(!valueProvider.ProvidesNonContextVariables) continue; + + if (valueProvider.TryGetValue(variableName, out var value)) return value; } - else - { - returnValue = null; - } - return returnValue; + + return ParentContext?.GetVariable(variableName); } - private IDictionary GetContextDictionary(object target) + private static IDictionary GetContextDictionary(object target) { var dict = new Dictionary(); diff --git a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs new file mode 100644 index 00000000..d3725c99 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs @@ -0,0 +1,39 @@ +using System.Reflection; + +namespace HandlebarsDotNet.Compiler +{ + internal class BindingContextValueProvider : IValueProvider + { + private readonly BindingContext _context; + + public BindingContextValueProvider(BindingContext context) + { + _context = context; + } + + public bool ProvidesNonContextVariables { get; } = false; + + public bool TryGetValue(string memberName, out object value) + { + value = GetContextVariable(memberName, _context) ?? GetContextVariable(memberName, _context.Value); + return value != null; + } + + private object GetContextVariable(string variableName, object target) + { + variableName = variableName.TrimStart('@'); + var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (member.Length <= 0) return _context.ParentContext?.GetContextVariable(variableName); + + switch (member[0]) + { + case PropertyInfo propertyInfo: + return propertyInfo.GetValue(target, null); + case FieldInfo fieldInfo: + return fieldInfo.GetValue(target); + default: + throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs index 5273bc5b..da158ad2 100644 --- a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using System.Collections.Generic; +using System.Linq; namespace HandlebarsDotNet.Compiler { @@ -8,6 +9,7 @@ internal class BlockHelperExpression : HelperExpression { private readonly Expression _body; private readonly Expression _inversion; + private readonly BlockParamsExpression _blockParams; public BlockHelperExpression( string helperName, @@ -15,10 +17,24 @@ public BlockHelperExpression( Expression body, Expression inversion, bool isRaw = false) + : this(helperName, arguments, BlockParamsExpression.Empty(), body, inversion, isRaw) + { + _body = body; + _inversion = inversion; + } + + public BlockHelperExpression( + string helperName, + IEnumerable arguments, + BlockParamsExpression blockParams, + Expression body, + Expression inversion, + bool isRaw = false) : base(helperName, arguments, isRaw) { _body = body; _inversion = inversion; + _blockParams = blockParams; } public Expression Body @@ -31,6 +47,11 @@ public Expression Inversion get { return _inversion; } } + public BlockParamsExpression BlockParams + { + get { return _blockParams; } + } + public override ExpressionType NodeType { get { return (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..17935347 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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)); + } + } + + 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..02103e83 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HandlebarsDotNet.Compiler +{ + /// + /// Parameters passed to BlockParams. + /// Function that perform binding of parameter to . + public delegate void ConfigureBlockParams(IReadOnlyDictionary> parameters, ValueBinder valueBinder); + + /// + /// 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. + public delegate void ValueBinder(string variableName, Func valueProvider); + + /// + internal class BlockParamsValueProvider : IValueProvider + { + private readonly BlockParam _params; + private readonly Action> _invoker; + private readonly Dictionary> _accessors; + private readonly PathResolver _pathResolver; + + public BlockParamsValueProvider(BindingContext context, HandlebarsConfiguration configuration, object @params) + { + _params = @params as BlockParam; + _invoker = action => action(context); + _pathResolver = new PathResolver(configuration); + _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + context.RegisterValueProvider(this); + } + + public bool ProvidesNonContextVariables { get; } = true; + + /// + /// Configures behavior of BlockParams. + /// + public void Configure(ConfigureBlockParams blockParamsConfiguration) + { + if(_params == null) return; + + Lazy ValueAccessor(BindingContext context, string paramName) => + new Lazy(() => _pathResolver.ResolvePath(context, paramName)); + + void BlockParamsAction(BindingContext context) + { + void ValueBinder(string name, Func value) + { + if (!string.IsNullOrEmpty(name)) _accessors[name] = value; + } + + var values = _params.Parameters + .ToDictionary(parameter => parameter, parameter => ValueAccessor(context, parameter)); + + blockParamsConfiguration.Invoke(values, ValueBinder); + } + + _invoker(BlockParamsAction); + } + + public bool TryGetValue(string param, out object value) + { + if (_accessors.TryGetValue(param, out var valueProvider)) + { + value = valueProvider(); + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs index 38326a89..01f89456 100644 --- a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs +++ b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs @@ -17,7 +17,8 @@ internal enum HandlebarsExpressionType SubExpression = 6009, HashParameterAssignmentExpression = 6010, HashParametersExpression = 6011, - CommentExpression = 6012 + CommentExpression = 6012, + BlockParamsExpression = 6013 } internal abstract class HandlebarsExpression : Expression @@ -35,17 +36,23 @@ public static HelperExpression Helper(string helperName, bool isRaw = false) 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,17 +66,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); + return new IteratorExpression(sequence, blockParams, template, ifEmpty); } public static DeferredSectionExpression DeferredSection( diff --git a/source/Handlebars/Compiler/Structure/IValueProvider.cs b/source/Handlebars/Compiler/Structure/IValueProvider.cs new file mode 100644 index 00000000..f2bd176e --- /dev/null +++ b/source/Handlebars/Compiler/Structure/IValueProvider.cs @@ -0,0 +1,8 @@ +namespace HandlebarsDotNet.Compiler +{ + internal interface IValueProvider + { + bool ProvidesNonContextVariables { get; } + bool TryGetValue(string memberName, out object value); + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index b42498aa..60c77b25 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +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; @@ -16,6 +18,13 @@ public IteratorExpression(Expression sequence, Expression template) } public IteratorExpression(Expression sequence, Expression template, Expression ifEmpty) + :this(sequence, BlockParamsExpression.Empty(), template, ifEmpty) + { + + } + + public IteratorExpression(Expression sequence, BlockParamsExpression blockParams, Expression template, Expression ifEmpty) + :base("each", Enumerable.Empty(), blockParams, template, ifEmpty, false) { _sequence = sequence; _template = template; diff --git a/source/Handlebars/Compiler/Structure/PathResolver.cs b/source/Handlebars/Compiler/Structure/PathResolver.cs new file mode 100644 index 00000000..b5c364e2 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/PathResolver.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace HandlebarsDotNet.Compiler +{ + internal class PathResolver + { + private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); + + private readonly HandlebarsConfiguration _configuration; + + public PathResolver(HandlebarsConfiguration configuration) + { + _configuration = configuration; + } + + //TODO: make path resolution logic smarter + public 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; + foreach (var memberName in GetPathChain(segmentString)) + { + instance = ResolveValue(context, instance, memberName); + + if (!(instance is UndefinedBindingResult)) + continue; + + if (hashParameters == null || hashParameters.ContainsKey(memberName) || context.ParentContext == null) + { + if (_configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(path, (instance as UndefinedBindingResult).Value); + return instance; + } + + instance = ResolveValue(context.ParentContext, context.ParentContext.Value, memberName); + if (!(instance is UndefinedBindingResult result)) continue; + + if (_configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(path, result.Value); + return result; + } + } + } + return instance; + } + + private static IEnumerable GetPathChain(string segmentString) + { + 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; + } + + if (next.StartsWith("[")) + { + insideEscapeBlock = true; + } + + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } + + list.Add(next); + return list; + }); + + return pathChain; + } + + private object ResolveValue(BindingContext context, object instance, string segment) + { + var undefined = new UndefinedBindingResult(segment, _configuration); + object resolvedValue = undefined; + 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 + { + if (!TryAccessMember(instance, segment, out resolvedValue)) + { + resolvedValue = context.GetVariable(segment) ?? undefined; + } + } + return resolvedValue; + } + + private bool TryAccessMember(object instance, string memberName, out object value) + { + value = new UndefinedBindingResult(memberName, _configuration); + if (instance == null) + return false; + + var instanceType = instance.GetType(); + memberName = ResolveMemberName(instance, memberName); + memberName = TrimSquareBrackets(memberName); + + return TryAccessContextMember(instance, memberName, out value) + || TryAccessStringIndexerMember(instance, memberName, instanceType, out value) + || TryAccessIEnumerableMember(instance, memberName, out value) + || TryAccessGetValueMethod(instance, memberName, instanceType, out value) + || TryAccessDynamicMember(instance, memberName, out value) + || TryAccessIDictionaryMember(instance, memberName, out value) + || TryAccessMemberWithReflection(instance, memberName, instanceType, out value); + } + + private static bool TryAccessContextMember(object instance, string memberName, out object value) + { + value = null; + if (!(instance is BindingContext context)) return false; + + value = context.GetContextVariable(memberName); + return value != null; + } + + private static bool TryAccessStringIndexerMember(object instance, string memberName, Type instanceType, out object value) + { + value = null; + var stringIndexPropertyGetter = GetStringIndexPropertyGetter(instanceType); + if (stringIndexPropertyGetter == null) return false; + + try + { + value = stringIndexPropertyGetter.Invoke(instance, new object[] {memberName}); + return true; + } + catch + { + return false; + } + } + + private static bool TryAccessIEnumerableMember(object instance, string memberName, out object value) + { + value = null; + if (!(instance is IEnumerable enumerable)) return false; + + var match = IndexRegex.Match(memberName); + if (!match.Success) return false; + if (!match.Groups["index"].Success || !int.TryParse(match.Groups["index"].Value, out var index)) return false; + + value = enumerable.ElementAtOrDefault(index); + return true; + } + + private static bool TryAccessMemberWithReflection(object instance, string memberName, Type instanceType, out object value) + { + switch (GetMember(memberName, instanceType)) + { + case PropertyInfo propertyInfo: + value = propertyInfo.GetValue(instance, null); + return true; + + case FieldInfo fieldInfo: + value = fieldInfo.GetValue(instance); + return true; + + default: + value = null; + return false; + } + } + + private static bool TryAccessIDictionaryMember(object instance, 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. + if (!(instance is IDictionary dictionary)) return false; + { + value = dictionary[memberName]; + return true; + } + } + + private static bool TryAccessGetValueMethod(object instance, string memberName, Type instanceType, out object value) + { + value = null; + // Check if the instance is has TryGetValue method + var tryGetValueMethod = GetTryGetValueMethod(instanceType); + + if (tryGetValueMethod == null) return false; + + object key = memberName; + + // Dictionary key type isn't a string, so attempt to convert. + var keyType = tryGetValueMethod.GetParameters()[0].ParameterType; + if (keyType != typeof(string)) + { + if (!typeof(IConvertible).IsAssignableFrom(keyType)) + { + value = null; + return false; + } + + key = Convert.ChangeType(memberName, keyType); + } + + var methodParameters = new[] { key, null }; + var result = (bool) tryGetValueMethod.Invoke(instance, methodParameters); + if (!result) return true; + + value = methodParameters[1]; + return true; + } + + private static bool TryAccessDynamicMember(object instance, string memberName, out object value) + { + value = null; + //crude handling for dynamic objects that don't have metadata + if (!(instance is IDynamicMetaObjectProvider metaObjectProvider)) return false; + + try + { + value = GetProperty(metaObjectProvider, memberName); + return value != null; + } + catch + { + return false; + } + } + + private 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; + } + + private static MethodInfo GetTryGetValueMethod(Type type) + { + return type.GetMethods() + .Where(o => o.Name == nameof(IDictionary.TryGetValue)) + .Where(o => + { + var parameters = o.GetParameters(); + return parameters.Length == 2 && parameters[1].IsOut && o.ReturnType == typeof(bool); + }) + .SingleOrDefault(); + } + + private static MethodInfo GetStringIndexPropertyGetter(Type type) + { + return type + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(prop => prop.Name == "Item" && prop.CanRead) + .SingleOrDefault(prop => + { + var indexParams = prop.GetIndexParameters(); + return indexParams.Length == 1 && indexParams.Single().ParameterType == typeof(string); + })?.GetMethod; + } + + private static MemberInfo GetMember(string memberName, Type instanceType) + { + var members = instanceType.GetMember(memberName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (members.Length == 0) return null; + + var preferredMember = members.Length > 1 + ? members.FirstOrDefault(m => m.Name == memberName) ?? members[0] + : members[0]; + + return preferredMember; + } + + 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 = _configuration.ExpressionNameResolver; + return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 46a44b20..aaa552b2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,4 +1,7 @@ -using System.Linq.Expressions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace HandlebarsDotNet.Compiler @@ -39,7 +42,11 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b CompilationContext.BindingContext, typeof(BindingContext).GetProperty("Value")); - var body = fb.Compile(((BlockExpression)bhex.Body).Expressions, CompilationContext.BindingContext); + var configuration = Expression.Constant(CompilationContext.Configuration); + var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); + var blockParamsExpression = Expression.New(ctor, CompilationContext.BindingContext, configuration, bhex.BlockParams); + + 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[] @@ -50,7 +57,8 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b Expression.New( typeof(HelperOptions).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0], body, - inversion), + inversion, + blockParamsExpression), //this next arg is usually data, like { first: "Marc" } //but for inline partials this is the complete BindingContext. bindingContext, diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs index 41d70b78..7078043a 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -59,9 +59,9 @@ public override Expression Visit(Expression 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); + if (body != sex.Body) + { + return HandlebarsExpression.Statement(body, sex.IsEscaped, sex.TrimBefore, sex.TrimAfter); } return sex; } @@ -72,23 +72,23 @@ protected virtual Expression VisitPathExpression(PathExpression pex) } protected virtual Expression VisitHelperExpression(HelperExpression hex) - { - var arguments = VisitExpressionList(hex.Arguments); - if (arguments != hex.Arguments) - { - return HandlebarsExpression.Helper(hex.HelperName, arguments, hex.IsRaw); + { + 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) - { + { 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); + // Don't visit Body/Inversion - they will be compiled separately + + if (arguments != bhex.Arguments) + { + return HandlebarsExpression.BlockHelper(bhex.HelperName, arguments, bhex.BlockParams, bhex.Body, bhex.Inversion, bhex.IsRaw); } return bhex; } @@ -103,9 +103,9 @@ 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); + if (sequence != iex.Sequence) + { + return HandlebarsExpression.Iterator(sequence, iex.BlockParams, iex.Template, iex.IfEmpty); } return iex; } @@ -115,9 +115,9 @@ protected virtual Expression VisitDeferredSectionExpression(DeferredSectionExpre 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); + if (path != dsex.Path) + { + return HandlebarsExpression.DeferredSection(path, dsex.Body, dsex.Inversion); } return dsex; } @@ -129,9 +129,9 @@ protected virtual Expression VisitPartialExpression(PartialExpression pex) // Don't visit Fallback - it will be compiled separately if (partialName != pex.PartialName - || argument != pex.Argument) - { - return HandlebarsExpression.Partial(partialName, argument, pex.Fallback); + || argument != pex.Argument) + { + return HandlebarsExpression.Partial(partialName, argument, pex.Fallback); } return pex; } @@ -139,9 +139,9 @@ protected virtual Expression VisitPartialExpression(PartialExpression pex) protected virtual Expression VisitBoolishExpression(BoolishExpression bex) { Expression condition = Visit(bex.Condition); - if (condition != bex.Condition) - { - return HandlebarsExpression.Boolish(condition); + if (condition != bex.Condition) + { + return HandlebarsExpression.Boolish(condition); } return bex; } @@ -149,9 +149,9 @@ protected virtual Expression VisitBoolishExpression(BoolishExpression bex) protected virtual Expression VisitSubExpression(SubExpressionExpression subex) { Expression expression = Visit(subex.Expression); - if (expression != subex.Expression) - { - return HandlebarsExpression.SubExpression(expression); + if (expression != subex.Expression) + { + return HandlebarsExpression.SubExpression(expression); } return subex; } @@ -176,35 +176,35 @@ protected virtual Expression VisitHashParametersExpression(HashParametersExpress return hpex; } - IEnumerable VisitExpressionList(IEnumerable original) - { - if (original == null) - { - return original; - } - - var originalAsList = original as IReadOnlyList ?? original.ToArray(); - List list = null; - for (int i = 0, n = originalAsList.Count; i < n; i++) - { - Expression p = Visit(originalAsList[i]); - if (list != null) - { - list.Add(p); - } - else if (p != originalAsList[i]) - { - list = new List(n); - for (int j = 0; j < i; j++) - { - list.Add(originalAsList[j]); - } - list.Add(p); - } - } - if (list != null) - return list.ToArray(); - return original; + IEnumerable VisitExpressionList(IEnumerable original) + { + if (original == null) + { + return original; + } + + var originalAsList = original as IReadOnlyList ?? original.ToArray(); + List list = null; + for (int i = 0, n = originalAsList.Count; i < n; i++) + { + Expression p = Visit(originalAsList[i]); + if (list != null) + { + list.Add(p); + } + else if (p != originalAsList[i]) + { + list = new List(n); + for (int j = 0; j < i; j++) + { + list.Add(originalAsList[j]); + } + list.Add(p); + } + } + if (list != null) + return list.ToArray(); + return original; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index cdbebfd5..03838ba6 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -24,11 +24,17 @@ private IteratorBinder(CompilationContext context) protected override Expression VisitIteratorExpression(IteratorExpression iex) { var iteratorBindingContext = Expression.Variable(typeof(BindingContext), "context"); + var blockParamsValueBinder = Expression.Variable(typeof(BlockParamsValueProvider), "blockParams"); + var configuration = Expression.Constant(CompilationContext.Configuration); + var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); + return Expression.Block( new ParameterExpression[] { - iteratorBindingContext + iteratorBindingContext, blockParamsValueBinder }, + Expression.Assign(iteratorBindingContext, CompilationContext.BindingContext), + Expression.Assign(blockParamsValueBinder, Expression.New(ctor, iteratorBindingContext, configuration, iex.BlockParams)), Expression.IfThenElse( Expression.TypeIs(iex.Sequence, typeof(IEnumerable)), Expression.IfThenElse( @@ -37,109 +43,93 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) #else Expression.Call(new Func(IsNonListDynamic).Method, new[] { iex.Sequence }), #endif - GetDynamicIterator(iteratorBindingContext, iex), + GetDynamicIterator(iteratorBindingContext, blockParamsValueBinder, 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)) + GetDictionaryIterator(iteratorBindingContext, blockParamsValueBinder, iex), + GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, iex))), + GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, iex)) ); } - private Expression GetEnumerableIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetEnumerableIterator(Expression contextParameter, Expression blockParamsParameter, 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( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateEnumerable).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateEnumerable).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) - })); + new Expression[] + { + contextParameter, + blockParamsParameter, + 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) + private Expression GetObjectIterator(Expression contextParameter, Expression blockParamsParameter, 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( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateObject).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateObject).Method, #endif - new Expression[] - { - Expression.Convert(contextParameter, typeof(ObjectEnumeratorBindingContext)), - iex.Sequence, - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - })); + new[] + { + contextParameter, + blockParamsParameter, + iex.Sequence, + fb.Compile(new [] { iex.Template }, contextParameter), + fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) + }); } - private Expression GetDictionaryIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetDictionaryIterator(Expression contextParameter, Expression blockParamsParameter, 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( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action, Action>(IterateDictionary).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateDictionary).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) - })); + new[] + { + contextParameter, + blockParamsParameter, + Expression.Convert(iex.Sequence, typeof(IEnumerable)), + fb.Compile(new[] {iex.Template}, contextParameter), + fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) + }); } - private Expression GetDynamicIterator(Expression contextParameter, IteratorExpression iex) + private Expression GetDynamicIterator(Expression contextParameter, Expression blockParamsParameter, 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( + return Expression.Call( #if netstandard - new Action, Action>(Iterate).GetMethodInfo(), + new Action,Action>(IterateDynamic).GetMethodInfo(), #else - new Action, Action>(Iterate).Method, + new Action, Action>(IterateDynamic).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) - })); + new[] + { + contextParameter, + blockParamsParameter, + Expression.Convert(iex.Sequence, typeof(IDynamicMetaObjectProvider)), + fb.Compile(new[] {iex.Template}, contextParameter), + fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) + }); } private static bool IsNonListDynamic(object target) @@ -164,31 +154,41 @@ private static bool IsGenericDictionary(object target) .Any(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateObject( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, object target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); + binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); + binder(parameters.Keys.ElementAtOrDefault(2), () => target); + }); + + objectEnumerator.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; + objectEnumerator.Key = member.Name; + objectEnumerator.Value = AccessMember(target, member); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; - template(context.TextWriter, value); + template(context.TextWriter, objectEnumerator.Value); } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -199,42 +199,48 @@ private static void Iterate( } } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateDictionary( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IEnumerable target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); + binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); + binder(parameters.Keys.ElementAtOrDefault(2), () => target); + }); + + objectEnumerator.Index = 0; var targetType = target.GetType(); #if netstandard var keysProperty = targetType.GetRuntimeProperty("Keys"); #else var keysProperty = targetType.GetProperty("Keys"); #endif - if (keysProperty != null) + if (keysProperty?.GetGetMethod().Invoke(target, null) is IEnumerable keys) { - 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 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; - - template(context.TextWriter, value); - } + var key = parameters[0] = enumerableValue.Value; + objectEnumerator.Key = key.ToString(); + objectEnumerator.Value = getItemMethodInfo.Invoke(target, parameters); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; + + template(context.TextWriter, objectEnumerator.Value); } } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -245,29 +251,39 @@ private static void Iterate( } } - private static void Iterate( - ObjectEnumeratorBindingContext context, + private static void IterateDynamic( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IDynamicMetaObjectProvider target, Action template, Action ifEmpty) { if (HandlebarsUtils.IsTruthy(target)) { - context.Index = 0; + var objectEnumerator = new ObjectEnumeratorValueProvider(); + context.RegisterValueProvider(objectEnumerator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); + binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); + binder(parameters.Keys.ElementAtOrDefault(2), () => target); + }); + + objectEnumerator.Index = 0; var meta = target.GetMetaObject(Expression.Constant(target)); foreach (var enumerableValue in new ExtendedEnumerable(meta.GetDynamicMemberNames())) { var name = enumerableValue.Value; - context.Key = name; - var value = GetProperty(target, name); - context.First = enumerableValue.IsFirst; - context.Last = enumerableValue.IsLast; - context.Index = enumerableValue.Index; + objectEnumerator.Key = name; + objectEnumerator.Value = GetProperty(target, name); + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; - template(context.TextWriter, value); + template(context.TextWriter, objectEnumerator.Value); } - if (context.Index == 0) + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -278,24 +294,34 @@ private static void Iterate( } } - private static void Iterate( - IteratorBindingContext context, + private static void IterateEnumerable( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, IEnumerable sequence, Action template, Action ifEmpty) { - context.Index = 0; + var iterator = new IteratorValueProvider(); + context.RegisterValueProvider(iterator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.Keys.ElementAtOrDefault(0), () => iterator.Index); + binder(parameters.Keys.ElementAtOrDefault(1), () => iterator.Value); + binder(parameters.Keys.ElementAtOrDefault(2), () => sequence); + }); + + iterator.Index = 0; foreach (var enumeratorValue in new ExtendedEnumerable(sequence)) { - var item = enumeratorValue.Value; - context.First = enumeratorValue.IsFirst; - context.Last = enumeratorValue.IsLast; - context.Index = enumeratorValue.Index; + iterator.Value = enumeratorValue.Value; + iterator.First = enumeratorValue.IsFirst; + iterator.Last = enumeratorValue.IsLast; + iterator.Index = enumeratorValue.Index; - template(context.TextWriter, item); + template(context.TextWriter, iterator.Value); } - if (context.Index == 0) + if (iterator.Index == 0) { ifEmpty(context.TextWriter, context.Value); } @@ -308,41 +334,71 @@ private static object GetProperty(object target, string name) return site.Target(site, target); } - private class IteratorBindingContext : BindingContext + private class IteratorValueProvider : IValueProvider { - public IteratorBindingContext(BindingContext context) - : base(context.Value, context.TextWriter, context.ParentContext, context.TemplatePath, context.InlinePartialTemplates) - { - } - + public object Value { get; set; } + public int Index { get; set; } public bool First { get; set; } public bool Last { get; set; } - } - private class ObjectEnumeratorBindingContext : IteratorBindingContext - { - public ObjectEnumeratorBindingContext(BindingContext context) - : base(context) + public bool ProvidesNonContextVariables { get; } = false; + + public virtual bool TryGetValue(string memberName, out object value) { + switch (memberName.ToLowerInvariant()) + { + 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; + } } - + } + + private class ObjectEnumeratorValueProvider : IteratorValueProvider + { public string Key { get; set; } + + public override bool TryGetValue(string memberName, out object value) + { + switch (memberName.ToLowerInvariant()) + { + case "key": + value = Key; + return true; + + default: + return base.TryGetValue(memberName, out value); + } + } } private static object AccessMember(object instance, MemberInfo member) { - if (member is PropertyInfo) - { - return ((PropertyInfo)member).GetValue(instance, null); - } - if (member is FieldInfo) + switch (member) { - return ((FieldInfo)member).GetValue(instance); + case PropertyInfo propertyInfo: + return propertyInfo.GetValue(instance, null); + case FieldInfo fieldInfo: + return fieldInfo.GetValue(instance); + default: + throw new InvalidOperationException("Requested member was not a field or property"); } - 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..9cfa52b5 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -19,9 +19,9 @@ private PartialBinder(CompilationContext context) { } - protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) - { - return bhex; + protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) + { + return bhex; } protected override Expression VisitStatementExpression(StatementExpression sex) @@ -47,7 +47,7 @@ protected override Expression VisitPartialExpression(PartialExpression pex) { bindingContext = Expression.Call( bindingContext, - typeof(BindingContext).GetMethod("CreateChildContext"), + typeof(BindingContext).GetMethod(nameof(BindingContext.CreateChildContext)), pex.Argument ?? Expression.Constant(null), partialBlockTemplate ?? Expression.Constant(null, typeof(Action))); } @@ -72,17 +72,17 @@ private static void InvokePartialWithFallback( { if (!InvokePartial(partialName, context, configuration)) { - if (context.PartialBlockTemplate == null) + if (context.PartialBlockTemplate == null) { - if (configuration.MissingPartialTemplateHandler != null) - { - configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); - return; + 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)); } - else - { - throw new HandlebarsRuntimeException(string.Format("Referenced partial name {0} could not be resolved", partialName)); - } } context.PartialBlockTemplate(context.TextWriter, context); @@ -115,11 +115,11 @@ private static bool InvokePartial( // Partial is not found, so call the resolver and attempt to load it. if (configuration.RegisteredTemplates.ContainsKey(partialName) == false) { - if (configuration.PartialTemplateResolver == null - || configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath) == false) - { - // Template not found. - return false; + if (configuration.PartialTemplateResolver == null + || configuration.PartialTemplateResolver.TryRegisterPartial(Handlebars.Create(configuration), partialName, context.TemplatePath) == false) + { + // Template not found. + return false; } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 41a6c893..e2e1f080 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,18 +1,14 @@ 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; namespace HandlebarsDotNet.Compiler { internal class PathBinder : HandlebarsExpressionVisitor { + private readonly PathResolver _pathResolver; + public static Expression Bind(Expression expr, CompilationContext context) { return new PathBinder(context).Visit(expr); @@ -21,6 +17,7 @@ public static Expression Bind(Expression expr, CompilationContext context) private PathBinder(CompilationContext context) : base(context) { + _pathResolver = new PathResolver(context.Configuration); } protected override Expression VisitStatementExpression(StatementExpression sex) @@ -47,346 +44,15 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitPathExpression(PathExpression pex) { return Expression.Call( - Expression.Constant(this), + Expression.Constant(_pathResolver), #if netstandard - new Func(ResolvePath).GetMethodInfo(), + new Func(_pathResolver.ResolvePath).GetMethodInfo(), #else - new Func(ResolvePath).Method, + new Func(_pathResolver.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) - { - 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); - } - } - - //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); - } - - //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) - { - 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; - } - - return false; - })?.GetMethod; - } - - - 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; - } - - static Type FirstGenericDictionaryTypeInstance(Type instanceType) - { - return instanceType.GetInterfaces() - .FirstOrDefault(i => -#if netstandard - i.GetTypeInfo().IsGenericType -#else - i.IsGenericType -#endif - && - ( - i.GetGenericTypeDefinition() == typeof(IDictionary<,>) - ) - ); - } - - 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/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index ab9efb40..9e03b656 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,30 +1,27 @@ using System; +using System.Collections.Generic; using System.IO; +using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { public sealed class HelperOptions { - private readonly Action _template; - private readonly Action _inverse; - internal HelperOptions( Action template, - Action inverse) + Action inverse, + BlockParamsValueProvider blockParamsValueProvider) { - _template = template; - _inverse = inverse; + Template = template; + Inverse = inverse; + BlockParams = blockParamsValueProvider.Configure; } - public Action Template - { - get { return _template; } - } + public Action Template { get; } - public Action Inverse - { - get { return _inverse; } - } + public Action Inverse { get; } + + public Action BlockParams { get; } } } From 4669550deb2ea78659d5037232fcec26dd1392bb Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 17 Jan 2020 17:11:20 -0800 Subject: [PATCH 13/53] Support for `ReturnHelper` Added support for helpers with return statement Refactored expression tree creation to improve readability and maintainability --- source/Handlebars.Benchmark/ClassBuilder.cs | 99 +++ source/Handlebars.Benchmark/Compilation.cs | 71 ++ source/Handlebars.Benchmark/Execution.cs | 225 +++++++ .../Handlebars.Benchmark.csproj | 18 + source/Handlebars.Benchmark/Program.cs | 19 + .../Handlebars.Test/BasicIntegrationTests.cs | 149 +++-- source/Handlebars.Test/DynamicTests.cs | 15 +- source/Handlebars.Test/HelperTests.cs | 2 +- source/Handlebars.Test/WhitespaceTests.cs | 6 +- source/Handlebars.sln | 6 + source/Handlebars/BuiltinHelpers.cs | 26 +- .../Compiler/ExpressionShortcuts.cs | 616 ++++++++++++++++++ source/Handlebars/Compiler/FunctionBuilder.cs | 46 +- .../Structure/BlockParamsExpression.cs | 2 +- .../Structure/BlockParamsValueProvider.cs | 18 +- .../Compiler/Structure/PathResolver.cs | 2 +- .../Expression/BlockHelperFunctionBinder.cs | 84 +-- .../Expression/BoolishConverter.cs | 12 +- .../Translation/Expression/ContextBinder.cs | 59 +- .../Expression/HelperFunctionBinder.cs | 108 ++- .../Translation/Expression/IteratorBinder.cs | 206 +++--- .../Translation/Expression/PartialBinder.cs | 53 +- .../Translation/Expression/PathBinder.cs | 37 +- .../Expression/SubExpressionVisitor.cs | 101 ++- source/Handlebars/Handlebars.cs | 6 + source/Handlebars/HandlebarsConfiguration.cs | 5 + source/Handlebars/HandlebarsEnvironment.cs | 12 + source/Handlebars/IHandlebars.cs | 2 + 28 files changed, 1495 insertions(+), 510 deletions(-) create mode 100644 source/Handlebars.Benchmark/ClassBuilder.cs create mode 100644 source/Handlebars.Benchmark/Compilation.cs create mode 100644 source/Handlebars.Benchmark/Execution.cs create mode 100644 source/Handlebars.Benchmark/Handlebars.Benchmark.csproj create mode 100644 source/Handlebars.Benchmark/Program.cs create mode 100644 source/Handlebars/Compiler/ExpressionShortcuts.cs diff --git a/source/Handlebars.Benchmark/ClassBuilder.cs b/source/Handlebars.Benchmark/ClassBuilder.cs new file mode 100644 index 00000000..c987a881 --- /dev/null +++ b/source/Handlebars.Benchmark/ClassBuilder.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Benchmark +{ + internal class ClassBuilder + { + readonly AssemblyName asemblyName; + + public ClassBuilder(string ClassName) + { + this.asemblyName = new AssemblyName(ClassName); + } + + public object CreateObject(string[] PropertyNames, Type[] Types) + { + var type = CreateType(PropertyNames, Types); + + return Activator.CreateInstance(type); + } + + public Type CreateType(string[] PropertyNames, Type[] Types) + { + if (PropertyNames.Length != Types.Length) + { + Console.WriteLine("The number of property names should match their corresopnding types number"); + } + + TypeBuilder DynamicClass = this.CreateClass(); + this.CreateConstructor(DynamicClass); + for (int ind = 0; ind < PropertyNames.Count(); ind++) + CreateProperty(DynamicClass, PropertyNames[ind], Types[ind]); + return DynamicClass.CreateType(); + } + + private TypeBuilder CreateClass() + { + AssemblyBuilder assemblyBuilder = + AssemblyBuilder.DefineDynamicAssembly(this.asemblyName, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); + TypeBuilder typeBuilder = moduleBuilder.DefineType(this.asemblyName.FullName + , TypeAttributes.Public | + TypeAttributes.Class | + TypeAttributes.AutoClass | + TypeAttributes.AnsiClass | + TypeAttributes.BeforeFieldInit | + TypeAttributes.AutoLayout + , null); + return typeBuilder; + } + + private void CreateConstructor(TypeBuilder typeBuilder) + { + typeBuilder.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName); + } + + private void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType) + { + FieldBuilder fieldBuilder = + typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); + + PropertyBuilder propertyBuilder = + typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); + MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, + Type.EmptyTypes); + ILGenerator getIl = getPropMthdBldr.GetILGenerator(); + + getIl.Emit(OpCodes.Ldarg_0); + getIl.Emit(OpCodes.Ldfld, fieldBuilder); + getIl.Emit(OpCodes.Ret); + + MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, + MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + null, new[] {propertyType}); + + ILGenerator setIl = setPropMthdBldr.GetILGenerator(); + Label modifyProperty = setIl.DefineLabel(); + Label exitSet = setIl.DefineLabel(); + + setIl.MarkLabel(modifyProperty); + setIl.Emit(OpCodes.Ldarg_0); + setIl.Emit(OpCodes.Ldarg_1); + setIl.Emit(OpCodes.Stfld, fieldBuilder); + + setIl.Emit(OpCodes.Nop); + setIl.MarkLabel(exitSet); + setIl.Emit(OpCodes.Ret); + + propertyBuilder.SetGetMethod(getPropMthdBldr); + propertyBuilder.SetSetMethod(setPropMthdBldr); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Compilation.cs b/source/Handlebars.Benchmark/Compilation.cs new file mode 100644 index 00000000..661167fb --- /dev/null +++ b/source/Handlebars.Benchmark/Compilation.cs @@ -0,0 +1,71 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using HandlebarsDotNet; + +namespace Benchmark +{ + [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] + public class Compilation + { + private IHandlebars _handlebars; + + [GlobalSetup] + public void Setup() + { + _handlebars = Handlebars.Create(); + _handlebars.RegisterHelper("customHelper", (writer, context, parameters) => + { + }); + } + + [Benchmark] + public Func Complex() + { + const string template = "{{#each County}}" + + "{{#if @first}}" + + "{{this}}" + + "{{else}}" + + "{{#if @last}}" + + " and {{this}}" + + "{{else}}" + + ", {{this}}" + + "{{/if}}" + + "{{/if}}" + + "{{/each}}"; + + return _handlebars.Compile(template); + } + + [Benchmark] + public Func Each() + { + const string template = "{{#each enumerateMe}}{{this}} {{/each}}"; + return _handlebars.Compile(template); + } + + [Benchmark] + public Func EachBlockParams() + { + const string template = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; + return _handlebars.Compile(template); + } + + [Benchmark] + public Func Helper() + { + const string source = @"{{customHelper 'value'}}"; + + return _handlebars.Compile(source); + } + + [Benchmark] + public Func HelperPostRegister() + { + const string source = @"{{not_registered_helper 'value'}}"; + + return _handlebars.Compile(source); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs new file mode 100644 index 00000000..2290a1dd --- /dev/null +++ b/source/Handlebars.Benchmark/Execution.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Bogus; +using HandlebarsDotNet; +using Newtonsoft.Json.Linq; + +namespace Benchmark +{ + [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] + public class Execution + { + private IHandlebars _handlebars; + + [Params(10000)] + public int N; + + private Faker _faker; + private Func[] _templates; + private Dictionary _arrayData; + private Dictionary _dictionaryData; + private Dictionary _objectData; + private Dictionary _jsonData; + private Type _type; + private string[] _propertyNames; + + [GlobalSetup] + public void Setup() + { + _handlebars = Handlebars.Create(); + _faker = new Faker(); + + const string eachTemplate = "{{#each County}}{{this}} {{/each}}"; + const string blockParamsEach = "{{#each County as |item val|}}{{item}} {{val}} {{/each}}"; + const string complexTemplate = "{{#each County}}" + + "{{#if @first}}" + + "{{this}}" + + "{{else}}" + + "{{#if @last}}" + + " and {{this}}" + + "{{else}}" + + ", {{this}}" + + "{{/if}}" + + "{{/if}}" + + "{{/each}}"; + const string helperTemplate = "{{customHelper 'value'}}"; + const string notRegisteredHelperTemplate = "{{not_registered_helper 'value'}}"; + + _handlebars.RegisterHelper("customHelper", (writer, context, parameters) => + { + writer.WriteSafeString(parameters[0]); + }); + + string RandomProperty() => new string( + Enumerable.Range(0, 40) + .Select(x => x % 2 == 0 ? _faker.Random.Char('a', 'z') : _faker.Random.Char('A', 'Z')) + .ToArray() + ); + + var stringType = typeof(string); + _propertyNames = Enumerable.Range(0, N).Select(o => RandomProperty()).ToArray(); + _type = new ClassBuilder($"BenchmarkCountry{N}") + .CreateType( + _propertyNames, + Enumerable.Range(0, N).Select(o => stringType).ToArray() + ); + + _templates = new[] + { + _handlebars.Compile(eachTemplate), + _handlebars.Compile(blockParamsEach), + _handlebars.Compile(complexTemplate), + _handlebars.Compile(helperTemplate), + _handlebars.Compile(notRegisteredHelperTemplate), + }; + } + + [IterationSetup(Targets = new[]{nameof(SimpleEachJsonInput), nameof(EachBlockParamsJsonInput), nameof(ComplexJsonInput)})] + public void JsonIterationSetup() + { + var json = new JObject(); + for (var index = 0; index < _propertyNames.Length; index++) + { + json.Add(_propertyNames[index], JToken.FromObject(Guid.NewGuid())); + } + + _jsonData = new Dictionary + { + ["Country"] = json + }; + } + + [IterationSetup(Targets = new[]{nameof(SimpleEachDictionaryInput), nameof(EachBlockParamsDictionaryInput), nameof(ComplexDictionaryInput)})] + public void DictionaryIterationSetup() + { + _dictionaryData = new Dictionary + { + ["Country"] = _propertyNames.ToDictionary(o => o, o => Guid.NewGuid().ToString()) + }; + } + + [IterationSetup(Targets = new[]{nameof(SimpleEachArrayInput), nameof(EachBlockParamsArrayInput), nameof(ComplexArrayInput)})] + public void ArrayIterationSetup() + { + _arrayData = new Dictionary + { + ["Country"] = _propertyNames.Select(o => Guid.NewGuid().ToString()).ToArray() + }; + } + + [IterationSetup(Targets = new[]{nameof(SimpleEachObjectInput), nameof(EachBlockParamsObjectInput), nameof(ComplexObjectInput)})] + public void ObjectIterationSetup() + { + var data = Activator.CreateInstance(_type); + var properties = data.GetType().GetRuntimeProperties().ToArray(); + for (var index = 0; index < properties.Length; index++) + { + properties[index].SetValue(data, Guid.NewGuid().ToString()); + } + + _objectData = new Dictionary + { + ["Country"] = data + }; + } + + [IterationSetup(Targets = new[]{nameof(Helper), nameof(HelperPostRegister)})] + public void HelpersSetup() + { + } + + [Benchmark] + public string SimpleEachArrayInput() + { + return _templates[0].Invoke(_arrayData); + } + + [Benchmark] + public string EachBlockParamsArrayInput() + { + return _templates[1].Invoke(_arrayData); + } + + [Benchmark] + public string ComplexArrayInput() + { + return _templates[2].Invoke(_arrayData); + } + + [Benchmark] + public string SimpleEachObjectInput() + { + return _templates[0].Invoke(_objectData); + } + + [Benchmark] + public string EachBlockParamsObjectInput() + { + return _templates[1].Invoke(_objectData); + } + + [Benchmark] + public string ComplexObjectInput() + { + return _templates[2].Invoke(_objectData); + } + + [Benchmark] + public string SimpleEachJsonInput() + { + return _templates[0].Invoke(_jsonData); + } + + [Benchmark] + public string EachBlockParamsJsonInput() + { + return _templates[1].Invoke(_jsonData); + } + + [Benchmark] + public string ComplexJsonInput() + { + return _templates[2].Invoke(_jsonData); + } + + [Benchmark] + public string SimpleEachDictionaryInput() + { + return _templates[0].Invoke(_dictionaryData); + } + + [Benchmark] + public string EachBlockParamsDictionaryInput() + { + return _templates[1].Invoke(_dictionaryData); + } + + [Benchmark] + public string ComplexDictionaryInput() + { + return _templates[2].Invoke(_dictionaryData); + } + + [Benchmark] + public string Helper() + { + return _templates[3].Invoke(new object()); + } + + [Benchmark] + public string HelperPostRegister() + { + _handlebars.RegisterHelper("not_registered_helper", (writer, context, parameters) => + { + writer.WriteSafeString(parameters[0]); + }); + + return _templates[4].Invoke(new object()); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj new file mode 100644 index 00000000..b9508a7d --- /dev/null +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp2.1;net461 + + + + + + + + + + + + + diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs new file mode 100644 index 00000000..6c6b4bea --- /dev/null +++ b/source/Handlebars.Benchmark/Program.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; +using HandlebarsDotNet; + +namespace Benchmark +{ + class Program + { + static void Main(string[] args) + { + var manualConfig = DefaultConfig.Instance.WithArtifactsPath( + $"./Benchmark-{FileVersionInfo.GetVersionInfo(typeof(Handlebars).Assembly.Location).FileVersion}" + ); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, manualConfig); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 87d703fc..1a5e130a 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -489,7 +489,7 @@ public void ObjectEnumeratorWithBlockParams() } }; var result = template(data); - Assert.Equal("foo: hello bar: world ", result); + Assert.Equal("hello: foo world: bar ", result); } [Fact] @@ -523,7 +523,7 @@ public void DictionaryEnumeratorWithBlockParams() } }; var result = template(data); - Assert.Equal("foo hello bar world ", result); + Assert.Equal("hello foo world bar ", result); } [Fact] @@ -1517,72 +1517,139 @@ public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() var result = template(data); Assert.Equal("foo foo bar baz buz", result); } - + [Fact] - public void ShouldBeAbleToHandleFieldContainingDots() + public void BasicReturnFromHelper() { - 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() - { - {"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); + + var result = template(new object()); + Assert.Equal("data", result); } - + [Fact] - public void ShouldBeAbleToHandleListWithNumericalFields() + public void CollectionReturnFromHelper() { - var source = "{{ [0] }}"; + 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 data = new List {"FOOBAR"}; - var result = template(data); - Assert.Equal("FOOBAR", result); + + var result = template(new object()); + Assert.Equal("Nils lives in Darmstadt. Yehuda lives in San Francisco. ", result); } + + + [Fact] + public void ReturnFromHelperWithSubExpression() + { + 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); + } + [Fact] - public void ShouldBeAbleToHandleDictionaryWithNumericalFields() + public void ReturnFromHelperLateBindWithSubExpression() { - var source = "{{ [0] }}"; + var formatData = $"formatData{Guid.NewGuid()}"; + var getData = $"getData{Guid.NewGuid()}"; + + var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; var template = Handlebars.Compile(source); - var data = new Dictionary + + Handlebars.RegisterHelper(formatData, (writer, context, arguments) => { - {"0", "FOOBAR"}, - }; - var result = template(data); - Assert.Equal("FOOBAR", result); + 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); } [Fact] - public void ShouldBeAbleToHandleJObjectsWithNumericalFields() + public void BasicLookup() { - var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); - var data = new JObject + var source = "{{#each people}}{{.}} lives in {{lookup ../cities @index}} {{/each}}"; + var template = Handlebars.Create().Compile(source); + var data = new { - {"0", "FOOBAR"}, + people = new[]{"Nils", "Yehuda"}, + cities = new[]{"Darmstadt", "San Francisco"} }; + var result = template(data); - Assert.Equal("FOOBAR", result); + Assert.Equal("Nils lives in Darmstadt Yehuda lives in San Francisco ", result); } [Fact] - public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() + public void LookupAsSubExpression() { - var source = - "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; - var template = Handlebars.Compile(source); - var data = new Dictionary + var source = "{{#each persons}}{{name}} lives in {{#with (lookup ../cities [resident])~}}{{name}} ({{country}}){{/with}}{{/each}}"; + var template = Handlebars.Create().Compile(source); + var data = new { - {"noBracket", "foo"}, - {"[startsWithBracket", "bar"}, - {"endsWithBracket]", "baz"}, - {"[bothBrackets]", "buz"} + 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("foo foo bar baz buz", result); + Assert.Equal("Nils lives in Darmstadt (Germany)Yehuda lives in San Francisco (USA)", result); } private class MockDictionary : IDictionary diff --git a/source/Handlebars.Test/DynamicTests.cs b/source/Handlebars.Test/DynamicTests.cs index a1f0a610..48b85f21 100644 --- a/source/Handlebars.Test/DynamicTests.cs +++ b/source/Handlebars.Test/DynamicTests.cs @@ -76,11 +76,24 @@ public void JsonTestArrays(){ 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/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 9f612693..3cab30f1 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -37,7 +37,7 @@ public void BlockHelperWithBlockParams() Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { var count = 0; options.BlockParams((parameters, binder) => - binder(parameters.Keys.First(), () => ++count)); + binder(parameters.ElementAtOrDefault(0), () => ++count)); foreach(var arg in args) { diff --git a/source/Handlebars.Test/WhitespaceTests.cs b/source/Handlebars.Test/WhitespaceTests.cs index ca64a623..395db786 100644 --- a/source/Handlebars.Test/WhitespaceTests.cs +++ b/source/Handlebars.Test/WhitespaceTests.cs @@ -54,8 +54,7 @@ public void ComplexTest() {{~/if~}} {{~/each}}"; - var h = Handlebars.Create(); - var template = h.Compile(source); + var template = Handlebars.Create().Compile(source); var data = new { nav = new [] { new { @@ -78,8 +77,7 @@ public void ComplexTest() public void StandaloneEach() { var source = "Links:\n {{#each nav}}\n \n {{#if test}}\n {{title}}\n {{else}}\n Empty\n {{/if}}\n \n {{/each}}"; - var h = Handlebars.Create(); - var template = h.Compile(source); + var template = Handlebars.Create().Compile(source); var data = new { nav = new[] diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 20bfec60..01e50843 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{E880C14C-96EE-4A1E-98BD-6348AB529090}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {2BD48FB6-C852-4141-B734-12E501B1D761}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.Build.0 = Release|Any CPU + {E880C14C-96EE-4A1E-98BD-6348AB529090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E880C14C-96EE-4A1E-98BD-6348AB529090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs index 8cf1594b..0e2e9c97 100644 --- a/source/Handlebars/BuiltinHelpers.cs +++ b/source/Handlebars/BuiltinHelpers.cs @@ -18,7 +18,7 @@ public static void With(TextWriter output, HelperOptions options, dynamic contex } options.BlockParams((parameters, binder) => - binder(parameters.Keys.First(), () => arguments[0])); + binder(parameters.ElementAtOrDefault(0), () => arguments[0])); if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) { @@ -29,7 +29,23 @@ public static void With(TextWriter output, HelperOptions options, dynamic contex options.Inverse(output, context); } } + + [Description("Lookup")] + public static object Lookup(dynamic context, params object[] arguments) + { + if (arguments.Length != 2) + { + throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); + } + var configuration = new HandlebarsConfiguration(); + var pathResolver = new PathResolver(configuration); + var memberName = arguments[1].ToString(); + return !pathResolver.TryAccessMember(arguments[0], memberName, out var value) + ? new UndefinedBindingResult(memberName, configuration) + : value; + } + [Description("*inline")] public static void Inline(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) { @@ -63,6 +79,14 @@ public static IEnumerable> Helpers return GetHelpers(); } } + + public static IEnumerable> ReturnHelpers + { + get + { + return GetHelpers(); + } + } public static IEnumerable> BlockHelpers { diff --git a/source/Handlebars/Compiler/ExpressionShortcuts.cs b/source/Handlebars/Compiler/ExpressionShortcuts.cs new file mode 100644 index 00000000..e3a140d8 --- /dev/null +++ b/source/Handlebars/Compiler/ExpressionShortcuts.cs @@ -0,0 +1,616 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; + +namespace HandlebarsDotNet.Compiler +{ + internal class BlockBody : List + { + + } + + internal class ExpressionContainer + { + private readonly Expression _expression; + + public ExpressionContainer(Expression expression) => _expression = expression; + + public virtual Expression Expression => _expression; + + public ExpressionContainer Typed() => new ExpressionContainer(Expression); + + public ExpressionContainer Is() => new ExpressionContainer(Expression.TypeIs(Expression, typeof(TV))); + public ExpressionContainer As() => new ExpressionContainer(Expression.TypeAs(Expression, typeof(TV))); + public ExpressionContainer Cast() => new ExpressionContainer(Expression.Convert(Expression, typeof(TV))); + + public static implicit operator Expression(ExpressionContainer expressionContainer) => expressionContainer.Expression; + public static implicit operator ExpressionContainer(Expression expression) => new ExpressionContainer(expression); + } + + /// + /// Provides strongly typed container for . + /// + /// Used to trick C# compiler in cases like in order to pass value to target method. + /// Type of expected result value. + internal class ExpressionContainer : ExpressionContainer + { + public static implicit operator T(ExpressionContainer expressionContainer) => default(T); + + public ExpressionContainer(Expression expression) : base(expression) + { + } + } + + /// + /// Stands for shortcuts. + /// + internal static class E + { + /// + /// Creates strongly typed representation of the + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Arg(Expression expression) => expression == null ? Null() : new ExpressionContainer(expression); + + /// + /// Creates strongly typed representation of the . + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Arg(Expression expression) => expression == null ? Null() : new ExpressionContainer(expression); + + /// + /// Creates strongly typed representation of the and performs on it. + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Cast(Expression expression) => expression == null ? Null() : new ExpressionContainer(Expression.Convert(expression, typeof(T))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable ReplaceValuesOf(IEnumerable instance, Expression newValue) + { + var targetType = typeof(T); + return instance.Select(value => targetType.IsAssignableFrom(value.Type) + ? newValue + : value); + } + + public static IEnumerable ReplaceParameters(IEnumerable instance, IList newValue) + { + return newValue.Count != 0 + ? PerformReplacement() + : instance; + + IEnumerable PerformReplacement() + { + var visitor = new ParameterReplacerVisitor(newValue); + return instance.Where(o => o != null).Select(expression => visitor.Visit(expression)); + } + } + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Var(string name = null) + { + return new ExpressionContainer(Expression.Variable(typeof(T), name ?? typeof(T).Name)); + } + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Parameter(string name = null) + { + return new ExpressionContainer(Expression.Parameter(typeof(T), name ?? typeof(T).Name)); + } + + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Property accessor expression + /// Expected type of resulting target + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Property(Expression instance, Expression> propertyLambda) + { + return Arg(ProcessPropertyLambda(instance, propertyLambda)); + } + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Property accessor expression + /// + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Property(Expression instance, string propertyName) + { + return Arg(Expression.Property(instance, propertyName)); + } + + /// + /// Creates strongly typed representation of the + /// + /// Items for the new array + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Array(IEnumerable items) + { + return Arg(Expression.NewArrayInit(typeof(T), items)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(Expression invocationExpression) + { + return new ExpressionContainer(ProcessCallLambda(invocationExpression)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(ExpressionContainer instance, ExpressionContainer method, params ExpressionContainer[] parameters) + { + var methodCallExpression = (MethodCallExpression) ProcessCall(method, instance); + return methodCallExpression.Update(methodCallExpression.Object, parameters.Cast()); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(Expression> invocationExpression) + { + return Arg(ProcessCallLambda(invocationExpression)); + } + + /// + /// Creates . Parameters for constructor and constructor itself are resolved based . + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer New(Expression> invocationExpression) + { + return Arg(ProcessCallLambda(invocationExpression)); + } + + /// + /// Creates using default constructor. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer New() where T: new() + { + return Arg(Expression.New(typeof(T).GetConstructor(new Type[0]))); + } + + /// + /// Provides fluent interface for creation + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BlockBuilder Block() + { + return new BlockBuilder(); + } + + /// + /// Creates strongly typed representation of null. + /// + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Null() + { + return Arg(Expression.Convert(Expression.Constant(null), typeof(T))); + } + + /// + /// Creates strongly typed representation of null. + /// + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Null(Type type) + { + return new ExpressionContainer(Expression.Convert(Expression.Constant(null), type)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) + { + return new ExpressionContainer(ProcessCallLambda(invocationExpression, instance)); + } + + /// + /// Creates strongly typed representation of the + /// + /// + /// Property accessor expression + /// Expected type of resulting target + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Property(this ExpressionContainer instance, Expression> propertyAccessor) + { + return Property(instance.Expression, propertyAccessor); + } + + /// + /// Creates strongly typed representation of the + /// + /// + /// Property accessor expression + /// Expected type of resulting target + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Property(this ExpressionContainer instance, string propertyName) + { + return Property(instance.Expression, propertyName); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) + { + return Arg(ProcessCallLambda(invocationExpression, instance)); + } + + /// + /// Creates assign . + /// Parameters are resolved based on actual passed parameters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Assign(this ExpressionContainer target, ExpressionContainer value) + { + return new ExpressionContainer(Expression.Assign(target, value)); + } + + /// + /// Creates assign . + /// Parameters are resolved based on actual passed parameters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Assign(this ExpressionContainer target, Expression value) + { + return new ExpressionContainer(Expression.Assign(target, value)); + } + + /// + /// Creates ternary assignment like target = condition ? ifTrue : ifFalse + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Expression TernaryAssign(this ExpressionContainer target, ExpressionContainer condition, ExpressionContainer ifTrue, ExpressionContainer ifFalse) + { + return Expression.IfThenElse( + condition.Expression, + Expression.Assign(target, ifTrue), + Expression.Assign(target, ifFalse)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static MemberExpression ProcessPropertyLambda(Expression instance, LambdaExpression propertyLambda) + { + var member = propertyLambda.Body as MemberExpression; + if (member == null) throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); + + var propInfo = member.Member as PropertyInfo; + if (propInfo == null) throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); + + return Expression.Property(instance, propInfo); + } + + private static Expression ProcessCallLambda(LambdaExpression propertyLambda, Expression instance = null) + { + return ProcessCall(propertyLambda.Body, instance); + } + + private static Expression ProcessCall(Expression propertyLambda, Expression instance = null) + { + var parameters = instance != null ? new[] { instance } : new Expression[0]; + + switch (propertyLambda) + { + case NewExpression newExpression: + return Expression.New(newExpression.Constructor, ExtractArguments(newExpression.Arguments)); + + case MethodCallExpression member: + var methodInfo = member.Method; + IEnumerable methodCallArguments = member.Arguments; + var inst = ReplaceParameters(new[] {member.Object}, parameters).SingleOrDefault(); + methodCallArguments = ReplaceParameters(methodCallArguments, parameters); + methodCallArguments = methodCallArguments.Select(ExtractArgument); + var memberObject = methodInfo.IsStatic + ? null : Expression.Convert(inst, methodInfo.DeclaringType); + return Expression.Call(memberObject, methodInfo, methodCallArguments); + + case InvocationExpression invocationExpression: + return invocationExpression.Update( + invocationExpression.Expression, + ExtractArguments(invocationExpression.Arguments) + ); + + default: + return propertyLambda; + } + } + + private static IReadOnlyCollection ExtractArguments(IReadOnlyCollection exprs) + { + var result = new Expression[exprs.Count]; + if (exprs is IList list) + { + for (var index = 0; index < list.Count; index++) + { + result[index] = ExtractArgument(list[index]); + } + + return result; + } + else + { + int index = 0; + foreach (var expr in exprs) + { + result[index++] = ExtractArgument(expr); + } + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression ExtractArgument(Expression expr) + { + var value = ExtractFromExpression(expr); + + if (value is Expression expression) return expression; + if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; + return Expression.Constant(value); + } + + private static object ExtractFromExpression(Expression expression) + { + while (true) + { + switch (expression) + { + case MethodCallExpression methodCall: + return Expression.Lambda(methodCall).Compile().DynamicInvoke(); + + case UnaryExpression unaryExpression: + switch (unaryExpression.NodeType) + { + case ExpressionType.Convert: + if (typeof(ExpressionContainer).IsAssignableFrom(unaryExpression.Operand.Type)) + { + var operand = ExtractArgument(unaryExpression.Operand); + if (operand.Type != unaryExpression.Type) + { + return Expression.Convert(operand, unaryExpression.Type); + } + + if (operand.NodeType == ExpressionType.Call || operand.NodeType == ExpressionType.Invoke || operand.NodeType == ExpressionType.Lambda) + { + return operand; + } + + expression = operand; + continue; + } + + if (unaryExpression.Type != unaryExpression.Operand.Type) + { + return expression; + } + + expression = unaryExpression.Operand; + continue; + + default: + expression = unaryExpression.Operand; + continue; + } + + case LambdaExpression lambda: + return ProcessCallLambda(lambda); + + case MemberExpression memberExpression: + switch (memberExpression.Expression) + { + case ConstantExpression constant: + var constantValue = constant.Value; + var value = constantValue.GetType().GetField(memberExpression.Member.Name)?.GetValue(constantValue); + if (value is ExpressionContainer) return value; + return Expression.Convert(Expression.Constant(value), memberExpression.Type); + + default: + return memberExpression; + } + + default: + return expression; + } + } + } + + internal class BlockBuilder: ExpressionContainer + { + private readonly List _expressions; + private readonly List _parameters; + + public BlockBuilder() : base(null) + { + _expressions = new List(); + _parameters = new List(); + } + + /// + /// Adds parameter to + /// + /// is not + public BlockBuilder Parameter(Expression expression) + { + if (expression is ParameterExpression parameterExpression) return Parameter(parameterExpression); + + throw new ArgumentException("is not ParameterExpression", nameof(expression)); + } + + /// + /// Adds parameter to with initial assignment + /// + /// is not + public BlockBuilder Parameter(ExpressionContainer expression, ExpressionContainer value) + { + if (!(expression.Expression is ParameterExpression parameterExpression)) + throw new ArgumentException("is not ParameterExpression", nameof(expression)); + + _parameters.Add(parameterExpression); + _expressions.Add(expression.Assign(value)); + return this; + } + + /// + /// Adds parameter to with initial assignment + /// + /// is not + public BlockBuilder Parameter(ExpressionContainer expression, Expression value) + { + if (!(expression.Expression is ParameterExpression parameterExpression)) + throw new ArgumentException("is not ParameterExpression", nameof(expression)); + + _parameters.Add(parameterExpression); + _expressions.Add(expression.Assign(value)); + return this; + } + + /// + /// Adds parameter to + /// + public BlockBuilder Parameter(ParameterExpression e) + { + _parameters.Add(e); + return this; + } + + /// + /// Adds new "line" to + /// + public BlockBuilder Line(Expression e) + { + _expressions.Add(e); + return this; + } + + /// + /// Adds new "line" to + /// + public BlockBuilder Line(ExpressionContainer e) + { + _expressions.Add(e); + return this; + } + + /// + /// Adds multiple new "lines" to + /// + public BlockBuilder Lines(IEnumerable e) + { + _expressions.AddRange(e); + return this; + } + + /// + /// Creates out of current . + /// + public ExpressionContainer Invoke(params ExpressionContainer[] parameters) + { + return Arg(Expression.Lambda(Expression, parameters.Select(o => (ParameterExpression) o.Expression))); + } + + public override Expression Expression => Expression.Block(_parameters, _expressions); + } + } + + internal class ParameterReplacerVisitor : ExpressionVisitor + { + private readonly ICollection _replacements; + private readonly bool _addIfMiss; + + public ParameterReplacerVisitor(ICollection replacements, bool addIfMiss = false) + { + _replacements = replacements; + _addIfMiss = addIfMiss; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + var replacement = _replacements.FirstOrDefault(o => o.Type == node.Type); + if (replacement == null || replacement == node) + { + if (_addIfMiss) + { + _replacements.Add(node); + } + return base.VisitParameter(node); + } + return base.Visit(replacement); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index 7b71b703..8a465edf 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -9,7 +9,7 @@ namespace HandlebarsDotNet.Compiler internal class FunctionBuilder { private readonly HandlebarsConfiguration _configuration; - private static readonly Expression _emptyLambda = + private static readonly Expression> _emptyLambda = Expression.Lambda>( Expression.Empty(), Expression.Parameter(typeof(TextWriter)), @@ -20,7 +20,25 @@ public FunctionBuilder(HandlebarsConfiguration configuration) _configuration = configuration; } - public Expression Compile(IEnumerable expressions, Expression parentContext, string templatePath = null) + public Expression Reduce(Expression expression, CompilationContext compilationContext) + { + 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); + + return expression; + } + + public Expression> Compile(IEnumerable expressions, Expression parentContext, string templatePath = null) { try { @@ -34,20 +52,9 @@ public Expression Compile(IEnumerable expressions, Expression parent } 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; + expression = Reduce(expression, compilationContext); + + return ContextBinder.Bind(expression, compilationContext, parentContext, templatePath); } catch (Exception ex) { @@ -61,16 +68,15 @@ public Action Compile(IEnumerable expressions, s { var expression = Compile(expressions, null, templatePath); - return ((Expression>)expression).Compile(); + return expression.Compile(); } catch (Exception ex) { throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); } } - - - private Expression CreateExpressionBlock(IEnumerable expressions) + + private static Expression CreateExpressionBlock(IEnumerable expressions) { return Expression.Block(expressions); } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs index 17935347..5401d177 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -31,7 +31,7 @@ public BlockParamsExpression(string action, string blockParams) protected override Expression Accept(ExpressionVisitor visitor) { - return visitor.Visit(Constant(_blockParam)); + return visitor.Visit(Expression.Convert(Constant(_blockParam), typeof(BlockParam))); } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs index 02103e83..1d19da85 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -7,7 +7,7 @@ namespace HandlebarsDotNet.Compiler /// /// Parameters passed to BlockParams. /// Function that perform binding of parameter to . - public delegate void ConfigureBlockParams(IReadOnlyDictionary> parameters, ValueBinder valueBinder); + public delegate void ConfigureBlockParams(string[] parameters, ValueBinder valueBinder); /// /// Function that perform binding of parameter to . @@ -22,13 +22,11 @@ internal class BlockParamsValueProvider : IValueProvider private readonly BlockParam _params; private readonly Action> _invoker; private readonly Dictionary> _accessors; - private readonly PathResolver _pathResolver; - - public BlockParamsValueProvider(BindingContext context, HandlebarsConfiguration configuration, object @params) + + public BlockParamsValueProvider(BindingContext context, BlockParam @params) { - _params = @params as BlockParam; + _params = @params; _invoker = action => action(context); - _pathResolver = new PathResolver(configuration); _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); context.RegisterValueProvider(this); @@ -42,9 +40,6 @@ public BlockParamsValueProvider(BindingContext context, HandlebarsConfiguration public void Configure(ConfigureBlockParams blockParamsConfiguration) { if(_params == null) return; - - Lazy ValueAccessor(BindingContext context, string paramName) => - new Lazy(() => _pathResolver.ResolvePath(context, paramName)); void BlockParamsAction(BindingContext context) { @@ -52,11 +47,8 @@ void ValueBinder(string name, Func value) { if (!string.IsNullOrEmpty(name)) _accessors[name] = value; } - - var values = _params.Parameters - .ToDictionary(parameter => parameter, parameter => ValueAccessor(context, parameter)); - blockParamsConfiguration.Invoke(values, ValueBinder); + blockParamsConfiguration.Invoke(_params.Parameters, ValueBinder); } _invoker(BlockParamsAction); diff --git a/source/Handlebars/Compiler/Structure/PathResolver.cs b/source/Handlebars/Compiler/Structure/PathResolver.cs index b5c364e2..f2b93058 100644 --- a/source/Handlebars/Compiler/Structure/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/PathResolver.cs @@ -141,7 +141,7 @@ private object ResolveValue(BindingContext context, object instance, string segm return resolvedValue; } - private bool TryAccessMember(object instance, string memberName, out object value) + public bool TryAccessMember(object instance, string memberName, out object value) { value = new UndefinedBindingResult(memberName, _configuration); if (instance == null) diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index aaa552b2..bb1be263 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { @@ -20,14 +16,7 @@ private BlockHelperFunctionBinder(CompilationContext context) protected override Expression VisitStatementExpression(StatementExpression sex) { - if (sex.Body is BlockHelperExpression) - { - return Visit(sex.Body); - } - else - { - return sex; - } + return sex.Body is BlockHelperExpression ? Visit(sex.Body) : sex; } protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) @@ -36,57 +25,26 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b var fb = new FunctionBuilder(CompilationContext.Configuration); - - var bindingContext = isInlinePartial ? (Expression)CompilationContext.BindingContext : - Expression.Property( - CompilationContext.BindingContext, - typeof(BindingContext).GetProperty("Value")); - - var configuration = Expression.Constant(CompilationContext.Configuration); - var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); - var blockParamsExpression = Expression.New(ctor, CompilationContext.BindingContext, configuration, bhex.BlockParams); - - var body = fb.Compile(((BlockExpression) bhex.Body).Expressions, CompilationContext.BindingContext); - var inversion = fb.Compile(((BlockExpression)bhex.Inversion).Expressions, CompilationContext.BindingContext); + var context = E.Arg(CompilationContext.BindingContext); + var bindingContext = isInlinePartial + ? context.Cast() + : context.Property(o => o.Value); + + var blockParamsExpression = E.New( + () => new BlockParamsValueProvider(context, E.Arg(bhex.BlockParams)) + ); + + var body = fb.Compile(((BlockExpression) bhex.Body).Expressions, context); + var inversion = fb.Compile(((BlockExpression) bhex.Inversion).Expressions, context); var helper = CompilationContext.Configuration.BlockHelpers[bhex.HelperName.Replace("#", "")]; - var arguments = new Expression[] - { - Expression.Property( - CompilationContext.BindingContext, - typeof(BindingContext).GetProperty("TextWriter")), - Expression.New( - typeof(HelperOptions).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0], - body, - inversion, - blockParamsExpression), - //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 (helper.Target != null) - { - return Expression.Call( - Expression.Constant(helper.Target), -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); - } - else - { - return Expression.Call( -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); - } + + var helperOptions = E.New( + () => new HelperOptions(E.Arg(body), E.Arg(inversion), blockParamsExpression) + ); + + return E.Call( + () => helper(context.Property(o => o.TextWriter), helperOptions, bindingContext, E.Array(bhex.Arguments)) + ); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs index 6ef6379b..2439a473 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using HandlebarsDotNet.Compiler; using System.Linq.Expressions; -using System.Reflection; namespace HandlebarsDotNet.Compiler { @@ -20,13 +16,7 @@ private BoolishConverter(CompilationContext context) 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)); + return E.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(Visit(bex.Condition))); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs index 6c63e477..387c2ed2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Linq.Expressions; -#if netstandard -using System.Reflection; -#endif namespace HandlebarsDotNet.Compiler { @@ -16,48 +12,23 @@ private ContextBinder() { } - public static Expression Bind(Expression body, CompilationContext context, Expression parentContext, string templatePath) + public static Expression> Bind(Expression body, CompilationContext context, 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 writerParameter = E.Parameter("buffer"); + var objectParameter = E.Parameter("data"); + + var bindingContext = E.Arg(context.BindingContext); + var inlinePartialsParameter = E.Null>>(); + var encodedWriterExpression = E.Call(() => EncodedTextWriter.From(E.Arg(writerParameter), context.Configuration.TextEncoder)); + var newBindingContext = E.New( + () => new BindingContext(objectParameter, encodedWriterExpression, E.Arg(parentContext), 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[] - { - 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 }); - } - - private static Expression ResolveEncodedWriter(ParameterExpression writerParameter, ITextEncoder textEncoder) - { - var outputEncoderExpression = Expression.Constant(textEncoder, typeof(ITextEncoder)); - -#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); + var blockBuilder = E.Block().Parameter(bindingContext) + .Line(bindingContext.TernaryAssign(objectParameter.Is(), objectParameter.As(), newBindingContext)) + .Lines(((BlockExpression) body).Expressions); + + return Expression.Lambda>(blockBuilder, (ParameterExpression) writerParameter.Expression, (ParameterExpression) objectParameter.Expression); } } } \ 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..3c29a2b4 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,8 +1,6 @@ -using System; using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using System.Collections.Generic; +using System.IO; namespace HandlebarsDotNet.Compiler { @@ -20,90 +18,58 @@ private HelperFunctionBinder(CompilationContext context) protected override Expression VisitStatementExpression(StatementExpression sex) { - if (sex.Body is HelperExpression) - { - return Visit(sex.Body); - } - else - { - return sex; - } + return sex.Body is HelperExpression ? Visit(sex.Body) : sex; } - + protected override Expression VisitHelperExpression(HelperExpression hex) { - if (CompilationContext.Configuration.Helpers.ContainsKey(hex.HelperName)) + var bindingContext = E.Arg(CompilationContext.BindingContext); + var textWriter = bindingContext.Property(o => o.TextWriter); + var args = E.Array(hex.Arguments.Select(Visit)); + + if (CompilationContext.Configuration.Helpers.TryGetValue(hex.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 - { - return Expression.Call( -#if netstandard - helper.GetMethodInfo(), -#else - helper.Method, -#endif - arguments); - } + return E.Call(() => helper(textWriter, bindingContext, args)); } - else + + if (CompilationContext.Configuration.ReturnHelpers.TryGetValue(hex.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)); + return E.Call(() => CaptureResult(textWriter, + E.Call(() => returnHelper(bindingContext, args))) + ); } + + return E.Call(() => CaptureResult(bindingContext.Property(o => o.TextWriter), + E.Call(() => LateBindHelperExpression(bindingContext, hex.HelperName, args))) + ); } - private void LateBindHelperExpression( + private object LateBindHelperExpression( BindingContext context, string helperName, - IEnumerable arguments) + object[] arguments) { - if (CompilationContext.Configuration.Helpers.ContainsKey(helperName)) + if (CompilationContext.Configuration.Helpers.TryGetValue(helperName, out var helper)) { - var helper = CompilationContext.Configuration.Helpers[helperName]; - helper(context.TextWriter, context.Value, arguments.ToArray()); + using (var write = new StringWriter()) + { + helper(write, context.Value, arguments.ToArray()); + return write.ToString(); + } } - else + + if (CompilationContext.Configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { - throw new HandlebarsRuntimeException(string.Format("Template references a helper that is not registered. Could not find helper '{0}'", helperName)); + return returnHelper(context.Value, arguments.ToArray()); } + + throw new HandlebarsRuntimeException($"Template references a helper that is not registered. Could not find helper '{helperName}'"); + } + + 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 03838ba6..c8ecc7d2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -4,6 +4,7 @@ using System.IO; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Dynamic; using System.Reflection; @@ -23,134 +24,103 @@ private IteratorBinder(CompilationContext context) protected override Expression VisitIteratorExpression(IteratorExpression iex) { - var iteratorBindingContext = Expression.Variable(typeof(BindingContext), "context"); - var blockParamsValueBinder = Expression.Variable(typeof(BlockParamsValueProvider), "blockParams"); - var configuration = Expression.Constant(CompilationContext.Configuration); - var ctor = typeof(BlockParamsValueProvider).GetConstructors().Single(); + var fb = new FunctionBuilder(CompilationContext.Configuration); + + var iteratorBindingContext = E.Var("context"); + var blockParamsValueBinder = E.Var("blockParams"); + var sequence = E.Var("sequence"); + + var template = E.Arg(fb.Compile(new[] {iex.Template}, iteratorBindingContext)); + var ifEmpty = E.Arg(fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext)); + + var context = CompilationContext.BindingContext; + var compiledSequence = fb.Reduce(iex.Sequence, CompilationContext); + var blockParamsProvider = E.New(() => new BlockParamsValueProvider(iteratorBindingContext, E.Arg(iex.BlockParams))); - return Expression.Block( - new ParameterExpression[] - { - iteratorBindingContext, blockParamsValueBinder - }, - Expression.Assign(iteratorBindingContext, CompilationContext.BindingContext), - Expression.Assign(blockParamsValueBinder, Expression.New(ctor, iteratorBindingContext, configuration, iex.BlockParams)), - Expression.IfThenElse( - Expression.TypeIs(iex.Sequence, typeof(IEnumerable)), + + return E.Block() + .Parameter(iteratorBindingContext, context) + .Parameter(blockParamsValueBinder, blockParamsProvider) + .Parameter(sequence, compiledSequence) + .Line(Expression.IfThenElse( + sequence.Is(), 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, blockParamsValueBinder, iex), + E.Call(() => IsGenericDictionary(sequence)), + GetDictionaryIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), 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, blockParamsValueBinder, iex), - GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, iex))), - GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, iex)) - ); + E.Call(() => IsNonListDynamic(sequence)), + GetDynamicIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), + GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty))), + GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty))); } - private Expression GetEnumerableIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) - { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Call( -#if netstandard - new Action, Action>(IterateEnumerable).GetMethodInfo(), -#else - new Action, Action>(IterateEnumerable).Method, -#endif - new Expression[] - { - contextParameter, - blockParamsParameter, - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - }); + private Expression GetEnumerableIterator( + ExpressionContainer contextParameter, + ExpressionContainer blockParamsParameter, + ExpressionContainer sequence, + ExpressionContainer> template, + ExpressionContainer> ifEmpty + ) + { + return E.Call( + () => IterateEnumerable(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) + ); } - private Expression GetObjectIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) + private Expression GetObjectIterator( + ExpressionContainer contextParameter, + ExpressionContainer blockParamsParameter, + ExpressionContainer sequence, + ExpressionContainer> template, + ExpressionContainer> ifEmpty + ) { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Call( -#if netstandard - new Action, Action>(IterateObject).GetMethodInfo(), -#else - new Action, Action>(IterateObject).Method, -#endif - new[] - { - contextParameter, - blockParamsParameter, - iex.Sequence, - fb.Compile(new [] { iex.Template }, contextParameter), - fb.Compile(new [] { iex.IfEmpty }, CompilationContext.BindingContext) - }); + return E.Call( + () => IterateObject(contextParameter, blockParamsParameter, sequence, template, ifEmpty) + ); } - private Expression GetDictionaryIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) + private Expression GetDictionaryIterator( + ExpressionContainer contextParameter, + ExpressionContainer blockParamsParameter, + ExpressionContainer sequence, + ExpressionContainer> template, + ExpressionContainer> ifEmpty + ) { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Call( -#if netstandard - new Action, Action>(IterateDictionary).GetMethodInfo(), -#else - new Action, Action>(IterateDictionary).Method, -#endif - new[] - { - contextParameter, - blockParamsParameter, - Expression.Convert(iex.Sequence, typeof(IEnumerable)), - fb.Compile(new[] {iex.Template}, contextParameter), - fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) - }); + return E.Call( + () => IterateDictionary(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) + ); } - private Expression GetDynamicIterator(Expression contextParameter, Expression blockParamsParameter, IteratorExpression iex) + private Expression GetDynamicIterator( + ExpressionContainer contextParameter, + ExpressionContainer blockParamsParameter, + ExpressionContainer sequence, + ExpressionContainer> template, + ExpressionContainer> ifEmpty) { - var fb = new FunctionBuilder(CompilationContext.Configuration); - return Expression.Call( -#if netstandard - new Action,Action>(IterateDynamic).GetMethodInfo(), -#else - new Action, Action>(IterateDynamic).Method, -#endif - new[] - { - contextParameter, - blockParamsParameter, - Expression.Convert(iex.Sequence, typeof(IDynamicMetaObjectProvider)), - fb.Compile(new[] {iex.Template}, contextParameter), - fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext) - }); + return E.Call( + () => IterateDynamic(contextParameter, blockParamsParameter, (IDynamicMetaObjectProvider) sequence, template, ifEmpty) + ); } private static bool IsNonListDynamic(object target) { - var interfaces = target.GetType().GetInterfaces(); - return interfaces.Contains(typeof(IDynamicMetaObjectProvider)) - && ((IDynamicMetaObjectProvider)target).GetMetaObject(Expression.Constant(target)).GetDynamicMemberNames().Any(); + if (target is IDynamicMetaObjectProvider metaObjectProvider) + { + return metaObjectProvider.GetMetaObject(Expression.Constant(target)).GetDynamicMemberNames().Any(); + } + + return false; } private static bool IsGenericDictionary(object target) { return target.GetType() -#if netstandard .GetInterfaces() .Where(i => i.GetTypeInfo().IsGenericType) - -#else - .GetInterfaces() - .Where(i => i.IsGenericType) -#endif .Any(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); } @@ -167,9 +137,8 @@ private static void IterateObject( context.RegisterValueProvider(objectEnumerator); blockParamsValueProvider.Configure((parameters, binder) => { - binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); - binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); - binder(parameters.Keys.ElementAtOrDefault(2), () => target); + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); }); objectEnumerator.Index = 0; @@ -212,21 +181,18 @@ private static void IterateDictionary( context.RegisterValueProvider(objectEnumerator); blockParamsValueProvider.Configure((parameters, binder) => { - binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); - binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); - binder(parameters.Keys.ElementAtOrDefault(2), () => target); + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); }); objectEnumerator.Index = 0; var targetType = target.GetType(); -#if netstandard - var keysProperty = targetType.GetRuntimeProperty("Keys"); -#else - var keysProperty = targetType.GetProperty("Keys"); -#endif - if (keysProperty?.GetGetMethod().Invoke(target, null) is IEnumerable keys) + var keysProperty = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(o => o.Name.EndsWith("Keys")); + if (keysProperty?.GetValue(target) is IEnumerable keys) { - var getItemMethodInfo = targetType.GetMethod("get_Item"); + var getItemMethodInfo = targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => o.Name == "get_Item"); + var parameters = new object[1]; foreach (var enumerableValue in new ExtendedEnumerable(keys)) { @@ -264,9 +230,8 @@ private static void IterateDynamic( context.RegisterValueProvider(objectEnumerator); blockParamsValueProvider.Configure((parameters, binder) => { - binder(parameters.Keys.ElementAtOrDefault(0), () => objectEnumerator.Key); - binder(parameters.Keys.ElementAtOrDefault(1), () => objectEnumerator.Value); - binder(parameters.Keys.ElementAtOrDefault(2), () => target); + binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); + binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); }); objectEnumerator.Index = 0; @@ -305,9 +270,8 @@ private static void IterateEnumerable( context.RegisterValueProvider(iterator); blockParamsValueProvider.Configure((parameters, binder) => { - binder(parameters.Keys.ElementAtOrDefault(0), () => iterator.Index); - binder(parameters.Keys.ElementAtOrDefault(1), () => iterator.Value); - binder(parameters.Keys.ElementAtOrDefault(2), () => sequence); + binder(parameters.ElementAtOrDefault(0), () => iterator.Value); + binder(parameters.ElementAtOrDefault(1), () => iterator.Index); }); iterator.Index = 0; diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 9cfa52b5..3e2173b2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -26,43 +26,26 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b protected override Expression VisitStatementExpression(StatementExpression sex) { - if (sex.Body is PartialExpression) - { - return Visit(sex.Body); - } - else - { - return sex; - } + return sex.Body is PartialExpression ? Visit(sex.Body) : sex; } protected override Expression VisitPartialExpression(PartialExpression pex) { - Expression bindingContext = CompilationContext.BindingContext; + var bindingContext = E.Arg(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(nameof(BindingContext.CreateChildContext)), - pex.Argument ?? Expression.Constant(null), - partialBlockTemplate ?? Expression.Constant(null, typeof(Action))); + bindingContext = bindingContext.Call(o => + o.CreateChildContext(E.Arg(pex.Argument), E.Arg(partialBlockTemplate)) + ); } - 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; + return E.Call(() => + InvokePartialWithFallback(E.Cast(pex.PartialName), bindingContext, CompilationContext.Configuration) + ); } private static void InvokePartialWithFallback( @@ -70,23 +53,19 @@ private static void InvokePartialWithFallback( BindingContext context, HandlebarsConfiguration configuration) { - if (!InvokePartial(partialName, context, configuration)) + if (InvokePartial(partialName, context, configuration)) return; + if (context.PartialBlockTemplate == null) { - if (context.PartialBlockTemplate == null) + if (configuration.MissingPartialTemplateHandler != 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)); - } + configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); + return; } - context.PartialBlockTemplate(context.TextWriter, context); + throw new HandlebarsRuntimeException($"Referenced partial name {partialName} could not be resolved"); } + + context.PartialBlockTemplate(context.TextWriter, context); } private static bool InvokePartial( diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index e2e1f080..4c80fef0 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,7 +1,4 @@ -using System; using System.Linq.Expressions; -using System.Reflection; -using System.IO; namespace HandlebarsDotNet.Compiler { @@ -22,36 +19,18 @@ private PathBinder(CompilationContext context) protected override Expression VisitStatementExpression(StatementExpression sex) { - 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); - } + if (!(sex.Body is PathExpression)) return Visit(sex.Body); + + var context = E.Arg(CompilationContext.BindingContext); + + return context.Property(o => o.TextWriter) + .Call(o => o.Write(E.Arg(Visit(sex.Body)))); } protected override Expression VisitPathExpression(PathExpression pex) { - return Expression.Call( - Expression.Constant(_pathResolver), -#if netstandard - new Func(_pathResolver.ResolvePath).GetMethodInfo(), -#else - new Func(_pathResolver.ResolvePath).Method, -#endif - CompilationContext.BindingContext, - Expression.Constant(pex.Path)); + var context = E.Arg(CompilationContext.BindingContext); + return E.Call(() => _pathResolver.ResolvePath(context, pex.Path)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index b7e6ac88..38b2023c 100755 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.IO; -using System.Text; -using System.Reflection; +using System.Linq; namespace HandlebarsDotNet.Compiler { @@ -20,65 +18,66 @@ private SubExpressionVisitor(CompilationContext context) 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 fb = new FunctionBuilder(CompilationContext.Configuration); + var expression = fb.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 (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 - } - else + if (helperCall.Type != typeof(void)) { -#if netstandard - helper = (HandlebarsHelper)helperCall.Method.CreateDelegate(typeof(HandlebarsHelper)); -#else - helper = (HandlebarsHelper)Delegate.CreateDelegate(typeof(HandlebarsHelper), helperCall.Method); -#endif + return helperCall.Update(helperCall.Object, + E.ReplaceValuesOf(helperCall.Arguments, E.Null()).Select(Visit) + ); } - return helper; + + var writer = E.Var(); + helperCall = helperCall.Update(helperCall.Object, + E.ReplaceValuesOf(helperCall.Arguments, writer).Select(Visit) + ); + + return E.Block() + .Parameter(writer) + .Line(Expression.Assign(writer, E.New())) + .Line(helperCall) + .Line(writer.Call(w => w.ToString())) + .Invoke(); } - 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, + E.ReplaceValuesOf(invocation.Arguments, E.Null()).Select(Visit) + ); } - return builder.ToString(); + + var writer = E.Var(); + invocation = invocation.Update(invocation.Expression, + E.ReplaceValuesOf(invocation.Arguments, writer).Select(Visit) + ); + + return E.Block() + .Parameter(writer) + .Line(Expression.Assign(writer, E.New())) + .Line(invocation) + .Line(writer.Call(w => w.ToString())) + .Invoke(); } } } diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index e59108ce..2db5e68d 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -4,6 +4,7 @@ namespace HandlebarsDotNet { public delegate void HandlebarsHelper(TextWriter output, dynamic context, params object[] arguments); + public delegate object HandlebarsReturnHelper(dynamic context, params object[] arguments); public delegate void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments); public sealed partial class Handlebars @@ -48,6 +49,11 @@ public static void RegisterHelper(string helperName, HandlebarsHelper helperFunc { Instance.RegisterHelper(helperName, helperFunction); } + + public static void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + { + Instance.RegisterHelper(helperName, helperFunction); + } public static void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { diff --git a/source/Handlebars/HandlebarsConfiguration.cs b/source/Handlebars/HandlebarsConfiguration.cs index d0910beb..e06ba1b2 100644 --- a/source/Handlebars/HandlebarsConfiguration.cs +++ b/source/Handlebars/HandlebarsConfiguration.cs @@ -1,13 +1,17 @@ using HandlebarsDotNet.Compiler.Resolvers; using System; +using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; namespace HandlebarsDotNet { public class HandlebarsConfiguration { public IDictionary Helpers { get; private set; } + + public IDictionary ReturnHelpers { get; private set; } public IDictionary BlockHelpers { get; private set; } @@ -37,6 +41,7 @@ public class HandlebarsConfiguration public HandlebarsConfiguration() { this.Helpers = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.ReturnHelpers = new Dictionary(StringComparer.OrdinalIgnoreCase); this.BlockHelpers = new Dictionary(StringComparer.OrdinalIgnoreCase); this.PartialTemplateResolver = new FileSystemPartialTemplateResolver(); this.RegisteredTemplates = new Dictionary>(StringComparer.OrdinalIgnoreCase); diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 53273882..5559cac8 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -91,6 +91,14 @@ public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) _configuration.Helpers.AddOrUpdate(helperName, helperFunction); } } + + public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + { + lock (_configuration) + { + _configuration.ReturnHelpers.AddOrUpdate(helperName, helperFunction); + } + } public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { @@ -106,6 +114,10 @@ private void RegisterBuiltinHelpers() { RegisterHelper(helperDefinition.Key, helperDefinition.Value); } + foreach (var helperDefinition in BuiltinHelpers.ReturnHelpers) + { + RegisterHelper(helperDefinition.Key, helperDefinition.Value); + } foreach (var helperDefinition in BuiltinHelpers.BlockHelpers) { RegisterHelper(helperDefinition.Key, helperDefinition.Value); diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 61140ea4..f6e88278 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -20,6 +20,8 @@ public interface IHandlebars void RegisterTemplate(string templateName, string template); void RegisterHelper(string helperName, HandlebarsHelper helperFunction); + + void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction); void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction); } From 7b68b87e72274dea3a1a259111e370f96b1612be Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 3 Mar 2020 14:06:03 -0800 Subject: [PATCH 14/53] Further improvements --- source/Handlebars.Benchmark/Compilation.cs | 38 ++- source/Handlebars.Benchmark/Execution.cs | 142 ++++++--- .../Handlebars.Benchmark.csproj | 1 + source/Handlebars.Benchmark/Program.cs | 5 + .../Handlebars.Test/BasicIntegrationTests.cs | 40 ++- source/Handlebars.Test/HelperTests.cs | 2 +- source/Handlebars/BuiltinHelpers.cs | 34 +-- .../Compiler/ExpressionShortcuts.cs | 78 +++-- source/Handlebars/Compiler/FunctionBuilder.cs | 2 +- .../Lexer/Converter/BlockParamsConverter.cs | 2 +- .../Converter/CommentAndLayoutConverter.cs | 12 +- .../Converter/ExpressionScopeConverter.cs | 5 +- .../Converter/HelperArgumentAccumulator.cs | 50 +-- .../Lexer/Converter/HelperConverter.cs | 55 ++-- .../Lexer/Converter/LiteralConverter.cs | 85 +++--- .../Lexer/Converter/PartialConverter.cs | 58 ++-- .../Compiler/Lexer/Converter/PathConverter.cs | 4 +- .../Lexer/Converter/StaticConverter.cs | 17 +- .../Compiler/Lexer/Tokens/LayoutToken.cs | 10 +- .../Compiler/Structure/BindingContext.cs | 21 +- .../Structure/BindingContextValueProvider.cs | 2 +- .../Structure/BlockHelperExpression.cs | 32 +- .../Structure/BlockParamsValueProvider.cs | 2 +- .../Compiler/Structure/BoolishExpression.cs | 19 +- .../Compiler/Structure/CommentExpression.cs | 18 +- .../Structure/DeferredSectionExpression.cs | 16 +- .../HashParameterAssignmentExpression.cs | 14 +- .../Structure/HashParametersExpression.cs | 12 +- .../Compiler/Structure/HelperExpression.cs | 37 +-- .../Compiler/Structure/IValueProvider.cs | 11 +- .../Compiler/Structure/IteratorExpression.cs | 38 +-- .../Compiler/Structure/PartialExpression.cs | 35 +-- .../Compiler/Structure/PathExpression.cs | 22 +- .../Compiler/Structure/PathResolver.cs | 180 +++++++---- .../Compiler/Structure/StatementExpression.cs | 18 +- .../Compiler/Structure/StaticExpression.cs | 19 +- .../Structure/SubExpressionExpression.cs | 19 +- .../Expression/BlockHelperFunctionBinder.cs | 14 +- .../Expression/BoolishConverter.cs | 2 +- .../Translation/Expression/ContextBinder.cs | 19 +- .../Expression/DeferredSectionVisitor.cs | 19 +- .../Expression/HashParameterBinder.cs | 30 +- .../Expression/HelperFunctionBinder.cs | 18 +- .../Translation/Expression/IteratorBinder.cs | 288 ++++++++++++++---- .../Translation/Expression/PartialBinder.cs | 8 +- .../Translation/Expression/PathBinder.cs | 12 +- .../Translation/Expression/StaticReplacer.cs | 11 +- .../Expression/SubExpressionVisitor.cs | 41 +-- .../Expression/UnencodedStatementVisitor.cs | 24 +- source/Handlebars/EncodedTextWriter.cs | 16 +- source/Handlebars/Handlebars.csproj | 4 +- source/Handlebars/HandlebarsEnvironment.cs | 114 +++++-- source/Handlebars/HandlebarsUtils.cs | 37 +-- 53 files changed, 1038 insertions(+), 774 deletions(-) diff --git a/source/Handlebars.Benchmark/Compilation.cs b/source/Handlebars.Benchmark/Compilation.cs index 661167fb..a77e4629 100644 --- a/source/Handlebars.Benchmark/Compilation.cs +++ b/source/Handlebars.Benchmark/Compilation.cs @@ -5,7 +5,7 @@ namespace Benchmark { - [SimpleJob(RuntimeMoniker.Net461)] + //[SimpleJob(RuntimeMoniker.Net461)] [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] public class Compilation { @@ -39,19 +39,45 @@ public Func Complex() } [Benchmark] - public Func Each() + public Func WithParentIndex() { - const string template = "{{#each enumerateMe}}{{this}} {{/each}}"; + const string template = @" + {{#each level1}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level2}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level3}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{/each}} + {{/each}} + {{/each}}"; + return _handlebars.Compile(template); } - + [Benchmark] - public Func EachBlockParams() + public Func Each() { - const string template = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; + const string template = "{{#each enumerateMe}}{{this}} {{/each}}"; return _handlebars.Compile(template); } + // [Benchmark] + // public Func EachBlockParams() + // { + // const string template = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; + // return _handlebars.Compile(template); + // } + [Benchmark] public Func Helper() { diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index 2290a1dd..f5e68c5e 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -10,13 +10,13 @@ namespace Benchmark { - [SimpleJob(RuntimeMoniker.Net461)] + //[SimpleJob(RuntimeMoniker.Net461)] [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] public class Execution { private IHandlebars _handlebars; - [Params(10000)] + [Params(100)] public int N; private Faker _faker; @@ -34,21 +34,35 @@ public void Setup() _handlebars = Handlebars.Create(); _faker = new Faker(); - const string eachTemplate = "{{#each County}}{{this}} {{/each}}"; - const string blockParamsEach = "{{#each County as |item val|}}{{item}} {{val}} {{/each}}"; + const string eachTemplate = "{{#each County}}{{@key}}:{{@value}}\n{{/each}}"; const string complexTemplate = "{{#each County}}" + "{{#if @first}}" + - "{{this}}" + + "{{@key}}:{{@value}}\n" + "{{else}}" + - "{{#if @last}}" + - " and {{this}}" + - "{{else}}" + - ", {{this}}" + - "{{/if}}" + + ", {{@key}}" + "{{/if}}" + "{{/each}}"; const string helperTemplate = "{{customHelper 'value'}}"; const string notRegisteredHelperTemplate = "{{not_registered_helper 'value'}}"; + const string withParentIndex = @" + {{#each level1}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level2}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level3}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{/each}} + {{/each}} + {{/each}}"; _handlebars.RegisterHelper("customHelper", (writer, context, parameters) => { @@ -72,14 +86,14 @@ public void Setup() _templates = new[] { _handlebars.Compile(eachTemplate), - _handlebars.Compile(blockParamsEach), + _handlebars.Compile(withParentIndex), _handlebars.Compile(complexTemplate), _handlebars.Compile(helperTemplate), _handlebars.Compile(notRegisteredHelperTemplate), }; } - [IterationSetup(Targets = new[]{nameof(SimpleEachJsonInput), nameof(EachBlockParamsJsonInput), nameof(ComplexJsonInput)})] + [IterationSetup(Targets = new[]{nameof(SimpleEachJsonInput)/*, nameof(EachBlockParamsJsonInput)*/, nameof(ComplexJsonInput)})] public void JsonIterationSetup() { var json = new JObject(); @@ -90,29 +104,29 @@ public void JsonIterationSetup() _jsonData = new Dictionary { - ["Country"] = json + ["County"] = json }; } - [IterationSetup(Targets = new[]{nameof(SimpleEachDictionaryInput), nameof(EachBlockParamsDictionaryInput), nameof(ComplexDictionaryInput)})] + [IterationSetup(Targets = new[]{nameof(SimpleEachDictionaryInput)/*, nameof(EachBlockParamsDictionaryInput)*/, nameof(ComplexDictionaryInput)})] public void DictionaryIterationSetup() { _dictionaryData = new Dictionary { - ["Country"] = _propertyNames.ToDictionary(o => o, o => Guid.NewGuid().ToString()) + ["County"] = _propertyNames.ToDictionary(o => o, o => Guid.NewGuid().ToString()) }; } - [IterationSetup(Targets = new[]{nameof(SimpleEachArrayInput), nameof(EachBlockParamsArrayInput), nameof(ComplexArrayInput)})] + [IterationSetup(Targets = new[]{nameof(SimpleEachArrayInput)/*, nameof(EachBlockParamsArrayInput)*/, nameof(ComplexArrayInput)})] public void ArrayIterationSetup() { _arrayData = new Dictionary { - ["Country"] = _propertyNames.Select(o => Guid.NewGuid().ToString()).ToArray() + ["County"] = _propertyNames.Select(o => Guid.NewGuid().ToString()).ToArray() }; } - [IterationSetup(Targets = new[]{nameof(SimpleEachObjectInput), nameof(EachBlockParamsObjectInput), nameof(ComplexObjectInput)})] + [IterationSetup(Targets = new[]{nameof(SimpleEachObjectInput)/*, nameof(EachBlockParamsObjectInput)*/, nameof(ComplexObjectInput)})] public void ObjectIterationSetup() { var data = Activator.CreateInstance(_type); @@ -124,7 +138,7 @@ public void ObjectIterationSetup() _objectData = new Dictionary { - ["Country"] = data + ["County"] = data }; } @@ -139,11 +153,11 @@ public string SimpleEachArrayInput() return _templates[0].Invoke(_arrayData); } - [Benchmark] - public string EachBlockParamsArrayInput() - { - return _templates[1].Invoke(_arrayData); - } + // [Benchmark] + // public string EachBlockParamsArrayInput() + // { + // return _templates[1].Invoke(_arrayData); + // } [Benchmark] public string ComplexArrayInput() @@ -157,11 +171,11 @@ public string SimpleEachObjectInput() return _templates[0].Invoke(_objectData); } - [Benchmark] - public string EachBlockParamsObjectInput() - { - return _templates[1].Invoke(_objectData); - } + // [Benchmark] + // public string EachBlockParamsObjectInput() + // { + // return _templates[1].Invoke(_objectData); + // } [Benchmark] public string ComplexObjectInput() @@ -175,11 +189,11 @@ public string SimpleEachJsonInput() return _templates[0].Invoke(_jsonData); } - [Benchmark] - public string EachBlockParamsJsonInput() - { - return _templates[1].Invoke(_jsonData); - } + // [Benchmark] + // public string EachBlockParamsJsonInput() + // { + // return _templates[1].Invoke(_jsonData); + // } [Benchmark] public string ComplexJsonInput() @@ -193,11 +207,11 @@ public string SimpleEachDictionaryInput() return _templates[0].Invoke(_dictionaryData); } - [Benchmark] - public string EachBlockParamsDictionaryInput() - { - return _templates[1].Invoke(_dictionaryData); - } + // [Benchmark] + // public string EachBlockParamsDictionaryInput() + // { + // return _templates[1].Invoke(_dictionaryData); + // } [Benchmark] public string ComplexDictionaryInput() @@ -221,5 +235,55 @@ public string HelperPostRegister() return _templates[4].Invoke(new object()); } + + [Benchmark] + public string WithParentIndex() + { + var data = new + { + level1 = new[]{ + new { + id = "0", + level2 = new[]{ + new { + id = "0-0", + level3 = new[]{ + new { id = "0-0-0" }, + new { id = "0-0-1" } + } + }, + new { + id = "0-1", + level3 = new[]{ + new { id = "0-1-0" }, + new { id = "0-1-1" } + } + } + } + }, + new { + id = "1", + level2 = new[]{ + new { + id = "1-0", + level3 = new[]{ + new { id = "1-0-0" }, + new { id = "1-0-1" } + } + }, + new { + id = "1-1", + level3 = new[]{ + new { id = "1-1-0" }, + new { id = "1-1-1" } + } + } + } + } + } + }; + + return _templates[1].Invoke(data); + } } } \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index b9508a7d..c3a9374b 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -8,6 +8,7 @@ + diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs index 6c6b4bea..1fdfb522 100644 --- a/source/Handlebars.Benchmark/Program.cs +++ b/source/Handlebars.Benchmark/Program.cs @@ -9,6 +9,11 @@ class Program { static void Main(string[] args) { + var execution = new Execution(); + execution.N = 100; + execution.Setup(); + execution.WithParentIndex(); + var manualConfig = DefaultConfig.Instance.WithArtifactsPath( $"./Benchmark-{FileVersionInfo.GetVersionInfo(typeof(Handlebars).Assembly.Location).FileVersion}" ); diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 1a5e130a..cc8ead36 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -441,6 +441,23 @@ public void BasicObjectEnumerator() Assert.Equal("hello world ", result); } + [Fact] + public void BasicListEnumerator() + { + 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); + } + [Fact] public void BasicObjectEnumeratorWithLast() { @@ -1541,7 +1558,7 @@ public void CollectionReturnFromHelper() {"Nils", arguments[0].ToString()}, {"Yehuda", arguments[1].ToString()} }; - + return data; }); var source = $"{{{{#each ({getData} 'Darmstadt' 'San Francisco')}}}}{{{{@key}}}} lives in {{{{@value}}}}. {{{{/each}}}}"; @@ -1562,7 +1579,7 @@ public void ReturnFromHelperWithSubExpression() writer.WriteSafeString(" "); writer.WriteSafeString(arguments[1]); }); - + var getData = $"getData{Guid.NewGuid()}"; Handlebars.RegisterHelper(getData, (context, arguments) => { @@ -1571,7 +1588,7 @@ public void ReturnFromHelperWithSubExpression() var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; var template = Handlebars.Compile(source); - + var result = template(new object()); Assert.Equal("data 42", result); } @@ -1597,7 +1614,7 @@ public void ReturnFromHelperLateBindWithSubExpression() var result = template(new object()); Assert.Equal("data 42", result); } - + [Fact] public void BasicLookup() { @@ -1612,7 +1629,7 @@ public void BasicLookup() var result = template(data); Assert.Equal("Nils lives in Darmstadt Yehuda lives in San Francisco ", result); } - + [Fact] public void LookupAsSubExpression() { @@ -1652,6 +1669,19 @@ public void LookupAsSubExpression() Assert.Equal("Nils lives in Darmstadt (Germany)Yehuda lives in San Francisco (USA)", result); } + [Fact] + private void StringConditionTest() + { + var template = "{{#if Email}}\"correo\": \"{{Email}}\",{{else}}\"correo\": \"no hay correo\",{{/if}}"; + var data = new + { + Email = "correo@gmail.com" + }; + + var func = Handlebars.Compile(template); + var s = func(data); + } + private class MockDictionary : IDictionary { public void Add(string key, string value) diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index 3cab30f1..e604096d 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -1,4 +1,4 @@ -using Xunit; +using Xunit; using System; using System.Collections; using System.Collections.Generic; diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs index 0e2e9c97..19685ae9 100644 --- a/source/Handlebars/BuiltinHelpers.cs +++ b/source/Handlebars/BuiltinHelpers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Reflection; using System.Collections.Generic; @@ -30,7 +30,7 @@ public static void With(TextWriter output, HelperOptions options, dynamic contex } } - [Description("Lookup")] + [Description("lookup")] public static object Lookup(dynamic context, params object[] arguments) { if (arguments.Length != 2) @@ -39,9 +39,9 @@ public static object Lookup(dynamic context, params object[] arguments) } var configuration = new HandlebarsConfiguration(); - var pathResolver = new PathResolver(configuration); + var pathResolver = new PathResolver(); var memberName = arguments[1].ToString(); - return !pathResolver.TryAccessMember(arguments[0], memberName, out var value) + return !pathResolver.TryAccessMember(arguments[0], new ChainSegment(memberName), configuration, out var value) ? new UndefinedBindingResult(memberName, configuration) : value; } @@ -72,29 +72,11 @@ public static void Inline(TextWriter output, HelperOptions options, dynamic cont context.InlinePartialTemplates.Add(key, options.Template); } - public static IEnumerable> Helpers - { - get - { - return GetHelpers(); - } - } - - public static IEnumerable> ReturnHelpers - { - get - { - return GetHelpers(); - } - } + public static IEnumerable> Helpers => GetHelpers(); - public static IEnumerable> BlockHelpers - { - get - { - return GetHelpers(); - } - } + public static IEnumerable> ReturnHelpers => GetHelpers(); + + public static IEnumerable> BlockHelpers => GetHelpers(); private static IEnumerable> GetHelpers() { diff --git a/source/Handlebars/Compiler/ExpressionShortcuts.cs b/source/Handlebars/Compiler/ExpressionShortcuts.cs index e3a140d8..5fe19316 100644 --- a/source/Handlebars/Compiler/ExpressionShortcuts.cs +++ b/source/Handlebars/Compiler/ExpressionShortcuts.cs @@ -36,7 +36,7 @@ internal class ExpressionContainer /// /// Provides strongly typed container for . /// - /// Used to trick C# compiler in cases like in order to pass value to target method. + /// Used to trick C# compiler in cases like in order to pass value to target method. /// Type of expected result value. internal class ExpressionContainer : ExpressionContainer { @@ -50,7 +50,7 @@ public ExpressionContainer(Expression expression) : base(expression) /// /// Stands for shortcuts. /// - internal static class E + internal static class ExpressionShortcuts { /// /// Creates strongly typed representation of the @@ -180,19 +180,6 @@ public static ExpressionContainer Call(Expression invocationExpression) { return new ExpressionContainer(ProcessCallLambda(invocationExpression)); } - - /// - /// Creates or based on . - /// Parameters are resolved based on actual passed parameters. - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(ExpressionContainer instance, ExpressionContainer method, params ExpressionContainer[] parameters) - { - var methodCallExpression = (MethodCallExpression) ProcessCall(method, instance); - return methodCallExpression.Update(methodCallExpression.Object, parameters.Cast()); - } /// /// Creates or based on . @@ -231,9 +218,9 @@ public static ExpressionContainer New(Expression> invocationExpres /// Provides fluent interface for creation /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BlockBuilder Block() + public static BlockBuilder Block(Type returnType = null) { - return new BlockBuilder(); + return new BlockBuilder(returnType); } /// @@ -286,30 +273,43 @@ public static ExpressionContainer Property(this ExpressionContainer - /// Creates strongly typed representation of the + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. /// /// - /// Property accessor expression - /// Expected type of resulting target - /// Expected type of resulting + /// Expression used to invoke the method. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Property(this ExpressionContainer instance, string propertyName) + public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) { - return Property(instance.Expression, propertyName); + return Arg(ProcessCallLambda(invocationExpression, instance)); } /// - /// Creates or based on . + /// Creates . /// Parameters are resolved based on actual passed parameters. /// /// - /// Expression used to invoke the method. - /// + /// Expressions used inside of try block. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) + public static TryExpression Using(this ExpressionContainer instance, Func, BlockBody> blockBody) where T : IDisposable { - return Arg(ProcessCallLambda(invocationExpression, instance)); + ExpressionContainer variable = Var(); + var block = Block() + .Parameter(variable, instance) + .Lines(blockBody(variable)); + + return Expression.TryFinally(block, instance.Call(o => o.Dispose())); + } + + public static Expression[] Return(this ExpressionContainer instance) + { + LabelTarget returnTarget = Expression.Label(typeof(T)); + + GotoExpression returnExpression = Expression.Return(returnTarget, instance, typeof(T)); + LabelExpression returnLabel = Expression.Label(returnTarget, Null()); + + return new Expression[]{returnExpression, returnLabel}; } /// @@ -322,6 +322,16 @@ public static ExpressionContainer Assign(this ExpressionContainer target, return new ExpressionContainer(Expression.Assign(target, value)); } + /// + /// Creates assign . + /// Parameters are resolved based on actual passed parameters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Assign(this ExpressionContainer target, T value) + { + return new ExpressionContainer(Expression.Assign(target, Expression.Constant(value, typeof(T)))); + } + /// /// Creates assign . /// Parameters are resolved based on actual passed parameters. @@ -492,11 +502,13 @@ private static object ExtractFromExpression(Expression expression) internal class BlockBuilder: ExpressionContainer { + private readonly Type _returnType; private readonly List _expressions; private readonly List _parameters; - public BlockBuilder() : base(null) + public BlockBuilder(Type returnType) : base(null) { + _returnType = returnType; _expressions = new List(); _parameters = new List(); } @@ -581,10 +593,14 @@ public BlockBuilder Lines(IEnumerable e) /// public ExpressionContainer Invoke(params ExpressionContainer[] parameters) { - return Arg(Expression.Lambda(Expression, parameters.Select(o => (ParameterExpression) o.Expression))); + var lambda = Expression.Lambda(Expression, parameters.Select(o => (ParameterExpression) o.Expression)); + return Arg(Expression.Invoke(lambda)); } - public override Expression Expression => Expression.Block(_parameters, _expressions); + public override Expression Expression => + _returnType == null + ? Expression.Block(_parameters, _expressions) + : Expression.Block(_returnType, _parameters, _expressions); } } diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index 8a465edf..be6465ee 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -20,7 +20,7 @@ public FunctionBuilder(HandlebarsConfiguration configuration) _configuration = configuration; } - public Expression Reduce(Expression expression, CompilationContext compilationContext) + public static Expression Reduce(Expression expression, CompilationContext compilationContext) { expression = CommentVisitor.Visit(expression, compilationContext); expression = UnencodedStatementVisitor.Visit(expression, compilationContext); diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs index b654ac38..3ef76923 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockParamsConverter.cs @@ -9,7 +9,7 @@ internal class BlockParamsConverter : TokenConverter { public static IEnumerable Convert(IEnumerable sequence) { - return new BlockParamsConverter().ConvertTokens(sequence).ToList(); + return new BlockParamsConverter().ConvertTokens(sequence); } private BlockParamsConverter() 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..7b721d68 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs @@ -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/HelperArgumentAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs index 18fee4bf..b1e15dd3 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs @@ -23,37 +23,41 @@ 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, 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, + 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; } } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs index 6bb21e9e..270a4f75 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs @@ -8,7 +8,10 @@ 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, @@ -30,53 +33,43 @@ 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) - { - 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; - } - } - else - { + case WordExpressionToken word when IsRegisteredHelperName(word.Value): + yield return HandlebarsExpression.Helper(word.Value); + break; + case WordExpressionToken word when IsRegisteredBlockHelperName(word.Value, isRaw): + yield return HandlebarsExpression.Helper(word.Value, isRaw); + break; + default: yield return item; - } - } - else - { - yield return item; + break; } } } private bool IsRegisteredHelperName(string name) { - return _configuration.Helpers.ContainsKey(name) || builtInHelpers.Contains(name); + return _configuration.Helpers.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); + return _configuration.BlockHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); } 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..e1defc5e 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs @@ -1,52 +1,47 @@ -using System; -using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Lexer; -using System.Linq.Expressions; -using System.Linq; - -namespace HandlebarsDotNet.Compiler -{ - internal class LiteralConverter : TokenConverter - { - public static IEnumerable Convert(IEnumerable sequence) - { - return new LiteralConverter().ConvertTokens(sequence).ToList(); - } - - private LiteralConverter() - { - } - - public override IEnumerable ConvertTokens(IEnumerable sequence) - { - foreach (var item in sequence) +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Lexer; +using System.Linq.Expressions; +using System.Linq; + +namespace HandlebarsDotNet.Compiler +{ + internal class LiteralConverter : TokenConverter + { + public static IEnumerable Convert(IEnumerable sequence) + { + return new LiteralConverter().ConvertTokens(sequence).ToList(); + } + + private LiteralConverter() + { + } + + public override IEnumerable ConvertTokens(IEnumerable sequence) + { + foreach (var item in sequence) { - bool boolValue; - int intValue; - - object result = item; - - if (item is LiteralExpressionToken literalExpression) - { - result = Expression.Convert(Expression.Constant(literalExpression.Value), typeof(object)); - - if (!literalExpression.IsDelimitedLiteral) + var result = item; + switch (item) + { + case LiteralExpressionToken literalExpression: { - if (int.TryParse(literalExpression.Value, out intValue)) + 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)); } - } - } - else if (item is WordExpressionToken wordExpression - && bool.TryParse(wordExpression.Value, out boolValue)) - { - result = Expression.Convert(Expression.Constant(boolValue), typeof(object)); + + break; + } + case WordExpressionToken wordExpression when bool.TryParse(wordExpression.Value, out var boolValue): + result = Expression.Convert(Expression.Constant(boolValue), typeof(object)); + break; } - yield return result; - } - } - } -} - + yield return result; + } + } + } +} + diff --git a/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs index 1d1691cc..d7589a4f 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 == false) { - if ((item is Expression) == false) - { - throw new HandlebarsCompilerException(string.Format("Token '{0}' could not be converted to an expression", item)); - } + if (item is Expression == false) 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..f9ecc2c8 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs @@ -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/StaticConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs index ff990bf0..dd6e4d47 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs @@ -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/Tokens/LayoutToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/LayoutToken.cs index ffaa60f6..5b58399d 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/LayoutToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/LayoutToken.cs @@ -9,14 +9,8 @@ public LayoutToken(string layout) _layout = layout.Trim('-', ' '); } - public override TokenType Type - { - get { return TokenType.Layout; } - } + public override TokenType Type => TokenType.Layout; - public override string Value - { - get { return _layout; } - } + public override string Value => _layout; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index b8baa8c7..83703da1 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -9,17 +9,18 @@ internal class BindingContext { private readonly List _valueProviders = new List(); - public BindingContext(object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : - this(value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } + public BindingContext(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : + this(configuration, 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) { } + private BindingContext(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) : + this(configuration, 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(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) { RegisterValueProvider(new BindingContextValueProvider(this)); TemplatePath = parent != null ? (parent.TemplatePath ?? templatePath) : templatePath; + Configuration = configuration; TextWriter = writer; Value = value; ParentContext = parent; @@ -59,18 +60,20 @@ public BindingContext(object value, EncodedTextWriter writer, BindingContext par public string TemplatePath { get; } + public HandlebarsConfiguration Configuration { get; } + public EncodedTextWriter TextWriter { get; } public IDictionary> InlinePartialTemplates { get; } public Action PartialBlockTemplate { get; } - + public bool SuppressEncoding { get => TextWriter.SuppressEncoding; set => TextWriter.SuppressEncoding = value; } - + public virtual object Value { get; } public virtual BindingContext ParentContext { get; } @@ -101,7 +104,7 @@ public virtual object GetVariable(string variableName) for (var index = _valueProviders.Count - 1; index >= 0; index--) { var valueProvider = _valueProviders[index]; - if(!valueProvider.ProvidesNonContextVariables) continue; + if(!valueProvider.SupportedValueTypes.HasFlag(ValueTypes.All)) continue; if (valueProvider.TryGetValue(variableName, out var value)) return value; } @@ -139,7 +142,7 @@ private static IDictionary GetContextDictionary(object target) public virtual BindingContext CreateChildContext(object value, Action partialBlockTemplate) { - return new BindingContext(value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); + return new BindingContext(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); } } } diff --git a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs index d3725c99..71e68199 100644 --- a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs @@ -11,7 +11,7 @@ public BindingContextValueProvider(BindingContext context) _context = context; } - public bool ProvidesNonContextVariables { get; } = false; + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; public bool TryGetValue(string memberName, out object value) { diff --git a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs index da158ad2..40dccdc4 100644 --- a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs @@ -7,10 +7,6 @@ namespace HandlebarsDotNet.Compiler { internal class BlockHelperExpression : HelperExpression { - private readonly Expression _body; - private readonly Expression _inversion; - private readonly BlockParamsExpression _blockParams; - public BlockHelperExpression( string helperName, IEnumerable arguments, @@ -19,8 +15,6 @@ public BlockHelperExpression( bool isRaw = false) : this(helperName, arguments, BlockParamsExpression.Empty(), body, inversion, isRaw) { - _body = body; - _inversion = inversion; } public BlockHelperExpression( @@ -32,30 +26,18 @@ public BlockHelperExpression( bool isRaw = false) : base(helperName, arguments, isRaw) { - _body = body; - _inversion = inversion; - _blockParams = blockParams; + Body = body; + Inversion = inversion; + BlockParams = blockParams; } - public Expression Body - { - get { return _body; } - } + public Expression Body { get; } - public Expression Inversion - { - get { return _inversion; } - } + public Expression Inversion { get; } - public BlockParamsExpression BlockParams - { - get { return _blockParams; } - } + public BlockParamsExpression BlockParams { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.BlockExpression; } - } + public override ExpressionType NodeType => (ExpressionType) HandlebarsExpressionType.BlockExpression; } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs index 1d19da85..68f18d91 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -32,7 +32,7 @@ public BlockParamsValueProvider(BindingContext context, BlockParam @params) context.RegisterValueProvider(this); } - public bool ProvidesNonContextVariables { get; } = true; + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context | ValueTypes.All; /// /// Configures behavior of BlockParams. diff --git a/source/Handlebars/Compiler/Structure/BoolishExpression.cs b/source/Handlebars/Compiler/Structure/BoolishExpression.cs index 816e6b6c..7c3489ad 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 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..8d41068f 100644 --- a/source/Handlebars/Compiler/Structure/CommentExpression.cs +++ b/source/Handlebars/Compiler/Structure/CommentExpression.cs @@ -5,21 +5,15 @@ 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; + + public override Type Type => typeof (void); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs b/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs index dfaf6d17..e8a22926 100644 --- a/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs +++ b/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs @@ -15,21 +15,15 @@ public DeferredSectionExpression( Inversion = inversion; } - public BlockExpression Body { get; private set; } + public BlockExpression Body { get; } - public BlockExpression Inversion { get; private set; } + public BlockExpression Inversion { get; } - public PathExpression Path { get; private set; } + public PathExpression Path { get; } - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => typeof(void); - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.DeferredSection; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.DeferredSection; } } diff --git a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs index 701da448..8e6e6eda 100644 --- a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs +++ b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs @@ -5,22 +5,16 @@ namespace HandlebarsDotNet.Compiler { internal class HashParameterAssignmentExpression : HandlebarsExpression { - public string Name { get; set; } - public HashParameterAssignmentExpression(string name) { Name = name; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.HashParameterAssignmentExpression; } - } + public string Name { get; } + + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HashParameterAssignmentExpression; - public override Type Type - { - get { return typeof(object); } - } + public override Type Type => typeof(object); } } diff --git a/source/Handlebars/Compiler/Structure/HashParametersExpression.cs b/source/Handlebars/Compiler/Structure/HashParametersExpression.cs index e4784148..243ec0fe 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(object); } } diff --git a/source/Handlebars/Compiler/Structure/HelperExpression.cs b/source/Handlebars/Compiler/Structure/HelperExpression.cs index 5ecf0f42..05815796 100644 --- a/source/Handlebars/Compiler/Structure/HelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/HelperExpression.cs @@ -7,47 +7,28 @@ 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) { - _arguments = arguments; + Arguments = arguments; } public HelperExpression(string helperName, bool isRaw = false) { - _helperName = helperName; - _isRaw = isRaw; - _arguments = Enumerable.Empty(); + HelperName = helperName; + IsRaw = isRaw; + Arguments = Enumerable.Empty(); } - 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 IEnumerable Arguments { get; } } } diff --git a/source/Handlebars/Compiler/Structure/IValueProvider.cs b/source/Handlebars/Compiler/Structure/IValueProvider.cs index f2bd176e..5e5062ce 100644 --- a/source/Handlebars/Compiler/Structure/IValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/IValueProvider.cs @@ -1,8 +1,17 @@ +using System; + namespace HandlebarsDotNet.Compiler { + [Flags] + internal enum ValueTypes + { + Context = 1, + All = 2 + } + internal interface IValueProvider { - bool ProvidesNonContextVariables { get; } + ValueTypes SupportedValueTypes { get; } bool TryGetValue(string memberName, out object value); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index 60c77b25..e914d428 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -7,13 +7,8 @@ namespace HandlebarsDotNet.Compiler { 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()) + : this(sequence, template, Empty()) { } @@ -26,35 +21,20 @@ public IteratorExpression(Expression sequence, Expression template, Expression i public IteratorExpression(Expression sequence, BlockParamsExpression blockParams, Expression template, Expression ifEmpty) :base("each", Enumerable.Empty(), blockParams, template, ifEmpty, false) { - _sequence = sequence; - _template = template; - _ifEmpty = ifEmpty; + Sequence = sequence; + Template = template; + IfEmpty = ifEmpty; } - public Expression Sequence - { - get { return _sequence; } - } + public Expression Sequence { get; } - public Expression Template - { - get { return _template; } - } + public Expression Template { get; } - public Expression IfEmpty - { - get { return _ifEmpty; } - } + public Expression IfEmpty { get; } - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => 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..8b855a2a 100644 --- a/source/Handlebars/Compiler/Structure/PartialExpression.cs +++ b/source/Handlebars/Compiler/Structure/PartialExpression.cs @@ -6,41 +6,22 @@ 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; + PartialName = partialName; + Argument = argument; + Fallback = fallback; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.PartialExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PartialExpression; - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => typeof(void); - 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/PathExpression.cs b/source/Handlebars/Compiler/Structure/PathExpression.cs index af760d72..537db589 100644 --- a/source/Handlebars/Compiler/Structure/PathExpression.cs +++ b/source/Handlebars/Compiler/Structure/PathExpression.cs @@ -5,27 +5,19 @@ namespace HandlebarsDotNet.Compiler { internal class PathExpression : HandlebarsExpression { - private readonly string _path; - public PathExpression(string path) { - _path = path; + Path = path; + PathInfo = PathResolver.GetPathInfo(path); } - public string Path - { - get { return _path; } - } + public string Path { get; } + + public PathInfo PathInfo { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.PathExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PathExpression; - public override Type Type - { - get { return typeof(object); } - } + public override Type Type => typeof(object); } } diff --git a/source/Handlebars/Compiler/Structure/PathResolver.cs b/source/Handlebars/Compiler/Structure/PathResolver.cs index f2b93058..5f18309c 100644 --- a/source/Handlebars/Compiler/Structure/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/PathResolver.cs @@ -2,46 +2,123 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Dynamic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Text.RegularExpressions; namespace HandlebarsDotNet.Compiler { - internal class PathResolver + [DebuggerDisplay("{Path}")] + internal class PathInfo { - private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); + public bool HasValue { get; set; } + public string Path { get; set; } + public bool IsVariable { get; set; } + public IList Segments { get; set; } = new List(); + } + + [DebuggerDisplay("{Segment}")] + internal class PathSegment + { + public string Segment { get; } + + public PathSegment(string segment, IEnumerable chain) + { + Segment = segment; + PathChain = chain.ToArray(); + } - private readonly HandlebarsConfiguration _configuration; + public bool IsSwitch { get; set; } - public PathResolver(HandlebarsConfiguration configuration) + public ChainSegment[] PathChain { get; } + } + + [DebuggerDisplay("{Value}")] + internal class ChainSegment + { + public ChainSegment(string value) { - _configuration = configuration; + Value = value; + IsVariable = value.StartsWith("@"); + IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); + TrimmedValue = TrimSquareBrackets(value); } + + public string Value { get; } + public string TrimmedValue { get; } + public bool IsVariable { get; } + public bool IsThis { get; } - //TODO: make path resolution logic smarter - public object ResolvePath(BindingContext context, string path) + private 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; + } + } + + internal class PathResolver + { + private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); + + public static PathInfo GetPathInfo(string path) { if (path == "null") - return null; + return new PathInfo(); - var containsVariable = path.StartsWith("@"); - if (containsVariable) + var pathInfo = new PathInfo + { + HasValue = true, + Path = path, + IsVariable = path.StartsWith("@") + }; + + if (pathInfo.IsVariable) { path = path.Substring(1); - if (path.Contains("..")) + } + + foreach (var segment in path.Split('/')) + { + if (segment == "..") { - context = context.ParentContext; + pathInfo.Segments.Add(new PathSegment(segment, Enumerable.Empty()) + { + IsSwitch = true + }); + continue; } + + var segmentString = pathInfo.IsVariable ? "@" + segment : segment; + pathInfo.Segments.Add(new PathSegment(segmentString, GetPathChain(segmentString))); + } + return pathInfo; + } + + + //TODO: make path resolution logic smarter + public object ResolvePath(BindingContext context, PathInfo pathInfo) + { + if (!pathInfo.HasValue) + return null; + + var configuration = context.Configuration; + var containsVariable = pathInfo.IsVariable; var instance = context.Value; var hashParameters = instance as HashParameterDictionary; - foreach (var segment in path.Split('/')) + foreach (var segment in pathInfo.Segments) { - if (segment == "..") + if (segment.IsSwitch) { context = context.ParentContext; if (context == null) @@ -54,26 +131,25 @@ public object ResolvePath(BindingContext context, string path) } else { - var segmentString = containsVariable ? "@" + segment : segment; - foreach (var memberName in GetPathChain(segmentString)) + foreach (var chainSegment in segment.PathChain) { - instance = ResolveValue(context, instance, memberName); + instance = ResolveValue(context, instance, chainSegment); if (!(instance is UndefinedBindingResult)) continue; - if (hashParameters == null || hashParameters.ContainsKey(memberName) || context.ParentContext == null) + if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || context.ParentContext == null) { - if (_configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(path, (instance as UndefinedBindingResult).Value); + if (configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo.Path, (instance as UndefinedBindingResult).Value); return instance; } - instance = ResolveValue(context.ParentContext, context.ParentContext.Value, memberName); + instance = ResolveValue(context.ParentContext, context.ParentContext.Value, chainSegment); if (!(instance is UndefinedBindingResult result)) continue; - if (_configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(path, result.Value); + if (configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo.Path, result.Value); return result; } } @@ -81,11 +157,11 @@ public object ResolvePath(BindingContext context, string path) return instance; } - private static IEnumerable GetPathChain(string segmentString) + private static IEnumerable GetPathChain(string segmentString) { var insideEscapeBlock = false; var pathChain = segmentString.Split('.') - .Aggregate(new List(), (list, next) => + .Aggregate(new List(), (list, next) => { if (insideEscapeBlock) { @@ -94,7 +170,7 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list[list.Count - 1] = list[list.Count - 1] + "." + next; + list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].Value}.{next}"); return list; } @@ -108,32 +184,34 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list.Add(next); + list.Add(new ChainSegment(next)); return list; }); return pathChain; } - private object ResolveValue(BindingContext context, object instance, string segment) + private object ResolveValue(BindingContext context, object instance, ChainSegment chainSegment) { - var undefined = new UndefinedBindingResult(segment, _configuration); + var configuration = context.Configuration; + var segment = chainSegment.Value; + object undefined = new UndefinedBindingResult(segment, configuration); object resolvedValue = undefined; - if (segment.StartsWith("@")) + if (chainSegment.IsVariable) { - var contextValue = context.GetContextVariable(segment.Substring(1)); + var contextValue = context.GetContextVariable(segment); if (contextValue != null) { resolvedValue = contextValue; } } - else if (segment == "this" || segment == string.Empty) + else if (chainSegment.IsThis) { resolvedValue = instance; } else { - if (!TryAccessMember(instance, segment, out resolvedValue)) + if (!TryAccessMember(instance, chainSegment, configuration, out resolvedValue)) { resolvedValue = context.GetVariable(segment) ?? undefined; } @@ -141,16 +219,19 @@ private object ResolveValue(BindingContext context, object instance, string segm return resolvedValue; } - public bool TryAccessMember(object instance, string memberName, out object value) + public bool TryAccessMember(object instance, ChainSegment chainSegment, HandlebarsConfiguration configuration, out object value) { - value = new UndefinedBindingResult(memberName, _configuration); + var memberName = chainSegment.Value; + value = new UndefinedBindingResult(memberName, configuration); if (instance == null) return false; var instanceType = instance.GetType(); - memberName = ResolveMemberName(instance, memberName); - memberName = TrimSquareBrackets(memberName); - + memberName = ResolveMemberName(instance, memberName, configuration); + memberName = ReferenceEquals(memberName, chainSegment.Value) + ? chainSegment.TrimmedValue + : TrimSquareBrackets(memberName); + return TryAccessContextMember(instance, memberName, out value) || TryAccessStringIndexerMember(instance, memberName, instanceType, out value) || TryAccessIEnumerableMember(instance, memberName, out value) @@ -201,20 +282,15 @@ private static bool TryAccessIEnumerableMember(object instance, string memberNam private static bool TryAccessMemberWithReflection(object instance, string memberName, Type instanceType, out object value) { - switch (GetMember(memberName, instanceType)) + var typeDescriptor = TypeDescriptors.Provider.GetObjectTypeDescriptor(instanceType); + if(typeDescriptor.Accessors.TryGetValue(memberName, out var accessor)) { - case PropertyInfo propertyInfo: - value = propertyInfo.GetValue(instance, null); - return true; - - case FieldInfo fieldInfo: - value = fieldInfo.GetValue(instance); - return true; - - default: - value = null; - return false; + value = accessor(instance); + return true; } + + value = null; + return false; } private static bool TryAccessIDictionaryMember(object instance, string memberName, out object value) @@ -333,9 +409,9 @@ private static object GetProperty(object target, string name) return site.Target(site, target); } - private string ResolveMemberName(object instance, string memberName) + private static string ResolveMemberName(object instance, string memberName, HandlebarsConfiguration configuration) { - var resolver = _configuration.ExpressionNameResolver; + var resolver = configuration.ExpressionNameResolver; return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; } } diff --git a/source/Handlebars/Compiler/Structure/StatementExpression.cs b/source/Handlebars/Compiler/Structure/StatementExpression.cs index c95d8af2..04a6b0b0 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 => typeof(void); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/StaticExpression.cs b/source/Handlebars/Compiler/Structure/StaticExpression.cs index e9478f79..e4a9a3c5 100644 --- a/source/Handlebars/Compiler/Structure/StaticExpression.cs +++ b/source/Handlebars/Compiler/Structure/StaticExpression.cs @@ -5,27 +5,16 @@ namespace HandlebarsDotNet.Compiler { internal class StaticExpression : HandlebarsExpression { - private readonly string _value; - public StaticExpression(string value) { - _value = value; + Value = value; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.StaticExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.StaticExpression; - public override Type Type - { - get { return typeof(void); } - } + public override Type Type => typeof(void); - public string Value - { - get { return _value; } - } + public string Value { get; } } } diff --git a/source/Handlebars/Compiler/Structure/SubExpressionExpression.cs b/source/Handlebars/Compiler/Structure/SubExpressionExpression.cs index 7a01b596..cb2b1a77 100644 --- a/source/Handlebars/Compiler/Structure/SubExpressionExpression.cs +++ b/source/Handlebars/Compiler/Structure/SubExpressionExpression.cs @@ -6,27 +6,16 @@ namespace HandlebarsDotNet { internal class SubExpressionExpression : HandlebarsExpression { - private readonly Expression _expression; - public SubExpressionExpression(Expression expression) { - _expression = expression; + Expression = expression; } - public override Type Type - { - get { return typeof(object); } - } + public override Type Type => typeof(object); - public Expression Expression - { - get { return _expression; } - } + public Expression Expression { get; } - public override ExpressionType NodeType - { - get { return (ExpressionType)HandlebarsExpressionType.SubExpression; } - } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.SubExpression; } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index bb1be263..9ed620e8 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -25,25 +25,25 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b var fb = new FunctionBuilder(CompilationContext.Configuration); - var context = E.Arg(CompilationContext.BindingContext); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var bindingContext = isInlinePartial ? context.Cast() : context.Property(o => o.Value); - var blockParamsExpression = E.New( - () => new BlockParamsValueProvider(context, E.Arg(bhex.BlockParams)) + var blockParamsExpression = ExpressionShortcuts.New( + () => new BlockParamsValueProvider(context, ExpressionShortcuts.Arg(bhex.BlockParams)) ); var body = fb.Compile(((BlockExpression) bhex.Body).Expressions, context); var inversion = fb.Compile(((BlockExpression) bhex.Inversion).Expressions, context); var helper = CompilationContext.Configuration.BlockHelpers[bhex.HelperName.Replace("#", "")]; - var helperOptions = E.New( - () => new HelperOptions(E.Arg(body), E.Arg(inversion), blockParamsExpression) + var helperOptions = ExpressionShortcuts.New( + () => new HelperOptions(ExpressionShortcuts.Arg(body), ExpressionShortcuts.Arg(inversion), blockParamsExpression) ); - return E.Call( - () => helper(context.Property(o => o.TextWriter), helperOptions, bindingContext, E.Array(bhex.Arguments)) + return ExpressionShortcuts.Call( + () => helper(context.Property(o => o.TextWriter), helperOptions, bindingContext, ExpressionShortcuts.Array(bhex.Arguments)) ); } } diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs index 2439a473..d5ba2ae1 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -16,7 +16,7 @@ private BoolishConverter(CompilationContext context) protected override Expression VisitBoolishExpression(BoolishExpression bex) { - return E.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(Visit(bex.Condition))); + return ExpressionShortcuts.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(Visit(bex.Condition))); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs index 387c2ed2..3b805ed2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -14,17 +14,20 @@ private ContextBinder() public static Expression> Bind(Expression body, CompilationContext context, Expression parentContext, string templatePath) { - var writerParameter = E.Parameter("buffer"); - var objectParameter = E.Parameter("data"); + var configuration = context.Configuration; - var bindingContext = E.Arg(context.BindingContext); - var inlinePartialsParameter = E.Null>>(); - var encodedWriterExpression = E.Call(() => EncodedTextWriter.From(E.Arg(writerParameter), context.Configuration.TextEncoder)); - var newBindingContext = E.New( - () => new BindingContext(objectParameter, encodedWriterExpression, E.Arg(parentContext), templatePath, (IDictionary>) inlinePartialsParameter) + var writerParameter = ExpressionShortcuts.Parameter("buffer"); + var objectParameter = ExpressionShortcuts.Parameter("data"); + + var bindingContext = ExpressionShortcuts.Arg(context.BindingContext); + var inlinePartialsParameter = ExpressionShortcuts.Null>>(); + var encodedWriterExpression = ExpressionShortcuts.Call(() => EncodedTextWriter.From(ExpressionShortcuts.Arg(writerParameter), context.Configuration.TextEncoder)); + var newBindingContext = ExpressionShortcuts.New( + () => new BindingContext(configuration, objectParameter, encodedWriterExpression, ExpressionShortcuts.Arg(parentContext), templatePath, (IDictionary>) inlinePartialsParameter) ); - var blockBuilder = E.Block().Parameter(bindingContext) + var blockBuilder = ExpressionShortcuts.Block() + .Parameter(bindingContext) .Line(bindingContext.TernaryAssign(objectParameter.Is(), objectParameter.As(), newBindingContext)) .Lines(((BlockExpression) body).Expressions); diff --git a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs index ba806f79..ada0749f 100644 --- a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs @@ -21,20 +21,17 @@ private DeferredSectionVisitor(CompilationContext 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)); + var templates = GetDeferredSectionTemplates(dsex); + + var path = ExpressionShortcuts.Arg(HandlebarsExpression.Path(dsex.Path.Path.Substring(1))); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var body = ExpressionShortcuts.Arg(templates[0]); + var inversion = ExpressionShortcuts.Arg(templates[1]); + return ExpressionShortcuts.Call(() => RenderSection(path, context, body, inversion)); } - private Expression[] GetDeferredSectionTemplates(DeferredSectionExpression dsex) + private Expression>[] GetDeferredSectionTemplates(DeferredSectionExpression dsex) { var fb = new FunctionBuilder(CompilationContext.Configuration); var body = fb.Compile(dsex.Body.Expressions, CompilationContext.BindingContext); diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs index 77a89a0c..cdf62f46 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs @@ -18,21 +18,21 @@ private HashParameterBinder(CompilationContext context) } protected override Expression VisitHashParametersExpression(HashParametersExpression hpex) - { - var addMethod = typeof(HashParameterDictionary).GetMethod("Add", new[] { typeof(string), typeof(object) }); - - var elementInits = new List(); - - foreach (var parameter in hpex.Parameters) - { - elementInits.Add(Expression.ElementInit( - addMethod, - Expression.Constant(parameter.Key), - Visit(parameter.Value))); - } - - return Expression.ListInit( - Expression.New(typeof(HashParameterDictionary).GetConstructor(new Type[0])), + { + var addMethod = typeof(HashParameterDictionary).GetMethod("Add", new[] { typeof(string), typeof(object) }); + + var elementInits = new List(); + + foreach (var parameter in hpex.Parameters) + { + elementInits.Add(Expression.ElementInit( + addMethod, + Expression.Constant(parameter.Key), + Visit(parameter.Value))); + } + + return Expression.ListInit( + Expression.New(typeof(HashParameterDictionary).GetConstructor(new Type[0])), elementInits); } } diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 3c29a2b4..98c7413b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -23,24 +23,26 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitHelperExpression(HelperExpression hex) { - var bindingContext = E.Arg(CompilationContext.BindingContext); + var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var textWriter = bindingContext.Property(o => o.TextWriter); - var args = E.Array(hex.Arguments.Select(Visit)); + var args = ExpressionShortcuts.Array(hex.Arguments.Select(Visit)); if (CompilationContext.Configuration.Helpers.TryGetValue(hex.HelperName, out var helper)) { - return E.Call(() => helper(textWriter, bindingContext, args)); + return ExpressionShortcuts.Call(() => helper(textWriter, bindingContext, args)); } if (CompilationContext.Configuration.ReturnHelpers.TryGetValue(hex.HelperName, out var returnHelper)) { - return E.Call(() => CaptureResult(textWriter, - E.Call(() => returnHelper(bindingContext, args))) + return ExpressionShortcuts.Call(() => + CaptureResult(textWriter, ExpressionShortcuts.Call(() => returnHelper(bindingContext, args))) ); } - return E.Call(() => CaptureResult(bindingContext.Property(o => o.TextWriter), - E.Call(() => LateBindHelperExpression(bindingContext, hex.HelperName, args))) + return ExpressionShortcuts.Call(() => + CaptureResult(textWriter, ExpressionShortcuts.Call(() => + LateBindHelperExpression(bindingContext, hex.HelperName, args)) + ) ); } @@ -51,7 +53,7 @@ private object LateBindHelperExpression( { if (CompilationContext.Configuration.Helpers.TryGetValue(helperName, out var helper)) { - using (var write = new StringWriter()) + using (var write = new PolledStringWriter()) { helper(write, context.Value, arguments.ToArray()); return write.ToString(); diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index c8ecc7d2..5b232aa2 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.IO; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Dynamic; @@ -10,6 +11,138 @@ namespace HandlebarsDotNet.Compiler { + internal class TypeDescriptors + { + private static readonly Lazy Instance = new Lazy(() => new TypeDescriptors()); + private readonly ConcurrentDictionary> _descriptors = new ConcurrentDictionary>(); + + public static TypeDescriptors Provider => Instance.Value; + + private TypeDescriptors(){} + + public bool TryGetGenericDictionaryTypeDescriptor(Type type, out DictionaryTypeDescriptor typeDescriptor) + { + var descriptors = _descriptors.GetOrAdd(type, t => new ConcurrentDictionary()); + typeDescriptor = (DictionaryTypeDescriptor) descriptors.GetOrAdd(nameof(TryGetGenericDictionaryTypeDescriptor), name => + { + var typeDefinition = type.GetInterfaces() + .Where(i => i.GetTypeInfo().IsGenericType) + .FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + + return typeDefinition == null + ? null + : new DictionaryTypeDescriptor(typeDefinition); + }); + + return typeDescriptor != null; + } + + public ObjectTypeDescriptor GetObjectTypeDescriptor(Type type) + { + var descriptors = _descriptors.GetOrAdd(type, t => new ConcurrentDictionary()); + return (ObjectTypeDescriptor) descriptors.GetOrAdd(nameof(GetObjectTypeDescriptor), name => new ObjectTypeDescriptor(type)); + } + + public class DictionaryTypeDescriptor : TypeDescriptor + { + public DictionaryTypeDescriptor(Type type) : base(type) + { + DictionaryAccessor = typeof(DictionaryAccessor<,>).MakeGenericType(type.GenericTypeArguments); + } + + public Type DictionaryAccessor { get; } + } + + public class ObjectTypeDescriptor : TypeDescriptor + { + private Dictionary> _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public ObjectTypeDescriptor(Type type) : base(type) + { + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + + Members = properties.OfType().Concat(fields).ToArray(); + + foreach (var member in fields) + { + _accessors[member.Name] = o => member.GetValue(o); + } + + foreach (var member in properties) + { + _accessors[member.Name] = GetValueGetter(member); + } + } + + public MemberInfo[] Members { get; } + + public IReadOnlyDictionary> Accessors => _accessors; + + private static Func GetValueGetter(PropertyInfo propertyInfo) + { + var instance = Expression.Parameter(typeof(object), "i"); + var property = Expression.Property(Expression.Convert(instance, propertyInfo.DeclaringType), propertyInfo); + var convert = Expression.TypeAs(property, typeof(object)); + + return (Func)Expression.Lambda(convert, instance).Compile(); + } + } + + public abstract class TypeDescriptor + { + public Type Type { get; } + + public TypeDescriptor(Type type) + { + Type = type; + } + } + } + + internal class DictionaryAccessor : IReadOnlyDictionary + { + private readonly IDictionary _wrapped; + + public DictionaryAccessor(IDictionary wrapped) + { + _wrapped = wrapped; + } + + public int Count => _wrapped.Count; + public bool ContainsKey(object key) + { + return _wrapped.ContainsKey((TKey) key); + } + + public bool TryGetValue(object key, out object value) + { + if(_wrapped.TryGetValue((TKey) key, out var inner)) + { + value = inner; + return true; + } + + value = null; + return false; + } + + public object this[object key] => _wrapped[(TKey) key]; + + public IEnumerable Keys => _wrapped.Keys.Cast(); + public IEnumerable Values => _wrapped.Values.Cast(); + + public IEnumerator> GetEnumerator() + { + return _wrapped.Select(value => new KeyValuePair(value.Key, value.Value)).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + internal class IteratorBinder : HandlebarsExpressionVisitor { public static Expression Bind(Expression expr, CompilationContext context) @@ -26,35 +159,40 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) { var fb = new FunctionBuilder(CompilationContext.Configuration); - var iteratorBindingContext = E.Var("context"); - var blockParamsValueBinder = E.Var("blockParams"); - var sequence = E.Var("sequence"); + var iteratorBindingContext = ExpressionShortcuts.Var("context"); + var blockParamsValueBinder = ExpressionShortcuts.Var("blockParams"); + var sequence = ExpressionShortcuts.Var("sequence"); - var template = E.Arg(fb.Compile(new[] {iex.Template}, iteratorBindingContext)); - var ifEmpty = E.Arg(fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext)); + var template = ExpressionShortcuts.Arg(fb.Compile(new[] {iex.Template}, iteratorBindingContext)); + var ifEmpty = ExpressionShortcuts.Arg(fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext)); var context = CompilationContext.BindingContext; - var compiledSequence = fb.Reduce(iex.Sequence, CompilationContext); - var blockParamsProvider = E.New(() => new BlockParamsValueProvider(iteratorBindingContext, E.Arg(iex.BlockParams))); + var compiledSequence = FunctionBuilder.Reduce(iex.Sequence, CompilationContext); + var blockParamsProvider = ExpressionShortcuts.New(() => new BlockParamsValueProvider(iteratorBindingContext, ExpressionShortcuts.Arg(iex.BlockParams))); - return E.Block() + return ExpressionShortcuts.Block() .Parameter(iteratorBindingContext, context) .Parameter(blockParamsValueBinder, blockParamsProvider) .Parameter(sequence, compiledSequence) .Line(Expression.IfThenElse( sequence.Is(), Expression.IfThenElse( - E.Call(() => IsGenericDictionary(sequence)), + ExpressionShortcuts.Call(() => IsGenericDictionary(sequence)), GetDictionaryIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), Expression.IfThenElse( - E.Call(() => IsNonListDynamic(sequence)), + ExpressionShortcuts.Call(() => IsNonListDynamic(sequence)), GetDynamicIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), - GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty))), + Expression.IfThenElse(ExpressionShortcuts.Call(() => IsList(sequence)), + GetListIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), + GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty) + ) + ) + ), GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty))); } - private Expression GetEnumerableIterator( + private static Expression GetEnumerableIterator( ExpressionContainer contextParameter, ExpressionContainer blockParamsParameter, ExpressionContainer sequence, @@ -62,12 +200,25 @@ private Expression GetEnumerableIterator( ExpressionContainer> ifEmpty ) { - return E.Call( + return ExpressionShortcuts.Call( () => IterateEnumerable(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) ); } + + private static Expression GetListIterator( + ExpressionContainer contextParameter, + ExpressionContainer blockParamsParameter, + ExpressionContainer sequence, + ExpressionContainer> template, + ExpressionContainer> ifEmpty + ) + { + return ExpressionShortcuts.Call( + () => IterateList(contextParameter, blockParamsParameter, (IList) sequence, template, ifEmpty) + ); + } - private Expression GetObjectIterator( + private static Expression GetObjectIterator( ExpressionContainer contextParameter, ExpressionContainer blockParamsParameter, ExpressionContainer sequence, @@ -75,12 +226,12 @@ private Expression GetObjectIterator( ExpressionContainer> ifEmpty ) { - return E.Call( + return ExpressionShortcuts.Call( () => IterateObject(contextParameter, blockParamsParameter, sequence, template, ifEmpty) ); } - private Expression GetDictionaryIterator( + private static Expression GetDictionaryIterator( ExpressionContainer contextParameter, ExpressionContainer blockParamsParameter, ExpressionContainer sequence, @@ -88,23 +239,28 @@ private Expression GetDictionaryIterator( ExpressionContainer> ifEmpty ) { - return E.Call( + return ExpressionShortcuts.Call( () => IterateDictionary(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) ); } - private Expression GetDynamicIterator( + private static Expression GetDynamicIterator( ExpressionContainer contextParameter, ExpressionContainer blockParamsParameter, ExpressionContainer sequence, ExpressionContainer> template, ExpressionContainer> ifEmpty) { - return E.Call( + return ExpressionShortcuts.Call( () => IterateDynamic(contextParameter, blockParamsParameter, (IDynamicMetaObjectProvider) sequence, template, ifEmpty) ); } + private static bool IsList(object target) + { + return target is IList; + } + private static bool IsNonListDynamic(object target) { if (target is IDynamicMetaObjectProvider metaObjectProvider) @@ -117,11 +273,7 @@ private static bool IsNonListDynamic(object target) private static bool IsGenericDictionary(object target) { - return - target.GetType() - .GetInterfaces() - .Where(i => i.GetTypeInfo().IsGenericType) - .Any(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + return TypeDescriptors.Provider.TryGetGenericDictionaryTypeDescriptor(target.GetType(), out _); } private static void IterateObject( @@ -142,19 +294,18 @@ private static void IterateObject( }); objectEnumerator.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 typeDescriptor = TypeDescriptors.Provider.GetObjectTypeDescriptor(target.GetType()); + var accessorsCount = typeDescriptor.Accessors.Count; + foreach (var accessor in typeDescriptor.Accessors) { - var member = enumerableValue.Value; - objectEnumerator.Key = member.Name; - objectEnumerator.Value = AccessMember(target, member); - objectEnumerator.First = enumerableValue.IsFirst; - objectEnumerator.Last = enumerableValue.IsLast; - objectEnumerator.Index = enumerableValue.Index; + objectEnumerator.Key = accessor.Key; + objectEnumerator.Value = accessor.Value.Invoke(target); + objectEnumerator.First = objectEnumerator.Index == 0; + objectEnumerator.Last = objectEnumerator.Index == accessorsCount - 1; template(context.TextWriter, objectEnumerator.Value); + + objectEnumerator.Index++; } if (objectEnumerator.Index == 0) @@ -177,6 +328,9 @@ private static void IterateDictionary( { if (HandlebarsUtils.IsTruthy(target)) { + TypeDescriptors.Provider.TryGetGenericDictionaryTypeDescriptor(target.GetType(), out var typeDescriptor); + var accessor = (IReadOnlyDictionary) Activator.CreateInstance(typeDescriptor.DictionaryAccessor, target); + var objectEnumerator = new ObjectEnumeratorValueProvider(); context.RegisterValueProvider(objectEnumerator); blockParamsValueProvider.Configure((parameters, binder) => @@ -186,26 +340,19 @@ private static void IterateDictionary( }); objectEnumerator.Index = 0; - var targetType = target.GetType(); - var keysProperty = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(o => o.Name.EndsWith("Keys")); - if (keysProperty?.GetValue(target) is IEnumerable keys) + var keys = accessor.Keys; + foreach (var enumerableValue in new ExtendedEnumerable(keys)) { - var getItemMethodInfo = targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => o.Name == "get_Item"); - - var parameters = new object[1]; - foreach (var enumerableValue in new ExtendedEnumerable(keys)) - { - var key = parameters[0] = enumerableValue.Value; - objectEnumerator.Key = key.ToString(); - objectEnumerator.Value = getItemMethodInfo.Invoke(target, parameters); - objectEnumerator.First = enumerableValue.IsFirst; - objectEnumerator.Last = enumerableValue.IsLast; - objectEnumerator.Index = enumerableValue.Index; - - template(context.TextWriter, objectEnumerator.Value); - } + var key = enumerableValue.Value; + objectEnumerator.Key = key.ToString(); + objectEnumerator.Value = accessor[key]; + objectEnumerator.First = enumerableValue.IsFirst; + objectEnumerator.Last = enumerableValue.IsLast; + objectEnumerator.Index = enumerableValue.Index; + + template(context.TextWriter, objectEnumerator.Value); } + if (objectEnumerator.Index == 0) { ifEmpty(context.TextWriter, context.Value); @@ -290,6 +437,39 @@ private static void IterateEnumerable( ifEmpty(context.TextWriter, context.Value); } } + + private static void IterateList( + BindingContext context, + BlockParamsValueProvider blockParamsValueProvider, + IList sequence, + Action template, + Action ifEmpty) + { + var iterator = new IteratorValueProvider(); + context.RegisterValueProvider(iterator); + blockParamsValueProvider.Configure((parameters, binder) => + { + binder(parameters.ElementAtOrDefault(0), () => iterator.Value); + binder(parameters.ElementAtOrDefault(1), () => iterator.Index); + }); + + iterator.Index = 0; + var sequenceCount = sequence.Count; + for (var index = 0; index < sequenceCount; index++) + { + iterator.Value = sequence[index]; + iterator.First = index == 0; + iterator.Last = index == sequenceCount - 1; + iterator.Index = index; + + template(context.TextWriter, iterator.Value); + } + + if (iterator.Index == 0) + { + ifEmpty(context.TextWriter, context.Value); + } + } private static object GetProperty(object target, string name) { @@ -307,8 +487,8 @@ private class IteratorValueProvider : IValueProvider public bool First { get; set; } public bool Last { get; set; } - - public bool ProvidesNonContextVariables { get; } = false; + + public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; public virtual bool TryGetValue(string memberName, out object value) { diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 3e2173b2..7c327426 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -31,7 +31,7 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitPartialExpression(PartialExpression pex) { - var bindingContext = E.Arg(CompilationContext.BindingContext); + var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var fb = new FunctionBuilder(CompilationContext.Configuration); var partialBlockTemplate = pex.Fallback == null ? null : fb.Compile(new[] {pex.Fallback}, null, null); @@ -39,12 +39,12 @@ protected override Expression VisitPartialExpression(PartialExpression pex) if (pex.Argument != null || partialBlockTemplate != null) { bindingContext = bindingContext.Call(o => - o.CreateChildContext(E.Arg(pex.Argument), E.Arg(partialBlockTemplate)) + o.CreateChildContext(ExpressionShortcuts.Arg(pex.Argument), ExpressionShortcuts.Arg(partialBlockTemplate)) ); } - return E.Call(() => - InvokePartialWithFallback(E.Cast(pex.PartialName), bindingContext, CompilationContext.Configuration) + return ExpressionShortcuts.Call(() => + InvokePartialWithFallback(ExpressionShortcuts.Cast(pex.PartialName), bindingContext, CompilationContext.Configuration) ); } diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 4c80fef0..9b5296e9 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -4,8 +4,6 @@ namespace HandlebarsDotNet.Compiler { internal class PathBinder : HandlebarsExpressionVisitor { - private readonly PathResolver _pathResolver; - public static Expression Bind(Expression expr, CompilationContext context) { return new PathBinder(context).Visit(expr); @@ -14,23 +12,23 @@ public static Expression Bind(Expression expr, CompilationContext context) private PathBinder(CompilationContext context) : base(context) { - _pathResolver = new PathResolver(context.Configuration); } protected override Expression VisitStatementExpression(StatementExpression sex) { if (!(sex.Body is PathExpression)) return Visit(sex.Body); - var context = E.Arg(CompilationContext.BindingContext); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); return context.Property(o => o.TextWriter) - .Call(o => o.Write(E.Arg(Visit(sex.Body)))); + .Call(o => o.Write(ExpressionShortcuts.Arg(Visit(sex.Body)))); } protected override Expression VisitPathExpression(PathExpression pex) { - var context = E.Arg(CompilationContext.BindingContext); - return E.Call(() => _pathResolver.ResolvePath(context, pex.Path)); + var pathResolver = ExpressionShortcuts.New(); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + return pathResolver.Call(o => o.ResolvePath(context, pex.PathInfo)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs index a8a07ac8..132d555c 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -18,14 +18,9 @@ private StaticReplacer(CompilationContext context) 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 writer = context.Property(o => o.TextWriter); + return writer.Call(o => o.Write(stex.Value, false)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index 38b2023c..edf3aac2 100755 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -27,8 +27,7 @@ protected override Expression VisitSubExpression(SubExpressionExpression subex) return HandleMethodCallExpression(callExpression); default: - var fb = new FunctionBuilder(CompilationContext.Configuration); - var expression = fb.Reduce(subex.Expression, CompilationContext); + var expression = FunctionBuilder.Reduce(subex.Expression, CompilationContext); if (expression is MethodCallExpression lateBoundCall) return HandleMethodCallExpression(lateBoundCall); @@ -41,20 +40,24 @@ private Expression HandleMethodCallExpression(MethodCallExpression helperCall) if (helperCall.Type != typeof(void)) { return helperCall.Update(helperCall.Object, - E.ReplaceValuesOf(helperCall.Arguments, E.Null()).Select(Visit) + ExpressionShortcuts.ReplaceValuesOf(helperCall.Arguments, ExpressionShortcuts.Null()).Select(Visit) ); } - var writer = E.Var(); + var writer = ExpressionShortcuts.Var(); helperCall = helperCall.Update(helperCall.Object, - E.ReplaceValuesOf(helperCall.Arguments, writer).Select(Visit) + ExpressionShortcuts.ReplaceValuesOf(helperCall.Arguments, writer).Select(Visit) ); - - return E.Block() + + return ExpressionShortcuts.Block() .Parameter(writer) - .Line(Expression.Assign(writer, E.New())) - .Line(helperCall) - .Line(writer.Call(w => w.ToString())) + .Line(Expression.Assign(writer, ExpressionShortcuts.New())) + .Line(writer.Using(container => + { + var body = new BlockBody { helperCall }; + body.AddRange(container.Call(o => o.ToString()).Return()); + return body; + })) .Invoke(); } @@ -63,20 +66,24 @@ private Expression HandleInvocationExpression(InvocationExpression invocation) if (invocation.Type != typeof(void)) { return invocation.Update(invocation.Expression, - E.ReplaceValuesOf(invocation.Arguments, E.Null()).Select(Visit) + ExpressionShortcuts.ReplaceValuesOf(invocation.Arguments, ExpressionShortcuts.Null()).Select(Visit) ); } - var writer = E.Var(); + var writer = ExpressionShortcuts.Var(); invocation = invocation.Update(invocation.Expression, - E.ReplaceValuesOf(invocation.Arguments, writer).Select(Visit) + ExpressionShortcuts.ReplaceValuesOf(invocation.Arguments, writer).Select(Visit) ); - return E.Block() + return ExpressionShortcuts.Block() .Parameter(writer) - .Line(Expression.Assign(writer, E.New())) - .Line(invocation) - .Line(writer.Call(w => w.ToString())) + .Line(Expression.Assign(writer, ExpressionShortcuts.New())) + .Line(writer.Using(container => + { + var body = new BlockBody { invocation }; + body.AddRange(container.Call(o => o.ToString()).Return()); + return body; + })) .Invoke(); } } diff --git a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs index 9dd75438..afde8022 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -17,23 +17,19 @@ private UnencodedStatementVisitor(CompilationContext context) protected override Expression VisitStatementExpression(StatementExpression sex) { + var context = ExpressionShortcuts.Var(); + var suppressEncoding = context.Property(o => o.SuppressEncoding); if (sex.IsEscaped == false) { - 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; + return ExpressionShortcuts.Block(typeof(void)) + .Parameter(context, CompilationContext.BindingContext) + .Line(suppressEncoding.Assign(true)) + .Line(sex) + .Line(suppressEncoding.Assign(false)) + .Line(Expression.Empty()); } + + return sex; } } } diff --git a/source/Handlebars/EncodedTextWriter.cs b/source/Handlebars/EncodedTextWriter.cs index 40b67c9f..d9b8a7cf 100644 --- a/source/Handlebars/EncodedTextWriter.cs +++ b/source/Handlebars/EncodedTextWriter.cs @@ -1,8 +1,22 @@ -using System.IO; +using System; +using System.IO; using System.Text; namespace HandlebarsDotNet { + internal class PolledStringWriter : StringWriter + { + public PolledStringWriter() : base(StringBuilderPool.Shared.GetObject()) + { + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + StringBuilderPool.Shared.PutObject(base.GetStringBuilder()); + } + } + internal class EncodedTextWriter : TextWriter { private readonly TextWriter _underlyingWriter; diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 494e3ba0..7b582203 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -4,8 +4,8 @@ portable true net452;netstandard1.3;netstandard2.0 - 1.10.2 - 7 + 1.10.7 + latest diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index 5559cac8..edbece82 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -1,26 +1,70 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Text; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { + internal abstract class ObjectPool + { + private readonly ConcurrentQueue _objects; + + protected ObjectPool() + { + _objects = new ConcurrentQueue(); + } + + public T GetObject() + { + return _objects.TryDequeue(out var item) + ? item + : CreateObject(); + } + + public virtual void PutObject(T item) + { + _objects.Enqueue(item); + } + + protected abstract T CreateObject(); + } + + internal class StringBuilderPool : ObjectPool + { + private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); + + private readonly int _initialCapacity; + + public static StringBuilderPool Shared => Lazy.Value; + + public StringBuilderPool(int initialCapacity = 100) + { + _initialCapacity = initialCapacity; + } + + protected override StringBuilder CreateObject() + { + return new StringBuilder(_initialCapacity); + } + + public override void PutObject(StringBuilder item) + { + item.Length = 0; + base.PutObject(item); + } + } + public partial class Handlebars { private class HandlebarsEnvironment : IHandlebars { - private readonly HandlebarsConfiguration _configuration; private readonly HandlebarsCompiler _compiler; public HandlebarsEnvironment(HandlebarsConfiguration configuration) { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - _configuration = configuration; - _compiler = new HandlebarsCompiler(_configuration); + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _compiler = new HandlebarsCompiler(Configuration); RegisterBuiltinHelpers(); } @@ -29,22 +73,23 @@ public Func CompileView(string templatePath) var compiledView = _compiler.CompileView(templatePath); return (vm) => { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) + var sb = StringBuilderPool.Shared.GetObject(); + try + { + using (var tw = new StringWriter(sb)) + { + compiledView(tw, vm); + } + return sb.ToString(); + } + finally { - compiledView(tw, vm); + StringBuilderPool.Shared.PutObject(sb); } - return sb.ToString(); }; } - public HandlebarsConfiguration Configuration - { - get - { - return this._configuration; - } - } + public HandlebarsConfiguration Configuration { get; } public Action Compile(TextReader template) { @@ -58,21 +103,28 @@ public Func Compile(string template) var compiledTemplate = Compile(reader); return context => { - var builder = new StringBuilder(); - using (var writer = new StringWriter(builder)) + var builder = StringBuilderPool.Shared.GetObject(); + try + { + using (var writer = new StringWriter(builder)) + { + compiledTemplate(writer, context); + } + return builder.ToString(); + } + finally { - compiledTemplate(writer, context); + StringBuilderPool.Shared.PutObject(builder); } - return builder.ToString(); }; } } public void RegisterTemplate(string templateName, Action template) { - lock (_configuration) + lock (Configuration) { - _configuration.RegisteredTemplates.AddOrUpdate(templateName, template); + Configuration.RegisteredTemplates.AddOrUpdate(templateName, template); } } @@ -86,25 +138,25 @@ public void RegisterTemplate(string templateName, string template) public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) { - lock (_configuration) + lock (Configuration) { - _configuration.Helpers.AddOrUpdate(helperName, helperFunction); + Configuration.Helpers.AddOrUpdate(helperName, helperFunction); } } public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) { - lock (_configuration) + lock (Configuration) { - _configuration.ReturnHelpers.AddOrUpdate(helperName, helperFunction); + Configuration.ReturnHelpers.AddOrUpdate(helperName, helperFunction); } } public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { - lock (_configuration) + lock (Configuration) { - _configuration.BlockHelpers.AddOrUpdate(helperName, helperFunction); + Configuration.BlockHelpers.AddOrUpdate(helperName, helperFunction); } } diff --git a/source/Handlebars/HandlebarsUtils.cs b/source/Handlebars/HandlebarsUtils.cs index 40ba1f50..0bc995fe 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -11,40 +11,23 @@ public static bool IsTruthy(object value) { return !IsFalsy(value); } - - public static bool IsUndefinedBindingResult(object value) - { - return value is UndefinedBindingResult; - } public static bool IsFalsy(object value) { - if (value is UndefinedBindingResult) - { - return true; - } - if (value == null) + switch (value) { - return true; - } - else if (value is bool) - { - return !(bool)value; - } - else if (value is string) - { - 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; } From ec4ce77da5537c89985daa66b0e4530c04497b98 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Thu, 23 Apr 2020 10:24:32 -0700 Subject: [PATCH 15/53] Extreme refactoring/enhancements rexm/Handlebars.Net/issues/294 - rexm/Handlebars.Net/issues/264 - rexm/Handlebars.Net/issues/151 - rexm/Handlebars.Net/issues/301 rexm/Handlebars.Net/issues/344 rexm/Handlebars.Net/issues/338 rexm/Handlebars.Net/issues/336 rexm/Handlebars.Net/issues/308 (for Newtonsoft.Json) rexm/Handlebars.Net/issues/307 rexm/Handlebars.Net/issues/243 rexm/Handlebars.Net/issues/194 rexm/Handlebars.Net/issues/122 --- .travis.yml | 34 - README.md | 14 +- appveyor.yml | 22 - hbnet-extension-icon.png | Bin 0 -> 9275 bytes hbnet-icon.png | Bin 11437 -> 8688 bytes source/Handlebars.Benchmark/ClassBuilder.cs | 1 + source/Handlebars.Benchmark/Compilation.cs | 88 +- source/Handlebars.Benchmark/Execution.cs | 402 ++++----- .../Handlebars.Benchmark.csproj | 12 +- .../PreviousVersion/1.10.1.dll | Bin 0 -> 92160 bytes source/Handlebars.Benchmark/Program.cs | 7 +- source/Handlebars.Code.sln | 6 + .../CompileFastExtensions.cs | 22 + .../FastCompilerFeature.cs | 26 + .../FastExpressionCompiler.cs | 63 ++ .../Handlebars.Extension.CompileFast.csproj | 53 ++ .../Handlebars.Extension.Logger.csproj | 47 + .../LoggerFeature.cs | 62 ++ .../LoggerFeatureExtensions.cs | 23 + .../LoggerFeatureFactory.cs | 19 + .../LoggingLevel.cs | 28 + .../netstandard2.0/LoggerFeatureExtensions.cs | 26 + .../netstandard2.0/LoggerFeatureFactory.cs | 37 + .../Handlebars.Test/BasicIntegrationTests.cs | 812 ++++++++++-------- source/Handlebars.Test/CollectionsTests.cs | 34 + .../CustomConfigurationTests.cs | 11 +- source/Handlebars.Test/DynamicTests.cs | 1 - source/Handlebars.Test/Handlebars.Test.csproj | 6 + source/Handlebars.Test/HelperTests.cs | 41 +- source/Handlebars.Test/HtmlEncoderTests.cs | 1 - source/Handlebars.Test/NumericLiteralTests.cs | 3 +- source/Handlebars.Test/PartialTests.cs | 37 +- source/Handlebars.Test/RawHelperTests.cs | 11 - source/Handlebars.Test/TripleStashTests.cs | 3 +- .../ViewEngine/ViewEngineTests.cs | 2 - source/Handlebars.Test/WhitespaceTests.cs | 11 +- source/Handlebars.sln | 12 + source/Handlebars/BuiltinHelpers.cs | 115 --- .../Collections/CascadeCollection.cs | 67 ++ .../Collections/CascadeDictionary.cs | 149 ++++ .../Handlebars/Collections/DeferredValue.cs | 24 + .../Collections/DisposableContainer.cs | 21 + .../ExtendedEnumerable.cs | 62 +- .../Collections/HashedCollection.cs | 67 ++ source/Handlebars/Collections/LookupSlim.cs | 39 + source/Handlebars/Collections/ObjectPool.cs | 33 + .../Handlebars/Collections/RefDictionary.cs | 290 +++++++ source/Handlebars/Collections/RefLookup.cs | 37 + .../Collections/StringBuilderPool.cs | 30 + .../Handlebars/Compiler/CompilationContext.cs | 28 +- .../Compiler/ExpressionShortcuts.cs | 632 -------------- source/Handlebars/Compiler/FunctionBuilder.cs | 93 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 69 +- .../Compiler/HandlebarsCompilerException.cs | 37 +- .../Lexer/Converter/BlockAccumulator.cs | 3 +- .../BlockAccumulatorContext.cs | 17 +- .../BlockHelperAccumulatorContext.cs | 10 +- .../ConditionalBlockAccumulatorContext.cs | 26 +- .../DeferredBlockAccumulatorContext.cs | 71 -- .../IteratorBlockAccumulatorContext.cs | 1 - .../Converter/ExpressionScopeConverter.cs | 3 +- .../Lexer/Converter/HashParameterConverter.cs | 23 +- .../Converter/HashParametersAccumulator.cs | 79 +- .../Converter/HelperArgumentAccumulator.cs | 5 +- .../Lexer/Converter/HelperConverter.cs | 34 +- .../Lexer/Converter/LiteralConverter.cs | 3 +- .../Compiler/Lexer/Converter/PathConverter.cs | 4 +- .../Lexer/Converter/RawHelperAccumulator.cs | 46 +- .../Lexer/Converter/StaticConverter.cs | 3 +- .../Lexer/Converter/SubExpressionConverter.cs | 4 +- .../Lexer/Converter/TokenConverter.cs | 6 +- .../Lexer/HandlebarsParserException.cs | 37 +- .../Lexer/Parsers/BlockParamsParser.cs | 43 +- .../Compiler/Lexer/Parsers/BlockWordParser.cs | 74 +- .../Compiler/Lexer/Parsers/CommentParser.cs | 104 +-- .../Compiler/Lexer/Parsers/LiteralParser.cs | 74 +- .../Compiler/Lexer/Parsers/Parser.cs | 7 +- .../Compiler/Lexer/Parsers/PartialParser.cs | 8 +- .../Lexer/Parsers/StringBuilderEnumerator.cs | 29 + .../Compiler/Lexer/Parsers/WordParser.cs | 109 ++- source/Handlebars/Compiler/Lexer/Tokenizer.cs | 238 +++-- .../Compiler/Lexer/Tokens/AssignmentToken.cs | 15 +- .../Lexer/Tokens/BlockParameterToken.cs | 11 +- .../Compiler/Lexer/Tokens/CommentToken.cs | 4 +- .../Lexer/Tokens/EndExpressionToken.cs | 48 +- .../Lexer/Tokens/EndSubExpressionToken.cs | 22 +- .../Lexer/Tokens/ExpressionScopeToken.cs | 4 +- .../Compiler/Lexer/Tokens/ExpressionToken.cs | 4 +- .../Lexer/Tokens/LiteralExpressionToken.cs | 36 +- .../Compiler/Lexer/Tokens/PartialToken.cs | 16 +- .../Lexer/Tokens/StartExpressionToken.cs | 49 +- .../Lexer/Tokens/StartSubExpressionToken.cs | 23 +- .../Compiler/Lexer/Tokens/StaticToken.cs | 34 +- .../Handlebars/Compiler/Lexer/Tokens/Token.cs | 43 +- .../Compiler/Lexer/Tokens/TokenType.cs | 2 - .../Lexer/Tokens/WordExpressionToken.cs | 22 +- .../Resolvers/IExpressionNameResolver.cs | 9 + .../UpperCamelCaseExpressionNameResolver.cs | 2 + .../Compiler/Structure/BindingContext.cs | 197 +++-- .../Structure/BindingContextValueProvider.cs | 39 - .../Structure/BlockHelperExpression.cs | 9 +- .../Structure/BlockParamsExpression.cs | 4 +- .../Structure/BlockParamsValueProvider.cs | 81 +- .../Compiler/Structure/BoolishExpression.cs | 2 +- .../Compiler/Structure/CommentExpression.cs | 5 +- .../Structure/DeferredSectionExpression.cs | 29 - .../Structure/HandlebarsExpression.cs | 22 +- .../HashParameterAssignmentExpression.cs | 3 +- .../Structure/HashParametersExpression.cs | 2 +- .../Compiler/Structure/HelperExpression.cs | 13 +- .../Compiler/Structure/IValueProvider.cs | 17 - .../Compiler/Structure/IteratorExpression.cs | 1 - .../Compiler/Structure/PartialExpression.cs | 6 +- .../Compiler/Structure/Path/ChainSegment.cs | 49 ++ .../Compiler/Structure/Path/PathInfo.cs | 50 ++ .../Compiler/Structure/Path/PathResolver.cs | 206 +++++ .../Compiler/Structure/Path/PathSegment.cs | 23 + .../Compiler/Structure/PathExpression.cs | 5 +- .../Compiler/Structure/PathResolver.cs | 418 --------- .../Compiler/Structure/StatementExpression.cs | 2 +- .../Compiler/Structure/StaticExpression.cs | 6 +- .../Structure/UndefinedBindingResult.cs | 5 +- .../Expression/BlockHelperFunctionBinder.cs | 156 +++- .../Expression/BoolishConverter.cs | 17 +- .../Translation/Expression/CommentVisitor.cs | 8 +- .../Translation/Expression/ContextBinder.cs | 82 +- .../Expression/DeferredSectionBlockHelper.cs | 57 ++ .../Expression/DeferredSectionVisitor.cs | 88 -- .../Expression/HandlebarsExpressionVisitor.cs | 33 +- .../HandlebarsUndefinedBindingException.cs | 9 + .../Expression/HashParameterBinder.cs | 10 - .../Expression/HashParameterDictionary.cs | 10 +- .../Expression/HelperFunctionBinder.cs | 63 +- .../Translation/Expression/IteratorBinder.cs | 645 ++++---------- .../Translation/Expression/LambdaReducer.cs | 35 + .../Translation/Expression/PartialBinder.cs | 58 +- .../Translation/Expression/PathBinder.cs | 23 +- .../Translation/Expression/StaticReplacer.cs | 21 +- .../Expression/SubExpressionVisitor.cs | 86 +- .../Expression/UnencodedStatementVisitor.cs | 16 +- .../Handlebars/Configuration/Compatibility.cs | 13 + .../Configuration/CompileTimeConfiguration.cs | 41 + .../Configuration/HandlebarsConfiguration.cs | 97 +++ .../InternalHandlebarsConfiguration.cs | 73 ++ source/Handlebars/DescriptionAttribute.cs | 14 - source/Handlebars/DynamicViewModel.cs | 11 + source/Handlebars/EnumerableExtensions.cs | 36 +- .../Features/BuildInHelpersFeature.cs | 96 +++ source/Handlebars/Features/ClosureFeature.cs | 172 ++++ .../Features/DefaultCompilerFeature.cs | 71 ++ source/Handlebars/Features/IFeature.cs | 19 + source/Handlebars/Features/IFeatureFactory.cs | 14 + source/Handlebars/Features/TemplateClosure.cs | 66 ++ source/Handlebars/Features/WarmUpFeature.cs | 75 ++ .../FileSystemPartialTemplateResolver.cs | 10 +- source/Handlebars/Handlebars.cs | 82 +- source/Handlebars/Handlebars.csproj | 45 +- source/Handlebars/HandlebarsConfiguration.cs | 53 -- source/Handlebars/HandlebarsEnvironment.cs | 211 ++--- source/Handlebars/HandlebarsException.cs | 42 +- source/Handlebars/HandlebarsExtensions.cs | 23 +- .../Handlebars/HandlebarsRuntimeException.cs | 41 +- source/Handlebars/HandlebarsUtils.cs | 30 +- source/Handlebars/HandlebarsViewEngine.cs | 20 +- source/Handlebars/HelperOptions.cs | 25 +- source/Handlebars/Helpers/IHelperResolver.cs | 27 + source/Handlebars/HtmlEncoder.cs | 77 -- source/Handlebars/IExpressionCompiler.cs | 19 + source/Handlebars/IExpressionMiddleware.cs | 17 + source/Handlebars/IHandlebars.cs | 49 +- .../Handlebars/{ => IO}/EncodedTextWriter.cs | 33 +- source/Handlebars/IO/ExtendedStringReader.cs | 65 ++ source/Handlebars/IO/HtmlEncoder.cs | 83 ++ source/Handlebars/IO/IReaderContext.cs | 8 + source/Handlebars/IO/ITextEncoder.cs | 15 + source/Handlebars/IO/PolledStringWriter.cs | 23 + source/Handlebars/ISafeString.cs | 9 - source/Handlebars/ITextEncoder.cs | 7 - .../MemberAccessors/ContextMemberAccessor.cs | 15 + .../DictionaryMemberAccessor.cs | 19 + .../MemberAccessors/DynamicMemberAccessor.cs | 31 + .../EnumerableMemberAccessor.cs | 35 + .../MemberAccessors/IMemberAccessor.cs | 20 + .../ReflectionMemberAccessor.cs | 193 +++++ .../CollectionMemberAliasProvider.cs | 72 ++ .../DelegatedMemberAliasProvider.cs | 75 ++ .../IMemberAliasProvider.cs | 20 + .../CollectionObjectDescriptor.cs | 54 ++ .../ContextObjectDescriptor.cs | 29 + .../DictionaryObjectDescriptor.cs | 36 + .../DynamicObjectDescriptor.cs | 27 + .../EnumerableObjectDescriptor.cs | 26 + ...nericDictionaryObjectDescriptorProvider.cs | 79 ++ .../IObjectDescriptorProvider.cs | 24 + .../KeyValuePairObjectDescriptorProvider.cs | 59 ++ .../ObjectDescriptors/ObjectDescriptor.cs | 41 + .../ObjectDescriptorFactory.cs | 85 ++ .../ObjectDescriptorProvider.cs | 55 ++ ...tringDictionaryObjectDescriptorProvider.cs | 74 ++ .../Handlebars/Polyfills/StringExtensions.cs | 15 + .../BindingContextValueProvider.cs | 59 ++ .../ValueProviders/IValueProvider.cs | 18 + .../ValueProviders/IteratorValueProvider.cs | 70 ++ .../ObjectEnumeratorValueProvider.cs | 57 ++ source/Handlebars/hbnet-icon.png | Bin 0 -> 2572 bytes 205 files changed, 6735 insertions(+), 4151 deletions(-) delete mode 100644 .travis.yml delete mode 100644 appveyor.yml create mode 100644 hbnet-extension-icon.png create mode 100644 source/Handlebars.Benchmark/PreviousVersion/1.10.1.dll create mode 100644 source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs create mode 100644 source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs create mode 100644 source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs create mode 100644 source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj create mode 100644 source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj create mode 100644 source/Handlebars.Extension.Logger/LoggerFeature.cs create mode 100644 source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs create mode 100644 source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs create mode 100644 source/Handlebars.Extension.Logger/LoggingLevel.cs create mode 100644 source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs create mode 100644 source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs create mode 100644 source/Handlebars.Test/CollectionsTests.cs delete mode 100644 source/Handlebars/BuiltinHelpers.cs create mode 100644 source/Handlebars/Collections/CascadeCollection.cs create mode 100644 source/Handlebars/Collections/CascadeDictionary.cs create mode 100644 source/Handlebars/Collections/DeferredValue.cs create mode 100644 source/Handlebars/Collections/DisposableContainer.cs rename source/Handlebars/{Compiler/Translation/Expression => Collections}/ExtendedEnumerable.cs (59%) create mode 100644 source/Handlebars/Collections/HashedCollection.cs create mode 100644 source/Handlebars/Collections/LookupSlim.cs create mode 100644 source/Handlebars/Collections/ObjectPool.cs create mode 100644 source/Handlebars/Collections/RefDictionary.cs create mode 100644 source/Handlebars/Collections/RefLookup.cs create mode 100644 source/Handlebars/Collections/StringBuilderPool.cs delete mode 100644 source/Handlebars/Compiler/ExpressionShortcuts.cs delete mode 100644 source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/DeferredBlockAccumulatorContext.cs create mode 100644 source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs delete mode 100644 source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs delete mode 100644 source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs delete mode 100644 source/Handlebars/Compiler/Structure/IValueProvider.cs create mode 100644 source/Handlebars/Compiler/Structure/Path/ChainSegment.cs create mode 100644 source/Handlebars/Compiler/Structure/Path/PathInfo.cs create mode 100644 source/Handlebars/Compiler/Structure/Path/PathResolver.cs create mode 100644 source/Handlebars/Compiler/Structure/Path/PathSegment.cs delete mode 100644 source/Handlebars/Compiler/Structure/PathResolver.cs create mode 100644 source/Handlebars/Compiler/Translation/Expression/DeferredSectionBlockHelper.cs delete mode 100644 source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs create mode 100644 source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs create mode 100644 source/Handlebars/Configuration/Compatibility.cs create mode 100644 source/Handlebars/Configuration/CompileTimeConfiguration.cs create mode 100644 source/Handlebars/Configuration/HandlebarsConfiguration.cs create mode 100644 source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs delete mode 100644 source/Handlebars/DescriptionAttribute.cs create mode 100644 source/Handlebars/Features/BuildInHelpersFeature.cs create mode 100644 source/Handlebars/Features/ClosureFeature.cs create mode 100644 source/Handlebars/Features/DefaultCompilerFeature.cs create mode 100644 source/Handlebars/Features/IFeature.cs create mode 100644 source/Handlebars/Features/IFeatureFactory.cs create mode 100644 source/Handlebars/Features/TemplateClosure.cs create mode 100644 source/Handlebars/Features/WarmUpFeature.cs delete mode 100644 source/Handlebars/HandlebarsConfiguration.cs create mode 100644 source/Handlebars/Helpers/IHelperResolver.cs delete mode 100644 source/Handlebars/HtmlEncoder.cs create mode 100644 source/Handlebars/IExpressionCompiler.cs create mode 100644 source/Handlebars/IExpressionMiddleware.cs rename source/Handlebars/{ => IO}/EncodedTextWriter.cs (57%) create mode 100644 source/Handlebars/IO/ExtendedStringReader.cs create mode 100644 source/Handlebars/IO/HtmlEncoder.cs create mode 100644 source/Handlebars/IO/IReaderContext.cs create mode 100644 source/Handlebars/IO/ITextEncoder.cs create mode 100644 source/Handlebars/IO/PolledStringWriter.cs delete mode 100644 source/Handlebars/ISafeString.cs delete mode 100644 source/Handlebars/ITextEncoder.cs create mode 100644 source/Handlebars/MemberAccessors/ContextMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DictionaryMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/DynamicMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/EnumerableMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/IMemberAccessor.cs create mode 100644 source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs create mode 100644 source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs create mode 100644 source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs create mode 100644 source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs create mode 100644 source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs create mode 100644 source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs create mode 100644 source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs create mode 100644 source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs create mode 100644 source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs create mode 100644 source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs create mode 100644 source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs create mode 100644 source/Handlebars/Polyfills/StringExtensions.cs create mode 100644 source/Handlebars/ValueProviders/BindingContextValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/IValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/IteratorValueProvider.cs create mode 100644 source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs create mode 100644 source/Handlebars/hbnet-icon.png 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 84b01adc..a00867ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -Handlebars.Net [![Build Status](https://travis-ci.org/rexm/Handlebars.Net.svg?branch=master)](https://travis-ci.org/rexm/Handlebars.Net) [![Nuget](https://img.shields.io/nuget/v/Handlebars.Net)](https://www.nuget.org/packages/Handlebars.Net/) +handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) ============== -**[Call for Input on v2](https://github.com/rexm/Handlebars.Net/issues/294)** +_This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while and I was not able to reach @rexm._ Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET application. @@ -8,11 +8,11 @@ Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET Check out the [handlebars.js documentation](http://handlebarsjs.com) for how to write Handlebars templates. -Handlebars.Net doesn't use a scripting engine to run a Javascript library - it **compiles Handlebars templates directly to IL bytecode**. It also mimics the JS library's API as closely as possible. +handlebars.csharp doesn't use a scripting engine to run a Javascript library - it **compiles Handlebars templates directly to IL bytecode**. It also mimics the JS library's API as closely as possible. ## Install - dotnet add package Handlebars.Net + dotnet add package handlebars.csharp ## Usage @@ -156,16 +156,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/hbnet-extension-icon.png b/hbnet-extension-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..340586359a431f50ce2ea10482959457cbaad806 GIT binary patch literal 9275 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 Hu0mjf3g1n9 diff --git a/source/Handlebars.Benchmark/ClassBuilder.cs b/source/Handlebars.Benchmark/ClassBuilder.cs index c987a881..5cd95cce 100644 --- a/source/Handlebars.Benchmark/ClassBuilder.cs +++ b/source/Handlebars.Benchmark/ClassBuilder.cs @@ -48,6 +48,7 @@ private TypeBuilder CreateClass() TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout , null); + return typeBuilder; } diff --git a/source/Handlebars.Benchmark/Compilation.cs b/source/Handlebars.Benchmark/Compilation.cs index a77e4629..6de4ff16 100644 --- a/source/Handlebars.Benchmark/Compilation.cs +++ b/source/Handlebars.Benchmark/Compilation.cs @@ -1,54 +1,67 @@ using System; +using System.Linq; +using System.Reflection; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using HandlebarsDotNet; +using HandlebarsDotNet.Extension.CompileFast; namespace Benchmark { - //[SimpleJob(RuntimeMoniker.Net461)] - [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class Compilation { private IHandlebars _handlebars; + + [Params("current", "current-fast", "1.10.1")] + public string Version; + private Func> _compileMethod; + private object _handlebarsRef; + [GlobalSetup] public void Setup() { - _handlebars = Handlebars.Create(); - _handlebars.RegisterHelper("customHelper", (writer, context, parameters) => + if (!Version.StartsWith("current")) { - }); - } + var assembly = Assembly.LoadFile($"{Environment.CurrentDirectory}\\PreviousVersion\\{Version}.dll"); + var type = assembly.GetTypes().FirstOrDefault(o => o.Name == nameof(Handlebars)); + var methodInfo = type.GetMethod("Create"); + _handlebarsRef = methodInfo.Invoke(null, new object[]{ null }); + var objType = _handlebarsRef.GetType(); - [Benchmark] - public Func Complex() - { - const string template = "{{#each County}}" + - "{{#if @first}}" + - "{{this}}" + - "{{else}}" + - "{{#if @last}}" + - " and {{this}}" + - "{{else}}" + - ", {{this}}" + - "{{/if}}" + - "{{/if}}" + - "{{/each}}"; + var compileMethod = objType.GetMethod("Compile", new[]{typeof(string)}); + _compileMethod = (Func>) compileMethod.CreateDelegate(typeof(Func>), _handlebarsRef); + } + else + { + _handlebars = Handlebars.Create(); + if (Version.Contains("fast")) + { + _handlebars.Configuration.UseCompileFast(); + } - return _handlebars.Compile(template); + _compileMethod = _handlebars.Compile; + } } [Benchmark] - public Func WithParentIndex() + public Func Template() { const string template = @" + childCount={{level1.Count}} + childCount2={{level1.Count}} {{#each level1}} id={{id}} + childCount={{level2.Count}} + childCount2={{level2.Count}} index=[{{@../../index}}:{{@../index}}:{{@index}}] first=[{{@../../first}}:{{@../first}}:{{@first}}] last=[{{@../../last}}:{{@../last}}:{{@last}}] {{#each level2}} id={{id}} + childCount={{level3.Count}} + childCount2={{level3.Count}} index=[{{@../../index}}:{{@../index}}:{{@index}}] first=[{{@../../first}}:{{@../first}}:{{@first}}] last=[{{@../../last}}:{{@../last}}:{{@last}}] @@ -61,37 +74,12 @@ public Func WithParentIndex() {{/each}} {{/each}}"; - return _handlebars.Compile(template); - } - - [Benchmark] - public Func Each() - { - const string template = "{{#each enumerateMe}}{{this}} {{/each}}"; - return _handlebars.Compile(template); + return Compile(template); } - - // [Benchmark] - // public Func EachBlockParams() - // { - // const string template = "{{#each enumerateMe as |item val|}}{{item}} {{val}} {{/each}}"; - // return _handlebars.Compile(template); - // } - - [Benchmark] - public Func Helper() - { - const string source = @"{{customHelper 'value'}}"; - return _handlebars.Compile(source); - } - - [Benchmark] - public Func HelperPostRegister() + private Func Compile(string template) { - const string source = @"{{not_registered_helper 'value'}}"; - - return _handlebars.Compile(source); + return _compileMethod(template); } } } \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index f5e68c5e..019bc8b3 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -3,55 +3,46 @@ using System.Linq; using System.Reflection; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Jobs; -using Bogus; using HandlebarsDotNet; +using HandlebarsDotNet.Extension.CompileFast; using Newtonsoft.Json.Linq; +using BenchmarkDotNet.Jobs; namespace Benchmark { - //[SimpleJob(RuntimeMoniker.Net461)] - [SimpleJob(RuntimeMoniker.NetCoreApp21, baseline: true)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class Execution { - private IHandlebars _handlebars; - - [Params(100)] + [Params(2, 5, 10)] public int N; + + [Params("current", "current-cache", "current-fast", "current-fast-cache", "1.10.1")] + public string Version; + + [Params("object", "dictionary")] + public string DataType; - private Faker _faker; private Func[] _templates; - private Dictionary _arrayData; - private Dictionary _dictionaryData; - private Dictionary _objectData; - private Dictionary _jsonData; - private Type _type; - private string[] _propertyNames; + + private object _data; [GlobalSetup] public void Setup() { - _handlebars = Handlebars.Create(); - _faker = new Faker(); - - const string eachTemplate = "{{#each County}}{{@key}}:{{@value}}\n{{/each}}"; - const string complexTemplate = "{{#each County}}" + - "{{#if @first}}" + - "{{@key}}:{{@value}}\n" + - "{{else}}" + - ", {{@key}}" + - "{{/if}}" + - "{{/each}}"; - const string helperTemplate = "{{customHelper 'value'}}"; - const string notRegisteredHelperTemplate = "{{not_registered_helper 'value'}}"; - const string withParentIndex = @" + const string template = @" + childCount={{level1.Count}} + childCount2={{level1.Count}} {{#each level1}} id={{id}} + childCount={{level2.Count}} + childCount2={{level2.Count}} index=[{{@../../index}}:{{@../index}}:{{@index}}] first=[{{@../../first}}:{{@../first}}:{{@first}}] last=[{{@../../last}}:{{@../last}}:{{@last}}] {{#each level2}} id={{id}} + childCount={{level3.Count}} + childCount2={{level3.Count}} index=[{{@../../index}}:{{@../index}}:{{@index}}] first=[{{@../../first}}:{{@../first}}:{{@first}}] last=[{{@../../last}}:{{@../last}}:{{@last}}] @@ -63,227 +54,188 @@ public void Setup() {{/each}} {{/each}} {{/each}}"; - - _handlebars.RegisterHelper("customHelper", (writer, context, parameters) => + + switch (DataType) { - writer.WriteSafeString(parameters[0]); - }); + case "object": + _data = new { level1 = ObjectLevel1Generator()}; + break; + + case "dictionary": + _data = new Dictionary(){ ["level1"] = DictionaryLevel1Generator()}; + break; + + case "json": + _data = new JObject {["level1"] = JsonLevel1Generator()}; + break; + } - string RandomProperty() => new string( - Enumerable.Range(0, 40) - .Select(x => x % 2 == 0 ? _faker.Random.Char('a', 'z') : _faker.Random.Char('A', 'Z')) - .ToArray() - ); + if (!Version.StartsWith("current")) + { + var assembly = Assembly.LoadFile($"{Environment.CurrentDirectory}\\PreviousVersion\\{Version}.dll"); + var type = assembly.GetTypes().FirstOrDefault(o => o.Name == nameof(Handlebars)); + var methodInfo = type.GetMethod("Create"); + var handlebars = methodInfo.Invoke(null, new object[]{ null }); + var objType = handlebars.GetType(); - var stringType = typeof(string); - _propertyNames = Enumerable.Range(0, N).Select(o => RandomProperty()).ToArray(); - _type = new ClassBuilder($"BenchmarkCountry{N}") - .CreateType( - _propertyNames, - Enumerable.Range(0, N).Select(o => stringType).ToArray() - ); + var compileMethod = objType.GetMethod("Compile", new[]{typeof(string)}); + _templates = new[] + { + (Func) compileMethod.Invoke(handlebars, new []{template}), + }; + } + else + { + Handlebars.Configuration.CompileTimeConfiguration.UseAggressiveCaching = Version.Contains("cache"); + + if (Version.Contains("fast")) + { + Handlebars.Configuration.UseCompileFast(); + } + + _templates = new[] + { + Handlebars.Compile(template) + }; + } - _templates = new[] + List ObjectLevel1Generator() { - _handlebars.Compile(eachTemplate), - _handlebars.Compile(withParentIndex), - _handlebars.Compile(complexTemplate), - _handlebars.Compile(helperTemplate), - _handlebars.Compile(notRegisteredHelperTemplate), - }; - } + var level = new List(); + for (int i = 0; i < N; i++) + { + level.Add(new + { + id = $"{i}", + level2 = ObjectLevel2Generator(i) + }); + } - [IterationSetup(Targets = new[]{nameof(SimpleEachJsonInput)/*, nameof(EachBlockParamsJsonInput)*/, nameof(ComplexJsonInput)})] - public void JsonIterationSetup() - { - var json = new JObject(); - for (var index = 0; index < _propertyNames.Length; index++) + return level; + } + + List ObjectLevel2Generator(int id1) { - json.Add(_propertyNames[index], JToken.FromObject(Guid.NewGuid())); + var level = new List(); + for (int i = 0; i < N; i++) + { + level.Add(new + { + id = $"{id1}-{i}", + level3 = ObjectLevel3Generator(id1, i) + }); + } + + return level; } - _jsonData = new Dictionary + List ObjectLevel3Generator(int id1, int id2) { - ["County"] = json - }; - } - - [IterationSetup(Targets = new[]{nameof(SimpleEachDictionaryInput)/*, nameof(EachBlockParamsDictionaryInput)*/, nameof(ComplexDictionaryInput)})] - public void DictionaryIterationSetup() - { - _dictionaryData = new Dictionary + var level = new List(); + for (int i = 0; i < N; i++) + { + level.Add(new + { + id = $"{id1}-{id2}-{i}" + }); + } + + return level; + } + + List> DictionaryLevel1Generator() { - ["County"] = _propertyNames.ToDictionary(o => o, o => Guid.NewGuid().ToString()) - }; - } - - [IterationSetup(Targets = new[]{nameof(SimpleEachArrayInput)/*, nameof(EachBlockParamsArrayInput)*/, nameof(ComplexArrayInput)})] - public void ArrayIterationSetup() - { - _arrayData = new Dictionary + var level = new List>(); + for (int i = 0; i < N; i++) + { + level.Add(new Dictionary() + { + ["id"] = $"{i}", + ["level2"] = DictionaryLevel2Generator(i) + }); + } + + return level; + } + + List> DictionaryLevel2Generator(int id1) { - ["County"] = _propertyNames.Select(o => Guid.NewGuid().ToString()).ToArray() - }; - } - - [IterationSetup(Targets = new[]{nameof(SimpleEachObjectInput)/*, nameof(EachBlockParamsObjectInput)*/, nameof(ComplexObjectInput)})] - public void ObjectIterationSetup() - { - var data = Activator.CreateInstance(_type); - var properties = data.GetType().GetRuntimeProperties().ToArray(); - for (var index = 0; index < properties.Length; index++) + var level = new List>(); + for (int i = 0; i < N; i++) + { + level.Add(new Dictionary() + { + ["id"] = $"{id1}-{i}", + ["level3"] = DictionaryLevel3Generator(id1, i) + }); + } + + return level; + } + + List> DictionaryLevel3Generator(int id1, int id2) { - properties[index].SetValue(data, Guid.NewGuid().ToString()); + var level = new List>(); + for (int i = 0; i < N; i++) + { + level.Add(new Dictionary() + { + ["id"] = $"{id1}-{id2}-{i}" + }); + } + + return level; + } + + JArray JsonLevel1Generator() + { + var level = new JArray(); + for (int i = 0; i < N; i++) + { + level.Add(new JObject + { + ["id"] = $"{i}", + ["level2"] = JsonLevel2Generator(i) + }); + } + + return level; } - _objectData = new Dictionary + JArray JsonLevel2Generator(int id1) { - ["County"] = data - }; - } - - [IterationSetup(Targets = new[]{nameof(Helper), nameof(HelperPostRegister)})] - public void HelpersSetup() - { - } + var level = new JArray(); + for (int i = 0; i < N; i++) + { + level.Add(new JObject + { + ["id"] = $"{id1}-{i}", + ["level3"] = JsonLevel3Generator(id1, i) + }); + } - [Benchmark] - public string SimpleEachArrayInput() - { - return _templates[0].Invoke(_arrayData); - } - - // [Benchmark] - // public string EachBlockParamsArrayInput() - // { - // return _templates[1].Invoke(_arrayData); - // } - - [Benchmark] - public string ComplexArrayInput() - { - return _templates[2].Invoke(_arrayData); - } - - [Benchmark] - public string SimpleEachObjectInput() - { - return _templates[0].Invoke(_objectData); - } - - // [Benchmark] - // public string EachBlockParamsObjectInput() - // { - // return _templates[1].Invoke(_objectData); - // } - - [Benchmark] - public string ComplexObjectInput() - { - return _templates[2].Invoke(_objectData); - } - - [Benchmark] - public string SimpleEachJsonInput() - { - return _templates[0].Invoke(_jsonData); - } - - // [Benchmark] - // public string EachBlockParamsJsonInput() - // { - // return _templates[1].Invoke(_jsonData); - // } - - [Benchmark] - public string ComplexJsonInput() - { - return _templates[2].Invoke(_jsonData); - } - - [Benchmark] - public string SimpleEachDictionaryInput() - { - return _templates[0].Invoke(_dictionaryData); - } - - // [Benchmark] - // public string EachBlockParamsDictionaryInput() - // { - // return _templates[1].Invoke(_dictionaryData); - // } - - [Benchmark] - public string ComplexDictionaryInput() - { - return _templates[2].Invoke(_dictionaryData); - } - - [Benchmark] - public string Helper() - { - return _templates[3].Invoke(new object()); - } - - [Benchmark] - public string HelperPostRegister() - { - _handlebars.RegisterHelper("not_registered_helper", (writer, context, parameters) => + return level; + } + + JArray JsonLevel3Generator(int id1, int id2) { - writer.WriteSafeString(parameters[0]); - }); + var level = new JArray(); + for (int i = 0; i < N; i++) + { + level.Add(new JObject() + { + ["id"] = $"{id1}-{id2}-{i}" + }); + } - return _templates[4].Invoke(new object()); + return level; + } } [Benchmark] public string WithParentIndex() { - var data = new - { - level1 = new[]{ - new { - id = "0", - level2 = new[]{ - new { - id = "0-0", - level3 = new[]{ - new { id = "0-0-0" }, - new { id = "0-0-1" } - } - }, - new { - id = "0-1", - level3 = new[]{ - new { id = "0-1-0" }, - new { id = "0-1-1" } - } - } - } - }, - new { - id = "1", - level2 = new[]{ - new { - id = "1-0", - level3 = new[]{ - new { id = "1-0-0" }, - new { id = "1-0-1" } - } - }, - new { - id = "1-1", - level3 = new[]{ - new { id = "1-1-0" }, - new { id = "1-1-1" } - } - } - } - } - } - }; - - return _templates[1].Invoke(data); + return _templates[0].Invoke(_data); } } } \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index c3a9374b..0f8fa402 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -2,18 +2,24 @@ Exe - netcoreapp2.1;net461 + netcoreapp2.1;netcoreapp2.1 - - + + + + + PreserveNewest + + + diff --git a/source/Handlebars.Benchmark/PreviousVersion/1.10.1.dll b/source/Handlebars.Benchmark/PreviousVersion/1.10.1.dll new file mode 100644 index 0000000000000000000000000000000000000000..c022e9a093ecae42eec091d88ed5ae7a3d6a365f GIT binary patch literal 92160 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 K|L^}j4g4>vOM|@t literal 0 HcmV?d00001 diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs index 1fdfb522..a7561f29 100644 --- a/source/Handlebars.Benchmark/Program.cs +++ b/source/Handlebars.Benchmark/Program.cs @@ -9,14 +9,9 @@ class Program { static void Main(string[] args) { - var execution = new Execution(); - execution.N = 100; - execution.Setup(); - execution.WithParentIndex(); - var manualConfig = DefaultConfig.Instance.WithArtifactsPath( $"./Benchmark-{FileVersionInfo.GetVersionInfo(typeof(Handlebars).Assembly.Location).FileVersion}" - ); + ).With(BenchmarkLogicalGroupRule.ByMethod); BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, manualConfig); } diff --git a/source/Handlebars.Code.sln b/source/Handlebars.Code.sln index 1a5ef6e5..eb0f0ef4 100644 --- a/source/Handlebars.Code.sln +++ b/source/Handlebars.Code.sln @@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +22,10 @@ Global {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.Build.0 = Debug|Any CPU {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.ActiveCfg = Release|Any CPU {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.Build.0 = Release|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs new file mode 100644 index 00000000..0d3d5b3b --- /dev/null +++ b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs @@ -0,0 +1,22 @@ +namespace HandlebarsDotNet.Extension.CompileFast +{ + /// + /// + /// + public static class CompileFastExtensions + { + /// + /// Changes to the one using FastExpressionCompiler + /// + /// + /// + public static HandlebarsConfiguration UseCompileFast(this HandlebarsConfiguration configuration) + { + var compileTimeConfiguration = configuration.CompileTimeConfiguration; + + compileTimeConfiguration.Features.Add(new FastCompilerFeatureFactory()); + + return configuration; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs b/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs new file mode 100644 index 00000000..261e85ca --- /dev/null +++ b/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs @@ -0,0 +1,26 @@ +using System.Linq; +using HandlebarsDotNet.Features; + +namespace HandlebarsDotNet.Extension.CompileFast +{ + internal class FastCompilerFeatureFactory : IFeatureFactory + { + public IFeature CreateFeature() + { + return new FastCompilerFeature(); + } + } + + internal class FastCompilerFeature : IFeature + { + public void OnCompiling(HandlebarsConfiguration configuration) + { + var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features.OfType().SingleOrDefault(); + configuration.CompileTimeConfiguration.ExpressionCompiler = new FastExpressionCompiler(configuration, templateFeature); + } + + public void CompilationCompleted() + { + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs new file mode 100644 index 00000000..b484d6c5 --- /dev/null +++ b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Expressions.Shortcuts; +using FastExpressionCompiler; +using HandlebarsDotNet.Features; + +namespace HandlebarsDotNet.Extension.CompileFast +{ + internal class FastExpressionCompiler : IExpressionCompiler + { + private readonly ClosureFeature _closureFeature; + private readonly TemplateClosure _templateClosure; + private readonly ExpressionContainer _closure; + private readonly ICollection _expressionMiddleware; + + public FastExpressionCompiler(HandlebarsConfiguration configuration, ClosureFeature closureFeature) + { + _closureFeature = closureFeature; + _templateClosure = closureFeature?.TemplateClosure; + _closure = closureFeature?.Closure; + _expressionMiddleware = configuration.CompileTimeConfiguration.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.CompileFast(); + } + + expression = (Expression) _closureFeature.ExpressionMiddleware.Invoke(expression); + + var parameters = new[] { (ParameterExpression) _closure }.Concat(expression.Parameters).ToArray(); + var lambda = Expression.Lambda(expression.Body, parameters); + var compiledDelegateType = Expression.GetDelegateType(parameters.Select(o => o.Type).Concat(new[] {lambda.ReturnType}).ToArray()); + + var method = typeof(FastExpressionCompiler) + .GetMethod(nameof(CompileGeneric), BindingFlags.Static | BindingFlags.NonPublic) + ?.MakeGenericMethod(compiledDelegateType); + + var compiledLambda = method?.Invoke(null, new object[] { lambda }) ?? throw new InvalidOperationException("lambda cannot be compiled"); + + var outerParameters = expression.Parameters.Select(o => Expression.Parameter(o.Type, o.Name)).ToArray(); + + var store = ExpressionShortcuts.Arg(_templateClosure).Property(o => o.Store); + var outerLambda = Expression.Lambda( + Expression.Invoke(Expression.Constant(compiledLambda), new[] {store.Expression}.Concat(outerParameters)), + outerParameters); + + return outerLambda.CompileFast(); + } + + private static T CompileGeneric(LambdaExpression expression) where T : class + { + return expression.CompileFast(); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj new file mode 100644 index 00000000..3ff2387b --- /dev/null +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -0,0 +1,53 @@ + + + + net452;netstandard1.3;netstandard2.0 + HandlebarsDotNet.Extension.CompileFast + 7 + 1.0.0 + 68ACA1C9-CAA6-40A9-B7B4-B619663E5E04 + + + + Oleh Formaniuk + Copyright © 2020 Oleh Formaniuk + FastExpressionCompiler adapter for handlebars.csharp + hbnet-extension-icon.png + handlebars.extension.compilefast + https://opensource.org/licenses/mit + https://github.com/zjklee/handlebars.csharp + handlebars;mustache;templating;engine;compiler + git + https://github.com/zjklee/handlebars.csharp + true + + + + + false + true + . + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj new file mode 100644 index 00000000..5edb4834 --- /dev/null +++ b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj @@ -0,0 +1,47 @@ + + + + net452;netstandard1.3;netstandard2.0 + HandlebarsDotNet.Extension.Logging + latest + enable + 1.0.0 + EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F + + + + Oleh Formaniuk + Copyright © 2020 Oleh Formaniuk + Microsoft.Extensions.Logging adapter for handlebars.csharp + hbnet-extension-icon.png + handlebars.extension.logging + https://opensource.org/licenses/mit + https://github.com/zjklee/handlebars.csharp + handlebars;mustache;templating;engine;compiler + git + https://github.com/zjklee/handlebars.csharp + true + + + + + false + true + . + + + + + + + + + + + + + + + + + diff --git a/source/Handlebars.Extension.Logger/LoggerFeature.cs b/source/Handlebars.Extension.Logger/LoggerFeature.cs new file mode 100644 index 00000000..d50ab786 --- /dev/null +++ b/source/Handlebars.Extension.Logger/LoggerFeature.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Features; + +namespace HandlebarsDotNet.Extension.Logger +{ + /// + /// + /// + /// + /// + /// + public delegate void Log(object[] arguments, LoggingLevel level, Func format); + + internal class LoggerFeature : IFeature + { + private readonly Log _logger; + + private readonly Func _defaultFormatter = objects => string.Join("; ", objects); + + public LoggerFeature(Log logger) + { + _logger = logger; + } + + public void OnCompiling(HandlebarsConfiguration configuration) + { + configuration.ReturnHelpers["log"] = LogHelper; + } + + public void CompilationCompleted() + { + } + + private string LogHelper(dynamic context, object[] arguments) + { + var logLevel = LoggingLevel.Info; + var formatter = _defaultFormatter; + + if (arguments.Last() is IDictionary hash) + { + if(hash.TryGetValue("level", out var level)) + { + if(Enum.TryParse(level.ToString(), true, out var hbLevel)) + { + logLevel = hbLevel; + } + } + + if (hash.TryGetValue("format", out var format)) + { + formatter = objects => string.Format(format.ToString(), arguments); + } + } + + _logger(arguments, logLevel, formatter); + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs b/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs new file mode 100644 index 00000000..6b8bc289 --- /dev/null +++ b/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs @@ -0,0 +1,23 @@ +namespace HandlebarsDotNet.Extension.Logger +{ + /// + /// + /// + public static partial class LoggerFeatureExtensions + { + /// + /// Adds log helper that uses provided + /// + /// + /// + /// + public static HandlebarsConfiguration UseLogger(this HandlebarsConfiguration configuration, Log logger) + { + var compileTimeConfiguration = configuration.CompileTimeConfiguration; + + compileTimeConfiguration.Features.Add(new LoggerFeatureFactory(logger)); + + return configuration; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs b/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs new file mode 100644 index 00000000..0a9d251a --- /dev/null +++ b/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs @@ -0,0 +1,19 @@ +using HandlebarsDotNet.Features; + +namespace HandlebarsDotNet.Extension.Logger +{ + internal partial class LoggerFeatureFactory : IFeatureFactory + { + private readonly Log _logger; + + public LoggerFeatureFactory(Log logger) + { + _logger = logger; + } + + public IFeature CreateFeature() + { + return new LoggerFeature(_logger); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggingLevel.cs b/source/Handlebars.Extension.Logger/LoggingLevel.cs new file mode 100644 index 00000000..0808bf31 --- /dev/null +++ b/source/Handlebars.Extension.Logger/LoggingLevel.cs @@ -0,0 +1,28 @@ +namespace HandlebarsDotNet.Extension.Logger +{ + /// + /// Handlebarsjs like logging levels + /// + public enum LoggingLevel + { + /// + /// + /// + Debug, + + /// + /// + /// + Info, + + /// + /// + /// + Warn, + + /// + /// + /// + Error + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs new file mode 100644 index 00000000..0f788a13 --- /dev/null +++ b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace HandlebarsDotNet.Extension.Logger +{ + /// + /// + /// + public static partial class LoggerFeatureExtensions + { + /// + /// Adds log helper that uses provided + /// + /// + /// + /// + public static HandlebarsConfiguration UseLogger(this HandlebarsConfiguration configuration, ILoggerFactory loggerFactory) + { + var compileTimeConfiguration = configuration.CompileTimeConfiguration; + + compileTimeConfiguration.Features.Add(new LoggerFeatureFactory(loggerFactory)); + + return configuration; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs new file mode 100644 index 00000000..2df358c5 --- /dev/null +++ b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Features; +using Microsoft.Extensions.Logging; + +namespace HandlebarsDotNet.Extension.Logger +{ + internal partial class LoggerFeatureFactory : IFeatureFactory + { + public LoggerFeatureFactory(ILoggerFactory factory) + { + _logger = CreateLogger(factory); + } + + private static Log CreateLogger(ILoggerFactory factory) + { + static LogLevel LogLevelMapper(LoggingLevel level) => + level switch + { + LoggingLevel.Debug => LogLevel.Debug, + LoggingLevel.Info => LogLevel.Information, + LoggingLevel.Warn => LogLevel.Warning, + LoggingLevel.Error => LogLevel.Error, + _ => LogLevel.Information + }; + + return (arguments, level, format) => + { + var logLevel = LogLevelMapper(level); + + factory.CreateLogger("Handlebars") + .Log(logLevel, 0, arguments, null, (objects, exception) => format(arguments)); + }; + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index cc8ead36..bc878d1b 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -3,19 +3,46 @@ 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.Extension.CompileFast; +using HandlebarsDotNet.Features; 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().UseCompileFast()), + 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() + [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 +51,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 +67,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 +83,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 +113,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 +127,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 +138,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 @@ -149,11 +159,11 @@ public void AssertHandlebarsUndefinedBindingException() 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" @@ -162,11 +172,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" @@ -175,11 +185,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" @@ -188,11 +198,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" @@ -201,11 +211,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" } @@ -214,11 +224,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" } } @@ -227,11 +237,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" } } @@ -239,12 +249,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" @@ -253,11 +276,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" @@ -266,11 +289,24 @@ 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" } } @@ -278,12 +314,43 @@ public void BasicPropertyOnArray() 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" } } + }; + 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 @@ -298,11 +365,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 @@ -317,11 +384,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 @@ -341,11 +408,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 @@ -357,11 +424,27 @@ public void BasicWith() Assert.Equal("Hello, my good friend Erik!", result); } - [Fact] - public void WithWithBlockParams() + [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 template = handlebars.Compile(source); var data = new { person = new @@ -374,22 +457,22 @@ public void WithWithBlockParams() 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" @@ -398,11 +481,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" @@ -411,11 +494,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" @@ -424,11 +507,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 @@ -441,11 +524,11 @@ public void BasicObjectEnumerator() Assert.Equal("hello world ", result); } - [Fact] - public void BasicListEnumerator() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicListEnumerator(IHandlebars handlebars) { var source = "{{#each enumerateMe}}{{this}} {{/each}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new { enumerateMe = new string[] @@ -458,11 +541,12 @@ public void BasicListEnumerator() Assert.Equal("hello world ", result); } - [Fact] - public void BasicObjectEnumeratorWithLast() + [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 @@ -475,11 +559,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 @@ -492,11 +576,11 @@ public void BasicObjectEnumeratorWithKey() Assert.Equal("foo: hello bar: world ", result); } - [Fact] - public void ObjectEnumeratorWithBlockParams() + [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 template = handlebars.Compile(source); var data = new { enumerateMe = new @@ -509,11 +593,11 @@ public void ObjectEnumeratorWithBlockParams() 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 @@ -526,11 +610,11 @@ public void BasicDictionaryEnumerator() Assert.Equal("hello world ", result); } - [Fact] - public void DictionaryEnumeratorWithBlockParams() + [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 template = handlebars.Compile(source); var data = new { enumerateMe = new Dictionary @@ -543,11 +627,12 @@ public void DictionaryEnumeratorWithBlockParams() 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 @@ -561,11 +646,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 @@ -578,11 +663,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 @@ -595,11 +680,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 @@ -613,11 +698,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 @@ -629,11 +714,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 @@ -645,11 +730,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 @@ -661,11 +746,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 @@ -677,11 +762,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 @@ -693,11 +778,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 @@ -709,33 +794,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"; @@ -744,11 +829,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"; @@ -757,17 +842,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 { @@ -779,14 +864,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] + ""); }); @@ -803,12 +888,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 { @@ -822,23 +907,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 { @@ -852,12 +937,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 { @@ -868,12 +953,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 { @@ -884,12 +969,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 { @@ -903,12 +988,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 { @@ -920,29 +1005,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[] { @@ -956,12 +1041,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 { @@ -972,12 +1057,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 { @@ -988,12 +1073,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 { @@ -1004,12 +1089,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 { @@ -1020,12 +1105,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 { @@ -1036,12 +1121,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 { @@ -1052,12 +1137,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 { @@ -1069,12 +1154,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 { @@ -1085,12 +1170,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 { @@ -1101,15 +1186,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 { }; @@ -1117,15 +1202,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 { }; @@ -1133,15 +1218,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); @@ -1149,12 +1234,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 { @@ -1170,8 +1255,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}}"; @@ -1183,35 +1268,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
"; @@ -1219,12 +1296,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 { @@ -1238,12 +1315,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 { @@ -1257,12 +1334,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 { @@ -1274,12 +1351,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 { @@ -1291,12 +1368,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 { @@ -1316,12 +1393,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 { @@ -1333,12 +1410,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 { @@ -1350,12 +1427,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 { @@ -1367,13 +1444,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]) { @@ -1382,7 +1459,7 @@ public void TestNoWhitespaceBetweenExpressions() }); - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new { @@ -1395,11 +1472,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 @@ -1414,11 +1491,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 @@ -1431,12 +1508,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 { @@ -1455,11 +1532,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 } }); @@ -1468,11 +1544,11 @@ public void ImplicitIDictionaryImplementationShouldNotThrowNullref() 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 template = handlebars.Compile(source); var data = new Dictionary() { {"foo.bar", "fu"}, @@ -1482,21 +1558,21 @@ public void ShouldBeAbleToHandleFieldContainingDots() Assert.Equal("Everybody was kung-fu fighting!", result); } - [Fact] - public void ShouldBeAbleToHandleListWithNumericalFields() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleListWithNumericalFields(IHandlebars handlebars) { var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new List {"FOOBAR"}; var result = template(data); Assert.Equal("FOOBAR", result); } - [Fact] - public void ShouldBeAbleToHandleDictionaryWithNumericalFields() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleDictionaryWithNumericalFields(IHandlebars handlebars) { var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new Dictionary { {"0", "FOOBAR"}, @@ -1505,11 +1581,11 @@ public void ShouldBeAbleToHandleDictionaryWithNumericalFields() Assert.Equal("FOOBAR", result); } - [Fact] - public void ShouldBeAbleToHandleJObjectsWithNumericalFields() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleJObjectsWithNumericalFields(IHandlebars handlebars) { var source = "{{ [0] }}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new JObject { {"0", "FOOBAR"}, @@ -1518,12 +1594,12 @@ public void ShouldBeAbleToHandleJObjectsWithNumericalFields() Assert.Equal("FOOBAR", result); } - [Fact] - public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets(IHandlebars handlebars) { var source = "{{ noBracket }} {{ [noBracket] }} {{ [[startsWithBracket] }} {{ [endsWithBracket]] }} {{ [[bothBrackets]] }}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var data = new Dictionary { {"noBracket", "foo"}, @@ -1535,8 +1611,8 @@ public void ShouldBeAbleToHandleKeysStartingAndEndingWithSquareBrackets() Assert.Equal("foo foo bar baz buz", result); } - [Fact] - public void BasicReturnFromHelper() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicReturnFromHelper(IHandlebars Handlebars) { var getData = $"getData{Guid.NewGuid()}"; Handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); @@ -1547,11 +1623,11 @@ public void BasicReturnFromHelper() Assert.Equal("data", result); } - [Fact] - public void CollectionReturnFromHelper() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void CollectionReturnFromHelper(IHandlebars handlebars) { var getData = $"getData{Guid.NewGuid()}"; - Handlebars.RegisterHelper(getData, (context, arguments) => + handlebars.RegisterHelper(getData, (context, arguments) => { var data = new Dictionary { @@ -1562,18 +1638,18 @@ public void CollectionReturnFromHelper() return data; }); var source = $"{{{{#each ({getData} 'Darmstadt' 'San Francisco')}}}}{{{{@key}}}} lives in {{{{@value}}}}. {{{{/each}}}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new object()); Assert.Equal("Nils lives in Darmstadt. Yehuda lives in San Francisco. ", result); } - [Fact] - public void ReturnFromHelperWithSubExpression() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void ReturnFromHelperWithSubExpression(IHandlebars handlebars) { var formatData = $"formatData{Guid.NewGuid()}"; - Handlebars.RegisterHelper(formatData, (writer, context, arguments) => + handlebars.RegisterHelper(formatData, (writer, context, arguments) => { writer.WriteSafeString(arguments[0]); writer.WriteSafeString(" "); @@ -1581,45 +1657,45 @@ public void ReturnFromHelperWithSubExpression() }); var getData = $"getData{Guid.NewGuid()}"; - Handlebars.RegisterHelper(getData, (context, arguments) => + handlebars.RegisterHelper(getData, (context, arguments) => { return arguments[0]; }); var source = $"{{{{{getData} ({formatData} 'data' '42')}}}}"; - var template = Handlebars.Compile(source); + var template = handlebars.Compile(source); var result = template(new object()); Assert.Equal("data 42", result); } - [Fact] - public void ReturnFromHelperLateBindWithSubExpression() + [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); + var template = handlebars.Compile(source); - Handlebars.RegisterHelper(formatData, (writer, context, arguments) => + handlebars.RegisterHelper(formatData, (writer, context, arguments) => { writer.WriteSafeString(arguments[0]); writer.WriteSafeString(" "); writer.WriteSafeString(arguments[1]); }); - Handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); + handlebars.RegisterHelper(getData, (context, arguments) => arguments[0]); var result = template(new object()); Assert.Equal("data 42", result); } - [Fact] - public void BasicLookup() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicLookup(IHandlebars handlebars) { var source = "{{#each people}}{{.}} lives in {{lookup ../cities @index}} {{/each}}"; - var template = Handlebars.Create().Compile(source); + var template = handlebars.Compile(source); var data = new { people = new[]{"Nils", "Yehuda"}, @@ -1630,11 +1706,11 @@ public void BasicLookup() Assert.Equal("Nils lives in Darmstadt Yehuda lives in San Francisco ", result); } - [Fact] - public void LookupAsSubExpression() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void LookupAsSubExpression(IHandlebars handlebars) { var source = "{{#each persons}}{{name}} lives in {{#with (lookup ../cities [resident])~}}{{name}} ({{country}}){{/with}}{{/each}}"; - var template = Handlebars.Create().Compile(source); + var template = handlebars.Compile(source); var data = new { persons = new[] @@ -1669,17 +1745,63 @@ public void LookupAsSubExpression() Assert.Equal("Nils lives in Darmstadt (Germany)Yehuda lives in San Francisco (USA)", result); } - [Fact] - private void StringConditionTest() + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + private void StringConditionTest(IHandlebars handlebars) { - var template = "{{#if Email}}\"correo\": \"{{Email}}\",{{else}}\"correo\": \"no hay correo\",{{/if}}"; + var expected = "\"correo\": \"correo@gmail.com\""; + var template = "{{#if Email}}\"correo\": \"{{Email}}\"{{else}}\"correo\": \"no hay correo\",{{/if}}"; var data = new { Email = "correo@gmail.com" }; - var func = Handlebars.Compile(template); - var s = func(data); + 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); + } + + private class StringHelperResolver : IHelperResolver + { + public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) + { + 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 @@ -1705,7 +1827,7 @@ public string this[string index] { get { - throw new NotImplementedException(); + return "Hello world!"; } set { diff --git a/source/Handlebars.Test/CollectionsTests.cs b/source/Handlebars.Test/CollectionsTests.cs new file mode 100644 index 00000000..7fe1c996 --- /dev/null +++ b/source/Handlebars.Test/CollectionsTests.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Collections; +using Xunit; + +namespace Handlebars.Test +{ + public class CollectionsTests + { + [Fact] + public void CascadeDictionary_OuterChanged_ValueExists() + { + var outer = new Dictionary(); + var cascadeDictionary = new CascadeDictionary(outer); + + outer.Add("s", "s"); + + Assert.True(cascadeDictionary.ContainsKey("s")); + } + + [Fact] + public void CascadeDictionary_ToArray_ContainsValuesFromOuter() + { + var outer = new Dictionary(); + var cascadeDictionary = new CascadeDictionary(outer); + + outer.Add("s", "s"); + + var array = cascadeDictionary.ToArray(); + + Assert.Single(array); + } + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/CustomConfigurationTests.cs b/source/Handlebars.Test/CustomConfigurationTests.cs index f26b10b2..49054e40 100644 --- a/source/Handlebars.Test/CustomConfigurationTests.cs +++ b/source/Handlebars.Test/CustomConfigurationTests.cs @@ -18,12 +18,11 @@ public class CustomConfigurationTests public CustomConfigurationTests() { var configuration = new HandlebarsConfiguration - { - ExpressionNameResolver = - new UpperCamelCaseExpressionNameResolver() - }; - - this.HandlebarsInstance = Handlebars.Create(configuration); + { + ExpressionNameResolver = new UpperCamelCaseExpressionNameResolver() + }; + + HandlebarsInstance = Handlebars.Create(configuration); } #region UpperCamelCaseExpressionNameResolver Tests diff --git a/source/Handlebars.Test/DynamicTests.cs b/source/Handlebars.Test/DynamicTests.cs index 48b85f21..6714304e 100644 --- a/source/Handlebars.Test/DynamicTests.cs +++ b/source/Handlebars.Test/DynamicTests.cs @@ -1,5 +1,4 @@ using Xunit; -using System; using System.Dynamic; using System.Collections.Generic; using Newtonsoft.Json.Linq; diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 662482e0..a3575d71 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -3,6 +3,11 @@ full net461;netcoreapp1.1;netcoreapp2.0;netcoreapp3.1 + 700AF0B4-EA70-47B7-9F9D-17351E977B00 + + + + 0618;1701 @@ -16,6 +21,7 @@ + diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index e604096d..45b34fb7 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -1,9 +1,7 @@ using Xunit; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet.Test { @@ -36,8 +34,10 @@ public void BlockHelperWithBlockParams() { Handlebars.RegisterHelper("myHelper", (writer, options, context, args) => { var count = 0; - options.BlockParams((parameters, binder) => - binder(parameters.ElementAtOrDefault(0), () => ++count)); + options.BlockParams((parameters, binder, deps) => + { + binder(parameters.ElementAtOrDefault(0), ctx => ++count); + }); foreach(var arg in args) { @@ -55,6 +55,34 @@ public void BlockHelperWithBlockParams() 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 HelperWithLiteralArgumentsWithQuotes() @@ -366,7 +394,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) { @@ -376,7 +405,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/HtmlEncoderTests.cs b/source/Handlebars.Test/HtmlEncoderTests.cs index 1fa90d7e..7a559cb7 100644 --- a/source/Handlebars.Test/HtmlEncoderTests.cs +++ b/source/Handlebars.Test/HtmlEncoderTests.cs @@ -1,5 +1,4 @@ using HandlebarsDotNet; -using System; using Xunit; namespace Handlebars.Test diff --git a/source/Handlebars.Test/NumericLiteralTests.cs b/source/Handlebars.Test/NumericLiteralTests.cs index d054218a..a38ad7df 100644 --- a/source/Handlebars.Test/NumericLiteralTests.cs +++ b/source/Handlebars.Test/NumericLiteralTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using Xunit; namespace HandlebarsDotNet.Test diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs index 0454e6ae..cff3bee3 100644 --- a/source/Handlebars.Test/PartialTests.cs +++ b/source/Handlebars.Test/PartialTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Xunit; using System.IO; @@ -188,15 +187,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 +596,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 +609,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/RawHelperTests.cs b/source/Handlebars.Test/RawHelperTests.cs index 9ccd56b4..bc0dd12e 100644 --- a/source/Handlebars.Test/RawHelperTests.cs +++ b/source/Handlebars.Test/RawHelperTests.cs @@ -155,17 +155,6 @@ public void TestNonClosingRawBlockExpressionException() inst.Compile("{{{{rawBlockHelper}}}}{{foo}}")(new { foo = "foo" }); }); } - - [Fact] - public void TestMissingRawHelperRawBlockExpressionException() - { - var inst = Handlebars.Create(); - - Assert.Throws(() => - { - inst.Compile("{{{{rawBlockHelper}}}}{{foo}}{{{{/rawBlockHelper}}}}")(new { foo = "foo" }); - }); - } } } diff --git a/source/Handlebars.Test/TripleStashTests.cs b/source/Handlebars.Test/TripleStashTests.cs index 2c6882fa..cb8fb9af 100644 --- a/source/Handlebars.Test/TripleStashTests.cs +++ b/source/Handlebars.Test/TripleStashTests.cs @@ -1,5 +1,4 @@ -using System; -using Xunit; +using Xunit; using System.IO; namespace HandlebarsDotNet.Test diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 0200373c..a0c04788 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using HandlebarsDotNet; using Xunit; namespace HandlebarsDotNet.Test.ViewEngine diff --git a/source/Handlebars.Test/WhitespaceTests.cs b/source/Handlebars.Test/WhitespaceTests.cs index 395db786..61f95e2c 100644 --- a/source/Handlebars.Test/WhitespaceTests.cs +++ b/source/Handlebars.Test/WhitespaceTests.cs @@ -116,14 +116,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] @@ -193,15 +193,16 @@ public void StandalonePartials() { string source = "Here are:\n {{>person}} \n {{>person}} "; - var template = Handlebars.Compile(source); + var handlebars = Handlebars.Create(); + var template = handlebars.Compile(source); var data = new {name = "Marc"}; var partialSource = "{{name}}"; using(var reader = new StringReader(partialSource)) { - var partialTemplate = Handlebars.Compile(reader); - Handlebars.RegisterTemplate("person", partialTemplate); + var partialTemplate = handlebars.Compile(reader); + handlebars.RegisterTemplate("person", partialTemplate); } var result = template(data); diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 01e50843..0f569a6c 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -14,6 +14,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{E880C14C-96EE-4A1E-98BD-6348AB529090}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.Logger", "Handlebars.Extension.Logger\Handlebars.Extension.Logger.csproj", "{68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +36,14 @@ Global {E880C14C-96EE-4A1E-98BD-6348AB529090}.Debug|Any CPU.Build.0 = Debug|Any CPU {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.ActiveCfg = Release|Any CPU {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.Build.0 = Release|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.Build.0 = Release|Any CPU + {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/Handlebars/BuiltinHelpers.cs b/source/Handlebars/BuiltinHelpers.cs deleted file mode 100644 index 19685ae9..00000000 --- a/source/Handlebars/BuiltinHelpers.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Collections.Generic; -using System.Linq; -using HandlebarsDotNet.Compiler; - -namespace HandlebarsDotNet -{ - internal 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"); - } - - options.BlockParams((parameters, binder) => - binder(parameters.ElementAtOrDefault(0), () => arguments[0])); - - if (HandlebarsUtils.IsTruthyOrNonEmpty(arguments[0])) - { - options.Template(output, arguments[0]); - } - else - { - options.Inverse(output, context); - } - } - - [Description("lookup")] - public static object Lookup(dynamic context, params object[] arguments) - { - if (arguments.Length != 2) - { - throw new HandlebarsException("{{lookup}} helper must have exactly two argument"); - } - - var configuration = new HandlebarsConfiguration(); - var pathResolver = new PathResolver(); - var memberName = arguments[1].ToString(); - return !pathResolver.TryAccessMember(arguments[0], new ChainSegment(memberName), configuration, out var value) - ? new UndefinedBindingResult(memberName, configuration) - : value; - } - - [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 => GetHelpers(); - - public static IEnumerable> ReturnHelpers => GetHelpers(); - - public static IEnumerable> BlockHelpers => 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..1e496e30 --- /dev/null +++ b/source/Handlebars/Collections/CascadeCollection.cs @@ -0,0 +1,67 @@ +using System.Collections; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + 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..bc51e684 --- /dev/null +++ b/source/Handlebars/Collections/CascadeDictionary.cs @@ -0,0 +1,149 @@ +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 + { + int count = 0; + foreach (var value in _outer) + { + if (_inner.TryGetValue(value.Key, out var innerValue)) + { + count++; + } + else + { + 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..458ac962 --- /dev/null +++ b/source/Handlebars/Collections/DeferredValue.cs @@ -0,0 +1,24 @@ +using System; + +namespace HandlebarsDotNet.Collections +{ + internal struct DeferredValue + { + private T _value; + private bool _isValueCreated; + + public Func Factory { get; set; } + + public T Value + { + get + { + if (_isValueCreated) return _value; + + _value = Factory(); + _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..94d83406 --- /dev/null +++ b/source/Handlebars/Collections/DisposableContainer.cs @@ -0,0 +1,21 @@ +using System; + +namespace HandlebarsDotNet +{ + internal class DisposableContainer : IDisposable + { + private readonly Action _onDispose; + public T Value { get; } + + 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/HashedCollection.cs b/source/Handlebars/Collections/HashedCollection.cs new file mode 100644 index 00000000..70120274 --- /dev/null +++ b/source/Handlebars/Collections/HashedCollection.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Compiler +{ + 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..241ce846 --- /dev/null +++ b/source/Handlebars/Collections/LookupSlim.cs @@ -0,0 +1,39 @@ +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) + { + _inner = new Dictionary(); + _comparer = comparer ?? EqualityComparer.Default; + } + + 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; + } + + private TValue Write(TKey key, TValue value) + { + var copy = new Dictionary(_inner, _comparer); + + 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..7aaca9fb --- /dev/null +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; + +namespace HandlebarsDotNet +{ + internal abstract class ObjectPool + { + private readonly ConcurrentQueue _objects; + + protected ObjectPool() + { + _objects = new ConcurrentQueue(); + } + + public T GetObject() + { + return !_objects.TryDequeue(out var item) + ? CreateObject() + : item; + } + + public DisposableContainer Use() + { + return new DisposableContainer(GetObject(), PutObject); + } + + public virtual void PutObject(T item) + { + _objects.Enqueue(item); + } + + protected abstract T CreateObject(); + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/RefDictionary.cs b/source/Handlebars/Collections/RefDictionary.cs new file mode 100644 index 00000000..9c6b1170 --- /dev/null +++ b/source/Handlebars/Collections/RefDictionary.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace HandlebarsDotNet.Collections +{ + /// + /// Represents -like collection optimized for storing s. + /// The class API is very limited due to performance demands. + /// ! Collection does not guaranty successful write in concurrent scenarios: writes may be lost but it guaranties to return a value. + /// + /// + /// + internal class RefDictionary : IReadOnlyDictionary + where TValue : struct + { + private readonly IEqualityComparer _comparer; + private Container _container; + + public RefDictionary(int capacity = 16, IEqualityComparer comparer = null) + { + var initialCapacity = HashHelpers.GetPrime(capacity); + _container = new Container(new int[initialCapacity], new Entry[initialCapacity]); + _comparer = comparer ?? EqualityComparer.Default; + } + + public int Count => _container.Count; + + public bool ContainsKey(TKey key) + { + var container = _container; + var entries = container.Entries; + var entryIndex = GetEntryIndex(key, container); + + while (entryIndex != -1) + { + if (_comparer.Equals(entries[entryIndex].Key, key)) return true; + + entryIndex = entries[entryIndex].Next; + } + + return false; + } + + bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) + { + if (!ContainsKey(key)) + { + value = default(TValue); + return false; + } + + value = this[key]; + return true; + } + + TValue IReadOnlyDictionary.this[TKey key] => + ContainsKey(key) ? this[key] : default(TValue); + + public ref TValue this[TKey key] + { + get + { + var container = _container; + var entries = container.Entries; + + var entryIndex = GetEntryIndex(key, container); + while (entryIndex != -1) + { + if (_comparer.Equals(entries[entryIndex].Key, key)) + { + return ref entries[entryIndex].Value; + } + + entryIndex = entries[entryIndex].Next; + } + + if (Count == entries.Length && !TryResize(container, out container)) + { + var entry = new Entry(key); + var holder = new CollisionHolder(ref entry); + return ref holder.Entry.Value; + } + + entries = container.Entries; + var buckets = container.Buckets; + + entryIndex = container.Count++; + entries[entryIndex].Key = key; + var bucket = GetBucketIndex(key, container); + entries[entryIndex].Next = buckets[bucket] - 1; + buckets[bucket] = entryIndex + 1; + return ref entries[entryIndex].Value; + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + var container = _container; + var entries = container.Entries; + for (var index = 0; index < container.Count; index++) + { + yield return entries[index].Key; + } + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + var container = _container; + var entries = container.Entries; + for (var index = 0; index < container.Count; index++) + { + yield return entries[index].Value; + } + } + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + var container = _container; + var entries = container.Entries; + for (var index = 0; index < Count; index++) + { + var entry = entries[index]; + yield return new KeyValuePair(entry.Key, entry.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetBucketIndex(TKey key, Container container) => + (_comparer.GetHashCode(key) & 0x7FFFFFFF) % container.Buckets.Length; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetEntryIndex(TKey key, Container container) => + container.Buckets[GetBucketIndex(key, container)] - 1; + + private bool TryResize(Container container, out Container newContainer) + { + var entries = container.Entries; + + var count = container.Count; + var newSize = HashHelpers.ExpandPrime(count); + + var newEntries = new Entry[newSize]; + Array.Copy(entries, 0, newEntries, 0, count); + + var newBuckets = new int[newSize]; + + newContainer = new Container(newBuckets, newEntries, count); + for (var index = 0; index < count;) + { + var bucketIndex = GetBucketIndex(newEntries[index].Key, newContainer); + newEntries[index].Next = newBuckets[bucketIndex] - 1; + newBuckets[bucketIndex] = ++index; + } + + return ReferenceEquals(Interlocked.CompareExchange(ref _container, newContainer, container), container); + } + + private struct Entry + { + public Entry(TKey key) : this() + { + Key = key; + } + + public TKey Key; + public TValue Value; + public int Next; + } + + private class CollisionHolder + { + public Entry Entry; + + public CollisionHolder(ref Entry entry) + { + Entry = entry; + } + } + + private class Container + { + public readonly int[] Buckets; + public readonly Entry[] Entries; + public int Count; + + public Container(int[] buckets, Entry[] entries, int count = 0) + { + Buckets = buckets; + Entries = entries; + Count = count; + } + } + + private static class HashHelpers + { + // This is the maximum prime smaller than Array.MaxArrayLength + private const int MaxPrimeArrayLength = 0x7FEFFFFD; + + private const int HashPrime = 101; + + // Table of prime numbers to use as hash table sizes. + // A typical resize algorithm would pick the smallest prime number in this array + // that is larger than twice the previous capacity. + // Suppose our Hashtable currently has capacity x and enough elements are added + // such that a resize needs to occur. Resizing first computes 2x then finds the + // first prime in the table greater than 2x, i.e. if primes are ordered + // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n. + // Doubling is important for preserving the asymptotic complexity of the + // hashtable operations such as add. Having a prime guarantees that double + // hashing does not lead to infinite loops. IE, your hash function will be + // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. + // We prefer the low computation costs of higher prime numbers over the increased + // memory allocation of a fixed prime number i.e. when right sizing a HashSet. + private static readonly int[] Primes = + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 + }; + + private static bool IsPrime(int candidate) + { + if ((candidate & 1) == 0) return candidate == 2; + + var limit = (int) Math.Sqrt(candidate); + for (var divisor = 3; divisor <= limit; divisor += 2) + { + if ((candidate % divisor) == 0) + return false; + } + + return true; + } + + public static int GetPrime(int min) + { + if (min < 0) + throw new ArgumentException(); + + for (var i = 0; i < Primes.Length; i++) + { + var prime = Primes[i]; + if (prime >= min) + return prime; + } + + //outside of our predefined table. + //compute the hard way. + for (var i = (min | 1); i < int.MaxValue; i += 2) + { + if (IsPrime(i) && ((i - 1) % HashPrime != 0)) + return i; + } + + return min; + } + + // Returns size of hashtable to grow to. + public static int ExpandPrime(int oldSize) + { + var newSize = 2 * oldSize; + + // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint) newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) + { + return MaxPrimeArrayLength; + } + + return GetPrime(newSize); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/RefLookup.cs b/source/Handlebars/Collections/RefLookup.cs new file mode 100644 index 00000000..2093831b --- /dev/null +++ b/source/Handlebars/Collections/RefLookup.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal sealed class RefLookup + where TValue: struct + { + private readonly RefDictionary _inner; + + public delegate ref TValue ValueFactory(TKey key, ref TValue value); + + public RefLookup(int capacity = 16, IEqualityComparer comparer = null) + { + _inner = new RefDictionary(capacity, comparer); + } + + public bool ContainsKey(TKey key) + { + return _inner.ContainsKey(key); + } + + public ref TValue GetValueOrDefault(TKey key) + { + return ref _inner[key]; + } + + public ref TValue GetOrAdd(TKey key, ValueFactory factory) + { + if (_inner.ContainsKey(key)) + { + return ref _inner[key]; + } + + return ref factory(key, ref _inner[key]); + } + } +} \ 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..59de5f57 --- /dev/null +++ b/source/Handlebars/Collections/StringBuilderPool.cs @@ -0,0 +1,30 @@ +using System; +using System.Text; + +namespace HandlebarsDotNet +{ + internal class StringBuilderPool : ObjectPool + { + private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); + + private readonly int _initialCapacity; + + public static StringBuilderPool Shared => Lazy.Value; + + public StringBuilderPool(int initialCapacity = 16) + { + _initialCapacity = initialCapacity; + } + + protected override StringBuilder CreateObject() + { + return new StringBuilder(_initialCapacity); + } + + public override void PutObject(StringBuilder item) + { + item.Length = 0; + base.PutObject(item); + } + } +} \ 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/ExpressionShortcuts.cs b/source/Handlebars/Compiler/ExpressionShortcuts.cs deleted file mode 100644 index 5fe19316..00000000 --- a/source/Handlebars/Compiler/ExpressionShortcuts.cs +++ /dev/null @@ -1,632 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; - -namespace HandlebarsDotNet.Compiler -{ - internal class BlockBody : List - { - - } - - internal class ExpressionContainer - { - private readonly Expression _expression; - - public ExpressionContainer(Expression expression) => _expression = expression; - - public virtual Expression Expression => _expression; - - public ExpressionContainer Typed() => new ExpressionContainer(Expression); - - public ExpressionContainer Is() => new ExpressionContainer(Expression.TypeIs(Expression, typeof(TV))); - public ExpressionContainer As() => new ExpressionContainer(Expression.TypeAs(Expression, typeof(TV))); - public ExpressionContainer Cast() => new ExpressionContainer(Expression.Convert(Expression, typeof(TV))); - - public static implicit operator Expression(ExpressionContainer expressionContainer) => expressionContainer.Expression; - public static implicit operator ExpressionContainer(Expression expression) => new ExpressionContainer(expression); - } - - /// - /// Provides strongly typed container for . - /// - /// Used to trick C# compiler in cases like in order to pass value to target method. - /// Type of expected result value. - internal class ExpressionContainer : ExpressionContainer - { - public static implicit operator T(ExpressionContainer expressionContainer) => default(T); - - public ExpressionContainer(Expression expression) : base(expression) - { - } - } - - /// - /// Stands for shortcuts. - /// - internal static class ExpressionShortcuts - { - /// - /// Creates strongly typed representation of the - /// - /// If is null returns result of - /// to wrap - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Arg(Expression expression) => expression == null ? Null() : new ExpressionContainer(expression); - - /// - /// Creates strongly typed representation of the . - /// - /// If is null returns result of - /// to wrap - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Arg(Expression expression) => expression == null ? Null() : new ExpressionContainer(expression); - - /// - /// Creates strongly typed representation of the and performs on it. - /// - /// If is null returns result of - /// to wrap - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Cast(Expression expression) => expression == null ? Null() : new ExpressionContainer(Expression.Convert(expression, typeof(T))); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable ReplaceValuesOf(IEnumerable instance, Expression newValue) - { - var targetType = typeof(T); - return instance.Select(value => targetType.IsAssignableFrom(value.Type) - ? newValue - : value); - } - - public static IEnumerable ReplaceParameters(IEnumerable instance, IList newValue) - { - return newValue.Count != 0 - ? PerformReplacement() - : instance; - - IEnumerable PerformReplacement() - { - var visitor = new ParameterReplacerVisitor(newValue); - return instance.Where(o => o != null).Select(expression => visitor.Visit(expression)); - } - } - - /// - /// Creates strongly typed representation of the - /// - /// Variable name. Corresponds to type name if omitted. - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Var(string name = null) - { - return new ExpressionContainer(Expression.Variable(typeof(T), name ?? typeof(T).Name)); - } - - /// - /// Creates strongly typed representation of the - /// - /// Variable name. Corresponds to type name if omitted. - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Parameter(string name = null) - { - return new ExpressionContainer(Expression.Parameter(typeof(T), name ?? typeof(T).Name)); - } - - - /// - /// Creates strongly typed representation of the - /// - /// Variable name. Corresponds to type name if omitted. - /// Property accessor expression - /// Expected type of resulting target - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Property(Expression instance, Expression> propertyLambda) - { - return Arg(ProcessPropertyLambda(instance, propertyLambda)); - } - - /// - /// Creates strongly typed representation of the - /// - /// Variable name. Corresponds to type name if omitted. - /// Property accessor expression - /// - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Property(Expression instance, string propertyName) - { - return Arg(Expression.Property(instance, propertyName)); - } - - /// - /// Creates strongly typed representation of the - /// - /// Items for the new array - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Array(IEnumerable items) - { - return Arg(Expression.NewArrayInit(typeof(T), items)); - } - - /// - /// Creates or based on . - /// Parameters are resolved based on actual passed parameters. - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(Expression invocationExpression) - { - return new ExpressionContainer(ProcessCallLambda(invocationExpression)); - } - - /// - /// Creates or based on . - /// Parameters are resolved based on actual passed parameters. - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(Expression> invocationExpression) - { - return Arg(ProcessCallLambda(invocationExpression)); - } - - /// - /// Creates . Parameters for constructor and constructor itself are resolved based . - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer New(Expression> invocationExpression) - { - return Arg(ProcessCallLambda(invocationExpression)); - } - - /// - /// Creates using default constructor. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer New() where T: new() - { - return Arg(Expression.New(typeof(T).GetConstructor(new Type[0]))); - } - - /// - /// Provides fluent interface for creation - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BlockBuilder Block(Type returnType = null) - { - return new BlockBuilder(returnType); - } - - /// - /// Creates strongly typed representation of null. - /// - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Null() - { - return Arg(Expression.Convert(Expression.Constant(null), typeof(T))); - } - - /// - /// Creates strongly typed representation of null. - /// - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Null(Type type) - { - return new ExpressionContainer(Expression.Convert(Expression.Constant(null), type)); - } - - /// - /// Creates or based on . - /// Parameters are resolved based on actual passed parameters. - /// - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) - { - return new ExpressionContainer(ProcessCallLambda(invocationExpression, instance)); - } - - /// - /// Creates strongly typed representation of the - /// - /// - /// Property accessor expression - /// Expected type of resulting target - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Property(this ExpressionContainer instance, Expression> propertyAccessor) - { - return Property(instance.Expression, propertyAccessor); - } - - /// - /// Creates or based on . - /// Parameters are resolved based on actual passed parameters. - /// - /// - /// Expression used to invoke the method. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) - { - return Arg(ProcessCallLambda(invocationExpression, instance)); - } - - /// - /// Creates . - /// Parameters are resolved based on actual passed parameters. - /// - /// - /// Expressions used inside of try block. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TryExpression Using(this ExpressionContainer instance, Func, BlockBody> blockBody) where T : IDisposable - { - ExpressionContainer variable = Var(); - var block = Block() - .Parameter(variable, instance) - .Lines(blockBody(variable)); - - return Expression.TryFinally(block, instance.Call(o => o.Dispose())); - } - - public static Expression[] Return(this ExpressionContainer instance) - { - LabelTarget returnTarget = Expression.Label(typeof(T)); - - GotoExpression returnExpression = Expression.Return(returnTarget, instance, typeof(T)); - LabelExpression returnLabel = Expression.Label(returnTarget, Null()); - - return new Expression[]{returnExpression, returnLabel}; - } - - /// - /// Creates assign . - /// Parameters are resolved based on actual passed parameters. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Assign(this ExpressionContainer target, ExpressionContainer value) - { - return new ExpressionContainer(Expression.Assign(target, value)); - } - - /// - /// Creates assign . - /// Parameters are resolved based on actual passed parameters. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Assign(this ExpressionContainer target, T value) - { - return new ExpressionContainer(Expression.Assign(target, Expression.Constant(value, typeof(T)))); - } - - /// - /// Creates assign . - /// Parameters are resolved based on actual passed parameters. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Assign(this ExpressionContainer target, Expression value) - { - return new ExpressionContainer(Expression.Assign(target, value)); - } - - /// - /// Creates ternary assignment like target = condition ? ifTrue : ifFalse - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Expression TernaryAssign(this ExpressionContainer target, ExpressionContainer condition, ExpressionContainer ifTrue, ExpressionContainer ifFalse) - { - return Expression.IfThenElse( - condition.Expression, - Expression.Assign(target, ifTrue), - Expression.Assign(target, ifFalse)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static MemberExpression ProcessPropertyLambda(Expression instance, LambdaExpression propertyLambda) - { - var member = propertyLambda.Body as MemberExpression; - if (member == null) throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); - - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); - - return Expression.Property(instance, propInfo); - } - - private static Expression ProcessCallLambda(LambdaExpression propertyLambda, Expression instance = null) - { - return ProcessCall(propertyLambda.Body, instance); - } - - private static Expression ProcessCall(Expression propertyLambda, Expression instance = null) - { - var parameters = instance != null ? new[] { instance } : new Expression[0]; - - switch (propertyLambda) - { - case NewExpression newExpression: - return Expression.New(newExpression.Constructor, ExtractArguments(newExpression.Arguments)); - - case MethodCallExpression member: - var methodInfo = member.Method; - IEnumerable methodCallArguments = member.Arguments; - var inst = ReplaceParameters(new[] {member.Object}, parameters).SingleOrDefault(); - methodCallArguments = ReplaceParameters(methodCallArguments, parameters); - methodCallArguments = methodCallArguments.Select(ExtractArgument); - var memberObject = methodInfo.IsStatic - ? null : Expression.Convert(inst, methodInfo.DeclaringType); - return Expression.Call(memberObject, methodInfo, methodCallArguments); - - case InvocationExpression invocationExpression: - return invocationExpression.Update( - invocationExpression.Expression, - ExtractArguments(invocationExpression.Arguments) - ); - - default: - return propertyLambda; - } - } - - private static IReadOnlyCollection ExtractArguments(IReadOnlyCollection exprs) - { - var result = new Expression[exprs.Count]; - if (exprs is IList list) - { - for (var index = 0; index < list.Count; index++) - { - result[index] = ExtractArgument(list[index]); - } - - return result; - } - else - { - int index = 0; - foreach (var expr in exprs) - { - result[index++] = ExtractArgument(expr); - } - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Expression ExtractArgument(Expression expr) - { - var value = ExtractFromExpression(expr); - - if (value is Expression expression) return expression; - if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; - return Expression.Constant(value); - } - - private static object ExtractFromExpression(Expression expression) - { - while (true) - { - switch (expression) - { - case MethodCallExpression methodCall: - return Expression.Lambda(methodCall).Compile().DynamicInvoke(); - - case UnaryExpression unaryExpression: - switch (unaryExpression.NodeType) - { - case ExpressionType.Convert: - if (typeof(ExpressionContainer).IsAssignableFrom(unaryExpression.Operand.Type)) - { - var operand = ExtractArgument(unaryExpression.Operand); - if (operand.Type != unaryExpression.Type) - { - return Expression.Convert(operand, unaryExpression.Type); - } - - if (operand.NodeType == ExpressionType.Call || operand.NodeType == ExpressionType.Invoke || operand.NodeType == ExpressionType.Lambda) - { - return operand; - } - - expression = operand; - continue; - } - - if (unaryExpression.Type != unaryExpression.Operand.Type) - { - return expression; - } - - expression = unaryExpression.Operand; - continue; - - default: - expression = unaryExpression.Operand; - continue; - } - - case LambdaExpression lambda: - return ProcessCallLambda(lambda); - - case MemberExpression memberExpression: - switch (memberExpression.Expression) - { - case ConstantExpression constant: - var constantValue = constant.Value; - var value = constantValue.GetType().GetField(memberExpression.Member.Name)?.GetValue(constantValue); - if (value is ExpressionContainer) return value; - return Expression.Convert(Expression.Constant(value), memberExpression.Type); - - default: - return memberExpression; - } - - default: - return expression; - } - } - } - - internal class BlockBuilder: ExpressionContainer - { - private readonly Type _returnType; - private readonly List _expressions; - private readonly List _parameters; - - public BlockBuilder(Type returnType) : base(null) - { - _returnType = returnType; - _expressions = new List(); - _parameters = new List(); - } - - /// - /// Adds parameter to - /// - /// is not - public BlockBuilder Parameter(Expression expression) - { - if (expression is ParameterExpression parameterExpression) return Parameter(parameterExpression); - - throw new ArgumentException("is not ParameterExpression", nameof(expression)); - } - - /// - /// Adds parameter to with initial assignment - /// - /// is not - public BlockBuilder Parameter(ExpressionContainer expression, ExpressionContainer value) - { - if (!(expression.Expression is ParameterExpression parameterExpression)) - throw new ArgumentException("is not ParameterExpression", nameof(expression)); - - _parameters.Add(parameterExpression); - _expressions.Add(expression.Assign(value)); - return this; - } - - /// - /// Adds parameter to with initial assignment - /// - /// is not - public BlockBuilder Parameter(ExpressionContainer expression, Expression value) - { - if (!(expression.Expression is ParameterExpression parameterExpression)) - throw new ArgumentException("is not ParameterExpression", nameof(expression)); - - _parameters.Add(parameterExpression); - _expressions.Add(expression.Assign(value)); - return this; - } - - /// - /// Adds parameter to - /// - public BlockBuilder Parameter(ParameterExpression e) - { - _parameters.Add(e); - return this; - } - - /// - /// Adds new "line" to - /// - public BlockBuilder Line(Expression e) - { - _expressions.Add(e); - return this; - } - - /// - /// Adds new "line" to - /// - public BlockBuilder Line(ExpressionContainer e) - { - _expressions.Add(e); - return this; - } - - /// - /// Adds multiple new "lines" to - /// - public BlockBuilder Lines(IEnumerable e) - { - _expressions.AddRange(e); - return this; - } - - /// - /// Creates out of current . - /// - public ExpressionContainer Invoke(params ExpressionContainer[] parameters) - { - var lambda = Expression.Lambda(Expression, parameters.Select(o => (ParameterExpression) o.Expression)); - return Arg(Expression.Invoke(lambda)); - } - - public override Expression Expression => - _returnType == null - ? Expression.Block(_parameters, _expressions) - : Expression.Block(_returnType, _parameters, _expressions); - } - } - - internal class ParameterReplacerVisitor : ExpressionVisitor - { - private readonly ICollection _replacements; - private readonly bool _addIfMiss; - - public ParameterReplacerVisitor(ICollection replacements, bool addIfMiss = false) - { - _replacements = replacements; - _addIfMiss = addIfMiss; - } - - protected override Expression VisitParameter(ParameterExpression node) - { - var replacement = _replacements.FirstOrDefault(o => o.Type == node.Type); - if (replacement == null || replacement == node) - { - if (_addIfMiss) - { - _replacements.Add(node); - } - return base.VisitParameter(node); - } - return base.Visit(replacement); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index be6465ee..d6c5ffae 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -6,79 +6,100 @@ 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); - public static Expression Reduce(Expression expression, CompilationContext compilationContext) - { - 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); - 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) { - 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 = Reduce(expression, compilationContext); - return ContextBinder.Bind(expression, compilationContext, parentContext, templatePath); + 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() == false) + { + 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.Compile(); + return ContextBinder.Bind(context, expression, parentContext, templatePath); + } } catch (Exception ex) { throw new HandlebarsCompilerException("An unhandled exception occurred while trying to compile the template", ex); } } - - private static 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..02e0e3df 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -4,64 +4,83 @@ 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(string templatePath, + InternalHandlebarsConfiguration configuration) { var fs = _configuration.FileSystem; - if (fs == null) throw new InvalidOperationException("Cannot compile view when configuration.FileSystem is not set"); + 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; + if (template == null) + throw new InvalidOperationException("Cannot find template at '" + templatePath + "'"); + IEnumerable tokens; using (var sr = new StringReader(template)) { - 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 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(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..656aa5d8 100644 --- a/source/Handlebars/Compiler/HandlebarsCompilerException.cs +++ b/source/Handlebars/Compiler/HandlebarsCompilerException.cs @@ -2,15 +2,48 @@ 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/BlockAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs index a6703271..365991f2 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 114c4586..c9400644 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,13 +68,13 @@ 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); + return item is HelperExpression expression && "#each".Equals(expression.HelperName, StringComparison.OrdinalIgnoreCase); } private static bool IsDeferredBlock(Expression item) { item = UnwrapStatement(item); - return (item is PathExpression) && (((PathExpression)item).Path.StartsWith("#") || ((PathExpression)item).Path.StartsWith("^")); + return item is PathExpression expression && (expression.Path.StartsWith("#") || expression.Path.StartsWith("^")); } private static bool IsPartialBlock (Expression item) diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockHelperAccumulatorContext.cs index f6cb0f20..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() diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs index d16207c4..1f2f691a 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Collections.Generic; using System.Linq; @@ -106,32 +105,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 89c57a23..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; diff --git a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs index 7b721d68..5e841bd4 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/ExpressionScopeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using HandlebarsDotNet.Compiler.Lexer; using System.Linq.Expressions; diff --git a/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs index c447425d..7d75060a 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HashParameterConverter.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq; -using System.Linq.Expressions; - + namespace HandlebarsDotNet.Compiler { internal class HashParameterConverter : TokenConverter @@ -24,19 +23,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 @@ Dictionary AccumulateParameters(IEnumerator enumerat } 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 b1e15dd3..dd0c2e0f 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; @@ -30,6 +29,7 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) var helperArguments = AccumulateArguments(enumerator); yield return HandlebarsExpression.Helper( helper.HelperName, + helper.IsBlock, helperArguments, helper.IsRaw); yield return enumerator.Current; @@ -42,6 +42,7 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) { yield return HandlebarsExpression.Helper( path.Path, + false, helperArguments, ((EndExpressionToken) enumerator.Current)?.IsRaw ?? false); yield return enumerator.Current; diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs index 270a4f75..6a6d8bd2 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperConverter.cs @@ -48,11 +48,20 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) yield return item; continue; case WordExpressionToken word when IsRegisteredHelperName(word.Value): - yield return HandlebarsExpression.Helper(word.Value); + yield return HandlebarsExpression.Helper(word.Value, false, isRaw, word.Context); break; case WordExpressionToken word when IsRegisteredBlockHelperName(word.Value, isRaw): - yield return HandlebarsExpression.Helper(word.Value, isRaw); + { + yield return HandlebarsExpression.Helper(word.Value, true, isRaw, word.Context); break; + } + case WordExpressionToken word when IsUnregisteredBlockHelperName(word.Value, isRaw, sequence): + { + var expression = HandlebarsExpression.Helper(word.Value, true, isRaw, word.Context); + expression.IsBlock = true; + yield return expression; + break; + } default: yield return item; break; @@ -62,15 +71,30 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) private bool IsRegisteredHelperName(string name) { - return _configuration.Helpers.ContainsKey(name) || BuiltInHelpers.Contains(name); + 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("#", ""); + if (!isRaw && !(name[0] == '#' || name[0] == '^')) return false; + name = name + .Replace("#", "") + .Replace("^", ""); + return _configuration.BlockHelpers.ContainsKey(name) || BuiltInHelpers.Contains(name); } + + private bool IsUnregisteredBlockHelperName(string name, bool isRaw, IEnumerable sequence) + { + if (!isRaw && !(name[0] == '#' || name[0] == '^')) return false; + name = name + .Replace("#", "") + .Replace("^", ""); + + 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 e1defc5e..8c66bcd9 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/LiteralConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq.Expressions; using System.Linq; diff --git a/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs index f9ecc2c8..6209165f 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/PathConverter.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; -using System.Linq.Expressions; using System.Linq; namespace HandlebarsDotNet.Compiler diff --git a/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs index 937ced02..11b90ea8 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/RawHelperAccumulator.cs @@ -1,7 +1,6 @@ using HandlebarsDotNet.Compiler.Lexer; using System.Collections.Generic; using System.Linq; -using System.Text; namespace HandlebarsDotNet.Compiler { @@ -86,36 +85,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 dd6e4d47..a93fb2bc 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/StaticConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using HandlebarsDotNet.Compiler.Lexer; using System.Linq; diff --git a/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs index 30b4e02e..e9e1aa00 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using HandlebarsDotNet.Compiler.Lexer; @@ -46,6 +45,7 @@ private static SubExpressionExpression BuildSubExpression(IEnumerator en return HandlebarsExpression.SubExpression( HandlebarsExpression.Helper( path.Path, + false, helperArguments)); } diff --git a/source/Handlebars/Compiler/Lexer/Converter/TokenConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/TokenConverter.cs index a4d90aa0..5c1c67c5 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/TokenConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/TokenConverter.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Lexer; +using System.Collections.Generic; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs b/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs index 24a55282..65d8983a 100644 --- a/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs +++ b/source/Handlebars/Compiler/Lexer/HandlebarsParserException.cs @@ -2,15 +2,48 @@ 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 index cdd08b8a..4f730b9f 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -1,39 +1,42 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - namespace HandlebarsDotNet.Compiler.Lexer { internal class BlockParamsParser : Parser { - public override Token Parse(TextReader reader) + public override Token Parse(ExtendedStringReader reader) { + var context = reader.GetContext(); var buffer = AccumulateWord(reader); return !string.IsNullOrEmpty(buffer) - ? Token.BlockParams(buffer) + ? Token.BlockParams(buffer, context) : null; } - private static string AccumulateWord(TextReader reader) + private static string AccumulateWord(ExtendedStringReader reader) { - var buffer = new StringBuilder(); - - if (reader.Peek() != '|') return null; + var buffer = StringBuilderPool.Shared.GetObject(); + + try + { + if (reader.Peek() != '|') return null; - reader.Read(); + reader.Read(); - while (reader.Peek() != '|') - { - buffer.Append((char) reader.Read()); - } + while (reader.Peek() != '|') + { + buffer.Append((char) reader.Read()); + } - reader.Read(); + reader.Read(); - var accumulateWord = buffer.ToString().Trim(); - if(string.IsNullOrEmpty(accumulateWord)) throw new HandlebarsParserException($"BlockParams expression is not valid"); + var accumulateWord = buffer.ToString().Trim(); + if(string.IsNullOrEmpty(accumulateWord)) throw new HandlebarsParserException($"BlockParams expression is not valid", reader.GetContext()); - return accumulateWord; + return accumulateWord; + } + finally + { + StringBuilderPool.Shared.PutObject(buffer); + } } } } \ 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..0af8e27e 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs @@ -1,57 +1,65 @@ -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())) - { - reader.Read(); - } - while(true) + var buffer = StringBuilderPool.Shared.GetObject(); + try { - var peek = (char)reader.Peek(); - if (peek == '}' || peek == '~' || char.IsWhiteSpace(peek)) - { - break; - } - var node = reader.Read(); - if (node == -1) + 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(); + } + finally + { + StringBuilderPool.Shared.PutObject(buffer); } - return buffer.ToString(); } } } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs index b31e84eb..ee78c861 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs @@ -1,90 +1,92 @@ -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..31c00b30 --- /dev/null +++ b/source/Handlebars/Compiler/Lexer/Parsers/StringBuilderEnumerator.cs @@ -0,0 +1,29 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace HandlebarsDotNet.Compiler.Lexer +{ + internal class StringBuilderEnumerator : IEnumerable + { + private readonly StringBuilder _stringBuilder; + + public StringBuilderEnumerator(StringBuilder stringBuilder) + { + _stringBuilder = stringBuilder; + } + + public IEnumerator GetEnumerator() + { + for (int index = 0; index < _stringBuilder.Length; index++) + { + yield return _stringBuilder[index]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ 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..b7c67263 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs @@ -1,78 +1,109 @@ -using System; -using System.IO; +using System.Collections.Generic; using System.Linq; -using System.Text; namespace HandlebarsDotNet.Compiler.Lexer { internal class WordParser : Parser { - private const string validWordStartCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]"; + private const string ValidWordStartCharactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]"; + private static readonly HashSet ValidWordStartCharacters = new HashSet(); - public override Token Parse(TextReader reader) + 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); + + return Token.Word(buffer, context); } return null; } - private bool IsWord(TextReader reader) + private static bool IsWord(ExtendedStringReader reader) { - var peek = (char)reader.Peek(); - return validWordStartCharacters.Contains(peek.ToString()); + var peek = reader.Peek(); + return ValidWordStartCharacters.Contains((char) peek); } - private string AccumulateWord(TextReader reader) + private static string AccumulateWord(ExtendedStringReader reader) { - StringBuilder buffer = new StringBuilder(); - - var inString = false; + var buffer = StringBuilderPool.Shared.GetObject(); - while (true) + try { - if (!inString) - { - var peek = (char)reader.Peek(); + var inString = false; - if (peek == '}' || peek == '~' || peek == ')' || peek == '=' || (char.IsWhiteSpace(peek) && CanBreakAtSpace(buffer.ToString()))) + while (true) + { + if (!inString) { - break; + var peek = (char) reader.Peek(); + + if (peek == '}' || peek == '~' || peek == ')' || peek == '=' || char.IsWhiteSpace(peek) && CanBreakAtSpace(new StringBuilderEnumerator(buffer))) + { + break; + } } - } - var node = reader.Read(); + var node = reader.Read(); - if (node == -1) - { - throw new InvalidOperationException("Reached end of template before the expression was closed."); - } + if (node == -1) + { + throw new HandlebarsParserException("Reached end of template before the expression was closed.", reader.GetContext()); + } - if (node == '\'' || node == '"') - { - inString = !inString; - } + if (node == '\'' || node == '"') + { + inString = !inString; + } - buffer.Append((char)node); + buffer.Append((char)node); + } + + return buffer.ToString().Trim(); } - - if (buffer.ToString().Contains("[") && !buffer.ToString().Contains("]")) + finally { - throw new HandlebarsCompilerException("Expression is missing a closing ]."); + StringBuilderPool.Shared.PutObject(buffer); } - - return buffer.ToString().Trim(); } - private bool CanBreakAtSpace(string buffer) + private static bool CanBreakAtSpace(IEnumerable buffer) { - var bufferQueryable = buffer.OfType(); - return (!buffer.Contains("[") || (bufferQueryable.Count(x => x == '[') == bufferQueryable.Count(x => x == ']'))); + CalculateBraces(buffer, out var left, out var right); + + return left == 0 || left == right; } + private static void CalculateBraces(IEnumerable buffer, out int left, out int right) + { + var result = buffer.Aggregate(new {Right = 0, Left = 0}, (acc, a) => + { + if (a == ']') + { + return new {Right = acc.Right + 1, acc.Left}; + } + + if (a == '[') + { + return new {acc.Right, Left = acc.Left + 1}; + } + + return acc; + }); + + left = result.Left; + right = result.Right; + } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index 2dafffa8..c86ddae4 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; namespace HandlebarsDotNet.Compiler.Lexer { - internal class Tokenizer + internal static class Tokenizer { - private readonly HandlebarsConfiguration _configuration; - private static readonly Parser WordParser = new WordParser(); private static readonly Parser LiteralParser = new LiteralParser(); private static readonly Parser CommentParser = new CommentParser(); @@ -17,13 +12,8 @@ internal class Tokenizer private static readonly Parser BlockWordParser = new BlockWordParser(); private static readonly Parser BlockParamsParser = new BlockParamsParser(); //TODO: structure parser - - public Tokenizer(HandlebarsConfiguration configuration) - { - _configuration = configuration; - } - - public IEnumerable Tokenize(TextReader source) + + public static IEnumerable Tokenize(ExtendedStringReader source) { try { @@ -35,154 +25,160 @@ public IEnumerable Tokenize(TextReader source) } } - private IEnumerable Parse(TextReader source) + private static IEnumerable Parse(ExtendedStringReader source) { bool inExpression = false; bool trimWhitespace = false; - var buffer = new StringBuilder(); - var node = source.Read(); - while (true) + var buffer = StringBuilderPool.Shared.GetObject(); + try { - if (node == -1) + var node = source.Read(); + while (true) { - if (buffer.Length > 0) + if (node == -1) { - if (inExpression) + if (buffer.Length > 0) { - throw new InvalidOperationException("Reached end of template before expression was closed"); - } - else - { - yield return Token.Static(buffer.ToString()); + if (inExpression) + { + throw new InvalidOperationException("Reached end of template before expression was closed"); + } + else + { + yield return Token.Static(buffer.ToString(), source.GetContext()); + } } + break; } - break; - } - if (inExpression) - { - if ((char)node == '(') + if (inExpression) { - yield return Token.StartSubExpression(); - } + 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); - token = token ?? BlockParamsParser.Parse(source); + var token = WordParser.Parse(source); + token = token ?? LiteralParser.Parse(source); + token = token ?? CommentParser.Parse(source); + token = token ?? PartialParser.Parse(source); + token = token ?? BlockWordParser.Parse(source); + token = token ?? BlockParamsParser.Parse(source); - if (token != null) - { - yield return token; - - if ((char)source.Peek() == '=') + if (token != null) { - source.Read(); - yield return Token.Assignment(); - continue; + yield return token; + + if ((char)source.Peek() == '=') + { + source.Read(); + yield return Token.Assignment(source.GetContext()); + continue; + } } - } - if ((char)node == '}' && (char)source.Read() == '}') - { - bool escaped = true; - bool raw = false; - if ((char)source.Peek() == '}') + 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(); - escaped = false; + yield return Token.EndExpression(escaped, trimWhitespace, raw, source.GetContext()); + inExpression = false; } - if ((char)source.Peek() == '}') + else if ((char)node == ')') { node = source.Read(); - raw = true; + yield return Token.EndSubExpression(source.GetContext()); } - 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) + else if (char.IsWhiteSpace((char)node) || char.IsWhiteSpace((char)source.Peek())) { - - throw new HandlebarsParserException("Reached unparseable token in expression: " + source.ReadLine()); + node = source.Read(); } - 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() == '{') + else if ((char)node == '~') { - source.Read(); - buffer.Append('{', 2); + node = source.Read(); + trimWhitespace = true; } else { - buffer.Append("\\{"); + if (token == null) + { + + throw new HandlebarsParserException("Reached unparseable token in expression: " + source.ReadLine(), source.GetContext()); + } + node = source.Read(); } - node = source.Read(); } - else if ((char)node == '{' && (char)source.Peek() == '{') + else { - bool escaped = true; - bool raw = false; - trimWhitespace = false; - node = source.Read(); - if ((char)source.Peek() == '{') + if ((char)node == '\\' && (char)source.Peek() == '\\') { + source.Read(); + buffer.Append('\\'); node = source.Read(); - escaped = false; } - if ((char)source.Peek() == '{') + else if ((char)node == '\\' && (char)source.Peek() == '{') { + source.Read(); + if ((char)source.Peek() == '{') + { + source.Read(); + buffer.Append('{', 2); + } + else + { + buffer.Append("\\{"); + } node = source.Read(); - raw = true; } - if ((char)source.Peek() == '~') + else if ((char)node == '{' && (char)source.Peek() == '{') { - source.Read(); - node = source.Peek(); - trimWhitespace = true; + 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(), 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(); } - 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(); } } } + finally + { + StringBuilderPool.Shared.PutObject(buffer); + } } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/AssignmentToken.cs index d7255d1e..740a39a9 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 + public AssignmentToken(IReaderContext context) { - get { return TokenType.Assignment; } + Context = context; } + + public IReaderContext Context { get; } - public override string Value - { - get { return "="; } - } + 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 index 7572a499..05bd0a52 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/BlockParameterToken.cs @@ -1,19 +1,16 @@ -using System; - namespace HandlebarsDotNet.Compiler.Lexer { internal class BlockParameterToken : Token { - public BlockParameterToken(string value) + public BlockParameterToken(string value, IReaderContext context = null) { Value = value; + Context = context; } - public override TokenType Type - { - get { return TokenType.BlockParams; } - } + 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/CommentToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/CommentToken.cs index d1c5afbe..257ff187 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/CommentToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/CommentToken.cs @@ -1,6 +1,4 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal class CommentToken : Token { 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/ExpressionScopeToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/ExpressionScopeToken.cs index 43e42399..5494de74 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/ExpressionScopeToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/ExpressionScopeToken.cs @@ -1,6 +1,4 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal abstract class ExpressionScopeToken : Token { diff --git a/source/Handlebars/Compiler/Lexer/Tokens/ExpressionToken.cs b/source/Handlebars/Compiler/Lexer/Tokens/ExpressionToken.cs index 2cd235c4..6cd51d43 100644 --- a/source/Handlebars/Compiler/Lexer/Tokens/ExpressionToken.cs +++ b/source/Handlebars/Compiler/Lexer/Tokens/ExpressionToken.cs @@ -1,6 +1,4 @@ -using System; - -namespace HandlebarsDotNet.Compiler.Lexer +namespace HandlebarsDotNet.Compiler.Lexer { internal abstract class ExpressionToken : Token { 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 56e0b6b3..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,19 +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(); + return new AssignmentToken(context); } - public static BlockParameterToken BlockParams(string blockParams) + public static BlockParameterToken BlockParams(string blockParams, IReaderContext context) { - return new BlockParameterToken(blockParams); + return new BlockParameterToken(blockParams, context); } } } diff --git a/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs b/source/Handlebars/Compiler/Lexer/Tokens/TokenType.cs index 2e93156d..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 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/Resolvers/IExpressionNameResolver.cs b/source/Handlebars/Compiler/Resolvers/IExpressionNameResolver.cs index db785c05..9eddc862 100644 --- a/source/Handlebars/Compiler/Resolvers/IExpressionNameResolver.cs +++ b/source/Handlebars/Compiler/Resolvers/IExpressionNameResolver.cs @@ -1,7 +1,16 @@ namespace HandlebarsDotNet.Compiler.Resolvers { + /// + /// + /// public interface IExpressionNameResolver { + /// + /// + /// + /// + /// + /// string ResolveExpressionName(object instance, string expressionName); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Resolvers/UpperCamelCaseExpressionNameResolver.cs b/source/Handlebars/Compiler/Resolvers/UpperCamelCaseExpressionNameResolver.cs index 76ef7f45..17661df6 100644 --- a/source/Handlebars/Compiler/Resolvers/UpperCamelCaseExpressionNameResolver.cs +++ b/source/Handlebars/Compiler/Resolvers/UpperCamelCaseExpressionNameResolver.cs @@ -2,8 +2,10 @@ namespace HandlebarsDotNet.Compiler.Resolvers { + /// public class UpperCamelCaseExpressionNameResolver : IExpressionNameResolver { + /// public string ResolveExpressionName(object instance, string expressionName) { if (string.IsNullOrEmpty(expressionName)) diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 83703da1..80884371 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -2,71 +2,85 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { - internal class BindingContext + internal sealed class BindingContext : IDisposable { - private readonly List _valueProviders = new List(); + private static readonly BindingContextPool Pool = new BindingContextPool(); - public BindingContext(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : + private readonly HashedCollection _valueProviders = new HashedCollection(); + + public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath, + IDictionary> inlinePartialTemplates) + { + return Pool.CreateContext(configuration, value, writer, parent, templatePath, null, inlinePartialTemplates); + } + + public static BindingContext Create(InternalHandlebarsConfiguration configuration, object value, + EncodedTextWriter writer, BindingContext parent, string templatePath, + Action partialBlockTemplate, + IDictionary> inlinePartialTemplates) + { + return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate, inlinePartialTemplates); + } + + private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : this(configuration, value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } - private BindingContext(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) : + private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) : this(configuration, value, writer, parent, templatePath, partialBlockTemplate, null, inlinePartialTemplates) { } - private BindingContext(HandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) + private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) { - RegisterValueProvider(new BindingContextValueProvider(this)); - - TemplatePath = parent != null ? (parent.TemplatePath ?? templatePath) : templatePath; Configuration = configuration; TextWriter = writer; Value = value; ParentContext = 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 string TemplatePath { get; } + public string TemplatePath { get; private set; } - public HandlebarsConfiguration Configuration { get; } + public InternalHandlebarsConfiguration Configuration { get; private set; } - public EncodedTextWriter TextWriter { get; } + public EncodedTextWriter TextWriter { get; private set; } - public IDictionary> InlinePartialTemplates { get; } + public IDictionary> InlinePartialTemplates { get; private set; } - public Action PartialBlockTemplate { get; } + public Action PartialBlockTemplate { get; private set; } public bool SuppressEncoding { @@ -74,11 +88,11 @@ public bool SuppressEncoding set => TextWriter.SuppressEncoding = value; } - public virtual object Value { get; } + public object Value { get; private set; } - public virtual BindingContext ParentContext { get; } + public BindingContext ParentContext { get; private set; } - public virtual object Root => ParentContext?.Root ?? this; + public object Root { get; private set; } public void RegisterValueProvider(IValueProvider valueProvider) { @@ -86,63 +100,134 @@ public void RegisterValueProvider(IValueProvider valueProvider) _valueProviders.Add(valueProvider); } + + public void UnregisterValueProvider(IValueProvider valueProvider) + { + _valueProviders.Remove(valueProvider); + } - public virtual object GetContextVariable(string variableName) + public bool TryGetContextVariable(ref ChainSegment segment, out object value) { // accessing value providers in reverse order as it gives more probability of hit for (var index = _valueProviders.Count - 1; index >= 0; index--) { - if (_valueProviders[index].TryGetValue(variableName, out var value)) return value; + if (_valueProviders[index].TryGetValue(ref segment, out value)) return true; } - return null; + value = null; + return false; } - public virtual object GetVariable(string variableName) + public bool TryGetVariable(ref ChainSegment segment, out object value, bool searchContext = false) { // 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)) continue; + if(!valueProvider.SupportedValueTypes.HasFlag(ValueTypes.All) && !searchContext) continue; - if (valueProvider.TryGetValue(variableName, out var value)) return value; + if (valueProvider.TryGetValue(ref segment, out value)) return true; } - return ParentContext?.GetVariable(variableName); + value = null; + return ParentContext?.TryGetVariable(ref segment, out value, searchContext) ?? false; } private static IDictionary GetContextDictionary(object target) { - var dict = new Dictionary(); + var contextDictionary = 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(); + switch (target) + { + case null: + return contextDictionary; + + case IDictionary dictionary: + { + foreach (var item in dictionary) + { + contextDictionary[item.Key] = item.Value; + } - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - foreach (var field in fields) { - dict[field.Name] = field.GetValue(target); + break; } + default: + { + var type = target.GetType(); + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + contextDictionary[field.Name] = field.GetValue(target); + } - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - foreach (var property in properties) { - if (property.GetIndexParameters().Length == 0) - dict[property.Name] = property.GetValue(target); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (var property in properties) + { + if (property.GetIndexParameters().Length == 0) + { + contextDictionary[property.Name] = property.GetValue(target); + } + } + + break; } } - return dict; + return contextDictionary; } - public virtual BindingContext CreateChildContext(object value, Action partialBlockTemplate) + public BindingContext CreateChildContext(object value, Action partialBlockTemplate = null) + { + return Create(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); + } + + public void Dispose() + { + Pool.PutObject(this); + } + + private class BindingContextPool : ObjectPool { - return new BindingContext(Configuration, value ?? Value, TextWriter, this, TemplatePath, partialBlockTemplate ?? PartialBlockTemplate, null); + public BindingContext CreateContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) + { + var context = GetObject(); + 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; + } + + protected override BindingContext CreateObject() + { + return new BindingContext(null, null, null, null, null, null); + } + + public override void PutObject(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]); + } + + base.PutObject(item); + } } } } diff --git a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs b/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs deleted file mode 100644 index 71e68199..00000000 --- a/source/Handlebars/Compiler/Structure/BindingContextValueProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; - -namespace HandlebarsDotNet.Compiler -{ - internal class BindingContextValueProvider : IValueProvider - { - private readonly BindingContext _context; - - public BindingContextValueProvider(BindingContext context) - { - _context = context; - } - - public ValueTypes SupportedValueTypes { get; } = ValueTypes.Context; - - public bool TryGetValue(string memberName, out object value) - { - value = GetContextVariable(memberName, _context) ?? GetContextVariable(memberName, _context.Value); - return value != null; - } - - private object GetContextVariable(string variableName, object target) - { - variableName = variableName.TrimStart('@'); - var member = target.GetType().GetMember(variableName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (member.Length <= 0) return _context.ParentContext?.GetContextVariable(variableName); - - switch (member[0]) - { - case PropertyInfo propertyInfo: - return propertyInfo.GetValue(target, null); - case FieldInfo fieldInfo: - return fieldInfo.GetValue(target); - default: - throw new HandlebarsRuntimeException("Context variable references a member that is not a field or property"); - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs index 40dccdc4..c50b31d4 100644 --- a/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockHelperExpression.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Collections.Generic; -using System.Linq; namespace HandlebarsDotNet.Compiler { @@ -24,18 +22,19 @@ public BlockHelperExpression( Expression body, Expression inversion, bool isRaw = false) - : base(helperName, arguments, isRaw) + : base(helperName, true, arguments, isRaw) { Body = body; Inversion = inversion; BlockParams = blockParams; + IsBlock = true; } public Expression Body { get; } public Expression Inversion { get; } - public BlockParamsExpression BlockParams { 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 index 5401d177..a0a3f5a9 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsExpression.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler @@ -31,7 +29,7 @@ public BlockParamsExpression(string action, string blockParams) protected override Expression Accept(ExpressionVisitor visitor) { - return visitor.Visit(Expression.Convert(Constant(_blockParam), typeof(BlockParam))); + return visitor.Visit(Constant(_blockParam, typeof(BlockParam))); } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs index 68f18d91..86273981 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -1,35 +1,51 @@ using System; using System.Collections.Generic; -using System.Linq; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { - /// + /// + /// Configures BlockParameters for current BlockHelper + /// /// Parameters passed to BlockParams. /// Function that perform binding of parameter to . - public delegate void ConfigureBlockParams(string[] parameters, ValueBinder valueBinder); + /// 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. - public delegate void ValueBinder(string variableName, Func valueProvider); + /// Context for the binding. + public delegate void ValueBinder(string variableName, Func valueProvider, object context = null); /// internal class BlockParamsValueProvider : IValueProvider { - private readonly BlockParam _params; - private readonly Action> _invoker; - private readonly Dictionary> _accessors; + 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 BlockParamsValueProvider(BindingContext context, BlockParam @params) + public static BlockParamsValueProvider Create(BindingContext context, object @params) { - _params = @params; - _invoker = action => action(context); - _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - context.RegisterValueProvider(this); + var blockParamsValueProvider = Pool.GetObject(); + + 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; @@ -37,33 +53,54 @@ public BlockParamsValueProvider(BindingContext context, BlockParam @params) /// /// Configures behavior of BlockParams. /// - public void Configure(ConfigureBlockParams blockParamsConfiguration) + public void Configure(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies) { - if(_params == null) return; - + var parameters = _params?.Parameters ?? EmptyParameters; void BlockParamsAction(BindingContext context) { - void ValueBinder(string name, Func value) + void ValueBinder(string name, Func value, object ctx) { - if (!string.IsNullOrEmpty(name)) _accessors[name] = value; + if (!string.IsNullOrEmpty(name)) _accessors[name] = new KeyValuePair>(ctx, value); } - blockParamsConfiguration.Invoke(_params.Parameters, ValueBinder); + blockParamsConfiguration.Invoke(parameters, ValueBinder, dependencies); } _invoker(BlockParamsAction); } - public bool TryGetValue(string param, out object value) + public bool TryGetValue(ref ChainSegment segment, out object value) { - if (_accessors.TryGetValue(param, out var valueProvider)) + if (_accessors.TryGetValue(segment.LowerInvariant, out var provider)) { - value = valueProvider(); + value = provider.Value(provider.Key); return true; } value = null; return false; } + + public void Dispose() + { + Pool.PutObject(this); + } + + private class BlockParamsValueProviderPool : ObjectPool + { + protected override BlockParamsValueProvider CreateObject() + { + return new BlockParamsValueProvider(); + } + + public override void PutObject(BlockParamsValueProvider item) + { + item._accessors.Clear(); + item._invoker = null; + item._params = null; + + base.PutObject(item); + } + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BoolishExpression.cs b/source/Handlebars/Compiler/Structure/BoolishExpression.cs index 7c3489ad..285211a5 100644 --- a/source/Handlebars/Compiler/Structure/BoolishExpression.cs +++ b/source/Handlebars/Compiler/Structure/BoolishExpression.cs @@ -10,7 +10,7 @@ public BoolishExpression(Expression condition) Condition = condition; } - public Expression Condition { get; } + public new Expression Condition { get; } public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.BoolishExpression; diff --git a/source/Handlebars/Compiler/Structure/CommentExpression.cs b/source/Handlebars/Compiler/Structure/CommentExpression.cs index 8d41068f..97aed36d 100644 --- a/source/Handlebars/Compiler/Structure/CommentExpression.cs +++ b/source/Handlebars/Compiler/Structure/CommentExpression.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { @@ -14,6 +13,6 @@ public CommentExpression(string value) public override ExpressionType NodeType => (ExpressionType) HandlebarsExpressionType.CommentExpression; - public override Type Type => typeof (void); + //public override Type Type => typeof (void); } } \ 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 e8a22926..00000000 --- a/source/Handlebars/Compiler/Structure/DeferredSectionExpression.cs +++ /dev/null @@ -1,29 +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; } - - public BlockExpression Inversion { get; } - - public PathExpression Path { get; } - - public override Type Type => typeof(void); - - public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.DeferredSection; - } -} - diff --git a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs index 01f89456..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,7 +12,6 @@ internal enum HandlebarsExpressionType HelperExpression = 6003, PathExpression = 6004, IteratorExpression = 6005, - DeferredSection = 6006, PartialExpression = 6007, BoolishExpression = 6008, SubExpression = 6009, @@ -23,14 +23,18 @@ internal enum HandlebarsExpressionType 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( @@ -81,14 +85,6 @@ public static IteratorExpression Iterator( return new IteratorExpression(sequence, blockParams, template, ifEmpty); } - public static DeferredSectionExpression DeferredSection( - PathExpression path, - BlockExpression body, - BlockExpression inversion) - { - return new DeferredSectionExpression(path, body, inversion); - } - public static PartialExpression Partial(Expression partialName) { return Partial(partialName, null); diff --git a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs index 8e6e6eda..47db24ca 100644 --- a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs +++ b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs @@ -1,4 +1,3 @@ -using System; using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler @@ -14,7 +13,7 @@ public HashParameterAssignmentExpression(string name) public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HashParameterAssignmentExpression; - public override Type Type => typeof(object); + //public override Type Type => typeof(object); } } diff --git a/source/Handlebars/Compiler/Structure/HashParametersExpression.cs b/source/Handlebars/Compiler/Structure/HashParametersExpression.cs index 243ec0fe..bb4aab1e 100644 --- a/source/Handlebars/Compiler/Structure/HashParametersExpression.cs +++ b/source/Handlebars/Compiler/Structure/HashParametersExpression.cs @@ -15,7 +15,7 @@ public HashParametersExpression(Dictionary parameters) public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HashParametersExpression; - public override Type Type => 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 05815796..b18423bb 100644 --- a/source/Handlebars/Compiler/Structure/HelperExpression.cs +++ b/source/Handlebars/Compiler/Structure/HelperExpression.cs @@ -7,17 +7,21 @@ namespace HandlebarsDotNet.Compiler { internal class HelperExpression : HandlebarsExpression { - 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; + 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(); + Context = context; + IsBlock = isBlock; } public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HelperExpression; @@ -28,7 +32,10 @@ public HelperExpression(string helperName, bool isRaw = false) public bool IsRaw { get; } + public bool IsBlock { get; set; } + public IEnumerable Arguments { get; } + public IReaderContext Context { get; } } } diff --git a/source/Handlebars/Compiler/Structure/IValueProvider.cs b/source/Handlebars/Compiler/Structure/IValueProvider.cs deleted file mode 100644 index 5e5062ce..00000000 --- a/source/Handlebars/Compiler/Structure/IValueProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace HandlebarsDotNet.Compiler -{ - [Flags] - internal enum ValueTypes - { - Context = 1, - All = 2 - } - - internal interface IValueProvider - { - ValueTypes SupportedValueTypes { get; } - bool TryGetValue(string memberName, out object value); - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index e914d428..ed975f73 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; diff --git a/source/Handlebars/Compiler/Structure/PartialExpression.cs b/source/Handlebars/Compiler/Structure/PartialExpression.cs index 8b855a2a..bc210d61 100644 --- a/source/Handlebars/Compiler/Structure/PartialExpression.cs +++ b/source/Handlebars/Compiler/Structure/PartialExpression.cs @@ -1,6 +1,4 @@ -using System; -using HandlebarsDotNet.Compiler; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { @@ -15,7 +13,7 @@ public PartialExpression(Expression partialName, Expression argument, Expression public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PartialExpression; - public override Type Type => typeof(void); + //public override Type Type => typeof(void); public Expression PartialName { 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..e8adb102 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + [DebuggerDisplay("{Value}")] + internal struct ChainSegment + { + private static readonly RefLookup ChainSegments = new RefLookup(); + + public static ref ChainSegment Create(string value) + { + if (ChainSegments.ContainsKey(value)) + { + return ref ChainSegments.GetValueOrDefault(value); + } + + return ref ChainSegments.GetOrAdd(value, (string key, ref ChainSegment segment) => + { + segment.IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); + segment.Value = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); + segment.IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"); + segment.TrimmedValue = TrimSquareBrackets(segment.Value).Intern(); + segment.LowerInvariant = segment.TrimmedValue.ToLowerInvariant().Intern(); + + return ref segment; + }); + } + + public string Value { get; private set; } + public string LowerInvariant { get; private set; } + public string TrimmedValue { get; private set; } + public bool IsVariable { get; private set; } + public bool IsThis { get; private set; } + + private 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..74c81171 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + [DebuggerDisplay("{Path}")] + internal struct PathInfo + { + public PathInfo(bool hasValue, string path, IEnumerable segments) + { + HasValue = hasValue; + Path = path; + IsVariable = path.StartsWith("@"); + IsInversion = path.StartsWith("^"); + IsHelper = path.StartsWith("#"); + Segments = segments?.ToArray(); + } + + public bool IsHelper { get; } + public bool IsInversion { get; } + public bool HasValue { get; } + public string Path { get; } + public bool IsVariable { get; } + public PathSegment[] Segments { get; } + + public bool Equals(PathInfo other) + { + return IsHelper == other.IsHelper && + IsInversion == other.IsInversion && + HasValue == other.HasValue && IsVariable == other.IsVariable && + Path == other.Path; + } + + public override bool Equals(object obj) + { + return obj is PathInfo other && Equals(other); + } + + public override int GetHashCode() + { + return Path.GetHashCode(); + } + + public static implicit operator string(PathInfo pathInfo) + { + return 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..66d8aae1 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.Linq; +using HandlebarsDotNet.Polyfills; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + internal static class PathResolver + { + public static PathInfo GetPathInfo(string path) + { + if (path == "null") + return new PathInfo(false, path, null); + + var originalPath = path; + + var isVariable = path.StartsWith("@"); + var isInversion = path.StartsWith("^"); + var isHelper = path.StartsWith("#"); + if (isVariable || isHelper || isInversion) + { + path = path.Substring(1); + } + + var segments = new List(); + foreach (var segment in path.Split('/')) + { + if (segment == "..") + { + segments.Add(new PathSegment(segment, Enumerable.Empty(), true)); + continue; + } + + var segmentString = isVariable ? "@" + segment : segment; + segments.Add(new PathSegment(segmentString, GetPathChain(segmentString), false)); + + } + + return new PathInfo(true, originalPath, segments); + } + + + //TODO: make path resolution logic smarter + public static object ResolvePath(BindingContext context, ref PathInfo pathInfo) + { + if (!pathInfo.HasValue) + return null; + + var configuration = context.Configuration; + var containsVariable = pathInfo.IsVariable; + var instance = context.Value; + var hashParameters = instance as HashParameterDictionary; + + for (var segmentIndex = 0; segmentIndex < pathInfo.Segments.Length; segmentIndex++) + { + ref var segment = ref pathInfo.Segments[segmentIndex]; + if (segment.IsJumpUp) + { + 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; + continue; + } + + for (var pathChainIndex = 0; pathChainIndex < segment.PathChain.Length; pathChainIndex++) + { + ref var chainSegment = ref segment.PathChain[pathChainIndex]; + instance = ResolveValue(context, instance, ref chainSegment); + + if (!(instance is UndefinedBindingResult)) + continue; + + if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || + context.ParentContext == null) + { + if (configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo.Path, + (instance as UndefinedBindingResult).Value); + return instance; + } + + instance = ResolveValue(context.ParentContext, context.ParentContext.Value, ref chainSegment); + if (!(instance is UndefinedBindingResult result)) continue; + + if (configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo.Path, result.Value); + return result; + } + } + + return instance; + } + + private static IEnumerable GetPathChain(string segmentString) + { + var insideEscapeBlock = false; + var pathChain = segmentString.Split('.') + .Aggregate(new List(), (list, next) => + { + if (insideEscapeBlock) + { + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } + + list[list.Count - 1] = ChainSegment.Create($"{list[list.Count - 1].Value}.{next}"); + return list; + } + + if (next.StartsWith("[")) + { + insideEscapeBlock = true; + } + + if (next.EndsWith("]")) + { + insideEscapeBlock = false; + } + + list.Add(ChainSegment.Create(next)); + return list; + }); + + return pathChain; + } + + private static object ResolveValue(BindingContext context, object instance, ref ChainSegment chainSegment) + { + var configuration = context.Configuration; + object resolvedValue; + if (chainSegment.IsVariable) + { + return context.TryGetContextVariable(ref chainSegment, out resolvedValue) ? resolvedValue : new UndefinedBindingResult(chainSegment.Value, configuration); + } + + if (chainSegment.IsThis) return instance; + + if (TryAccessMember(instance, ref chainSegment, 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.Value, configuration); + } + + public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, InternalHandlebarsConfiguration configuration, out object value) + { + var memberName = chainSegment.Value; + if (instance == null) + { + value = new UndefinedBindingResult(memberName, configuration); + return false; + } + + var instanceType = instance.GetType(); + memberName = ResolveMemberName(instance, memberName, configuration); + memberName = ReferenceEquals(memberName, chainSegment.Value) + ? chainSegment.TrimmedValue + : TrimSquareBrackets(memberName).Intern(); + + var descriptorProvider = configuration.ObjectDescriptorProvider; + if (!descriptorProvider.CanHandleType(instanceType)) + { + value = new UndefinedBindingResult(memberName, configuration); + return false; + } + + if (!descriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) + { + value = new UndefinedBindingResult(memberName, configuration); + return false; + } + + return descriptor.MemberAccessor.TryGetValue(instance, instanceType, memberName, out value); + } + + private 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; + } + + private static string ResolveMemberName(object instance, string memberName, HandlebarsConfiguration configuration) + { + var resolver = configuration.ExpressionNameResolver; + return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; + } + } +} \ 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..ed238625 --- /dev/null +++ b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace HandlebarsDotNet.Compiler.Structure.Path +{ + [DebuggerDisplay("{Segment}")] + internal struct PathSegment + { + public PathSegment(string segment, IEnumerable chain, bool isJumpUp) + { + Segment = segment; + IsJumpUp = isJumpUp; + PathChain = chain.ToArray(); + } + + public string Segment { get; } + + public bool IsJumpUp { get; } + + public ChainSegment[] PathChain { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/PathExpression.cs b/source/Handlebars/Compiler/Structure/PathExpression.cs index 537db589..dffed456 100644 --- a/source/Handlebars/Compiler/Structure/PathExpression.cs +++ b/source/Handlebars/Compiler/Structure/PathExpression.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { @@ -11,13 +12,13 @@ public PathExpression(string path) PathInfo = PathResolver.GetPathInfo(path); } - public string Path { get; } + public new string Path { get; } public PathInfo PathInfo { get; } public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.PathExpression; - public override Type Type => typeof(object); + public override Type Type => typeof(PathInfo); } } diff --git a/source/Handlebars/Compiler/Structure/PathResolver.cs b/source/Handlebars/Compiler/Structure/PathResolver.cs deleted file mode 100644 index 5f18309c..00000000 --- a/source/Handlebars/Compiler/Structure/PathResolver.cs +++ /dev/null @@ -1,418 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Dynamic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace HandlebarsDotNet.Compiler -{ - [DebuggerDisplay("{Path}")] - internal class PathInfo - { - public bool HasValue { get; set; } - public string Path { get; set; } - public bool IsVariable { get; set; } - public IList Segments { get; set; } = new List(); - } - - [DebuggerDisplay("{Segment}")] - internal class PathSegment - { - public string Segment { get; } - - public PathSegment(string segment, IEnumerable chain) - { - Segment = segment; - PathChain = chain.ToArray(); - } - - public bool IsSwitch { get; set; } - - public ChainSegment[] PathChain { get; } - } - - [DebuggerDisplay("{Value}")] - internal class ChainSegment - { - public ChainSegment(string value) - { - Value = value; - IsVariable = value.StartsWith("@"); - IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); - TrimmedValue = TrimSquareBrackets(value); - } - - public string Value { get; } - public string TrimmedValue { get; } - public bool IsVariable { get; } - public bool IsThis { get; } - - private 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; - } - } - - internal class PathResolver - { - private static readonly Regex IndexRegex = new Regex(@"^\[?(?\d+)\]?$", RegexOptions.Compiled); - - public static PathInfo GetPathInfo(string path) - { - if (path == "null") - return new PathInfo(); - - var pathInfo = new PathInfo - { - HasValue = true, - Path = path, - IsVariable = path.StartsWith("@") - }; - - if (pathInfo.IsVariable) - { - path = path.Substring(1); - } - - foreach (var segment in path.Split('/')) - { - if (segment == "..") - { - pathInfo.Segments.Add(new PathSegment(segment, Enumerable.Empty()) - { - IsSwitch = true - }); - continue; - } - - var segmentString = pathInfo.IsVariable ? "@" + segment : segment; - pathInfo.Segments.Add(new PathSegment(segmentString, GetPathChain(segmentString))); - - } - - return pathInfo; - } - - - //TODO: make path resolution logic smarter - public object ResolvePath(BindingContext context, PathInfo pathInfo) - { - if (!pathInfo.HasValue) - return null; - - var configuration = context.Configuration; - var containsVariable = pathInfo.IsVariable; - var instance = context.Value; - var hashParameters = instance as HashParameterDictionary; - - foreach (var segment in pathInfo.Segments) - { - if (segment.IsSwitch) - { - 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 - { - foreach (var chainSegment in segment.PathChain) - { - instance = ResolveValue(context, instance, chainSegment); - - if (!(instance is UndefinedBindingResult)) - continue; - - if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || context.ParentContext == null) - { - if (configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, (instance as UndefinedBindingResult).Value); - return instance; - } - - instance = ResolveValue(context.ParentContext, context.ParentContext.Value, chainSegment); - if (!(instance is UndefinedBindingResult result)) continue; - - if (configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, result.Value); - return result; - } - } - } - return instance; - } - - private static IEnumerable GetPathChain(string segmentString) - { - var insideEscapeBlock = false; - var pathChain = segmentString.Split('.') - .Aggregate(new List(), (list, next) => - { - if (insideEscapeBlock) - { - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].Value}.{next}"); - return list; - } - - if (next.StartsWith("[")) - { - insideEscapeBlock = true; - } - - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - - list.Add(new ChainSegment(next)); - return list; - }); - - return pathChain; - } - - private object ResolveValue(BindingContext context, object instance, ChainSegment chainSegment) - { - var configuration = context.Configuration; - var segment = chainSegment.Value; - object undefined = new UndefinedBindingResult(segment, configuration); - object resolvedValue = undefined; - if (chainSegment.IsVariable) - { - var contextValue = context.GetContextVariable(segment); - if (contextValue != null) - { - resolvedValue = contextValue; - } - } - else if (chainSegment.IsThis) - { - resolvedValue = instance; - } - else - { - if (!TryAccessMember(instance, chainSegment, configuration, out resolvedValue)) - { - resolvedValue = context.GetVariable(segment) ?? undefined; - } - } - return resolvedValue; - } - - public bool TryAccessMember(object instance, ChainSegment chainSegment, HandlebarsConfiguration configuration, out object value) - { - var memberName = chainSegment.Value; - value = new UndefinedBindingResult(memberName, configuration); - if (instance == null) - return false; - - var instanceType = instance.GetType(); - memberName = ResolveMemberName(instance, memberName, configuration); - memberName = ReferenceEquals(memberName, chainSegment.Value) - ? chainSegment.TrimmedValue - : TrimSquareBrackets(memberName); - - return TryAccessContextMember(instance, memberName, out value) - || TryAccessStringIndexerMember(instance, memberName, instanceType, out value) - || TryAccessIEnumerableMember(instance, memberName, out value) - || TryAccessGetValueMethod(instance, memberName, instanceType, out value) - || TryAccessDynamicMember(instance, memberName, out value) - || TryAccessIDictionaryMember(instance, memberName, out value) - || TryAccessMemberWithReflection(instance, memberName, instanceType, out value); - } - - private static bool TryAccessContextMember(object instance, string memberName, out object value) - { - value = null; - if (!(instance is BindingContext context)) return false; - - value = context.GetContextVariable(memberName); - return value != null; - } - - private static bool TryAccessStringIndexerMember(object instance, string memberName, Type instanceType, out object value) - { - value = null; - var stringIndexPropertyGetter = GetStringIndexPropertyGetter(instanceType); - if (stringIndexPropertyGetter == null) return false; - - try - { - value = stringIndexPropertyGetter.Invoke(instance, new object[] {memberName}); - return true; - } - catch - { - return false; - } - } - - private static bool TryAccessIEnumerableMember(object instance, string memberName, out object value) - { - value = null; - if (!(instance is IEnumerable enumerable)) return false; - - var match = IndexRegex.Match(memberName); - if (!match.Success) return false; - if (!match.Groups["index"].Success || !int.TryParse(match.Groups["index"].Value, out var index)) return false; - - value = enumerable.ElementAtOrDefault(index); - return true; - } - - private static bool TryAccessMemberWithReflection(object instance, string memberName, Type instanceType, out object value) - { - var typeDescriptor = TypeDescriptors.Provider.GetObjectTypeDescriptor(instanceType); - if(typeDescriptor.Accessors.TryGetValue(memberName, out var accessor)) - { - value = accessor(instance); - return true; - } - - value = null; - return false; - } - - private static bool TryAccessIDictionaryMember(object instance, 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. - if (!(instance is IDictionary dictionary)) return false; - { - value = dictionary[memberName]; - return true; - } - } - - private static bool TryAccessGetValueMethod(object instance, string memberName, Type instanceType, out object value) - { - value = null; - // Check if the instance is has TryGetValue method - var tryGetValueMethod = GetTryGetValueMethod(instanceType); - - if (tryGetValueMethod == null) return false; - - object key = memberName; - - // Dictionary key type isn't a string, so attempt to convert. - var keyType = tryGetValueMethod.GetParameters()[0].ParameterType; - if (keyType != typeof(string)) - { - if (!typeof(IConvertible).IsAssignableFrom(keyType)) - { - value = null; - return false; - } - - key = Convert.ChangeType(memberName, keyType); - } - - var methodParameters = new[] { key, null }; - var result = (bool) tryGetValueMethod.Invoke(instance, methodParameters); - if (!result) return true; - - value = methodParameters[1]; - return true; - } - - private static bool TryAccessDynamicMember(object instance, string memberName, out object value) - { - value = null; - //crude handling for dynamic objects that don't have metadata - if (!(instance is IDynamicMetaObjectProvider metaObjectProvider)) return false; - - try - { - value = GetProperty(metaObjectProvider, memberName); - return value != null; - } - catch - { - return false; - } - } - - private 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; - } - - private static MethodInfo GetTryGetValueMethod(Type type) - { - return type.GetMethods() - .Where(o => o.Name == nameof(IDictionary.TryGetValue)) - .Where(o => - { - var parameters = o.GetParameters(); - return parameters.Length == 2 && parameters[1].IsOut && o.ReturnType == typeof(bool); - }) - .SingleOrDefault(); - } - - private static MethodInfo GetStringIndexPropertyGetter(Type type) - { - return type - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(prop => prop.Name == "Item" && prop.CanRead) - .SingleOrDefault(prop => - { - var indexParams = prop.GetIndexParameters(); - return indexParams.Length == 1 && indexParams.Single().ParameterType == typeof(string); - })?.GetMethod; - } - - private static MemberInfo GetMember(string memberName, Type instanceType) - { - var members = instanceType.GetMember(memberName, - BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - - if (members.Length == 0) return null; - - var preferredMember = members.Length > 1 - ? members.FirstOrDefault(m => m.Name == memberName) ?? members[0] - : members[0]; - - return preferredMember; - } - - 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 static string ResolveMemberName(object instance, string memberName, HandlebarsConfiguration configuration) - { - var resolver = configuration.ExpressionNameResolver; - return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/StatementExpression.cs b/source/Handlebars/Compiler/Structure/StatementExpression.cs index 04a6b0b0..4f4fc185 100644 --- a/source/Handlebars/Compiler/Structure/StatementExpression.cs +++ b/source/Handlebars/Compiler/Structure/StatementExpression.cs @@ -23,6 +23,6 @@ public StatementExpression(Expression body, bool isEscaped, bool trimBefore, boo public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.StatementExpression; - public override Type Type => typeof(void); + public override Type Type => Body.Type; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/StaticExpression.cs b/source/Handlebars/Compiler/Structure/StaticExpression.cs index e4a9a3c5..a01556db 100644 --- a/source/Handlebars/Compiler/Structure/StaticExpression.cs +++ b/source/Handlebars/Compiler/Structure/StaticExpression.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace HandlebarsDotNet.Compiler { @@ -12,9 +11,10 @@ public StaticExpression(string value) public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.StaticExpression; - public override Type Type => typeof(void); + //public override Type Type => typeof(void); public string Value { get; } + } } diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index 42de7b06..12914168 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; namespace HandlebarsDotNet.Compiler { @@ -15,7 +14,7 @@ public UndefinedBindingResult(string value, HandlebarsConfiguration configuratio _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 9ed620e8..141c8772 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,19 +1,22 @@ -using System.Linq.Expressions; +using System; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using Expressions.Shortcuts; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { internal class BlockHelperFunctionBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new BlockHelperFunctionBinder(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private BlockHelperFunctionBinder(CompilationContext context) - : base(context) + public BlockHelperFunctionBinder(CompilationContext compilationContext) { + CompilationContext = compilationContext; } - + protected override Expression VisitStatementExpression(StatementExpression sex) { return sex.Body is BlockHelperExpression ? Visit(sex.Body) : sex; @@ -22,29 +25,140 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) { var isInlinePartial = bhex.HelperName == "#*inline"; - - var fb = new FunctionBuilder(CompilationContext.Configuration); - + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var bindingContext = isInlinePartial ? context.Cast() : context.Property(o => o.Value); - var blockParamsExpression = ExpressionShortcuts.New( - () => new BlockParamsValueProvider(context, ExpressionShortcuts.Arg(bhex.BlockParams)) - ); + var readerContext = ExpressionShortcuts.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 = bhex.HelperName.TrimStart('#', '^'); + var textWriter = context.Property(o => o.TextWriter); + var arguments = ExpressionShortcuts.Array(bhex.Arguments.Select(o => FunctionBuilder.Reduce(o, CompilationContext))); + var configuration = ExpressionShortcuts.Arg(CompilationContext.Configuration); - var body = fb.Compile(((BlockExpression) bhex.Body).Expressions, context); - var inversion = fb.Compile(((BlockExpression) bhex.Inversion).Expressions, context); - var helper = CompilationContext.Configuration.BlockHelpers[bhex.HelperName.Replace("#", "")]; + var reducerNew = ExpressionShortcuts.New(() => new LambdaReducer(context, body, inverse)); + var reducer = ExpressionShortcuts.Var(); + + var blockParamsProvider = ExpressionShortcuts.Var(); + var blockParamsExpression = ExpressionShortcuts.Call( + () => BlockParamsValueProvider.Create(context, ExpressionShortcuts.Arg(bhex.BlockParams)) + ); var helperOptions = ExpressionShortcuts.New( - () => new HelperOptions(ExpressionShortcuts.Arg(body), ExpressionShortcuts.Arg(inversion), blockParamsExpression) + () => new HelperOptions( + reducer.Property(o => o.Direct), + reducer.Property(o => o.Inverse), + blockParamsProvider, + configuration) ); + + var blockHelpers = CompilationContext.Configuration.BlockHelpers; + if (blockHelpers.TryGetValue(helperName, out var helper)) + { + return ExpressionShortcuts.Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(ExpressionShortcuts.Try() + .Body(ExpressionShortcuts.Call( + () => helper(textWriter, helperOptions, bindingContext, arguments) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); + } + + foreach (var resolver in CompilationContext.Configuration.HelperResolvers) + { + if (resolver.TryResolveBlockHelper(helperName, out helper)) + return ExpressionShortcuts.Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(ExpressionShortcuts.Try() + .Body(ExpressionShortcuts.Call( + () => helper(textWriter, helperOptions, bindingContext, arguments) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); + } - return ExpressionShortcuts.Call( - () => helper(context.Property(o => o.TextWriter), helperOptions, bindingContext, ExpressionShortcuts.Array(bhex.Arguments)) - ); + var helperPrefix = bhex.HelperName[0]; + return ExpressionShortcuts.Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(ExpressionShortcuts.Try() + .Body(ExpressionShortcuts.Call( + () => LateBoundCall( + helperName, + helperPrefix, + context, + (IReaderContext) readerContext, + textWriter, helperOptions, + body, + inverse, + bindingContext, + self, + arguments + ) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); + } + + 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 + { + 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; + } + + bindingContext.TryGetContextVariable(ref ChainSegment.Create(helperName), out var value); + DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); + } + catch(Exception e) + { + 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 d5ba2ae1..a00ccb0b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -1,22 +1,23 @@ using System.Linq.Expressions; +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 ExpressionShortcuts.Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(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 3b805ed2..dced784e 100644 --- a/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/ContextBinder.cs @@ -2,36 +2,98 @@ using System.Collections.Generic; using System.IO; using System.Linq.Expressions; +using Expressions.Shortcuts; 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 configuration = context.Configuration; + var configuration = ExpressionShortcuts.Arg(context.Configuration); var writerParameter = ExpressionShortcuts.Parameter("buffer"); var objectParameter = ExpressionShortcuts.Parameter("data"); var bindingContext = ExpressionShortcuts.Arg(context.BindingContext); var inlinePartialsParameter = ExpressionShortcuts.Null>>(); - var encodedWriterExpression = ExpressionShortcuts.Call(() => EncodedTextWriter.From(ExpressionShortcuts.Arg(writerParameter), context.Configuration.TextEncoder)); - var newBindingContext = ExpressionShortcuts.New( - () => new BindingContext(configuration, objectParameter, encodedWriterExpression, ExpressionShortcuts.Arg(parentContext), templatePath, (IDictionary>) inlinePartialsParameter) + var textEncoder = configuration.Property(o => o.TextEncoder); + var encodedWriterExpression = ExpressionShortcuts.Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); + var parentContextArg = ExpressionShortcuts.Arg(parentContext); + + var newBindingContext = ExpressionShortcuts.Call( + () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) ); - var blockBuilder = ExpressionShortcuts.Block() + var shouldDispose = ExpressionShortcuts.Var("shouldDispose"); + + Expression blockBuilder = ExpressionShortcuts.Block() .Parameter(bindingContext) - .Line(bindingContext.TernaryAssign(objectParameter.Is(), objectParameter.As(), newBindingContext)) - .Lines(((BlockExpression) body).Expressions); - + .Parameter(shouldDispose) + .Line(ExpressionShortcuts.Condition() + .If(objectParameter.Is(), + bindingContext.Assign(objectParameter.As()) + ) + .Else(block => + { + block.Line(shouldDispose.Assign(true)); + block.Line(bindingContext.Assign(newBindingContext)); + }) + ) + .Line(ExpressionShortcuts.Try() + .Body(block => block.Lines(((BlockExpression) body).Expressions)) + .Finally(ExpressionShortcuts.Condition() + .If(shouldDispose, bindingContext.Call(o => o.Dispose())) + ) + ); + return Expression.Lambda>(blockBuilder, (ParameterExpression) writerParameter.Expression, (ParameterExpression) objectParameter.Expression); } + + public static Expression> Bind(CompilationContext context, Expression body, string templatePath) + { + var configuration = ExpressionShortcuts.Arg(context.Configuration); + + var writerParameter = ExpressionShortcuts.Parameter("buffer"); + var objectParameter = ExpressionShortcuts.Parameter("data"); + + var bindingContext = ExpressionShortcuts.Arg(context.BindingContext); + var inlinePartialsParameter = ExpressionShortcuts.Null>>(); + var textEncoder = configuration.Property(o => o.TextEncoder); + var encodedWriterExpression = ExpressionShortcuts.Call(() => EncodedTextWriter.From(writerParameter, (ITextEncoder) textEncoder)); + var parentContextArg = ExpressionShortcuts.Var("parentContext"); + + var newBindingContext = ExpressionShortcuts.Call( + () => BindingContext.Create(configuration, objectParameter, encodedWriterExpression, parentContextArg, templatePath, (IDictionary>) inlinePartialsParameter) + ); + + var shouldDispose = ExpressionShortcuts.Var("shouldDispose"); + + Expression blockBuilder = ExpressionShortcuts.Block() + .Parameter(bindingContext) + .Parameter(shouldDispose) + .Line(ExpressionShortcuts.Condition() + .If(objectParameter.Is(), + bindingContext.Assign(objectParameter.As()) + ) + .Else(block => + { + block.Line(shouldDispose.Assign(true)); + block.Line(bindingContext.Assign(newBindingContext)); + }) + ) + .Line(ExpressionShortcuts.Try() + .Body(block => block.Lines(((BlockExpression) body).Expressions)) + .Finally(ExpressionShortcuts.Condition() + .If(shouldDispose, 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 ada0749f..00000000 --- a/source/Handlebars/Compiler/Translation/Expression/DeferredSectionVisitor.cs +++ /dev/null @@ -1,88 +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) - { - var templates = GetDeferredSectionTemplates(dsex); - - var path = ExpressionShortcuts.Arg(HandlebarsExpression.Path(dsex.Path.Path.Substring(1))); - var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - var body = ExpressionShortcuts.Arg(templates[0]); - var inversion = ExpressionShortcuts.Arg(templates[1]); - - return ExpressionShortcuts.Call(() => RenderSection(path, context, body, inversion)); - } - - 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 7078043a..901e6224 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -1,24 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; +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) @@ -43,8 +30,6 @@ public override Expression Visit(Expression exp) 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: @@ -76,7 +61,7 @@ protected virtual Expression VisitHelperExpression(HelperExpression hex) var arguments = VisitExpressionList(hex.Arguments); if (arguments != hex.Arguments) { - return HandlebarsExpression.Helper(hex.HelperName, arguments, hex.IsRaw); + return HandlebarsExpression.Helper(hex.HelperName, hex.IsBlock, arguments, hex.IsRaw); } return hex; } @@ -110,18 +95,6 @@ protected virtual Expression VisitIteratorExpression(IteratorExpression iex) 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); diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs index cd1470e8..4c7b8437 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsUndefinedBindingException.cs @@ -2,16 +2,25 @@ namespace HandlebarsDotNet.Compiler { + /// public class HandlebarsUndefinedBindingException : Exception { + + /// public HandlebarsUndefinedBindingException(string path, string missingKey) : base(missingKey + " is undefined") { this.Path = path; this.MissingKey = missingKey; } + /// + /// + /// public string Path { get; set; } + /// + /// + /// public string MissingKey { get; set; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs index cdf62f46..a2493bc8 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HashParameterBinder.cs @@ -7,16 +7,6 @@ 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) { var addMethod = typeof(HashParameterDictionary).GetMethod("Add", new[] { typeof(string), typeof(object) }); diff --git a/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs b/source/Handlebars/Compiler/Translation/Expression/HashParameterDictionary.cs index eb764ea0..6ba1edd0 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 { } + internal 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 98c7413b..2504b806 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,21 +1,19 @@ using System.Linq; using System.Linq.Expressions; using System.IO; +using Expressions.Shortcuts; 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) { return sex.Body is HelperExpression ? Visit(sex.Body) : sex; @@ -23,46 +21,67 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitHelperExpression(HelperExpression hex) { + var helperName = hex.HelperName; var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var textWriter = bindingContext.Property(o => o.TextWriter); - var args = ExpressionShortcuts.Array(hex.Arguments.Select(Visit)); + var arguments = hex.Arguments.Select(o => FunctionBuilder.Reduce(o, CompilationContext)); + var args = ExpressionShortcuts.Array(arguments); - if (CompilationContext.Configuration.Helpers.TryGetValue(hex.HelperName, out var helper)) + var configuration = CompilationContext.Configuration; + if (configuration.Helpers.TryGetValue(helperName, out var helper)) { return ExpressionShortcuts.Call(() => helper(textWriter, bindingContext, args)); } - if (CompilationContext.Configuration.ReturnHelpers.TryGetValue(hex.HelperName, out var returnHelper)) + if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { - return ExpressionShortcuts.Call(() => + return ExpressionShortcuts.Call(() => CaptureResult(textWriter, ExpressionShortcuts.Call(() => returnHelper(bindingContext, args))) ); } + var pureHelperName = helperName.Substring(1); + foreach (var resolver in configuration.HelperResolvers) + { + if (resolver.TryResolveReturnHelper(pureHelperName, typeof(object), out var resolvedHelper)) + { + return ExpressionShortcuts.Call(() => + CaptureResult(textWriter, ExpressionShortcuts.Call(() => resolvedHelper(bindingContext, args))) + ); + } + } + return ExpressionShortcuts.Call(() => CaptureResult(textWriter, ExpressionShortcuts.Call(() => - LateBindHelperExpression(bindingContext, hex.HelperName, args)) - ) + LateBindHelperExpression(bindingContext, helperName, args) + )) ); } - private object LateBindHelperExpression( - BindingContext context, - string helperName, - object[] arguments) + private static object LateBindHelperExpression(BindingContext context, string helperName, object[] arguments) { - if (CompilationContext.Configuration.Helpers.TryGetValue(helperName, out var helper)) + var configuration = context.Configuration; + if (configuration.Helpers.TryGetValue(helperName, out var helper)) { - using (var write = new PolledStringWriter()) + using (var write = new PolledStringWriter(configuration.FormatProvider)) { - helper(write, context.Value, arguments.ToArray()); + helper(write, context.Value, arguments); return write.ToString(); } } - if (CompilationContext.Configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) + if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { - return returnHelper(context.Value, arguments.ToArray()); + return returnHelper(context.Value, arguments); + } + + var pureHelperName = helperName.Substring(1); + foreach (var resolver in configuration.HelperResolvers) + { + if (resolver.TryResolveReturnHelper(pureHelperName, arguments.FirstOrDefault()?.GetType(), out returnHelper)) + { + return returnHelper(context.Value, arguments); + } } throw new HandlebarsRuntimeException($"Template references a helper that is not registered. Could not find helper '{helperName}'"); diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index 5b232aa2..938d1f70 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -3,547 +3,260 @@ using System.Linq.Expressions; using System.IO; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; -using System.Dynamic; -using System.Reflection; +using Expressions.Shortcuts; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.ObjectDescriptors; +using HandlebarsDotNet.Polyfills; +using HandlebarsDotNet.ValueProviders; namespace HandlebarsDotNet.Compiler { - internal class TypeDescriptors - { - private static readonly Lazy Instance = new Lazy(() => new TypeDescriptors()); - private readonly ConcurrentDictionary> _descriptors = new ConcurrentDictionary>(); - - public static TypeDescriptors Provider => Instance.Value; - - private TypeDescriptors(){} - - public bool TryGetGenericDictionaryTypeDescriptor(Type type, out DictionaryTypeDescriptor typeDescriptor) - { - var descriptors = _descriptors.GetOrAdd(type, t => new ConcurrentDictionary()); - typeDescriptor = (DictionaryTypeDescriptor) descriptors.GetOrAdd(nameof(TryGetGenericDictionaryTypeDescriptor), name => - { - var typeDefinition = type.GetInterfaces() - .Where(i => i.GetTypeInfo().IsGenericType) - .FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); - - return typeDefinition == null - ? null - : new DictionaryTypeDescriptor(typeDefinition); - }); - - return typeDescriptor != null; - } - - public ObjectTypeDescriptor GetObjectTypeDescriptor(Type type) - { - var descriptors = _descriptors.GetOrAdd(type, t => new ConcurrentDictionary()); - return (ObjectTypeDescriptor) descriptors.GetOrAdd(nameof(GetObjectTypeDescriptor), name => new ObjectTypeDescriptor(type)); - } - - public class DictionaryTypeDescriptor : TypeDescriptor - { - public DictionaryTypeDescriptor(Type type) : base(type) - { - DictionaryAccessor = typeof(DictionaryAccessor<,>).MakeGenericType(type.GenericTypeArguments); - } - - public Type DictionaryAccessor { get; } - } - - public class ObjectTypeDescriptor : TypeDescriptor - { - private Dictionary> _accessors = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - public ObjectTypeDescriptor(Type type) : base(type) - { - var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - - Members = properties.OfType().Concat(fields).ToArray(); - - foreach (var member in fields) - { - _accessors[member.Name] = o => member.GetValue(o); - } - - foreach (var member in properties) - { - _accessors[member.Name] = GetValueGetter(member); - } - } - - public MemberInfo[] Members { get; } - - public IReadOnlyDictionary> Accessors => _accessors; - - private static Func GetValueGetter(PropertyInfo propertyInfo) - { - var instance = Expression.Parameter(typeof(object), "i"); - var property = Expression.Property(Expression.Convert(instance, propertyInfo.DeclaringType), propertyInfo); - var convert = Expression.TypeAs(property, typeof(object)); - - return (Func)Expression.Lambda(convert, instance).Compile(); - } - } - - public abstract class TypeDescriptor - { - public Type Type { get; } - - public TypeDescriptor(Type type) - { - Type = type; - } - } - } - - internal class DictionaryAccessor : IReadOnlyDictionary - { - private readonly IDictionary _wrapped; - - public DictionaryAccessor(IDictionary wrapped) - { - _wrapped = wrapped; - } - - public int Count => _wrapped.Count; - public bool ContainsKey(object key) - { - return _wrapped.ContainsKey((TKey) key); - } - - public bool TryGetValue(object key, out object value) - { - if(_wrapped.TryGetValue((TKey) key, out var inner)) - { - value = inner; - return true; - } - - value = null; - return false; - } - - public object this[object key] => _wrapped[(TKey) key]; - - public IEnumerable Keys => _wrapped.Keys.Cast(); - public IEnumerable Values => _wrapped.Values.Cast(); - - public IEnumerator> GetEnumerator() - { - return _wrapped.Select(value => new KeyValuePair(value.Key, value.Value)).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - 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 fb = new FunctionBuilder(CompilationContext.Configuration); - - var iteratorBindingContext = ExpressionShortcuts.Var("context"); - var blockParamsValueBinder = ExpressionShortcuts.Var("blockParams"); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var sequence = ExpressionShortcuts.Var("sequence"); - var template = ExpressionShortcuts.Arg(fb.Compile(new[] {iex.Template}, iteratorBindingContext)); - var ifEmpty = ExpressionShortcuts.Arg(fb.Compile(new[] {iex.IfEmpty}, CompilationContext.BindingContext)); + var template = FunctionBuilder.CompileCore(new[] {iex.Template}, CompilationContext.Configuration); + var ifEmpty = FunctionBuilder.CompileCore(new[] {iex.IfEmpty}, CompilationContext.Configuration); - var context = CompilationContext.BindingContext; - var compiledSequence = FunctionBuilder.Reduce(iex.Sequence, CompilationContext); - var blockParamsProvider = ExpressionShortcuts.New(() => new BlockParamsValueProvider(iteratorBindingContext, ExpressionShortcuts.Arg(iex.BlockParams))); - + var compiledSequence = ExpressionShortcuts.Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext)); + var blockParams = ExpressionShortcuts.Arg(iex.BlockParams); + var blockParamsProvider = ExpressionShortcuts.Call(() => BlockParamsValueProvider.Create(context, blockParams)); + var blockParamsProviderVar = ExpressionShortcuts.Var(); return ExpressionShortcuts.Block() - .Parameter(iteratorBindingContext, context) - .Parameter(blockParamsValueBinder, blockParamsProvider) .Parameter(sequence, compiledSequence) - .Line(Expression.IfThenElse( - sequence.Is(), - Expression.IfThenElse( - ExpressionShortcuts.Call(() => IsGenericDictionary(sequence)), - GetDictionaryIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), - Expression.IfThenElse( - ExpressionShortcuts.Call(() => IsNonListDynamic(sequence)), - GetDynamicIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), - Expression.IfThenElse(ExpressionShortcuts.Call(() => IsList(sequence)), - GetListIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty), - GetEnumerableIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty) - ) - ) - ), - GetObjectIterator(iteratorBindingContext, blockParamsValueBinder, sequence, template, ifEmpty))); - } - - private static Expression GetEnumerableIterator( - ExpressionContainer contextParameter, - ExpressionContainer blockParamsParameter, - ExpressionContainer sequence, - ExpressionContainer> template, - ExpressionContainer> ifEmpty - ) - { - return ExpressionShortcuts.Call( - () => IterateEnumerable(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) - ); - } - - private static Expression GetListIterator( - ExpressionContainer contextParameter, - ExpressionContainer blockParamsParameter, - ExpressionContainer sequence, - ExpressionContainer> template, - ExpressionContainer> ifEmpty - ) - { - return ExpressionShortcuts.Call( - () => IterateList(contextParameter, blockParamsParameter, (IList) sequence, template, ifEmpty) - ); - } - - private static Expression GetObjectIterator( - ExpressionContainer contextParameter, - ExpressionContainer blockParamsParameter, - ExpressionContainer sequence, - ExpressionContainer> template, - ExpressionContainer> ifEmpty - ) - { - return ExpressionShortcuts.Call( - () => IterateObject(contextParameter, blockParamsParameter, sequence, template, ifEmpty) - ); - } - - private static Expression GetDictionaryIterator( - ExpressionContainer contextParameter, - ExpressionContainer blockParamsParameter, - ExpressionContainer sequence, - ExpressionContainer> template, - ExpressionContainer> ifEmpty - ) - { - return ExpressionShortcuts.Call( - () => IterateDictionary(contextParameter, blockParamsParameter, (IEnumerable) sequence, template, ifEmpty) - ); - } - - private static Expression GetDynamicIterator( - ExpressionContainer contextParameter, - ExpressionContainer blockParamsParameter, - ExpressionContainer sequence, - ExpressionContainer> template, - ExpressionContainer> ifEmpty) - { - return ExpressionShortcuts.Call( - () => IterateDynamic(contextParameter, blockParamsParameter, (IDynamicMetaObjectProvider) sequence, template, ifEmpty) - ); + .Parameter(blockParamsProviderVar, blockParamsProvider) + .Line(blockParamsProviderVar.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(ExpressionShortcuts.Try() + .Body(ExpressionShortcuts.Call(() => + Iterator.Iterate(context, self, sequence, template, ifEmpty) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); } + } - private static bool IsList(object target) - { - return target is IList; - } - - private static bool IsNonListDynamic(object target) + internal static class Iterator + { + private static readonly ConfigureBlockParams BlockParamsEnumerableConfiguration = (parameters, binder, deps) => { - if (target is IDynamicMetaObjectProvider metaObjectProvider) - { - return metaObjectProvider.GetMetaObject(Expression.Constant(target)).GetDynamicMemberNames().Any(); - } - - return false; - } + 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 TypeDescriptors.Provider.TryGetGenericDictionaryTypeDescriptor(target.GetType(), out _); - } + binder(parameters.ElementAtOrDefault(0), ctx => ctx.As().Value, deps[0]); + binder(parameters.ElementAtOrDefault(1), ctx => ctx.As().Key, deps[0]); + }; - private static void IterateObject( - BindingContext 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)) { - var objectEnumerator = new ObjectEnumeratorValueProvider(); - context.RegisterValueProvider(objectEnumerator); - blockParamsValueProvider.Configure((parameters, binder) => - { - binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); - binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); - }); - - objectEnumerator.Index = 0; - var typeDescriptor = TypeDescriptors.Provider.GetObjectTypeDescriptor(target.GetType()); - var accessorsCount = typeDescriptor.Accessors.Count; - foreach (var accessor in typeDescriptor.Accessors) - { - objectEnumerator.Key = accessor.Key; - objectEnumerator.Value = accessor.Value.Invoke(target); - objectEnumerator.First = objectEnumerator.Index == 0; - objectEnumerator.Last = objectEnumerator.Index == accessorsCount - 1; + ifEmpty(context, context.TextWriter, context.Value); + return; + } - template(context.TextWriter, objectEnumerator.Value); - - objectEnumerator.Index++; - } + 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 (objectEnumerator.Index == 0) + if (!descriptor.ShouldEnumerate) + { + var properties = descriptor.GetProperties(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 IterateDictionary( - BindingContext context, + IterateEnumerable(context, blockParamsValueProvider, (IEnumerable) target, template, ifEmpty); + } + + private static void IterateObject(BindingContext context, + ObjectDescriptor descriptor, BlockParamsValueProvider blockParamsValueProvider, - IEnumerable target, - Action template, - Action ifEmpty) + object target, + IEnumerable properties, + Type targetType, + Action template, + Action ifEmpty) { - if (HandlebarsUtils.IsTruthy(target)) + using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - TypeDescriptors.Provider.TryGetGenericDictionaryTypeDescriptor(target.GetType(), out var typeDescriptor); - var accessor = (IReadOnlyDictionary) Activator.CreateInstance(typeDescriptor.DictionaryAccessor, target); - - var objectEnumerator = new ObjectEnumeratorValueProvider(); - context.RegisterValueProvider(objectEnumerator); - blockParamsValueProvider.Configure((parameters, binder) => - { - binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); - binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); - }); - - objectEnumerator.Index = 0; - var keys = accessor.Keys; - foreach (var enumerableValue in new ExtendedEnumerable(keys)) - { - var key = enumerableValue.Value; - objectEnumerator.Key = key.ToString(); - objectEnumerator.Value = accessor[key]; - objectEnumerator.First = enumerableValue.IsFirst; - objectEnumerator.Last = enumerableValue.IsLast; - objectEnumerator.Index = enumerableValue.Index; + blockParamsValueProvider?.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); + + iterator.Index = 0; + var accessor = descriptor.MemberAccessor; + var enumerable = new ExtendedEnumerable(properties); + bool enumerated = false; - template(context.TextWriter, objectEnumerator.Value); + foreach (var enumerableValue in enumerable) + { + enumerated = true; + iterator.Key = enumerableValue.Value.ToString(); + var key = iterator.Key.Intern(); + iterator.Value = accessor.TryGetValue(target, targetType, key, out var value) ? value : null; + iterator.First = enumerableValue.IsFirst; + iterator.Last = enumerableValue.IsLast; + iterator.Index = enumerableValue.Index; + + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + innerContext.UnregisterValueProvider(iterator); + } } - if (objectEnumerator.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 IterateDynamic( - BindingContext context, + + private static void IterateObjectWithStaticProperties(BindingContext context, + ObjectDescriptor descriptor, BlockParamsValueProvider blockParamsValueProvider, - IDynamicMetaObjectProvider target, - Action template, - Action ifEmpty) + object target, + IList properties, + Type targetType, + Action template, + Action ifEmpty) { - if (HandlebarsUtils.IsTruthy(target)) + using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - var objectEnumerator = new ObjectEnumeratorValueProvider(); - context.RegisterValueProvider(objectEnumerator); - blockParamsValueProvider.Configure((parameters, binder) => - { - binder(parameters.ElementAtOrDefault(0), () => objectEnumerator.Value); - binder(parameters.ElementAtOrDefault(1), () => objectEnumerator.Key); - }); + blockParamsValueProvider?.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); - objectEnumerator.Index = 0; - var meta = target.GetMetaObject(Expression.Constant(target)); - foreach (var enumerableValue in new ExtendedEnumerable(meta.GetDynamicMemberNames())) - { - var name = enumerableValue.Value; - objectEnumerator.Key = name; - objectEnumerator.Value = GetProperty(target, name); - objectEnumerator.First = enumerableValue.IsFirst; - objectEnumerator.Last = enumerableValue.IsLast; - objectEnumerator.Index = enumerableValue.Index; + var accessor = descriptor.MemberAccessor; - template(context.TextWriter, objectEnumerator.Value); + var count = properties.Count; + for (iterator.Index = 0; iterator.Index < count; iterator.Index++) + { + iterator.Key = properties[iterator.Index].ToString(); + iterator.Value = accessor.TryGetValue(target, targetType, iterator.Key, out var value) ? value : null; + iterator.First = iterator.Index == 0; + iterator.Last = iterator.Index == count - 1; + iterator.Index = iterator.Index; + + using (var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + innerContext.UnregisterValueProvider(iterator); + } } - if (objectEnumerator.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 IterateEnumerable( - BindingContext context, + + private static void IterateList(BindingContext context, BlockParamsValueProvider blockParamsValueProvider, - IEnumerable sequence, - Action template, - Action ifEmpty) + IList target, + Action template, + Action ifEmpty) { - var iterator = new IteratorValueProvider(); - context.RegisterValueProvider(iterator); - blockParamsValueProvider.Configure((parameters, binder) => + using (var iterator = IteratorValueProvider.Create()) { - binder(parameters.ElementAtOrDefault(0), () => iterator.Value); - binder(parameters.ElementAtOrDefault(1), () => iterator.Index); - }); - - iterator.Index = 0; - foreach (var enumeratorValue in new ExtendedEnumerable(sequence)) - { - iterator.Value = enumeratorValue.Value; - iterator.First = enumeratorValue.IsFirst; - iterator.Last = enumeratorValue.IsLast; - iterator.Index = enumeratorValue.Index; + blockParamsValueProvider?.Configure(BlockParamsEnumerableConfiguration, iterator); - template(context.TextWriter, iterator.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; + + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + innerContext.UnregisterValueProvider(iterator); + } + } - if (iterator.Index == 0) - { - ifEmpty(context.TextWriter, context.Value); + if (iterator.Index == 0) + { + ifEmpty(context, context.TextWriter, context.Value); + } } } - private static void IterateList( - BindingContext context, + private static void IterateEnumerable(BindingContext context, BlockParamsValueProvider blockParamsValueProvider, - IList sequence, - Action template, - Action ifEmpty) + IEnumerable target, + Action template, + Action ifEmpty) { - var iterator = new IteratorValueProvider(); - context.RegisterValueProvider(iterator); - blockParamsValueProvider.Configure((parameters, binder) => - { - binder(parameters.ElementAtOrDefault(0), () => iterator.Value); - binder(parameters.ElementAtOrDefault(1), () => iterator.Index); - }); - - iterator.Index = 0; - var sequenceCount = sequence.Count; - for (var index = 0; index < sequenceCount; index++) - { - iterator.Value = sequence[index]; - iterator.First = index == 0; - iterator.Last = index == sequenceCount - 1; - iterator.Index = index; - - template(context.TextWriter, iterator.Value); - } - - if (iterator.Index == 0) + using (var iterator = IteratorValueProvider.Create()) { - ifEmpty(context.TextWriter, context.Value); - } - } + blockParamsValueProvider?.Configure(BlockParamsEnumerableConfiguration, iterator); - 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 class IteratorValueProvider : IValueProvider - { - 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(string memberName, out object value) - { - switch (memberName.ToLowerInvariant()) + iterator.Index = 0; + var enumerable = new ExtendedEnumerable(target); + bool enumerated = false; + + foreach (var enumerableValue in enumerable) { - 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; + enumerated = true; + iterator.Value = enumerableValue.Value; + iterator.First = enumerableValue.IsFirst; + iterator.Last = enumerableValue.IsLast; + iterator.Index = enumerableValue.Index; + + using(var innerContext = context.CreateChildContext(iterator.Value)) + { + innerContext.RegisterValueProvider(iterator); + template(context, context.TextWriter, innerContext); + innerContext.UnregisterValueProvider(iterator); + } } - } - } - - private class ObjectEnumeratorValueProvider : IteratorValueProvider - { - public string Key { get; set; } - public override bool TryGetValue(string memberName, out object value) - { - switch (memberName.ToLowerInvariant()) + if (iterator.Index == 0 && !enumerated) { - case "key": - value = Key; - return true; - - default: - return base.TryGetValue(memberName, out value); + ifEmpty(context, context.TextWriter, context.Value); } } } - - private static object AccessMember(object instance, MemberInfo member) - { - switch (member) - { - case PropertyInfo propertyInfo: - return propertyInfo.GetValue(instance, null); - case FieldInfo fieldInfo: - return fieldInfo.GetValue(instance); - default: - throw new InvalidOperationException("Requested member was not a field or property"); - } - } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs b/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs new file mode 100644 index 00000000..cc4a6cf3 --- /dev/null +++ b/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace HandlebarsDotNet.Compiler +{ + internal struct LambdaReducer + { + private readonly BindingContext _context; + private readonly Action _direct; + private readonly Action _inverse; + + public LambdaReducer(BindingContext context, Action direct, Action inverse) : this() + { + _context = context; + _direct = direct; + _inverse = inverse; + + Direct = DirectCall; + Inverse = InverseCall; + } + + public Action Direct { get; } + public Action Inverse { get; } + + private void DirectCall(TextWriter writer, object arg) + { + _direct(_context, writer, arg); + } + + private void InverseCall(TextWriter writer, object arg) + { + _inverse(_context, writer, arg); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 7c327426..77d2b588 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -1,7 +1,6 @@ using System; -using System.IO; using System.Linq.Expressions; -using System.Reflection; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { @@ -9,42 +8,35 @@ internal class PartialBinder : HandlebarsExpressionVisitor { private static string SpecialPartialBlockName = "@partial-block"; - public static Expression Bind(Expression expr, CompilationContext context) + private CompilationContext CompilationContext { get; } + + public PartialBinder(CompilationContext compilationContext) { - return new PartialBinder(context).Visit(expr); + CompilationContext = compilationContext; } + + protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) => bhex; - private PartialBinder(CompilationContext context) - : base(context) - { - } - - protected override Expression VisitBlockHelperExpression(BlockHelperExpression bhex) - { - return bhex; - } - - protected override Expression VisitStatementExpression(StatementExpression sex) - { - return sex.Body is PartialExpression ? Visit(sex.Body) : sex; - } + 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 fb = new FunctionBuilder(CompilationContext.Configuration); - var partialBlockTemplate = pex.Fallback == null ? null : fb.Compile(new[] {pex.Fallback}, null, null); + var partialBlockTemplate = pex.Fallback != null + ? FunctionBuilder.CompileCore(new[] { pex.Fallback }, null, CompilationContext.Configuration) + : null; if (pex.Argument != null || partialBlockTemplate != null) { - bindingContext = bindingContext.Call(o => - o.CreateChildContext(ExpressionShortcuts.Arg(pex.Argument), ExpressionShortcuts.Arg(partialBlockTemplate)) - ); + 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(ExpressionShortcuts.Cast(pex.PartialName), bindingContext, CompilationContext.Configuration) + InvokePartialWithFallback(partialName, bindingContext, configuration) ); } @@ -56,13 +48,11 @@ private static void InvokePartialWithFallback( if (InvokePartial(partialName, context, configuration)) return; if (context.PartialBlockTemplate == null) { - if (configuration.MissingPartialTemplateHandler != null) - { - configuration.MissingPartialTemplateHandler.Handle(configuration, partialName, context.TextWriter); - return; - } - - throw new HandlebarsRuntimeException($"Referenced partial name {partialName} could not be resolved"); + 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); @@ -109,9 +99,7 @@ private static bool InvokePartial( } catch (Exception exception) { - throw new HandlebarsRuntimeException( - $"Runtime error while rendering partial '{partialName}', see inner exception for more information", - 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 9b5296e9..f8b60672 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,34 +1,33 @@ using System.Linq.Expressions; +using Expressions.Shortcuts; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { internal class PathBinder : HandlebarsExpressionVisitor { - public static Expression Bind(Expression expr, CompilationContext context) - { - return new PathBinder(context).Visit(expr); - } + private CompilationContext CompilationContext { get; } - private PathBinder(CompilationContext context) - : base(context) + public PathBinder(CompilationContext compilationContext) { + CompilationContext = compilationContext; } - + protected override Expression VisitStatementExpression(StatementExpression sex) { if (!(sex.Body is PathExpression)) return Visit(sex.Body); var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - - return context.Property(o => o.TextWriter) - .Call(o => o.Write(ExpressionShortcuts.Arg(Visit(sex.Body)))); + var value = ExpressionShortcuts.Arg(Visit(sex.Body)); + return context.Call(o => o.TextWriter.Write(value)); } protected override Expression VisitPathExpression(PathExpression pex) { - var pathResolver = ExpressionShortcuts.New(); var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - return pathResolver.Call(o => o.ResolvePath(context, pex.PathInfo)); + var pathInfo = pex.PathInfo; + + return ExpressionShortcuts.Call(() => PathResolver.ResolvePath(context, ref pathInfo)); } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs index 132d555c..c9c0155d 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -1,26 +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 context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - var writer = context.Property(o => o.TextWriter); - return writer.Call(o => o.Write(stex.Value, false)); + 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 index edf3aac2..ba10dac0 100755 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,21 +1,25 @@ -using System.Linq.Expressions; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; using System.IO; using System.Linq; +using System.Reflection; +using Expressions.Shortcuts; namespace HandlebarsDotNet.Compiler { 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) { switch (subex.Expression) @@ -40,25 +44,36 @@ private Expression HandleMethodCallExpression(MethodCallExpression helperCall) if (helperCall.Type != typeof(void)) { return helperCall.Update(helperCall.Object, - ExpressionShortcuts.ReplaceValuesOf(helperCall.Arguments, ExpressionShortcuts.Null()).Select(Visit) + ReplaceValuesOf(helperCall.Arguments, ExpressionShortcuts.Null()).Select(Visit) ); } + var context = ExpressionShortcuts.Var(); var writer = ExpressionShortcuts.Var(); - helperCall = helperCall.Update(helperCall.Object, - ExpressionShortcuts.ReplaceValuesOf(helperCall.Arguments, writer).Select(Visit) + 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())) + )); - return ExpressionShortcuts.Block() - .Parameter(writer) - .Line(Expression.Assign(writer, ExpressionShortcuts.New())) - .Line(writer.Using(container => - { - var body = new BlockBody { helperCall }; - body.AddRange(container.Call(o => o.ToString()).Return()); - return body; - })) - .Invoke(); + var continuation = _expressionCompiler.Compile(Expression.Lambda>(block, (ParameterExpression) context)); + return ExpressionShortcuts.Arg(Expression.Invoke(Expression.Constant(continuation), CompilationContext.BindingContext)); + } + + private static IEnumerable ReplaceValuesOf(IEnumerable instance, Expression newValue) + { + var targetType = typeof(T); + return instance.Select(value => targetType.IsAssignableFrom(value.Type) + ? newValue + : value); } private Expression HandleInvocationExpression(InvocationExpression invocation) @@ -66,25 +81,28 @@ private Expression HandleInvocationExpression(InvocationExpression invocation) if (invocation.Type != typeof(void)) { return invocation.Update(invocation.Expression, - ExpressionShortcuts.ReplaceValuesOf(invocation.Arguments, ExpressionShortcuts.Null()).Select(Visit) + ReplaceValuesOf(invocation.Arguments, ExpressionShortcuts.Null()).Select(Visit) ); } + var context = ExpressionShortcuts.Var(); var writer = ExpressionShortcuts.Var(); - invocation = invocation.Update(invocation.Expression, - ExpressionShortcuts.ReplaceValuesOf(invocation.Arguments, writer).Select(Visit) + 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())) + )); - return ExpressionShortcuts.Block() - .Parameter(writer) - .Line(Expression.Assign(writer, ExpressionShortcuts.New())) - .Line(writer.Using(container => - { - var body = new BlockBody { invocation }; - body.AddRange(container.Call(o => o.ToString()).Return()); - return body; - })) - .Invoke(); + 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 afde8022..6f59b2bb 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -1,28 +1,24 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; +using Expressions.Shortcuts; 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) { - var context = ExpressionShortcuts.Var(); + var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var suppressEncoding = context.Property(o => o.SuppressEncoding); if (sex.IsEscaped == false) { return ExpressionShortcuts.Block(typeof(void)) - .Parameter(context, CompilationContext.BindingContext) .Line(suppressEncoding.Assign(true)) .Line(sex) .Line(suppressEncoding.Assign(false)) diff --git a/source/Handlebars/Configuration/Compatibility.cs b/source/Handlebars/Configuration/Compatibility.cs new file mode 100644 index 00000000..da0b8bf9 --- /dev/null +++ b/source/Handlebars/Configuration/Compatibility.cs @@ -0,0 +1,13 @@ +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; + } +} \ 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..6e0e32c6 --- /dev/null +++ b/source/Handlebars/Configuration/CompileTimeConfiguration.cs @@ -0,0 +1,41 @@ +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() + }; + + /// + 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..b6291987 --- /dev/null +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -0,0 +1,97 @@ +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/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs new file mode 100644 index 00000000..7293e261 --- /dev/null +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.Compiler.Resolvers; +using HandlebarsDotNet.Features; +using HandlebarsDotNet.Helpers; +using HandlebarsDotNet.ObjectDescriptors; + +namespace HandlebarsDotNet +{ + internal class InternalHandlebarsConfiguration : HandlebarsConfiguration + { + 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 sealed override CompileTimeConfiguration CompileTimeConfiguration { get; } + + public IObjectDescriptorProvider ObjectDescriptorProvider { get; } + public List Features { get; } + + 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); + + 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); + + 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) + } + }; + + Features = CompileTimeConfiguration.Features.Select(o => o.CreateFeature()).ToList(); + } + } +} \ 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/DynamicViewModel.cs b/source/Handlebars/DynamicViewModel.cs index 07ef1f39..64a8a0e5 100644 --- a/source/Handlebars/DynamicViewModel.cs +++ b/source/Handlebars/DynamicViewModel.cs @@ -5,18 +5,28 @@ namespace HandlebarsDotNet { + /// + /// + /// public class DynamicViewModel : DynamicObject { + /// + /// + /// public object[] Objects { get; set; } private static readonly BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + /// + /// + /// public DynamicViewModel(params object[] objects) { Objects = objects; } + /// public override IEnumerable GetDynamicMemberNames() { return Objects.Select(o => o.GetType()) @@ -24,6 +34,7 @@ public override IEnumerable GetDynamicMemberNames() .Select(m => m.Name); } + /// public override bool TryGetMember(GetMemberBinder binder, out object result) { result = null; diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index a161d0e4..b18274d9 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.IO; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; namespace HandlebarsDotNet { @@ -12,21 +9,26 @@ internal static class EnumerableExtensions public static bool IsOneOf(this IEnumerable source) where TExpected : TSource { - var enumerator = source.GetEnumerator(); - enumerator.MoveNext(); - return (enumerator.Current is TExpected) && (enumerator.MoveNext() == false); - } - - public static void AddOrUpdate(this IDictionary dictionary, TKey key, TValue value) - { - if (dictionary.ContainsKey(key)) + using(var enumerator = source.GetEnumerator()) { - dictionary[key] = value; + enumerator.MoveNext(); + return enumerator.Current is TExpected && !enumerator.MoveNext(); } - else + } + + public static bool IsMultiple(this IEnumerable source) + { + using(var enumerator = source.GetEnumerator()) { - dictionary.Add(key, value); + return enumerator.MoveNext() && enumerator.MoveNext(); } } } + + internal static class ObjectExtensions + { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T As(this object source) => (T) source; + } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs new file mode 100644 index 00000000..fbdcfbf1 --- /dev/null +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -0,0 +1,96 @@ +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(); + } + } + + internal class BuildInHelpersFeature : IFeature + { + private InternalHandlebarsConfiguration _configuration; + + private static readonly ConfigureBlockParams WithBlockParamsConfiguration = (parameters, binder, deps) => + { + binder(parameters.ElementAtOrDefault(0), ctx => ctx, deps[0]); + }; + + public void OnCompiling(HandlebarsConfiguration configuration) + { + _configuration = (InternalHandlebarsConfiguration) configuration; + + configuration.BlockHelpers["with"] = With; + configuration.BlockHelpers["*inline"] = Inline; + + configuration.ReturnHelpers["lookup"] = Lookup; + } + + public void CompilationCompleted() + { + } + + 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(); + return !PathResolver.TryAccessMember(arguments[0], ref ChainSegment.Create(memberName), _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..84541a9d --- /dev/null +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -0,0 +1,172 @@ +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 + /// + public class ClosureFeature : IFeature + { + /// + /// Parameter of actual closure + /// + internal ExpressionContainer Closure { get; } = ExpressionShortcuts.Var("closure"); + + /// + /// 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, Closure); + } + + /// + public void OnCompiling(HandlebarsConfiguration configuration) + { + } + + /// + 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().IsValueType) 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..e197c5e0 --- /dev/null +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Expressions.Shortcuts; + +namespace HandlebarsDotNet.Features +{ + internal class DefaultCompilerFeatureFactory : IFeatureFactory + { + public IFeature CreateFeature() + { + return new DefaultCompilerFeature(); + } + } + + internal class DefaultCompilerFeature : IFeature + { + public void OnCompiling(HandlebarsConfiguration configuration) + { + var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features + .OfType().SingleOrDefault(); + + configuration.CompileTimeConfiguration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); + } + + public void CompilationCompleted() + { + } + + private class DefaultExpressionCompiler : IExpressionCompiler + { + private readonly ClosureFeature _closureFeature; + private readonly TemplateClosure _templateClosure; + private readonly ExpressionContainer _closure; + private readonly ICollection _expressionMiddleware; + + public DefaultExpressionCompiler(HandlebarsConfiguration configuration, ClosureFeature closureFeature) + { + _closureFeature = closureFeature; + _templateClosure = closureFeature?.TemplateClosure; + _closure = closureFeature?.Closure; + _expressionMiddleware = configuration.CompileTimeConfiguration.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 = ExpressionShortcuts.Arg(_templateClosure).Property(o => o.Store); + var outerLambda = Expression.Lambda( + Expression.Invoke(Expression.Constant(compiledLambda), new[] {store.Expression}.Concat(outerParameters)), + outerParameters); + + return outerLambda.Compile(); + } + } + } +} \ 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..5b83e1cd --- /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(HandlebarsConfiguration 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..696f96cc --- /dev/null +++ b/source/Handlebars/Features/IFeatureFactory.cs @@ -0,0 +1,14 @@ +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/TemplateClosure.cs b/source/Handlebars/Features/TemplateClosure.cs new file mode 100644 index 00000000..d33f5fcd --- /dev/null +++ b/source/Handlebars/Features/TemplateClosure.cs @@ -0,0 +1,66 @@ +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(); + private object[] _store = new object[0]; + + /// + /// Index for the next item reference + /// + public int CurrentIndex => _inner?.Count ?? -1; + + /// + /// Actual closure storage + /// + public object[] Store => _store; + + /// + /// Adds value to store + /// + /// + public object this[int key] + { + set + { + if(value == null) return; + _inner?.Add(value); + _objectSet?.Add(value, key); + } + } + + /// + /// Uses reverse index to lookup for object key in storage using it's value + /// + /// + /// + /// + public 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; + + Array.Resize(ref _store, _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..796a882b --- /dev/null +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace HandlebarsDotNet.Features +{ + /// + /// Allows to warm-up internal caches for specific types + /// + public class WarmUpFeature : IFeature + { + private readonly HashSet _types; + + /// + /// + /// + /// + public WarmUpFeature(HashSet types) + { + _types = types; + } + + /// + public void OnCompiling(HandlebarsConfiguration configuration) + { + var internalConfiguration = (InternalHandlebarsConfiguration) configuration; + + var descriptorProvider = internalConfiguration.ObjectDescriptorProvider; + foreach (var type in _types) + { + if(!descriptorProvider.CanHandleType(type)) continue; + descriptorProvider.TryGetDescriptor(type, out _); + } + } + + /// + public void CompilationCompleted() + { + } + } + + internal class WarmUpFeatureFactory : IFeatureFactory + { + private readonly HashSet _types; + + public WarmUpFeatureFactory(HashSet types) + { + _types = types; + } + + public IFeature CreateFeature() + { + return new WarmUpFeature(_types); + } + } + + /// + /// + /// + 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; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/FileSystemPartialTemplateResolver.cs b/source/Handlebars/FileSystemPartialTemplateResolver.cs index 161734a9..8a4e179d 100644 --- a/source/Handlebars/FileSystemPartialTemplateResolver.cs +++ b/source/Handlebars/FileSystemPartialTemplateResolver.cs @@ -2,8 +2,10 @@ namespace HandlebarsDotNet { + /// public class FileSystemPartialTemplateResolver : IPartialTemplateResolver { + /// public bool TryRegisterPartial(IHandlebars env, string partialName, string templatePath) { if (env == null) @@ -31,11 +33,9 @@ public bool TryRegisterPartial(IHandlebars env, string partialName, string templ return true; } - else - { - // Failed to find partial in filesystem - return false; - } + + // Failed to find partial in filesystem + return false; } } } diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index 2db5e68d..44d3d990 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -3,69 +3,135 @@ 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); } + /// + /// + /// + /// + /// public static Func CompileView(string templatePath) { return Instance.CompileView(templatePath); } + /// + /// + /// + /// + /// 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 + /// + /// + /// + [Obsolete("Consider switching to HandlebarsReturnHelper")] public static void RegisterHelper(string helperName, HandlebarsHelper helperFunction) { Instance.RegisterHelper(helperName, helperFunction); } + /// + /// Registers new + /// + /// + /// public static void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) { Instance.RegisterHelper(helperName, helperFunction); } + /// + /// Registers new + /// + /// + /// public static void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) { Instance.RegisterHelper(helperName, helperFunction); } /// - /// Expose the configuration on order to have access in all Helpers and Templates. + /// Expose the configuration in order to have access in all Helpers and Templates. /// - public static HandlebarsConfiguration Configuration - { - get { return Instance.Configuration; } - } + 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 7b582203..1e15c026 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -2,10 +2,12 @@ portable - true net452;netstandard1.3;netstandard2.0 - 1.10.7 - latest + 1.0.0 + 7 + HandlebarsDotNet + 7F6A54F4-161A-46EA-9A27-DF834B7810DB + true @@ -16,31 +18,44 @@ - Rex Morgan - Copyright © 2014-2019 Rex Morgan + Rex Morgan, Oleh Formaniuk + Oleh Formaniuk + Copyright © 2020 Oleh Formaniuk Blistering-fast Handlebars.js templates in your .NET application. - https://raw.githubusercontent.com/rexm/Handlebars.Net/master/hbnet-icon.png - Handlebars.Net + hbnet-icon.png + handlebars.csharp https://opensource.org/licenses/mit - https://github.com/rexm/Handlebars.Net - Added quad-stache syntax, better support for inline and partial blocks, various bug fixes + https://github.com/zjklee/handlebars.csharp + Improved performance; added new features handlebars;mustache;templating;engine;compiler git - https://github.com/rexm/Handlebars.Net + https://github.com/zjklee/handlebars.csharp true + + + false + true + . + + + - + - - + + + - - + + + + + diff --git a/source/Handlebars/HandlebarsConfiguration.cs b/source/Handlebars/HandlebarsConfiguration.cs deleted file mode 100644 index e06ba1b2..00000000 --- a/source/Handlebars/HandlebarsConfiguration.cs +++ /dev/null @@ -1,53 +0,0 @@ -using HandlebarsDotNet.Compiler.Resolvers; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace HandlebarsDotNet -{ - public class HandlebarsConfiguration - { - public IDictionary Helpers { get; private set; } - - public IDictionary ReturnHelpers { 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.ReturnHelpers = 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 edbece82..e303639e 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -1,180 +1,101 @@ using System; -using System.Collections.Concurrent; using System.IO; -using System.Text; +using System.Runtime.CompilerServices; using HandlebarsDotNet.Compiler; +[assembly: InternalsVisibleTo("Handlebars.Extension.CompileFast")] +[assembly: InternalsVisibleTo("Handlebars.Test")] + namespace HandlebarsDotNet { - internal abstract class ObjectPool - { - private readonly ConcurrentQueue _objects; - - protected ObjectPool() - { - _objects = new ConcurrentQueue(); - } - - public T GetObject() - { - return _objects.TryDequeue(out var item) - ? item - : CreateObject(); - } - - public virtual void PutObject(T item) - { - _objects.Enqueue(item); - } - - protected abstract T CreateObject(); - } - - internal class StringBuilderPool : ObjectPool + internal class HandlebarsEnvironment : IHandlebars { - private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); - - private readonly int _initialCapacity; - - public static StringBuilderPool Shared => Lazy.Value; - - public StringBuilderPool(int initialCapacity = 100) + public HandlebarsEnvironment(HandlebarsConfiguration configuration) { - _initialCapacity = initialCapacity; - } - - protected override StringBuilder CreateObject() - { - return new StringBuilder(_initialCapacity); + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - public override void PutObject(StringBuilder item) + public Func CompileView(string templatePath) { - item.Length = 0; - base.PutObject(item); - } - } - - public partial class Handlebars - { - private class HandlebarsEnvironment : IHandlebars - { - private readonly HandlebarsCompiler _compiler; - - public HandlebarsEnvironment(HandlebarsConfiguration configuration) + var configuration = new InternalHandlebarsConfiguration(Configuration); + var createdFeatures = configuration.Features; + for (var index = 0; index < createdFeatures.Count; index++) { - Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _compiler = new HandlebarsCompiler(Configuration); - RegisterBuiltinHelpers(); + createdFeatures[index].OnCompiling(configuration); } - - public Func CompileView(string templatePath) + + var compiler = new HandlebarsCompiler(configuration); + var compiledView = compiler.CompileView(templatePath, configuration); + Func action = (vm) => { - var compiledView = _compiler.CompileView(templatePath); - return (vm) => + using (var writer = new PolledStringWriter(configuration.FormatProvider)) { - var sb = StringBuilderPool.Shared.GetObject(); - try - { - using (var tw = new StringWriter(sb)) - { - compiledView(tw, vm); - } - return sb.ToString(); - } - finally - { - StringBuilderPool.Shared.PutObject(sb); - } - }; + compiledView(writer, vm); + return writer.ToString(); + } + }; + + for (var index = 0; index < createdFeatures.Count; index++) + { + createdFeatures[index].CompilationCompleted(); } - public HandlebarsConfiguration Configuration { get; } + return action; + } - public Action Compile(TextReader template) + public HandlebarsConfiguration Configuration { get; } + + public Action Compile(TextReader template) + { + using (var reader = new ExtendedStringReader(template)) { - return _compiler.Compile(template); + var compiler = new HandlebarsCompiler(Configuration); + return compiler.Compile(reader); } + } - public Func Compile(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 => { - var compiledTemplate = Compile(reader); - return context => + using (var writer = new PolledStringWriter(Configuration.FormatProvider)) { - var builder = StringBuilderPool.Shared.GetObject(); - try - { - using (var writer = new StringWriter(builder)) - { - compiledTemplate(writer, context); - } - return builder.ToString(); - } - finally - { - StringBuilderPool.Shared.PutObject(builder); - } - }; - } + compiledTemplate(writer, context); + return writer.ToString(); + } + }; } + } - public void RegisterTemplate(string templateName, Action template) - { - lock (Configuration) - { - Configuration.RegisteredTemplates.AddOrUpdate(templateName, template); - } - } + public void RegisterTemplate(string templateName, Action template) + { + Configuration.RegisteredTemplates[templateName] = template; + } - public void RegisterTemplate(string templateName, string template) + public void RegisterTemplate(string templateName, string template) + { + using (var reader = new StringReader(template)) { - using (var reader = new StringReader(template)) - { - RegisterTemplate(templateName, Compile(reader)); - } + RegisterTemplate(templateName, Compile(reader)); } + } - public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) - { - lock (Configuration) - { - Configuration.Helpers.AddOrUpdate(helperName, helperFunction); - } - } + public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) + { + Configuration.Helpers[helperName] = helperFunction; + } - public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) - { - lock (Configuration) - { - Configuration.ReturnHelpers.AddOrUpdate(helperName, helperFunction); - } - } - - public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) - { - lock (Configuration) - { - Configuration.BlockHelpers.AddOrUpdate(helperName, helperFunction); - } - } + public void RegisterHelper(string helperName, HandlebarsReturnHelper helperFunction) + { + Configuration.ReturnHelpers[helperName] = helperFunction; + } - private void RegisterBuiltinHelpers() - { - foreach (var helperDefinition in BuiltinHelpers.Helpers) - { - RegisterHelper(helperDefinition.Key, helperDefinition.Value); - } - foreach (var helperDefinition in BuiltinHelpers.ReturnHelpers) - { - RegisterHelper(helperDefinition.Key, helperDefinition.Value); - } - foreach (var helperDefinition in BuiltinHelpers.BlockHelpers) - { - RegisterHelper(helperDefinition.Key, helperDefinition.Value); - } - } + 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..1f5f613f 100644 --- a/source/Handlebars/HandlebarsException.cs +++ b/source/Handlebars/HandlebarsException.cs @@ -2,17 +2,57 @@ 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}\nOccured at:{context.LineNumber}:{context.CharNumber}"; + } } } diff --git a/source/Handlebars/HandlebarsExtensions.cs b/source/Handlebars/HandlebarsExtensions.cs index 5a21eec5..2f0dba0f 100644 --- a/source/Handlebars/HandlebarsExtensions.cs +++ b/source/Handlebars/HandlebarsExtensions.cs @@ -1,20 +1,34 @@ -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()); } + [DebuggerDisplay("{_value}")] private class SafeString : ISafeString { private readonly string _value; @@ -30,5 +44,12 @@ public override string ToString() } } } + + /// + /// + /// + public interface ISafeString + { + } } diff --git a/source/Handlebars/HandlebarsRuntimeException.cs b/source/Handlebars/HandlebarsRuntimeException.cs index ea85cc33..a9978f82 100644 --- a/source/Handlebars/HandlebarsRuntimeException.cs +++ b/source/Handlebars/HandlebarsRuntimeException.cs @@ -1,20 +1,49 @@ 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 0bc995fe..de59b00a 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -5,13 +5,26 @@ namespace HandlebarsDotNet { + /// + /// + /// public static class HandlebarsUtils { + /// + /// Implementation of JS's `==` + /// + /// + /// public static bool IsTruthy(object value) { return !IsFalsy(value); } + /// + /// Implementation of JS's `!=` + /// + /// + /// public static bool IsFalsy(object value) { switch (value) @@ -32,22 +45,29 @@ public static bool IsFalsy(object 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() == false; } private static bool IsNumber(object value) diff --git a/source/Handlebars/HandlebarsViewEngine.cs b/source/Handlebars/HandlebarsViewEngine.cs index 02d87011..2f929b6f 100644 --- a/source/Handlebars/HandlebarsViewEngine.cs +++ b/source/Handlebars/HandlebarsViewEngine.cs @@ -1,13 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; namespace HandlebarsDotNet { + /// + /// + /// public abstract class ViewEngineFileSystem { + /// + /// + /// public abstract string GetFileContent(string filename); private static string GetDir(string currentFilePath) @@ -18,6 +21,9 @@ private static string GetDir(string currentFilePath) return string.Join("/", parts.Take(parts.Length - 1)); } + /// + /// + /// public string Closest(string filename, string otherFileName) { var dir = GetDir(filename); @@ -31,8 +37,14 @@ public string Closest(string filename, string otherFileName) return null; } + /// + /// + /// protected abstract string CombinePath(string dir, string otherFileName); + /// + /// + /// public abstract bool FileExists(string filePath); } } diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index 9e03b656..d34b167a 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,27 +1,46 @@ using System; -using System.Collections.Generic; using System.IO; using HandlebarsDotNet.Compiler; namespace HandlebarsDotNet { + /// + /// + /// + public delegate void BlockParamsConfiguration(ConfigureBlockParams blockParamsConfiguration, params object[] dependencies); + + /// + /// Contains properties accessible withing function + /// public sealed class HelperOptions { internal HelperOptions( Action template, Action inverse, - BlockParamsValueProvider blockParamsValueProvider) + BlockParamsValueProvider blockParamsValueProvider, + HandlebarsConfiguration configuration) { Template = template; Inverse = inverse; + Configuration = configuration; BlockParams = blockParamsValueProvider.Configure; } + /// + /// BlockHelper body + /// public Action Template { get; } + /// + /// BlockHelper else body + /// public Action Inverse { get; } - public Action BlockParams { get; } + /// + public BlockParamsConfiguration BlockParams { get; } + + /// + internal HandlebarsConfiguration Configuration { get; } } } 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..4201157b --- /dev/null +++ b/source/Handlebars/IExpressionCompiler.cs @@ -0,0 +1,19 @@ +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..2b5de221 --- /dev/null +++ b/source/Handlebars/IExpressionMiddleware.cs @@ -0,0 +1,17 @@ +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 f6e88278..ab41de16 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -1,28 +1,73 @@ 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); + /// + /// + /// HandlebarsConfiguration Configuration { get; } + /// + /// + /// + /// + /// void RegisterTemplate(string templateName, Action template); + /// + /// + /// + /// + /// void RegisterTemplate(string templateName, string template); + /// + /// + /// + /// + /// + [Obsolete("Consider switching to HandlebarsReturnHelper")] 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 57% rename from source/Handlebars/EncodedTextWriter.cs rename to source/Handlebars/IO/EncodedTextWriter.cs index d9b8a7cf..52322967 100644 --- a/source/Handlebars/EncodedTextWriter.cs +++ b/source/Handlebars/IO/EncodedTextWriter.cs @@ -1,32 +1,17 @@ -using System; -using System.IO; +using System.IO; using System.Text; namespace HandlebarsDotNet { - internal class PolledStringWriter : StringWriter - { - public PolledStringWriter() : base(StringBuilderPool.Shared.GetObject()) - { - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - StringBuilderPool.Shared.PutObject(base.GetStringBuilder()); - } - } - 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; } @@ -44,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) @@ -68,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/IO/ITextEncoder.cs b/source/Handlebars/IO/ITextEncoder.cs new file mode 100644 index 00000000..3ac09778 --- /dev/null +++ b/source/Handlebars/IO/ITextEncoder.cs @@ -0,0 +1,15 @@ +namespace HandlebarsDotNet +{ + /// + /// Encoder used for output encoding. + /// + public interface ITextEncoder + { + /// + /// + /// + /// + /// + string Encode(string value); + } +} \ No newline at end of file diff --git a/source/Handlebars/IO/PolledStringWriter.cs b/source/Handlebars/IO/PolledStringWriter.cs new file mode 100644 index 00000000..ad8e18c1 --- /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.GetObject()) + { + + } + + public PolledStringWriter(IFormatProvider formatProvider) : base(StringBuilderPool.Shared.GetObject(), formatProvider) + { + } + + protected override void Dispose(bool disposing) + { + StringBuilderPool.Shared.PutObject(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/ITextEncoder.cs b/source/Handlebars/ITextEncoder.cs deleted file mode 100644 index 953196b2..00000000 --- a/source/Handlebars/ITextEncoder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace HandlebarsDotNet -{ - public interface ITextEncoder - { - string Encode(string value); - } -} \ No newline at end of file diff --git a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs new file mode 100644 index 00000000..5f71e9ee --- /dev/null +++ b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs @@ -0,0 +1,15 @@ +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; + return bindingContext.TryGetContextVariable(ref ChainSegment.Create(memberName), 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..9607779b --- /dev/null +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -0,0 +1,193 @@ +using System; +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 CompiledReflectionMemberAccessor() + : (IMemberAccessor) new PlainReflectionMemberAccessor(); + } + + 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 class PlainReflectionMemberAccessor : IMemberAccessor + { + private readonly RefLookup> _descriptors = + new RefLookup>(); + + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + ObjectTypeDescriptor descriptor; + if (_descriptors.ContainsKey(instanceType)) + { + ref var deferredValue = ref _descriptors.GetValueOrDefault(instanceType); + descriptor = deferredValue.Value; + }else + { + ref var deferredValue = ref _descriptors.GetOrAdd(instanceType, ValueFactory); + descriptor = deferredValue.Value; + } + + var accessor = descriptor.GetOrCreateAccessor(memberName); + value = accessor?.Invoke(instance); + return accessor != null; + } + + private static ref DeferredValue ValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => new RawObjectTypeDescriptor(type); + return ref deferredValue; + } + } + + private class CompiledReflectionMemberAccessor : IMemberAccessor + { + private readonly RefLookup> _descriptors = + new RefLookup>(); + + public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + { + ObjectTypeDescriptor descriptor; + if (_descriptors.ContainsKey(instanceType)) + { + ref var deferredValue = ref _descriptors.GetValueOrDefault(instanceType); + descriptor = deferredValue.Value; + } + else + { + ref var deferredValue = ref _descriptors.GetOrAdd(instanceType, ValueFactory); + descriptor = deferredValue.Value; + } + + var accessor = descriptor.GetOrCreateAccessor(memberName); + value = accessor?.Invoke(instance); + return accessor != null; + } + + private static ref DeferredValue ValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => new CompiledObjectTypeDescriptor(type); + return ref deferredValue; + } + } + + private class CompiledObjectTypeDescriptor : ObjectTypeDescriptor + { + public CompiledObjectTypeDescriptor(Type type) : base(type) + { + } + + protected override 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; + } + } + + private class RawObjectTypeDescriptor : ObjectTypeDescriptor + { + public RawObjectTypeDescriptor(Type type) : base(type) + { + } + + protected override 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 o => property.GetValue(o); + } + + 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 abstract class ObjectTypeDescriptor + { + private readonly Type _type; + + private readonly RefLookup>> _accessors = + new RefLookup>>(); + + protected ObjectTypeDescriptor(Type type) + { + _type = type; + } + + public Func GetOrCreateAccessor(string name) + { + if (_accessors.ContainsKey(name)) + { + ref var existing = ref _accessors.GetValueOrDefault(name); + return existing.Value; + } + + ref var deferredValue = ref _accessors.GetOrAdd(name, ValueFactory); + return deferredValue.Value; + } + + private ref DeferredValue> ValueFactory(string name, ref DeferredValue> deferredValue) + { + deferredValue.Factory = () => GetValueGetter(name, _type); + return ref deferredValue; + } + + protected abstract Func GetValueGetter(string name, Type type); + } + } +} \ 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..ed47f114 --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; +using System.Collections.Generic; +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) + { + ref var segment = ref ChainSegment.Create(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(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..1ea59ce2 --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/DelegatedMemberAliasProvider.cs @@ -0,0 +1,75 @@ +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..ccc0386a --- /dev/null +++ b/source/Handlebars/MemberAliasProvider/IMemberAliasProvider.cs @@ -0,0 +1,20 @@ +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..3448e189 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs @@ -0,0 +1,54 @@ +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; + + value.ShouldEnumerate = true; + value.MemberAccessor = new MergedMemberAccessor(new EnumerableMemberAccessor(), value.MemberAccessor); + + 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..8b8fc2e5 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs @@ -0,0 +1,29 @@ +using System; +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", "value" }; + + private static readonly ObjectDescriptor Descriptor = new ObjectDescriptor(BindingContextType) + { + MemberAccessor = new ContextMemberAccessor(), + GetProperties = o => Properties + }; + + 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..abb43b19 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class DictionaryObjectDescriptor : IObjectDescriptorProvider + { + public bool CanHandleType(Type type) + { + return typeof(IDictionary).IsAssignableFrom(type); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = new ObjectDescriptor(type) + { + GetProperties = GetProperties, + MemberAccessor = new DictionaryMemberAccessor() + }; + + return true; + } + + private static IEnumerable GetProperties(object arg) + { + var dictionary = (IDictionary) arg; + foreach (var key in dictionary.Keys) + { + yield return key; + } + } + } +} \ 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..7aa78a4f --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs @@ -0,0 +1,27 @@ +using System; +using System.Dynamic; +using System.Linq.Expressions; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class DynamicObjectDescriptor : IObjectDescriptorProvider + { + public bool CanHandleType(Type type) + { + return typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = new ObjectDescriptor(type) + { + GetProperties = o => ((IDynamicMetaObjectProvider) o).GetMetaObject(Expression.Constant(o)).GetDynamicMemberNames(), + MemberAccessor = new DynamicMemberAccessor() + }; + + 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..2a952eba --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal sealed class GenericDictionaryObjectDescriptorProvider : IObjectDescriptorProvider + { + private static readonly object[] EmptyArray = new object[0]; + + private readonly RefLookup> _typeCache = new RefLookup>(); + + public bool CanHandleType(Type type) + { + ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); + return deferredValue.Value != null; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = null; + var interfaceType = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; + if (interfaceType == null) return false; + + var descriptorCreator = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) + ?.MakeGenericMethod(interfaceType.GetGenericArguments()); + + value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); + return value != null; + } + + private static ref DeferredValue InterfaceTypeValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => + { + return type.GetInterfaces() + .Where(i => i.GetTypeInfo().IsGenericType) + .Where(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + .FirstOrDefault(i => + TypeDescriptor.GetConverter(i.GetGenericArguments()[0]).CanConvertFrom(typeof(string)) + ); + }; + + return ref deferredValue; + } + + private static ObjectDescriptor CreateDescriptor() + { + return new ObjectDescriptor(typeof(IDictionary)) + { + GetProperties = o => ((IDictionary) o).Keys.Cast(), + MemberAccessor = new DictionaryAccessor() + }; + } + + 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..3eafc256 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs @@ -0,0 +1,24 @@ +using System; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + /// + /// Factory 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..f4f3c76a --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal sealed class KeyValuePairObjectDescriptorProvider : IObjectDescriptorProvider + { + private static readonly string[] Properties = { "key", "value" }; + private static readonly object[] EmptyArray = new object[0]; + + 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 = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) + ?.MakeGenericMethod(genericArguments[0], genericArguments[1]); + + value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); + return value != null; + } + + private static ObjectDescriptor CreateDescriptor() + { + return new ObjectDescriptor(typeof(KeyValuePair)) + { + GetProperties = o => Properties, + MemberAccessor = new KeyValuePairAccessor() + }; + } + + 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..36dbd65e --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + /// + /// Provides meta-information about + /// + public class ObjectDescriptor + { + /// + /// + /// + /// + public ObjectDescriptor(Type describedType) + { + DescribedType = describedType; + } + + /// + /// Specifies whether the type should be treated as + /// + public bool ShouldEnumerate { get; set; } + + /// + /// Returns type described by this instance of + /// + public Type DescribedType { get; } + + /// + /// Factory enabling receiving properties of specific instance + /// + public Func> GetProperties { get; set; } + + /// + /// associated with the + /// + public IMemberAccessor MemberAccessor { get; set; } + } +} \ 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..bf532966 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using HandlebarsDotNet.Collections; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal class ObjectDescriptorFactory : IObjectDescriptorProvider + { + private readonly IList _providers; + private readonly RefLookup> _descriptors = new RefLookup>(); + private readonly RefLookup> _supportedTypes = new RefLookup>(); + + public ObjectDescriptorFactory(IList providers) + { + _providers = providers; + } + + public bool CanHandleType(Type type) + { + if (_supportedTypes.ContainsKey(type)) + { + ref var contains = ref _supportedTypes.GetValueOrDefault(type); + return contains.Value; + } + + ref var deferredValue = ref _supportedTypes.GetOrAdd(type, SupportedTypesValueFactory); + return deferredValue.Value; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = null; + ObjectDescriptor descriptor; + if (_descriptors.ContainsKey(type)) + { + ref var existingDeferredValue = ref _descriptors.GetValueOrDefault(type); + descriptor = existingDeferredValue.Value; + } + else + { + ref var deferredValue = ref _descriptors.GetOrAdd(type, DescriptorsValueFactory); + descriptor = deferredValue.Value; + } + + if (descriptor == null) return false; + + value = descriptor; + return true; + } + + private ref DeferredValue SupportedTypesValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => + { + for (var index = 0; index < _providers.Count; index++) + { + if (_providers[index].CanHandleType(type)) return true; + } + + return false; + }; + return ref deferredValue; + } + + private ref DeferredValue DescriptorsValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => + { + for (var index = 0; index < _providers.Count; index++) + { + var provider = _providers[index]; + if (!provider.CanHandleType(type)) continue; + if (provider.TryGetDescriptor(type, out var value)) + { + return value; + } + } + + return null; + }; + + return ref deferredValue; + } + } +} \ 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..236e1279 --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -0,0 +1,55 @@ +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 static readonly Type DynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); + + private readonly InternalHandlebarsConfiguration _configuration; + private readonly RefLookup> _membersCache = new RefLookup>(); + + public ObjectDescriptorProvider(InternalHandlebarsConfiguration configuration) + { + _configuration = configuration; + } + + public bool CanHandleType(Type type) + { + return !DynamicMetaObjectProviderType.IsAssignableFrom(type) + && type != typeof(string); + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + ref var members = ref _membersCache.GetOrAdd(type, DescriptorValueFactory); + var membersValue = members.Value; + + value = new ObjectDescriptor(type) + { + GetProperties = o => membersValue, + MemberAccessor = new ReflectionMemberAccessor(_configuration) + }; + + return true; + } + + private static ref DeferredValue DescriptorValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(o => o.CanRead && o.GetIndexParameters().Length == 0); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + return properties.Cast().Concat(fields).Select(o => o.Name).ToArray(); + }; + + return ref deferredValue; + } + } +} \ 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..4135233e --- /dev/null +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HandlebarsDotNet.Collections; +using HandlebarsDotNet.MemberAccessors; + +namespace HandlebarsDotNet.ObjectDescriptors +{ + internal sealed class StringDictionaryObjectDescriptorProvider : IObjectDescriptorProvider + { + private readonly RefLookup> _typeCache = new RefLookup>(); + private static readonly object[] EmptyArray = new object[0]; + + public bool CanHandleType(Type type) + { + ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); + return deferredValue.Value != null; + } + + public bool TryGetDescriptor(Type type, out ObjectDescriptor value) + { + value = null; + ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); + var interfaceType = deferredValue.Value; + + if (interfaceType == null) return false; + + var descriptorCreator = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) + ?.MakeGenericMethod(interfaceType.GetGenericArguments()[1]); + + value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); + return value != null; + } + + private static ref DeferredValue InterfaceTypeValueFactory(Type type, ref DeferredValue deferredValue) + { + deferredValue.Factory = () => + { + return type.GetInterfaces() + .FirstOrDefault(i => + i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && + i.GetGenericArguments()[0] == typeof(string)); + }; + + return ref deferredValue; + } + + private static ObjectDescriptor CreateDescriptor() + { + return new ObjectDescriptor(typeof(IDictionary)) + { + GetProperties = o => ((IDictionary) o).Keys, + MemberAccessor = new DictionaryAccessor() + }; + } + + 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/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..4aef53df --- /dev/null +++ b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs @@ -0,0 +1,59 @@ +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; + + case "value": + value = _context.Value; + 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(); + if(_context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) + { + if (descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment.Value, 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..5c0d9fee --- /dev/null +++ b/source/Handlebars/ValueProviders/IteratorValueProvider.cs @@ -0,0 +1,70 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet.ValueProviders +{ + internal class IteratorValueProvider : IValueProvider + { + private static readonly IteratorValueProviderPool Pool = new IteratorValueProviderPool(); + + public static IteratorValueProvider Create() + { + return Pool.GetObject(); + } + + 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; + } + } + + private class IteratorValueProviderPool : ObjectPool + { + protected override IteratorValueProvider CreateObject() + { + return new IteratorValueProvider(); + } + + public override void PutObject(IteratorValueProvider item) + { + item.First = false; + item.Last = false; + item.Index = 0; + item.Value = null; + + base.PutObject(item); + } + } + + public void Dispose() + { + Pool.PutObject(this); + } + } +} \ 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..56c71aec --- /dev/null +++ b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs @@ -0,0 +1,57 @@ +using HandlebarsDotNet.Compiler.Structure.Path; + +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.GetObject(); + 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); + } + } + + private class ObjectEnumeratorValueProviderPool : ObjectPool + { + protected override ObjectEnumeratorValueProvider CreateObject() + { + return new ObjectEnumeratorValueProvider(); + } + + public override void PutObject(ObjectEnumeratorValueProvider item) + { + item.First = false; + item.Last = false; + item.Index = 0; + item.Value = null; + item.Key = null; + item._configuration = null; + + base.PutObject(item); + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/hbnet-icon.png b/source/Handlebars/hbnet-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7f409048ca3a946081fbf87f1ad875e54192d827 GIT binary patch literal 2572 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 i5=$(x#1cy^5%~wDe*lG>Y1U)_0000 Date: Thu, 23 Apr 2020 11:31:07 -0700 Subject: [PATCH 16/53] Update readme --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a00867ff..f8eda475 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) +#handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) + ============== -_This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while and I was not able to reach @rexm._ + +_This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while. I'd be glad to back-merge all the changes back to original repo if I'd have a chance. Meanwhile I'd try to support the fork._ Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET application. From 1165fa2bc774063a13017cd40107e6e6ff411766 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Thu, 23 Apr 2020 10:39:18 -0700 Subject: [PATCH 17/53] Update `Handlebars.Code.sln` --- source/Handlebars.Code.sln | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/Handlebars.Code.sln b/source/Handlebars.Code.sln index eb0f0ef4..c431dd94 100644 --- a/source/Handlebars.Code.sln +++ b/source/Handlebars.Code.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.Logger", "Handlebars.Extension.Logger\Handlebars.Extension.Logger.csproj", "{EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.Build.0 = Debug|Any CPU {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.ActiveCfg = Release|Any CPU {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.Build.0 = Release|Any CPU + {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 26a1a8bc1e2ec18ecc7ffa9c101609f02f162d2a Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Thu, 23 Apr 2020 11:18:26 -0700 Subject: [PATCH 18/53] Add performance results --- Performance.md | 138 +++++++++++++++++++++++++ source/Handlebars.Benchmark/Program.cs | 2 +- source/Handlebars.Code.sln | 1 + source/Handlebars.sln | 1 + 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 Performance.md diff --git a/Performance.md b/Performance.md new file mode 100644 index 00000000..7d47268c --- /dev/null +++ b/Performance.md @@ -0,0 +1,138 @@ +#Performance + +### Setup + +#### Template + +```handlebars +childCount={{level1.Count}} +childCount2={{level1.Count}} +{{#each level1}} + id={{id}} + childCount={{level2.Count}} + childCount2={{level2.Count}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level2}} + id={{id}} + childCount={{level3.Count}} + childCount2={{level3.Count}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{#each level3}} + id={{id}} + index=[{{@../../index}}:{{@../index}}:{{@index}}] + first=[{{@../../first}}:{{@../first}}:{{@first}}] + last=[{{@../../last}}:{{@../last}}:{{@last}}] + {{/each}} + {{/each}} +{{/each}} +``` + +#### Input example + +```json +{ + "level1": [ + { + "id": "0", + "level2": [ + { + "id": "0-0", + "level3": [ + { + "id": "0-0-0" + } + ] + } + ] + } + ] +} +``` + +#### Environment + +``` ini + +BenchmarkDotNet=v0.12.0, OS=Windows 10.0.17763.973 (1809/October2018Update/Redstone5), VM=Hyper-V +Intel Xeon CPU E5-2640 v3 2.60GHz, 1 CPU, 16 logical and 16 physical cores +.NET Core SDK=3.1.100 + [Host] : .NET Core 2.1.13 (CoreCLR 4.6.28008.01, CoreFX 4.6.28008.01), X64 RyuJIT + Job-GBTPJU : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + +Runtime=.NET Core 3.1 + +``` + +#### Legend + +##### Version +`1.10.1` - Handlebars.Net version + +`current` - current handlebars.csharp version + +`current-cache` - current handlebars.csharp version with aggressive caching (`Default`) + +`current-fast` - current handlebars.csharp version with `CompileFast` feature + +`current-fast-cache` - current handlebars.csharp version with `CompileFast` feature and aggressive caching + +##### N + +Number of items on each level of the input. + +### Results + +#### Compilation + +| Method | Version | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|--------- |------------- |----------:|----------:|----------:|---------:|---------:|------:|----------:| +| **Template** | **1.10.1** | **26.270 ms** | **0.4970 ms** | **0.4406 ms** | **375.0000** | **187.5000** | **-** | **3900.8 KB** | +| **Template** | **current** | **13.723 ms** | **0.2681 ms** | **0.2238 ms** | **62.5000** | **31.2500** | **-** | **735.62 KB** | +| **Template** | **current-fast** | **3.078 ms** | **0.0615 ms** | **0.0862 ms** | **78.1250** | **39.0625** | **-** | **804.66 KB** | + +#### Execution + +| Method | N | Version | DataType | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|---------------- |--- |------------------- |----------- |-------------:|-----------:|-----------:|---------:|---------:|---------:|-----------:| +| **WithParentIndex** | **2** | **1.10.1** | **dictionary** | **385.33 us** | **6.026 us** | **5.342 us** | **11.7188** | **0.4883** | **-** | **120.58 KB** | +| **WithParentIndex** | **2** | **1.10.1** | **object** | **363.27 us** | **7.111 us** | **6.984 us** | **9.7656** | **0.4883** | **-** | **102.74 KB** | +| **WithParentIndex** | **2** | **current** | **dictionary** | **49.35 us** | **0.973 us** | **1.396 us** | **1.0376** | **-** | **-** | **11.19 KB** | +| **WithParentIndex** | **2** | **current** | **object** | **63.92 us** | **1.221 us** | **1.454 us** | **1.0986** | **-** | **-** | **11.61 KB** | +| **WithParentIndex** | **2** | **current-cache** | **dictionary** | **45.84 us** | **0.810 us** | **0.718 us** | **1.0376** | **-** | **-** | **11.19 KB** | +| **WithParentIndex** | **2** | **current-cache** | **object** | **54.90 us** | **1.509 us** | **1.338 us** | **1.0986** | **-** | **-** | **11.61 KB** | +| **WithParentIndex** | **2** | **current-fast** | **dictionary** | **52.31 us** | **1.005 us** | **1.307 us** | **1.0376** | **-** | **-** | **11.19 KB** | +| **WithParentIndex** | **2** | **current-fast** | **object** | **64.17 us** | **1.238 us** | **1.325 us** | **1.0986** | **-** | **-** | **11.61 KB** | +| **WithParentIndex** | **2** | **current-fast-cache** | **dictionary** | **43.94 us** | **0.863 us** | **0.765 us** | **1.0376** | **-** | **-** | **11.19 KB** | +| **WithParentIndex** | **2** | **current-fast-cache** | **object** | **53.77 us** | **0.840 us** | **0.744 us** | **1.0986** | **-** | **-** | **11.61 KB** | + + +| Method | N | Version | DataType | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|---------------- |--- |------------------- |----------- |-------------:|-----------:|-----------:|---------:|---------:|---------:|-----------:| +| **WithParentIndex** | **5** | **1.10.1** | **dictionary** | **3,436.11 us** | **68.128 us** | **88.585 us** | **101.5625** | **23.4375** | **-** | **1111.08 KB** | +| **WithParentIndex** | **5** | **1.10.1** | **object** | **3,492.48 us** | **68.605 us** | **93.907 us** | **93.7500** | **23.4375** | **-** | **1002.9 KB** | +| **WithParentIndex** | **5** | **current** | **dictionary** | **369.02 us** | **7.016 us** | **6.563 us** | **9.7656** | **1.4648** | **-** | **103.43 KB** | +| **WithParentIndex** | **5** | **current** | **object** | **496.41 us** | **9.794 us** | **14.659 us** | **9.7656** | **0.9766** | **-** | **105.54 KB** | +| **WithParentIndex** | **5** | **current-cache** | **dictionary** | **356.12 us** | **6.967 us** | **7.744 us** | **9.7656** | **1.4648** | **-** | **103.43 KB** | +| **WithParentIndex** | **5** | **current-cache** | **object** | **418.22 us** | **7.863 us** | **8.075 us** | **10.2539** | **1.4648** | **-** | **105.54 KB** | +| **WithParentIndex** | **5** | **current-fast** | **dictionary** | **375.19 us** | **6.064 us** | **5.672 us** | **9.7656** | **1.4648** | **-** | **103.43 KB** | +| **WithParentIndex** | **5** | **current-fast** | **object** | **479.95 us** | **9.304 us** | **8.248 us** | **9.7656** | **0.9766** | **-** | **105.54 KB** | +| **WithParentIndex** | **5** | **current-fast-cache** | **dictionary** | **350.70 us** | **7.537 us** | **7.050 us** | **9.7656** | **1.4648** | **-** | **103.43 KB** | +| **WithParentIndex** | **5** | **current-fast-cache** | **object** | **403.11 us** | **6.821 us** | **6.381 us** | **10.2539** | **1.4648** | **-** | **105.54 KB** | + + +| Method | N | Version | DataType | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|---------------- |--- |------------------- |----------- |-------------:|-----------:|-----------:|---------:|---------:|---------:|-----------:| +| **WithParentIndex** | **10** | **1.10.1** | **dictionary** | **25,667.09 us** | **572.214 us** | **723.667 us** | **687.5000** | **250.0000** | **93.7500** | **7482.11 KB** | +| **WithParentIndex** | **10** | **1.10.1** | **object** | **22,741.41 us** | **437.277 us** | **486.033 us** | **656.2500** | **218.7500** | **125.0000** | **6919.99 KB** | +| **WithParentIndex** | **10** | **current** | **dictionary** | **2,513.83 us** | **50.250 us** | **53.767 us** | **136.7188** | **109.3750** | **109.3750** | **701.46 KB** | +| **WithParentIndex** | **10** | **current** | **object** | **2,827.85 us** | **55.088 us** | **77.226 us** | **136.7188** | **109.3750** | **109.3750** | **709.18 KB** | +| **WithParentIndex** | **10** | **current-cache** | **dictionary** | **2,203.95 us** | **43.770 us** | **48.650 us** | **136.7188** | **109.3750** | **109.3750** | **701.43 KB** | +| **WithParentIndex** | **10** | **current-cache** | **object** | **2,530.40 us** | **49.594 us** | **81.485 us** | **136.7188** | **109.3750** | **109.3750** | **709.24 KB** | +| **WithParentIndex** | **10** | **current-fast** | **dictionary** | **2,264.14 us** | **45.091 us** | **83.578 us** | **136.7188** | **109.3750** | **109.3750** | **701.53 KB** | +| **WithParentIndex** | **10** | **current-fast** | **object** | **2,906.42 us** | **57.859 us** | **91.770 us** | **136.7188** | **109.3750** | **109.3750** | **709.22 KB** | +| **WithParentIndex** | **10** | **current-fast-cache** | **dictionary** | **2,164.81 us** | **40.893 us** | **45.452 us** | **136.7188** | **109.3750** | **109.3750** | **701.49 KB** | +| **WithParentIndex** | **10** | **current-fast-cache** | **object** | **2,497.93 us** | **49.739 us** | **88.412 us** | **136.7188** | **109.3750** | **109.3750** | **709.18 KB** | \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs index a7561f29..75b9c312 100644 --- a/source/Handlebars.Benchmark/Program.cs +++ b/source/Handlebars.Benchmark/Program.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; using HandlebarsDotNet; diff --git a/source/Handlebars.Code.sln b/source/Handlebars.Code.sln index c431dd94..f0d513a6 100644 --- a/source/Handlebars.Code.sln +++ b/source/Handlebars.Code.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" ProjectSection(SolutionItems) = preProject ..\README.md = ..\README.md + ..\Performance.md = ..\Performance.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 0f569a6c..7d915844 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -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 + ..\Performance.md = ..\Performance.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{E880C14C-96EE-4A1E-98BD-6348AB529090}" From 380fe48d8c55655b508021fbaa9e7e061b72972e Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 08:48:11 -0700 Subject: [PATCH 19/53] Fixing problems + improvements - Use `ObjectPool` from NuGet - Repack of IL to prevent additional dependencies - Path segment proper caching - Add test coverage --- .github/workflows/ci.yml | 5 +- .github/workflows/pull_request.yml | 4 +- README.md | 3 + .../Handlebars.Extension.CompileFast.csproj | 10 +- .../Handlebars.Extension.Logger.csproj | 2 +- .../Handlebars.Test/BasicIntegrationTests.cs | 4 +- .../ComplexIntegrationTests.cs | 4 +- source/Handlebars.Test/Handlebars.Test.csproj | 34 ++- source/Handlebars.Test/IteratorTests.cs | 4 +- .../Collections/DisposableContainer.cs | 4 +- source/Handlebars/Collections/HashHelpers.cs | 86 +++++++ .../Handlebars/Collections/IRefDictionary.cs | 9 + source/Handlebars/Collections/ObjectPool.cs | 29 +-- .../Handlebars/Collections/RefDictionary.cs | 85 +------ .../Collections/RefDictionarySafe.cs | 232 ++++++++++++++++++ source/Handlebars/Collections/RefLookup.cs | 7 +- .../Collections/StringBuilderPool.cs | 20 +- .../Lexer/Parsers/BlockParamsParser.cs | 10 +- .../Compiler/Lexer/Parsers/BlockWordParser.cs | 8 +- .../Compiler/Lexer/Parsers/CommentParser.cs | 8 +- .../Compiler/Lexer/Parsers/LiteralParser.cs | 32 +-- .../Compiler/Lexer/Parsers/WordParser.cs | 10 +- source/Handlebars/Compiler/Lexer/Tokenizer.cs | 9 +- .../Compiler/Structure/BindingContext.cs | 50 ++-- .../Structure/BlockParamsValueProvider.cs | 32 ++- .../Compiler/Structure/Path/ChainSegment.cs | 3 +- .../Expression/BlockHelperFunctionBinder.cs | 2 +- source/Handlebars/EnumerableExtensions.cs | 9 - .../Features/BuildInHelpersFeature.cs | 3 +- source/Handlebars/Handlebars.csproj | 54 +++- source/Handlebars/IO/PolledStringWriter.cs | 6 +- .../ObjectDescriptorProvider.cs | 5 +- ...tringDictionaryObjectDescriptorProvider.cs | 3 +- source/Handlebars/ObjectExtensions.cs | 33 +++ .../ValueProviders/IteratorValueProvider.cs | 40 +-- .../ObjectEnumeratorValueProvider.cs | 41 ++-- 36 files changed, 608 insertions(+), 292 deletions(-) create mode 100644 source/Handlebars/Collections/HashHelpers.cs create mode 100644 source/Handlebars/Collections/IRefDictionary.cs create mode 100644 source/Handlebars/Collections/RefDictionarySafe.cs create mode 100644 source/Handlebars/ObjectExtensions.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9166b109..1ff65a2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,14 +43,15 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect "code coverage" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee beginArguments: > /d:sonar.verbose="true" - /d:sonar.cs.opencover.reportsPaths='"*.opencover.xml"' + /d:sonar.cs.opencover.reportsPaths='"**/*.opencover.xml"' /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' + /d:sonar.cs.vstest.reportsPaths='**/*.trx' env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bf218810..7235ac04 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -39,12 +39,14 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect "code coverage" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee beginArguments: > /d:sonar.verbose="true" + /d:sonar.cs.opencover.reportsPaths='"**/*.opencover.xml"' + /d:sonar.cs.vstest.reportsPaths='**/*.trx' /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' /d:sonar.pullrequest.key=${{ github.event.number }} /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} diff --git a/README.md b/README.md index f8eda475..48973653 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,9 @@ Nearly all time spent in rendering is in the routine that resolves values agains ~~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.~~ +### More +For mo retails see [Performance measurements](Performance.md) + ## Future roadmap TBD diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index 3ff2387b..d01420e6 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -1,7 +1,7 @@ - net452;netstandard1.3;netstandard2.0 + netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.CompileFast 7 1.0.0 @@ -13,7 +13,7 @@ Copyright © 2020 Oleh Formaniuk FastExpressionCompiler adapter for handlebars.csharp hbnet-extension-icon.png - handlebars.extension.compilefast + Handlebars.Extension.CompileFast https://opensource.org/licenses/mit https://github.com/zjklee/handlebars.csharp handlebars;mustache;templating;engine;compiler @@ -37,11 +37,7 @@ - - - - - + diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj index 5edb4834..c66a9dbb 100644 --- a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj +++ b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj @@ -14,7 +14,7 @@ Copyright © 2020 Oleh Formaniuk Microsoft.Extensions.Logging adapter for handlebars.csharp hbnet-extension-icon.png - handlebars.extension.logging + Handlebars.Extension.Logging https://opensource.org/licenses/mit https://github.com/zjklee/handlebars.csharp handlebars;mustache;templating;engine;compiler diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index bc878d1b..4fbfeff8 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -20,7 +20,9 @@ public class HandlebarsEnvGenerator : IEnumerable { Handlebars.Create(), Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), - Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), +#if compileFast + Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), +#endif Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { types.Add(typeof(Dictionary)); diff --git a/source/Handlebars.Test/ComplexIntegrationTests.cs b/source/Handlebars.Test/ComplexIntegrationTests.cs index 19554726..fcbf371c 100644 --- a/source/Handlebars.Test/ComplexIntegrationTests.cs +++ b/source/Handlebars.Test/ComplexIntegrationTests.cs @@ -195,7 +195,7 @@ public void CountyHasTwoValue() County = new[] { "Kane", "Salt Lake" } }; - var template = Handlebars.Compile(naturalLanguageListTemplate); + var template = Handlebars.Create().Compile(naturalLanguageListTemplate); var result = template(data); @@ -210,7 +210,7 @@ public void CountyHasMoreThanTwoValue() County = new[] { "Kane", "Salt Lake", "Weber" } }; - var template = Handlebars.Compile(naturalLanguageListTemplate); + var template = Handlebars.Create().Compile(naturalLanguageListTemplate); var result = template(data); diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index a3575d71..f2a42b3a 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - net461;netcoreapp1.1;netcoreapp2.0;netcoreapp3.1 + net461;netcoreapp1.1;netcoreapp2.1;netcoreapp3.1 700AF0B4-EA70-47B7-9F9D-17351E977B00 @@ -10,13 +10,19 @@ 0618;1701 + + $(DefineConstants);netFramework + + $(DefineConstants);compileFast $(DefineConstants);netstandard - + + $(DefineConstants);compileFast $(DefineConstants);netstandard + $(DefineConstants);compileFast $(DefineConstants);netstandard @@ -26,7 +32,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -35,22 +44,25 @@ - + + + - - + + + - - - - + + + + - + diff --git a/source/Handlebars.Test/IteratorTests.cs b/source/Handlebars.Test/IteratorTests.cs index a6ad6a20..7cebd84e 100644 --- a/source/Handlebars.Test/IteratorTests.cs +++ b/source/Handlebars.Test/IteratorTests.cs @@ -9,7 +9,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 +86,7 @@ public void WithParentIndex() {{/each}} {{/each}} {{/each}}"; - var template = Handlebars.Compile( source ); + var template = Handlebars.Create().Compile( source ); var data = new { level1 = new[]{ diff --git a/source/Handlebars/Collections/DisposableContainer.cs b/source/Handlebars/Collections/DisposableContainer.cs index 94d83406..5d0e795b 100644 --- a/source/Handlebars/Collections/DisposableContainer.cs +++ b/source/Handlebars/Collections/DisposableContainer.cs @@ -2,10 +2,10 @@ namespace HandlebarsDotNet { - internal class DisposableContainer : IDisposable + internal struct DisposableContainer : IDisposable { private readonly Action _onDispose; - public T Value { get; } + public readonly T Value; public DisposableContainer(T value, Action onDispose) { diff --git a/source/Handlebars/Collections/HashHelpers.cs b/source/Handlebars/Collections/HashHelpers.cs new file mode 100644 index 00000000..15620c15 --- /dev/null +++ b/source/Handlebars/Collections/HashHelpers.cs @@ -0,0 +1,86 @@ +using System; + +namespace HandlebarsDotNet.Collections +{ + internal static class HashHelpers + { + // This is the maximum prime smaller than Array.MaxArrayLength + private const int MaxPrimeArrayLength = 0x7FEFFFFD; + + private const int HashPrime = 101; + + // Table of prime numbers to use as hash table sizes. + // A typical resize algorithm would pick the smallest prime number in this array + // that is larger than twice the previous capacity. + // Suppose our Hashtable currently has capacity x and enough elements are added + // such that a resize needs to occur. Resizing first computes 2x then finds the + // first prime in the table greater than 2x, i.e. if primes are ordered + // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n. + // Doubling is important for preserving the asymptotic complexity of the + // hashtable operations such as add. Having a prime guarantees that double + // hashing does not lead to infinite loops. IE, your hash function will be + // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. + // We prefer the low computation costs of higher prime numbers over the increased + // memory allocation of a fixed prime number i.e. when right sizing a HashSet. + private static readonly int[] Primes = + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 + }; + + private static bool IsPrime(int candidate) + { + if ((candidate & 1) == 0) return candidate == 2; + + var limit = (int) Math.Sqrt(candidate); + for (var divisor = 3; divisor <= limit; divisor += 2) + { + if ((candidate % divisor) == 0) + return false; + } + + return true; + } + + public static int GetPrime(int min) + { + if (min < 0) + throw new ArgumentException(); + + for (var i = 0; i < Primes.Length; i++) + { + var prime = Primes[i]; + if (prime >= min) + return prime; + } + + //outside of our predefined table. + //compute the hard way. + for (var i = (min | 1); i < Int32.MaxValue; i += 2) + { + if (IsPrime(i) && ((i - 1) % HashPrime != 0)) + return i; + } + + return min; + } + + // Returns size of hashtable to grow to. + public static int ExpandPrime(int oldSize) + { + var newSize = 2 * oldSize; + + // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint) newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) + { + return MaxPrimeArrayLength; + } + + return GetPrime(newSize); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/IRefDictionary.cs b/source/Handlebars/Collections/IRefDictionary.cs new file mode 100644 index 00000000..5c645272 --- /dev/null +++ b/source/Handlebars/Collections/IRefDictionary.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace HandlebarsDotNet.Collections +{ + internal interface IRefDictionary : IReadOnlyDictionary where TValue : struct + { + new ref TValue this[TKey key] { get; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/ObjectPool.cs b/source/Handlebars/Collections/ObjectPool.cs index 7aaca9fb..ca55c043 100644 --- a/source/Handlebars/Collections/ObjectPool.cs +++ b/source/Handlebars/Collections/ObjectPool.cs @@ -1,33 +1,12 @@ -using System.Collections.Concurrent; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet { - internal abstract class ObjectPool + internal static class ObjectPoolExtensions { - private readonly ConcurrentQueue _objects; - - protected ObjectPool() - { - _objects = new ConcurrentQueue(); - } - - public T GetObject() + public static DisposableContainer Use(this ObjectPool objectPool) where T : class { - return !_objects.TryDequeue(out var item) - ? CreateObject() - : item; + return new DisposableContainer(objectPool.Get(), objectPool.Return); } - - public DisposableContainer Use() - { - return new DisposableContainer(GetObject(), PutObject); - } - - public virtual void PutObject(T item) - { - _objects.Enqueue(item); - } - - protected abstract T CreateObject(); } } \ No newline at end of file diff --git a/source/Handlebars/Collections/RefDictionary.cs b/source/Handlebars/Collections/RefDictionary.cs index 9c6b1170..00355965 100644 --- a/source/Handlebars/Collections/RefDictionary.cs +++ b/source/Handlebars/Collections/RefDictionary.cs @@ -13,8 +13,7 @@ namespace HandlebarsDotNet.Collections /// /// /// - internal class RefDictionary : IReadOnlyDictionary - where TValue : struct + internal class RefDictionary : IRefDictionary where TValue : struct { private readonly IEqualityComparer _comparer; private Container _container; @@ -204,87 +203,5 @@ public Container(int[] buckets, Entry[] entries, int count = 0) Count = count; } } - - private static class HashHelpers - { - // This is the maximum prime smaller than Array.MaxArrayLength - private const int MaxPrimeArrayLength = 0x7FEFFFFD; - - private const int HashPrime = 101; - - // Table of prime numbers to use as hash table sizes. - // A typical resize algorithm would pick the smallest prime number in this array - // that is larger than twice the previous capacity. - // Suppose our Hashtable currently has capacity x and enough elements are added - // such that a resize needs to occur. Resizing first computes 2x then finds the - // first prime in the table greater than 2x, i.e. if primes are ordered - // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n. - // Doubling is important for preserving the asymptotic complexity of the - // hashtable operations such as add. Having a prime guarantees that double - // hashing does not lead to infinite loops. IE, your hash function will be - // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. - // We prefer the low computation costs of higher prime numbers over the increased - // memory allocation of a fixed prime number i.e. when right sizing a HashSet. - private static readonly int[] Primes = - { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, - 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, - 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, - 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 - }; - - private static bool IsPrime(int candidate) - { - if ((candidate & 1) == 0) return candidate == 2; - - var limit = (int) Math.Sqrt(candidate); - for (var divisor = 3; divisor <= limit; divisor += 2) - { - if ((candidate % divisor) == 0) - return false; - } - - return true; - } - - public static int GetPrime(int min) - { - if (min < 0) - throw new ArgumentException(); - - for (var i = 0; i < Primes.Length; i++) - { - var prime = Primes[i]; - if (prime >= min) - return prime; - } - - //outside of our predefined table. - //compute the hard way. - for (var i = (min | 1); i < int.MaxValue; i += 2) - { - if (IsPrime(i) && ((i - 1) % HashPrime != 0)) - return i; - } - - return min; - } - - // Returns size of hashtable to grow to. - public static int ExpandPrime(int oldSize) - { - var newSize = 2 * oldSize; - - // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. - // Note that this check works even when _items.Length overflowed thanks to the (uint) cast - if ((uint) newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) - { - return MaxPrimeArrayLength; - } - - return GetPrime(newSize); - } - } } } \ No newline at end of file diff --git a/source/Handlebars/Collections/RefDictionarySafe.cs b/source/Handlebars/Collections/RefDictionarySafe.cs new file mode 100644 index 00000000..cb7858f0 --- /dev/null +++ b/source/Handlebars/Collections/RefDictionarySafe.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace HandlebarsDotNet.Collections +{ + /// + /// Represents -like collection optimized for storing s. + /// The class API is very limited due to performance demands. + /// ! Collection guaranties successful write + /// + /// + /// + internal class RefDictionarySafe : IRefDictionary + where TValue : struct + { + private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + private readonly IEqualityComparer _comparer; + private readonly Container _container; + + public RefDictionarySafe(int capacity = 16, IEqualityComparer comparer = null) + { + var initialCapacity = HashHelpers.GetPrime(capacity); + _container = new Container(new int[initialCapacity], new Entry[initialCapacity]); + _comparer = comparer ?? EqualityComparer.Default; + } + + public int Count + { + get + { + using (_readerWriterLock.UseRead()) + { + return _container.Count; + } + } + } + + public bool ContainsKey(TKey key) + { + using (_readerWriterLock.UseRead()) + { + var entries = _container.Entries; + var entryIndex = GetEntryIndex(key, _container); + + while (entryIndex != -1) + { + if (_comparer.Equals(entries[entryIndex].Key, key)) return true; + + entryIndex = entries[entryIndex].Next; + } + + return false; + } + } + + bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) + { + if (!ContainsKey(key)) + { + value = default(TValue); + return false; + } + + value = this[key]; + return true; + } + + TValue IReadOnlyDictionary.this[TKey key] => + ContainsKey(key) ? this[key] : default(TValue); + + public ref TValue this[TKey key] + { + get + { + using (_readerWriterLock.UseRead()) + { + var entryIndex = GetEntryIndex(key, _container); + while (entryIndex != -1) + { + if (_comparer.Equals(_container.Entries[entryIndex].Key, key)) + { + return ref _container.Entries[entryIndex].Value; + } + + entryIndex = _container.Entries[entryIndex].Next; + } + } + + using (_readerWriterLock.UseWrite()) + { + if (Count == _container.Entries.Length) + { + Resize(); + } + + var entryIndex = _container.Count++; + _container.Entries[entryIndex].Key = key; + var bucket = GetBucketIndex(key, _container); + _container.Entries[entryIndex].Next = _container.Buckets[bucket] - 1; + _container.Buckets[bucket] = entryIndex + 1; + return ref _container.Entries[entryIndex].Value; + } + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + using (_readerWriterLock.UseRead()) + { + var entries = _container.Entries; + for (var index = 0; index < _container.Count; index++) + { + yield return entries[index].Key; + } + } + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + using (_readerWriterLock.UseRead()) + { + var entries = _container.Entries; + for (var index = 0; index < _container.Count; index++) + { + yield return entries[index].Value; + } + } + } + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + using (_readerWriterLock.UseRead()) + { + var entries = _container.Entries; + for (var index = 0; index < Count; index++) + { + var entry = entries[index]; + yield return new KeyValuePair(entry.Key, entry.Value); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetBucketIndex(TKey key, Container container) + { + using (_readerWriterLock.UseRead()) + { + return (_comparer.GetHashCode(key) & 0x7FFFFFFF) % container.Buckets.Length; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetEntryIndex(TKey key, Container container) + { + using (_readerWriterLock.UseRead()) + { + return container.Buckets[GetBucketIndex(key, container)] - 1; + } + } + + private void Resize() + { + using (_readerWriterLock.UseWrite()) + { + var count = _container.Count; + var newSize = HashHelpers.ExpandPrime(count); + + var newEntries = new Entry[newSize]; + Array.Copy(_container.Entries, 0, newEntries, 0, count); + + var newBuckets = new int[newSize]; + + var newContainer = new Container(newBuckets, newEntries, count); + for (var index = 0; index < count;) + { + var bucketIndex = GetBucketIndex(newEntries[index].Key, newContainer); + newEntries[index].Next = newBuckets[bucketIndex] - 1; + newBuckets[bucketIndex] = ++index; + } + + _container.Update(newContainer); + } + } + + private struct Entry + { + public Entry(TKey key) : this() + { + Key = key; + } + + public TKey Key; + public TValue Value; + public int Next; + } + + private class Container + { + public int[] Buckets { get; private set; } + public Entry[] Entries { get; private set; } + public int Count { get; set; } + + public Container(int[] buckets, Entry[] entries, int count = 0) + { + Buckets = buckets; + Entries = entries; + Count = count; + } + + public void Update(Container container) + { + Buckets = container.Buckets; + Entries = container.Entries; + Count = container.Count; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Collections/RefLookup.cs b/source/Handlebars/Collections/RefLookup.cs index 2093831b..f5d7f2d4 100644 --- a/source/Handlebars/Collections/RefLookup.cs +++ b/source/Handlebars/Collections/RefLookup.cs @@ -5,7 +5,7 @@ namespace HandlebarsDotNet.Collections internal sealed class RefLookup where TValue: struct { - private readonly RefDictionary _inner; + private readonly IRefDictionary _inner; public delegate ref TValue ValueFactory(TKey key, ref TValue value); @@ -13,6 +13,11 @@ public RefLookup(int capacity = 16, IEqualityComparer comparer = null) { _inner = new RefDictionary(capacity, comparer); } + + public RefLookup(IRefDictionary inner) + { + _inner = inner; + } public bool ContainsKey(TKey key) { diff --git a/source/Handlebars/Collections/StringBuilderPool.cs b/source/Handlebars/Collections/StringBuilderPool.cs index 59de5f57..42b03caf 100644 --- a/source/Handlebars/Collections/StringBuilderPool.cs +++ b/source/Handlebars/Collections/StringBuilderPool.cs @@ -1,30 +1,18 @@ using System; using System.Text; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet { - internal class StringBuilderPool : ObjectPool + internal class StringBuilderPool : DefaultObjectPool { private static readonly Lazy Lazy = new Lazy(() => new StringBuilderPool()); - private readonly int _initialCapacity; - public static StringBuilderPool Shared => Lazy.Value; - public StringBuilderPool(int initialCapacity = 16) - { - _initialCapacity = initialCapacity; - } - - protected override StringBuilder CreateObject() - { - return new StringBuilder(_initialCapacity); - } - - public override void PutObject(StringBuilder item) + public StringBuilderPool(int initialCapacity = 16) + : base(new StringBuilderPooledObjectPolicy{ InitialCapacity = initialCapacity }) { - item.Length = 0; - base.PutObject(item); } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs index 4f730b9f..dd238657 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockParamsParser.cs @@ -13,10 +13,10 @@ public override Token Parse(ExtendedStringReader reader) private static string AccumulateWord(ExtendedStringReader reader) { - var buffer = StringBuilderPool.Shared.GetObject(); - - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; + if (reader.Peek() != '|') return null; reader.Read(); @@ -33,10 +33,6 @@ private static string AccumulateWord(ExtendedStringReader reader) return accumulateWord; } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } } } \ 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 0af8e27e..d9bba3db 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/BlockWordParser.cs @@ -27,9 +27,9 @@ private static bool IsBlockWord(ExtendedStringReader reader) private static string AccumulateBlockWord(ExtendedStringReader reader) { - var buffer = StringBuilderPool.Shared.GetObject(); - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; buffer.Append((char)reader.Read()); while(char.IsWhiteSpace((char)reader.Peek())) { @@ -56,10 +56,6 @@ private static string AccumulateBlockWord(ExtendedStringReader reader) return buffer.ToString(); } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } } } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs index ee78c861..fcecb213 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs @@ -29,9 +29,9 @@ private static string AccumulateComment(ExtendedStringReader reader) { reader.Read(); bool? escaped = null; - var buffer = StringBuilderPool.Shared.GetObject(); - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; while (true) { if (escaped == null) @@ -55,10 +55,6 @@ private static string AccumulateComment(ExtendedStringReader reader) return buffer.ToString(); } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } private static bool IsClosed(ExtendedStringReader reader, StringBuilder buffer, bool isEscaped) diff --git a/source/Handlebars/Compiler/Lexer/Parsers/LiteralParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/LiteralParser.cs index a5869384..b959701f 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/LiteralParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/LiteralParser.cs @@ -37,9 +37,9 @@ private static bool IsNonDelimitedLiteral(ExtendedStringReader reader) private static string AccumulateLiteral(ExtendedStringReader reader, bool captureDelimiter, params char[] delimiters) { - var buffer = StringBuilderPool.Shared.GetObject(); - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; while (true) { var node = reader.Peek(); @@ -47,32 +47,26 @@ private static string AccumulateLiteral(ExtendedStringReader reader, bool captur { throw new HandlebarsParserException("Reached end of template before the expression was closed.", reader.GetContext()); } - else - { - if (delimiters.Contains((char)node)) - { - if (captureDelimiter) - { - reader.Read(); - } - break; - } - if (!captureDelimiter && (char)node == '}') + if (delimiters.Contains((char)node)) + { + if (captureDelimiter) { - break; + reader.Read(); } + break; + } - buffer.Append((char)reader.Read()); + if (!captureDelimiter && (char)node == '}') + { + break; } + + buffer.Append((char)reader.Read()); } return buffer.ToString(); } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } } } diff --git a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs index b7c67263..efa2548e 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs @@ -36,10 +36,10 @@ private static bool IsWord(ExtendedStringReader reader) private static string AccumulateWord(ExtendedStringReader reader) { - var buffer = StringBuilderPool.Shared.GetObject(); - - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; + var inString = false; while (true) @@ -71,10 +71,6 @@ private static string AccumulateWord(ExtendedStringReader reader) return buffer.ToString().Trim(); } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } private static bool CanBreakAtSpace(IEnumerable buffer) diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index c86ddae4..91c6c223 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -29,9 +29,10 @@ private static IEnumerable Parse(ExtendedStringReader source) { bool inExpression = false; bool trimWhitespace = false; - var buffer = StringBuilderPool.Shared.GetObject(); - try + using(var container = StringBuilderPool.Shared.Use()) { + var buffer = container.Value; + var node = source.Read(); while (true) { @@ -175,10 +176,6 @@ private static IEnumerable Parse(ExtendedStringReader source) } } } - finally - { - StringBuilderPool.Shared.PutObject(buffer); - } } } } diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 80884371..7ff5a834 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -4,6 +4,7 @@ using System.Reflection; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.ValueProviders; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.Compiler { @@ -184,14 +185,18 @@ public BindingContext CreateChildContext(object value, Action + private class BindingContextPool : DefaultObjectPool { + public BindingContextPool() : base(new BindingContextPolicy()) + { + } + public BindingContext CreateContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) { - var context = GetObject(); + var context = Get(); context.Configuration = configuration; context.Value = value; context.TextWriter = writer; @@ -205,28 +210,31 @@ public BindingContext CreateContext(InternalHandlebarsConfiguration configuratio return context; } - protected override BindingContext CreateObject() + private class BindingContextPolicy : IPooledObjectPolicy { - return new BindingContext(null, null, null, null, null, null); - } + public BindingContext Create() + { + return new BindingContext(null, null, null, null, null, null); + } - public override void PutObject(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--) + public bool Return(BindingContext item) { - valueProviders.Remove(valueProviders[index]); + 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; } - - base.PutObject(item); } } } diff --git a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs index 86273981..ebb11521 100644 --- a/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs +++ b/source/Handlebars/Compiler/Structure/BlockParamsValueProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.ValueProviders; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.Compiler { @@ -35,7 +36,7 @@ internal class BlockParamsValueProvider : IValueProvider public static BlockParamsValueProvider Create(BindingContext context, object @params) { - var blockParamsValueProvider = Pool.GetObject(); + var blockParamsValueProvider = Pool.Get(); blockParamsValueProvider._params = @params as BlockParam; blockParamsValueProvider._invoker = action => action(context); @@ -83,23 +84,30 @@ public bool TryGetValue(ref ChainSegment segment, out object value) public void Dispose() { - Pool.PutObject(this); + Pool.Return(this); } - private class BlockParamsValueProviderPool : ObjectPool + private class BlockParamsValueProviderPool : DefaultObjectPool { - protected override BlockParamsValueProvider CreateObject() + public BlockParamsValueProviderPool() : base(new BlockParamsValueProviderPolicy()) { - return new BlockParamsValueProvider(); } - - public override void PutObject(BlockParamsValueProvider item) + + private class BlockParamsValueProviderPolicy : IPooledObjectPolicy { - item._accessors.Clear(); - item._invoker = null; - item._params = null; - - base.PutObject(item); + public BlockParamsValueProvider Create() + { + return new BlockParamsValueProvider(); + } + + public bool Return(BlockParamsValueProvider item) + { + item._accessors.Clear(); + item._invoker = null; + item._params = null; + + return true; + } } } } diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index e8adb102..cbe921f2 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -8,7 +8,8 @@ namespace HandlebarsDotNet.Compiler.Structure.Path [DebuggerDisplay("{Value}")] internal struct ChainSegment { - private static readonly RefLookup ChainSegments = new RefLookup(); + private static readonly RefLookup ChainSegments = + new RefLookup(new RefDictionarySafe()); public static ref ChainSegment Create(string value) { diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 141c8772..c104c198 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -151,7 +151,7 @@ params object[] arguments return; } - + bindingContext.TryGetContextVariable(ref ChainSegment.Create(helperName), out var value); DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); } diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index b18274d9..12d52f0f 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; namespace HandlebarsDotNet { @@ -24,11 +22,4 @@ public static bool IsMultiple(this IEnumerable source) } } } - - internal static class ObjectExtensions - { - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T As(this object source) => (T) source; - } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index fbdcfbf1..315f3f81 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -63,7 +63,8 @@ private object Lookup(dynamic context, params object[] arguments) } var memberName = arguments[1].ToString(); - return !PathResolver.TryAccessMember(arguments[0], ref ChainSegment.Create(memberName), _configuration, out var value) + ref var segment = ref ChainSegment.Create(memberName); + return !PathResolver.TryAccessMember(arguments[0], ref segment, _configuration, out var value) ? new UndefinedBindingResult(memberName, _configuration) : value; } diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 1e15c026..751ac1c7 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -1,13 +1,18 @@  + Handlebars portable net452;netstandard1.3;netstandard2.0 - 1.0.0 + 1.0.2 7 HandlebarsDotNet 7F6A54F4-161A-46EA-9A27-DF834B7810DB - true + + + true + + false @@ -23,7 +28,7 @@ Copyright © 2020 Oleh Formaniuk Blistering-fast Handlebars.js templates in your .NET application. hbnet-icon.png - handlebars.csharp + Handlebars.CSharp https://opensource.org/licenses/mit https://github.com/zjklee/handlebars.csharp Improved performance; added new features @@ -41,21 +46,62 @@ + + + lib\netstandard2.0\ + false + + + lib\netstandard1.3\ + false + + + lib\net452\ + false + + + + - + + + + + + + + + + $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) + + + + + + + + + + + + + + + + diff --git a/source/Handlebars/IO/PolledStringWriter.cs b/source/Handlebars/IO/PolledStringWriter.cs index ad8e18c1..7f34fc1b 100644 --- a/source/Handlebars/IO/PolledStringWriter.cs +++ b/source/Handlebars/IO/PolledStringWriter.cs @@ -5,18 +5,18 @@ namespace HandlebarsDotNet { internal class PolledStringWriter : StringWriter { - public PolledStringWriter() : base(StringBuilderPool.Shared.GetObject()) + public PolledStringWriter() : base(StringBuilderPool.Shared.Get()) { } - public PolledStringWriter(IFormatProvider formatProvider) : base(StringBuilderPool.Shared.GetObject(), formatProvider) + public PolledStringWriter(IFormatProvider formatProvider) : base(StringBuilderPool.Shared.Get(), formatProvider) { } protected override void Dispose(bool disposing) { - StringBuilderPool.Shared.PutObject(base.GetStringBuilder()); + StringBuilderPool.Shared.Return(base.GetStringBuilder()); base.Dispose(disposing); } } diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs index 236e1279..f747deb5 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -9,8 +9,7 @@ namespace HandlebarsDotNet.ObjectDescriptors { internal class ObjectDescriptorProvider : IObjectDescriptorProvider { - private static readonly Type DynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); - + private readonly Type _dynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); private readonly InternalHandlebarsConfiguration _configuration; private readonly RefLookup> _membersCache = new RefLookup>(); @@ -21,7 +20,7 @@ public ObjectDescriptorProvider(InternalHandlebarsConfiguration configuration) public bool CanHandleType(Type type) { - return !DynamicMetaObjectProviderType.IsAssignableFrom(type) + return !_dynamicMetaObjectProviderType.IsAssignableFrom(type) && type != typeof(string); } diff --git a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs index 4135233e..ddffd56e 100644 --- a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -9,8 +9,9 @@ namespace HandlebarsDotNet.ObjectDescriptors { internal sealed class StringDictionaryObjectDescriptorProvider : IObjectDescriptorProvider { - private readonly RefLookup> _typeCache = new RefLookup>(); private static readonly object[] EmptyArray = new object[0]; + + private readonly RefLookup> _typeCache = new RefLookup>(); public bool CanHandleType(Type type) { diff --git a/source/Handlebars/ObjectExtensions.cs b/source/Handlebars/ObjectExtensions.cs new file mode 100644 index 00000000..fe2a9444 --- /dev/null +++ b/source/Handlebars/ObjectExtensions.cs @@ -0,0 +1,33 @@ +using System; +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/ValueProviders/IteratorValueProvider.cs b/source/Handlebars/ValueProviders/IteratorValueProvider.cs index 5c0d9fee..ed5b24bb 100644 --- a/source/Handlebars/ValueProviders/IteratorValueProvider.cs +++ b/source/Handlebars/ValueProviders/IteratorValueProvider.cs @@ -1,4 +1,5 @@ using HandlebarsDotNet.Compiler.Structure.Path; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.ValueProviders { @@ -8,7 +9,7 @@ internal class IteratorValueProvider : IValueProvider public static IteratorValueProvider Create() { - return Pool.GetObject(); + return Pool.Get(); } public object Value { get; set; } @@ -44,27 +45,34 @@ public virtual bool TryGetValue(ref ChainSegment segment, out object value) } } - private class IteratorValueProviderPool : ObjectPool + public virtual void Dispose() { - protected override IteratorValueProvider CreateObject() + Pool.Return(this); + } + + private class IteratorValueProviderPool : DefaultObjectPool + { + public IteratorValueProviderPool() : base(new IteratorValueProviderPolicy()) { - return new IteratorValueProvider(); } - - public override void PutObject(IteratorValueProvider item) + + private class IteratorValueProviderPolicy : IPooledObjectPolicy { - item.First = false; - item.Last = false; - item.Index = 0; - item.Value = null; + IteratorValueProvider IPooledObjectPolicy.Create() + { + return new IteratorValueProvider(); + } - base.PutObject(item); - } - } + bool IPooledObjectPolicy.Return(IteratorValueProvider item) + { + item.First = true; + item.Last = false; + item.Index = 0; + item.Value = null; - public void Dispose() - { - Pool.PutObject(this); + return true; + } + } } } } \ No newline at end of file diff --git a/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs index 56c71aec..c62d5c07 100644 --- a/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs +++ b/source/Handlebars/ValueProviders/ObjectEnumeratorValueProvider.cs @@ -1,4 +1,5 @@ using HandlebarsDotNet.Compiler.Structure.Path; +using Microsoft.Extensions.ObjectPool; namespace HandlebarsDotNet.ValueProviders { @@ -10,7 +11,7 @@ internal class ObjectEnumeratorValueProvider : IteratorValueProvider public static ObjectEnumeratorValueProvider Create(HandlebarsConfiguration configuration) { - var provider = Pool.GetObject(); + var provider = Pool.Get(); provider._configuration = configuration; return provider; } @@ -34,23 +35,35 @@ public override bool TryGetValue(ref ChainSegment segment, out object value) } } - private class ObjectEnumeratorValueProviderPool : ObjectPool + public override void Dispose() { - protected override ObjectEnumeratorValueProvider CreateObject() + Pool.Return(this); + } + + private class ObjectEnumeratorValueProviderPool : DefaultObjectPool + { + public ObjectEnumeratorValueProviderPool() : base(new ObjectEnumeratorValueProviderPolicy()) { - return new ObjectEnumeratorValueProvider(); } - - public override void PutObject(ObjectEnumeratorValueProvider item) + + private class ObjectEnumeratorValueProviderPolicy : IPooledObjectPolicy { - item.First = false; - item.Last = false; - item.Index = 0; - item.Value = null; - item.Key = null; - item._configuration = null; - - base.PutObject(item); + 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; + } } } } From 294958e02f03df3e73bd369793c0b7c5d6994aa3 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 08:58:23 -0700 Subject: [PATCH 20/53] Fix repack --- source/Handlebars/Handlebars.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 751ac1c7..eb7507e3 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -69,7 +69,7 @@ - + @@ -94,7 +94,6 @@ - From 17b0d43ab3b5c4c93d8fe58c5540cdbae2721faf Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 09:15:05 -0700 Subject: [PATCH 21/53] Remove path segment cache --- .../Collections/RefDictionarySafe.cs | 232 ------------------ .../Compiler/Structure/Path/ChainSegment.cs | 29 +-- .../Expression/BlockHelperFunctionBinder.cs | 5 +- .../Features/BuildInHelpersFeature.cs | 2 +- .../MemberAccessors/ContextMemberAccessor.cs | 3 +- .../CollectionMemberAliasProvider.cs | 2 +- 6 files changed, 18 insertions(+), 255 deletions(-) delete mode 100644 source/Handlebars/Collections/RefDictionarySafe.cs diff --git a/source/Handlebars/Collections/RefDictionarySafe.cs b/source/Handlebars/Collections/RefDictionarySafe.cs deleted file mode 100644 index cb7858f0..00000000 --- a/source/Handlebars/Collections/RefDictionarySafe.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace HandlebarsDotNet.Collections -{ - /// - /// Represents -like collection optimized for storing s. - /// The class API is very limited due to performance demands. - /// ! Collection guaranties successful write - /// - /// - /// - internal class RefDictionarySafe : IRefDictionary - where TValue : struct - { - private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private readonly IEqualityComparer _comparer; - private readonly Container _container; - - public RefDictionarySafe(int capacity = 16, IEqualityComparer comparer = null) - { - var initialCapacity = HashHelpers.GetPrime(capacity); - _container = new Container(new int[initialCapacity], new Entry[initialCapacity]); - _comparer = comparer ?? EqualityComparer.Default; - } - - public int Count - { - get - { - using (_readerWriterLock.UseRead()) - { - return _container.Count; - } - } - } - - public bool ContainsKey(TKey key) - { - using (_readerWriterLock.UseRead()) - { - var entries = _container.Entries; - var entryIndex = GetEntryIndex(key, _container); - - while (entryIndex != -1) - { - if (_comparer.Equals(entries[entryIndex].Key, key)) return true; - - entryIndex = entries[entryIndex].Next; - } - - return false; - } - } - - bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) - { - if (!ContainsKey(key)) - { - value = default(TValue); - return false; - } - - value = this[key]; - return true; - } - - TValue IReadOnlyDictionary.this[TKey key] => - ContainsKey(key) ? this[key] : default(TValue); - - public ref TValue this[TKey key] - { - get - { - using (_readerWriterLock.UseRead()) - { - var entryIndex = GetEntryIndex(key, _container); - while (entryIndex != -1) - { - if (_comparer.Equals(_container.Entries[entryIndex].Key, key)) - { - return ref _container.Entries[entryIndex].Value; - } - - entryIndex = _container.Entries[entryIndex].Next; - } - } - - using (_readerWriterLock.UseWrite()) - { - if (Count == _container.Entries.Length) - { - Resize(); - } - - var entryIndex = _container.Count++; - _container.Entries[entryIndex].Key = key; - var bucket = GetBucketIndex(key, _container); - _container.Entries[entryIndex].Next = _container.Buckets[bucket] - 1; - _container.Buckets[bucket] = entryIndex + 1; - return ref _container.Entries[entryIndex].Value; - } - } - } - - IEnumerable IReadOnlyDictionary.Keys - { - get - { - using (_readerWriterLock.UseRead()) - { - var entries = _container.Entries; - for (var index = 0; index < _container.Count; index++) - { - yield return entries[index].Key; - } - } - } - } - - IEnumerable IReadOnlyDictionary.Values - { - get - { - using (_readerWriterLock.UseRead()) - { - var entries = _container.Entries; - for (var index = 0; index < _container.Count; index++) - { - yield return entries[index].Value; - } - } - } - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - using (_readerWriterLock.UseRead()) - { - var entries = _container.Entries; - for (var index = 0; index < Count; index++) - { - var entry = entries[index]; - yield return new KeyValuePair(entry.Key, entry.Value); - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable>)this).GetEnumerator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetBucketIndex(TKey key, Container container) - { - using (_readerWriterLock.UseRead()) - { - return (_comparer.GetHashCode(key) & 0x7FFFFFFF) % container.Buckets.Length; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetEntryIndex(TKey key, Container container) - { - using (_readerWriterLock.UseRead()) - { - return container.Buckets[GetBucketIndex(key, container)] - 1; - } - } - - private void Resize() - { - using (_readerWriterLock.UseWrite()) - { - var count = _container.Count; - var newSize = HashHelpers.ExpandPrime(count); - - var newEntries = new Entry[newSize]; - Array.Copy(_container.Entries, 0, newEntries, 0, count); - - var newBuckets = new int[newSize]; - - var newContainer = new Container(newBuckets, newEntries, count); - for (var index = 0; index < count;) - { - var bucketIndex = GetBucketIndex(newEntries[index].Key, newContainer); - newEntries[index].Next = newBuckets[bucketIndex] - 1; - newBuckets[bucketIndex] = ++index; - } - - _container.Update(newContainer); - } - } - - private struct Entry - { - public Entry(TKey key) : this() - { - Key = key; - } - - public TKey Key; - public TValue Value; - public int Next; - } - - private class Container - { - public int[] Buckets { get; private set; } - public Entry[] Entries { get; private set; } - public int Count { get; set; } - - public Container(int[] buckets, Entry[] entries, int count = 0) - { - Buckets = buckets; - Entries = entries; - Count = count; - } - - public void Update(Container container) - { - Buckets = container.Buckets; - Entries = container.Entries; - Count = container.Count; - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index cbe921f2..aebdf6f8 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -8,26 +8,19 @@ namespace HandlebarsDotNet.Compiler.Structure.Path [DebuggerDisplay("{Value}")] internal struct ChainSegment { - private static readonly RefLookup ChainSegments = - new RefLookup(new RefDictionarySafe()); - - public static ref ChainSegment Create(string value) + public static ChainSegment Create(string value) { - if (ChainSegments.ContainsKey(value)) - { - return ref ChainSegments.GetValueOrDefault(value); - } - - return ref ChainSegments.GetOrAdd(value, (string key, ref ChainSegment segment) => - { - segment.IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); - segment.Value = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); - segment.IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"); - segment.TrimmedValue = TrimSquareBrackets(segment.Value).Intern(); - segment.LowerInvariant = segment.TrimmedValue.ToLowerInvariant().Intern(); + var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); + var segmentTrimmedValue = TrimSquareBrackets(segmentValue).Intern(); - return ref segment; - }); + return new ChainSegment + { + IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase), + Value = segmentValue, + IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"), + TrimmedValue = segmentTrimmedValue, + LowerInvariant = segmentTrimmedValue.ToLowerInvariant().Intern() + }; } public string Value { get; private set; } diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index c104c198..17bfc39f 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -151,8 +151,9 @@ params object[] arguments return; } - - bindingContext.TryGetContextVariable(ref ChainSegment.Create(helperName), out var value); + + var segment = ChainSegment.Create(helperName); + bindingContext.TryGetContextVariable(ref segment, out var value); DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); } catch(Exception e) diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index 315f3f81..c89ed984 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -63,7 +63,7 @@ private object Lookup(dynamic context, params object[] arguments) } var memberName = arguments[1].ToString(); - ref var segment = ref ChainSegment.Create(memberName); + var segment = ChainSegment.Create(memberName); return !PathResolver.TryAccessMember(arguments[0], ref segment, _configuration, out var value) ? new UndefinedBindingResult(memberName, _configuration) : value; diff --git a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs index 5f71e9ee..e631d7dd 100644 --- a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs @@ -9,7 +9,8 @@ internal class ContextMemberAccessor : IMemberAccessor public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) { var bindingContext = (BindingContext) instance; - return bindingContext.TryGetContextVariable(ref ChainSegment.Create(memberName), out value); + var segment = ChainSegment.Create(memberName); + return bindingContext.TryGetContextVariable(ref segment, out value); } } } \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs index ed47f114..e2c7e988 100644 --- a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -17,7 +17,7 @@ public CollectionMemberAliasProvider(InternalHandlebarsConfiguration configurati public bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) { - ref var segment = ref ChainSegment.Create(memberAlias); + var segment = ChainSegment.Create(memberAlias); switch (instance) { case Array array: From 34d92563a2d5e0ae7e3cd03b1973252235221ffc Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 09:18:30 -0700 Subject: [PATCH 22/53] Add extra test steps --- .github/workflows/pull_request.yml | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7235ac04..5091f147 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: working-directory: ./source run: dotnet build Handlebars.Code.sln -c Release - test: + test-windows: runs-on: windows-latest needs: [build] steps: @@ -29,6 +29,32 @@ jobs: - name: Test working-directory: ./source run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + + test-linux: + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + + test-macos: + runs-on: macos-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx sonar-pr: runs-on: ubuntu-latest @@ -38,7 +64,7 @@ jobs: - name: Sonarscanner for dotnet uses: Secbyte/dotnet-sonarscanner@v2.2 with: - buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 + buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 -c Release testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect "code coverage" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp From dbf2d06b6361bc63ecfbfed4d5b44b155fb13846 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 09:25:59 -0700 Subject: [PATCH 23/53] WIP: try disable FastCompile --- source/Handlebars.Test/BasicIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 4fbfeff8..fbbca2f1 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -21,7 +21,7 @@ public class HandlebarsEnvGenerator : IEnumerable Handlebars.Create(), Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), #if compileFast - Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), + //Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), #endif Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { From 4980f869e38639707194688230feb5175b0075a6 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 09:59:34 -0700 Subject: [PATCH 24/53] Fix SonarCloud comments --- README.md | 1 + .../netstandard2.0/LoggerFeatureExtensions.cs | 1 - .../netstandard2.0/LoggerFeatureFactory.cs | 3 -- .../Handlebars.Test/BasicIntegrationTests.cs | 1 - .../Collections/CascadeDictionary.cs | 14 +------- source/Handlebars/Collections/LookupSlim.cs | 5 ++- source/Handlebars/Compiler/FunctionBuilder.cs | 22 +++++------- .../BlockAccumulatorContext.cs | 35 +++++++------------ .../ConditionalBlockAccumulatorContext.cs | 5 ++- .../Converter/HelperArgumentAccumulator.cs | 8 ++--- .../Lexer/Converter/PartialConverter.cs | 4 +-- .../Lexer/Converter/SubExpressionConverter.cs | 6 ++-- .../Compiler/Lexer/Parsers/CommentParser.cs | 2 +- .../Compiler/Structure/BindingContext.cs | 12 ++----- .../HashParameterAssignmentExpression.cs | 2 -- .../Compiler/Structure/Path/ChainSegment.cs | 1 - .../Translation/Expression/IteratorBinder.cs | 2 -- .../Translation/Expression/PartialBinder.cs | 4 +-- .../Expression/UnencodedStatementVisitor.cs | 2 +- source/Handlebars/EnumerableExtensions.cs | 4 ++- .../Features/BuildInHelpersFeature.cs | 1 + source/Handlebars/Features/ClosureFeature.cs | 1 + .../Features/DefaultCompilerFeature.cs | 1 + source/Handlebars/Features/WarmUpFeature.cs | 1 + source/Handlebars/HandlebarsUtils.cs | 2 +- .../CollectionMemberAliasProvider.cs | 1 - source/Handlebars/ObjectExtensions.cs | 1 - 27 files changed, 56 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 48973653..919ae994 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ #handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) +![Build](https://github.com/zjklee/Handlebars.CSharp/workflows/CI/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs index 0f788a13..800d5b1f 100644 --- a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs +++ b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Logging; namespace HandlebarsDotNet.Extension.Logger diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs index 2df358c5..e0118ad1 100644 --- a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs +++ b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using HandlebarsDotNet.Features; using Microsoft.Extensions.Logging; diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index fbbca2f1..bb620cb6 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -9,7 +9,6 @@ using HandlebarsDotNet.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using HandlebarsDotNet.Extension.CompileFast; using HandlebarsDotNet.Features; namespace HandlebarsDotNet.Test diff --git a/source/Handlebars/Collections/CascadeDictionary.cs b/source/Handlebars/Collections/CascadeDictionary.cs index bc51e684..206c5898 100644 --- a/source/Handlebars/Collections/CascadeDictionary.cs +++ b/source/Handlebars/Collections/CascadeDictionary.cs @@ -91,19 +91,7 @@ public int Count { get { - int count = 0; - foreach (var value in _outer) - { - if (_inner.TryGetValue(value.Key, out var innerValue)) - { - count++; - } - else - { - count++; - } - } - + var count = _outer.Count; foreach (var value in _inner) { if (_outer.ContainsKey(value.Key)) continue; diff --git a/source/Handlebars/Collections/LookupSlim.cs b/source/Handlebars/Collections/LookupSlim.cs index 241ce846..743fc5b0 100644 --- a/source/Handlebars/Collections/LookupSlim.cs +++ b/source/Handlebars/Collections/LookupSlim.cs @@ -29,7 +29,10 @@ public TValue GetOrAdd(TKey key, Func valueFactory) private TValue Write(TKey key, TValue value) { - var copy = new Dictionary(_inner, _comparer); + var copy = new Dictionary(_inner, _comparer) + { + [key] = value + }; Interlocked.CompareExchange(ref _inner, copy, _inner); diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index d6c5ffae..f1f95d9f 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -37,7 +37,7 @@ public static Action CompileCore(IEnumerable { try { - if (expressions.Any() == false) + if (!expressions.Any()) { return EmptyLambdaWithContext; } @@ -47,13 +47,11 @@ public static Action CompileCore(IEnumerable } var context = new CompilationContext(configuration); - { - var expression = (Expression) Expression.Block(expressions); - expression = Reduce(expression, context); + var expression = (Expression) Expression.Block(expressions); + expression = Reduce(expression, context); - var lambda = ContextBinder.Bind(context, expression, templatePath); - return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(lambda); - } + var lambda = ContextBinder.Bind(context, expression, templatePath); + return configuration.CompileTimeConfiguration.ExpressionCompiler.Compile(lambda); } catch (Exception ex) { @@ -65,7 +63,7 @@ public static Expression> CompileCore(IEnumerable> CompileCore(IEnumerable"); - } - else if (item is HelperExpression) + switch (item) { - 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) + if (item is StatementExpression expression) { - return ((StatementExpression)item).Body; - } - else - { - return item; + return expression.Body; } + + return item; } protected BlockAccumulatorContext(Expression startingNode) diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs index 1f2f691a..71d884e0 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs @@ -6,9 +6,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) @@ -16,7 +19,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)); diff --git a/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs index dd0c2e0f..cfc3d8e7 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/HelperArgumentAccumulator.cs @@ -66,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/PartialConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs index d7589a4f..686d60ef 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/PartialConverter.cs @@ -58,9 +58,9 @@ private static List AccumulateArguments(IEnumerator enumerat { var item = GetNext(enumerator); var helperArguments = new List(); - while (item is EndExpressionToken == false) + while (!(item is EndExpressionToken)) { - if (item is Expression == false) throw new HandlebarsCompilerException($"Token '{item}' could not be converted to an expression"); + 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/SubExpressionConverter.cs b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs index e9e1aa00..ea55fa82 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/SubExpressionConverter.cs @@ -53,15 +53,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/Parsers/CommentParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs index fcecb213..039182df 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/CommentParser.cs @@ -59,7 +59,7 @@ private static string AccumulateComment(ExtendedStringReader reader) private static bool IsClosed(ExtendedStringReader reader, StringBuilder buffer, bool isEscaped) { - return isEscaped && CheckIfEscaped(reader, buffer) && CheckIfStatementClosed(reader) || isEscaped == false && CheckIfStatementClosed(reader); + return isEscaped && CheckIfEscaped(reader, buffer) && CheckIfStatementClosed(reader) || !isEscaped && CheckIfStatementClosed(reader); } private static bool CheckIfStatementClosed(ExtendedStringReader reader) diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 7ff5a834..3ddf0e19 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -27,15 +27,9 @@ public static BindingContext Create(InternalHandlebarsConfiguration configuratio IDictionary> inlinePartialTemplates) { return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate, inlinePartialTemplates); - } - - private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, IDictionary> inlinePartialTemplates) : - this(configuration, value, writer, parent, templatePath, null, null, inlinePartialTemplates) { } - - private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, IDictionary> inlinePartialTemplates) : - this(configuration, value, writer, parent, templatePath, partialBlockTemplate, null, inlinePartialTemplates) { } + } - private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent, string templatePath, Action partialBlockTemplate, BindingContext current, IDictionary> inlinePartialTemplates) + private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent) { Configuration = configuration; TextWriter = writer; @@ -214,7 +208,7 @@ private class BindingContextPolicy : IPooledObjectPolicy { public BindingContext Create() { - return new BindingContext(null, null, null, null, null, null); + return new BindingContext((InternalHandlebarsConfiguration) null, (object) null, (EncodedTextWriter) null, (BindingContext) null); } public bool Return(BindingContext item) diff --git a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs index 47db24ca..a9719bf2 100644 --- a/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs +++ b/source/Handlebars/Compiler/Structure/HashParameterAssignmentExpression.cs @@ -12,8 +12,6 @@ public HashParameterAssignmentExpression(string name) public string Name { get; } public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.HashParameterAssignmentExpression; - - //public override Type Type => typeof(object); } } diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index aebdf6f8..21a5d22f 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using HandlebarsDotNet.Collections; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index 938d1f70..6add517f 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -172,7 +172,6 @@ private static void IterateObjectWithStaticProperties(BindingContext context, iterator.Value = accessor.TryGetValue(target, targetType, iterator.Key, out var value) ? value : null; iterator.First = iterator.Index == 0; iterator.Last = iterator.Index == count - 1; - iterator.Index = iterator.Index; using (var innerContext = context.CreateChildContext(iterator.Value)) { @@ -241,7 +240,6 @@ private static void IterateEnumerable(BindingContext context, iterator.Value = enumerableValue.Value; iterator.First = enumerableValue.IsFirst; iterator.Last = enumerableValue.IsLast; - iterator.Index = enumerableValue.Index; using(var innerContext = context.CreateChildContext(iterator.Value)) { diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 77d2b588..43b21ea8 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -82,10 +82,10 @@ private static bool InvokePartial( } // Partial is not found, so call the resolver and attempt to load it. - if (configuration.RegisteredTemplates.ContainsKey(partialName) == false) + 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; diff --git a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs index 6f59b2bb..75274e0b 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -16,7 +16,7 @@ protected override Expression VisitStatementExpression(StatementExpression sex) { var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); var suppressEncoding = context.Property(o => o.SuppressEncoding); - if (sex.IsEscaped == false) + if (!sex.IsEscaped) { return ExpressionShortcuts.Block(typeof(void)) .Line(suppressEncoding.Assign(true)) diff --git a/source/Handlebars/EnumerableExtensions.cs b/source/Handlebars/EnumerableExtensions.cs index 12d52f0f..5b616fde 100644 --- a/source/Handlebars/EnumerableExtensions.cs +++ b/source/Handlebars/EnumerableExtensions.cs @@ -18,7 +18,9 @@ public static bool IsMultiple(this IEnumerable source) { using(var enumerator = source.GetEnumerator()) { - return enumerator.MoveNext() && enumerator.MoveNext(); + var hasNext = enumerator.MoveNext(); + hasNext = hasNext && enumerator.MoveNext(); + return hasNext; } } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index c89ed984..cad20a8a 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -34,6 +34,7 @@ public void OnCompiling(HandlebarsConfiguration configuration) public void CompilationCompleted() { + // noting to do here } private static void With(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs index 84541a9d..dbc167a7 100644 --- a/source/Handlebars/Features/ClosureFeature.cs +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -45,6 +45,7 @@ public ClosureFeature() /// public void OnCompiling(HandlebarsConfiguration configuration) { + // noting to do here } /// diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index e197c5e0..8c462622 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -25,6 +25,7 @@ public void OnCompiling(HandlebarsConfiguration configuration) public void CompilationCompleted() { + // noting to do here } private class DefaultExpressionCompiler : IExpressionCompiler diff --git a/source/Handlebars/Features/WarmUpFeature.cs b/source/Handlebars/Features/WarmUpFeature.cs index 796a882b..01150bcb 100644 --- a/source/Handlebars/Features/WarmUpFeature.cs +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -35,6 +35,7 @@ public void OnCompiling(HandlebarsConfiguration configuration) /// public void CompilationCompleted() { + // noting to do here } } diff --git a/source/Handlebars/HandlebarsUtils.cs b/source/Handlebars/HandlebarsUtils.cs index de59b00a..2607062e 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -67,7 +67,7 @@ public static bool IsFalsyOrEmpty(object value) return true; } - return value is IEnumerable enumerable && enumerable.OfType().Any() == false; + return value is IEnumerable enumerable && !enumerable.OfType().Any(); } private static bool IsNumber(object value) diff --git a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs index e2c7e988..836e187e 100644 --- a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Linq; using HandlebarsDotNet.Compiler.Structure.Path; diff --git a/source/Handlebars/ObjectExtensions.cs b/source/Handlebars/ObjectExtensions.cs index fe2a9444..98a5db1f 100644 --- a/source/Handlebars/ObjectExtensions.cs +++ b/source/Handlebars/ObjectExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; From 9683df922a7ceb42f16171b26dc5480aeb0aa066 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 11:09:49 -0700 Subject: [PATCH 25/53] Exclude `CompileFast` for Linux and MacOS --- .github/workflows/ci.yml | 46 +++++++++++++++---- .github/workflows/pull_request.yml | 10 ++-- .../CompileFastExtensions.cs | 8 ++++ .../OperatingSystem.cs | 16 +++++++ .../Handlebars.Test/BasicIntegrationTests.cs | 3 +- 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 source/Handlebars.Extension.CompileFast/OperatingSystem.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff65a2b..57bf36a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: - name: Build Main working-directory: ./source run: dotnet build Handlebars.sln -c Release - - test: - name: Test + + + test-windows: runs-on: windows-latest needs: [build] steps: @@ -30,9 +30,39 @@ jobs: - name: Test working-directory: ./source run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + + + test-linux: + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + + + test-macos: + runs-on: macos-latest + needs: [build] + steps: + - uses: actions/checkout@master + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.102 + - name: Test + working-directory: ./source + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + sonar-ci: - runs-on: ubuntu-latest + runs-on: windows-latest + needs: [build] steps: - uses: actions/checkout@v2 - name: Setup dotnet @@ -49,9 +79,9 @@ jobs: sonarOrganisation: zjklee beginArguments: > /d:sonar.verbose="true" - /d:sonar.cs.opencover.reportsPaths='"**/*.opencover.xml"' - /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' - /d:sonar.cs.vstest.reportsPaths='**/*.trx' + /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" + /d:sonar.coverage.exclusions="**/*.cs","**/*.md" + /d:sonar.cs.vstest.reportsPaths="**/*.trx" env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -59,7 +89,7 @@ jobs: publish: - needs: [build,test,sonar-ci] + needs: [build,test-windows,test-linux,test-macos,sonar-ci] runs-on: windows-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5091f147..36dade2a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -57,8 +57,8 @@ jobs: run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx sonar-pr: - runs-on: ubuntu-latest - + runs-on: windows-latest + needs: [build] steps: - uses: actions/checkout@v2 - name: Sonarscanner for dotnet @@ -71,9 +71,9 @@ jobs: sonarOrganisation: zjklee beginArguments: > /d:sonar.verbose="true" - /d:sonar.cs.opencover.reportsPaths='"**/*.opencover.xml"' - /d:sonar.cs.vstest.reportsPaths='**/*.trx' - /d:sonar.coverage.exclusions='"**/*.cs","**/*.md"' + /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" + /d:sonar.cs.vstest.reportsPaths="**/*.trx" + /d:sonar.coverage.exclusions="**/*.cs","**/*.md" /d:sonar.pullrequest.key=${{ github.event.number }} /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} /d:sonar.pullrequest.base=${{ github.event.pull_request.base.ref }} diff --git a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs index 0d3d5b3b..a64b0cfc 100644 --- a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs +++ b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace HandlebarsDotNet.Extension.CompileFast { /// @@ -12,6 +14,12 @@ public static class CompileFastExtensions /// public static HandlebarsConfiguration UseCompileFast(this HandlebarsConfiguration configuration) { + if (!OperatingSystem.IsWindows()) + { + Debug.WriteLine("[WARNING] Only Windows OS is supported at the moment. Skipping feature."); + return configuration; + } + var compileTimeConfiguration = configuration.CompileTimeConfiguration; compileTimeConfiguration.Features.Add(new FastCompilerFeatureFactory()); diff --git a/source/Handlebars.Extension.CompileFast/OperatingSystem.cs b/source/Handlebars.Extension.CompileFast/OperatingSystem.cs new file mode 100644 index 00000000..fb92a774 --- /dev/null +++ b/source/Handlebars.Extension.CompileFast/OperatingSystem.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace HandlebarsDotNet.Extension.CompileFast +{ + internal static class OperatingSystem + { + public static bool IsWindows() => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public static bool IsMacOS() => + RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + public static bool IsLinux() => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } +} \ No newline at end of file diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index bb620cb6..4ecea921 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using HandlebarsDotNet.Features; +using HandlebarsDotNet.Extension.CompileFast; namespace HandlebarsDotNet.Test { @@ -20,7 +21,7 @@ public class HandlebarsEnvGenerator : IEnumerable Handlebars.Create(), Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), #if compileFast - //Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), + Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), #endif Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { From cb466f7ae0382bfe38721202637ef9b55933ca62 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 11:13:10 -0700 Subject: [PATCH 26/53] Make `sonar` actions back to Linux VMs --- .github/workflows/ci.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57bf36a5..d5a5234e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: sonar-ci: - runs-on: windows-latest + runs-on: ubuntu-latest needs: [build] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 36dade2a..1e62ff95 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -57,7 +57,7 @@ jobs: run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx sonar-pr: - runs-on: windows-latest + runs-on: ubuntu-latest needs: [build] steps: - uses: actions/checkout@v2 From 4754191db6d226b12ee4589fac637c9525e1ba41 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 11:41:57 -0700 Subject: [PATCH 27/53] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 919ae994..eddf68be 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -#handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) +handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) +============== ![Build](https://github.com/zjklee/Handlebars.CSharp/workflows/CI/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) - ============== _This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while. I'd be glad to back-merge all the changes back to original repo if I'd have a chance. Meanwhile I'd try to support the fork._ From a8c46f1aef228b062f868e7bbd87f75ef6d4b14c Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 11:59:10 -0700 Subject: [PATCH 28/53] Fix `ILRepack` problem on build/pack --- .github/workflows/ci.yml | 25 ++++++++++++++++--- .github/workflows/pull_request.yml | 10 ++++++-- .github/workflows/release.yml | 12 ++++++++- .../Handlebars.Extension.CompileFast.csproj | 6 ++++- .../OperatingSystem.cs | 10 +++----- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5a5234e..9f87ad54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,16 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.102 - - name: Build Main + - name: Build netstandard2.0 working-directory: ./source - run: dotnet build Handlebars.sln -c Release - + run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + - name: Build netstandard1.3 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + - name: Build net452 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f net452 + test-windows: runs-on: windows-latest @@ -103,6 +109,17 @@ jobs: run: git fetch --depth 500 - name: Determine version run: echo "::set-env name=VERSION::$(git describe --tags --abbrev=0)" + + - name: Build netstandard2.0 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + - name: Build netstandard1.3 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + - name: Build net452 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f net452 + - name: publish working-directory: ./source - run: dotnet pack Handlebars.Code.sln -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1e62ff95..0023c5f5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,9 +13,15 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.102 - - name: Build + - name: Build netstandard2.0 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release + run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + - name: Build netstandard1.3 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + - name: Build net452 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f net452 test-windows: runs-on: windows-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce4a5237..3135703a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,16 @@ jobs: with: dotnet-version: 3.1.100 + - name: Build netstandard2.0 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + - name: Build netstandard1.3 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + - name: Build net452 + working-directory: ./source + run: dotnet build Handlebars.Code.sln -c Release -f net452 + - name: publish working-directory: ./source - run: dotnet pack Handlebars.Code.sln -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json + run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index d01420e6..f677797c 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -1,7 +1,7 @@ - netstandard1.3;netstandard2.0 + net452;netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.CompileFast 7 1.0.0 @@ -21,6 +21,10 @@ https://github.com/zjklee/handlebars.csharp true + + + $(DefineConstants);netFramework + diff --git a/source/Handlebars.Extension.CompileFast/OperatingSystem.cs b/source/Handlebars.Extension.CompileFast/OperatingSystem.cs index fb92a774..52015c07 100644 --- a/source/Handlebars.Extension.CompileFast/OperatingSystem.cs +++ b/source/Handlebars.Extension.CompileFast/OperatingSystem.cs @@ -4,13 +4,11 @@ namespace HandlebarsDotNet.Extension.CompileFast { internal static class OperatingSystem { +#if netFramework + public static bool IsWindows() => true; +#else public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - public static bool IsMacOS() => - RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - - public static bool IsLinux() => - RuntimeInformation.IsOSPlatform(OSPlatform.Linux); +#endif } } \ No newline at end of file From f3423fc37161163eba92c1b51c279afb8aa24801 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 13:15:38 -0700 Subject: [PATCH 29/53] Fix publish --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f87ad54..58547998 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,4 +122,4 @@ jobs: - name: publish working-directory: ./source - run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json -n true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3135703a..a0fd0c23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,4 +27,4 @@ jobs: - name: publish working-directory: ./source - run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json + run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json -n true From c8c8d67091421b43b4b3788e2b17c1995364d80c Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Fri, 24 Apr 2020 13:49:32 -0700 Subject: [PATCH 30/53] Fix packaging --- source/Handlebars/Handlebars.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index eb7507e3..db0f5c74 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -47,15 +47,15 @@ - + lib\netstandard2.0\ false - + lib\netstandard1.3\ false - + lib\net452\ false From 3a4f8fa36d888f052de09479b3773d34309daafe Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 27 Apr 2020 03:33:04 -0700 Subject: [PATCH 31/53] Remove `InternalsVisible` attribute --- .../FastCompilerFeature.cs | 6 +- .../FastExpressionCompiler.cs | 8 +- .../Handlebars.Extension.CompileFast.csproj | 1 + .../LoggerFeature.cs | 2 +- source/Handlebars.Test/CollectionsTests.cs | 34 ------ .../Compiler/Structure/Path/PathResolver.cs | 4 +- .../Structure/UndefinedBindingResult.cs | 4 +- .../ICompiledHandlebarsConfiguration.cs | 104 ++++++++++++++++++ .../InternalHandlebarsConfiguration.cs | 19 +++- .../Features/BuildInHelpersFeature.cs | 6 +- source/Handlebars/Features/ClosureFeature.cs | 13 ++- .../Features/DefaultCompilerFeature.cs | 10 +- source/Handlebars/Features/IFeature.cs | 2 +- source/Handlebars/Features/WarmUpFeature.cs | 6 +- source/Handlebars/HandlebarsEnvironment.cs | 3 - .../IObjectDescriptorProvider.cs | 2 +- 16 files changed, 155 insertions(+), 69 deletions(-) delete mode 100644 source/Handlebars.Test/CollectionsTests.cs create mode 100644 source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs diff --git a/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs b/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs index 261e85ca..081bd0f5 100644 --- a/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs +++ b/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs @@ -13,10 +13,10 @@ public IFeature CreateFeature() internal class FastCompilerFeature : IFeature { - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features.OfType().SingleOrDefault(); - configuration.CompileTimeConfiguration.ExpressionCompiler = new FastExpressionCompiler(configuration, templateFeature); + var templateFeature = configuration.Features.OfType().SingleOrDefault(); + configuration.ExpressionCompiler = new FastExpressionCompiler(configuration, templateFeature); } public void CompilationCompleted() diff --git a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs index b484d6c5..d8580816 100644 --- a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs +++ b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs @@ -13,15 +13,15 @@ internal class FastExpressionCompiler : IExpressionCompiler { private readonly ClosureFeature _closureFeature; private readonly TemplateClosure _templateClosure; - private readonly ExpressionContainer _closure; + private readonly ParameterExpression _closure; private readonly ICollection _expressionMiddleware; - public FastExpressionCompiler(HandlebarsConfiguration configuration, ClosureFeature closureFeature) + public FastExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) { _closureFeature = closureFeature; _templateClosure = closureFeature?.TemplateClosure; _closure = closureFeature?.Closure; - _expressionMiddleware = configuration.CompileTimeConfiguration.ExpressionMiddleware; + _expressionMiddleware = configuration.ExpressionMiddleware; } public T Compile(Expression expression) where T: class @@ -35,7 +35,7 @@ public T Compile(Expression expression) where T: class expression = (Expression) _closureFeature.ExpressionMiddleware.Invoke(expression); - var parameters = new[] { (ParameterExpression) _closure }.Concat(expression.Parameters).ToArray(); + var parameters = new[] { _closure }.Concat(expression.Parameters).ToArray(); var lambda = Expression.Lambda(expression.Body, parameters); var compiledDelegateType = Expression.GetDelegateType(parameters.Select(o => o.Type).Concat(new[] {lambda.ReturnType}).ToArray()); diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index f677797c..c9697d66 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -35,6 +35,7 @@ + diff --git a/source/Handlebars.Extension.Logger/LoggerFeature.cs b/source/Handlebars.Extension.Logger/LoggerFeature.cs index d50ab786..15504231 100644 --- a/source/Handlebars.Extension.Logger/LoggerFeature.cs +++ b/source/Handlebars.Extension.Logger/LoggerFeature.cs @@ -24,7 +24,7 @@ public LoggerFeature(Log logger) _logger = logger; } - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { configuration.ReturnHelpers["log"] = LogHelper; } diff --git a/source/Handlebars.Test/CollectionsTests.cs b/source/Handlebars.Test/CollectionsTests.cs deleted file mode 100644 index 7fe1c996..00000000 --- a/source/Handlebars.Test/CollectionsTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using HandlebarsDotNet.Collections; -using Xunit; - -namespace Handlebars.Test -{ - public class CollectionsTests - { - [Fact] - public void CascadeDictionary_OuterChanged_ValueExists() - { - var outer = new Dictionary(); - var cascadeDictionary = new CascadeDictionary(outer); - - outer.Add("s", "s"); - - Assert.True(cascadeDictionary.ContainsKey("s")); - } - - [Fact] - public void CascadeDictionary_ToArray_ContainsValuesFromOuter() - { - var outer = new Dictionary(); - var cascadeDictionary = new CascadeDictionary(outer); - - outer.Add("s", "s"); - - var array = cascadeDictionary.ToArray(); - - Assert.Single(array); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index 66d8aae1..1c51e8e8 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -155,7 +155,7 @@ private static object ResolveValue(BindingContext context, object instance, ref return new UndefinedBindingResult(chainSegment.Value, configuration); } - public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, InternalHandlebarsConfiguration configuration, out object value) + public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) { var memberName = chainSegment.Value; if (instance == null) @@ -197,7 +197,7 @@ private static string TrimSquareBrackets(string key) return key; } - private static string ResolveMemberName(object instance, string memberName, HandlebarsConfiguration configuration) + private static string ResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration) { var resolver = configuration.ExpressionNameResolver; return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index 12914168..ce47a968 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -6,9 +6,9 @@ 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; diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs new file mode 100644 index 00000000..7bded712 --- /dev/null +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -0,0 +1,104 @@ +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; } + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs index 7293e261..afa072d8 100644 --- a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -10,7 +10,7 @@ namespace HandlebarsDotNet { - internal class InternalHandlebarsConfiguration : HandlebarsConfiguration + internal class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompiledHandlebarsConfiguration { private readonly HandlebarsConfiguration _configuration; @@ -25,8 +25,23 @@ internal class InternalHandlebarsConfiguration : HandlebarsConfiguration public override Compatibility Compatibility => _configuration.Compatibility; public sealed override CompileTimeConfiguration CompileTimeConfiguration { get; } - public IObjectDescriptorProvider ObjectDescriptorProvider { 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; internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) { diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index cad20a8a..d41ff85f 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -15,16 +15,16 @@ public IFeature CreateFeature() internal class BuildInHelpersFeature : IFeature { - private InternalHandlebarsConfiguration _configuration; + private ICompiledHandlebarsConfiguration _configuration; private static readonly ConfigureBlockParams WithBlockParamsConfiguration = (parameters, binder, deps) => { binder(parameters.ElementAtOrDefault(0), ctx => ctx, deps[0]); }; - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - _configuration = (InternalHandlebarsConfiguration) configuration; + _configuration = configuration; configuration.BlockHelpers["with"] = With; configuration.BlockHelpers["*inline"] = Inline; diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs index dbc167a7..c0114f86 100644 --- a/source/Handlebars/Features/ClosureFeature.cs +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -22,8 +22,13 @@ public class ClosureFeature : IFeature /// /// Parameter of actual closure /// - internal ExpressionContainer Closure { get; } = ExpressionShortcuts.Var("closure"); - + internal ExpressionContainer ClosureInternal { get; } = ExpressionShortcuts.Var("closure"); + + /// + /// + /// + public ParameterExpression Closure => (ParameterExpression) ClosureInternal.Expression; + /// /// Build-time closure store /// @@ -39,11 +44,11 @@ public class ClosureFeature : IFeature /// public ClosureFeature() { - ExpressionMiddleware = new ClosureExpressionMiddleware(TemplateClosure, Closure); + ExpressionMiddleware = new ClosureExpressionMiddleware(TemplateClosure, ClosureInternal); } /// - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { // noting to do here } diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index 8c462622..4778a5a2 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -15,12 +15,12 @@ public IFeature CreateFeature() internal class DefaultCompilerFeature : IFeature { - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { var templateFeature = ((InternalHandlebarsConfiguration) configuration).Features .OfType().SingleOrDefault(); - configuration.CompileTimeConfiguration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); + configuration.ExpressionCompiler = new DefaultExpressionCompiler(configuration, templateFeature); } public void CompilationCompleted() @@ -35,12 +35,12 @@ private class DefaultExpressionCompiler : IExpressionCompiler private readonly ExpressionContainer _closure; private readonly ICollection _expressionMiddleware; - public DefaultExpressionCompiler(HandlebarsConfiguration configuration, ClosureFeature closureFeature) + public DefaultExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) { _closureFeature = closureFeature; _templateClosure = closureFeature?.TemplateClosure; - _closure = closureFeature?.Closure; - _expressionMiddleware = configuration.CompileTimeConfiguration.ExpressionMiddleware; + _closure = closureFeature?.ClosureInternal; + _expressionMiddleware = configuration.ExpressionMiddleware; } public T Compile(Expression expression) where T: class diff --git a/source/Handlebars/Features/IFeature.cs b/source/Handlebars/Features/IFeature.cs index 5b83e1cd..dee2b762 100644 --- a/source/Handlebars/Features/IFeature.cs +++ b/source/Handlebars/Features/IFeature.cs @@ -9,7 +9,7 @@ public interface IFeature /// Executes before any template parsing/compiling activity /// /// - void OnCompiling(HandlebarsConfiguration configuration); + void OnCompiling(ICompiledHandlebarsConfiguration configuration); /// /// Executes after template is compiled diff --git a/source/Handlebars/Features/WarmUpFeature.cs b/source/Handlebars/Features/WarmUpFeature.cs index 01150bcb..850727ba 100644 --- a/source/Handlebars/Features/WarmUpFeature.cs +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -20,11 +20,9 @@ public WarmUpFeature(HashSet types) } /// - public void OnCompiling(HandlebarsConfiguration configuration) + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - var internalConfiguration = (InternalHandlebarsConfiguration) configuration; - - var descriptorProvider = internalConfiguration.ObjectDescriptorProvider; + var descriptorProvider = configuration.ObjectDescriptorProvider; foreach (var type in _types) { if(!descriptorProvider.CanHandleType(type)) continue; diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index e303639e..fbcee503 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -3,9 +3,6 @@ using System.Runtime.CompilerServices; using HandlebarsDotNet.Compiler; -[assembly: InternalsVisibleTo("Handlebars.Extension.CompileFast")] -[assembly: InternalsVisibleTo("Handlebars.Test")] - namespace HandlebarsDotNet { internal class HandlebarsEnvironment : IHandlebars diff --git a/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs index 3eafc256..e29dd458 100644 --- a/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/IObjectDescriptorProvider.cs @@ -3,7 +3,7 @@ namespace HandlebarsDotNet.ObjectDescriptors { /// - /// Factory for + /// Facade for /// public interface IObjectDescriptorProvider { From bfcac83e82d5bf32c343126339ab734953719478 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 17 May 2020 06:59:54 -0700 Subject: [PATCH 32/53] Set dummy token for PR action --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0023c5f5..177d5017 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -84,5 +84,5 @@ jobs: /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} /d:sonar.pullrequest.base=${{ github.event.pull_request.base.ref }} env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_TOKEN: empty_token GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 92d5bb1740297a67a328ab2236f8bffb22592553 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 17 May 2020 07:07:14 -0700 Subject: [PATCH 33/53] Set real token --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 177d5017..20c8661a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -84,5 +84,5 @@ jobs: /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} /d:sonar.pullrequest.base=${{ github.event.pull_request.base.ref }} env: - SONAR_TOKEN: empty_token + SONAR_TOKEN: 0a2d91d681b618b29a293e3d1e7e1e9ea82710a2 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0b40d469cfbda8bb12558f7e9810752f7ba10bff Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 17 May 2020 07:18:50 -0700 Subject: [PATCH 34/53] Remove release notes --- source/Handlebars/Handlebars.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index db0f5c74..9c13ef01 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -31,7 +31,6 @@ Handlebars.CSharp https://opensource.org/licenses/mit https://github.com/zjklee/handlebars.csharp - Improved performance; added new features handlebars;mustache;templating;engine;compiler git https://github.com/zjklee/handlebars.csharp From 820463357785bc8864241921c2cc6d2e325f3c49 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 18 May 2020 04:54:40 -0700 Subject: [PATCH 35/53] Fix `value` variable leaking from `BindingContext` --- source/Handlebars.Test/IssueTests.cs | 21 +++++++++++++++++++ .../ContextObjectDescriptor.cs | 2 +- .../BindingContextValueProvider.cs | 4 ---- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 source/Handlebars.Test/IssueTests.cs diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs new file mode 100644 index 00000000..3bad21f2 --- /dev/null +++ b/source/Handlebars.Test/IssueTests.cs @@ -0,0 +1,21 @@ +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); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs index 8b8fc2e5..665f1948 100644 --- a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs @@ -7,7 +7,7 @@ namespace HandlebarsDotNet.ObjectDescriptors internal class ContextObjectDescriptor : IObjectDescriptorProvider { private static readonly Type BindingContextType = typeof(BindingContext); - private static readonly string[] Properties = { "root", "parent", "value" }; + private static readonly string[] Properties = { "root", "parent" }; private static readonly ObjectDescriptor Descriptor = new ObjectDescriptor(BindingContextType) { diff --git a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs index 4aef53df..7f14d320 100644 --- a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs +++ b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs @@ -26,10 +26,6 @@ public bool TryGetValue(ref ChainSegment segment, out object value) value = _context.ParentContext; return true; - case "value": - value = _context.Value; - return true; - default: return TryGetContextVariable(_context.Value, ref segment, out value); } From ae3a40dd1d83b494e6e79d7c50b0305522dd2e26 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 18 May 2020 05:51:33 -0700 Subject: [PATCH 36/53] Fix code coverage --- .github/workflows/ci.yml | 4 ++-- .github/workflows/pull_request.yml | 4 ++-- source/Handlebars.Test/Handlebars.Test.csproj | 2 +- source/Handlebars.Test/coverlet.settings.xml | 13 +++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 source/Handlebars.Test/coverlet.settings.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58547998..6bf330ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,14 +79,14 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect "code coverage" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.settings.xml projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee beginArguments: > /d:sonar.verbose="true" /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" - /d:sonar.coverage.exclusions="**/*.cs","**/*.md" + /d:sonar.coverage.exclusions="**/*.md" /d:sonar.cs.vstest.reportsPaths="**/*.trx" env: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 20c8661a..34e9c1ba 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -71,7 +71,7 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 -c Release - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect "code coverage" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.settings.xml projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee @@ -79,7 +79,7 @@ jobs: /d:sonar.verbose="true" /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" /d:sonar.cs.vstest.reportsPaths="**/*.trx" - /d:sonar.coverage.exclusions="**/*.cs","**/*.md" + /d:sonar.coverage.exclusions="**/*.md" /d:sonar.pullrequest.key=${{ github.event.number }} /d:sonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} /d:sonar.pullrequest.base=${{ github.event.pull_request.base.ref }} diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index f2a42b3a..6bfc331f 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/source/Handlebars.Test/coverlet.settings.xml b/source/Handlebars.Test/coverlet.settings.xml new file mode 100644 index 00000000..c909ac85 --- /dev/null +++ b/source/Handlebars.Test/coverlet.settings.xml @@ -0,0 +1,13 @@ + + + + + + + opencover + [Expressions.Shortcuts]*,[Expressions.Shortcuts.*]* + + + + + \ No newline at end of file From d0270f119fa41a386b93fb6ba81d9b9010630f94 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 19 May 2020 02:14:14 +0300 Subject: [PATCH 37/53] Fix code coverage - do not use ILRepack as it's not working with Unix - use proper settings --- .github/workflows/ci.yml | 2 +- .github/workflows/pull_request.yml | 2 +- ...rlet.settings.xml => coverlet.runsettings} | 5 +-- source/Handlebars/Handlebars.csproj | 31 +++---------------- 4 files changed, 10 insertions(+), 30 deletions(-) rename source/Handlebars.Test/{coverlet.settings.xml => coverlet.runsettings} (54%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf330ea..a8d38916 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.settings.xml + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 34e9c1ba..872ca8da 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -71,7 +71,7 @@ jobs: uses: Secbyte/dotnet-sonarscanner@v2.2 with: buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 -c Release - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.settings.xml + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee diff --git a/source/Handlebars.Test/coverlet.settings.xml b/source/Handlebars.Test/coverlet.runsettings similarity index 54% rename from source/Handlebars.Test/coverlet.settings.xml rename to source/Handlebars.Test/coverlet.runsettings index c909ac85..dd5debe7 100644 --- a/source/Handlebars.Test/coverlet.settings.xml +++ b/source/Handlebars.Test/coverlet.runsettings @@ -2,10 +2,11 @@ - + opencover - [Expressions.Shortcuts]*,[Expressions.Shortcuts.*]* + [Handlebars*]HandlebarsDotNet,[Handlebars*]HandlebarsDotNet.* + true diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 9c13ef01..b80d3985 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -1,4 +1,4 @@ - + Handlebars @@ -62,44 +62,23 @@ - + - + - + - - + - - - - $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) - - - - - - - - - - - - - - - - From 58b6a7d8b99cf652756b1fa648ed1b8971f83209 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 19 May 2020 02:40:39 +0300 Subject: [PATCH 38/53] Fix CompileFast for Unix - fix CompileFast extension for Unix - fix closure extraction for ValueTypes - obsolete code cleanup --- .../CompileFastExtensions.cs | 6 --- .../Handlebars.Extension.CompileFast.csproj | 7 +++- source/Handlebars/Collections/LookupSlim.cs | 42 ------------------- .../Compiler/Structure/IteratorExpression.cs | 11 ----- source/Handlebars/Features/ClosureFeature.cs | 2 +- 5 files changed, 7 insertions(+), 61 deletions(-) delete mode 100644 source/Handlebars/Collections/LookupSlim.cs diff --git a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs index a64b0cfc..21e3a918 100644 --- a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs +++ b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs @@ -14,12 +14,6 @@ public static class CompileFastExtensions /// public static HandlebarsConfiguration UseCompileFast(this HandlebarsConfiguration configuration) { - if (!OperatingSystem.IsWindows()) - { - Debug.WriteLine("[WARNING] Only Windows OS is supported at the moment. Skipping feature."); - return configuration; - } - var compileTimeConfiguration = configuration.CompileTimeConfiguration; compileTimeConfiguration.Features.Add(new FastCompilerFeatureFactory()); diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index c9697d66..cf43127c 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -35,9 +35,14 @@ - + + + + + + diff --git a/source/Handlebars/Collections/LookupSlim.cs b/source/Handlebars/Collections/LookupSlim.cs deleted file mode 100644 index 743fc5b0..00000000 --- a/source/Handlebars/Collections/LookupSlim.cs +++ /dev/null @@ -1,42 +0,0 @@ -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) - { - _inner = new Dictionary(); - _comparer = comparer ?? EqualityComparer.Default; - } - - 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; - } - - 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/Compiler/Structure/IteratorExpression.cs b/source/Handlebars/Compiler/Structure/IteratorExpression.cs index ed975f73..a67622b6 100644 --- a/source/Handlebars/Compiler/Structure/IteratorExpression.cs +++ b/source/Handlebars/Compiler/Structure/IteratorExpression.cs @@ -6,17 +6,6 @@ namespace HandlebarsDotNet.Compiler { internal class IteratorExpression : BlockHelperExpression { - public IteratorExpression(Expression sequence, Expression template) - : this(sequence, template, Empty()) - { - } - - public IteratorExpression(Expression sequence, Expression template, Expression ifEmpty) - :this(sequence, BlockParamsExpression.Empty(), template, ifEmpty) - { - - } - public IteratorExpression(Expression sequence, BlockParamsExpression blockParams, Expression template, Expression ifEmpty) :base("each", Enumerable.Empty(), blockParams, template, ifEmpty, false) { diff --git a/source/Handlebars/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs index c0114f86..2dc9e189 100644 --- a/source/Handlebars/Features/ClosureFeature.cs +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -106,7 +106,7 @@ protected override Expression VisitConstant(ConstantExpression node) return node; default: - if (node.Type.GetTypeInfo().IsValueType) return node; + if (node.Type.GetTypeInfo().IsPrimitive) return node; break; } From e9f254eb30933cfb5d61c884b5dd8c354abaff3d Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Mon, 18 May 2020 17:26:38 -0700 Subject: [PATCH 39/53] Remove unused `using` --- .../Handlebars.Extension.CompileFast/CompileFastExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs index 21e3a918..0d3d5b3b 100644 --- a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs +++ b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace HandlebarsDotNet.Extension.CompileFast { /// From 6b13c17584c8e7a9754e7cc62a775b32b0246950 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Tue, 19 May 2020 03:34:59 +0300 Subject: [PATCH 40/53] Update Readme with extensions info --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eddf68be..cfba0ea6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -handlebars.csharp [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) -============== +| Package | Version | +|-------------------|---| +| Handlebars.CSharp | [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) | +| Handlebars.Extension.CompileFast | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.CompileFast)](https://www.nuget.org/packages/Handlebars.Extension.CompileFast/) | +| Handlebars.Extension.Logging | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.Logging)](https://www.nuget.org/packages/Handlebars.Extension.Logging/) | ![Build](https://github.com/zjklee/Handlebars.CSharp/workflows/CI/badge.svg) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) -============== - +## + _This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while. I'd be glad to back-merge all the changes back to original repo if I'd have a chance. Meanwhile I'd try to support the fork._ Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET application. @@ -19,8 +22,16 @@ Check out the [handlebars.js documentation](http://handlebarsjs.com) for how to handlebars.csharp doesn't use a scripting engine to run a Javascript library - it **compiles Handlebars templates directly to IL bytecode**. It also mimics the JS library's API as closely as possible. ## Install - - dotnet add package handlebars.csharp +```cmd +dotnet add package Handlebars.CSharp +``` +### Extensions +```cmd +dotnet add package Handlebars.Extension.CompileFast +``` +```cmd +dotnet add package Handlebars.Extension.Logging +``` ## Usage From 02c13707c5e9ad9558cf981a56db56b906492225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6gberg?= Date: Sat, 16 May 2020 16:28:16 +0200 Subject: [PATCH 41/53] Overload for CreateWriterView for CreateView --- .../ViewEngine/ViewEngineTests.cs | 30 +++++++++++++++++-- source/Handlebars/HandlebarsEnvironment.cs | 29 +++++++++++------- source/Handlebars/IHandlebars.cs | 7 +++++ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index a0c04788..023a89a4 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using System.Text; using Xunit; namespace HandlebarsDotNet.Test.ViewEngine @@ -20,12 +22,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 renderView = handleBars.CompileWriterView("views\\someview.hbs"); + 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 CanLoadAViewWithALayoutAndStreamWriting() + { + //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"); 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 CanLoadAViewWithALayoutInTheRoot() { diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index fbcee503..a9b66f23 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -12,7 +12,24 @@ public HandlebarsEnvironment(HandlebarsConfiguration configuration) Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - public Func CompileView(string templatePath) + public Action CompileWriterView(string templatePath) + { + return CompileViewInternal(templatePath); + } + public Func CompileView(string templatePath) + { + var view = CompileViewInternal(templatePath); + return (vm) => + { + using (var writer = new PolledStringWriter(Configuration.FormatProvider)) + { + view(writer, vm); + return writer.ToString(); + } + }; + } + + private Action CompileViewInternal(string templatePath) { var configuration = new InternalHandlebarsConfiguration(Configuration); var createdFeatures = configuration.Features; @@ -23,21 +40,13 @@ public Func CompileView(string templatePath) var compiler = new HandlebarsCompiler(configuration); var compiledView = compiler.CompileView(templatePath, configuration); - Func action = (vm) => - { - using (var writer = new PolledStringWriter(configuration.FormatProvider)) - { - compiledView(writer, vm); - return writer.ToString(); - } - }; for (var index = 0; index < createdFeatures.Count; index++) { createdFeatures[index].CompilationCompleted(); } - return action; + return compiledView; } public HandlebarsConfiguration Configuration { get; } diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index ab41de16..780959b3 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -29,6 +29,13 @@ public interface IHandlebars /// Func CompileView(string templatePath); + /// + /// + /// + /// + /// + Action CompileWriterView(string templatePath); + /// /// /// From 7f822b4b5f9f99a17bed27907f1bd59b00586645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6gberg?= Date: Sat, 16 May 2020 16:36:40 +0200 Subject: [PATCH 42/53] Cleaned up test for cleaner pr --- .../ViewEngine/ViewEngineTests.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index 023a89a4..c70e6b60 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -21,19 +21,15 @@ public void CanLoadAViewWithALayout() }; //When a viewengine renders that view - var handleBars = Handlebars.Create(new HandlebarsConfiguration() {FileSystem = files}); - var renderView = handleBars.CompileWriterView("views\\someview.hbs"); - var sb = new StringBuilder(); - var writer = new StringWriter(sb); - renderView(writer, null); - var output = sb.ToString(); + 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 CanLoadAViewWithALayoutAndStreamWriting() + public void CanLoadAWriterViewWithALayout() { //Given a layout in a subfolder var files = new FakeFileSystem() @@ -45,13 +41,15 @@ public void CanLoadAViewWithALayoutAndStreamWriting() //When a viewengine renders that view var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); - var renderView = handleBars.CompileView ("views\\someview.hbs"); - var output = renderView(null); + var renderView = handleBars.CompileWriterView("views\\someview.hbs"); + 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() { From b5ef30d67842c55d13b96b96252db31cadbe2b87 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 14:42:37 -0700 Subject: [PATCH 43/53] Implement `ViewReaderFactory` approach --- .../ViewEngine/ViewEngineTests.cs | 3 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 14 +++----- source/Handlebars/Handlebars.cs | 11 ++++++ source/Handlebars/HandlebarsEnvironment.cs | 35 +++++++++++++++---- source/Handlebars/IHandlebars.cs | 7 ++-- 5 files changed, 49 insertions(+), 21 deletions(-) diff --git a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs index c70e6b60..63571124 100644 --- a/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs +++ b/source/Handlebars.Test/ViewEngine/ViewEngineTests.cs @@ -41,7 +41,7 @@ public void CanLoadAWriterViewWithALayout() //When a viewengine renders that view var handleBars = Handlebars.Create(new HandlebarsConfiguration() { FileSystem = files }); - var renderView = handleBars.CompileWriterView("views\\someview.hbs"); + var renderView = handleBars.CompileView("views\\someview.hbs", null); var sb = new StringBuilder(); var writer = new StringWriter(sb); renderView(writer, null); @@ -50,6 +50,7 @@ public void CanLoadAWriterViewWithALayout() //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/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index 02e0e3df..5123a789 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -39,17 +39,10 @@ public Action Compile(ExtendedStringReader source) return action; } - internal Action CompileView(string templatePath, - InternalHandlebarsConfiguration configuration) + 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; - using (var sr = new StringReader(template)) + using (var sr = readerFactoryFactory(configuration, templatePath)) { using (var reader = new ExtendedStringReader(sr)) { @@ -64,12 +57,13 @@ internal Action CompileView(string templatePath, 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 + "'"); - var compiledLayout = CompileView(layoutPath, configuration); + var compiledLayout = CompileView(readerFactoryFactory, layoutPath, configuration); return (tw, vm) => { diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index 44d3d990..810cd34f 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -78,6 +78,17 @@ public static Func CompileView(string templatePath) return Instance.CompileView(templatePath); } + /// + /// + /// + /// + /// + /// + public static Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) + { + return Instance.CompileView(templatePath, readerFactoryFactory); + } + /// /// /// diff --git a/source/Handlebars/HandlebarsEnvironment.cs b/source/Handlebars/HandlebarsEnvironment.cs index a9b66f23..85cad15e 100644 --- a/source/Handlebars/HandlebarsEnvironment.cs +++ b/source/Handlebars/HandlebarsEnvironment.cs @@ -5,20 +5,41 @@ namespace HandlebarsDotNet { + /// + /// + /// + /// + /// + public delegate TextReader ViewReaderFactory(ICompiledHandlebarsConfiguration configuration, string templatePath); + internal class HandlebarsEnvironment : IHandlebars { + private static readonly ViewReaderFactory ViewReaderFactory = (configuration, path) => + { + 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 Action CompileWriterView(string templatePath) + public Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory) { - return CompileViewInternal(templatePath); + readerFactoryFactory = readerFactoryFactory ?? ViewReaderFactory; + return CompileViewInternal(templatePath, readerFactoryFactory); } + public Func CompileView(string templatePath) { - var view = CompileViewInternal(templatePath); + var view = CompileViewInternal(templatePath, ViewReaderFactory); return (vm) => { using (var writer = new PolledStringWriter(Configuration.FormatProvider)) @@ -29,7 +50,7 @@ public Func CompileView(string templatePath) }; } - private Action CompileViewInternal(string templatePath) + private Action CompileViewInternal(string templatePath, ViewReaderFactory readerFactoryFactory) { var configuration = new InternalHandlebarsConfiguration(Configuration); var createdFeatures = configuration.Features; @@ -37,10 +58,10 @@ private Action CompileViewInternal(string templatePath) { createdFeatures[index].OnCompiling(configuration); } - + var compiler = new HandlebarsCompiler(configuration); - var compiledView = compiler.CompileView(templatePath, configuration); - + var compiledView = compiler.CompileView(readerFactoryFactory, templatePath, configuration); + for (var index = 0; index < createdFeatures.Count; index++) { createdFeatures[index].CompilationCompleted(); diff --git a/source/Handlebars/IHandlebars.cs b/source/Handlebars/IHandlebars.cs index 780959b3..322d8953 100644 --- a/source/Handlebars/IHandlebars.cs +++ b/source/Handlebars/IHandlebars.cs @@ -28,14 +28,15 @@ public interface IHandlebars /// /// Func CompileView(string templatePath); - + /// /// /// /// + /// Can be null /// - Action CompileWriterView(string templatePath); - + Action CompileView(string templatePath, ViewReaderFactory readerFactoryFactory); + /// /// /// From e700983358dfcb4ae2a206e6c7a95f90277e8844 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 24 May 2020 22:04:09 +0300 Subject: [PATCH 44/53] Improve memory usage and performance --- source/Handlebars.Benchmark/Execution.cs | 32 +- .../Handlebars.Benchmark.csproj | 4 +- .../FastExpressionCompiler.cs | 4 +- .../Handlebars.Extension.CompileFast.csproj | 2 +- .../Handlebars.Extension.Logger.csproj | 2 +- source/Handlebars.Test/Handlebars.Test.csproj | 2 +- .../Handlebars/Collections/DeferredValue.cs | 15 +- source/Handlebars/Collections/HashHelpers.cs | 86 ----- source/Handlebars/Collections/HashSetSlim.cs | 32 ++ .../Handlebars/Collections/IRefDictionary.cs | 9 - source/Handlebars/Collections/LookupSlim.cs | 54 +++ .../Handlebars/Collections/RefDictionary.cs | 207 ----------- source/Handlebars/Collections/RefLookup.cs | 42 --- .../Compiler/Structure/BindingContext.cs | 9 +- .../Compiler/Structure/Path/ChainSegment.cs | 59 ++- .../Compiler/Structure/Path/PathInfo.cs | 33 +- .../Compiler/Structure/Path/PathResolver.cs | 345 +++++++++++++++--- .../Compiler/Structure/Path/PathSegment.cs | 54 ++- .../Structure/UndefinedBindingResult.cs | 7 + .../Expression/BlockHelperFunctionBinder.cs | 2 +- .../Translation/Expression/IteratorBinder.cs | 22 +- .../InternalHandlebarsConfiguration.cs | 26 +- .../Features/BuildInHelpersFeature.cs | 2 +- .../Features/DefaultCompilerFeature.cs | 6 +- source/Handlebars/Features/TemplateClosure.cs | 17 +- source/Handlebars/Handlebars.csproj | 4 +- .../MemberAccessors/ContextMemberAccessor.cs | 2 +- .../ReflectionMemberAccessor.cs | 218 +++++------ .../CollectionMemberAliasProvider.cs | 4 +- .../CollectionObjectDescriptor.cs | 11 +- .../ContextObjectDescriptor.cs | 9 +- .../DictionaryObjectDescriptor.cs | 27 +- .../DynamicObjectDescriptor.cs | 10 +- ...nericDictionaryObjectDescriptorProvider.cs | 51 +-- .../KeyValuePairObjectDescriptorProvider.cs | 19 +- .../ObjectDescriptors/ObjectDescriptor.cs | 80 +++- .../ObjectDescriptorFactory.cs | 84 ++--- .../ObjectDescriptorProvider.cs | 41 +-- ...tringDictionaryObjectDescriptorProvider.cs | 48 +-- .../BindingContextValueProvider.cs | 12 +- 40 files changed, 888 insertions(+), 805 deletions(-) delete mode 100644 source/Handlebars/Collections/HashHelpers.cs create mode 100644 source/Handlebars/Collections/HashSetSlim.cs delete mode 100644 source/Handlebars/Collections/IRefDictionary.cs create mode 100644 source/Handlebars/Collections/LookupSlim.cs delete mode 100644 source/Handlebars/Collections/RefDictionary.cs delete mode 100644 source/Handlebars/Collections/RefLookup.cs diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index 019bc8b3..b02d08ce 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using BenchmarkDotNet.Attributes; @@ -10,19 +11,18 @@ namespace Benchmark { - [SimpleJob(RuntimeMoniker.NetCoreApp31)] public class Execution { - [Params(2, 5, 10)] + [Params(2)] public int N; - [Params("current", "current-cache", "current-fast", "current-fast-cache", "1.10.1")] + [Params("current", "current-cache"/*, "current-fast", "current-fast-cache", "1.10.1"*/)] public string Version; [Params("object", "dictionary")] public string DataType; - private Func[] _templates; + private Action[] _templates; private object _data; @@ -78,11 +78,14 @@ public void Setup() var handlebars = methodInfo.Invoke(null, new object[]{ null }); var objType = handlebars.GetType(); - var compileMethod = objType.GetMethod("Compile", new[]{typeof(string)}); - _templates = new[] + using (var reader = new StringReader(template)) { - (Func) compileMethod.Invoke(handlebars, new []{template}), - }; + var compileMethod = objType.GetMethod("Compile", new[] {typeof(TextReader)}); + _templates = new[] + { + (Action) compileMethod.Invoke(handlebars, new[] {reader}), + }; + } } else { @@ -93,10 +96,13 @@ public void Setup() Handlebars.Configuration.UseCompileFast(); } - _templates = new[] + using (var reader = new StringReader(template)) { - Handlebars.Compile(template) - }; + _templates = new[] + { + Handlebars.Compile(reader) + }; + } } List ObjectLevel1Generator() @@ -233,9 +239,9 @@ JArray JsonLevel3Generator(int id1, int id2) } [Benchmark] - public string WithParentIndex() + public void Render() { - return _templates[0].Invoke(_data); + _templates[0].Invoke(TextWriter.Null, _data); } } } \ No newline at end of file diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index 0f8fa402..56442923 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp2.1;netcoreapp2.1 + netcoreapp3.1 - + diff --git a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs index d8580816..0863c98c 100644 --- a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs +++ b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs @@ -47,9 +47,9 @@ public T Compile(Expression expression) where T: class var outerParameters = expression.Parameters.Select(o => Expression.Parameter(o.Type, o.Name)).ToArray(); - var store = ExpressionShortcuts.Arg(_templateClosure).Property(o => o.Store); + var store = (Expression) Expression.Field(Expression.Constant(_templateClosure), nameof(TemplateClosure.Store)); var outerLambda = Expression.Lambda( - Expression.Invoke(Expression.Constant(compiledLambda), new[] {store.Expression}.Concat(outerParameters)), + Expression.Invoke(Expression.Constant(compiledLambda), new[] {store}.Concat(outerParameters)), outerParameters); return outerLambda.CompileFast(); diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index cf43127c..04d8ad89 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -1,7 +1,7 @@ - net452;netstandard1.3;netstandard2.0 + netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.CompileFast 7 1.0.0 diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj index c66a9dbb..3c4acb15 100644 --- a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj +++ b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj @@ -1,7 +1,7 @@ - net452;netstandard1.3;netstandard2.0 + netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.Logging latest enable diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 6bfc331f..cdade6a6 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - net461;netcoreapp1.1;netcoreapp2.1;netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1 700AF0B4-EA70-47B7-9F9D-17351E977B00 diff --git a/source/Handlebars/Collections/DeferredValue.cs b/source/Handlebars/Collections/DeferredValue.cs index 458ac962..fed3e601 100644 --- a/source/Handlebars/Collections/DeferredValue.cs +++ b/source/Handlebars/Collections/DeferredValue.cs @@ -2,20 +2,27 @@ namespace HandlebarsDotNet.Collections { - internal struct DeferredValue + internal class DeferredValue { + private readonly TState _state; + private readonly Func _factory; + private T _value; private bool _isValueCreated; - - public Func Factory { get; set; } + public DeferredValue(TState state, Func factory) + { + _state = state; + _factory = factory; + } + public T Value { get { if (_isValueCreated) return _value; - _value = Factory(); + _value = _factory(_state); _isValueCreated = true; return _value; } diff --git a/source/Handlebars/Collections/HashHelpers.cs b/source/Handlebars/Collections/HashHelpers.cs deleted file mode 100644 index 15620c15..00000000 --- a/source/Handlebars/Collections/HashHelpers.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace HandlebarsDotNet.Collections -{ - internal static class HashHelpers - { - // This is the maximum prime smaller than Array.MaxArrayLength - private const int MaxPrimeArrayLength = 0x7FEFFFFD; - - private const int HashPrime = 101; - - // Table of prime numbers to use as hash table sizes. - // A typical resize algorithm would pick the smallest prime number in this array - // that is larger than twice the previous capacity. - // Suppose our Hashtable currently has capacity x and enough elements are added - // such that a resize needs to occur. Resizing first computes 2x then finds the - // first prime in the table greater than 2x, i.e. if primes are ordered - // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n. - // Doubling is important for preserving the asymptotic complexity of the - // hashtable operations such as add. Having a prime guarantees that double - // hashing does not lead to infinite loops. IE, your hash function will be - // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. - // We prefer the low computation costs of higher prime numbers over the increased - // memory allocation of a fixed prime number i.e. when right sizing a HashSet. - private static readonly int[] Primes = - { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, - 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, - 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, - 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 - }; - - private static bool IsPrime(int candidate) - { - if ((candidate & 1) == 0) return candidate == 2; - - var limit = (int) Math.Sqrt(candidate); - for (var divisor = 3; divisor <= limit; divisor += 2) - { - if ((candidate % divisor) == 0) - return false; - } - - return true; - } - - public static int GetPrime(int min) - { - if (min < 0) - throw new ArgumentException(); - - for (var i = 0; i < Primes.Length; i++) - { - var prime = Primes[i]; - if (prime >= min) - return prime; - } - - //outside of our predefined table. - //compute the hard way. - for (var i = (min | 1); i < Int32.MaxValue; i += 2) - { - if (IsPrime(i) && ((i - 1) % HashPrime != 0)) - return i; - } - - return min; - } - - // Returns size of hashtable to grow to. - public static int ExpandPrime(int oldSize) - { - var newSize = 2 * oldSize; - - // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. - // Note that this check works even when _items.Length overflowed thanks to the (uint) cast - if ((uint) newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) - { - return MaxPrimeArrayLength; - } - - return GetPrime(newSize); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/HashSetSlim.cs b/source/Handlebars/Collections/HashSetSlim.cs new file mode 100644 index 00000000..b8aa95df --- /dev/null +++ b/source/Handlebars/Collections/HashSetSlim.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading; + +namespace HandlebarsDotNet.Collections +{ + 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/IRefDictionary.cs b/source/Handlebars/Collections/IRefDictionary.cs deleted file mode 100644 index 5c645272..00000000 --- a/source/Handlebars/Collections/IRefDictionary.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace HandlebarsDotNet.Collections -{ - internal interface IRefDictionary : IReadOnlyDictionary where TValue : struct - { - new ref TValue this[TKey key] { get; } - } -} \ 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/RefDictionary.cs b/source/Handlebars/Collections/RefDictionary.cs deleted file mode 100644 index 00355965..00000000 --- a/source/Handlebars/Collections/RefDictionary.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace HandlebarsDotNet.Collections -{ - /// - /// Represents -like collection optimized for storing s. - /// The class API is very limited due to performance demands. - /// ! Collection does not guaranty successful write in concurrent scenarios: writes may be lost but it guaranties to return a value. - /// - /// - /// - internal class RefDictionary : IRefDictionary where TValue : struct - { - private readonly IEqualityComparer _comparer; - private Container _container; - - public RefDictionary(int capacity = 16, IEqualityComparer comparer = null) - { - var initialCapacity = HashHelpers.GetPrime(capacity); - _container = new Container(new int[initialCapacity], new Entry[initialCapacity]); - _comparer = comparer ?? EqualityComparer.Default; - } - - public int Count => _container.Count; - - public bool ContainsKey(TKey key) - { - var container = _container; - var entries = container.Entries; - var entryIndex = GetEntryIndex(key, container); - - while (entryIndex != -1) - { - if (_comparer.Equals(entries[entryIndex].Key, key)) return true; - - entryIndex = entries[entryIndex].Next; - } - - return false; - } - - bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) - { - if (!ContainsKey(key)) - { - value = default(TValue); - return false; - } - - value = this[key]; - return true; - } - - TValue IReadOnlyDictionary.this[TKey key] => - ContainsKey(key) ? this[key] : default(TValue); - - public ref TValue this[TKey key] - { - get - { - var container = _container; - var entries = container.Entries; - - var entryIndex = GetEntryIndex(key, container); - while (entryIndex != -1) - { - if (_comparer.Equals(entries[entryIndex].Key, key)) - { - return ref entries[entryIndex].Value; - } - - entryIndex = entries[entryIndex].Next; - } - - if (Count == entries.Length && !TryResize(container, out container)) - { - var entry = new Entry(key); - var holder = new CollisionHolder(ref entry); - return ref holder.Entry.Value; - } - - entries = container.Entries; - var buckets = container.Buckets; - - entryIndex = container.Count++; - entries[entryIndex].Key = key; - var bucket = GetBucketIndex(key, container); - entries[entryIndex].Next = buckets[bucket] - 1; - buckets[bucket] = entryIndex + 1; - return ref entries[entryIndex].Value; - } - } - - IEnumerable IReadOnlyDictionary.Keys - { - get - { - var container = _container; - var entries = container.Entries; - for (var index = 0; index < container.Count; index++) - { - yield return entries[index].Key; - } - } - } - - IEnumerable IReadOnlyDictionary.Values - { - get - { - var container = _container; - var entries = container.Entries; - for (var index = 0; index < container.Count; index++) - { - yield return entries[index].Value; - } - } - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - var container = _container; - var entries = container.Entries; - for (var index = 0; index < Count; index++) - { - var entry = entries[index]; - yield return new KeyValuePair(entry.Key, entry.Value); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable>)this).GetEnumerator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetBucketIndex(TKey key, Container container) => - (_comparer.GetHashCode(key) & 0x7FFFFFFF) % container.Buckets.Length; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetEntryIndex(TKey key, Container container) => - container.Buckets[GetBucketIndex(key, container)] - 1; - - private bool TryResize(Container container, out Container newContainer) - { - var entries = container.Entries; - - var count = container.Count; - var newSize = HashHelpers.ExpandPrime(count); - - var newEntries = new Entry[newSize]; - Array.Copy(entries, 0, newEntries, 0, count); - - var newBuckets = new int[newSize]; - - newContainer = new Container(newBuckets, newEntries, count); - for (var index = 0; index < count;) - { - var bucketIndex = GetBucketIndex(newEntries[index].Key, newContainer); - newEntries[index].Next = newBuckets[bucketIndex] - 1; - newBuckets[bucketIndex] = ++index; - } - - return ReferenceEquals(Interlocked.CompareExchange(ref _container, newContainer, container), container); - } - - private struct Entry - { - public Entry(TKey key) : this() - { - Key = key; - } - - public TKey Key; - public TValue Value; - public int Next; - } - - private class CollisionHolder - { - public Entry Entry; - - public CollisionHolder(ref Entry entry) - { - Entry = entry; - } - } - - private class Container - { - public readonly int[] Buckets; - public readonly Entry[] Entries; - public int Count; - - public Container(int[] buckets, Entry[] entries, int count = 0) - { - Buckets = buckets; - Entries = entries; - Count = count; - } - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Collections/RefLookup.cs b/source/Handlebars/Collections/RefLookup.cs deleted file mode 100644 index f5d7f2d4..00000000 --- a/source/Handlebars/Collections/RefLookup.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; - -namespace HandlebarsDotNet.Collections -{ - internal sealed class RefLookup - where TValue: struct - { - private readonly IRefDictionary _inner; - - public delegate ref TValue ValueFactory(TKey key, ref TValue value); - - public RefLookup(int capacity = 16, IEqualityComparer comparer = null) - { - _inner = new RefDictionary(capacity, comparer); - } - - public RefLookup(IRefDictionary inner) - { - _inner = inner; - } - - public bool ContainsKey(TKey key) - { - return _inner.ContainsKey(key); - } - - public ref TValue GetValueOrDefault(TKey key) - { - return ref _inner[key]; - } - - public ref TValue GetOrAdd(TKey key, ValueFactory factory) - { - if (_inner.ContainsKey(key)) - { - return ref _inner[key]; - } - - return ref factory(key, ref _inner[key]); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/BindingContext.cs b/source/Handlebars/Compiler/Structure/BindingContext.cs index 3ddf0e19..a4c6dfab 100644 --- a/source/Handlebars/Compiler/Structure/BindingContext.cs +++ b/source/Handlebars/Compiler/Structure/BindingContext.cs @@ -29,13 +29,8 @@ public static BindingContext Create(InternalHandlebarsConfiguration configuratio return Pool.CreateContext(configuration, value, writer, parent, templatePath, partialBlockTemplate, inlinePartialTemplates); } - private BindingContext(InternalHandlebarsConfiguration configuration, object value, EncodedTextWriter writer, BindingContext parent) + private BindingContext() { - Configuration = configuration; - TextWriter = writer; - Value = value; - ParentContext = parent; - RegisterValueProvider(new BindingContextValueProvider(this)); } @@ -208,7 +203,7 @@ private class BindingContextPolicy : IPooledObjectPolicy { public BindingContext Create() { - return new BindingContext((InternalHandlebarsConfiguration) null, (object) null, (EncodedTextWriter) null, (BindingContext) null); + return new BindingContext(); } public bool Return(BindingContext item) diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index 21a5d22f..318c8136 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -5,30 +5,57 @@ namespace HandlebarsDotNet.Compiler.Structure.Path { [DebuggerDisplay("{Value}")] - internal struct ChainSegment + internal struct ChainSegment : IEquatable { - public static ChainSegment Create(string value) + public ChainSegment(string value) { var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); var segmentTrimmedValue = TrimSquareBrackets(segmentValue).Intern(); - return new ChainSegment - { - IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase), - Value = segmentValue, - IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"), - TrimmedValue = segmentTrimmedValue, - LowerInvariant = segmentTrimmedValue.ToLowerInvariant().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(); + } + + public readonly string Value; + public readonly string LowerInvariant; + public readonly string TrimmedValue; + public readonly bool IsVariable; + public readonly bool IsThis; + + public override string ToString() + { + return Value; + } + + public bool Equals(ChainSegment other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + return obj is ChainSegment other && Equals(other); + } + + public override int GetHashCode() + { + return Value != null ? Value.GetHashCode() : 0; } - public string Value { get; private set; } - public string LowerInvariant { get; private set; } - public string TrimmedValue { get; private set; } - public bool IsVariable { get; private set; } - public bool IsThis { get; private set; } + public static bool operator ==(ChainSegment a, ChainSegment b) + { + return a.Equals(b); + } + + public static bool operator !=(ChainSegment a, ChainSegment b) + { + return !a.Equals(b); + } - private static string TrimSquareBrackets(string key) + public static string TrimSquareBrackets(string key) { //Only trim a single layer of brackets. if (key.StartsWith("[") && key.EndsWith("]")) diff --git a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs index 74c81171..fe06d721 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -1,28 +1,30 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System; namespace HandlebarsDotNet.Compiler.Structure.Path { - [DebuggerDisplay("{Path}")] - internal struct PathInfo + internal delegate object ProcessSegment(ref PathInfo pathInfo, ref BindingContext context, object instance, HashParameterDictionary hashParameters); + + internal struct PathInfo : IEquatable { - public PathInfo(bool hasValue, string path, IEnumerable segments) + public PathInfo(bool hasValue, string path, PathSegment[] segments, ProcessSegment processSegment) { HasValue = hasValue; Path = path; IsVariable = path.StartsWith("@"); IsInversion = path.StartsWith("^"); IsHelper = path.StartsWith("#"); - Segments = segments?.ToArray(); + Segments = segments; + ProcessSegment = processSegment; } - public bool IsHelper { get; } - public bool IsInversion { get; } - public bool HasValue { get; } - public string Path { get; } - public bool IsVariable { get; } - public PathSegment[] Segments { get; } + public readonly bool IsHelper; + public readonly bool IsInversion; + public readonly bool HasValue; + public readonly string Path; + public readonly bool IsVariable; + public readonly PathSegment[] Segments; + + public readonly ProcessSegment ProcessSegment; public bool Equals(PathInfo other) { @@ -42,6 +44,11 @@ public override int GetHashCode() return Path.GetHashCode(); } + public override string ToString() + { + return Path; + } + public static implicit operator string(PathInfo pathInfo) { return pathInfo.Path; diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index 1c51e8e8..4922fff0 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -1,15 +1,17 @@ +using System; using System.Collections.Generic; using System.Linq; +using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path { - internal static class PathResolver + internal static partial class PathResolver { public static PathInfo GetPathInfo(string path) { if (path == "null") - return new PathInfo(false, path, null); + return new PathInfo(false, path, null, null); var originalPath = path; @@ -26,16 +28,39 @@ public static PathInfo GetPathInfo(string path) { if (segment == "..") { - segments.Add(new PathSegment(segment, Enumerable.Empty(), true)); + segments.Add(new PathSegment(segment, Array.Empty(), true, null)); continue; } var segmentString = isVariable ? "@" + segment : segment; - segments.Add(new PathSegment(segmentString, GetPathChain(segmentString), false)); + var chainSegments = GetPathChain(segmentString).ToArray(); + 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)); } - return new PathInfo(true, originalPath, segments); + 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, segments.ToArray(), @delegate); } @@ -44,56 +69,77 @@ public static object ResolvePath(BindingContext context, ref PathInfo pathInfo) { if (!pathInfo.HasValue) return null; - - var configuration = context.Configuration; - var containsVariable = pathInfo.IsVariable; + var instance = context.Value; var hashParameters = instance as HashParameterDictionary; - for (var segmentIndex = 0; segmentIndex < pathInfo.Segments.Length; segmentIndex++) - { - ref var segment = ref pathInfo.Segments[segmentIndex]; - if (segment.IsJumpUp) - { - context = context.ParentContext; - if (context == null) - { - if (containsVariable) return string.Empty; + return pathInfo.ProcessSegment(ref pathInfo, ref context, instance, hashParameters); + } - throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); - } + 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 = context.Value; - continue; - } + instance = segment.ProcessPathChain(context, hashParameters, ref pathInfo, ref segment, instance); + return !(instance is UndefinedBindingResult); + } - for (var pathChainIndex = 0; pathChainIndex < segment.PathChain.Length; pathChainIndex++) + private static bool TryProcessJumpSegment( + ref PathInfo pathInfo, + ref object instance, + ref BindingContext context + ) + { + context = context.ParentContext; + if (context == null) + { + if (pathInfo.IsVariable) { - ref var chainSegment = ref segment.PathChain[pathChainIndex]; - instance = ResolveValue(context, instance, ref chainSegment); + instance = string.Empty; + return false; + } - if (!(instance is UndefinedBindingResult)) - continue; + throw new HandlebarsCompilerException("Path expression tried to reference parent of root"); + } - if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || - context.ParentContext == null) - { - if (configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, - (instance as UndefinedBindingResult).Value); - return instance; - } + instance = context.Value; + return true; + } - instance = ResolveValue(context.ParentContext, context.ParentContext.Value, ref chainSegment); - if (!(instance is UndefinedBindingResult result)) continue; + private static object ProcessChainSegment( + BindingContext context, + HashParameterDictionary hashParameters, + ref PathInfo pathInfo, + ref ChainSegment chainSegment, + object instance + ) + { + instance = ResolveValue(context, instance, ref chainSegment); - if (configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, result.Value); - return result; - } + if (!(instance is UndefinedBindingResult)) + return instance; + + if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || + context.ParentContext == null) + { + if (context.Configuration.ThrowOnUnresolvedBindingExpression) + throw new HandlebarsUndefinedBindingException(pathInfo.Path, + (instance as UndefinedBindingResult).Value); + return instance; } - 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.Path, result.Value); + return result; } private static IEnumerable GetPathChain(string segmentString) @@ -109,7 +155,7 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list[list.Count - 1] = ChainSegment.Create($"{list[list.Count - 1].Value}.{next}"); + list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].Value}.{next}"); return list; } @@ -123,7 +169,7 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list.Add(ChainSegment.Create(next)); + list.Add(new ChainSegment(next)); return list; }); @@ -132,16 +178,17 @@ private static IEnumerable GetPathChain(string segmentString) private static object ResolveValue(BindingContext context, object instance, ref ChainSegment chainSegment) { - var configuration = context.Configuration; object resolvedValue; if (chainSegment.IsVariable) { - return context.TryGetContextVariable(ref chainSegment, out resolvedValue) ? resolvedValue : new UndefinedBindingResult(chainSegment.Value, configuration); + return !context.TryGetContextVariable(ref chainSegment, out resolvedValue) + ? new UndefinedBindingResult(chainSegment.Value, context.Configuration) + : resolvedValue; } if (chainSegment.IsThis) return instance; - if (TryAccessMember(instance, ref chainSegment, configuration, out resolvedValue) + if (TryAccessMember(instance, ref chainSegment, context.Configuration, out resolvedValue) || context.TryGetVariable(ref chainSegment, out resolvedValue)) { return resolvedValue; @@ -152,32 +199,30 @@ private static object ResolveValue(BindingContext context, object instance, ref return resolvedValue; } - return new UndefinedBindingResult(chainSegment.Value, configuration); + return new UndefinedBindingResult(chainSegment.Value, context.Configuration); } public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) { - var memberName = chainSegment.Value; if (instance == null) { - value = new UndefinedBindingResult(memberName, configuration); + value = new UndefinedBindingResult(chainSegment, configuration); return false; } - + + var memberName = chainSegment.Value; var instanceType = instance.GetType(); - memberName = ResolveMemberName(instance, memberName, configuration); - memberName = ReferenceEquals(memberName, chainSegment.Value) - ? chainSegment.TrimmedValue - : TrimSquareBrackets(memberName).Intern(); + memberName = TryResolveMemberName(instance, memberName, configuration, out var result) + ? TrimSquareBrackets(result).Intern() + : chainSegment.TrimmedValue; - var descriptorProvider = configuration.ObjectDescriptorProvider; - if (!descriptorProvider.CanHandleType(instanceType)) + if (!configuration.ObjectDescriptorProvider.CanHandleType(instanceType)) { value = new UndefinedBindingResult(memberName, configuration); return false; } - if (!descriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) + if (!configuration.ObjectDescriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) { value = new UndefinedBindingResult(memberName, configuration); return false; @@ -197,10 +242,190 @@ private static string TrimSquareBrackets(string key) return key; } - private static string ResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration) + private static bool TryResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration, out string value) { var resolver = configuration.ExpressionNameResolver; - return resolver != null ? resolver.ResolveExpressionName(instance, memberName) : memberName; + 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 index ed238625..e228442a 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs @@ -1,23 +1,55 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System; namespace HandlebarsDotNet.Compiler.Structure.Path { - [DebuggerDisplay("{Segment}")] - internal struct PathSegment + internal delegate object ProcessPathChain(BindingContext context, HashParameterDictionary hashParameters, ref PathInfo pathInfo, ref PathSegment segment, object instance); + + internal struct PathSegment : IEquatable { - public PathSegment(string segment, IEnumerable chain, bool isJumpUp) + public PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, ProcessPathChain processPathChain) { Segment = segment; IsJumpUp = isJumpUp; - PathChain = chain.ToArray(); + PathChain = chain; + ProcessPathChain = processPathChain; } - - public string Segment { get; } - public bool IsJumpUp { get; } + public readonly string Segment; + + public readonly bool IsJumpUp; + + public readonly ChainSegment[] PathChain; - public ChainSegment[] PathChain { get; } + public readonly ProcessPathChain ProcessPathChain; + + public override string ToString() + { + return Segment; + } + + public bool Equals(PathSegment other) + { + return Segment == other.Segment; + } + + public override bool Equals(object obj) + { + return obj is PathSegment other && Equals(other); + } + + public override int GetHashCode() + { + return Segment != null ? Segment.GetHashCode() : 0; + } + + public static bool operator ==(PathSegment a, PathSegment b) + { + return a.Equals(b); + } + + public static bool operator !=(PathSegment a, PathSegment b) + { + return !a.Equals(b); + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index ce47a968..216be7a0 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using HandlebarsDotNet.Compiler.Structure.Path; namespace HandlebarsDotNet.Compiler { @@ -13,6 +14,12 @@ public UndefinedBindingResult(string value, ICompiledHandlebarsConfiguration con Value = value; _configuration = configuration; } + + public UndefinedBindingResult(ChainSegment value, ICompiledHandlebarsConfiguration configuration) + { + Value = value.Value; + _configuration = configuration; + } public override string ToString() { diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 17bfc39f..26bd36ad 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -152,7 +152,7 @@ params object[] arguments return; } - var segment = ChainSegment.Create(helperName); + var segment = new ChainSegment(helperName); bindingContext.TryGetContextVariable(ref segment, out var value); DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); } diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index 6add517f..1ff78ab1 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -40,13 +40,9 @@ protected override Expression VisitIteratorExpression(IteratorExpression iex) .Line(blockParamsProviderVar.Using((self, builder) => { builder - .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) - .Line(ExpressionShortcuts.Try() - .Body(ExpressionShortcuts.Call(() => - Iterator.Iterate(context, self, sequence, template, ifEmpty) - )) - .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) - ); + .Line(ExpressionShortcuts.Call(() => + Iterator.Iterate(context, self, sequence, template, ifEmpty) + )); })); } } @@ -87,7 +83,7 @@ public static void Iterate(BindingContext context, if (!descriptor.ShouldEnumerate) { - var properties = descriptor.GetProperties(target); + var properties = descriptor.GetProperties(descriptor, target); if (properties is IList propertiesList) { IterateObjectWithStaticProperties(context, descriptor, blockParamsValueProvider, target, propertiesList, targetType, template, ifEmpty); @@ -137,9 +133,9 @@ private static void IterateObject(BindingContext context, using(var innerContext = context.CreateChildContext(iterator.Value)) { + innerContext.RegisterValueProvider(blockParamsValueProvider); innerContext.RegisterValueProvider(iterator); template(context, context.TextWriter, innerContext); - innerContext.UnregisterValueProvider(iterator); } } @@ -161,7 +157,7 @@ private static void IterateObjectWithStaticProperties(BindingContext context, { using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - blockParamsValueProvider?.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); + blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); var accessor = descriptor.MemberAccessor; @@ -175,9 +171,9 @@ private static void IterateObjectWithStaticProperties(BindingContext context, using (var innerContext = context.CreateChildContext(iterator.Value)) { + innerContext.RegisterValueProvider(blockParamsValueProvider); innerContext.RegisterValueProvider(iterator); template(context, context.TextWriter, innerContext); - innerContext.UnregisterValueProvider(iterator); } } @@ -207,9 +203,9 @@ private static void IterateList(BindingContext context, using(var innerContext = context.CreateChildContext(iterator.Value)) { + innerContext.RegisterValueProvider(blockParamsValueProvider); innerContext.RegisterValueProvider(iterator); template(context, context.TextWriter, innerContext); - innerContext.UnregisterValueProvider(iterator); } } @@ -243,9 +239,9 @@ private static void IterateEnumerable(BindingContext context, using(var innerContext = context.CreateChildContext(iterator.Value)) { + innerContext.RegisterValueProvider(blockParamsValueProvider); innerContext.RegisterValueProvider(iterator); template(context, context.TextWriter, innerContext); - innerContext.UnregisterValueProvider(iterator); } } diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs index afa072d8..9595bab0 100644 --- a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -52,7 +52,19 @@ internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) BlockHelpers = new CascadeDictionary(configuration.BlockHelpers, StringComparer.OrdinalIgnoreCase); RegisteredTemplates = new CascadeDictionary>(configuration.RegisteredTemplates, StringComparer.OrdinalIgnoreCase); HelperResolvers = new CascadeCollection(configuration.HelperResolvers); - + + 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) + } + }; + var objectDescriptorProvider = new ObjectDescriptorProvider(this); var listObjectDescriptor = new CollectionObjectDescriptor(objectDescriptorProvider); var providers = new List(configuration.CompileTimeConfiguration.ObjectDescriptorProviders) @@ -70,18 +82,6 @@ internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) ObjectDescriptorProvider = new ObjectDescriptorFactory(providers); - 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) - } - }; - Features = CompileTimeConfiguration.Features.Select(o => o.CreateFeature()).ToList(); } } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index d41ff85f..d27c8231 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -64,7 +64,7 @@ private object Lookup(dynamic context, params object[] arguments) } var memberName = arguments[1].ToString(); - var segment = ChainSegment.Create(memberName); + var segment = new ChainSegment(memberName); return !PathResolver.TryAccessMember(arguments[0], ref segment, _configuration, out var value) ? new UndefinedBindingResult(memberName, _configuration) : value; diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index 4778a5a2..de738679 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -59,10 +59,10 @@ public T Compile(Expression expression) where T: class var compiledLambda = lambda.Compile(); var outerParameters = expression.Parameters.Select(o => Expression.Parameter(o.Type, o.Name)).ToArray(); - - var store = ExpressionShortcuts.Arg(_templateClosure).Property(o => o.Store); + + var store = (Expression) Expression.Field(Expression.Constant(_templateClosure), nameof(TemplateClosure.Store)); var outerLambda = Expression.Lambda( - Expression.Invoke(Expression.Constant(compiledLambda), new[] {store.Expression}.Concat(outerParameters)), + Expression.Invoke(Expression.Constant(compiledLambda), new[] {store}.Concat(outerParameters)), outerParameters); return outerLambda.Compile(); diff --git a/source/Handlebars/Features/TemplateClosure.cs b/source/Handlebars/Features/TemplateClosure.cs index d33f5fcd..882573c3 100644 --- a/source/Handlebars/Features/TemplateClosure.cs +++ b/source/Handlebars/Features/TemplateClosure.cs @@ -10,17 +10,18 @@ public sealed class TemplateClosure { private Dictionary _objectSet = new Dictionary(); private List _inner = new List(); - private object[] _store = new object[0]; - + /// - /// Index for the next item reference + /// Actual closure storage /// - public int CurrentIndex => _inner?.Count ?? -1; + public object[] Store = new object[0]; /// - /// Actual closure storage + /// Index for the next item reference /// - public object[] Store => _store; + public int CurrentIndex => _inner?.Count ?? -1; + + //public object[] Store => _store; /// /// Adds value to store @@ -54,8 +55,8 @@ internal void Build() { if(_inner == null) return; - Array.Resize(ref _store, _inner.Count); - _inner.CopyTo(_store, 0); + Array.Resize(ref Store, _inner.Count); + _inner.CopyTo(Store, 0); _inner.Clear(); _inner = null; diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index b80d3985..9e9c5695 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -3,7 +3,7 @@ Handlebars portable - net452;netstandard1.3;netstandard2.0 + netstandard1.3;netstandard2.0 1.0.2 7 HandlebarsDotNet @@ -63,6 +63,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs index e631d7dd..a48d9032 100644 --- a/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ContextMemberAccessor.cs @@ -9,7 +9,7 @@ internal class ContextMemberAccessor : IMemberAccessor public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) { var bindingContext = (BindingContext) instance; - var segment = ChainSegment.Create(memberName); + var segment = new ChainSegment(memberName); return bindingContext.TryGetContextVariable(ref segment, out value); } } diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs index 9607779b..bb57fc4c 100644 --- a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -29,7 +29,8 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o var aliasProviders = _configuration.CompileTimeConfiguration.AliasProviders; for (var index = 0; index < aliasProviders.Count; index++) { - if (aliasProviders[index].TryGetMemberByAlias(instance, instanceType, memberName, out value)) return true; + if (aliasProviders[index].TryGetMemberByAlias(instance, instanceType, memberName, out value)) + return true; } value = null; @@ -38,156 +39,161 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o private class PlainReflectionMemberAccessor : IMemberAccessor { - private readonly RefLookup> _descriptors = - new RefLookup>(); + private readonly LookupSlim> _descriptors = + new LookupSlim>(); + + private static readonly Func> ValueFactory = + key => new DeferredValue(key, type => new RawObjectTypeDescriptor(type)); public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) { - ObjectTypeDescriptor descriptor; - if (_descriptors.ContainsKey(instanceType)) - { - ref var deferredValue = ref _descriptors.GetValueOrDefault(instanceType); - descriptor = deferredValue.Value; - }else + if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) { - ref var deferredValue = ref _descriptors.GetOrAdd(instanceType, ValueFactory); - descriptor = deferredValue.Value; + deferredValue = _descriptors.GetOrAdd(instanceType, ValueFactory); } - var accessor = descriptor.GetOrCreateAccessor(memberName); + var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); value = accessor?.Invoke(instance); return accessor != null; } - private static ref DeferredValue ValueFactory(Type type, ref DeferredValue deferredValue) + private class RawObjectTypeDescriptor { - deferredValue.Factory = () => new RawObjectTypeDescriptor(type); - return ref deferredValue; + private readonly Type _type; + + private static readonly MethodInfo CreateGetDelegateMethodInfo = typeof(RawObjectTypeDescriptor) + .GetMethod(nameof(CreateGetDelegate), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly Func<(string key, Type type), Func> ValueGetterFactory = + o => GetValueGetter(o.key, o.type); + + private readonly LookupSlim>> + _accessors = new LookupSlim>>(); + + private static readonly Func>> ValueFactory = + (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); + + public RawObjectTypeDescriptor(Type type) + { + _type = type; + } + + public 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 class CompiledReflectionMemberAccessor : IMemberAccessor { - private readonly RefLookup> _descriptors = - new RefLookup>(); + private readonly LookupSlim> _descriptors = + new LookupSlim>(); + + private static readonly Func> ValueFactory = + key => new DeferredValue(key, type => new CompiledObjectTypeDescriptor(type)); public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) { - ObjectTypeDescriptor descriptor; - if (_descriptors.ContainsKey(instanceType)) + if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) { - ref var deferredValue = ref _descriptors.GetValueOrDefault(instanceType); - descriptor = deferredValue.Value; - } - else - { - ref var deferredValue = ref _descriptors.GetOrAdd(instanceType, ValueFactory); - descriptor = deferredValue.Value; + deferredValue = _descriptors.GetOrAdd(instanceType, ValueFactory); } - var accessor = descriptor.GetOrCreateAccessor(memberName); + var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); value = accessor?.Invoke(instance); return accessor != null; } - private static ref DeferredValue ValueFactory(Type type, ref DeferredValue deferredValue) + private class CompiledObjectTypeDescriptor { - deferredValue.Factory = () => new CompiledObjectTypeDescriptor(type); - return ref deferredValue; - } - } + private readonly Type _type; - private class CompiledObjectTypeDescriptor : ObjectTypeDescriptor - { - public CompiledObjectTypeDescriptor(Type type) : base(type) - { - } + private static readonly Func<(string key, Type type), Func> ValueGetterFactory = + o => GetValueGetter(o.key, o.type); - protected override 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)); + private readonly LookupSlim>> + _accessors = + new LookupSlim>>(); - return (Func) Expression.Lambda(convert, instance).Compile(); - } + private static readonly Func>> ValueFactory = + (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); - var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - if (field != null) + public CompiledObjectTypeDescriptor(Type type) { - 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(); + _type = type; } - return null; - } - } - - private class RawObjectTypeDescriptor : ObjectTypeDescriptor - { - public RawObjectTypeDescriptor(Type type) : base(type) - { - } - - protected override 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) + public Func GetOrCreateAccessor(string name) { - return o => property.GetValue(o); + return _accessors.TryGetValue(name, out var deferredValue) + ? deferredValue.Value + : _accessors.GetOrAdd(name, ValueFactory, _type).Value; } - var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) - .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - if (field != null) + private static Func GetValueGetter(string name, Type type) { - return o => field.GetValue(o); - } + var property = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => + o.GetIndexParameters().Length == 0 && + string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - return null; - } - } + 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)); - private abstract class ObjectTypeDescriptor - { - private readonly Type _type; + return (Func) Expression.Lambda(convert, instance).Compile(); + } - private readonly RefLookup>> _accessors = - new RefLookup>>(); + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - protected ObjectTypeDescriptor(Type type) - { - _type = type; - } + 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)); - public Func GetOrCreateAccessor(string name) - { - if (_accessors.ContainsKey(name)) - { - ref var existing = ref _accessors.GetValueOrDefault(name); - return existing.Value; - } - - ref var deferredValue = ref _accessors.GetOrAdd(name, ValueFactory); - return deferredValue.Value; - } + return (Func) Expression.Lambda(convert, instance).Compile(); + } - private ref DeferredValue> ValueFactory(string name, ref DeferredValue> deferredValue) - { - deferredValue.Factory = () => GetValueGetter(name, _type); - return ref deferredValue; + return null; + } } - - protected abstract Func GetValueGetter(string name, Type type); } } } \ No newline at end of file diff --git a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs index 836e187e..3b5d4f45 100644 --- a/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs +++ b/source/Handlebars/MemberAliasProvider/CollectionMemberAliasProvider.cs @@ -16,7 +16,7 @@ public CollectionMemberAliasProvider(InternalHandlebarsConfiguration configurati public bool TryGetMemberByAlias(object instance, Type targetType, string memberAlias, out object value) { - var segment = ChainSegment.Create(memberAlias); + var segment = new ChainSegment(memberAlias); switch (instance) { case Array array: @@ -50,7 +50,7 @@ public bool TryGetMemberByAlias(object instance, Type targetType, string memberA return false; } - var properties = descriptor.GetProperties(enumerable); + var properties = descriptor.GetProperties(descriptor, enumerable); var property = properties.FirstOrDefault(o => { var name = o.ToString().ToLowerInvariant(); diff --git a/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs index 3448e189..ebdf9a87 100644 --- a/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/CollectionObjectDescriptor.cs @@ -23,9 +23,14 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { if (!_objectDescriptorProvider.TryGetDescriptor(type, out value)) return false; - value.ShouldEnumerate = true; - value.MemberAccessor = new MergedMemberAccessor(new EnumerableMemberAccessor(), value.MemberAccessor); - + var mergedMemberAccessor = new MergedMemberAccessor(new EnumerableMemberAccessor(), value.MemberAccessor); + value = new ObjectDescriptor( + value.DescribedType, + mergedMemberAccessor, + value.GetProperties, + true + ); + return true; } diff --git a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs index 665f1948..03d4d666 100644 --- a/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ContextObjectDescriptor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using HandlebarsDotNet.Compiler; using HandlebarsDotNet.MemberAccessors; @@ -8,12 +9,10 @@ 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) - { - MemberAccessor = new ContextMemberAccessor(), - GetProperties = o => Properties - }; + private static readonly ObjectDescriptor Descriptor = + new ObjectDescriptor(BindingContextType, new ContextMemberAccessor(), PropertiesDelegate); public bool CanHandleType(Type type) { diff --git a/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs index abb43b19..4e80caa3 100644 --- a/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/DictionaryObjectDescriptor.cs @@ -8,6 +8,18 @@ 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); @@ -15,22 +27,9 @@ public bool CanHandleType(Type type) public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - value = new ObjectDescriptor(type) - { - GetProperties = GetProperties, - MemberAccessor = new DictionaryMemberAccessor() - }; + value = new ObjectDescriptor(type, DictionaryMemberAccessor, GetProperties); return true; } - - private static IEnumerable GetProperties(object arg) - { - var dictionary = (IDictionary) arg; - foreach (var key in dictionary.Keys) - { - yield return key; - } - } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs index 7aa78a4f..484db2b4 100644 --- a/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/DynamicObjectDescriptor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Dynamic; using System.Linq.Expressions; using System.Reflection; @@ -8,6 +9,9 @@ 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); @@ -15,11 +19,7 @@ public bool CanHandleType(Type type) public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - value = new ObjectDescriptor(type) - { - GetProperties = o => ((IDynamicMetaObjectProvider) o).GetMetaObject(Expression.Constant(o)).GetDynamicMemberNames(), - MemberAccessor = new DynamicMemberAccessor() - }; + value = new ObjectDescriptor(type, DynamicMemberAccessor, GetProperties); return true; } diff --git a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs index 2a952eba..e6f6d5e5 100644 --- a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs @@ -10,51 +10,56 @@ namespace HandlebarsDotNet.ObjectDescriptors { internal sealed class GenericDictionaryObjectDescriptorProvider : IObjectDescriptorProvider { - private static readonly object[] EmptyArray = new object[0]; + private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(GenericDictionaryObjectDescriptorProvider) + .GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); - private readonly RefLookup> _typeCache = new RefLookup>(); + private readonly LookupSlim> _typeCache = new LookupSlim>(); public bool CanHandleType(Type type) { - ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); + var deferredValue = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); return deferredValue.Value != null; } public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - value = null; var interfaceType = _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; - if (interfaceType == null) return false; - - var descriptorCreator = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) - ?.MakeGenericMethod(interfaceType.GetGenericArguments()); + if (interfaceType == null) + { + value = ObjectDescriptor.Empty; + return false; + } - value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); - return value != null; + var descriptorCreator = CreateDescriptorMethodInfo + .MakeGenericMethod(interfaceType.GetGenericArguments()); + + value = (ObjectDescriptor) descriptorCreator.Invoke(null, Array.Empty()); + return true; } - - private static ref DeferredValue InterfaceTypeValueFactory(Type type, ref DeferredValue deferredValue) - { - deferredValue.Factory = () => + + 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 => + .FirstOrDefault(i => TypeDescriptor.GetConverter(i.GetGenericArguments()[0]).CanConvertFrom(typeof(string)) ); - }; - - return ref deferredValue; - } + }); private static ObjectDescriptor CreateDescriptor() { - return new ObjectDescriptor(typeof(IDictionary)) + IEnumerable Enumerate(IDictionary o) { - GetProperties = o => ((IDictionary) o).Keys.Cast(), - MemberAccessor = new DictionaryAccessor() - }; + 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 diff --git a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs index f4f3c76a..0734ae2e 100644 --- a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs @@ -8,8 +8,9 @@ namespace HandlebarsDotNet.ObjectDescriptors internal sealed class KeyValuePairObjectDescriptorProvider : IObjectDescriptorProvider { private static readonly string[] Properties = { "key", "value" }; - private static readonly object[] EmptyArray = new object[0]; - + 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<,>); @@ -18,20 +19,16 @@ public bool CanHandleType(Type type) public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { var genericArguments = type.GetGenericArguments(); - var descriptorCreator = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) - ?.MakeGenericMethod(genericArguments[0], genericArguments[1]); + var descriptorCreator = CreateDescriptorMethodInfo + .MakeGenericMethod(genericArguments[0], genericArguments[1]); - value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); - return value != null; + value = (ObjectDescriptor) descriptorCreator.Invoke(null, Array.Empty()); + return true; } private static ObjectDescriptor CreateDescriptor() { - return new ObjectDescriptor(typeof(KeyValuePair)) - { - GetProperties = o => Properties, - MemberAccessor = new KeyValuePairAccessor() - }; + return new ObjectDescriptor(typeof(KeyValuePair), new KeyValuePairAccessor(), GetProperties); } private class KeyValuePairAccessor : IMemberAccessor diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs index 36dbd65e..f9f21cf1 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -7,35 +7,95 @@ namespace HandlebarsDotNet.ObjectDescriptors /// /// Provides meta-information about /// - public class ObjectDescriptor + public class ObjectDescriptor : IEquatable { /// /// /// - /// - public ObjectDescriptor(Type describedType) + 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 bool ShouldEnumerate { get; set; } - + public readonly bool ShouldEnumerate; + + public readonly object[] Dependencies; + /// /// Returns type described by this instance of /// - public Type DescribedType { get; } - + public readonly Type DescribedType; + /// /// Factory enabling receiving properties of specific instance /// - public Func> GetProperties { get; set; } - + public readonly Func> GetProperties; + /// /// associated with the /// - public IMemberAccessor MemberAccessor { get; set; } + 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 index bf532966..41ef1855 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs @@ -7,8 +7,22 @@ namespace HandlebarsDotNet.ObjectDescriptors internal class ObjectDescriptorFactory : IObjectDescriptorProvider { private readonly IList _providers; - private readonly RefLookup> _descriptors = new RefLookup>(); - private readonly RefLookup> _supportedTypes = new RefLookup>(); + 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) { @@ -17,69 +31,23 @@ public ObjectDescriptorFactory(IList providers) public bool CanHandleType(Type type) { - if (_supportedTypes.ContainsKey(type)) - { - ref var contains = ref _supportedTypes.GetValueOrDefault(type); - return contains.Value; - } - - ref var deferredValue = ref _supportedTypes.GetOrAdd(type, SupportedTypesValueFactory); - return deferredValue.Value; + 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) { - value = null; - ObjectDescriptor descriptor; - if (_descriptors.ContainsKey(type)) - { - ref var existingDeferredValue = ref _descriptors.GetValueOrDefault(type); - descriptor = existingDeferredValue.Value; - } - else + if (_descriptorsCache.TryGetValue(type, out var deferredValue)) { - ref var deferredValue = ref _descriptors.GetOrAdd(type, DescriptorsValueFactory); - descriptor = deferredValue.Value; + value = deferredValue.Value; + return true; } - if (descriptor == null) return false; - - value = descriptor; - return true; - } - - private ref DeferredValue SupportedTypesValueFactory(Type type, ref DeferredValue deferredValue) - { - deferredValue.Factory = () => - { - for (var index = 0; index < _providers.Count; index++) - { - if (_providers[index].CanHandleType(type)) return true; - } - - return false; - }; - return ref deferredValue; - } - - private ref DeferredValue DescriptorsValueFactory(Type type, ref DeferredValue deferredValue) - { - deferredValue.Factory = () => - { - for (var index = 0; index < _providers.Count; index++) - { - var provider = _providers[index]; - if (!provider.CanHandleType(type)) continue; - if (provider.TryGetDescriptor(type, out var value)) - { - return value; - } - } - - return null; - }; - - return ref deferredValue; + 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 index f747deb5..9446f667 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -10,45 +10,40 @@ namespace HandlebarsDotNet.ObjectDescriptors internal class ObjectDescriptorProvider : IObjectDescriptorProvider { private readonly Type _dynamicMetaObjectProviderType = typeof(IDynamicMetaObjectProvider); - private readonly InternalHandlebarsConfiguration _configuration; - private readonly RefLookup> _membersCache = new RefLookup>(); + private readonly LookupSlim> _membersCache = new LookupSlim>(); + private readonly ReflectionMemberAccessor _reflectionMemberAccessor; public ObjectDescriptorProvider(InternalHandlebarsConfiguration configuration) { - _configuration = configuration; + _reflectionMemberAccessor = new ReflectionMemberAccessor(configuration); } public bool CanHandleType(Type type) { - return !_dynamicMetaObjectProviderType.IsAssignableFrom(type) - && type != typeof(string); + return !_dynamicMetaObjectProviderType.IsAssignableFrom(type) && type != typeof(string); } public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - ref var members = ref _membersCache.GetOrAdd(type, DescriptorValueFactory); - var membersValue = members.Value; - - value = new ObjectDescriptor(type) + value = new ObjectDescriptor(type, _reflectionMemberAccessor, (descriptor, o) => { - GetProperties = o => membersValue, - MemberAccessor = new ReflectionMemberAccessor(_configuration) - }; + var cache = (LookupSlim>) descriptor.Dependencies[0]; + return cache.GetOrAdd(descriptor.DescribedType, DescriptorValueFactory).Value; + }, dependencies: _membersCache); return true; } - - private static ref DeferredValue DescriptorValueFactory(Type type, ref DeferredValue deferredValue) - { - deferredValue.Factory = () => + + private static readonly Func> DescriptorValueFactory = + key => { - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(o => o.CanRead && o.GetIndexParameters().Length == 0); - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - return properties.Cast().Concat(fields).Select(o => o.Name).ToArray(); + return new DeferredValue(key, type => + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(o => o.CanRead && o.GetIndexParameters().Length == 0); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + return properties.Cast().Concat(fields).Select(o => o.Name).ToArray(); + }); }; - - return ref deferredValue; - } } } \ No newline at end of file diff --git a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs index ddffd56e..e2261306 100644 --- a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -9,51 +9,51 @@ namespace HandlebarsDotNet.ObjectDescriptors { internal sealed class StringDictionaryObjectDescriptorProvider : IObjectDescriptorProvider { - private static readonly object[] EmptyArray = new object[0]; + private static readonly object[] EmptyArray = Array.Empty(); + private static readonly MethodInfo CreateDescriptorMethodInfo = typeof(StringDictionaryObjectDescriptorProvider).GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static); - private readonly RefLookup> _typeCache = new RefLookup>(); + private readonly LookupSlim> _typeCache = new LookupSlim>(); public bool CanHandleType(Type type) { - ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); - return deferredValue.Value != null; + return _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value != null; } public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { - value = null; - ref var deferredValue = ref _typeCache.GetOrAdd(type, InterfaceTypeValueFactory); - var interfaceType = deferredValue.Value; + var interfaceType = _typeCache.TryGetValue(type, out var deferredValue) + ? deferredValue.Value + : _typeCache.GetOrAdd(type, InterfaceTypeValueFactory).Value; - if (interfaceType == null) return false; + if (interfaceType == null) + { + value = ObjectDescriptor.Empty; + return false; + } - var descriptorCreator = GetType().GetMethod(nameof(CreateDescriptor), BindingFlags.NonPublic | BindingFlags.Static) - ?.MakeGenericMethod(interfaceType.GetGenericArguments()[1]); + var descriptorCreator = CreateDescriptorMethodInfo + .MakeGenericMethod(interfaceType.GetGenericArguments()[1]); - value = (ObjectDescriptor) descriptorCreator?.Invoke(null, EmptyArray); - return value != null; + value = (ObjectDescriptor) descriptorCreator.Invoke(null, EmptyArray); + return true; } - private static ref DeferredValue InterfaceTypeValueFactory(Type type, ref DeferredValue deferredValue) - { - deferredValue.Factory = () => + 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)); - }; - - return ref deferredValue; - } + }); private static ObjectDescriptor CreateDescriptor() { - return new ObjectDescriptor(typeof(IDictionary)) - { - GetProperties = o => ((IDictionary) o).Keys, - MemberAccessor = new DictionaryAccessor() - }; + return new ObjectDescriptor( + typeof(IDictionary), + new DictionaryAccessor(), + (descriptor, o) => ((IDictionary) o).Keys + ); } private class DictionaryAccessor : IMemberAccessor diff --git a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs index 7f14d320..2e369d49 100644 --- a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs +++ b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs @@ -37,12 +37,14 @@ private bool TryGetContextVariable(object instance, ref ChainSegment segment, ou if (instance == null) return false; var instanceType = instance.GetType(); - if(_context.Configuration.ObjectDescriptorProvider.TryGetDescriptor(instanceType, out var descriptor)) + var descriptorProvider = _context.Configuration.ObjectDescriptorProvider; + if( + descriptorProvider.CanHandleType(instanceType) && + descriptorProvider.TryGetDescriptor(instanceType, out var descriptor) && + descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment.Value, out value) + ) { - if (descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment.Value, out value)) - { - return true; - } + return true; } return _context.ParentContext?.TryGetContextVariable(ref segment, out value) ?? false; From ec7a51c92e8edd454acf415425b06a56368b3e1b Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 14:10:07 -0700 Subject: [PATCH 45/53] Add Array.Empty polyfill, add Logger tests --- source/Handlebars.Benchmark/Execution.cs | 7 +- .../Handlebars.Benchmark.csproj | 2 +- source/Handlebars.Benchmark/Program.cs | 4 +- .../Handlebars.Extension.CompileFast.csproj | 2 +- .../Handlebars.Extension.Logger.csproj | 2 +- .../LoggerFeature.cs | 12 ++- .../Handlebars.Test/BasicIntegrationTests.cs | 89 +++++++++++++++++++ source/Handlebars.Test/Handlebars.Test.csproj | 3 +- .../Compiler/Structure/Path/PathResolver.cs | 2 +- .../Features/BuildInHelpersFeature.cs | 1 + source/Handlebars/Handlebars.csproj | 2 +- ...nericDictionaryObjectDescriptorProvider.cs | 3 +- .../KeyValuePairObjectDescriptorProvider.cs | 3 +- .../ObjectDescriptors/ObjectDescriptor.cs | 3 + ...tringDictionaryObjectDescriptorProvider.cs | 3 +- source/Handlebars/Polyfills/ArrayEx.cs | 16 ++++ 16 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 source/Handlebars/Polyfills/ArrayEx.cs diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index b02d08ce..d229b7f5 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -7,16 +7,15 @@ using HandlebarsDotNet; using HandlebarsDotNet.Extension.CompileFast; using Newtonsoft.Json.Linq; -using BenchmarkDotNet.Jobs; namespace Benchmark { public class Execution { - [Params(2)] + [Params(2, 5, 10)] public int N; - [Params("current", "current-cache"/*, "current-fast", "current-fast-cache", "1.10.1"*/)] + [Params("current", "current-cache", "current-fast", "current-fast-cache", "1.10.1")] public string Version; [Params("object", "dictionary")] @@ -237,7 +236,7 @@ JArray JsonLevel3Generator(int id1, int id2) return level; } } - + [Benchmark] public void Render() { diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index 56442923..432e922a 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + netcoreapp2.1 diff --git a/source/Handlebars.Benchmark/Program.cs b/source/Handlebars.Benchmark/Program.cs index 75b9c312..331237fb 100644 --- a/source/Handlebars.Benchmark/Program.cs +++ b/source/Handlebars.Benchmark/Program.cs @@ -7,11 +7,11 @@ namespace Benchmark { class Program { - static void Main(string[] args) + public static void Main(string[] args) { var manualConfig = DefaultConfig.Instance.WithArtifactsPath( $"./Benchmark-{FileVersionInfo.GetVersionInfo(typeof(Handlebars).Assembly.Location).FileVersion}" - ).With(BenchmarkLogicalGroupRule.ByMethod); + ).AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByMethod); BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, manualConfig); } diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj index 04d8ad89..cf43127c 100644 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj @@ -1,7 +1,7 @@ - netstandard1.3;netstandard2.0 + net452;netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.CompileFast 7 1.0.0 diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj index 3c4acb15..c66a9dbb 100644 --- a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj +++ b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj @@ -1,7 +1,7 @@ - netstandard1.3;netstandard2.0 + net452;netstandard1.3;netstandard2.0 HandlebarsDotNet.Extension.Logging latest enable diff --git a/source/Handlebars.Extension.Logger/LoggerFeature.cs b/source/Handlebars.Extension.Logger/LoggerFeature.cs index 15504231..22bc2291 100644 --- a/source/Handlebars.Extension.Logger/LoggerFeature.cs +++ b/source/Handlebars.Extension.Logger/LoggerFeature.cs @@ -37,9 +37,11 @@ private string LogHelper(dynamic context, object[] arguments) { var logLevel = LoggingLevel.Info; var formatter = _defaultFormatter; - + + var logArguments = arguments; if (arguments.Last() is IDictionary hash) { + logArguments = arguments.Take(arguments.Length - 1).ToArray(); if(hash.TryGetValue("level", out var level)) { if(Enum.TryParse(level.ToString(), true, out var hbLevel)) @@ -50,11 +52,15 @@ private string LogHelper(dynamic context, object[] arguments) if (hash.TryGetValue("format", out var format)) { - formatter = objects => string.Format(format.ToString(), arguments); + var formatString = format.ToString() + .Replace("[", "{") + .Replace("]", "}"); + + formatter = objects => string.Format(formatString, objects); } } - _logger(arguments, logLevel, formatter); + _logger(logArguments, logLevel, formatter); return string.Empty; } diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index 4ecea921..b509cb47 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -11,6 +11,8 @@ using Newtonsoft.Json.Linq; using HandlebarsDotNet.Features; using HandlebarsDotNet.Extension.CompileFast; +using HandlebarsDotNet.Extension.Logger; +using Xunit.Abstractions; namespace HandlebarsDotNet.Test { @@ -39,6 +41,13 @@ public class HandlebarsEnvGenerator : IEnumerable public class BasicIntegrationTests { + private readonly ITestOutputHelper _testOutputHelper; + + public BasicIntegrationTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Theory] [ClassData(typeof(HandlebarsEnvGenerator))] public void BasicPath(IHandlebars handlebars) @@ -1775,6 +1784,86 @@ private void CustomHelperResolverTest(IHandlebars handlebars) Assert.Equal(data.input.ToLower(), actual); } + + [Fact] + public void BasicLog() + { + var logs = new List(); + var handlebars = Handlebars.Create(); + handlebars.Configuration + .UseLogger((arguments, level, format) => logs.Add(format(arguments))); + + var source = "{{log name}}"; + var template = handlebars.Compile(source); + var data = new + { + name = "Handlebars.Net" + }; + _ = template(data); + var log = Assert.Single(logs); + Assert.Equal("Handlebars.Net", log); + } + + [Fact] + public void BasicLogWithLevel() + { + var logs = new List(); + var handlebars = Handlebars.Create(); + handlebars.Configuration + .UseLogger((arguments, level, format) => logs.Add($"Level: {level} -> {format(arguments)}")); + + var source = $"{{{{log name level='{LoggingLevel.Warn}'}}}}"; + var template = handlebars.Compile(source); + var data = new + { + name = "Handlebars.Net" + }; + _ = template(data); + var log = Assert.Single(logs); + Assert.Equal("Level: Warn -> Handlebars.Net", log); + } + + [Fact] + public void BasicLogWithFormat() + { + var logs = new List(); + var handlebars = Handlebars.Create(); + handlebars.Configuration + .UseLogger((arguments, level, format) => logs.Add(format(arguments))); + + var source = "{{log foo bar format='[0], [1]'}}"; + var template = handlebars.Compile(source); + var data = new + { + foo = "foo", + bar = "bar" + }; + _ = template(data); + + var log = Assert.Single(logs); + Assert.Equal("foo, bar", log); + } + + [Fact] + public void LogWithMultipleArguments() + { + var logs = new List(); + var handlebars = Handlebars.Create(); + handlebars.Configuration + .UseLogger((arguments, level, format) => logs.Add(format(arguments))); + + var source = "{{log foo bar}}"; + var template = handlebars.Compile(source); + var data = new + { + foo = "foo", + bar = "bar" + }; + _ = template(data); + + var log = Assert.Single(logs); + Assert.Equal("foo; bar", log); + } private class StringHelperResolver : IHelperResolver { diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index cdade6a6..53293144 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - netcoreapp2.1;netcoreapp3.1 + net461;netcoreapp2.1;netcoreapp3.1 700AF0B4-EA70-47B7-9F9D-17351E977B00 @@ -28,6 +28,7 @@ + diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index 4922fff0..29a734dc 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -28,7 +28,7 @@ public static PathInfo GetPathInfo(string path) { if (segment == "..") { - segments.Add(new PathSegment(segment, Array.Empty(), true, null)); + segments.Add(new PathSegment(segment, ArrayEx.Empty(), true, null)); continue; } diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index d27c8231..91f158ac 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -30,6 +30,7 @@ public void OnCompiling(ICompiledHandlebarsConfiguration configuration) configuration.BlockHelpers["*inline"] = Inline; configuration.ReturnHelpers["lookup"] = Lookup; + configuration.ReturnHelpers["log"] = (context, arguments) => string.Empty; } public void CompilationCompleted() diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 9e9c5695..7393b3e1 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -3,7 +3,7 @@ Handlebars portable - netstandard1.3;netstandard2.0 + net452;netstandard1.3;netstandard2.0 1.0.2 7 HandlebarsDotNet diff --git a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs index e6f6d5e5..461d29c2 100644 --- a/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/GenericDictionaryObjectDescriptorProvider.cs @@ -5,6 +5,7 @@ using System.Reflection; using HandlebarsDotNet.Collections; using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.ObjectDescriptors { @@ -33,7 +34,7 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) var descriptorCreator = CreateDescriptorMethodInfo .MakeGenericMethod(interfaceType.GetGenericArguments()); - value = (ObjectDescriptor) descriptorCreator.Invoke(null, Array.Empty()); + value = (ObjectDescriptor) descriptorCreator.Invoke(null, ArrayEx.Empty()); return true; } diff --git a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs index 0734ae2e..2ce84577 100644 --- a/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/KeyValuePairObjectDescriptorProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using HandlebarsDotNet.MemberAccessors; +using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.ObjectDescriptors { @@ -22,7 +23,7 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) var descriptorCreator = CreateDescriptorMethodInfo .MakeGenericMethod(genericArguments[0], genericArguments[1]); - value = (ObjectDescriptor) descriptorCreator.Invoke(null, Array.Empty()); + value = (ObjectDescriptor) descriptorCreator.Invoke(null, ArrayEx.Empty()); return true; } diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs index f9f21cf1..14a1cdae 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptor.cs @@ -48,6 +48,9 @@ private ObjectDescriptor(){ } /// public readonly bool ShouldEnumerate; + /// + /// Contains dependencies for delegate + /// public readonly object[] Dependencies; /// diff --git a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs index e2261306..b1f591c7 100644 --- a/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/StringDictionaryObjectDescriptorProvider.cs @@ -4,12 +4,13 @@ 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 = Array.Empty(); + 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>(); 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 From 2462b020ef93ef2134e4bc046594371f795f72ad Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 14:18:49 -0700 Subject: [PATCH 46/53] Remove unused method --- .../OperatingSystem.cs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 source/Handlebars.Extension.CompileFast/OperatingSystem.cs diff --git a/source/Handlebars.Extension.CompileFast/OperatingSystem.cs b/source/Handlebars.Extension.CompileFast/OperatingSystem.cs deleted file mode 100644 index 52015c07..00000000 --- a/source/Handlebars.Extension.CompileFast/OperatingSystem.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; - -namespace HandlebarsDotNet.Extension.CompileFast -{ - internal static class OperatingSystem - { -#if netFramework - public static bool IsWindows() => true; -#else - public static bool IsWindows() => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows); -#endif - } -} \ No newline at end of file From 726d4816f6dbdeee4db35ff87e70a869363371cf Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 15:49:15 -0700 Subject: [PATCH 47/53] Decrease code duplication --- .../ReflectionMemberAccessor.cs | 205 ++++++++---------- 1 file changed, 93 insertions(+), 112 deletions(-) diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs index bb57fc4c..254ecceb 100644 --- a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -15,8 +15,8 @@ public ReflectionMemberAccessor(InternalHandlebarsConfiguration configuration) { _configuration = configuration; _inner = configuration.CompileTimeConfiguration.UseAggressiveCaching - ? (IMemberAccessor) new CompiledReflectionMemberAccessor() - : (IMemberAccessor) new PlainReflectionMemberAccessor(); + ? (IMemberAccessor) new MemberAccessor() + : (IMemberAccessor) new MemberAccessor(); } public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) @@ -37,13 +37,29 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o return false; } - private class PlainReflectionMemberAccessor : IMemberAccessor + private abstract class ObjectTypeDescriptor { - private readonly LookupSlim> _descriptors = - new LookupSlim>(); + protected readonly LookupSlim>> + Accessors = new LookupSlim>>(); + + protected Type Type { get; } - private static readonly Func> ValueFactory = - key => new DeferredValue(key, type => new RawObjectTypeDescriptor(type)); + 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) { @@ -56,143 +72,108 @@ public bool TryGetValue(object instance, Type instanceType, string memberName, o value = accessor?.Invoke(instance); return accessor != null; } + } - private class RawObjectTypeDescriptor - { - private readonly Type _type; + private sealed class RawObjectTypeDescriptor : ObjectTypeDescriptor + { + private static readonly MethodInfo CreateGetDelegateMethodInfo = typeof(RawObjectTypeDescriptor) + .GetMethod(nameof(CreateGetDelegate), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly Func<(string key, Type type), Func> ValueGetterFactory = o => GetValueGetter(o.key, o.type); - private static readonly MethodInfo CreateGetDelegateMethodInfo = typeof(RawObjectTypeDescriptor) - .GetMethod(nameof(CreateGetDelegate), BindingFlags.Static | BindingFlags.NonPublic); + private static readonly Func>> + ValueFactory = (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); - private static readonly Func<(string key, Type type), Func> ValueGetterFactory = - o => GetValueGetter(o.key, o.type); + public RawObjectTypeDescriptor(Type type) : base(type) + { + } - private readonly LookupSlim>> - _accessors = new LookupSlim>>(); + public override Func GetOrCreateAccessor(string name) + { + return Accessors.TryGetValue(name, out var deferredValue) + ? deferredValue.Value + : Accessors.GetOrAdd(name, ValueFactory, Type).Value; + } - private static readonly Func>> ValueFactory = - (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); + 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)); - public RawObjectTypeDescriptor(Type type) + if (property != null) { - _type = type; + return (Func) CreateGetDelegateMethodInfo + .MakeGenericMethod(type, property.PropertyType) + .Invoke(null, new[] {property}); } - public Func GetOrCreateAccessor(string name) + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); + if (field != null) { - return _accessors.TryGetValue(name, out var deferredValue) - ? deferredValue.Value - : _accessors.GetOrAdd(name, ValueFactory, _type).Value; + return o => field.GetValue(o); } - 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; - } + return null; + } - private static Func CreateGetDelegate(PropertyInfo property) - { - var @delegate = (Func) property.GetMethod.CreateDelegate(typeof(Func)); - return o => (object) @delegate((T) o); - } + private static Func CreateGetDelegate(PropertyInfo property) + { + var @delegate = (Func) property.GetMethod.CreateDelegate(typeof(Func)); + return o => (object) @delegate((T) o); } } - private class CompiledReflectionMemberAccessor : IMemberAccessor + private sealed class CompiledObjectTypeDescriptor : ObjectTypeDescriptor { - private readonly LookupSlim> _descriptors = - new LookupSlim>(); + private static readonly Func<(string key, Type type), Func> ValueGetterFactory = + o => GetValueGetter(o.key, o.type); - private static readonly Func> ValueFactory = - key => new DeferredValue(key, type => new CompiledObjectTypeDescriptor(type)); + private static readonly Func>> + ValueFactory = (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); - public bool TryGetValue(object instance, Type instanceType, string memberName, out object value) + public CompiledObjectTypeDescriptor(Type type) : base(type) { - 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 class CompiledObjectTypeDescriptor + public override Func GetOrCreateAccessor(string name) { - private readonly Type _type; - - private static readonly Func<(string key, Type type), Func> ValueGetterFactory = - o => GetValueGetter(o.key, o.type); - - private readonly LookupSlim>> - _accessors = - new LookupSlim>>(); + return Accessors.TryGetValue(name, out var deferredValue) + ? deferredValue.Value + : Accessors.GetOrAdd(name, ValueFactory, Type).Value; + } - private static readonly Func>> ValueFactory = - (key, state) => new DeferredValue<(string key, Type type), Func>((key, state), ValueGetterFactory); + 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)); - public CompiledObjectTypeDescriptor(Type type) + if (property != null) { - _type = type; - } + var instance = Expression.Parameter(typeof(object), "i"); + var memberExpression = Expression.Property(Expression.Convert(instance, type), name); + var convert = Expression.TypeAs(memberExpression, typeof(object)); - public Func GetOrCreateAccessor(string name) - { - return _accessors.TryGetValue(name, out var deferredValue) - ? deferredValue.Value - : _accessors.GetOrAdd(name, ValueFactory, _type).Value; + return (Func) Expression.Lambda(convert, instance).Compile(); } - 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)); + var field = type.GetFields(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(o => string.Equals(o.Name, name, StringComparison.OrdinalIgnoreCase)); - 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(); - } + 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 null; + return (Func) Expression.Lambda(convert, instance).Compile(); } + + return null; } } } From 55965c45177d51ee4e59974db8fd41ed5bebcafe Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 16:05:03 -0700 Subject: [PATCH 48/53] Add test for the issue rexm/Handlebars.Net/issues/351 --- source/Handlebars.Test/IssueTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 3bad21f2..e6a39fb2 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -1,3 +1,4 @@ +using System.Dynamic; using Xunit; namespace HandlebarsDotNet.Test @@ -17,5 +18,18 @@ public void ValueVariableShouldNotBeAccessibleFromContext() 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); + } } } \ No newline at end of file From b182b8f6dd9dbb9a6e36c9d284ee02f124d74bd3 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 16:23:31 -0700 Subject: [PATCH 49/53] Update Readme --- README.md | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index cfba0ea6..c6dd2b36 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,6 @@ -| Package | Version | -|-------------------|---| -| Handlebars.CSharp | [![Nuget](https://img.shields.io/nuget/v/handlebars.csharp)](https://www.nuget.org/packages/handlebars.csharp/) | -| Handlebars.Extension.CompileFast | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.CompileFast)](https://www.nuget.org/packages/Handlebars.Extension.CompileFast/) | -| Handlebars.Extension.Logging | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.Logging)](https://www.nuget.org/packages/Handlebars.Extension.Logging/) | - -![Build](https://github.com/zjklee/Handlebars.CSharp/workflows/CI/badge.svg) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) -[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) -## - -_This is a fork of [rexm/Handlebars.Net](https://github.com/rexm/Handlebars.Net) developed by @rexm. Unfortunately project had no activity for a while. I'd be glad to back-merge all the changes back to original repo if I'd have a chance. Meanwhile I'd try to support the fork._ +Handlebars.Net [![Build Status](https://travis-ci.org/rexm/Handlebars.Net.svg?branch=master)](https://travis-ci.org/rexm/Handlebars.Net) [![Nuget](https://img.shields.io/nuget/v/Handlebars.Net)](https://www.nuget.org/packages/Handlebars.Net/) +============== +**[Call for Input on v2](https://github.com/rexm/Handlebars.Net/issues/294)** Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET application. @@ -19,19 +8,11 @@ Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET Check out the [handlebars.js documentation](http://handlebarsjs.com) for how to write Handlebars templates. -handlebars.csharp doesn't use a scripting engine to run a Javascript library - it **compiles Handlebars templates directly to IL bytecode**. It also mimics the JS library's API as closely as possible. +Handlebars.Net doesn't use a scripting engine to run a Javascript library - it **compiles Handlebars templates directly to IL bytecode**. It also mimics the JS library's API as closely as possible. ## Install -```cmd -dotnet add package Handlebars.CSharp -``` -### Extensions -```cmd -dotnet add package Handlebars.Extension.CompileFast -``` -```cmd -dotnet add package Handlebars.Extension.Logging -``` + + dotnet add package Handlebars.Net ## Usage @@ -187,7 +168,7 @@ For mo retails see [Performance measurements](Performance.md) ## Future roadmap -TBD +**[Call for Input on v2](https://github.com/rexm/Handlebars.Net/issues/294)** ## Contributing From c141f3fc12fdeb68d8cb12f78d59fa223b7841c2 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 16:51:01 -0700 Subject: [PATCH 50/53] Make changes compatible with main repo --- source/Handlebars.Benchmark/Compilation.cs | 7 +- source/Handlebars.Benchmark/Execution.cs | 10 +-- .../Handlebars.Benchmark.csproj | 6 +- source/Handlebars.Code.sln | 79 ----------------- .../CompileFastExtensions.cs | 22 ----- .../FastCompilerFeature.cs | 26 ------ .../FastExpressionCompiler.cs | 63 -------------- .../Handlebars.Extension.CompileFast.csproj | 59 ------------- .../Handlebars.Extension.Logger.csproj | 47 ---------- .../LoggerFeature.cs | 68 --------------- .../LoggerFeatureExtensions.cs | 23 ----- .../LoggerFeatureFactory.cs | 19 ----- .../LoggingLevel.cs | 28 ------ .../netstandard2.0/LoggerFeatureExtensions.cs | 25 ------ .../netstandard2.0/LoggerFeatureFactory.cs | 34 -------- .../Handlebars.Test/BasicIntegrationTests.cs | 85 ------------------- source/Handlebars.Test/Handlebars.Test.csproj | 36 +++----- source/Handlebars.sln | 22 ++--- source/Handlebars/Handlebars.csproj | 49 +++-------- 19 files changed, 32 insertions(+), 676 deletions(-) delete mode 100644 source/Handlebars.Code.sln delete mode 100644 source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs delete mode 100644 source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs delete mode 100644 source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs delete mode 100644 source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj delete mode 100644 source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj delete mode 100644 source/Handlebars.Extension.Logger/LoggerFeature.cs delete mode 100644 source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs delete mode 100644 source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs delete mode 100644 source/Handlebars.Extension.Logger/LoggingLevel.cs delete mode 100644 source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs delete mode 100644 source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs diff --git a/source/Handlebars.Benchmark/Compilation.cs b/source/Handlebars.Benchmark/Compilation.cs index 6de4ff16..30ab9bb9 100644 --- a/source/Handlebars.Benchmark/Compilation.cs +++ b/source/Handlebars.Benchmark/Compilation.cs @@ -4,7 +4,6 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using HandlebarsDotNet; -using HandlebarsDotNet.Extension.CompileFast; namespace Benchmark { @@ -13,7 +12,7 @@ public class Compilation { private IHandlebars _handlebars; - [Params("current", "current-fast", "1.10.1")] + [Params("current", "1.10.1")] public string Version; private Func> _compileMethod; @@ -36,10 +35,6 @@ public void Setup() else { _handlebars = Handlebars.Create(); - if (Version.Contains("fast")) - { - _handlebars.Configuration.UseCompileFast(); - } _compileMethod = _handlebars.Compile; } diff --git a/source/Handlebars.Benchmark/Execution.cs b/source/Handlebars.Benchmark/Execution.cs index d229b7f5..ad9b1608 100644 --- a/source/Handlebars.Benchmark/Execution.cs +++ b/source/Handlebars.Benchmark/Execution.cs @@ -5,7 +5,6 @@ using System.Reflection; using BenchmarkDotNet.Attributes; using HandlebarsDotNet; -using HandlebarsDotNet.Extension.CompileFast; using Newtonsoft.Json.Linq; namespace Benchmark @@ -15,7 +14,7 @@ public class Execution [Params(2, 5, 10)] public int N; - [Params("current", "current-cache", "current-fast", "current-fast-cache", "1.10.1")] + [Params("current", "current-cache", "1.10.1")] public string Version; [Params("object", "dictionary")] @@ -89,12 +88,7 @@ public void Setup() else { Handlebars.Configuration.CompileTimeConfiguration.UseAggressiveCaching = Version.Contains("cache"); - - if (Version.Contains("fast")) - { - Handlebars.Configuration.UseCompileFast(); - } - + using (var reader = new StringReader(template)) { _templates = new[] diff --git a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj index 432e922a..65991f5f 100644 --- a/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj +++ b/source/Handlebars.Benchmark/Handlebars.Benchmark.csproj @@ -6,13 +6,11 @@ - - + + - - diff --git a/source/Handlebars.Code.sln b/source/Handlebars.Code.sln deleted file mode 100644 index f0d513a6..00000000 --- a/source/Handlebars.Code.sln +++ /dev/null @@ -1,79 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars", "Handlebars\Handlebars.csproj", "{A09CFF95-B671-48FE-96A8-D3CBDC110B75}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E9AC0BCD-C060-4634-BBBB-636167C809B4}" - ProjectSection(SolutionItems) = preProject - ..\README.md = ..\README.md - ..\Performance.md = ..\Performance.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.Logger", "Handlebars.Extension.Logger\Handlebars.Extension.Logger.csproj", "{EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A09CFF95-B671-48FE-96A8-D3CBDC110B75}.Release|Any CPU.Build.0 = Release|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.Build.0 = Release|Any CPU - {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.TextStylePolicy = $1 - $1.inheritsSet = VisualStudio - $1.inheritsScope = text/plain - $1.scope = text/x-csharp - $0.CSharpFormattingPolicy = $2 - $2.IndentSwitchBody = True - $2.IndentBlocksInsideExpressions = True - $2.AnonymousMethodBraceStyle = NextLine - $2.PropertyBraceStyle = NextLine - $2.PropertyGetBraceStyle = NextLine - $2.PropertySetBraceStyle = NextLine - $2.EventBraceStyle = NextLine - $2.EventAddBraceStyle = NextLine - $2.EventRemoveBraceStyle = NextLine - $2.StatementBraceStyle = NextLine - $2.ElseNewLinePlacement = NewLine - $2.CatchNewLinePlacement = NewLine - $2.FinallyNewLinePlacement = NewLine - $2.WhileNewLinePlacement = DoNotCare - $2.ArrayInitializerWrapping = DoNotChange - $2.ArrayInitializerBraceStyle = NextLine - $2.BeforeMethodDeclarationParentheses = False - $2.BeforeMethodCallParentheses = False - $2.BeforeConstructorDeclarationParentheses = False - $2.NewLineBeforeConstructorInitializerColon = NewLine - $2.NewLineAfterConstructorInitializerColon = SameLine - $2.BeforeDelegateDeclarationParentheses = False - $2.NewParentheses = False - $2.SpacesBeforeBrackets = False - $2.inheritsSet = Mono - $2.inheritsScope = text/x-csharp - $2.scope = text/x-csharp - $0.DotNetNamingPolicy = $3 - $3.DirectoryNamespaceAssociation = None - $3.ResourceNamePolicy = FileFormatDefault - version = 1.0.0 - EndGlobalSection -EndGlobal diff --git a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs b/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs deleted file mode 100644 index 0d3d5b3b..00000000 --- a/source/Handlebars.Extension.CompileFast/CompileFastExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace HandlebarsDotNet.Extension.CompileFast -{ - /// - /// - /// - public static class CompileFastExtensions - { - /// - /// Changes to the one using FastExpressionCompiler - /// - /// - /// - public static HandlebarsConfiguration UseCompileFast(this HandlebarsConfiguration configuration) - { - var compileTimeConfiguration = configuration.CompileTimeConfiguration; - - compileTimeConfiguration.Features.Add(new FastCompilerFeatureFactory()); - - return configuration; - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs b/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs deleted file mode 100644 index 081bd0f5..00000000 --- a/source/Handlebars.Extension.CompileFast/FastCompilerFeature.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using HandlebarsDotNet.Features; - -namespace HandlebarsDotNet.Extension.CompileFast -{ - internal class FastCompilerFeatureFactory : IFeatureFactory - { - public IFeature CreateFeature() - { - return new FastCompilerFeature(); - } - } - - internal class FastCompilerFeature : IFeature - { - public void OnCompiling(ICompiledHandlebarsConfiguration configuration) - { - var templateFeature = configuration.Features.OfType().SingleOrDefault(); - configuration.ExpressionCompiler = new FastExpressionCompiler(configuration, templateFeature); - } - - public void CompilationCompleted() - { - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs deleted file mode 100644 index 0863c98c..00000000 --- a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Expressions.Shortcuts; -using FastExpressionCompiler; -using HandlebarsDotNet.Features; - -namespace HandlebarsDotNet.Extension.CompileFast -{ - internal class FastExpressionCompiler : IExpressionCompiler - { - private readonly ClosureFeature _closureFeature; - private readonly TemplateClosure _templateClosure; - private readonly ParameterExpression _closure; - private readonly ICollection _expressionMiddleware; - - public FastExpressionCompiler(ICompiledHandlebarsConfiguration configuration, ClosureFeature closureFeature) - { - _closureFeature = closureFeature; - _templateClosure = closureFeature?.TemplateClosure; - _closure = closureFeature?.Closure; - _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.CompileFast(); - } - - expression = (Expression) _closureFeature.ExpressionMiddleware.Invoke(expression); - - var parameters = new[] { _closure }.Concat(expression.Parameters).ToArray(); - var lambda = Expression.Lambda(expression.Body, parameters); - var compiledDelegateType = Expression.GetDelegateType(parameters.Select(o => o.Type).Concat(new[] {lambda.ReturnType}).ToArray()); - - var method = typeof(FastExpressionCompiler) - .GetMethod(nameof(CompileGeneric), BindingFlags.Static | BindingFlags.NonPublic) - ?.MakeGenericMethod(compiledDelegateType); - - var compiledLambda = method?.Invoke(null, new object[] { lambda }) ?? throw new InvalidOperationException("lambda cannot be compiled"); - - var outerParameters = expression.Parameters.Select(o => Expression.Parameter(o.Type, o.Name)).ToArray(); - - var store = (Expression) Expression.Field(Expression.Constant(_templateClosure), nameof(TemplateClosure.Store)); - var outerLambda = Expression.Lambda( - Expression.Invoke(Expression.Constant(compiledLambda), new[] {store}.Concat(outerParameters)), - outerParameters); - - return outerLambda.CompileFast(); - } - - private static T CompileGeneric(LambdaExpression expression) where T : class - { - return expression.CompileFast(); - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj b/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj deleted file mode 100644 index cf43127c..00000000 --- a/source/Handlebars.Extension.CompileFast/Handlebars.Extension.CompileFast.csproj +++ /dev/null @@ -1,59 +0,0 @@ - - - - net452;netstandard1.3;netstandard2.0 - HandlebarsDotNet.Extension.CompileFast - 7 - 1.0.0 - 68ACA1C9-CAA6-40A9-B7B4-B619663E5E04 - - - - Oleh Formaniuk - Copyright © 2020 Oleh Formaniuk - FastExpressionCompiler adapter for handlebars.csharp - hbnet-extension-icon.png - Handlebars.Extension.CompileFast - https://opensource.org/licenses/mit - https://github.com/zjklee/handlebars.csharp - handlebars;mustache;templating;engine;compiler - git - https://github.com/zjklee/handlebars.csharp - true - - - - $(DefineConstants);netFramework - - - - - false - true - . - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj deleted file mode 100644 index c66a9dbb..00000000 --- a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - - net452;netstandard1.3;netstandard2.0 - HandlebarsDotNet.Extension.Logging - latest - enable - 1.0.0 - EBBB4CFE-B576-4E7F-AD04-2E1DDB7C3B3F - - - - Oleh Formaniuk - Copyright © 2020 Oleh Formaniuk - Microsoft.Extensions.Logging adapter for handlebars.csharp - hbnet-extension-icon.png - Handlebars.Extension.Logging - https://opensource.org/licenses/mit - https://github.com/zjklee/handlebars.csharp - handlebars;mustache;templating;engine;compiler - git - https://github.com/zjklee/handlebars.csharp - true - - - - - false - true - . - - - - - - - - - - - - - - - - - diff --git a/source/Handlebars.Extension.Logger/LoggerFeature.cs b/source/Handlebars.Extension.Logger/LoggerFeature.cs deleted file mode 100644 index 22bc2291..00000000 --- a/source/Handlebars.Extension.Logger/LoggerFeature.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HandlebarsDotNet.Features; - -namespace HandlebarsDotNet.Extension.Logger -{ - /// - /// - /// - /// - /// - /// - public delegate void Log(object[] arguments, LoggingLevel level, Func format); - - internal class LoggerFeature : IFeature - { - private readonly Log _logger; - - private readonly Func _defaultFormatter = objects => string.Join("; ", objects); - - public LoggerFeature(Log logger) - { - _logger = logger; - } - - public void OnCompiling(ICompiledHandlebarsConfiguration configuration) - { - configuration.ReturnHelpers["log"] = LogHelper; - } - - public void CompilationCompleted() - { - } - - private string LogHelper(dynamic context, object[] arguments) - { - var logLevel = LoggingLevel.Info; - var formatter = _defaultFormatter; - - var logArguments = arguments; - if (arguments.Last() is IDictionary hash) - { - logArguments = arguments.Take(arguments.Length - 1).ToArray(); - if(hash.TryGetValue("level", out var level)) - { - if(Enum.TryParse(level.ToString(), true, out var hbLevel)) - { - logLevel = hbLevel; - } - } - - if (hash.TryGetValue("format", out var format)) - { - var formatString = format.ToString() - .Replace("[", "{") - .Replace("]", "}"); - - formatter = objects => string.Format(formatString, objects); - } - } - - _logger(logArguments, logLevel, formatter); - - return string.Empty; - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs b/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs deleted file mode 100644 index 6b8bc289..00000000 --- a/source/Handlebars.Extension.Logger/LoggerFeatureExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace HandlebarsDotNet.Extension.Logger -{ - /// - /// - /// - public static partial class LoggerFeatureExtensions - { - /// - /// Adds log helper that uses provided - /// - /// - /// - /// - public static HandlebarsConfiguration UseLogger(this HandlebarsConfiguration configuration, Log logger) - { - var compileTimeConfiguration = configuration.CompileTimeConfiguration; - - compileTimeConfiguration.Features.Add(new LoggerFeatureFactory(logger)); - - return configuration; - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs b/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs deleted file mode 100644 index 0a9d251a..00000000 --- a/source/Handlebars.Extension.Logger/LoggerFeatureFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using HandlebarsDotNet.Features; - -namespace HandlebarsDotNet.Extension.Logger -{ - internal partial class LoggerFeatureFactory : IFeatureFactory - { - private readonly Log _logger; - - public LoggerFeatureFactory(Log logger) - { - _logger = logger; - } - - public IFeature CreateFeature() - { - return new LoggerFeature(_logger); - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/LoggingLevel.cs b/source/Handlebars.Extension.Logger/LoggingLevel.cs deleted file mode 100644 index 0808bf31..00000000 --- a/source/Handlebars.Extension.Logger/LoggingLevel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace HandlebarsDotNet.Extension.Logger -{ - /// - /// Handlebarsjs like logging levels - /// - public enum LoggingLevel - { - /// - /// - /// - Debug, - - /// - /// - /// - Info, - - /// - /// - /// - Warn, - - /// - /// - /// - Error - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs deleted file mode 100644 index 800d5b1f..00000000 --- a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace HandlebarsDotNet.Extension.Logger -{ - /// - /// - /// - public static partial class LoggerFeatureExtensions - { - /// - /// Adds log helper that uses provided - /// - /// - /// - /// - public static HandlebarsConfiguration UseLogger(this HandlebarsConfiguration configuration, ILoggerFactory loggerFactory) - { - var compileTimeConfiguration = configuration.CompileTimeConfiguration; - - compileTimeConfiguration.Features.Add(new LoggerFeatureFactory(loggerFactory)); - - return configuration; - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs b/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs deleted file mode 100644 index e0118ad1..00000000 --- a/source/Handlebars.Extension.Logger/netstandard2.0/LoggerFeatureFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using HandlebarsDotNet.Features; -using Microsoft.Extensions.Logging; - -namespace HandlebarsDotNet.Extension.Logger -{ - internal partial class LoggerFeatureFactory : IFeatureFactory - { - public LoggerFeatureFactory(ILoggerFactory factory) - { - _logger = CreateLogger(factory); - } - - private static Log CreateLogger(ILoggerFactory factory) - { - static LogLevel LogLevelMapper(LoggingLevel level) => - level switch - { - LoggingLevel.Debug => LogLevel.Debug, - LoggingLevel.Info => LogLevel.Information, - LoggingLevel.Warn => LogLevel.Warning, - LoggingLevel.Error => LogLevel.Error, - _ => LogLevel.Information - }; - - return (arguments, level, format) => - { - var logLevel = LogLevelMapper(level); - - factory.CreateLogger("Handlebars") - .Log(logLevel, 0, arguments, null, (objects, exception) => format(arguments)); - }; - } - } -} \ No newline at end of file diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index b509cb47..14f50ff1 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -10,8 +10,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using HandlebarsDotNet.Features; -using HandlebarsDotNet.Extension.CompileFast; -using HandlebarsDotNet.Extension.Logger; using Xunit.Abstractions; namespace HandlebarsDotNet.Test @@ -22,9 +20,6 @@ public class HandlebarsEnvGenerator : IEnumerable { Handlebars.Create(), Handlebars.Create(new HandlebarsConfiguration{ CompileTimeConfiguration = { UseAggressiveCaching = false}}), -#if compileFast - Handlebars.Create(new HandlebarsConfiguration().UseCompileFast()), -#endif Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types => { types.Add(typeof(Dictionary)); @@ -1784,86 +1779,6 @@ private void CustomHelperResolverTest(IHandlebars handlebars) Assert.Equal(data.input.ToLower(), actual); } - - [Fact] - public void BasicLog() - { - var logs = new List(); - var handlebars = Handlebars.Create(); - handlebars.Configuration - .UseLogger((arguments, level, format) => logs.Add(format(arguments))); - - var source = "{{log name}}"; - var template = handlebars.Compile(source); - var data = new - { - name = "Handlebars.Net" - }; - _ = template(data); - var log = Assert.Single(logs); - Assert.Equal("Handlebars.Net", log); - } - - [Fact] - public void BasicLogWithLevel() - { - var logs = new List(); - var handlebars = Handlebars.Create(); - handlebars.Configuration - .UseLogger((arguments, level, format) => logs.Add($"Level: {level} -> {format(arguments)}")); - - var source = $"{{{{log name level='{LoggingLevel.Warn}'}}}}"; - var template = handlebars.Compile(source); - var data = new - { - name = "Handlebars.Net" - }; - _ = template(data); - var log = Assert.Single(logs); - Assert.Equal("Level: Warn -> Handlebars.Net", log); - } - - [Fact] - public void BasicLogWithFormat() - { - var logs = new List(); - var handlebars = Handlebars.Create(); - handlebars.Configuration - .UseLogger((arguments, level, format) => logs.Add(format(arguments))); - - var source = "{{log foo bar format='[0], [1]'}}"; - var template = handlebars.Compile(source); - var data = new - { - foo = "foo", - bar = "bar" - }; - _ = template(data); - - var log = Assert.Single(logs); - Assert.Equal("foo, bar", log); - } - - [Fact] - public void LogWithMultipleArguments() - { - var logs = new List(); - var handlebars = Handlebars.Create(); - handlebars.Configuration - .UseLogger((arguments, level, format) => logs.Add(format(arguments))); - - var source = "{{log foo bar}}"; - var template = handlebars.Compile(source); - var data = new - { - foo = "foo", - bar = "bar" - }; - _ = template(data); - - var log = Assert.Single(logs); - Assert.Equal("foo; bar", log); - } private class StringHelperResolver : IHelperResolver { diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index 53293144..dcc34293 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - net461;netcoreapp2.1;netcoreapp3.1 + net461;netcoreapp1.1;netcoreapp2.0 700AF0B4-EA70-47B7-9F9D-17351E977B00 @@ -14,29 +14,18 @@ $(DefineConstants);netFramework - $(DefineConstants);compileFast $(DefineConstants);netstandard - - $(DefineConstants);compileFast - $(DefineConstants);netstandard - - - $(DefineConstants);compileFast + $(DefineConstants);netstandard - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -45,25 +34,22 @@ - - + - - - - - - - - + - + + + + + + diff --git a/source/Handlebars.sln b/source/Handlebars.sln index 7d915844..6030ac0b 100644 --- a/source/Handlebars.sln +++ b/source/Handlebars.sln @@ -13,11 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\Performance.md = ..\Performance.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{E880C14C-96EE-4A1E-98BD-6348AB529090}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.CompileFast", "Handlebars.Extension.CompileFast\Handlebars.Extension.CompileFast.csproj", "{725422F0-556E-48B1-8E3A-F26881FFACE2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Extension.Logger", "Handlebars.Extension.Logger\Handlebars.Extension.Logger.csproj", "{68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Handlebars.Benchmark", "Handlebars.Benchmark\Handlebars.Benchmark.csproj", "{9DEAB317-76CD-4C92-8622-935DCB9B606D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,18 +29,10 @@ Global {2BD48FB6-C852-4141-B734-12E501B1D761}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BD48FB6-C852-4141-B734-12E501B1D761}.Release|Any CPU.Build.0 = Release|Any CPU - {E880C14C-96EE-4A1E-98BD-6348AB529090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E880C14C-96EE-4A1E-98BD-6348AB529090}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E880C14C-96EE-4A1E-98BD-6348AB529090}.Release|Any CPU.Build.0 = Release|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {725422F0-556E-48B1-8E3A-F26881FFACE2}.Release|Any CPU.Build.0 = Release|Any CPU - {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68ACA1C9-CAA6-40A9-B7B4-B619663E5E04}.Release|Any CPU.Build.0 = Release|Any CPU + {9DEAB317-76CD-4C92-8622-935DCB9B606D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DEAB317-76CD-4C92-8622-935DCB9B606D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DEAB317-76CD-4C92-8622-935DCB9B606D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DEAB317-76CD-4C92-8622-935DCB9B606D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index 7393b3e1..40381b3a 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -3,16 +3,13 @@ Handlebars portable + true net452;netstandard1.3;netstandard2.0 - 1.0.2 + 2.0.0 7 HandlebarsDotNet + 7F6A54F4-161A-46EA-9A27-DF834B7810DB - - - true - - false @@ -23,43 +20,19 @@ - Rex Morgan, Oleh Formaniuk - Oleh Formaniuk - Copyright © 2020 Oleh Formaniuk + Rex Morgan + Copyright © 2014-2019 Rex Morgan Blistering-fast Handlebars.js templates in your .NET application. - hbnet-icon.png - Handlebars.CSharp + https://raw.githubusercontent.com/rexm/Handlebars.Net/master/hbnet-icon.png + Handlebars.Net https://opensource.org/licenses/mit - https://github.com/zjklee/handlebars.csharp + https://github.com/rexm/Handlebars.Net handlebars;mustache;templating;engine;compiler git - https://github.com/zjklee/handlebars.csharp + https://github.com/rexm/Handlebars.Net true - - - false - true - . - - - - - - lib\netstandard2.0\ - false - - - lib\netstandard1.3\ - false - - - lib\net452\ - false - - - @@ -67,14 +40,14 @@ - + - + From e0d71ecbc0360bed6575806dab52beacd2117a11 Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 16:56:01 -0700 Subject: [PATCH 51/53] Add `Extensions` information to Readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c6dd2b36..f37c8f31 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,12 @@ The animal, Fido, is a dog. The animal, Chewy, is not a dog. */ ``` +## Extensions +| Package | Description | Version | +|-------------------|---|---| +| Handlebars.Extension.CompileFast | Uses [FastExpressionCompiler](https://github.com/dadhi/FastExpressionCompiler) to compile templates | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.CompileFast)](https://www.nuget.org/packages/Handlebars.Extension.CompileFast/) | +| Handlebars.Extension.Logging | Adds logging infrastructure integration via `log` helper | [![Nuget](https://img.shields.io/nuget/v/Handlebars.Extension.Logging)](https://www.nuget.org/packages/Handlebars.Extension.Logging/) | + ## Performance ### Compilation From 756f136f8d753fb630279d5c070cca3b98a8693b Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sat, 30 May 2020 17:24:44 -0700 Subject: [PATCH 52/53] Enable SonarCloud integration --- .github/workflows/ci.yml | 59 +++++-------------- .github/workflows/pull_request.yml | 24 ++++---- .github/workflows/release.yml | 30 ---------- README.md | 6 +- source/Handlebars.Test/Handlebars.Test.csproj | 25 ++++---- 5 files changed, 43 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8d38916..b58329b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,16 +12,16 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Build netstandard2.0 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f netstandard2.0 - name: Build netstandard1.3 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f netstandard1.3 - name: Build net452 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f net452 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f net452 test-windows: @@ -32,10 +32,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx test-linux: @@ -46,10 +46,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx test-macos: @@ -60,10 +60,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx sonar-ci: @@ -74,12 +74,12 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Sonarscanner for dotnet uses: Secbyte/dotnet-sonarscanner@v2.2 with: - buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings + buildCommand: dotnet build source/Handlebars/Handlebars.csproj -f netstandard2.0 + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee @@ -90,36 +90,5 @@ jobs: /d:sonar.cs.vstest.reportsPaths="**/*.trx" env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_TOKEN: 0a2d91d681b618b29a293e3d1e7e1e9ea82710a2 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - publish: - needs: [build,test-windows,test-linux,test-macos,sonar-ci] - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.1.100 - - name: Fetch - run: git fetch --depth 500 - - name: Determine version - run: echo "::set-env name=VERSION::$(git describe --tags --abbrev=0)" - - - name: Build netstandard2.0 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 - - name: Build netstandard1.3 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 - - name: Build net452 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f net452 - - - name: publish - working-directory: ./source - run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ env.VERSION }}.${{ github.run_number }}-beta && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json -n true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 872ca8da..7ab199a9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,16 +12,16 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Build netstandard2.0 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f netstandard2.0 - name: Build netstandard1.3 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f netstandard1.3 - name: Build net452 working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f net452 + run: dotnet build Handlebars/Handlebars.csproj -c Release -f net452 test-windows: runs-on: windows-latest @@ -31,10 +31,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx test-linux: runs-on: ubuntu-latest @@ -44,10 +44,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx test-macos: runs-on: macos-latest @@ -57,10 +57,10 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.102 + dotnet-version: 2.1.806 - name: Test working-directory: ./source - run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx + run: dotnet test ./Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx sonar-pr: runs-on: ubuntu-latest @@ -70,8 +70,8 @@ jobs: - name: Sonarscanner for dotnet uses: Secbyte/dotnet-sonarscanner@v2.2 with: - buildCommand: dotnet build source/Handlebars.Code.sln -f netstandard2.0 -c Release - testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp3.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings + buildCommand: dotnet build source/Handlebars/Handlebars.csproj -f netstandard2.0 + testCommand: dotnet test source/Handlebars.Test/Handlebars.Test.csproj -f netcoreapp2.1 --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings projectKey: zjklee_handlebars.csharp projectName: handlebars.csharp sonarOrganisation: zjklee diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a0fd0c23..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Release - -on: - release: - types: [published] - -jobs: - publish: - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.1.100 - - - name: Build netstandard2.0 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard2.0 - - name: Build netstandard1.3 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f netstandard1.3 - - name: Build net452 - working-directory: ./source - run: dotnet build Handlebars.Code.sln -c Release -f net452 - - - name: publish - working-directory: ./source - run: dotnet pack Handlebars.Code.sln --no-build -c Release /p:version=${{ github.event.release.tag_name }} && dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json -n true diff --git a/README.md b/README.md index f37c8f31..3faaee59 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -Handlebars.Net [![Build Status](https://travis-ci.org/rexm/Handlebars.Net.svg?branch=master)](https://travis-ci.org/rexm/Handlebars.Net) [![Nuget](https://img.shields.io/nuget/v/Handlebars.Net)](https://www.nuget.org/packages/Handlebars.Net/) -============== +##Handlebars.Net [![Build Status](https://travis-ci.org/rexm/Handlebars.Net.svg?branch=master)](https://travis-ci.org/rexm/Handlebars.Net) [![Nuget](https://img.shields.io/nuget/v/Handlebars.Net)](https://www.nuget.org/packages/Handlebars.Net/) + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=alert_status)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=bugs)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=zjklee_handlebars.csharp&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=zjklee_handlebars.csharp) + ============== **[Call for Input on v2](https://github.com/rexm/Handlebars.Net/issues/294)** Blistering-fast [Handlebars.js templates](http://handlebarsjs.com) in your .NET application. diff --git a/source/Handlebars.Test/Handlebars.Test.csproj b/source/Handlebars.Test/Handlebars.Test.csproj index dcc34293..017edac1 100644 --- a/source/Handlebars.Test/Handlebars.Test.csproj +++ b/source/Handlebars.Test/Handlebars.Test.csproj @@ -2,7 +2,7 @@ full - net461;netcoreapp1.1;netcoreapp2.0 + net461;netcoreapp1.1;netcoreapp2.1 700AF0B4-EA70-47B7-9F9D-17351E977B00 @@ -16,7 +16,7 @@ $(DefineConstants);netstandard - + $(DefineConstants);netstandard @@ -25,7 +25,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -34,22 +37,20 @@ - + + - - - - + - - - + + + - + From 68181df41a04b28609d9b68c7bb6c746d114032f Mon Sep 17 00:00:00 2001 From: Oleh Formaniuk Date: Sun, 31 May 2020 04:44:30 -0700 Subject: [PATCH 53/53] Fix passing context to `helper`s --- .../Translation/Expression/HelperFunctionBinder.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 2504b806..daaf544d 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -23,6 +23,7 @@ protected override Expression VisitHelperExpression(HelperExpression hex) { var helperName = hex.HelperName; var bindingContext = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var contextValue = bindingContext.Property(o => o.Value); var textWriter = bindingContext.Property(o => o.TextWriter); var arguments = hex.Arguments.Select(o => FunctionBuilder.Reduce(o, CompilationContext)); var args = ExpressionShortcuts.Array(arguments); @@ -30,13 +31,13 @@ protected override Expression VisitHelperExpression(HelperExpression hex) var configuration = CompilationContext.Configuration; if (configuration.Helpers.TryGetValue(helperName, out var helper)) { - return ExpressionShortcuts.Call(() => helper(textWriter, bindingContext, args)); + return ExpressionShortcuts.Call(() => helper(textWriter, contextValue, args)); } if (configuration.ReturnHelpers.TryGetValue(helperName, out var returnHelper)) { return ExpressionShortcuts.Call(() => - CaptureResult(textWriter, ExpressionShortcuts.Call(() => returnHelper(bindingContext, args))) + CaptureResult(textWriter, ExpressionShortcuts.Call(() => returnHelper(contextValue, args))) ); } @@ -46,7 +47,7 @@ protected override Expression VisitHelperExpression(HelperExpression hex) if (resolver.TryResolveReturnHelper(pureHelperName, typeof(object), out var resolvedHelper)) { return ExpressionShortcuts.Call(() => - CaptureResult(textWriter, ExpressionShortcuts.Call(() => resolvedHelper(bindingContext, args))) + CaptureResult(textWriter, ExpressionShortcuts.Call(() => resolvedHelper(contextValue, args))) ); } }