diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 5ec052f77..ee742f475 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -6,7 +6,7 @@ let SourceNode; try { /* istanbul ignore next */ if (typeof define !== 'function' || !define.amd) { - // We don't support this in AMD environments. For these environments, we asusme that + // We don't support this in AMD environments. For these environments, we assume that // they are running on the browser and thus have no need for the source-map library. let SourceMap = require('source-map'); SourceNode = SourceMap.SourceNode; @@ -107,8 +107,8 @@ CodeGen.prototype = { return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); }, - functionCall: function(fn, type, params) { - params = this.generateList(params); + functionCall: function(fn, type, rawParams) { + let params = this.generateList(rawParams); return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); }, diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 34ef5dd18..c7e5b8a03 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -156,11 +156,11 @@ Compiler.prototype = { DecoratorBlock(decorator) { let program = decorator.program && this.compileProgram(decorator.program); - let params = this.setupFullMustacheParams(decorator, program, undefined), + let { params, splatMap } = this.setupFullMustacheParams(decorator, program, undefined), path = decorator.path; this.useDecorators = true; - this.opcode('registerDecorator', params.length, path.original); + this.opcode('registerDecorator', params.length, path.original, splatMap); }, PartialStatement: function(partial) { @@ -199,6 +199,7 @@ Compiler.prototype = { this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('append'); }, + PartialBlockStatement: function(partialBlock) { this.PartialStatement(partialBlock); }, @@ -212,11 +213,11 @@ Compiler.prototype = { this.opcode('append'); } }, + Decorator(decorator) { this.DecoratorBlock(decorator); }, - ContentStatement: function(content) { if (content.value) { this.opcode('appendContent', content.value); @@ -261,12 +262,14 @@ Compiler.prototype = { }, helperSexpr: function(sexpr, program, inverse) { - let params = this.setupFullMustacheParams(sexpr, program, inverse), + let setup = this.setupFullMustacheParams(sexpr, program, inverse), + params = setup.params, + splatMap = setup.splatMap, path = sexpr.path, name = path.parts[0]; if (this.options.knownHelpers[name]) { - this.opcode('invokeKnownHelper', params.length, name); + this.opcode('invokeKnownHelper', params.length, name, splatMap); } else if (this.options.knownHelpersOnly) { throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); } else { @@ -274,7 +277,7 @@ Compiler.prototype = { path.falsy = true; this.accept(path); - this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path)); + this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path), splatMap); } }, @@ -326,12 +329,29 @@ Compiler.prototype = { this.opcode('pushHash'); - for (; i < l; i++) { - this.pushParam(pairs[i].value); - } - while (i--) { - this.opcode('assignToHash', pairs[i].key); + while (i < l) { + if (pairs[i].type === 'Splat') { + this.pushParam(pairs[i].value); + this.opcode('pushSplatHashPiece'); + ++i; + } else { + let start = i; + let end = i; + while (end < l && pairs[end].type == 'HashPair') { + ++end; + } + + this.opcode('pushHashPiece'); + for (let g = start; g < end; ++g) { + let pair = pairs[g]; + this.pushParam(pair.value); + this.opcode('assignToHash', pair.key); + } + + i = end; + } } + this.opcode('popHash'); }, @@ -384,19 +404,25 @@ Compiler.prototype = { } }, - pushParams: function(params) { - for (let i = 0, l = params.length; i < l; i++) { - this.pushParam(params[i]); - } - }, - pushParam: function(val) { this.accept(val); }, setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { let params = sexpr.params; - this.pushParams(params); + + let splatMap = []; + for (let i = 0, l = params.length; i < l; i++) { + let p = params[i]; + this.pushParam(p); + if (p.splat) { + splatMap.push(i); + } + } + + if (splatMap.length === 0) { + splatMap = null; + } this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); @@ -407,7 +433,7 @@ Compiler.prototype = { this.opcode('emptyHash', omitEmpty); } - return params; + return { params, splatMap }; }, blockParamIndex: function(name) { diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 25ddd137b..aa591d3cc 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -419,7 +419,6 @@ JavaScriptCompiler.prototype = { // it onto the stack. lookupOnContext: function(parts, falsy, strict, scoped) { let i = 0; - if (!scoped && this.options.compat && !this.lastContext) { // The depthed query is expected to handle the undefined logic for the root level that // is implemented below, so we evaluate that directly in compat mode @@ -500,16 +499,55 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral(omitEmpty ? 'undefined' : '{}'); }, pushHash: function() { - if (this.hash) { - this.hashes.push(this.hash); + if (this.hashPieces) { + this.hashes.push(this.hashPieces); } - this.hash = {values: {}}; + this.hashPieces = []; }, popHash: function() { - let hash = this.hash; - this.hash = this.hashes.pop(); + // Restore hashPieces array and make sure hashPiece points to + // the last element in that array, if present. + let hashPieces = this.hashPieces; + this.hashPieces = this.hashes.pop(); + this.hashPiece = this.hashPieces && this.hashPieces[this.hashPieces.length - 1]; + + let splatParams = []; + for (let i = 0, l = hashPieces.length; i < l; ++i) { + let piece = hashPieces[i]; + if (piece.type === 'pairs') { + splatParams.push(this.objectLiteral(piece.values)); + } else { + splatParams.push(piece.value); + } + } + + if (hashPieces.length === 1) { + // don't splat(); just use single piece as hash + this.push(splatParams[0]); + } else { + if (hashPieces[0].type === 'splat') { + // we merge into an empty POJO so that we don't mutate first splat param + splatParams.unshift('{}'); + } + + this.push([this.aliasable('container.splat'), '('].concat(splatParams.join(','), ')')); + } + }, - this.push(this.objectLiteral(hash.values)); + pushHashPiece: function() { + this.hashPiece = {type: 'pairs', values: {}}; + this.hashPieces.push(this.hashPiece); + }, + + // [pushSplatHashPiece] + // + // On stack, before: value, ..., hash, ... + // On stack, after: ..., hash, ... + // + // Pops a splat value off the stack and pushes it to hashPieces + pushSplatHashPiece: function() { + this.hashPiece = null; + this.hashPieces.push({ type: 'splat', value: this.popStack() }); }, // [pushString] @@ -577,7 +615,7 @@ JavaScriptCompiler.prototype = { // and pushes the helper's return value onto the stack. // // If the helper is not found, `helperMissing` is called. - invokeHelper: function(paramSize, name, isSimple) { + invokeHelper: function(paramSize, name, isSimple, splatMap) { let nonHelper = this.popStack(), helper = this.setupHelper(paramSize, name), simple = isSimple ? [helper.name, ' || '] : ''; @@ -588,7 +626,7 @@ JavaScriptCompiler.prototype = { } lookup.push(')'); - this.push(this.source.functionCall(lookup, 'call', helper.callParams)); + this.push(this.helperFunctionCall(lookup, helper, splatMap)); }, // [invokeKnownHelper] @@ -598,9 +636,9 @@ JavaScriptCompiler.prototype = { // // This operation is used when the helper is known to exist, // so a `helperMissing` fallback is not required. - invokeKnownHelper: function(paramSize, name) { + invokeKnownHelper: function(paramSize, name, splatMap) { let helper = this.setupHelper(paramSize, name); - this.push(this.source.functionCall(helper.name, 'call', helper.callParams)); + this.push(this.helperFunctionCall(helper.name, helper, splatMap)); }, // [invokeAmbiguous] @@ -638,10 +676,26 @@ JavaScriptCompiler.prototype = { '(', lookup, (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', '(typeof helper === ', this.aliasable('"function"'), ' ? ', - this.source.functionCall('helper', 'call', helper.callParams), ' : helper))' + this.helperFunctionCall('helper', helper, null), ' : helper))' ]); }, + helperFunctionCall: function(helperName, helperOptions, splatMap) { + if (splatMap) { + let splatMapObj = {}; + for (let i = 0, l = splatMap.length; i < l; ++i) { + splatMapObj[splatMap[i]] = 1; + } + + let argsWithSplatMap = [ this.objectLiteral(splatMapObj), ...helperOptions.params ]; + let splattedArgs = this.source.functionCall(this.aliasable('container.splatArgs'), null, argsWithSplatMap); + return this.source.functionCall(helperName, 'apply', [helperOptions.callContext, splattedArgs]); + } else { + let args = [helperOptions.callContext].concat(helperOptions.params); + return this.source.functionCall(helperName, 'call', args); + } + }, + // [invokePartial] // // On stack, before: context, ... @@ -685,9 +739,9 @@ JavaScriptCompiler.prototype = { // On stack, before: value, ..., hash, ... // On stack, after: ..., hash, ... // - // Pops a value off the stack and assigns it to the current hash + // Pops a value off the stack and assigns it to the current hash piece assignToHash: function(key) { - this.hash.values[key] = this.popStack(); + this.hashPiece.values[key] = this.popStack(); }, // HELPERS @@ -910,13 +964,14 @@ JavaScriptCompiler.prototype = { let params = [], paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); let foundHelper = this.nameLookup('helpers', name, 'helper'), - callContext = this.aliasable(`${this.contextName(0)} != null ? ${this.contextName(0)} : (container.nullContext || {})`); + contextName = this.contextName(0), + callContext = this.aliasable(`${contextName} != null ? ${contextName} : (container.nullContext || {})`); return { - params: params, - paramsInit: paramsInit, + params, + paramsInit, name: foundHelper, - callParams: [callContext].concat(params) + callContext }; }, @@ -930,6 +985,7 @@ JavaScriptCompiler.prototype = { } options.name = this.quotedString(helper); + options.hash = this.popStack(); let inverse = this.popStack(), diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 6ad43baec..3fab298b2 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -131,7 +131,9 @@ PrintVisitor.prototype.SubExpression = function(sexpr) { PrintVisitor.prototype.PathExpression = function(id) { let path = id.parts.join('/'); - return (id.data ? '@' : '') + 'PATH:' + path; + let pathFormatted = id.splat ? `SPLAT{PATH:${path}}` : `PATH:${path}`; + + return `${id.data ? '@' : ''}${pathFormatted}`; }; @@ -168,4 +170,8 @@ PrintVisitor.prototype.Hash = function(hash) { PrintVisitor.prototype.HashPair = function(pair) { return pair.key + '=' + this.accept(pair.value); }; + +PrintVisitor.prototype.Splat = function(splat) { + return 'SPLAT{' + this.accept(splat.value) + '}'; +}; /* eslint-enable new-cap */ diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 60f8e244d..b6c2ed1c1 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -85,6 +85,22 @@ export function template(templateSpec, env) { return typeof current === 'function' ? current.call(context) : current; }, + splat: function() { + return Utils.extend.apply(null, arguments); + }, + + splatArgs: function(splatMap, ...values) { + let args = []; + for (let i = 0, l = values.length; i < l; ++i) { + if (splatMap[i]) { + args.push.apply(args, values[i]); + } else { + args.push(values[i]); + } + } + return args; + }, + escapeExpression: Utils.escapeExpression, invokePartial: invokePartialWrapper, diff --git a/spec/helpers.js b/spec/helpers.js index 94e503f12..250b4102c 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -737,4 +737,145 @@ describe('helpers', function() { shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo'); }); }); + + describe('hash splat operators', function() { + it('basic hash splat', function() { + var string = '{{hello ...=splat }}'; + var hash = {splat: {firstName: 'Guybrush', lastName: 'Threepwood'}}; + var helpers = { + hello: function(options) { + var hash = options.hash; + return 'Hi, my name is ' + hash.firstName + ' ' + hash.lastName; + } + }; + + shouldCompileTo(string, [hash, helpers], 'Hi, my name is Guybrush Threepwood'); + }); + + it('hash splat shadowing', function() { + var string = '{{helper ...=splat occupation="pirate" }}'; + var hash = {splat: {occupation: 'cannonball'}}; + var helpers = { + helper: function(options) { + var hash = options.hash; + return 'I want to be a ' + hash.occupation + '!'; + } + }; + + shouldCompileTo(string, [hash, helpers], 'I want to be a pirate!'); + }); + + it('hash splat with nested value', function() { + var template = CompilerContext.compile('{{helper ...=foo.splat character=character }}'); + + var helpers = { + helper: function(options) { + return options.hash.character + ' and the ' + options.hash.numberOfHeads + ' monkey on ' + options.hash.island + ' Island'; + } + }; + + var context = {foo: { splat: {numberOfHeads: '3 headed', island: 'Dinky'}}, character: 'Guybrush'}; + + var result = template(context, {helpers: helpers}); + equals(result, 'Guybrush and the 3 headed monkey on Dinky Island', 'Splat test'); + }); + + it('multiple hash splats', function() { + var string = '{{hello ...=splat0 middleName="Wallace" ...=splat1}}'; + var hash = {splat0: {firstName: 'Guybrush', middleName: '__', lastName: '__'}, + splat1: {foo: '__', lastName: 'Threepwood'}}; + var helpers = { + hello: function(options) { + var hash = options.hash; + return 'Hi, my name is ' + hash.firstName + ' ' + hash.middleName + ' ' + hash.lastName; + } + }; + + shouldCompileTo(string, [hash, helpers], 'Hi, my name is Guybrush Wallace Threepwood'); + }); + + it('hash splat with function', function() { + var template = CompilerContext.compile('{{helper ...=foo character=character }}'); + + var helpers = { + helper: function(options) { + var hash = options.hash; + return hash.character + ' and the ' + hash.numberOfHeads + ' monkey on ' + hash.format(hash.island) + ' Island'; + } + }; + + var context = { + foo: { + numberOfHeads: '3 headed', + island: 'Dinky', + format: function(str) { + return str.toUpperCase(); + } + }, + character: 'Guybrush' + }; + + var result = template(context, {helpers: helpers}); + equals(result, 'Guybrush and the 3 headed monkey on DINKY Island', 'Splat function test'); + }); + + it('hash splat following hash pairs', function() { + var string = '{{hello firstName="Alex" middleName="Wallace" ...=splat }}'; + var hash = {splat: {firstName: 'Guybrush', lastName: 'Threepwood'}}; + var helpers = { + hello: function(options) { + var hash = options.hash; + return 'Hi, my name is ' + hash.firstName + ' ' + hash.middleName + ' ' + hash.lastName; + } + }; + + shouldCompileTo(string, [hash, helpers], 'Hi, my name is Guybrush Wallace Threepwood'); + }); + + it('basic param splat', function() { + var string = '{{hello ...names}}'; + var data = {names: ['Borflex', 'Snaggletooth']}; + var helpers = { + hello: function(firstName, lastName) { + return 'Hi, my name is ' + firstName + ' ' + lastName; + } + }; + + shouldCompileTo(string, [data, helpers], 'Hi, my name is Borflex Snaggletooth'); + }); + + function join() { + var args = [].slice.call(arguments); + args.pop(); + return args.join('-'); + } + + it('multiple param splats', function() { + var string = '{{join ...foo ...bar}}'; + var data = {foo: ['a', 'b'], bar: ['c', 'd']}; + var helpers = { join: join }; + shouldCompileTo(string, [data, helpers], 'a-b-c-d'); + }); + + it('param splats mixed with normal params, start with splat', function() { + var string = '{{join ...foo bar ...baz woot}}'; + var data = {foo: ['a', 'b'], bar: 'c', baz: ['d', 'e'], woot: 'f'}; + var helpers = { join: join }; + shouldCompileTo(string, [data, helpers], 'a-b-c-d-e-f'); + }); + + it('param splats mixed with normal params, start with normal param', function() { + var string = '{{join aaa ...foo bar ...baz woot}}'; + var data = {aaa: 'z', foo: ['a', 'b'], bar: 'c', baz: ['d', 'e'], woot: 'f'}; + var helpers = { join: join }; + shouldCompileTo(string, [data, helpers], 'z-a-b-c-d-e-f'); + }); + + it('param splats mixed with literals, start with normal param', function() { + var string = '{{join "WOOP" aaa "THERE" ...foo "IT" bar "IS" ...baz woot}}'; + var data = {aaa: 'z', foo: ['a', 'b'], bar: 'c', baz: ['d', 'e'], woot: 'f'}; + var helpers = { join: join }; + shouldCompileTo(string, [data, helpers], 'WOOP-z-THERE-a-b-IT-c-IS-d-e-f'); + }); + }); }); diff --git a/spec/parser.js b/spec/parser.js index 4527d19c1..a8db8047f 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -93,6 +93,13 @@ describe('parser', function() { equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); }); + it('parses splat', function() { + equals(astFor('{{foo ...arr}}'), '{{ PATH:foo [SPLAT{PATH:arr}] }}\n'); + equals(astFor('{{foo ...=obj}}'), '{{ PATH:foo [] HASH{SPLAT{PATH:obj}} }}\n'); + equals(astFor('{{foo ...arr ...=obj}}'), '{{ PATH:foo [SPLAT{PATH:arr}] HASH{SPLAT{PATH:obj}} }}\n'); + equals(astFor('{{foo foo ...arr bar ...=obj baz=obj2}}'), '{{ PATH:foo [PATH:foo, SPLAT{PATH:arr}, PATH:bar] HASH{SPLAT{PATH:obj}, baz=PATH:obj2} }}\n'); + }); + it('parses contents followed by a mustache', function() { equals(astFor('foo bar {{baz}}'), 'CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n'); }); @@ -215,6 +222,14 @@ describe('parser', function() { shouldThrow(function() { astFor('{{{{goodbyes}}}} {{{{/hellos}}}}'); }, Error, /goodbyes doesn't match hellos/); + + shouldThrow(function() { + astFor('{{foo ...}}'); + }, Error, /Parse error on line 1/); + + shouldThrow(function() { + astFor('{{foo ...=lol ...baz}}'); + }, Error, /Parse error on line 1/); }); it('should handle invalid paths', function() { diff --git a/spec/subexpressions.js b/spec/subexpressions.js index 3810eb85f..f28cb66bc 100644 --- a/spec/subexpressions.js +++ b/spec/subexpressions.js @@ -191,4 +191,65 @@ describe('subexpressions', function() { shouldCompileTo(string, [context, helpers], 'LOLLOL!'); }); }); + + it('subexpression with hash splat', function() { + var string = '{{component greeting=(translate ...=translateOptions)}}'; + var context = { + translateOptions: {lang: 'esp', key: 'greeting'} + }; + + var helpers = { + component: function(options) { + return options.hash.greeting + ' Guybrush!'; + }, + translate: function(options) { + var hash = options.hash; + var dictionary = { + 'esp': {greeting: 'Hola'} + }; + + return dictionary[hash.lang][hash.key]; + } + }; + + shouldCompileTo(string, [context, helpers], 'Hola Guybrush!'); + }); + + it('subexpression with hash splat', function() { + var string = '{{foo ...=(bar ...=baz)}}'; + var context = { + baz: { a: 123, b: 456 } + }; + + var helpers = { + bar: function(options) { + return options.hash; + }, + foo: function(options) { + var hash = options.hash; + return hash.a + hash.b; + } + }; + + shouldCompileTo(string, [context, helpers], '579'); + }); + + it('subexpression with mixed splats', function() { + var string = '{{foo ...(bar ...=baz)}}'; + var context = { + baz: { a: 123, b: 456 } + }; + + var helpers = { + bar: function(options) { + var hash = options.hash; + return [ hash.a, hash.b ]; + }, + foo: function(a, b) { + return a + b; + } + }; + + shouldCompileTo(string, [context, helpers], '579'); + }); }); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index 428804e01..c67f555ed 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -1,7 +1,5 @@ function shouldMatchTokens(result, tokens) { - for (var index = 0; index < result.length; index++) { - equals(result[index].name, tokens[index]); - } + equals(result.map(function(r) { return r.name; }).toString(), tokens.toString()); } function shouldBeToken(result, name, text) { equals(result.name, name); @@ -97,7 +95,7 @@ describe('Tokenizer', function() { it('supports escaped mustaches after escaped escape characters', function() { var result = tokenize('{{foo}} \\\\{{bar}} \\{{baz}}'); - shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']); + shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT']); shouldBeToken(result[3], 'CONTENT', ' \\'); shouldBeToken(result[4], 'OPEN', '{{'); @@ -376,6 +374,20 @@ describe('Tokenizer', function() { shouldBeToken(result[2], 'ID', 'omg'); }); + it('tokenizes splat', function() { + var result = tokenize('{{foo ...bar}}'); + shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'ID', 'CLOSE']); + + result = tokenize('{{foo ...=bar}}'); + shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize('{{foo ...bar ...bar ...=bar}}'); + shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'ID', 'SPLAT', 'ID', 'SPLAT', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize('{{foo ...=bar ...=foo}}'); + shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'EQUALS', 'ID', 'SPLAT', 'EQUALS', 'ID', 'CLOSE']); + }); + it('tokenizes special @ identifiers', function() { var result = tokenize('{{ @foo }}'); shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']); diff --git a/src/handlebars.l b/src/handlebars.l index e9de1029b..c7ca72eda 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -101,6 +101,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "{{"{LEFT_STRIP}?"*"? return 'OPEN'; "=" return 'EQUALS'; +"..." return 'SPLAT' ".." return 'ID'; "."/{LOOKAHEAD} return 'ID'; [\/.] return 'SEP'; @@ -119,7 +120,6 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "|" return 'CLOSE_BLOCK_PARAMS'; {ID} return 'ID'; - '['('\\]'|[^\]])*']' yytext = yytext.replace(/\\([\\\]])/g,'$1'); return 'ID'; . return 'INVALID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index ce0649838..f84036edc 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -89,6 +89,7 @@ mustache | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) ; + partial : OPEN_PARTIAL partialName param* hash? CLOSE { $$ = { @@ -109,11 +110,15 @@ openPartialBlock : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } ; + param : helperName -> $1 + | SPLAT helperName { $2.splat = $1; $$ = $2; } | sexpr -> $1 + | SPLAT sexpr { $2.splat = $1; $$ = $2; } ; + sexpr : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR { $$ = { @@ -131,6 +136,7 @@ hash hashSegment : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)} + | SPLAT EQUALS param -> {type: 'Splat', value: $3, loc: yy.locInfo(@$)} ; blockParams