Skip to content

Commit 35f48f4

Browse files
committed
Finish ES6-style param and hash splatting
1 parent 26c699f commit 35f48f4

File tree

11 files changed

+286
-90
lines changed

11 files changed

+286
-90
lines changed

lib/handlebars/compiler/code-gen.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let SourceNode;
66
try {
77
/* istanbul ignore next */
88
if (typeof define !== 'function' || !define.amd) {
9-
// We don't support this in AMD environments. For these environments, we asusme that
9+
// We don't support this in AMD environments. For these environments, we assume that
1010
// they are running on the browser and thus have no need for the source-map library.
1111
let SourceMap = require('source-map');
1212
SourceNode = SourceMap.SourceNode;
@@ -107,8 +107,8 @@ CodeGen.prototype = {
107107
return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
108108
},
109109

110-
functionCall: function(fn, type, params) {
111-
params = this.generateList(params);
110+
functionCall: function(fn, type, rawParams) {
111+
let params = this.generateList(rawParams);
112112
return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
113113
},
114114

lib/handlebars/compiler/compiler.js

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,13 @@ Compiler.prototype = {
156156

157157
DecoratorBlock(decorator) {
158158
let program = decorator.program && this.compileProgram(decorator.program);
159-
let params = this.setupFullMustacheParams(decorator, program, undefined),
159+
let setup = this.setupFullMustacheParams(decorator, program, undefined),
160+
params = setup.params,
161+
splatMap = setup.splatMap,
160162
path = decorator.path;
161163

162164
this.useDecorators = true;
163-
this.opcode('registerDecorator', params.length, path.original);
165+
this.opcode('registerDecorator', params.length, path.original, splatMap);
164166
},
165167

166168
PartialStatement: function(partial) {
@@ -199,6 +201,7 @@ Compiler.prototype = {
199201
this.opcode('invokePartial', isDynamic, partialName, indent);
200202
this.opcode('append');
201203
},
204+
202205
PartialBlockStatement: function(partialBlock) {
203206
this.PartialStatement(partialBlock);
204207
},
@@ -212,11 +215,11 @@ Compiler.prototype = {
212215
this.opcode('append');
213216
}
214217
},
218+
215219
Decorator(decorator) {
216220
this.DecoratorBlock(decorator);
217221
},
218222

219-
220223
ContentStatement: function(content) {
221224
if (content.value) {
222225
this.opcode('appendContent', content.value);
@@ -261,20 +264,22 @@ Compiler.prototype = {
261264
},
262265

263266
helperSexpr: function(sexpr, program, inverse) {
264-
let params = this.setupFullMustacheParams(sexpr, program, inverse),
267+
let setup = this.setupFullMustacheParams(sexpr, program, inverse),
268+
params = setup.params,
269+
splatMap = setup.splatMap,
265270
path = sexpr.path,
266271
name = path.parts[0];
267272

268273
if (this.options.knownHelpers[name]) {
269-
this.opcode('invokeKnownHelper', params.length, name);
274+
this.opcode('invokeKnownHelper', params.length, name, splatMap);
270275
} else if (this.options.knownHelpersOnly) {
271276
throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr);
272277
} else {
273278
path.strict = true;
274279
path.falsy = true;
275280

276281
this.accept(path);
277-
this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
282+
this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path), splatMap);
278283
}
279284
},
280285

@@ -321,28 +326,35 @@ Compiler.prototype = {
321326

322327
Hash: function(hash) {
323328
let pairs = hash.pairs,
324-
i = 0, splat = null,
329+
i = 0,
325330
l = pairs.length;
326331

327332
this.opcode('pushHash');
328-
for (; i < l; i++) {
329333

334+
while (i < l) {
330335
if (pairs[i].type === 'Splat') {
331-
if (splat !== null) {
332-
throw new Exception('Multiple splats are not supported yet');
336+
this.pushParam(pairs[i].value);
337+
this.opcode('pushSplatHashPiece');
338+
++i;
339+
} else {
340+
let start = i;
341+
let end = i;
342+
while (end < l && pairs[end].type == 'HashPair') {
343+
++end;
333344
}
334-
splat = pairs[i].value;
335-
continue;
336-
}
337-
this.pushParam(pairs[i].value);
338-
}
339-
while (i--) {
340-
if (pairs[i].type === 'Splat') {
341-
continue;
345+
346+
this.opcode('pushHashPiece');
347+
for (let g = start; g < end; ++g) {
348+
let pair = pairs[g];
349+
this.pushParam(pair.value);
350+
this.opcode('assignToHash', pair.key);
351+
}
352+
353+
i = end;
342354
}
343-
this.opcode('assignToHash', pairs[i].key);
344355
}
345-
this.opcode('popHash', splat);
356+
357+
this.opcode('popHash');
346358
},
347359

348360
// HELPERS
@@ -394,19 +406,25 @@ Compiler.prototype = {
394406
}
395407
},
396408

397-
pushParams: function(params) {
398-
for (let i = 0, l = params.length; i < l; i++) {
399-
this.pushParam(params[i]);
400-
}
401-
},
402-
403409
pushParam: function(val) {
404410
this.accept(val);
405411
},
406412

407413
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
408414
let params = sexpr.params;
409-
this.pushParams(params);
415+
416+
let splatMap = [];
417+
for (let i = 0, l = params.length; i < l; i++) {
418+
let p = params[i];
419+
this.pushParam(p);
420+
if (p.splat) {
421+
splatMap.push(i);
422+
}
423+
}
424+
425+
if (splatMap.length === 0) {
426+
splatMap = null;
427+
}
410428

411429
this.opcode('pushProgram', program);
412430
this.opcode('pushProgram', inverse);
@@ -417,7 +435,7 @@ Compiler.prototype = {
417435
this.opcode('emptyHash', omitEmpty);
418436
}
419437

420-
return params;
438+
return { params, splatMap };
421439
},
422440

423441
blockParamIndex: function(name) {

lib/handlebars/compiler/javascript-compiler.js

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -499,20 +499,53 @@ JavaScriptCompiler.prototype = {
499499
this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
500500
},
501501
pushHash: function() {
502-
if (this.hash) {
503-
this.hashes.push(this.hash);
502+
if (this.hashPieces) {
503+
this.hashes.push(this.hashPieces);
504504
}
505-
this.hash = {values: [], types: [], contexts: [], ids: []};
505+
this.hashPieces = [];
506506
},
507-
popHash: function(splat) {
508-
let hash = this.hash;
509-
this.hash = this.hashes.pop();
510-
511-
this.push(this.objectLiteral(hash.values));
512-
if (splat) {
513-
let splatIdentifier = 'depth' + splat.depth + '.' + splat.original; // or parts[0] ? what's the different
514-
this.push([this.aliasable('container.splat'), '(', this.popStack(), ', ', splatIdentifier, ')']);
507+
popHash: function() {
508+
let hashPieces = this.hashPieces;
509+
this.hashPieces = this.hashes.pop();
510+
this.hashPiece = this.hashPieces && this.hashPieces[this.hashPieces.length - 1];
511+
512+
let splatParams = [];
513+
for (let i = 0, l = hashPieces.length; i < l; ++i) {
514+
let piece = hashPieces[i];
515+
if (piece.type === 'pairs') {
516+
splatParams.push(this.objectLiteral(piece.values));
517+
} else {
518+
splatParams.push(piece.value);
519+
}
515520
}
521+
522+
if (hashPieces.length === 1) {
523+
// don't splat(); just use single piece as hash
524+
this.push(splatParams[0]);
525+
} else {
526+
if (hashPieces[0].type === 'splat') {
527+
// we merge into an empty POJO so that we don't mutate first splat param
528+
splatParams.unshift('{}');
529+
}
530+
531+
this.push([this.aliasable('container.splat'), '('].concat(splatParams.join(','), ')'));
532+
}
533+
},
534+
535+
pushHashPiece: function() {
536+
this.hashPiece = {type: 'pairs', values: {}};
537+
this.hashPieces.push(this.hashPiece);
538+
},
539+
540+
// [pushSplatHashPiece]
541+
//
542+
// On stack, before: value, ..., hash, ...
543+
// On stack, after: ..., hash, ...
544+
//
545+
// Pops a splat value off the stack and pushes it to hashPieces
546+
pushSplatHashPiece: function() {
547+
this.hashPiece = null;
548+
this.hashPieces.push({ type: 'splat', value: this.popStack() });
516549
},
517550

518551
// [pushString]
@@ -580,7 +613,7 @@ JavaScriptCompiler.prototype = {
580613
// and pushes the helper's return value onto the stack.
581614
//
582615
// If the helper is not found, `helperMissing` is called.
583-
invokeHelper: function(paramSize, name, isSimple) {
616+
invokeHelper: function(paramSize, name, isSimple, splatMap) {
584617
let nonHelper = this.popStack(),
585618
helper = this.setupHelper(paramSize, name),
586619
simple = isSimple ? [helper.name, ' || '] : '';
@@ -591,7 +624,7 @@ JavaScriptCompiler.prototype = {
591624
}
592625
lookup.push(')');
593626

594-
this.push(this.source.functionCall(lookup, 'call', helper.callParams));
627+
this.push(this.helperFunctionCall(lookup, helper, splatMap));
595628
},
596629

597630
// [invokeKnownHelper]
@@ -601,9 +634,9 @@ JavaScriptCompiler.prototype = {
601634
//
602635
// This operation is used when the helper is known to exist,
603636
// so a `helperMissing` fallback is not required.
604-
invokeKnownHelper: function(paramSize, name) {
637+
invokeKnownHelper: function(paramSize, name, splatMap) {
605638
let helper = this.setupHelper(paramSize, name);
606-
this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
639+
this.push(this.helperFunctionCall(helper.name, helper, splatMap));
607640
},
608641

609642
// [invokeAmbiguous]
@@ -641,10 +674,27 @@ JavaScriptCompiler.prototype = {
641674
'(', lookup,
642675
(helper.paramsInit ? ['),(', helper.paramsInit] : []), '),',
643676
'(typeof helper === ', this.aliasable('"function"'), ' ? ',
644-
this.source.functionCall('helper', 'call', helper.callParams), ' : helper))'
677+
this.helperFunctionCall('helper', helper, null), ' : helper))'
645678
]);
646679
},
647680

681+
helperFunctionCall: function(helperName, helperOptions, splatMap) {
682+
if (splatMap) {
683+
let splatMapObj = {};
684+
for (let i = 0, l = splatMap.length; i < l; ++i) {
685+
splatMapObj[splatMap[i]] = 1;
686+
}
687+
688+
let argsWithSplatMap = helperOptions.params.slice();
689+
argsWithSplatMap.push(this.objectLiteral(splatMapObj));
690+
let splattedArgs = this.source.functionCall(this.aliasable('container.splatArgs'), null, argsWithSplatMap);
691+
return this.source.functionCall(helperName, 'apply', [helperOptions.callContext, splattedArgs]);
692+
} else {
693+
let args = [helperOptions.callContext].concat(helperOptions.params);
694+
return this.source.functionCall(helperName, 'call', args);
695+
}
696+
},
697+
648698
// [invokePartial]
649699
//
650700
// On stack, before: context, ...
@@ -688,9 +738,9 @@ JavaScriptCompiler.prototype = {
688738
// On stack, before: value, ..., hash, ...
689739
// On stack, after: ..., hash, ...
690740
//
691-
// Pops a value off the stack and assigns it to the current hash
741+
// Pops a value off the stack and assigns it to the current hash piece
692742
assignToHash: function(key) {
693-
this.hash.values[key] = this.popStack();
743+
this.hashPiece.values[key] = this.popStack();
694744
},
695745

696746
// HELPERS
@@ -911,13 +961,14 @@ JavaScriptCompiler.prototype = {
911961
let params = [],
912962
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
913963
let foundHelper = this.nameLookup('helpers', name, 'helper'),
914-
callContext = this.aliasable(`${this.contextName(0)} != null ? ${this.contextName(0)} : {}`);
964+
contextName = this.contextName(0),
965+
callContext = this.aliasable(`${contextName} != null ? ${contextName} : {}`);
915966

916967
return {
917-
params: params,
918-
paramsInit: paramsInit,
968+
params,
969+
paramsInit,
919970
name: foundHelper,
920-
callParams: [callContext].concat(params)
971+
callContext
921972
};
922973
},
923974

lib/handlebars/compiler/printer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ PrintVisitor.prototype.SubExpression = function(sexpr) {
131131

132132
PrintVisitor.prototype.PathExpression = function(id) {
133133
let path = id.parts.join('/');
134-
return (id.data ? '@' : '') + 'PATH:' + path;
134+
let pathFormatted = id.splat ? `SPLAT{PATH:${path}}` : `PATH:${path}`;
135+
136+
return `${id.data ? '@' : ''}${pathFormatted}`;
135137
};
136138

137139

lib/handlebars/runtime.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,21 @@ export function template(templateSpec, env) {
8585
return typeof current === 'function' ? current.call(context) : current;
8686
},
8787

88-
splat: function(current, context) {
89-
return Utils.extend(context, current);
88+
splat: function() {
89+
return Utils.extend.apply(null, arguments);
90+
},
91+
92+
splatArgs: function() {
93+
let splatMap = arguments[arguments.length - 1];
94+
let args = [];
95+
for (let i = 0, l = arguments.length - 1; i < l; ++i) {
96+
if (splatMap[i]) {
97+
args.push.apply(args, arguments[i]);
98+
} else {
99+
args.push(arguments[i]);
100+
}
101+
}
102+
return args;
90103
},
91104

92105
escapeExpression: Utils.escapeExpression,

0 commit comments

Comments
 (0)