diff --git a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs index 0863c98c..88e3f14b 100644 --- a/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs +++ b/source/Handlebars.Extension.CompileFast/FastExpressionCompiler.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Expressions.Shortcuts; using FastExpressionCompiler; using HandlebarsDotNet.Features; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Extension.CompileFast { @@ -46,11 +46,10 @@ public T Compile(Expression expression) where T: class 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); + var store = Arg(_templateClosure).Member(o => o.Store); + var parameterExpressions = new[] { store.Expression }.Concat(outerParameters); + var invocationExpression = Expression.Invoke(Expression.Constant(compiledLambda), parameterExpressions); + var outerLambda = Expression.Lambda(invocationExpression, outerParameters); return outerLambda.CompileFast(); } diff --git a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj index 57b07773..e9400b08 100644 --- a/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj +++ b/source/Handlebars.Extension.Logger/Handlebars.Extension.Logger.csproj @@ -32,6 +32,10 @@ + + + + diff --git a/source/Handlebars.Extension.Logger/LoggerFeature.cs b/source/Handlebars.Extension.Logger/LoggerFeature.cs index 22bc2291..0d35e7fe 100644 --- a/source/Handlebars.Extension.Logger/LoggerFeature.cs +++ b/source/Handlebars.Extension.Logger/LoggerFeature.cs @@ -26,7 +26,18 @@ public LoggerFeature(Log logger) public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { - configuration.ReturnHelpers["log"] = LogHelper; + if (configuration.ReturnHelpers.TryGetValue("log", out var logger)) + { + configuration.ReturnHelpers["log"] = (context, arguments) => + { + logger(context, arguments); + return LogHelper(context, arguments); + }; + } + else + { + configuration.ReturnHelpers["log"] = LogHelper; + } } public void CompilationCompleted() diff --git a/source/Handlebars.Test/HelperTests.cs b/source/Handlebars.Test/HelperTests.cs index dce500d2..6d92170f 100644 --- a/source/Handlebars.Test/HelperTests.cs +++ b/source/Handlebars.Test/HelperTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using HandlebarsDotNet.Features; namespace HandlebarsDotNet.Test { @@ -29,6 +30,7 @@ public void HelperWithLiteralArguments() Assert.Equal(expected, output); } + [Fact] public void BlockHelperWithBlockParams() { @@ -84,6 +86,44 @@ public void BlockHelperLateBound() Assert.Equal(expected, output); } + [Fact] + public void BlockHelperLateBoundConflictsWithValue() + { + var source = "{{#late}}late{{/late}}"; + + var handlebars = Handlebars.Create(); + var template = handlebars.Compile(source); + + handlebars.RegisterHelper("late", (writer, options, context, args) => + { + options.Template(writer, context); + }); + + var output = template(new { late = "should be ignored" }); + + var expected = "late"; + + Assert.Equal(expected, output); + } + + [Fact] + public void BlockHelperLateBoundMissingHelperFallbackToDeferredSection() + { + var source = "{{#late}}late{{/late}}"; + + var handlebars = Handlebars.Create(); + handlebars.Configuration.RegisterMissingHelperHook( + (context, arguments) => "Hook" + ); + var template = handlebars.Compile(source); + + var output = template(new { late = "late" }); + + var expected = "late"; + + Assert.Equal(expected, output); + } + [Fact] public void HelperLateBound() { @@ -144,6 +184,152 @@ public void WrongHelperLiteralLateBound(string source) Assert.Equal(string.Empty, output); } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingHelperHook(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing helper: {0}"; + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => + { + var name = arguments.Last().ToString(); + return string.Format(format, name.Trim('[', ']')); + }); + + var source = "{{"+ helperName +"}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Fact] + public void MissingHelperHookViaFeatureAndMethod() + { + var expected = "Hook"; + var handlebars = Handlebars.Create(); + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => expected + ); + + handlebars.RegisterHelper("helperMissing", + (context, arguments) => "Should be ignored" + ); + + var source = "{{missing}}"; + var template = handlebars.Compile(source); + + var output = template(null); + + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingHelperHookViaHelperRegistration(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing helper: {0}"; + handlebars.RegisterHelper("helperMissing", (context, arguments) => + { + var name = arguments.Last().ToString(); + return string.Format(format, name.Trim('[', ']')); + }); + + var source = "{{"+ helperName +"}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingBlockHelperHook(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing block helper: {0}"; + handlebars.Configuration + .RegisterMissingHelperHook( + blockHelperMissing: (writer, options, context, arguments) => + { + var name = options.GetValue("name"); + writer.WriteSafeString(string.Format(format, name.Trim('[', ']'))); + }); + + var source = "{{#"+ helperName +"}}should not appear{{/" + helperName + "}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("missing")] + [InlineData("[missing]")] + [InlineData("[$missing]")] + [InlineData("[m.i.s.s.i.n.g]")] + public void MissingBlockHelperHookViaHelperRegistration(string helperName) + { + var handlebars = Handlebars.Create(); + var format = "Missing block helper: {0}"; + handlebars.RegisterHelper("blockHelperMissing", (writer, options, context, arguments) => + { + var name = options.GetValue("name"); + writer.WriteSafeString(string.Format(format, name.Trim('[', ']'))); + }); + + var source = "{{#"+ helperName +"}}should not appear{{/" + helperName + "}}"; + + var template = handlebars.Compile(source); + + var output = template(null); + + var expected = string.Format(format, helperName.Trim('[', ']')); + Assert.Equal(expected, output); + } + + [Fact] + public void MissingHelperHookWhenVariableExists() + { + var handlebars = Handlebars.Create(); + var expected = "Variable"; + + handlebars.Configuration + .RegisterMissingHelperHook( + (context, arguments) => "Hook" + ); + + var source = "{{missing}}"; + + var template = Handlebars.Compile(source); + + var output = template(new { missing = "Variable" }); + + Assert.Equal(expected, output); + } [Fact] public void HelperWithLiteralArgumentsWithQuotes() diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index d4bf09c9..985ee853 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -79,5 +79,31 @@ public void LateBoundHelperWithSameNameVariablePath() actual = template(data); Assert.Equal(expected, actual); } + + // Issue https://github.com/rexm/Handlebars.Net/issues/354 + [Fact] + public void BlockHelperWithInversion() + { + string source = "{{^test input}}empty{{else}}not empty{{/test}}"; + + var handlebars = Handlebars.Create(); + handlebars.RegisterHelper("test", (output, options, context, arguments) => + { + if (HandlebarsUtils.IsTruthy(arguments[0])) + { + options.Template(output, context); + } + else + { + options.Inverse(output, context); + } + }); + + var template = handlebars.Compile(source); + + Assert.Equal("empty", template(null)); + Assert.Equal("empty", template(new { otherInput = 1 })); + Assert.Equal("not empty", template(new { input = 1 })); + } } } \ No newline at end of file diff --git a/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs b/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs new file mode 100644 index 00000000..0f222313 --- /dev/null +++ b/source/Handlebars/Adapters/HelperToReturnHelperAdapter.cs @@ -0,0 +1,26 @@ +namespace HandlebarsDotNet.Adapters +{ + internal class HelperToReturnHelperAdapter + { + private readonly HandlebarsHelper _helper; + private readonly HandlebarsReturnHelper _delegate; + + public HelperToReturnHelperAdapter(HandlebarsHelper helper) + { + _helper = helper; + _delegate = (context, arguments) => + { + using (var writer = new PolledStringWriter()) + { + _helper(writer, context, arguments); + return writer.ToString(); + } + }; + } + + public static implicit operator HandlebarsReturnHelper(HelperToReturnHelperAdapter adapter) + { + return adapter._delegate; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaEnricher.cs b/source/Handlebars/Adapters/LambdaEnricher.cs new file mode 100644 index 00000000..9e9467dd --- /dev/null +++ b/source/Handlebars/Adapters/LambdaEnricher.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Adapters +{ + internal class LambdaEnricher + { + private readonly Action _direct; + private readonly Action _inverse; + + public LambdaEnricher(Action direct, Action inverse) + { + _direct = direct; + _inverse = inverse; + + Direct = (context, writer, arg) => _direct(writer, arg); + Inverse = (context, writer, arg) => _inverse(writer, arg); + } + + public readonly Action Direct; + public readonly Action Inverse; + } +} \ No newline at end of file diff --git a/source/Handlebars/Adapters/LambdaReducer.cs b/source/Handlebars/Adapters/LambdaReducer.cs new file mode 100644 index 00000000..1a8d82bd --- /dev/null +++ b/source/Handlebars/Adapters/LambdaReducer.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using HandlebarsDotNet.Compiler; + +namespace HandlebarsDotNet.Adapters +{ + internal class LambdaReducer + { + private readonly BindingContext _context; + private readonly Action _direct; + private readonly Action _inverse; + + public LambdaReducer(BindingContext context, Action direct, Action inverse) + { + _context = context; + _direct = direct; + _inverse = inverse; + + Direct = (writer, arg) => _direct(_context, writer, arg); + Inverse = (writer, arg) => _inverse(_context, writer, arg); + } + + public readonly Action Direct; + public readonly Action Inverse; + } +} \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs index 318c8136..b360c59c 100644 --- a/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/ChainSegment.cs @@ -1,61 +1,71 @@ using System; -using System.Diagnostics; using HandlebarsDotNet.Polyfills; namespace HandlebarsDotNet.Compiler.Structure.Path { - [DebuggerDisplay("{Value}")] - internal struct ChainSegment : IEquatable + /// + /// Represents parts of single separated with dots. + /// + public struct ChainSegment : IEquatable { + private readonly string _value; + + internal readonly string LowerInvariant; + + /// + /// + /// public ChainSegment(string value) { var segmentValue = string.IsNullOrEmpty(value) ? "this" : value.TrimStart('@').Intern(); var segmentTrimmedValue = TrimSquareBrackets(segmentValue).Intern(); IsThis = string.IsNullOrEmpty(value) || string.Equals(value, "this", StringComparison.OrdinalIgnoreCase); - Value = segmentValue; + _value = segmentValue; IsVariable = !string.IsNullOrEmpty(value) && value.StartsWith("@"); TrimmedValue = segmentTrimmedValue; LowerInvariant = segmentTrimmedValue.ToLowerInvariant().Intern(); } - public readonly string Value; - public readonly string LowerInvariant; + /// + /// Value with trimmed '[' and ']' + /// public readonly string TrimmedValue; + + /// + /// Indicates whether is part of @ variable + /// public readonly bool IsVariable; + + /// + /// Indicates whether is this or . + /// public readonly bool IsThis; - public override string ToString() - { - return Value; - } + /// + /// Returns string representation of current + /// + public override string ToString() => _value; - public bool Equals(ChainSegment other) - { - return Value == other.Value; - } + /// + public bool Equals(ChainSegment other) => _value == other._value; - public override bool Equals(object obj) - { - return obj is ChainSegment other && Equals(other); - } + /// + public override bool Equals(object obj) => obj is ChainSegment other && Equals(other); - public override int GetHashCode() - { - return Value != null ? Value.GetHashCode() : 0; - } + /// + public override int GetHashCode() => _value != null ? _value.GetHashCode() : 0; - public static bool operator ==(ChainSegment a, ChainSegment b) - { - return a.Equals(b); - } - - public static bool operator !=(ChainSegment a, ChainSegment b) - { - return !a.Equals(b); - } + /// + public static bool operator ==(ChainSegment a, ChainSegment b) => a.Equals(b); + + /// + public static bool operator !=(ChainSegment a, ChainSegment b) => !a.Equals(b); + + /// + public static implicit operator string(ChainSegment segment) => segment._value; - public static string TrimSquareBrackets(string key) + internal 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 74117646..cc0467ab 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathInfo.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathInfo.cs @@ -4,9 +4,19 @@ namespace HandlebarsDotNet.Compiler.Structure.Path { internal delegate object ProcessSegment(ref PathInfo pathInfo, ref BindingContext context, object instance, HashParameterDictionary hashParameters); - internal struct PathInfo : IEquatable + /// + /// Represents path expression + /// + public struct PathInfo : IEquatable { - public PathInfo( + private readonly string _path; + + internal readonly ProcessSegment ProcessSegment; + internal readonly bool IsValidHelperLiteral; + internal readonly bool HasValue; + internal readonly bool IsThis; + + internal PathInfo( bool hasValue, string path, bool isValidHelperLiteral, @@ -16,50 +26,41 @@ ProcessSegment processSegment { IsValidHelperLiteral = isValidHelperLiteral; HasValue = hasValue; - Path = path; + _path = path; IsVariable = path.StartsWith("@"); - IsInversion = path.StartsWith("^"); - IsBlockHelper = path.StartsWith("#"); Segments = segments; ProcessSegment = processSegment; + IsThis = string.Equals(path, "this", StringComparison.OrdinalIgnoreCase) || path == "."; } - public readonly bool IsBlockHelper; - public bool IsValidHelperLiteral; - public readonly bool IsInversion; - public readonly bool HasValue; - public readonly string Path; + /// + /// Indicates whether is part of @ variable + /// public readonly bool IsVariable; + + /// public readonly PathSegment[] Segments; - public readonly ProcessSegment ProcessSegment; - + /// public bool Equals(PathInfo other) { - return IsBlockHelper == other.IsBlockHelper && - IsInversion == other.IsInversion && - HasValue == other.HasValue && IsVariable == other.IsVariable && - Path == other.Path; + return HasValue == other.HasValue + && IsVariable == other.IsVariable + && _path == other._path; } - public override bool Equals(object obj) - { - return obj is PathInfo other && Equals(other); - } + /// + public override bool Equals(object obj) => obj is PathInfo other && Equals(other); - public override int GetHashCode() - { - return Path.GetHashCode(); - } + /// + public override int GetHashCode() => _path.GetHashCode(); - public override string ToString() - { - return Path; - } + /// + /// Returns string representation of current + /// + public override string ToString() => _path; - public static implicit operator string(PathInfo pathInfo) - { - return pathInfo.Path; - } + /// + public static implicit operator string(PathInfo pathInfo) => pathInfo._path; } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs index 74b69419..511bf23a 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathResolver.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathResolver.cs @@ -130,12 +130,11 @@ object instance if (!(instance is UndefinedBindingResult)) return instance; - if (hashParameters == null || hashParameters.ContainsKey(chainSegment.Value) || + if (hashParameters == null || hashParameters.ContainsKey(chainSegment) || context.ParentContext == null) { if (context.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, - (instance as UndefinedBindingResult).Value); + throw new HandlebarsUndefinedBindingException(pathInfo, (instance as UndefinedBindingResult).Value); return instance; } @@ -143,7 +142,7 @@ object instance if (!(instance is UndefinedBindingResult result)) return instance; if (context.Configuration.ThrowOnUnresolvedBindingExpression) - throw new HandlebarsUndefinedBindingException(pathInfo.Path, result.Value); + throw new HandlebarsUndefinedBindingException(pathInfo, result.Value); return result; } @@ -160,7 +159,7 @@ private static IEnumerable GetPathChain(string segmentString) insideEscapeBlock = false; } - list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].Value}.{next}"); + list[list.Count - 1] = new ChainSegment($"{list[list.Count - 1].ToString()}.{next}"); return list; } @@ -187,7 +186,7 @@ private static object ResolveValue(BindingContext context, object instance, ref if (chainSegment.IsVariable) { return !context.TryGetContextVariable(ref chainSegment, out resolvedValue) - ? new UndefinedBindingResult(chainSegment.Value, context.Configuration) + ? new UndefinedBindingResult(chainSegment, context.Configuration) : resolvedValue; } @@ -204,7 +203,7 @@ private static object ResolveValue(BindingContext context, object instance, ref return resolvedValue; } - return new UndefinedBindingResult(chainSegment.Value, context.Configuration); + return new UndefinedBindingResult(chainSegment, context.Configuration); } public static bool TryAccessMember(object instance, ref ChainSegment chainSegment, ICompiledHandlebarsConfiguration configuration, out object value) @@ -215,10 +214,10 @@ public static bool TryAccessMember(object instance, ref ChainSegment chainSegmen return false; } - var memberName = chainSegment.Value; + var memberName = chainSegment.ToString(); var instanceType = instance.GetType(); memberName = TryResolveMemberName(instance, memberName, configuration, out var result) - ? TrimSquareBrackets(result).Intern() + ? ChainSegment.TrimSquareBrackets(result).Intern() : chainSegment.TrimmedValue; if (!configuration.ObjectDescriptorProvider.CanHandleType(instanceType)) @@ -236,17 +235,6 @@ public static bool TryAccessMember(object instance, ref ChainSegment chainSegmen 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 bool TryResolveMemberName(object instance, string memberName, ICompiledHandlebarsConfiguration configuration, out string value) { var resolver = configuration.ExpressionNameResolver; diff --git a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs index e228442a..fed4e09b 100644 --- a/source/Handlebars/Compiler/Structure/Path/PathSegment.cs +++ b/source/Handlebars/Compiler/Structure/Path/PathSegment.cs @@ -4,52 +4,46 @@ namespace HandlebarsDotNet.Compiler.Structure.Path { internal delegate object ProcessPathChain(BindingContext context, HashParameterDictionary hashParameters, ref PathInfo pathInfo, ref PathSegment segment, object instance); - internal struct PathSegment : IEquatable + /// + /// Represents parts of single separated with '/'. + /// + public struct PathSegment : IEquatable { - public PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, ProcessPathChain processPathChain) + private readonly string _segment; + + internal readonly ProcessPathChain ProcessPathChain; + internal readonly bool IsJumpUp; + + internal PathSegment(string segment, ChainSegment[] chain, bool isJumpUp, ProcessPathChain processPathChain) { - Segment = segment; + _segment = segment; IsJumpUp = isJumpUp; PathChain = chain; ProcessPathChain = processPathChain; } - public readonly string Segment; - - public readonly bool IsJumpUp; - + /// public readonly ChainSegment[] PathChain; - public readonly ProcessPathChain ProcessPathChain; + /// + /// Returns string representation of current + /// + /// + public override string ToString() => _segment; - public override string ToString() - { - return Segment; - } + /// + public bool Equals(PathSegment other) => _segment == other._segment; - public bool Equals(PathSegment other) - { - return Segment == other.Segment; - } + /// + public override bool Equals(object obj) => obj is PathSegment other && Equals(other); - public override bool Equals(object obj) - { - return obj is PathSegment other && Equals(other); - } + /// + public override int GetHashCode() => _segment != null ? _segment.GetHashCode() : 0; - public override int GetHashCode() - { - return Segment != null ? Segment.GetHashCode() : 0; - } + /// + public static bool operator ==(PathSegment a, PathSegment b) => a.Equals(b); - public static bool operator ==(PathSegment a, PathSegment b) - { - return a.Equals(b); - } - - public static bool operator !=(PathSegment a, PathSegment b) - { - return !a.Equals(b); - } + /// + public static bool operator !=(PathSegment a, PathSegment b) => !a.Equals(b); } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs index 216be7a0..69c1b9d1 100644 --- a/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs +++ b/source/Handlebars/Compiler/Structure/UndefinedBindingResult.cs @@ -17,7 +17,7 @@ public UndefinedBindingResult(string value, ICompiledHandlebarsConfiguration con public UndefinedBindingResult(ChainSegment value, ICompiledHandlebarsConfiguration configuration) { - Value = value.Value; + Value = value; _configuration = configuration; } diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 26bd36ad..644b3f3a 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -3,8 +3,10 @@ using System.Linq; using System.Linq.Expressions; using Expressions.Shortcuts; +using HandlebarsDotNet.Adapters; using HandlebarsDotNet.Compiler.Structure.Path; using HandlebarsDotNet.ValueProviders; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -26,47 +28,46 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b { var isInlinePartial = bhex.HelperName == "#*inline"; - var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); + var context = Arg(CompilationContext.BindingContext); var bindingContext = isInlinePartial ? context.Cast() : context.Property(o => o.Value); - var readerContext = ExpressionShortcuts.Arg(bhex.Context); + var readerContext = Arg(bhex.Context); var body = FunctionBuilder.CompileCore(((BlockExpression) bhex.Body).Expressions, CompilationContext.Configuration); var inverse = FunctionBuilder.CompileCore(((BlockExpression) bhex.Inversion).Expressions, CompilationContext.Configuration); var helperName = bhex.HelperName.TrimStart('#', '^'); + var helperPrefix = bhex.IsRaw ? '#' : bhex.HelperName[0]; 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 args = bhex.Arguments + .ApplyOn((PathExpression pex) => pex.Context = PathExpression.ResolutionContext.Parameter) + .Select(o => FunctionBuilder.Reduce(o, CompilationContext)); - var reducerNew = ExpressionShortcuts.New(() => new LambdaReducer(context, body, inverse)); - var reducer = ExpressionShortcuts.Var(); + var arguments = Array(args); + var configuration = Arg(CompilationContext.Configuration); + + var reducerNew = New(() => new LambdaReducer(context, body, inverse)); + var reducer = Var(); - var blockParamsProvider = ExpressionShortcuts.Var(); - var blockParamsExpression = ExpressionShortcuts.Call( - () => BlockParamsValueProvider.Create(context, ExpressionShortcuts.Arg(bhex.BlockParams)) + var blockParamsProvider = Var(); + var blockParamsExpression = Call( + () => BlockParamsValueProvider.Create(context, Arg(bhex.BlockParams)) ); - var helperOptions = ExpressionShortcuts.New( - () => new HelperOptions( - reducer.Property(o => o.Direct), - reducer.Property(o => o.Inverse), - blockParamsProvider, - configuration) - ); + var helperOptions = CreateHelperOptions(bhex, helperPrefix, reducer, blockParamsProvider, configuration, context); var blockHelpers = CompilationContext.Configuration.BlockHelpers; if (blockHelpers.TryGetValue(helperName, out var helper)) { - return ExpressionShortcuts.Block() + return 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( + .Line(Try() + .Body(Call( () => helper(textWriter, helperOptions, bindingContext, arguments) )) .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) @@ -76,33 +77,33 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b 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))) - ); - })); + if (!resolver.TryResolveBlockHelper(helperName, out helper)) continue; + + return Block() + .Parameter(reducer, reducerNew) + .Parameter(blockParamsProvider, blockParamsExpression) + .Line(blockParamsProvider.Using((self, builder) => + { + builder + .Line(context.Call(o => o.RegisterValueProvider((IValueProvider) self))) + .Line(Try() + .Body(Call( + () => helper(textWriter, helperOptions, bindingContext, arguments) + )) + .Finally(context.Call(o => o.UnregisterValueProvider((IValueProvider) self))) + ); + })); } - var helperPrefix = bhex.HelperName[0]; - return ExpressionShortcuts.Block() + return 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( + .Line(Try() + .Body(Call( () => LateBoundCall( helperName, helperPrefix, @@ -120,7 +121,47 @@ protected override Expression VisitBlockHelperExpression(BlockHelperExpression b ); })); } - + + private static ExpressionContainer CreateHelperOptions( + BlockHelperExpression bhex, + char helperPrefix, + ExpressionContainer reducer, + ExpressionContainer blockParamsProvider, + ExpressionContainer configuration, + ExpressionContainer context) + { + ExpressionContainer helperOptions; + switch (helperPrefix) + { + case '#': + helperOptions = New( + () => new HelperOptions( + reducer.Member(o => o.Direct), + reducer.Member(o => o.Inverse), + blockParamsProvider, + configuration, + context) + ); + break; + + case '^': + helperOptions = New( + () => new HelperOptions( + reducer.Member(o => o.Inverse), + reducer.Member(o => o.Direct), + blockParamsProvider, + configuration, + context) + ); + break; + + default: + throw new HandlebarsCompilerException($"Helper {bhex.HelperName} referenced with unsupported prefix", bhex.Context); + } + + return helperOptions; + } + private static void LateBoundCall( string helperName, char helperPrefix, @@ -152,11 +193,13 @@ params object[] arguments return; } - var segment = new ChainSegment(helperName); - bindingContext.TryGetContextVariable(ref segment, out var value); + if(arguments.Length > 0) throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. BlockHelper '{helperName}'", readerContext); + + var pathInfo = bindingContext.Configuration.PathInfoStore.GetOrAdd(helperName); + var value = PathResolver.ResolvePath(bindingContext, ref pathInfo); DeferredSectionBlockHelper.Helper(bindingContext, helperPrefix, value, body, inverse, blockParamsValueProvider); } - catch(Exception e) + catch(Exception e) when(!(e is HandlebarsException)) { throw new HandlebarsRuntimeException($"Error occured while executing `{helperName}.`", e, readerContext); } diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 78bacd22..0ff68555 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -22,9 +22,10 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitHelperExpression(HelperExpression hex) { - var pathInfo = CompilationContext.Configuration.Paths.GetOrAdd(hex.HelperName); + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(hex.HelperName); if(!pathInfo.IsValidHelperLiteral) return Expression.Empty(); - + + var readerContext = Arg(hex.Context); var helperName = pathInfo.Segments[0].PathChain[0].TrimmedValue; var bindingContext = Arg(CompilationContext.BindingContext); var contextValue = bindingContext.Property(o => o.Value); @@ -60,7 +61,7 @@ protected override Expression VisitHelperExpression(HelperExpression hex) return Call(() => CaptureResult(textWriter, Call(() => - LateBindHelperExpression(bindingContext, helperName, args) + LateBindHelperExpression(bindingContext, helperName, args, (IReaderContext) readerContext) )) ); } @@ -96,7 +97,8 @@ public static ResultHolder TryLateBindHelperExpression(BindingContext context, s return new ResultHolder(false, null); } - private static object LateBindHelperExpression(BindingContext context, string helperName, object[] arguments) + private static object LateBindHelperExpression(BindingContext context, string helperName, object[] arguments, + IReaderContext readerContext) { var result = TryLateBindHelperExpression(context, helperName, arguments); if (result.Success) @@ -104,7 +106,7 @@ private static object LateBindHelperExpression(BindingContext context, string he return result.Value; } - throw new HandlebarsRuntimeException($"Template references a helper that is not registered. Could not find helper '{helperName}'"); + throw new HandlebarsRuntimeException($"Template references a helper that cannot be resolved. Helper '{helperName}'", readerContext); } private static object CaptureResult(TextWriter writer, object result) diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index 1ff78ab1..22ab9773 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -9,6 +9,7 @@ using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.Polyfills; using HandlebarsDotNet.ValueProviders; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -23,24 +24,29 @@ public IteratorBinder(CompilationContext compilationContext) protected override Expression VisitIteratorExpression(IteratorExpression iex) { - var context = ExpressionShortcuts.Arg(CompilationContext.BindingContext); - var sequence = ExpressionShortcuts.Var("sequence"); + var context = Arg(CompilationContext.BindingContext); + var sequence = Var("sequence"); var template = FunctionBuilder.CompileCore(new[] {iex.Template}, CompilationContext.Configuration); var ifEmpty = FunctionBuilder.CompileCore(new[] {iex.IfEmpty}, CompilationContext.Configuration); + + if (iex.Sequence is PathExpression pathExpression) + { + pathExpression.Context = PathExpression.ResolutionContext.Parameter; + } - 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(); + var compiledSequence = Arg(FunctionBuilder.Reduce(iex.Sequence, CompilationContext)); + var blockParams = Arg(iex.BlockParams); + var blockParamsProvider = Call(() => BlockParamsValueProvider.Create(context, blockParams)); + var blockParamsProviderVar = Var(); - return ExpressionShortcuts.Block() + return Block() .Parameter(sequence, compiledSequence) .Parameter(blockParamsProviderVar, blockParamsProvider) .Line(blockParamsProviderVar.Using((self, builder) => { builder - .Line(ExpressionShortcuts.Call(() => + .Line(Call(() => Iterator.Iterate(context, self, sequence, template, ifEmpty) )); })); @@ -114,7 +120,7 @@ private static void IterateObject(BindingContext context, { using(var iterator = ObjectEnumeratorValueProvider.Create(context.Configuration)) { - blockParamsValueProvider?.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); + blockParamsValueProvider.Configure(BlockParamsObjectEnumeratorConfiguration, iterator); iterator.Index = 0; var accessor = descriptor.MemberAccessor; @@ -192,7 +198,7 @@ private static void IterateList(BindingContext context, { using (var iterator = IteratorValueProvider.Create()) { - blockParamsValueProvider?.Configure(BlockParamsEnumerableConfiguration, iterator); + blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); var count = target.Count; for (iterator.Index = 0; iterator.Index < count; iterator.Index++) @@ -224,7 +230,7 @@ private static void IterateEnumerable(BindingContext context, { using (var iterator = IteratorValueProvider.Create()) { - blockParamsValueProvider?.Configure(BlockParamsEnumerableConfiguration, iterator); + blockParamsValueProvider.Configure(BlockParamsEnumerableConfiguration, iterator); iterator.Index = 0; var enumerable = new ExtendedEnumerable(target); diff --git a/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs b/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs deleted file mode 100644 index cc4a6cf3..00000000 --- a/source/Handlebars/Compiler/Translation/Expression/LambdaReducer.cs +++ /dev/null @@ -1,35 +0,0 @@ -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/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 37c9f373..9d1ed075 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -27,11 +27,11 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitPathExpression(PathExpression pex) { var context = Arg(CompilationContext.BindingContext); - var pathInfo = CompilationContext.Configuration.Paths.GetOrAdd(pex.Path); + var pathInfo = CompilationContext.Configuration.PathInfoStore.GetOrAdd(pex.Path); var resolvePath = Call(() => PathResolver.ResolvePath(context, ref pathInfo)); - if (!pathInfo.IsValidHelperLiteral) return resolvePath; + if (!pathInfo.IsValidHelperLiteral || pathInfo.IsThis) return resolvePath; var helperName = pathInfo.Segments[0].PathChain[0].TrimmedValue; var tryBoundHelper = Call(() => diff --git a/source/Handlebars/Configuration/CompileTimeConfiguration.cs b/source/Handlebars/Configuration/CompileTimeConfiguration.cs index 6e0e32c6..ae82743c 100644 --- a/source/Handlebars/Configuration/CompileTimeConfiguration.cs +++ b/source/Handlebars/Configuration/CompileTimeConfiguration.cs @@ -22,7 +22,8 @@ public class CompileTimeConfiguration { new BuildInHelpersFeatureFactory(), new ClosureFeatureFactory(), - new DefaultCompilerFeatureFactory() + new DefaultCompilerFeatureFactory(), + new MissingHelperFeatureFactory() }; /// diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs index 7bded712..c9dc0dd9 100644 --- a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -100,5 +100,8 @@ public interface ICompiledHandlebarsConfiguration /// List of associated s /// IReadOnlyList Features { get; } + + /// + IReadOnlyPathInfoStore PathInfoStore { get; } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs index 2cd3e93c..5e766ab4 100644 --- a/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/InternalHandlebarsConfiguration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using HandlebarsDotNet.Collections; using HandlebarsDotNet.Compiler.Resolvers; using HandlebarsDotNet.Features; @@ -10,7 +11,7 @@ namespace HandlebarsDotNet { - internal class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompiledHandlebarsConfiguration + internal sealed class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompiledHandlebarsConfiguration { private readonly HandlebarsConfiguration _configuration; @@ -23,11 +24,10 @@ internal class InternalHandlebarsConfiguration : HandlebarsConfiguration, ICompi 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 override CompileTimeConfiguration CompileTimeConfiguration { get; } public List Features { get; } public IObjectDescriptorProvider ObjectDescriptorProvider { get; } - public ICollection ExpressionMiddleware => CompileTimeConfiguration.ExpressionMiddleware; public ICollection AliasProviders => CompileTimeConfiguration.AliasProviders; public IExpressionCompiler ExpressionCompiler @@ -43,8 +43,10 @@ public bool UseAggressiveCaching } IReadOnlyList ICompiledHandlebarsConfiguration.Features => Features; - public PathStore Paths { get; } - + public PathInfoStore PathInfoStore { get; } + + IReadOnlyPathInfoStore ICompiledHandlebarsConfiguration.PathInfoStore => PathInfoStore; + internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) { _configuration = configuration; @@ -54,7 +56,7 @@ internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) BlockHelpers = new CascadeDictionary(configuration.BlockHelpers, StringComparer.OrdinalIgnoreCase); RegisteredTemplates = new CascadeDictionary>(configuration.RegisteredTemplates, StringComparer.OrdinalIgnoreCase); HelperResolvers = new CascadeCollection(configuration.HelperResolvers); - Paths = new PathStore(); + PathInfoStore = new PathInfoStore(); CompileTimeConfiguration = new CompileTimeConfiguration { @@ -85,7 +87,10 @@ internal InternalHandlebarsConfiguration(HandlebarsConfiguration configuration) ObjectDescriptorProvider = new ObjectDescriptorFactory(providers); - Features = CompileTimeConfiguration.Features.Select(o => o.CreateFeature()).ToList(); + Features = CompileTimeConfiguration + .Features.Select(o => o.CreateFeature()) + .OrderBy(o => o.GetType().GetTypeInfo().GetCustomAttribute()?.Order ?? 100) + .ToList(); } } } \ No newline at end of file diff --git a/source/Handlebars/Configuration/PathInfoStore.cs b/source/Handlebars/Configuration/PathInfoStore.cs new file mode 100644 index 00000000..ce2aeed0 --- /dev/null +++ b/source/Handlebars/Configuration/PathInfoStore.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using HandlebarsDotNet.Compiler.Structure.Path; + +namespace HandlebarsDotNet +{ + /// + /// Provides access to path expressions in the template + /// + public interface IReadOnlyPathInfoStore : IReadOnlyDictionary + { + } + + internal class PathInfoStore : IReadOnlyPathInfoStore + { + private readonly Dictionary _paths = new Dictionary(); + + public PathInfo GetOrAdd(string path) + { + if (_paths.TryGetValue(path, out var pathInfo)) return pathInfo; + + pathInfo = PathResolver.GetPathInfo(path); + _paths.Add(path, pathInfo); + + return pathInfo; + } + + public IEnumerator> GetEnumerator() => _paths.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _paths.GetEnumerator(); + + int IReadOnlyCollection>.Count => _paths.Count; + + bool IReadOnlyDictionary.ContainsKey(string key) => _paths.ContainsKey(key); + + public bool TryGetValue(string key, out PathInfo value) => _paths.TryGetValue(key, out value); + + public PathInfo this[string key] => _paths[key]; + + public IEnumerable Keys => _paths.Keys; + + public IEnumerable Values => _paths.Values; + } +} \ No newline at end of file diff --git a/source/Handlebars/Configuration/PathStore.cs b/source/Handlebars/Configuration/PathStore.cs deleted file mode 100644 index 87f6292a..00000000 --- a/source/Handlebars/Configuration/PathStore.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using HandlebarsDotNet.Compiler.Structure.Path; - -namespace HandlebarsDotNet -{ - internal class PathStore - { - private readonly Dictionary _paths = new Dictionary(); - - public PathInfo GetOrAdd(string path) - { - if (_paths.TryGetValue(path, out var pathInfo)) return pathInfo; - - pathInfo = PathResolver.GetPathInfo(path); - _paths.Add(path, pathInfo); - - return pathInfo; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/Features/BuildInHelpersFeature.cs b/source/Handlebars/Features/BuildInHelpersFeature.cs index 91f158ac..15e8fa2f 100644 --- a/source/Handlebars/Features/BuildInHelpersFeature.cs +++ b/source/Handlebars/Features/BuildInHelpersFeature.cs @@ -13,6 +13,7 @@ public IFeature CreateFeature() } } + [FeatureOrder(int.MinValue)] internal class BuildInHelpersFeature : IFeature { private ICompiledHandlebarsConfiguration _configuration; @@ -30,7 +31,6 @@ 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/Features/ClosureFeature.cs b/source/Handlebars/Features/ClosureFeature.cs index 2dc9e189..419f6d02 100644 --- a/source/Handlebars/Features/ClosureFeature.cs +++ b/source/Handlebars/Features/ClosureFeature.cs @@ -17,6 +17,7 @@ public IFeature CreateFeature() /// /// Extracts all items into precompiled closure allowing to compile static delegates /// + [FeatureOrder(0)] public class ClosureFeature : IFeature { /// diff --git a/source/Handlebars/Features/DefaultCompilerFeature.cs b/source/Handlebars/Features/DefaultCompilerFeature.cs index de738679..fde35512 100644 --- a/source/Handlebars/Features/DefaultCompilerFeature.cs +++ b/source/Handlebars/Features/DefaultCompilerFeature.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using Expressions.Shortcuts; +using static Expressions.Shortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Features { @@ -13,6 +14,7 @@ public IFeature CreateFeature() } } + [FeatureOrder(1)] internal class DefaultCompilerFeature : IFeature { public void OnCompiling(ICompiledHandlebarsConfiguration configuration) @@ -59,11 +61,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 = (Expression) Expression.Field(Expression.Constant(_templateClosure), nameof(TemplateClosure.Store)); - var outerLambda = Expression.Lambda( - Expression.Invoke(Expression.Constant(compiledLambda), new[] {store}.Concat(outerParameters)), - outerParameters); + var store = Arg(_templateClosure).Member(o => o.Store); + var parameterExpressions = new[] { store.Expression }.Concat(outerParameters); + var invocationExpression = Expression.Invoke(Expression.Constant(compiledLambda), parameterExpressions); + var outerLambda = Expression.Lambda(invocationExpression, outerParameters); return outerLambda.Compile(); } diff --git a/source/Handlebars/Features/FeatureOrderAttribute.cs b/source/Handlebars/Features/FeatureOrderAttribute.cs new file mode 100644 index 00000000..47d0e8f3 --- /dev/null +++ b/source/Handlebars/Features/FeatureOrderAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace HandlebarsDotNet.Features +{ + internal class FeatureOrderAttribute : Attribute + { + public int Order { get; } + + public FeatureOrderAttribute(int order) + { + Order = order; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/MissingHelperFeature.cs b/source/Handlebars/Features/MissingHelperFeature.cs new file mode 100644 index 00000000..894d8a12 --- /dev/null +++ b/source/Handlebars/Features/MissingHelperFeature.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using HandlebarsDotNet.Adapters; +using HandlebarsDotNet.Compiler; +using HandlebarsDotNet.Compiler.Structure.Path; +using HandlebarsDotNet.Helpers; + +namespace HandlebarsDotNet.Features +{ + /// + /// + /// + public static class MissingHelperFeatureExtension + { + /// + /// Allows to intercept calls to missing helpers. + /// For Handlebarsjs docs see: https://handlebarsjs.com/guide/hooks.html#helpermissing + /// + /// + /// Delegate that returns interceptor for and + /// Delegate that returns interceptor for + /// + public static HandlebarsConfiguration RegisterMissingHelperHook( + this HandlebarsConfiguration configuration, + HandlebarsReturnHelper helperMissing = null, + HandlebarsBlockHelper blockHelperMissing = null + ) + { + var feature = new MissingHelperFeatureFactory(helperMissing, blockHelperMissing); + configuration.CompileTimeConfiguration.Features.Add(feature); + + return configuration; + } + } + + internal class MissingHelperFeatureFactory : IFeatureFactory + { + private readonly HandlebarsReturnHelper _returnHelper; + private readonly HandlebarsBlockHelper _blockHelper; + + public MissingHelperFeatureFactory( + HandlebarsReturnHelper returnHelper = null, + HandlebarsBlockHelper blockHelper = null + ) + { + _returnHelper = returnHelper; + _blockHelper = blockHelper; + } + + public IFeature CreateFeature() => new MissingHelperFeature(_returnHelper, _blockHelper); + } + + [FeatureOrder(int.MaxValue)] + internal class MissingHelperFeature : IFeature, IHelperResolver + { + private ICompiledHandlebarsConfiguration _configuration; + private HandlebarsReturnHelper _returnHelper; + private HandlebarsBlockHelper _blockHelper; + + public MissingHelperFeature( + HandlebarsReturnHelper returnHelper, + HandlebarsBlockHelper blockHelper + ) + { + _returnHelper = returnHelper; + _blockHelper = blockHelper; + } + + public void OnCompiling(ICompiledHandlebarsConfiguration configuration) => _configuration = configuration; + + public void CompilationCompleted() + { + var existingFeatureRegistrations = _configuration + .HelperResolvers + .OfType() + .ToList(); + + if (existingFeatureRegistrations.Any()) + { + existingFeatureRegistrations.ForEach(o => _configuration.HelperResolvers.Remove(o)); + } + + ResolveHelpersRegistrations(); + + _configuration.HelperResolvers.Add(this); + } + + public bool TryResolveReturnHelper(string name, Type targetType, out HandlebarsReturnHelper helper) + { + if (_returnHelper == null) + { + _configuration.ReturnHelpers.TryGetValue("helperMissing", out _returnHelper); + } + + if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var simpleHelper)) + { + _returnHelper = new HelperToReturnHelperAdapter(simpleHelper); + } + + if (_returnHelper == null) + { + helper = null; + return false; + } + + helper = (context, arguments) => + { + var instance = (object) context; + var chainSegment = new ChainSegment(name); + if (PathResolver.TryAccessMember(instance, ref chainSegment, _configuration, out var value)) + return value; + + var newArguments = new object[arguments.Length + 1]; + Array.Copy(arguments, newArguments, arguments.Length); + newArguments[arguments.Length] = name; + + return _returnHelper(context, newArguments); + }; + + return true; + } + + public bool TryResolveBlockHelper(string name, out HandlebarsBlockHelper helper) + { + if (_blockHelper == null) + { + _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out _blockHelper); + } + + if (_blockHelper == null) + { + helper = null; + return false; + } + + helper = (output, options, context, arguments) => + { + options["name"] = name; + _blockHelper(output, options, context, arguments); + }; + + return true; + } + + private void ResolveHelpersRegistrations() + { + if (_returnHelper == null && _configuration.Helpers.TryGetValue("helperMissing", out var helperMissing)) + { + _returnHelper = new HelperToReturnHelperAdapter(helperMissing); + } + + if (_returnHelper == null && + _configuration.ReturnHelpers.TryGetValue("helperMissing", out var returnHelperMissing)) + { + _returnHelper = returnHelperMissing; + } + + if (_blockHelper == null && + _configuration.BlockHelpers.TryGetValue("blockHelperMissing", out var blockHelperMissing)) + { + _blockHelper = blockHelperMissing; + } + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Features/TemplateClosure.cs b/source/Handlebars/Features/TemplateClosure.cs index 882573c3..430b0e80 100644 --- a/source/Handlebars/Features/TemplateClosure.cs +++ b/source/Handlebars/Features/TemplateClosure.cs @@ -10,24 +10,15 @@ public sealed class TemplateClosure { private Dictionary _objectSet = new Dictionary(); private List _inner = new List(); - + /// /// Actual closure storage /// - public object[] Store = new object[0]; + public object[] Store { get; private set; } - /// - /// Index for the next item reference - /// - public int CurrentIndex => _inner?.Count ?? -1; - - //public object[] Store => _store; + internal int CurrentIndex => _inner?.Count ?? -1; - /// - /// Adds value to store - /// - /// - public object this[int key] + internal object this[int key] { set { @@ -36,14 +27,8 @@ public object this[int key] _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) + + internal bool TryGetKeyByValue(object obj, out int key) { key = -1; if (obj != null) return _objectSet?.TryGetValue(obj, out key) ?? false; @@ -55,7 +40,7 @@ internal void Build() { if(_inner == null) return; - Array.Resize(ref Store, _inner.Count); + Store = new object[_inner.Count]; _inner.CopyTo(Store, 0); _inner.Clear(); diff --git a/source/Handlebars/Features/WarmUpFeature.cs b/source/Handlebars/Features/WarmUpFeature.cs index 850727ba..e1e9e5c8 100644 --- a/source/Handlebars/Features/WarmUpFeature.cs +++ b/source/Handlebars/Features/WarmUpFeature.cs @@ -4,22 +4,31 @@ namespace HandlebarsDotNet.Features { /// - /// Allows to warm-up internal caches for specific types + /// /// - public class WarmUpFeature : IFeature + public static class WarmUpFeatureExtensions { - private readonly HashSet _types; - /// - /// + /// Allows to warm-up internal caches for specific types /// - /// - public WarmUpFeature(HashSet types) + public static HandlebarsConfiguration UseWarmUp(this HandlebarsConfiguration configuration, Action> configure) { - _types = types; + var types = new HashSet(); + + configure(types); + + configuration.CompileTimeConfiguration.Features.Add(new WarmUpFeatureFactory(types)); + + return configuration; } + } + + internal class WarmUpFeature : IFeature + { + private readonly HashSet _types; + + public WarmUpFeature(HashSet types) => _types = types; - /// public void OnCompiling(ICompiledHandlebarsConfiguration configuration) { var descriptorProvider = configuration.ObjectDescriptorProvider; @@ -29,8 +38,7 @@ public void OnCompiling(ICompiledHandlebarsConfiguration configuration) descriptorProvider.TryGetDescriptor(type, out _); } } - - /// + public void CompilationCompleted() { // noting to do here @@ -41,34 +49,8 @@ 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(); + public WarmUpFeatureFactory(HashSet types) => _types = types; - configure(types); - - configuration.CompileTimeConfiguration.Features.Add(new WarmUpFeatureFactory(types)); - - return configuration; - } + public IFeature CreateFeature() => new WarmUpFeature(_types); } } \ No newline at end of file diff --git a/source/Handlebars/HandlebarsException.cs b/source/Handlebars/HandlebarsException.cs index 1f5f613f..55e237a0 100644 --- a/source/Handlebars/HandlebarsException.cs +++ b/source/Handlebars/HandlebarsException.cs @@ -51,7 +51,7 @@ private static string FormatMessage(string message, IReaderContext context) { if (context == null) return message; - return $"{message}\nOccured at:{context.LineNumber}:{context.CharNumber}"; + return $"{message}\n\nOccured at: {context.LineNumber}:{context.CharNumber}"; } } } diff --git a/source/Handlebars/HelperOptions.cs b/source/Handlebars/HelperOptions.cs index d34b167a..f786f6f8 100644 --- a/source/Handlebars/HelperOptions.cs +++ b/source/Handlebars/HelperOptions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using HandlebarsDotNet.Compiler; @@ -12,18 +14,30 @@ namespace HandlebarsDotNet /// /// Contains properties accessible withing function /// - public sealed class HelperOptions + public sealed class HelperOptions : IReadOnlyDictionary { + private readonly Dictionary _extensions; + internal HelperOptions( Action template, Action inverse, BlockParamsValueProvider blockParamsValueProvider, - HandlebarsConfiguration configuration) + InternalHandlebarsConfiguration configuration, + BindingContext bindingContext) { Template = template; Inverse = inverse; - Configuration = configuration; BlockParams = blockParamsValueProvider.Configure; + + BindingContext = bindingContext; + Configuration = configuration; + + _extensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [nameof(Template)] = Template, + [nameof(Inverse)] = Inverse, + [nameof(BlockParams)] = BlockParams + }; } /// @@ -40,7 +54,56 @@ internal HelperOptions( public BlockParamsConfiguration BlockParams { get; } /// - internal HandlebarsConfiguration Configuration { get; } + internal InternalHandlebarsConfiguration Configuration { get; } + + internal BindingContext BindingContext { get; } + + bool IReadOnlyDictionary.ContainsKey(string key) + { + return _extensions.ContainsKey(key); + } + + bool IReadOnlyDictionary.TryGetValue(string key, out object value) + { + return _extensions.TryGetValue(key, out value); + } + + /// + /// Provides access to dynamic data entries + /// + /// + public object this[string property] + { + get => _extensions.TryGetValue(property, out var value) ? value : null; + internal set => _extensions[property] = value; + } + + /// + /// Provides access to dynamic data entries in a typed manner + /// + /// + /// + /// + public T GetValue(string property) + { + return (T) this[property]; + } + + IEnumerable IReadOnlyDictionary.Keys => _extensions.Keys; + + IEnumerable IReadOnlyDictionary.Values => _extensions.Values; + + IEnumerator> IEnumerable>.GetEnumerator() + { + return _extensions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) _extensions).GetEnumerator(); + } + + int IReadOnlyCollection>.Count => _extensions.Count; } } diff --git a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs index 2e369d49..518bc99c 100644 --- a/source/Handlebars/ValueProviders/BindingContextValueProvider.cs +++ b/source/Handlebars/ValueProviders/BindingContextValueProvider.cs @@ -41,7 +41,7 @@ private bool TryGetContextVariable(object instance, ref ChainSegment segment, ou if( descriptorProvider.CanHandleType(instanceType) && descriptorProvider.TryGetDescriptor(instanceType, out var descriptor) && - descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment.Value, out value) + descriptor.MemberAccessor.TryGetValue(instance, instanceType, segment, out value) ) { return true;