From cb51b82b8e8114aaa3e1b19c41e5c4eebf0539d5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 13:23:45 -0600 Subject: [PATCH 1/4] Add support for dynamic partial names Uses the subexpression syntax to allow for dynamic partial lookups. Ex: ``` {{> (helper) }} ``` Fixes #933 --- docs/compiler-api.md | 13 ++++++++++ lib/handlebars/compiler/ast.js | 9 +++++++ lib/handlebars/compiler/compiler.js | 10 +++++-- .../compiler/javascript-compiler.js | 13 ++++++++-- lib/handlebars/compiler/printer.js | 2 +- lib/handlebars/compiler/visitor.js | 5 ++++ lib/handlebars/runtime.js | 15 ++++++++--- spec/partials.js | 26 +++++++++++++++++++ src/handlebars.yy | 8 +++++- 9 files changed, 92 insertions(+), 9 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 74af67246..4a0ec0149 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -115,6 +115,17 @@ interface SubExpression <: Expression { `isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. +```java +interface PartialExpression <: Expression { + type: "PartialExpression"; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; +} +``` + +`path` may be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}` + ##### Paths ```java @@ -221,6 +232,8 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c - `name` is the current path component - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`. + Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases. + - `depthedLookup(name)` Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode. diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 113c03e79..c193460fc 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -63,6 +63,15 @@ var AST = { this.hash = hash; }, + PartialExpression: function(name, params, hash, locInfo) { + this.loc = locInfo; + + this.type = 'PartialExpression'; + this.name = name; + this.params = params || []; + this.hash = hash; + }, + PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 95fe101b8..524f9aa2f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -145,7 +145,6 @@ Compiler.prototype = { }, PartialStatement: function(partial) { - var partialName = partial.sexpr.path.original; this.usePartial = true; var params = partial.sexpr.params; @@ -155,6 +154,12 @@ Compiler.prototype = { params.push({type: 'PathExpression', parts: [], depth: 0}); } + var partialName = partial.sexpr.name.original, + isDynamic = partial.sexpr.name.type === 'SubExpression'; + if (isDynamic) { + this.accept(partial.sexpr.name); + } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); var indent = partial.indent || ''; @@ -162,7 +167,8 @@ Compiler.prototype = { this.opcode('appendContent', indent); indent = ''; } - this.opcode('invokePartial', partialName, indent); + + this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('append'); }, diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 75f996071..a027edb91 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -644,17 +644,26 @@ JavaScriptCompiler.prototype = { // // This operation pops off a context, invokes a partial with that context, // and pushes the result of the invocation back. - invokePartial: function(name, indent) { + invokePartial: function(isDynamic, name, indent) { var params = [], options = this.setupParams(name, 1, params, false); + if (isDynamic) { + name = this.popStack(); + delete options.name; + } + if (indent) { options.indent = JSON.stringify(indent); } options.helpers = 'helpers'; options.partials = 'partials'; - params.unshift(this.nameLookup('partials', name, 'partial')); + if (!isDynamic) { + params.unshift(this.nameLookup('partials', name, 'partial')); + } else { + params.unshift(name); + } if (this.options.compat) { options.depths = 'depths'; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index ad276f19a..448331c03 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -75,7 +75,7 @@ PrintVisitor.prototype.BlockStatement = function(block) { PrintVisitor.prototype.PartialStatement = function(partial) { var sexpr = partial.sexpr, - content = 'PARTIAL:' + sexpr.path.original; + content = 'PARTIAL:' + sexpr.name.original; if(sexpr.params[0]) { content += ' ' + this.accept(sexpr.params[0]); } diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 3fb37fbfc..03af9153e 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -92,6 +92,11 @@ Visitor.prototype = { this.acceptArray(sexpr.params); this.acceptKey(sexpr, 'hash'); }, + PartialExpression: function(partial) { + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); + }, PathExpression: function(/* path */) {}, diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 2b9f47414..4e8c33ae8 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -39,10 +39,8 @@ export function template(templateSpec, env) { if (options.hash) { context = Utils.extend({}, context, options.hash); } - if (!partial) { - partial = options.partials[options.name]; - } + partial = env.VM.resolvePartial.call(this, partial, context, options); var result = env.VM.invokePartial.call(this, partial, context, options); if (result == null && env.compile) { @@ -187,6 +185,17 @@ export function program(container, i, fn, data, declaredBlockParams, blockParams return prog; } +export function resolvePartial(partial, context, options) { + if (!partial) { + partial = options.partials[options.name]; + } else if (!partial.call && !options.name) { + // This is a dynamic partial that returned a string + options.name = partial; + partial = options.partials[partial]; + } + return partial; +} + export function invokePartial(partial, context, options) { options.partial = true; diff --git a/spec/partials.js b/spec/partials.js index b1509423a..0c9e0f6dc 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -8,6 +8,32 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); }); + it('dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'dude'; + } + }; + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }); + it('failing dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'missing'; + } + }; + shouldThrow(function() { + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }, Handlebars.Exception, 'The partial missing could not be found'); + }); + it("partials with context", function() { var string = "Dudes: {{>dude dudes}}"; var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; diff --git a/src/handlebars.yy b/src/handlebars.yy index 27985b8d0..6ca32d942 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -77,7 +77,13 @@ mustache ; partial - : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_PARTIAL partial_expr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + ; + +partial_expr + : helperName param* hash? -> new yy.PartialExpression($1, $2, $3, yy.locInfo(@$)) + | dataName -> new yy.PartialExpression($1, null, null, yy.locInfo(@$)) + | OPEN_SEXPR sexpr CLOSE_SEXPR param* hash? -> new yy.PartialExpression($2, $4, $5, yy.locInfo(@$)) ; sexpr From d567d9c34566a6adab1eaea40ade2729fbce8144 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 13:29:15 -0600 Subject: [PATCH 2/4] Update PartialExpression AST docs --- docs/compiler-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 4a0ec0149..7a5db380e 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -118,13 +118,13 @@ interface SubExpression <: Expression { ```java interface PartialExpression <: Expression { type: "PartialExpression"; - name: PathExpression | SubExpression; + name: PathExpression | Literal | SubExpression; params: [ Expression ]; hash: Hash; } ``` -`path` may be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}` +`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. ##### Paths From 999da739a66199483ffc4d82426550aee5ac798f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 14:21:04 -0600 Subject: [PATCH 3/4] Update for proposed SubExpression dependency break Avoids parsing against SubExpressions and instead inlines the content that a subexpression otherwise would have. This can still be based via duck typing so should not add much overhead to the compiler. --- docs/compiler-api.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 7a5db380e..39bcacdaf 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -55,15 +55,21 @@ interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; - sexpr: SubExpression; - escaped: boolean; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + + escaped: boolean; strip: StripFlags | null; } interface BlockStatement <: Statement { type: "BlockStatement"; - sexpr: SubExpression; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + program: Program | null; inverse: Program | null; @@ -74,12 +80,19 @@ interface BlockStatement <: Statement { interface PartialStatement <: Statement { type: "PartialStatement"; - sexpr: SubExpression; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; indent: string; strip: StripFlags | null; } +``` +`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. + + +```java interface ContentStatement <: Statement { type: "ContentStatement"; value: string; @@ -108,24 +121,9 @@ interface SubExpression <: Expression { path: PathExpression; params: [ Expression ]; hash: Hash; - - isHelper: true | null; } ``` -`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. - -```java -interface PartialExpression <: Expression { - type: "PartialExpression"; - name: PathExpression | Literal | SubExpression; - params: [ Expression ]; - hash: Hash; -} -``` - -`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. - ##### Paths ```java From 884bf1553663734f22ffcd9d758c9d71d4373bf9 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 18 Jan 2015 17:27:27 -0600 Subject: [PATCH 4/4] Avoid direct references to sexpr in statements This allows us to avoid creating unnecessary AST nodes and avoids things like isHelper. Side effect of these changes is that @data functions can now have data parameters passed to them. --- docs/compiler-api.md | 2 +- lib/handlebars/compiler/ast.js | 36 +++++++++++++--------------- lib/handlebars/compiler/compiler.js | 30 ++++++++++------------- lib/handlebars/compiler/helpers.js | 22 +++++++++-------- lib/handlebars/compiler/printer.js | 15 ++++++------ lib/handlebars/compiler/visitor.js | 13 +++++++--- spec/ast.js | 10 ++++---- spec/data.js | 11 +++++++++ spec/visitor.js | 15 ++++++------ src/handlebars.yy | 37 ++++++++++++++--------------- 10 files changed, 100 insertions(+), 91 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 39bcacdaf..3593228bc 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -204,7 +204,7 @@ function ImportScanner() { ImportScanner.prototype = new Visitor(); ImportScanner.prototype.PartialStatement = function(partial) { - this.partials.push({request: partial.sexpr.original}); + this.partials.push({request: partial.name.original}); Visitor.prototype.PartialStatement.call(this, partial); }; diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index c193460fc..45212b383 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -8,21 +8,25 @@ var AST = { this.strip = strip; }, - MustacheStatement: function(sexpr, escaped, strip, locInfo) { + MustacheStatement: function(path, params, hash, escaped, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; - this.sexpr = sexpr; + this.path = path; + this.params = params || []; + this.hash = hash; this.escaped = escaped; this.strip = strip; }, - BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { + BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { this.loc = locInfo; - this.type = 'BlockStatement'; - this.sexpr = sexpr; + + this.path = path; + this.params = params || []; + this.hash = hash; this.program = program; this.inverse = inverse; @@ -31,12 +35,15 @@ var AST = { this.closeStrip = closeStrip; }, - PartialStatement: function(sexpr, strip, locInfo) { + PartialStatement: function(name, params, hash, strip, locInfo) { this.loc = locInfo; this.type = 'PartialStatement'; - this.sexpr = sexpr; - this.indent = ''; + this.name = name; + this.params = params || []; + this.hash = hash; + + this.indent = ''; this.strip = strip; }, @@ -63,15 +70,6 @@ var AST = { this.hash = hash; }, - PartialExpression: function(name, params, hash, locInfo) { - this.loc = locInfo; - - this.type = 'PartialExpression'; - this.name = name; - this.params = params || []; - this.hash = hash; - }, - PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; @@ -121,8 +119,8 @@ var AST = { // * it is an eligible helper, and // * it has at least one parameter or hash segment // TODO: Make these public utility methods - helperExpression: function(sexpr) { - return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); + helperExpression: function(node) { + return !!(node.type === 'SubExpression' || node.params.length || node.hash); }, scopedId: function(path) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 524f9aa2f..03bf8f430 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -110,28 +110,27 @@ Compiler.prototype = { }, BlockStatement: function(block) { - var sexpr = block.sexpr, - program = block.program, + var program = block.program, inverse = block.inverse; program = program && this.compileProgram(program); inverse = inverse && this.compileProgram(inverse); - var type = this.classifySexpr(sexpr); + var type = this.classifySexpr(block); if (type === 'helper') { - this.helperSexpr(sexpr, program, inverse); + this.helperSexpr(block, program, inverse); } else if (type === 'simple') { - this.simpleSexpr(sexpr); + this.simpleSexpr(block); // 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('emptyHash'); - this.opcode('blockValue', sexpr.path.original); + this.opcode('blockValue', block.path.original); } else { - this.ambiguousSexpr(sexpr, program, inverse); + this.ambiguousSexpr(block, program, inverse); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` @@ -147,20 +146,20 @@ Compiler.prototype = { PartialStatement: function(partial) { this.usePartial = true; - var params = partial.sexpr.params; + var params = partial.params; if (params.length > 1) { throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); } else if (!params.length) { params.push({type: 'PathExpression', parts: [], depth: 0}); } - var partialName = partial.sexpr.name.original, - isDynamic = partial.sexpr.name.type === 'SubExpression'; + var partialName = partial.name.original, + isDynamic = partial.name.type === 'SubExpression'; if (isDynamic) { - this.accept(partial.sexpr.name); + this.accept(partial.name); } - this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); + this.setupFullMustacheParams(partial, undefined, undefined, true); var indent = partial.indent || ''; if (this.options.preventIndent && indent) { @@ -173,7 +172,7 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - this.accept(mustache.sexpr); + this.SubExpression(mustache); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); @@ -340,11 +339,6 @@ Compiler.prototype = { pushParam: function(val) { var value = val.value != null ? val.value : val.original || ''; - // Force helper evaluation - if (val.type === 'SubExpression') { - val.isHelper = true; - } - if (this.stringParams) { if (value.replace) { value = value diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 6cfc0e076..beaf98869 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -52,28 +52,29 @@ export function preparePath(data, parts, locInfo) { return new this.PathExpression(data, depth, dig, original, locInfo); } -export function prepareMustache(sexpr, open, strip, locInfo) { +export function prepareMustache(path, params, hash, open, strip, locInfo) { /*jshint -W040 */ // Must use charAt to support IE pre-10 var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; - return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo)); + return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); } export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ - if (openRawBlock.sexpr.path.original !== close) { - var errorNode = {loc: openRawBlock.sexpr.loc}; + if (openRawBlock.path.original !== close) { + var errorNode = {loc: openRawBlock.path.loc}; - throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); + throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); } locInfo = this.locInfo(locInfo); var program = new this.Program([content], null, {}, locInfo); return new this.BlockStatement( - openRawBlock.sexpr, program, undefined, + openRawBlock.path, openRawBlock.params, openRawBlock.hash, + program, undefined, {}, {}, {}, locInfo); } @@ -81,10 +82,10 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ // When we are chaining inverse calls, we will not have a close path - if (close && close.path && openBlock.sexpr.path.original !== close.path.original) { - var errorNode = {loc: openBlock.sexpr.loc}; + if (close && close.path && openBlock.path.original !== close.path.original) { + var errorNode = {loc: openBlock.path.loc}; - throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode); + throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); } program.blockParams = openBlock.blockParams; @@ -108,7 +109,8 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } return new this.BlockStatement( - openBlock.sexpr, program, inverse, + openBlock.path, openBlock.params, openBlock.hash, + program, inverse, openBlock.strip, inverseStrip, close && close.strip, this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 448331c03..55232cc45 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -45,7 +45,7 @@ PrintVisitor.prototype.Program = function(program) { }; PrintVisitor.prototype.MustacheStatement = function(mustache) { - return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}'); + return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; PrintVisitor.prototype.BlockStatement = function(block) { @@ -53,7 +53,7 @@ PrintVisitor.prototype.BlockStatement = function(block) { out = out + this.pad('BLOCK:'); this.padding++; - out = out + this.pad(this.accept(block.sexpr)); + out = out + this.pad(this.SubExpression(block)); if (block.program) { out = out + this.pad('PROGRAM:'); this.padding++; @@ -74,13 +74,12 @@ PrintVisitor.prototype.BlockStatement = function(block) { }; PrintVisitor.prototype.PartialStatement = function(partial) { - var sexpr = partial.sexpr, - content = 'PARTIAL:' + sexpr.name.original; - if(sexpr.params[0]) { - content += ' ' + this.accept(sexpr.params[0]); + var content = 'PARTIAL:' + partial.name.original; + if(partial.params[0]) { + content += ' ' + this.accept(partial.params[0]); } - if (sexpr.hash) { - content += ' ' + this.accept(sexpr.hash); + if (partial.hash) { + content += ' ' + this.accept(partial.hash); } return this.pad('{{> ' + content + ' }}'); }; diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 03af9153e..4101a4f04 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -71,17 +71,24 @@ Visitor.prototype = { }, MustacheStatement: function(mustache) { - this.acceptRequired(mustache, 'sexpr'); + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params); + this.acceptKey(mustache, 'hash'); }, BlockStatement: function(block) { - this.acceptRequired(block, 'sexpr'); + this.acceptRequired(block, 'path'); + this.acceptArray(block.params); + this.acceptKey(block, 'hash'); + this.acceptKey(block, 'program'); this.acceptKey(block, 'inverse'); }, PartialStatement: function(partial) { - this.acceptRequired(partial, 'sexpr'); + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); }, ContentStatement: function(/* content */) {}, diff --git a/spec/ast.js b/spec/ast.js index d464cf110..3b2a4ae20 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -26,7 +26,7 @@ describe('ast', function() { it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheStatement({}, true, {}, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); @@ -40,10 +40,10 @@ describe('ast', function() { }); it('stores location info', function(){ - var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); var block = new handlebarsEnv.AST.BlockStatement( mustacheNode, + null, null, {body: []}, {body: []}, {}, @@ -104,7 +104,7 @@ describe('ast', function() { describe('PartialStatement', function(){ it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialStatement('so_partial', {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -200,7 +200,7 @@ describe('ast', function() { block = ast.body[0]; equals(block.program.body[0].value, ''); - equals(block.program.body[1].sexpr.path.original, 'foo'); + equals(block.program.body[1].path.original, 'foo'); equals(block.program.body[2].value, '\n'); }); it('marks nested block mustaches as standalone', function() { diff --git a/spec/data.js b/spec/data.js index bb90df5a8..1678eea85 100644 --- a/spec/data.js +++ b/spec/data.js @@ -93,6 +93,17 @@ describe('data', function() { }, Error); }); + it('data can be functions', function() { + var template = CompilerContext.compile('{{@hello}}'); + var result = template({}, { data: { hello: function() { return 'hello'; } } }); + equals('hello', result); + }); + it('data can be functions with params', function() { + var template = CompilerContext.compile('{{@hello "hello"}}'); + var result = template({}, { data: { hello: function(arg) { return arg; } } }); + equals('hello', result); + }); + it("data is inherited downstream", function() { var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true }); var helpers = { diff --git a/spec/visitor.js b/spec/visitor.js index 0c23c0d64..32172309c 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -24,11 +24,10 @@ describe('Visitor', function() { visitor.BooleanLiteral = function(bool) { equal(bool.value, true); - equal(this.parents.length, 4); + equal(this.parents.length, 3); equal(this.parents[0].type, 'SubExpression'); - equal(this.parents[1].type, 'SubExpression'); - equal(this.parents[2].type, 'BlockStatement'); - equal(this.parents[3].type, 'Program'); + equal(this.parents[1].type, 'BlockStatement'); + equal(this.parents[2].type, 'Program'); }; visitor.PathExpression = function(id) { equal(/(foo\.)?bar$/.test(id.original), true); @@ -84,26 +83,26 @@ describe('Visitor', function() { var visitor = new Handlebars.Visitor(); visitor.mutating = true; - visitor.SubExpression = function() { + visitor.PathExpression = function() { return false; }; var ast = Handlebars.parse('{{foo 42}}'); visitor.accept(ast); - }, Handlebars.Exception, 'MustacheStatement requires sexpr'); + }, Handlebars.Exception, 'MustacheStatement requires path'); }); it('should throw when returning non-node responses', function() { shouldThrow(function() { var visitor = new Handlebars.Visitor(); visitor.mutating = true; - visitor.SubExpression = function() { + visitor.PathExpression = function() { return {}; }; var ast = Handlebars.parse('{{foo 42}}'); visitor.accept(ast); - }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting sexpr on MustacheStatement'); + }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting path on MustacheStatement'); }); }); describe('arrays', function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index 6ca32d942..39f802757 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -30,7 +30,7 @@ rawBlock ; openRawBlock - : OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> { sexpr: $2 } + : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 } ; block @@ -39,15 +39,15 @@ block ; openBlock - : OPEN_BLOCK sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse - : OPEN_INVERSE sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr blockParams? CLOSE -> { sexpr: $2, blockParams: $3, strip: yy.stripFlags($1, $4) } + : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; inverseAndProgram @@ -72,23 +72,12 @@ 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 sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) + : OPEN helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) + | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) ; partial - : OPEN_PARTIAL partial_expr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) - ; - -partial_expr - : helperName param* hash? -> new yy.PartialExpression($1, $2, $3, yy.locInfo(@$)) - | dataName -> new yy.PartialExpression($1, null, null, yy.locInfo(@$)) - | OPEN_SEXPR sexpr CLOSE_SEXPR param* hash? -> new yy.PartialExpression($2, $4, $5, yy.locInfo(@$)) - ; - -sexpr - : helperName param* hash? -> new yy.SubExpression($1, $2, $3, yy.locInfo(@$)) - | dataName -> new yy.SubExpression($1, null, null, yy.locInfo(@$)) + : OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) ; param @@ -97,7 +86,11 @@ param | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) | dataName -> $1 - | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 + | sexpr -> $1 + ; + +sexpr + : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$)) ; hash @@ -114,10 +107,16 @@ blockParams helperName : path -> $1 + | dataName -> $1 | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) ; +partialName + : helperName -> $1 + | sexpr -> $1 + ; + dataName : DATA pathSegments -> yy.preparePath(true, $2, @$) ;