From b09333db7946d20ba7dbc6d32d5496ab8295b8e1 Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 24 Dec 2013 11:01:23 -0500 Subject: [PATCH] Added support for subexpressions Handlebars now supports subexpressions. {{foo (bar 3)}} Subexpressions are always evaluated as helpers; if `3` were omitted from the above example, `bar` would be invoked as a param-less helper, even though a top-levell `{{bar}}` would be considered ambiguous. The return value of a subexpression helper is passed in as a parameter of a parent subexpression helper, even in string params mode. Their type, as listed in `options.types` or `options.hashTypes` in string params mode, is "sexpr". The main conceptual change in the Handlebars code is that there is a new AST.SexprNode that manages the params/hash passed into a mustache, as well as the logic that governs whether that mustache is a helper invocation, property lookup, etc. MustacheNode, which used to manage this stuff, still exists, but only manages things like white-space stripping and whether the mustache is escaped or not. So, a MustacheNode _has_ a SexprNode. The introduction of subexpressions is fully backwards compatible, but a few things needed to change about the compiled output of a template in order to support subexpressions. The main one is that the options hash is no longer stashed in a local `options` var before being passed to either the helper being invoked or the `helperMissing` fallback. Rather, the options object is inlined in these cases. This does mean compiled template sizes will be a little bit larger, even those that don't make use of subexpressions, but shouldn't have any noticeable impact on performance when actually rendering templates as only one of these inlined objects will actually get evaluated. --- lib/handlebars/compiler/ast.js | 24 ++- lib/handlebars/compiler/compiler.js | 90 +++++----- .../compiler/javascript-compiler.js | 42 +++-- lib/handlebars/compiler/printer.js | 12 +- spec/ast.js | 14 +- spec/string-params.js | 16 ++ spec/subexpressions.js | 166 ++++++++++++++++++ spec/tokenizer.js | 27 +++ src/handlebars.l | 7 +- src/handlebars.yy | 16 +- 10 files changed, 329 insertions(+), 85 deletions(-) create mode 100644 spec/subexpressions.js diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index b6c3a035..ce5ee110 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -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 @@ -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); @@ -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); diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 259c543a..00353a8f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -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` @@ -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` @@ -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); } @@ -226,26 +233,17 @@ Compiler.prototype = { }, mustache: function(mustache) { - var options = this.options; - var type = this.classifyMustache(mustache); + this.sexpr(mustache.sexpr); - if (type === "simple") { - this.simpleMustache(mustache); - } else if (type === "helper") { - this.helperMustache(mustache); - } else { - this.ambiguousMustache(mustache); - } - - 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; @@ -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); @@ -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); @@ -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); @@ -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; @@ -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'); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 159a38bf..d920d52c 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -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(", ") + "); }"); }, @@ -398,10 +395,14 @@ 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); + } } }, @@ -409,8 +410,8 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('{}'); if (this.options.stringParams) { - this.register('hashTypes', '{}'); - this.register('hashContexts', '{}'); + this.push('{}'); // hashContexts + this.push('{}'); // hashTypes } }, pushHash: function() { @@ -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 }'); }, @@ -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'); @@ -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(); @@ -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(",") + "}"); } }; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index f91ff021..ad55c7d4 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -62,8 +62,8 @@ 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@\[-\^`\{-~]+/{LOOKAHEAD} [\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; +"(" return 'OPEN_SEXPR'; +")" return 'CLOSE_SEXPR'; + "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; "{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 63de17b0..319b8efb 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -44,11 +44,11 @@ statement ; openBlock - : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3), @$) + : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; openInverse - : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3), @$) + : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; closeBlock @@ -58,11 +58,10 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3), @$) - | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3), @$) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$) ; - partial : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4), @$) ; @@ -71,9 +70,9 @@ simpleInverse : OPEN_INVERSE CLOSE -> stripFlags($1, $2) ; -inMustache - : path param* hash? -> [[$1].concat($2), $3] - | dataName -> [[$1], null] +sexpr + : path param* hash? -> new yy.SexprNode([$1].concat($2), $3) + | dataName -> new yy.SexprNode([$1], null) ; param @@ -82,6 +81,7 @@ param | INTEGER -> new yy.IntegerNode($1, @$) | BOOLEAN -> new yy.BooleanNode($1, @$) | dataName -> $1 + | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} ; hash