Skip to content

Commit

Permalink
Merge pull request #489 from Handlebars-Net/feature/decorators
Browse files Browse the repository at this point in the history
Decorators implementation
  • Loading branch information
oformaniuk authored Jan 24, 2022
2 parents bf2925d + c127e41 commit 558562b
Show file tree
Hide file tree
Showing 62 changed files with 1,786 additions and 300 deletions.
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,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:
Expand Down Expand Up @@ -262,7 +294,7 @@ Will not encode:\
` (backtick)\
' (single quote)

Will encode non-ascii characters `â`, `ß`, ...\
Will encode non-ascii characters ``, ``, ...\
Into HTML entities (`<`, `â`, `ß`, ...).

##### Areas
Expand All @@ -277,12 +309,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("&lt; â", actual);
Assert.Equal("&lt; ", actual);
}
```

Expand All @@ -301,8 +333,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
Expand Down
2 changes: 1 addition & 1 deletion source/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>8</LangVersion>
<LangVersion>9</LangVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
48 changes: 42 additions & 6 deletions source/Handlebars.Benchmark/EndToEnd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using HandlebarsDotNet;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.PathStructure;

namespace HandlebarsNet.Benchmark
{
Expand Down Expand Up @@ -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))
Expand All @@ -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<object> ObjectLevel1Generator()
{
var level = new List<object>();
Expand Down Expand Up @@ -171,6 +173,40 @@ List<Dictionary<string, object>> DictionaryLevel3Generator(int id1, int id2)
}
}

private class PowHelper : IHelperDescriptor<HelperOptions>
{
public PowHelper(PathInfo name) => Name = name;

public PathInfo Name { get; }

public object Invoke(in HelperOptions options, in Context context, in Arguments arguments)
{
return ((int)arguments[0] * (int)arguments[0]).ToString();
}

public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments)
{
output.WriteSafeString(((int)arguments[0] * (int)arguments[0]).ToString());
}
}

private class BlockPowHelper : IHelperDescriptor<BlockHelperOptions>
{
public BlockPowHelper(PathInfo name) => Name = name;

public PathInfo Name { get; }

public object Invoke(in BlockHelperOptions options, in Context context, in Arguments arguments)
{
return ((int)arguments[0] * (int)arguments[0]).ToString();
}

public void Invoke(in EncodedTextWriter output, in BlockHelperOptions options, in Context context, in Arguments arguments)
{
output.WriteSafeString(((int)arguments[0] * (int)arguments[0]).ToString());
}
}

[Benchmark]
public void Default() => _default(TextWriter.Null, _data);
}
Expand Down
22 changes: 1 addition & 21 deletions source/Handlebars.Test/BasicIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,10 @@
using HandlebarsDotNet.Features;
using HandlebarsDotNet.IO;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.ValueProviders;

namespace HandlebarsDotNet.Test
{
public class HandlebarsEnvGenerator : IEnumerable<object[]>
{
private readonly List<IHandlebars> _data = new List<IHandlebars>
{
Handlebars.Create(),
Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)),
Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types =>
{
types.Add(typeof(Dictionary<string, object>));
types.Add(typeof(Dictionary<int, object>));
types.Add(typeof(Dictionary<long, object>));
types.Add(typeof(Dictionary<string, string>));
})),
Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.TextEncoder = new HtmlEncoder())),
};

public IEnumerator<object[]> GetEnumerator() => _data.Select(o => new object[] { o }).GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class BasicIntegrationTests
{
private static string HtmlEncodeStringHelper(IHandlebars handlebars, string inputString)
Expand Down
28 changes: 26 additions & 2 deletions source/Handlebars.Test/ClosureBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ public class ClosureBuilderTests
[Fact]
public void GeneratesClosureWithOverflow()
{
var builder = new ClosureBuilder();
using var builder = ClosureBuilder.Create();

var paths = GeneratePaths(builder, 6);
var helpers = GenerateHelpers(builder, 6);
var blockHelpers = GenerateBlockHelpers(builder, 6);
var decoratorDelegates = GenerateDecoratorDelegates(builder, 6);
var others = GenerateOther(builder, 6);

_ = builder.Build(out var closure);
Expand All @@ -34,6 +35,10 @@ public void GeneratesClosureWithOverflow()
Assert.Equal(blockHelpers[3], closure.BHD3);
Assert.Equal(blockHelpers[5], closure.BHDA[1]);

Assert.Equal(decoratorDelegates[0], closure.DDD0);
Assert.Equal(decoratorDelegates[3], closure.DDD3);
Assert.Equal(decoratorDelegates[5], closure.DDDA[1]);

Assert.Equal(others[0], closure.A[0]);
Assert.Equal(others[3], closure.A[3]);
Assert.Equal(others[5], closure.A[5]);
Expand All @@ -42,11 +47,12 @@ public void GeneratesClosureWithOverflow()
[Fact]
public void GeneratesClosureWithoutOverflow()
{
var builder = new ClosureBuilder();
using var builder = ClosureBuilder.Create();

var paths = GeneratePaths(builder, 2);
var helpers = GenerateHelpers(builder, 2);
var blockHelpers = GenerateBlockHelpers(builder, 2);
var decorators = GenerateDecoratorDelegates(builder, 2);
var others = GenerateOther(builder, 2);

_ = builder.Build(out var closure);
Expand All @@ -63,6 +69,11 @@ public void GeneratesClosureWithoutOverflow()
Assert.Equal(blockHelpers[1], closure.BHD1);
Assert.Null(closure.BHDA);

Assert.Equal(decorators[0], closure.DDD0);
Assert.Equal(decorators[1], closure.DDD1);
Assert.Null(closure.DDD2);
Assert.Null(closure.BHDA);

Assert.Equal(others[0], closure.A[0]);
Assert.Equal(others[1], closure.A[1]);
Assert.Equal(2, closure.A.Length);
Expand Down Expand Up @@ -106,6 +117,19 @@ private static List<Ref<IHelperDescriptor<HelperOptions>>> GenerateHelpers(Closu

return helpers;
}

private static List<DecoratorDelegate> GenerateDecoratorDelegates(ClosureBuilder builder, int count)
{
var helpers = new List<DecoratorDelegate>();
for (int i = 0; i < count; i++)
{
DecoratorDelegate helper = (in EncodedTextWriter writer, BindingContext context, TemplateDelegate function) => function;
builder.Add(Const(helper));
helpers.Add(helper);
}

return helpers;
}

private static List<PathInfo> GeneratePaths(ClosureBuilder builder, int count)
{
Expand Down
Loading

0 comments on commit 558562b

Please sign in to comment.