More cleanup

* Make block and inverse use the main helper path
* Eliminate separate inverse AST node
This commit is contained in:
tomhuda
2012-05-28 17:00:41 -07:00
parent 3486b530be
commit 175c6fae0f
4 changed files with 164 additions and 80 deletions
+2 -8
View File
@@ -48,18 +48,12 @@ var Handlebars = require('./base');
}
};
Handlebars.AST.BlockNode = function(mustache, program, close) {
Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
verifyMatch(mustache.id, close);
this.type = "block";
this.mustache = mustache;
this.program = program;
};
Handlebars.AST.InverseNode = function(mustache, program, close) {
verifyMatch(mustache.id, close);
this.type = "inverse";
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
};
Handlebars.AST.ContentNode = function(string) {
+156 -66
View File
@@ -106,32 +106,42 @@ Handlebars.JavaScriptCompiler = function() {};
},
block: function(block) {
var mustache = block.mustache;
var depth, child, inverse, inverseGuid;
var mustache = block.mustache,
program = block.program,
inverse = block.inverse;
var params = this.setupStackForMustache(mustache);
var programGuid = this.compileProgram(block.program);
if(block.program.inverse) {
inverseGuid = this.compileProgram(block.program.inverse);
this.declare('inverse', inverseGuid);
if (program) {
program = this.compileProgram(program);
}
this.opcode('invokeProgram', programGuid, params.length, !!mustache.hash);
this.declare('inverse', null);
this.opcode('append');
},
if (inverse) {
inverse = this.compileProgram(inverse);
}
inverse: function(block) {
var params = this.setupStackForMustache(block.mustache);
var type = this.classifyMustache(mustache);
var programGuid = this.compileProgram(block.program);
if (type === "helper") {
this.helperMustache(mustache, program, inverse);
} else if (type === "simple") {
this.simpleMustache(mustache);
this.declare('inverse', programGuid);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('pushLiteral', '{}');
this.opcode('blockValue');
} else {
this.ambiguousMustache(mustache, program, inverse);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('pushLiteral', '{}');
this.opcode('ambiguousBlockValue');
}
this.opcode('invokeProgram', null, params.length, !!block.mustache.hash);
this.declare('inverse', null);
this.opcode('append');
},
@@ -168,24 +178,12 @@ Handlebars.JavaScriptCompiler = function() {};
},
mustache: function(mustache) {
var isHelper = mustache.isHelper;
var isEligible = mustache.eligibleHelper;
var options = this.options;
var options = this.options;
var type = this.classifyMustache(mustache);
// if ambiguous, we can possibly resolve the ambiguity now
if (isEligible && !isHelper) {
var name = mustache.id.parts[0];
if (options.knownHelpers[name]) {
isHelper = true;
} else if (options.knownHelpersOnly) {
isEligible = false;
}
}
if (!isEligible) {
if (type === "simple") {
this.simpleMustache(mustache);
} else if (isHelper) {
} else if (type === "helper") {
this.helperMustache(mustache);
} else {
this.ambiguousMustache(mustache);
@@ -198,14 +196,18 @@ Handlebars.JavaScriptCompiler = function() {};
}
},
ambiguousMustache: function(mustache) {
var params = this.setupStackForMustache(mustache),
id = mustache.id, name = id.parts[0];
ambiguousMustache: function(mustache, program, inverse) {
var id = mustache.id, name = id.parts[0];
this.opcode('invokeMustache', params.length, id.original);
this.opcode('getContext', id.depth);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('invokeAmbiguous', name);
},
simpleMustache: function(mustache) {
simpleMustache: function(mustache, program, inverse) {
var id = mustache.id;
this.addDepth(id.depth);
@@ -222,8 +224,8 @@ Handlebars.JavaScriptCompiler = function() {};
this.opcode('resolvePossibleLambda');
},
helperMustache: function(mustache) {
var params = this.setupMustacheParams(mustache),
helperMustache: function(mustache, program, inverse) {
var params = this.setupFullMustacheParams(mustache, program, inverse),
name = mustache.id.parts[0];
if (this.options.knownHelpers[name]) {
@@ -264,11 +266,11 @@ Handlebars.JavaScriptCompiler = function() {};
},
INTEGER: function(integer) {
this.opcode('push', integer.integer);
this.opcode('pushLiteral', integer.integer);
},
BOOLEAN: function(bool) {
this.opcode('push', bool.bool);
this.opcode('pushLiteral', bool.bool);
},
comment: function() {},
@@ -291,6 +293,27 @@ Handlebars.JavaScriptCompiler = function() {};
}
},
classifyMustache: function(mustache) {
var isHelper = mustache.isHelper;
var isEligible = mustache.eligibleHelper;
var options = this.options;
// if ambiguous, we can possibly resolve the ambiguity now
if (isEligible && !isHelper) {
var name = mustache.id.parts[0];
if (options.knownHelpers[name]) {
isHelper = true;
} else if (options.knownHelpersOnly) {
isEligible = false;
}
}
if (isHelper) { return "helper"; }
else if (isEligible) { return "ambiguous"; }
else { return "simple"; }
},
pushParams: function(params) {
var i = params.length, param;
@@ -323,6 +346,23 @@ Handlebars.JavaScriptCompiler = function() {};
return params;
},
// this will replace setupMustacheParams when we're done
setupFullMustacheParams: function(mustache, program, inverse) {
var params = mustache.params;
this.pushParams(params);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
if(mustache.hash) {
this.hash(mustache.hash);
} else {
this.opcode('pushLiteral', '{}');
}
return params;
},
setupStackForMustache: function(mustache) {
var params = this.setupMustacheParams(mustache);
@@ -485,6 +525,30 @@ Handlebars.JavaScriptCompiler = function() {};
}
},
blockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
this.replaceStack(function(current) {
params.splice(1, 0, current);
return current + " = blockHelperMissing.call(" + params.join(", ") + ")";
});
},
ambiguousBlockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
var current = this.topStack();
params.splice(1, 0, current);
this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
},
appendContent: function(content) {
this.source.push(this.appendToBuffer(this.quotedString(content)));
},
@@ -536,7 +600,8 @@ Handlebars.JavaScriptCompiler = function() {};
},
lookupWithHelpers: function(name) {
this.register('foundHelper', this.nameLookup('helpers', name, 'helper'));
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
this.register('foundHelper', helperName);
this.pushStack("foundHelper || " + this.nameLookup('depth' + this.lastContext, name, 'context'));
},
@@ -563,25 +628,6 @@ Handlebars.JavaScriptCompiler = function() {};
this.pushStackLiteral(value);
},
// The rules for mustaches are:
//
// If the first parameter resolves to a function, call the function with the remaining parameters
// If the first parameter resolves to undefined, call helperMissing with the string form of the
// first parameter and the remaining parameters
// Otherwise, if there is only one parameter, the expression evaluates to the value of the first
// parameter
invokeMustache: function(paramSize, original) {
this.populateParams(paramSize, this.quotedString(original), [], true, function(nextStack, helperMissingString, id) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
this.context.aliases.undef = 'void 0';
this.source.push("else if(" + id + "=== undef) { " + nextStack + " = helperMissing.call(" + helperMissingString + "); }");
if (nextStack !== id) {
this.source.push("else { " + nextStack + " = " + id + "; }");
}
});
},
// The rules for mustaches containing a block are:
//
// If the first parameter resolves to a function, call the function with the remaining parameters
@@ -604,10 +650,18 @@ Handlebars.JavaScriptCompiler = function() {};
});
},
pushProgram: function(guid) {
if (guid != null) {
this.pushStackLiteral(this.programExpression(guid));
} else {
this.pushStackLiteral(null);
}
},
invokeHelper: function(paramSize, name) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
var helper = this.setupHelper(paramSize, name);
var helper = this.lastHelper = this.setupHelper(paramSize, name);
this.register('foundHelper', helper.name);
this.pushStack("foundHelper ? foundHelper.call(" +
@@ -620,6 +674,22 @@ Handlebars.JavaScriptCompiler = function() {};
this.pushStack(helper.name + ".call(" + helper.callParams + ")");
},
invokeAmbiguous: function(name) {
this.context.aliases.functionType = '"function"';
this.pushStackLiteral('{}');
var helper = this.setupHelper(0, name);
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
this.register('foundHelper', helperName);
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nextStack = this.nextStack();
this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }');
this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '() : ' + nextStack + '; }');
},
setupHelper: function(paramSize, name) {
var params = [];
this.setupParams(paramSize, params);
@@ -636,10 +706,30 @@ Handlebars.JavaScriptCompiler = function() {};
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params) {
var options = [], contexts = [], param;
var options = [], contexts = [], param, inverse, program;
options.push("hash:" + this.popStack());
inverse = this.popStack();
program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)`
if (program || inverse) {
if (!program) {
this.context.aliases.self = "this";
program = "self.noop";
}
if (!inverse) {
this.context.aliases.self = "this";
inverse = "self.noop";
}
options.push("inverse:" + inverse);
options.push("fn:" + program);
}
for(var i=0; i<paramSize; i++) {
param = this.popStack();
params.push(param);
+4 -4
View File
@@ -376,10 +376,10 @@ test("block helper inverted sections", function() {
module("helpers hash");
test("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: "world"}], "Goodbye cruel world!",
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
"helpers hash is available");
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: "world"}],
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
"Goodbye cruel world!", "helpers hash is available inside other blocks");
});
@@ -888,8 +888,8 @@ test("helpers can take an optional hash with booleans", function() {
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
var result = template(context, {helpers: helpers});
template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
result = template(context, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean helper parameter honored");
});
+2 -2
View File
@@ -18,8 +18,8 @@ statements
;
statement
: openInverse program closeBlock { $$ = new yy.InverseNode($1, $2, $3); }
| openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $3); }
: openInverse program closeBlock { $$ = new yy.BlockNode($1, $2.inverse, $2, $3); }
| openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $2.inverse, $3); }
| mustache { $$ = $1; }
| partial { $$ = $1; }
| CONTENT { $$ = new yy.ContentNode($1); }