diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..2866c977
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: Handlebars-Net/Handlebars.Net # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f41bf666..3059dd4e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,156 +1,156 @@
-name: CI
-
-on:
- push:
- branches: [ master ]
-
-jobs:
- build:
- name: Build
- runs-on: windows-latest
- steps:
- - uses: actions/checkout@master
- - name: Setup dotnet
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: |
- 2.1.x
- 3.1.x
- 5.0.x
- 6.0.x
- - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
- working-directory: ./source
- run: dotnet clean -c Release && dotnet nuget locals all --clear
- - name: Build
- working-directory: ./source
- run: dotnet build -c Release
-
- test:
- name: Tests on ${{ matrix.os }}
- needs: [build]
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ macos-latest, ubuntu-latest, windows-latest ]
- steps:
- - uses: actions/checkout@master
- - name: Setup dotnet
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: |
- 2.1.x
- 3.1.x
- 5.0.x
- 6.0.x
- - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
- working-directory: ./source
- run: dotnet clean -c Release && dotnet nuget locals all --clear
- - name: Test
- working-directory: ./source
- run: dotnet test --logger:trx --logger:GitHubActions
-
- sonar-ci:
- name: SonarCloud
- runs-on: windows-latest
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- - name: Setup dotnet
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: |
- 2.1.x
- 3.1.x
- 5.0.x
- 6.0.x
- - uses: actions/setup-java@v1
- with:
- java-version: '13' # The JDK version to make available on the path.
- - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
- working-directory: ./source
- run: dotnet clean -c Release && dotnet nuget locals all --clear
- - name: Cache SonarCloud packages
- uses: actions/cache@v1
- with:
- path: ~\sonar\cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
- - name: Cache SonarCloud scanner
- id: cache-sonar-scanner
- uses: actions/cache@v1
- with:
- path: .\.sonar\scanner
- key: ${{ runner.os }}-sonar-scanner
- restore-keys: ${{ runner.os }}-sonar-scanner
- - name: Install SonarCloud scanner
- if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
- shell: powershell
- run: |
- New-Item -Path .\.sonar\scanner -ItemType Directory
- dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
- - name: Run tests
- run: dotnet test source/Handlebars.sln --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings
- - name: Build and analyze
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
- SONAR_TOKEN: 22a5e1c1df52b7200ac14fc139ed1dfbe53dda4d
- shell: powershell
- run: |
- .\.sonar\scanner\dotnet-sonarscanner begin /k:"Handlebars-Net_Handlebars.Net" /o:"handlebars-net" /d:sonar.login="${{ env.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.coverage.exclusions="**/*.md;source/Handlebars.Benchmark/**/*.*" /d:sonar.cpd.exclusions="source/Handlebars/Iterators/**/*.*"
- dotnet build source/Handlebars.sln -c Release
- .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ env.SONAR_TOKEN }}"
-
- benchmark:
- name: Run Benchmark.Net
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Setup dotnet
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: |
- 2.1.x
- 3.1.x
- 5.0.x
- 6.0.x
- - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
- working-directory: ./source
- run: dotnet clean -c Release && dotnet nuget locals all --clear
- - name: Run benchmark
- working-directory: ./source/Handlebars.Benchmark
- run: dotnet run -c Release -f net6 --exporters json --filter '*' -m --join
- - name: Get benchmark file name
- id: benchmarkfilename
- working-directory: ./source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results
- run: |
- filePath=$(find . -type f -name 'BenchmarkRun-joined-*-report-full-compressed.json' | rev | cut -d '/' -f1 | rev)
- echo $filePath
- echo "::set-output name=file::$filePath"
- - name: Store benchmark result
- uses: Happypig375/github-action-benchmark@v1.8.2
- with:
- name: Benchmark.Net Benchmark
- tool: 'benchmarkdotnet'
- output-file-path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/${{ steps.benchmarkfilename.outputs.file }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
- auto-push: true
- # Show alert with commit comment on detecting possible performance regression
- alert-threshold: '200%'
- comment-on-alert: true
- fail-on-alert: false
- alert-comment-cc-users: '@zjklee'
- - name: Upload Artifacts
- uses: actions/upload-artifact@v2
- with:
- name: Benchmark
- path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/
-
- update_release_draft:
- name: Release Drafter
- runs-on: ubuntu-latest
- needs: [build, test, sonar-ci]
- steps:
- - uses: release-drafter/release-drafter@v5
- env:
+name: CI
+
+on:
+ push:
+ branches: [ master ]
+
+jobs:
+ build:
+ name: Build
+ runs-on: windows-2019
+ steps:
+ - uses: actions/checkout@master
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 2.1.x
+ 3.1.x
+ 5.0.x
+ 6.0.x
+ - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
+ working-directory: ./source
+ run: dotnet clean -c Release && dotnet nuget locals all --clear
+ - name: Build
+ working-directory: ./source
+ run: dotnet build -c Release
+
+ test:
+ name: Tests on ${{ matrix.os }}
+ needs: [build]
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ macos-latest, ubuntu-latest, windows-2019 ]
+ steps:
+ - uses: actions/checkout@master
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 2.1.x
+ 3.1.x
+ 5.0.x
+ 6.0.x
+ - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
+ working-directory: ./source
+ run: dotnet clean -c Release && dotnet nuget locals all --clear
+ - name: Test
+ working-directory: ./source
+ run: dotnet test --logger:trx --logger:GitHubActions
+
+ sonar-ci:
+ name: SonarCloud
+ runs-on: windows-2019
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 2.1.x
+ 3.1.x
+ 5.0.x
+ 6.0.x
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '17' # The JDK version to make available on the path.
+ - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
+ working-directory: ./source
+ run: dotnet clean -c Release && dotnet nuget locals all --clear
+ - name: Cache SonarCloud packages
+ uses: actions/cache@v1
+ with:
+ path: ~\sonar\cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache SonarCloud scanner
+ id: cache-sonar-scanner
+ uses: actions/cache@v1
+ with:
+ path: .\.sonar\scanner
+ key: ${{ runner.os }}-sonar-scanner
+ restore-keys: ${{ runner.os }}-sonar-scanner
+ - name: Install SonarCloud scanner
+ if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
+ shell: powershell
+ run: |
+ New-Item -Path .\.sonar\scanner -ItemType Directory
+ dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
+ - name: Run tests
+ run: dotnet test source/Handlebars.sln --logger:trx --collect:"XPlat Code Coverage" --settings source/Handlebars.Test/coverlet.runsettings
+ - name: Build and analyze
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
+ SONAR_TOKEN: 22a5e1c1df52b7200ac14fc139ed1dfbe53dda4d
+ shell: powershell
+ run: |
+ .\.sonar\scanner\dotnet-sonarscanner begin /k:"Handlebars-Net_Handlebars.Net" /o:"handlebars-net" /d:sonar.login="${{ env.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/*.opencover.xml" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.coverage.exclusions="**/*.md;source/Handlebars.Benchmark/**/*.*" /d:sonar.cpd.exclusions="source/Handlebars/Iterators/**/*.*"
+ dotnet build source/Handlebars.sln -c Release
+ .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ env.SONAR_TOKEN }}"
+
+ benchmark:
+ name: Run Benchmark.Net
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 2.1.x
+ 3.1.x
+ 5.0.x
+ 6.0.x
+ - name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
+ working-directory: ./source
+ run: dotnet clean -c Release && dotnet nuget locals all --clear
+ - name: Run benchmark
+ working-directory: ./source/Handlebars.Benchmark
+ run: dotnet run -c Release -f net6 --exporters json --filter '*' -m --join
+ - name: Get benchmark file name
+ id: benchmarkfilename
+ working-directory: ./source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results
+ run: |
+ filePath=$(find . -type f -name 'BenchmarkRun-joined-*-report-full-compressed.json' | rev | cut -d '/' -f1 | rev)
+ echo $filePath
+ echo "::set-output name=file::$filePath"
+ - name: Store benchmark result
+ uses: Happypig375/github-action-benchmark@v1.8.2
+ with:
+ name: Benchmark.Net Benchmark
+ tool: 'benchmarkdotnet'
+ output-file-path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/${{ steps.benchmarkfilename.outputs.file }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ auto-push: true
+ # Show alert with commit comment on detecting possible performance regression
+ alert-threshold: '200%'
+ comment-on-alert: true
+ fail-on-alert: false
+ alert-comment-cc-users: '@zjklee'
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: Benchmark
+ path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/
+
+ update_release_draft:
+ name: Release Drafter
+ runs-on: ubuntu-latest
+ needs: [build, test, sonar-ci]
+ steps:
+ - uses: release-drafter/release-drafter@v5
+ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 41ad537b..04aacd27 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -7,7 +7,7 @@ on:
jobs:
build:
name: Build
- runs-on: windows-latest
+ runs-on: windows-2019
steps:
- uses: actions/checkout@master
- name: Setup dotnet
@@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ macos-latest, ubuntu-latest, windows-latest ]
+ os: [ macos-latest, ubuntu-latest, windows-2019 ]
steps:
- uses: actions/checkout@master
- name: Setup dotnet
@@ -50,7 +50,7 @@ jobs:
sonar-pr:
name: SonarCloud
- runs-on: windows-latest
+ runs-on: windows-2019
steps:
- uses: actions/checkout@v2
with:
@@ -65,7 +65,7 @@ jobs:
6.0.x
- uses: actions/setup-java@v1
with:
- java-version: '13' # The JDK version to make available on the path.
+ java-version: '17' # The JDK version to make available on the path.
- name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
working-directory: ./source
run: dotnet clean -c Release && dotnet nuget locals all --clear
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8b3d1595..0fc87b87 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,7 +7,7 @@ on:
jobs:
publish:
name: Publish
- runs-on: windows-latest
+ runs-on: windows-2019
steps:
- uses: actions/checkout@v2
diff --git a/README.md b/README.md
index 594cd2c1..aa05f778 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,13 @@ Handlebars.Net doesn't use a scripting engine to run a Javascript library - it *
dotnet add package Handlebars.Net
+## Extensions
+The following projects are extending Handlebars.Net:
+- [Handlebars.Net.Extension.Json](https://github.com/Handlebars-Net/Handlebars.Net.Extension.Json) (Adds `System.Text.Json.JsonDocument` support)
+- [Handlebars.Net.Extension.NewtonsoftJson](https://github.com/Handlebars-Net/Handlebars.Net.Extension.NewtonsoftJson) (Adds `Newtonsoft.Json` support)
+- [Handlebars.Net.Helpers](https://github.com/Handlebars-Net/Handlebars.Net.Helpers) (Additional helpers in the categories: 'Constants', 'Enumerable', 'Math', 'Regex', 'String', 'DateTime', 'Url' , 'DynamicLinq', 'Humanizer', 'Json', 'Random', 'Xeger' and 'XPath'.)
+
+
## Usage
```c#
@@ -160,6 +167,38 @@ The animal, Chewy, is not a dog.
*/
```
+### Registering Decorators
+
+```c#
+[Fact]
+public void BasicDecorator(IHandlebars handlebars)
+{
+ string source = "{{#block @value-from-decorator}}{{*decorator 42}}{{@value}}{{/block}}";
+
+ var handlebars = Handlebars.Create();
+ handlebars.RegisterHelper("block", (output, options, context, arguments) =>
+ {
+ options.Data.CreateProperty("value", arguments[0], out _);
+ options.Template(output, context);
+ });
+
+ handlebars.RegisterDecorator("decorator",
+ (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
+ {
+ options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
+ });
+
+ var template = handlebars.Compile(source);
+
+ var result = template(null);
+ Assert.Equal("42", result);
+}
+```
+For more examples see [DecoratorTests.cs](https://github.com/Handlebars-Net/Handlebars.Net/tree/master/source/Handlebars.Test/DecoratorTests.cs)
+
+#### Known limitations:
+- helpers registered inside of a decorator will not override existing registrations
+
### Register custom value formatter
In case you need to apply custom value formatting (e.g. `DateTime`) you can use `IFormatter` and `IFormatterProvider` interfaces:
@@ -214,6 +253,56 @@ public void DateTimeFormatter(IHandlebars handlebars)
#### Notes
- Formatters are resolved in reverse order according to registration. If multiple providers can provide formatter for a type the last registered would be used.
+### Shared environment
+
+By default Handlebars will create standalone copy of environment for each compiled template. This is done in order to eliminate a chance of altering behavior of one template from inside of other one.
+
+Unfortunately, in case runtime has a lot of compiled templates (regardless of the template size) it may have significant memory footprint. This can be solved by using `SharedEnvironment`.
+
+Templates compiled in `SharedEnvironment` will share the same configuration.
+
+#### Limitations
+
+Only runtime configuration properties can be changed after the shared environment has been created. Changes to `Configuration.CompileTimeConfiguration` and other compile-time properties will have no effect.
+
+#### Example
+
+```c#
+[Fact]
+public void BasicSharedEnvironment()
+{
+ var handlebars = Handlebars.CreateSharedEnvironment();
+ handlebars.RegisterHelper("registerLateHelper",
+ (in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) =>
+ {
+ var configuration = options.Frame
+ .GetType()
+ .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)?
+ .GetValue(options.Frame) as ICompiledHandlebarsConfiguration;
+
+ var helpers = configuration?.Helpers;
+
+ const string name = "lateHelper";
+ if (helpers?.TryGetValue(name, out var @ref) ?? false)
+ {
+ @ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42);
+ }
+ });
+
+ var _0_template = "{{registerLateHelper}}";
+ var _0 = handlebars.Compile(_0_template);
+ var _1_template = "{{lateHelper}}";
+ var _1 = handlebars.Compile(_1_template);
+
+ var result = _1(null);
+ Assert.Equal("", result); // `lateHelper` is not registered yet
+
+ _0(null);
+ result = _1(null);
+ Assert.Equal("42", result);
+}
+```
+
### Compatibility feature toggles
Compatibility feature toggles defines a set of settings responsible for controlling compilation/rendering behavior. Each of those settings would enable certain feature that would break compatibility with canonical Handlebars.
@@ -262,7 +351,7 @@ Will not encode:\
` (backtick)\
' (single quote)
-Will encode non-ascii characters `â`, `ß`, ...\
+Will encode non-ascii characters `�`, `�`, ...\
Into HTML entities (`<`, `â`, `ß`, ...).
##### Areas
@@ -277,12 +366,12 @@ public void UseCanonicalHtmlEncodingRules()
handlebars.Configuration.TextEncoder = new HtmlEncoder();
var source = "{{Text}}";
- var value = new { Text = "< â" };
+ var value = new { Text = "< �" };
var template = handlebars.Compile(source);
var actual = template(value);
- Assert.Equal("< â", actual);
+ Assert.Equal("< �", actual);
}
```
@@ -301,8 +390,6 @@ Nearly all time spent in rendering is in the routine that resolves values agains
- Rendering starts to get slower (into the tens of milliseconds or more) on dynamic objects.
- The slowest (up to hundreds of milliseconds or worse) tend to be objects with custom type implementations (such as `ICustomTypeDescriptor`) that are not optimized for heavy reflection.
-~~A frequent performance issue that comes up is JSON.NET's `JObject`, which for reasons we haven't fully researched, has very slow reflection characteristics when used as a model in Handlebars.Net. A simple fix is to just use JSON.NET's built-in ability to deserialize a JSON string to an `ExpandoObject` instead of a `JObject`. This will yield nearly an order of magnitude improvement in render times on average.~~
-
## Future roadmap
TBD
diff --git a/source/Directory.Build.props b/source/Directory.Build.props
index 261a4872..f2a72002 100644
--- a/source/Directory.Build.props
+++ b/source/Directory.Build.props
@@ -1,29 +1,21 @@
-
- false
- ToBeObtained.snk
-
-
-
- LICENSE
- true
- Rex Morgan; Handlebars-Net
- true
- false
- true
- snupkg
- 8
-
-
-
- 1591;1574;1584;1658
-
-
-
-
- false
- true
- .
-
-
+
+ false
+ ToBeObtained.snk
+
+
+
+ MIT
+ true
+ Rex Morgan; Handlebars-Net
+ true
+ false
+ true
+ snupkg
+ 9
+
+
+
+ 1591;1574;1584;1658
+
\ No newline at end of file
diff --git a/source/Handlebars.Benchmark/EndToEnd.cs b/source/Handlebars.Benchmark/EndToEnd.cs
index b12de82b..2c65981c 100644
--- a/source/Handlebars.Benchmark/EndToEnd.cs
+++ b/source/Handlebars.Benchmark/EndToEnd.cs
@@ -3,6 +3,8 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using HandlebarsDotNet;
+using HandlebarsDotNet.Helpers;
+using HandlebarsDotNet.PathStructure;
namespace HandlebarsNet.Benchmark
{
@@ -66,9 +68,9 @@ public void Setup()
var handlebars = Handlebars.Create();
using(handlebars.Configure())
{
- handlebars.RegisterHelper("pow1", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow2", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow5", (output, options, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
+ handlebars.RegisterHelper(new PowHelper("pow1"));
+ handlebars.RegisterHelper(new PowHelper("pow2"));
+ handlebars.RegisterHelper(new BlockPowHelper("pow5"));
}
using (var reader = new StringReader(template))
@@ -78,10 +80,10 @@ public void Setup()
using(handlebars.Configure())
{
- handlebars.RegisterHelper("pow3", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
- handlebars.RegisterHelper("pow4", (output, context, arguments) => output.WriteSafeString(((int) arguments[0] * (int) arguments[0]).ToString()));
+ handlebars.RegisterHelper(new PowHelper("pow3"));
+ handlebars.RegisterHelper(new PowHelper("pow4"));
}
-
+
List