From 072e5caa788e6e85d21e50aea5ff4984f77aa751 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 4 Jan 2014 11:05:57 -0600 Subject: [PATCH] Include name option for all helper calls All helper calls will have access to `options.name` which is the first id value of the mustache operation. As part of this the helperMissing call has been simplified to remove the indexed name in order to optimize the call. This is a breaking change. Fixes #634 --- lib/handlebars/base.js | 8 ++-- lib/handlebars/compiler/compiler.js | 2 +- .../compiler/javascript-compiler.js | 26 +++++------ spec/helpers.js | 43 +++++++++++++++++-- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 00c8bf1ff..059b1c3fb 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -49,11 +49,13 @@ HandlebarsEnvironment.prototype = { }; function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { + instance.registerHelper('helperMissing', function(/* [args, ]options */) { + if(arguments.length === 1) { + // A missing field in a {{foo}} constuct. return undefined; } else { - throw new Exception("Missing helper: '" + arg + "'"); + // Someone is actually trying to call something, blow up. + throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); } }); diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 8389f8482..97a5a973c 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -169,7 +169,7 @@ Compiler.prototype = { this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); this.opcode('emptyHash'); - this.opcode('blockValue'); + this.opcode('blockValue', sexpr.id.original); } else { this.ambiguousSexpr(sexpr, program, inverse); diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index cdf16935e..1e8959d66 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -223,11 +223,11 @@ JavaScriptCompiler.prototype = { // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and // replace it on the stack with the result of properly // invoking blockHelperMissing. - blockValue: function() { + blockValue: function(name) { this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; var params = ["depth0"]; - this.setupParams(0, params); + this.setupParams(name, 0, params); this.replaceStack(function(current) { params.splice(1, 0, current); @@ -246,7 +246,7 @@ JavaScriptCompiler.prototype = { // We're being a bit cheeky and reusing the options value from the prior exec var params = ["depth0"]; - this.setupParams(0, params, true); + this.setupParams('', 0, params, true); this.flushInline(); @@ -503,20 +503,15 @@ JavaScriptCompiler.prototype = { this.context.aliases.helperMissing = 'helpers.helperMissing'; this.useRegister('helper'); - var helper = this.lastHelper = this.setupHelper(paramSize, name, true); + var helper = this.lastHelper = this.setupHelper(paramSize, name); var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); - var lookup = 'helper = ' + helper.name + ' || ' + nonHelper; + var lookup = 'helper = ' + helper.name + ' || ' + nonHelper + ' || helperMissing'; if (helper.paramsInit) { lookup += ',' + helper.paramsInit; } - this.push( - '(' - + lookup - + ',helper ' - + '? helper.call(' + helper.callParams + ') ' - + ': helperMissing.call(' + helper.helperMissingParams + '))'); + this.push('(' + lookup + ',helper.call(' + helper.callParams + '))'); // Always flush subexpressions. This is both to prevent the compounding size issue that // occurs when the code has to be duplicated for inlining and also to prevent errors @@ -825,7 +820,7 @@ JavaScriptCompiler.prototype = { setupHelper: function(paramSize, name, missingParams) { var params = [], - paramsInit = this.setupParams(paramSize, params, missingParams); + paramsInit = this.setupParams(name, paramSize, params, missingParams); var foundHelper = this.nameLookup('helpers', name, 'helper'); return { @@ -837,9 +832,10 @@ JavaScriptCompiler.prototype = { }; }, - setupOptions: function(paramSize, params) { + setupOptions: function(helper, paramSize, params) { var options = [], contexts = [], types = [], param, inverse, program; + options.push("name:" + this.quotedString(helper)); options.push("hash:" + this.popStack()); if (this.options.stringParams) { @@ -891,8 +887,8 @@ JavaScriptCompiler.prototype = { // the params and contexts arguments are passed in arrays // to fill in - setupParams: function(paramSize, params, useRegister) { - var options = '{' + this.setupOptions(paramSize, params).join(',') + '}'; + setupParams: function(helperName, paramSize, params, useRegister) { + var options = '{' + this.setupOptions(helperName, paramSize, params).join(',') + '}'; if (useRegister) { this.useRegister('options'); diff --git a/spec/helpers.js b/spec/helpers.js index b0eb91e65..180fc9809 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -362,9 +362,9 @@ describe('helpers', function() { var context = { hello: "Hello", world: "world" }; var helpers = { - helperMissing: function(helper, context) { - if(helper === "link_to") { - return new Handlebars.SafeString("" + context + ""); + helperMissing: function(mesg, options) { + if(options.name === "link_to") { + return new Handlebars.SafeString("" + mesg + ""); } } }; @@ -436,6 +436,43 @@ describe('helpers', function() { }); }); + describe('name field', function() { + var context = {}; + var helpers = { + blockHelperMissing: function() { + return 'missing: ' + arguments[arguments.length-1].name; + }, + helper: function() { + return 'ran: ' + arguments[arguments.length-1].name; + } + }; + + it('should include in ambiguous mustache calls', function() { + shouldCompileTo('{{helper}}', [context, helpers], 'ran: helper'); + }); + it('should include in helper mustache calls', function() { + shouldCompileTo('{{helper 1}}', [context, helpers], 'ran: helper'); + }); + it('should include in ambiguous block calls', function() { + shouldCompileTo('{{#helper}}{{/helper}}', [context, helpers], 'ran: helper'); + }); + it('should include in simple block calls', function() { + shouldCompileTo('{{#./helper}}{{/./helper}}', [context, helpers], 'missing: ./helper'); + }); + it('should include in helper block calls', function() { + shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper'); + }); + it('should include in known helper calls', function() { + var template = CompilerContext.compile("{{helper}}", {knownHelpers: {'helper': true}, knownHelpersOnly: true}); + + equal(template({}, {helpers: helpers}), 'ran: helper'); + }); + + it('should include full id', function() { + shouldCompileTo('{{#foo.helper}}{{/foo.helper}}', [{foo: {}}, helpers], 'missing: foo.helper'); + }); + }); + describe('name conflicts', function() { it("helpers take precedence over same-named context properties", function() { var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");