Skip to content

Commit

Permalink
Merge pull request #690 from machty/subsexpr
Browse files Browse the repository at this point in the history
Added support for subexpressions
  • Loading branch information
kpdecker committed Dec 31, 2013
2 parents ac98e7b + b09333d commit a2ca31b
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 85 deletions.
24 changes: 21 additions & 3 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ var AST = {
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "mustache";
this.hash = hash;
this.strip = strip;

// Open may be a string parsed from the parser or a passed boolean flag
Expand All @@ -58,6 +57,25 @@ var AST = {
this.escaped = !!open;
}

if (rawParams instanceof AST.SexprNode) {
this.sexpr = rawParams;
} else {
// Support old AST API
this.sexpr = new AST.SexprNode(rawParams, hash);
}

// Support old AST API that stored this info in MustacheNode
this.id = this.sexpr.id;
this.params = this.sexpr.params;
this.hash = this.sexpr.hash;
this.eligibleHelper = this.sexpr.eligibleHelper;
this.isHelper = this.sexpr.isHelper;
},

SexprNode: function(rawParams, hash) {
this.type = "sexpr";
this.hash = hash;

var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);

Expand All @@ -84,8 +102,8 @@ var AST = {
},

BlockNode: function(mustache, program, inverse, close, locInfo) {
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
if(mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original);
}

LocationInfo.call(this, locInfo);
Expand Down
90 changes: 46 additions & 44 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,13 @@ Compiler.prototype = {
inverse = this.compileProgram(inverse);
}

var type = this.classifyMustache(mustache);
var sexpr = mustache.sexpr;
var type = this.classifySexpr(sexpr);

if (type === "helper") {
this.helperMustache(mustache, program, inverse);
this.helperSexpr(sexpr, program, inverse);
} else if (type === "simple") {
this.simpleMustache(mustache);
this.simpleSexpr(sexpr);

// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
Expand All @@ -170,7 +171,7 @@ Compiler.prototype = {
this.opcode('emptyHash');
this.opcode('blockValue');
} else {
this.ambiguousMustache(mustache, program, inverse);
this.ambiguousSexpr(sexpr, program, inverse);

// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
Expand Down Expand Up @@ -198,6 +199,12 @@ Compiler.prototype = {
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);

if (val.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(val);
}
} else {
this.accept(val);
}
Expand Down Expand Up @@ -226,26 +233,17 @@ Compiler.prototype = {
},

mustache: function(mustache) {
var options = this.options;
var type = this.classifyMustache(mustache);

if (type === "simple") {
this.simpleMustache(mustache);
} else if (type === "helper") {
this.helperMustache(mustache);
} else {
this.ambiguousMustache(mustache);
}
this.sexpr(mustache.sexpr);

if(mustache.escaped && !options.noEscape) {
if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},

ambiguousMustache: function(mustache, program, inverse) {
var id = mustache.id,
ambiguousSexpr: function(sexpr, program, inverse) {
var id = sexpr.id,
name = id.parts[0],
isBlock = program != null || inverse != null;

Expand All @@ -257,8 +255,8 @@ Compiler.prototype = {
this.opcode('invokeAmbiguous', name, isBlock);
},

simpleMustache: function(mustache) {
var id = mustache.id;
simpleSexpr: function(sexpr) {
var id = sexpr.id;

if (id.type === 'DATA') {
this.DATA(id);
Expand All @@ -274,9 +272,9 @@ Compiler.prototype = {
this.opcode('resolvePossibleLambda');
},

helperMustache: function(mustache, program, inverse) {
var params = this.setupFullMustacheParams(mustache, program, inverse),
name = mustache.id.parts[0];
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
name = sexpr.id.parts[0];

if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
Expand All @@ -287,6 +285,18 @@ Compiler.prototype = {
}
},

sexpr: function(sexpr) {
var type = this.classifySexpr(sexpr);

if (type === "simple") {
this.simpleSexpr(sexpr);
} else if (type === "helper") {
this.helperSexpr(sexpr);
} else {
this.ambiguousSexpr(sexpr);
}
},

ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
Expand Down Expand Up @@ -349,14 +359,14 @@ Compiler.prototype = {
}
},

classifyMustache: function(mustache) {
var isHelper = mustache.isHelper;
var isEligible = mustache.eligibleHelper;
classifySexpr: function(sexpr) {
var isHelper = sexpr.isHelper;
var isEligible = sexpr.eligibleHelper;
var options = this.options;

// if ambiguous, we can possibly resolve the ambiguity now
if (isEligible && !isHelper) {
var name = mustache.id.parts[0];
var name = sexpr.id.parts[0];

if (options.knownHelpers[name]) {
isHelper = true;
Expand All @@ -383,35 +393,27 @@ Compiler.prototype = {

this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.stringModeValue, param.type);

if (param.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(param);
}
} 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('emptyHash');
}

return params;
},

// this will replace setupMustacheParams when we're done
setupFullMustacheParams: function(mustache, program, inverse) {
var params = mustache.params;
setupFullMustacheParams: function(sexpr, program, inverse) {
var params = sexpr.params;
this.pushParams(params);

this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);

if(mustache.hash) {
this.hash(mustache.hash);
if (sexpr.hash) {
this.hash(sexpr.hash);
} else {
this.opcode('emptyHash');
}
Expand Down
42 changes: 20 additions & 22 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,6 @@ JavaScriptCompiler.prototype = {
var current = this.topStack();
params.splice(1, 0, current);

// Use the options value generated from the invocation
params[params.length-1] = 'options';

this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
},

Expand Down Expand Up @@ -398,19 +395,23 @@ JavaScriptCompiler.prototype = {

this.pushString(type);

if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
// If it's a subexpression, the string result
// will be pushed after this opcode.
if (type !== 'sexpr') {
if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
}
}
},

emptyHash: function() {
this.pushStackLiteral('{}');

if (this.options.stringParams) {
this.register('hashTypes', '{}');
this.register('hashContexts', '{}');
this.push('{}'); // hashContexts
this.push('{}'); // hashTypes
}
},
pushHash: function() {
Expand All @@ -421,9 +422,10 @@ JavaScriptCompiler.prototype = {
this.hash = undefined;

if (this.options.stringParams) {
this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
this.register('hashTypes', '{' + hash.types.join(',') + '}');
this.push('{' + hash.contexts.join(',') + '}');
this.push('{' + hash.types.join(',') + '}');
}

this.push('{\n ' + hash.values.join(',\n ') + '\n }');
},

Expand Down Expand Up @@ -526,7 +528,7 @@ JavaScriptCompiler.prototype = {
invokeAmbiguous: function(name, helperCall) {
this.context.aliases.functionType = '"function"';

this.pushStackLiteral('{}'); // Hash value
this.emptyHash();
var helper = this.setupHelper(0, name, helperCall);

var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
Expand Down Expand Up @@ -805,6 +807,11 @@ JavaScriptCompiler.prototype = {

options.push("hash:" + this.popStack());

if (this.options.stringParams) {
options.push("hashTypes:" + this.popStack());
options.push("hashContexts:" + this.popStack());
}

inverse = this.popStack();
program = this.popStack();

Expand Down Expand Up @@ -838,22 +845,13 @@ JavaScriptCompiler.prototype = {
if (this.options.stringParams) {
options.push("contexts:[" + contexts.join(",") + "]");
options.push("types:[" + types.join(",") + "]");
options.push("hashContexts:hashContexts");
options.push("hashTypes:hashTypes");
}

if(this.options.data) {
options.push("data:data");
}

options = "{" + options.join(",") + "}";
if (useRegister) {
this.register('options', options);
params.push('options');
} else {
params.push(options);
}
return params.join(", ");
params.push("{" + options.join(",") + "}");
}
};

Expand Down
12 changes: 8 additions & 4 deletions lib/handlebars/compiler/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,22 @@ PrintVisitor.prototype.block = function(block) {
return out;
};

PrintVisitor.prototype.mustache = function(mustache) {
var params = mustache.params, paramStrings = [], hash;
PrintVisitor.prototype.sexpr = function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;

for(var i=0, l=params.length; i<l; i++) {
paramStrings.push(this.accept(params[i]));
}

params = "[" + paramStrings.join(", ") + "]";

hash = mustache.hash ? " " + this.accept(mustache.hash) : "";
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";

return this.accept(sexpr.id) + " " + params + hash;
};

return this.pad("{{ " + this.accept(mustache.id) + " " + params + hash + " }}");
PrintVisitor.prototype.mustache = function(mustache) {
return this.pad("{{ " + this.accept(mustache.sexpr) + " }}");
};

PrintVisitor.prototype.partial = function(partial) {
Expand Down
14 changes: 12 additions & 2 deletions spec/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,24 @@ describe('ast', function() {
});
});
describe('BlockNode', function() {
it('should throw on mustache mismatch (old sexpr-less version)', function() {
shouldThrow(function() {
var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
new handlebarsEnv.AST.BlockNode({id: {original: 'foo'}}, {}, {}, {path: {original: 'bar'}});
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});

it('stores location info', function(){
var block = new handlebarsEnv.AST.BlockNode({strip: {}, id: {original: 'foo'}},
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
var block = new handlebarsEnv.AST.BlockNode(mustacheNode,
{strip: {}}, {strip: {}},
{
strip: {},
Expand Down
16 changes: 16 additions & 0 deletions spec/string-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,20 @@ describe('string params mode', function() {

equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
});

it("with nested block ambiguous", function() {
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});

var helpers = {
with: function(options) {
return "WITH";
},
view: function() {
return "VIEW";
}
};

var result = template({}, {helpers: helpers});
equals(result, "WITH");
});
});
Loading

0 comments on commit a2ca31b

Please sign in to comment.