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 b428c5fc5..3c4a9b790 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -170,7 +170,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 213672497..d11275d1d 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -224,11 +224,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); @@ -247,7 +247,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(); @@ -504,20 +504,15 @@ JavaScriptCompiler.prototype = { this.context.aliases.helperMissing = 'helpers.helperMissing'; this.useRegister('helper'); - var helper = this.lastHelper = this.setupHelper(paramSize, name, true); + var helper = 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 @@ -824,23 +819,23 @@ JavaScriptCompiler.prototype = { .replace(/\u2029/g, '\\u2029') + '"'; }, - setupHelper: function(paramSize, name, missingParams) { + setupHelper: function(paramSize, name, blockHelper) { var params = [], - paramsInit = this.setupParams(paramSize, params, missingParams); + paramsInit = this.setupParams(name, paramSize, params, blockHelper); var foundHelper = this.nameLookup('helpers', name, 'helper'); return { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") + callParams: ["depth0"].concat(params).join(", ") }; }, - 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.stringParams) { @@ -892,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}}");