diff options
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 148 | ||||
-rw-r--r-- | spec/qunit_spec.js | 100 |
2 files changed, 144 insertions, 104 deletions
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 2cdc7c0..8fccc90 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -52,6 +52,11 @@ Handlebars.JavaScriptCompiler = function() {}; return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; }; + // the foundHelper register will disambiguate helper lookup from finding a + // function in a context. This is necessary for mustache compatibility, which + // requires that context functions in blocks are evaluated by blockHelperMissing, + // and then proceed as if the resulting value was provided to blockHelperMissing. + Compiler.prototype = { compiler: Compiler, @@ -314,6 +319,10 @@ Handlebars.JavaScriptCompiler = function() {}; } }; + var Literal = function(value) { + this.value = value; + }; + JavaScriptCompiler.prototype = { // PUBLIC API: You can override these methods in a subclass to provide // alternative compiled forms for name lookup and buffering semantics @@ -347,11 +356,13 @@ Handlebars.JavaScriptCompiler = function() {}; this.environment = environment; this.options = options || {}; + Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n"); + this.name = this.environment.name; this.isChild = !!context; this.context = context || { programs: [], - aliases: { self: 'this' }, + aliases: { }, registers: {list: []} }; @@ -359,6 +370,7 @@ Handlebars.JavaScriptCompiler = function() {}; this.stackSlot = 0; this.stackVars = []; + this.compileStack = []; this.compileChildren(environment, options); @@ -410,12 +422,6 @@ Handlebars.JavaScriptCompiler = function() {}; preamble: function() { var out = []; - // this register will disambiguate helper lookup from finding a function in - // a context. This is necessary for mustache compatibility, which requires - // that context functions in blocks are evaluated by blockHelperMissing, and - // then proceed as if the resulting value was provided to blockHelperMissing. - this.useRegister('foundHelper'); - if (!this.isChild) { var namespace = this.namespace; var copies = "helpers = helpers || " + namespace + ".helpers;"; @@ -517,41 +523,36 @@ Handlebars.JavaScriptCompiler = function() {}; lookupWithHelpers: function(name, isScoped) { if(name) { - var topStack = this.nextStack(); - this.usingKnownHelper = false; var toPush; if (!isScoped && this.options.knownHelpers[name]) { - toPush = topStack + " = " + this.nameLookup('helpers', name, 'helper'); this.usingKnownHelper = true; + this.pushStackLiteral(this.nameLookup('helpers', name, 'helper')); } else if (isScoped || this.options.knownHelpersOnly) { - toPush = topStack + " = " + this.nameLookup('depth' + this.lastContext, name, 'context'); + this.pushStackLiteral(this.nameLookup('depth' + this.lastContext, name, 'context')); } else { this.register('foundHelper', this.nameLookup('helpers', name, 'helper')); - toPush = topStack + " = foundHelper || " + this.nameLookup('depth' + this.lastContext, name, 'context'); + this.pushStack("foundHelper || " + this.nameLookup('depth' + this.lastContext, name, 'context')); } - - toPush += ';'; - this.source.push(toPush); } else { - this.pushStack('depth' + this.lastContext); + this.pushStackLiteral('depth' + this.lastContext); } }, lookup: function(name) { - var topStack = this.topStack(); - this.source.push(topStack + " = (" + topStack + " === null || " + topStack + " === undefined || " + topStack + " === false ? " + - topStack + " : " + this.nameLookup(topStack, name, 'context') + ");"); + this.replaceStack(function(current) { + return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context'); + }); }, pushStringParam: function(string) { - this.pushStack('depth' + this.lastContext); + this.pushStackLiteral('depth' + this.lastContext); this.pushString(string); }, pushString: function(string) { - this.pushStack(this.quotedString(string)); + this.pushStackLiteral(this.quotedString(string)); }, push: function(name) { @@ -559,7 +560,7 @@ Handlebars.JavaScriptCompiler = function() {}; }, invokeMustache: function(paramSize, original, hasHash) { - this.populateParams(paramSize, this.quotedString(original), "{}", null, hasHash, function(nextStack, helperMissingString, id) { + this.populateParams(paramSize, this.quotedString(original), null, null, hasHash, function(nextStack, helperMissingString, id) { if (!this.usingKnownHelper) { this.context.aliases.helperMissing = 'helpers.helperMissing'; this.context.aliases.undef = 'void 0'; @@ -583,25 +584,17 @@ Handlebars.JavaScriptCompiler = function() {}; }); }, - populateParams: function(paramSize, helperId, program, inverse, hasHash, fn) { + populateParams: function(paramSize, helperId, program, inverse, hasHash, callback) { var needsRegister = hasHash || this.options.stringParams || inverse || this.options.data; var id = this.popStack(), nextStack; var params = [], param, stringParam, stringOptions; - if (needsRegister) { - this.register('tmp1', program); - stringOptions = 'tmp1'; - } else { - stringOptions = '{ hash: {} }'; - } + var options = [], contexts = []; - if (needsRegister) { - var hash = (hasHash ? this.popStack() : '{}'); - this.source.push('tmp1.hash = ' + hash + ';'); - } - - if(this.options.stringParams) { - this.source.push('tmp1.contexts = [];'); + if (hasHash) { + options.push("hash:" + this.popStack()); + } else { + options.push("hash:{}"); } for(var i=0; i<paramSize; i++) { @@ -609,38 +602,48 @@ Handlebars.JavaScriptCompiler = function() {}; params.push(param); if(this.options.stringParams) { - this.source.push('tmp1.contexts.push(' + this.popStack() + ');'); + contexts.push(this.popStack()); } } - if(inverse) { - this.source.push('tmp1.fn = tmp1;'); - this.source.push('tmp1.inverse = ' + inverse + ';'); + if (this.options.stringParams) { + options.push("contexts:[" + contexts.join(",") + "]"); + } + + if (program) { + options.push("fn:" + program); + } + + if (inverse) { + options.push("inverse:" + inverse); } if(this.options.data) { - this.source.push('tmp1.data = data;'); + options.push("data:data"); } - params.push(stringOptions); + this.register("params", "{" + options.join(",") + "}"); - this.populateCall(params, id, helperId || id, fn, program !== '{}'); + params.push("params"); + + this.populateCall(params, id, helperId || id, callback, program); }, - populateCall: function(params, id, helperId, fn, program) { - var paramString = ["depth0"].concat(params).join(", "); + populateCall: function(params, id, helperId, callback, program) { + var paramString = ["depth0"].concat(params).join(", "), nextStack; var helperMissingString = ["depth0"].concat(helperId).concat(params).join(", "); - var nextStack = this.nextStack(); - if (this.usingKnownHelper) { - this.source.push(nextStack + " = " + id + ".call(" + paramString + ");"); + nextStack = this.pushStack(id + ".call(" + paramString + ")"); } else { + this.useRegister('foundHelper'); + + nextStack = this.nextStack(); this.context.aliases.functionType = '"function"'; var condition = program ? "foundHelper && " : ""; this.source.push("if(" + condition + "typeof " + id + " === functionType) { " + nextStack + " = " + id + ".call(" + paramString + "); }"); } - fn.call(this, nextStack, helperMissingString, id); + callback.call(this, nextStack, helperMissingString, id); this.usingKnownHelper = false; }, @@ -651,6 +654,7 @@ Handlebars.JavaScriptCompiler = function() {}; params.push("data"); } + this.context.aliases.self = "this"; this.pushStack("self.invokePartial(" + params.join(", ") + ");"); }, @@ -681,7 +685,11 @@ Handlebars.JavaScriptCompiler = function() {}; }, programExpression: function(guid) { - if(guid == null) { return "self.noop"; } + this.context.aliases.self = "this"; + + if(guid == null) { + return "self.noop"; + } var child = this.environment.children[guid], depths = child.depths.list, depth; @@ -715,23 +723,55 @@ Handlebars.JavaScriptCompiler = function() {}; } }, + pushStackLiteral: function(item) { + this.compileStack.push(new Literal(item)); + return item; + }, + pushStack: function(item) { - this.source.push(this.nextStack() + " = " + item + ";"); + this.source.push(this.incrStack() + " = " + item + ";"); + this.compileStack.push("stack" + this.stackSlot); + return "stack" + this.stackSlot; + }, + + replaceStack: function(callback) { + var item = callback.call(this, this.topStack()); + + this.source.push(this.topStack() + " = " + item + ";"); return "stack" + this.stackSlot; }, - nextStack: function() { + nextStack: function(skipCompileStack) { + var name = this.incrStack(); + this.compileStack.push("stack" + this.stackSlot); + return name; + }, + + incrStack: function() { this.stackSlot++; if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } return "stack" + this.stackSlot; }, popStack: function() { - return "stack" + this.stackSlot--; + var item = this.compileStack.pop(); + + if (item instanceof Literal) { + return item.value; + } else { + this.stackSlot--; + return item; + } }, topStack: function() { - return "stack" + this.stackSlot; + var item = this.compileStack[this.compileStack.length - 1]; + + if (item instanceof Literal) { + return item.value; + } else { + return item; + } }, quotedString: function(str) { diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js index 3a87d6c..2c181ea 100644 --- a/spec/qunit_spec.js +++ b/spec/qunit_spec.js @@ -215,7 +215,7 @@ test("nested iteration", function() { }); test("block with complex lookup", function() { - var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}" + var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", @@ -223,22 +223,22 @@ test("block with complex lookup", function() { }); test("helper with complex lookup", function() { - var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}" + var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var helpers = {link: function(prefix) { - return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>" + return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"; }}; - shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>") + shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>"); }); test("helper block with complex lookup expression", function() { - var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}" + var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; var hash = {name: "Alan"}; - var helpers = {goodbyes: function(fn) { + var helpers = {goodbyes: function(options) { var out = ""; var byes = ["Goodbye", "goodbye", "GOODBYE"]; for (var i = 0,j = byes.length; i < j; i++) { - out += byes[i] + " " + fn(this) + "! "; + out += byes[i] + " " + options.fn(this) + "! "; } return out; }}; @@ -248,8 +248,8 @@ test("helper block with complex lookup expression", function() { test("helper with complex lookup and nested template", function() { var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function (prefix, fn) { - return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>"; + var helpers = {link: function (prefix, options) { + return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; }}; shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>"); }); @@ -257,8 +257,8 @@ test("helper with complex lookup and nested template", function() { test("helper with complex lookup and nested template in VM+Compiler", function() { var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function (prefix, fn) { - return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>"; + var helpers = {link: function (prefix, options) { + return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>"; }}; shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>"); }); @@ -274,22 +274,22 @@ test("block helper", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; var template = CompilerContext.compile(string); - result = template({world: "world"}, { helpers: {goodbyes: function(fn) { return fn({text: "GOODBYE"}); }}}); + result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); equal(result, "GOODBYE! cruel world!", "Block helper executed"); }); test("block helper staying in the same context", function() { - var string = "{{#form}}<p>{{name}}</p>{{/form}}" + var string = "{{#form}}<p>{{name}}</p>{{/form}}"; var template = CompilerContext.compile(string); - result = template({name: "Yehuda"}, {helpers: {form: function(fn) { return "<form>" + fn(this) + "</form>" } }}); + result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }}); equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context"); }); test("block helper should have context in this", function() { var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>"; - var link = function(fn) { - return '<a href="/people/' + this.id + '">' + fn(this) + '</a>'; + var link = function(options) { + return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; }; var data = { "people": [ { "name": "Alan", "id": 1 }, @@ -304,31 +304,31 @@ test("block helper for undefined value", function() { }); test("block helper passing a new context", function() { - var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}" + var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"; var template = CompilerContext.compile(string); - result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, fn) { return "<form>" + fn(context) + "</form>" }}}); + result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved"); }); test("block helper passing a complex path context", function() { - var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}" + var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"; var template = CompilerContext.compile(string); - result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, fn) { return "<form>" + fn(context) + "</form>" }}}); + result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved"); }); test("nested block helpers", function() { - var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}" + var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"; var template = CompilerContext.compile(string); result = template({ yehuda: {name: "Yehuda" } }, { helpers: { - link: function(fn) { return "<a href='" + this.name + "'>" + fn(this) + "</a>" }, - form: function(context, fn) { return "<form>" + fn(context) + "</form>" } + link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; }, + form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; } } }); equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed"); @@ -345,7 +345,7 @@ test("block inverted sections with empty arrays", function() { }); test("block helper inverted sections", function() { - var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}" + var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"; var list = function(context, options) { if (context.length > 0) { var out = "<ul>"; @@ -366,7 +366,7 @@ test("block helper inverted sections", function() { var rootMessage = { people: [], message: "Nobody's here" - } + }; var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}"; @@ -492,8 +492,8 @@ test("escaping a String is possible", function(){ test("it works with ' marks", function() { var string = 'Message: {{{hello "Alan\'s world"}}}'; - var hash = {} - var helpers = {hello: function(param) { return "Hello " + param; }} + var hash = {}; + var helpers = {hello: function(param) { return "Hello " + param; }}; shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); }); @@ -501,19 +501,19 @@ module("multiple parameters"); test("simple multi-params work", function() { var string = 'Message: {{goodbye cruel world}}'; - var hash = {cruel: "cruel", world: "world"} - var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }} + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); }); test("block multi-params work", function() { var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; - var hash = {cruel: "cruel", world: "world"} - var helpers = {goodbye: function(cruel, world, fn) { - return fn({greeting: "Goodbye", adj: cruel, noun: world}); - }} + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world, options) { + return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); + }}; shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); -}) +}); module("safestring"); @@ -526,10 +526,10 @@ test("constructing a safestring from a string and checking its type", function() module("helperMissing"); test("if a context is not found, helperMissing is used", function() { - var string = "{{hello}} {{link_to world}}" + var string = "{{hello}} {{link_to world}}"; var context = { hello: "Hello", world: "world" }; - shouldCompileTo(string, context, "Hello <a>world</a>") + shouldCompileTo(string, context, "Hello <a>world</a>"); }); module("knownHelpers"); @@ -693,8 +693,8 @@ test("passing in data to a compiled function that expects data - works with bloc var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); var helpers = { - hello: function(fn) { - return fn(this); + hello: function(options) { + return options.fn(this); }, world: function(options) { return options.data.adjective + " world" + (this.exclaim ? "!" : ""); @@ -709,8 +709,8 @@ test("passing in data to a compiled function that expects data - works with bloc var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { - hello: function(fn) { - return fn({exclaim: "?"}); + hello: function(options) { + return options.fn({exclaim: "?"}); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); @@ -725,8 +725,8 @@ test("passing in data to a compiled function that expects data - data is passed var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { - hello: function(fn, inverse) { - return fn.data.accessData + " " + fn({exclaim: "?"}); + hello: function(options) { + return options.data.accessData + " " + options.fn({exclaim: "?"}); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); @@ -741,8 +741,8 @@ test("you can override inherited data when invoking a helper", function() { var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); var helpers = { - hello: function(fn) { - return fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); + hello: function(options) { + return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); @@ -758,8 +758,8 @@ test("you can override inherited data when invoking a helper with depth", functi var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var helpers = { - hello: function(fn) { - return fn({exclaim: "?"}, { data: {adjective: "sad"} }); + hello: function(options) { + return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); }, world: function(thing, options) { return options.data.adjective + " " + thing + (this.exclaim || ""); @@ -796,8 +796,8 @@ test("helpers take precedence over same-named context properties", function() { var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); var helpers = { - goodbye: function(fn) { - return this.goodbye.toUpperCase() + fn(this); + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); } }; @@ -840,8 +840,8 @@ test("Scoped names take precedence over block helpers", function() { var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); var helpers = { - goodbye: function(fn) { - return this.goodbye.toUpperCase() + fn(this); + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); } }; |