Start doing earlier work on helpers

This commit is contained in:
Yehuda Katz
2012-05-28 00:15:52 -07:00
parent 246325085f
commit 8786a6c6e2
3 changed files with 167 additions and 58 deletions
+22 -5
View File
@@ -11,12 +11,26 @@ var Handlebars = require('./base');
if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
};
Handlebars.AST.MustacheNode = function(params, hash, unescaped) {
Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
this.type = "mustache";
this.id = params[0];
this.params = params.slice(1);
this.hash = hash;
this.escaped = !unescaped;
this.hash = hash;
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var eligibleHelper = this.eligibleHelper = id.isSimple;
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = eligibleHelper && (params.length || hash);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
};
Handlebars.AST.PartialNode = function(id, context) {
@@ -75,7 +89,10 @@ var Handlebars = require('./base');
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.isSimple = (dig.length === 1) && (depth === 0);
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
};
Handlebars.AST.StringNode = function(string) {
+127 -28
View File
@@ -25,7 +25,10 @@ Handlebars.JavaScriptCompiler = function() {};
pushStringParam: 16,
knownHelper: 17,
pushContext: 18,
lookupOnContext: 19
lookupOnContext: 19,
resolvePossibleLambda: 20,
invokeHelper: 21,
pushLiteral: 22
};
Compiler.MULTI_PARAM_OPCODES = {
@@ -44,7 +47,10 @@ Handlebars.JavaScriptCompiler = function() {};
pushStringParam: 1,
knownHelper: 1,
pushContext: 0,
lookupOnContext: 1
lookupOnContext: 1,
resolvePossibleLambda: 0,
invokeHelper: 2,
pushLiteral: 1
};
Compiler.DISASSEMBLE_MAP = {};
@@ -231,9 +237,18 @@ Handlebars.JavaScriptCompiler = function() {};
},
mustache: function(mustache) {
var params = this.setupStackForMustache(mustache);
var id = mustache.id;
this.opcode('invokeMustache', params.length, mustache.id.original);
if (!mustache.eligibleHelper) {
this.simpleMustache(mustache);
} else if (mustache.isHelper) {
this.helperMustache(mustache);
} else {
var params = this.setupStackForMustache(mustache),
name = id.parts[0];
this.opcode('invokeMustache', params.length, mustache.id.original);
}
if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
@@ -242,9 +257,32 @@ Handlebars.JavaScriptCompiler = function() {};
}
},
simpleMustache: function(mustache) {
var id = mustache.id;
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
if (id.parts.length) {
this.opcode('lookupOnContext', id.parts[0]);
for(var i=1, l=id.parts.length; i<l; i++) {
this.opcode('lookup', id.parts[i]);
}
} else {
this.opcode('pushContext');
}
this.opcode('resolvePossibleLambda');
},
helperMustache: function(mustache) {
var params = this.setupMustacheParams(mustache),
name = mustache.id.parts[0];
this.opcode('invokeHelper', params.length, name);
},
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
var name = id.parts[0];
@@ -282,25 +320,6 @@ Handlebars.JavaScriptCompiler = function() {};
comment: function() {},
// HELPERS
pushParams: function(params) {
var i = params.length, param;
while(i--) {
param = params[i];
if(this.options.stringParams) {
if(param.depth) {
this.addDepth(param.depth);
}
this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.string);
} else {
this[param.type](param);
}
}
},
opcode: function(name, val1, val2, val3) {
this.opcodes.push(Compiler.OPCODE_MAP[name]);
if(val1 !== undefined) { this.opcodes.push(val1); }
@@ -323,17 +342,41 @@ Handlebars.JavaScriptCompiler = function() {};
}
},
setupStackForMustache: function(mustache) {
var params = mustache.params;
pushParams: function(params) {
var i = params.length, param;
while(i--) {
param = params[i];
if(this.options.stringParams) {
if(param.depth) {
this.addDepth(param.depth);
}
this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.string);
} else {
this[param.type](param);
}
}
},
setupMustacheParams: function(mustache) {
var params = mustache.params;
this.pushParams(params);
if(mustache.hash) {
this.hash(mustache.hash);
} else {
this.opcode('push', '{}');
this.opcode('pushLiteral', '{}');
}
return params;
},
setupStackForMustache: function(mustache) {
var params = this.setupMustacheParams(mustache);
this.ID(mustache.id);
return params;
@@ -547,13 +590,21 @@ Handlebars.JavaScriptCompiler = function() {};
},
lookupOnContext: function(name) {
this.pushStackLiteral(this.nameLookup('depth' + this.lastContext, name, 'context'));
this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context'));
},
pushContext: function() {
this.pushStackLiteral('depth' + this.lastContext);
},
resolvePossibleLambda: function() {
this.context.aliases.functionType = '"function"';
this.replaceStack(function(current) {
return "typeof " + current + " === functionType ? " + current + "() : " + current;
});
},
lookupWithHelpers: function(name) {
this.register('foundHelper', this.nameLookup('helpers', name, 'helper'));
this.pushStack("foundHelper || " + this.nameLookup('depth' + this.lastContext, name, 'context'));
@@ -578,6 +629,10 @@ Handlebars.JavaScriptCompiler = function() {};
this.pushStack(name);
},
pushLiteral: function(value) {
this.pushStackLiteral(value);
},
// The rules for mustaches are:
//
// If the first parameter resolves to a function, call the function with the remaining parameters
@@ -619,6 +674,50 @@ Handlebars.JavaScriptCompiler = function() {};
});
},
invokeHelper: function(paramSize, name) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
var params = [], contexts = [];
this.setupParams(paramSize, params, contexts);
this.register('foundHelper', this.nameLookup('helpers', name, 'helper'));
var main = ["depth0"].concat(params).join(", ");
var helperMissing = ["depth0", this.quotedString(name)].concat(params).join(", ");
this.pushStack("foundHelper ? foundHelper.call(" + main + ") " +
": helperMissing.call(" + helperMissing + ")");
//this.replaceStack(function(current) {
//});
},
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params) {
var options = [], contexts = [], param;
options.push("hash:" + this.popStack());
for(var i=0; i<paramSize; i++) {
param = this.popStack();
params.push(param);
if(this.options.stringParams) {
contexts.push(this.popStack());
}
}
if (this.options.stringParams) {
options.push("contexts:[" + contexts.join(",") + "]");
}
if(this.options.data) {
options.push("data:data");
}
params.push("{" + options.join(",") + "}");
return params.join(", ");
},
populateParams: function(paramSize, helperId, options, resolveLambdas, callback) {
var id = this.popStack(), nextStack;
var params = [], contexts = [], param, paramString, stringOptions;
+18 -25
View File
@@ -70,8 +70,8 @@ test("boolean", function() {
});
test("zeros", function() {
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
"num1: 42, num2: 0");
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
"num1: 42, num2: 0");
shouldCompileTo("num: {{.}}", 0, "num: 0");
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
});
@@ -112,13 +112,6 @@ test("functions", function() {
"functions are called and render their output");
});
test("functions with context argument", function() {
shouldCompileTo("{{awesome frank}}",
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
});
test("paths with hyphens", function() {
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
});
@@ -225,7 +218,7 @@ test("block with complex lookup", function() {
"Templates can access variables in contexts up the stack with relative path syntax");
});
test("helper with complex lookup", function() {
test("helper with complex lookup$", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var helpers = {link: function(prefix) {
@@ -779,14 +772,14 @@ test("helpers take precedence over same-named context properties", function() {
var helpers = {
goodbye: function() {
return this.goodbye.toUpperCase();
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
@@ -795,20 +788,20 @@ test("helpers take precedence over same-named context properties", function() {
equals(result, "GOODBYE cruel WORLD", "Helper executed");
});
test("helpers take precedence over same-named context properties", function() {
test("helpers take precedence over same-named context properties$", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
var helpers = {
goodbye: function(options) {
return this.goodbye.toUpperCase() + options.fn(this);
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
@@ -823,14 +816,14 @@ test("Scoped names take precedence over helpers", function() {
var helpers = {
goodbye: function() {
return this.goodbye.toUpperCase();
}
};
},
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
};
var context = {
goodbye: "goodbye",
world: "world"
};
@@ -845,14 +838,14 @@ test("Scoped names take precedence over block helpers", function() {
var helpers = {
goodbye: function(options) {
return this.goodbye.toUpperCase() + options.fn(this);
}
};
},
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
};
var context = {
goodbye: "goodbye",
world: "world"
};