diff --git a/source/Handlebars.Test/ComplexIntegrationTests.cs b/source/Handlebars.Test/ComplexIntegrationTests.cs index 19554726..d168838a 100644 --- a/source/Handlebars.Test/ComplexIntegrationTests.cs +++ b/source/Handlebars.Test/ComplexIntegrationTests.cs @@ -90,6 +90,36 @@ public void IfImplicitIteratorHelper() Assert.Equal("GoogleYahoo!", result); } + // the helper has priority + // https://handlebarsjs.com/guide/expressions.html#disambiguating-helpers-calls-and-property-lookup + [Fact] + public void HelperWithSameNameVariable() + { + Handlebars.RegisterHelper("foo", (writer, context, arguments) => + { + writer.Write("Helper"); + }); + + var template = Handlebars.Compile("{{foo}}"); + var result = template(new { foo = "Variable" }); + Assert.Equal("Helper", result); + } + + [Fact] + public void LateBoundHelperWithSameNameVariable() + { + var template = Handlebars.Compile("{{amoeba}}"); + + Assert.Equal("Variable", template(new { amoeba = "Variable" })); + + Handlebars.RegisterHelper("amoeba", (writer, context, arguments) => + { + writer.Write("Helper"); + }); + + Assert.Equal("Helper", template(new { amoeba = "Variable" })); + } + [Fact] public void BlockHelperWithSameNameVariable() { diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index 1205ea9e..4bca8b1c 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,8 +1,8 @@ -using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Collections.Generic; +using System.IO; namespace HandlebarsDotNet.Compiler { @@ -32,27 +32,31 @@ protected override Expression VisitStatementExpression(StatementExpression sex) protected override Expression VisitHelperExpression(HelperExpression hex) { - if (CompilationContext.Configuration.Helpers.ContainsKey(hex.HelperName)) + var arguments = new Expression[] { - var helper = CompilationContext.Configuration.Helpers[hex.HelperName]; - var arguments = new Expression[] - { - Expression.Property( - CompilationContext.BindingContext, + Expression.Property( + CompilationContext.BindingContext, #if netstandard - typeof(BindingContext).GetRuntimeProperty("TextWriter")), + typeof(BindingContext).GetRuntimeProperty("TextWriter") #else - typeof(BindingContext).GetProperty("TextWriter")), + typeof(BindingContext).GetProperty("TextWriter") #endif - Expression.Property( - CompilationContext.BindingContext, + ), + Expression.Property( + CompilationContext.BindingContext, #if netstandard - typeof(BindingContext).GetRuntimeProperty("Value")), + typeof(BindingContext).GetRuntimeProperty("Value") #else - typeof(BindingContext).GetProperty("Value")), + typeof(BindingContext).GetProperty("Value") #endif - Expression.NewArrayInit(typeof(object), hex.Arguments.Select(a => Visit(a))) - }; + ), + Expression.Constant(hex.HelperName), + Expression.NewArrayInit(typeof(object), hex.Arguments.Select(a => Visit(a))) + }; + + if (CompilationContext.Configuration.Helpers.ContainsKey(hex.HelperName)) + { + var helper = GetHelperWithName(CompilationContext.Configuration.Helpers[hex.HelperName]); if (helper.Target != null) { return Expression.Call( @@ -80,30 +84,43 @@ protected override Expression VisitHelperExpression(HelperExpression hex) return Expression.Call( Expression.Constant(this), #if netstandard - new Action>(LateBindHelperExpression).GetMethodInfo(), + new HandlebarsHelperWithName(InvokeLateBindHelper).GetMethodInfo(), #else - new Action>(LateBindHelperExpression).Method, + new HandlebarsHelperWithName(InvokeLateBindHelper).Method, #endif - CompilationContext.BindingContext, - Expression.Constant(hex.HelperName), - Expression.NewArrayInit(typeof(object), hex.Arguments)); + arguments); } } - private void LateBindHelperExpression( - BindingContext context, + private HandlebarsHelperWithName GetHelperWithName(HandlebarsHelper helper) + => (writer, context, name, arguments) => helper(writer, context, arguments); + + private void InvokeLateBindHelper( + TextWriter writer, + dynamic bindingContext, + string helperName, + IEnumerable arguments) + { + if (!TryInvokeLateBoundHelper(CompilationContext, writer, bindingContext, helperName, arguments)) + throw new HandlebarsRuntimeException(string.Format( + "Template references a helper that is not registered. Could not find helper '{0}'", helperName)); + } + + public static bool TryInvokeLateBoundHelper( + CompilationContext compilationContext, + TextWriter writer, + dynamic bindingContext, string helperName, IEnumerable arguments) { - if (CompilationContext.Configuration.Helpers.ContainsKey(helperName)) + if (compilationContext.Configuration.Helpers.ContainsKey(helperName)) { - var helper = CompilationContext.Configuration.Helpers[helperName]; - helper(context.TextWriter, context.Value, arguments.ToArray()); - } - else - { - throw new HandlebarsRuntimeException(string.Format("Template references a helper that is not registered. Could not find helper '{0}'", helperName)); + var helper = compilationContext.Configuration.Helpers[helperName]; + helper(writer, bindingContext, arguments.ToArray()); + return true; } + + return false; } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 41a6c893..83a78bbf 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -27,16 +27,15 @@ protected override Expression VisitStatementExpression(StatementExpression sex) { if (sex.Body is PathExpression) { + return Expression.Call( + Expression.Constant(this), #if netstandard - var writeMethod = typeof(TextWriter).GetRuntimeMethod("Write", new [] { typeof(object) }); + new Action(WriteLateBoundHelperResultOrResolvedPath).GetMethodInfo(), #else - var writeMethod = typeof(TextWriter).GetMethod("Write", new[] { typeof(object) }); + new Action(WriteLateBoundHelperResultOrResolvedPath).Method, #endif - return Expression.Call( - Expression.Property( - CompilationContext.BindingContext, - "TextWriter"), - writeMethod, Visit(sex.Body)); + CompilationContext.BindingContext, + Expression.Constant(((PathExpression) sex.Body).Path)); } else { @@ -44,6 +43,18 @@ protected override Expression VisitStatementExpression(StatementExpression sex) } } + private void WriteLateBoundHelperResultOrResolvedPath(BindingContext bindingContext, string path) + { + var writer = bindingContext.TextWriter; + + if (HelperFunctionBinder.TryInvokeLateBoundHelper(CompilationContext, writer, bindingContext, path, new object[] { })) + { + return; + } + + writer.Write(ResolvePath(bindingContext, path)); + } + protected override Expression VisitPathExpression(PathExpression pex) { return Expression.Call( @@ -103,25 +114,19 @@ private object ResolvePath(BindingContext context, string path) } list[list.Count - 1] = list[list.Count - 1] + "." + next; - return list; } else { - if (next.StartsWith("[")) + if (next.StartsWith("[") && !next.EndsWith("]")) { insideEscapeBlock = true; } - if (next.EndsWith("]")) - { - insideEscapeBlock = false; - } - list.Add(next); - return list; } + return list; }); - + foreach (var memberName in pathChain) { instance = ResolveValue(context, instance, memberName); @@ -172,12 +177,12 @@ private object ResolveValue(BindingContext context, object instance, string segm } 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(); diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index b7e6ac88..3cc23c68 100755 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,8 +1,8 @@ using System; using System.Linq.Expressions; using System.IO; -using System.Text; using System.Reflection; +using System.Text; namespace HandlebarsDotNet.Compiler { @@ -25,22 +25,23 @@ protected override Expression VisitSubExpression(SubExpressionExpression subex) { throw new HandlebarsCompilerException("Sub-expression does not contain a converted MethodCall expression"); } - HandlebarsHelper helper = GetHelperDelegateFromMethodCallExpression(helperCall); + HandlebarsHelperWithName helper = GetHelperDelegateFromMethodCallExpression(helperCall); return Expression.Call( #if netstandard - new Func(CaptureTextWriterOutputFromHelper).GetMethodInfo(), + new Func(CaptureTextWriterOutputFromHelper).GetMethodInfo(), #else - new Func(CaptureTextWriterOutputFromHelper).Method, + new Func(CaptureTextWriterOutputFromHelper).Method, #endif Expression.Constant(helper), Visit(helperCall.Arguments[1]), - Visit(helperCall.Arguments[2])); + Visit(helperCall.Arguments[2]), + Visit(helperCall.Arguments[3])); } - private static HandlebarsHelper GetHelperDelegateFromMethodCallExpression(MethodCallExpression helperCall) + private static HandlebarsHelperWithName GetHelperDelegateFromMethodCallExpression(MethodCallExpression helperCall) { object target = helperCall.Object; - HandlebarsHelper helper; + HandlebarsHelperWithName helper; if (target != null) { if (target is ConstantExpression) @@ -52,31 +53,32 @@ private static HandlebarsHelper GetHelperDelegateFromMethodCallExpression(Method throw new NotSupportedException("Helper method instance target must be reduced to a ConstantExpression"); } #if netstandard - helper = (HandlebarsHelper)helperCall.Method.CreateDelegate(typeof(HandlebarsHelper), target); + helper = (HandlebarsHelperWithName)helperCall.Method.CreateDelegate(typeof(HandlebarsHelperWithName), target); #else - helper = (HandlebarsHelper)Delegate.CreateDelegate(typeof(HandlebarsHelper), target, helperCall.Method); + helper = (HandlebarsHelperWithName)Delegate.CreateDelegate(typeof(HandlebarsHelperWithName), target, helperCall.Method); #endif } else { #if netstandard - helper = (HandlebarsHelper)helperCall.Method.CreateDelegate(typeof(HandlebarsHelper)); + helper = (HandlebarsHelperWithName)helperCall.Method.CreateDelegate(typeof(HandlebarsHelperWithName)); #else - helper = (HandlebarsHelper)Delegate.CreateDelegate(typeof(HandlebarsHelper), helperCall.Method); + helper = (HandlebarsHelperWithName)Delegate.CreateDelegate(typeof(HandlebarsHelperWithName), helperCall.Method); #endif } return helper; } private static string CaptureTextWriterOutputFromHelper( - HandlebarsHelper helper, - object context, + HandlebarsHelperWithName helper, + dynamic context, + string name, object[] arguments) { var builder = new StringBuilder(); using (var writer = new StringWriter(builder)) { - helper(writer, context, arguments); + helper(writer, context, name, arguments); } return builder.ToString(); } diff --git a/source/Handlebars/Handlebars.cs b/source/Handlebars/Handlebars.cs index e59108ce..af0e0e9f 100644 --- a/source/Handlebars/Handlebars.cs +++ b/source/Handlebars/Handlebars.cs @@ -6,6 +6,9 @@ namespace HandlebarsDotNet public delegate void HandlebarsHelper(TextWriter output, dynamic context, params object[] arguments); public delegate void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments); + // a HandlebarsHelper that also stores the name of the helper for later use + internal delegate void HandlebarsHelperWithName(TextWriter output, dynamic context, string name, params object[] arguments); + public sealed partial 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