Skip to content

Commit e61f186

Browse files
committed
Splat operator
1 parent 9365b82 commit e61f186

File tree

9 files changed

+127
-7
lines changed

9 files changed

+127
-7
lines changed

lib/handlebars/compiler/compiler.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,18 +323,28 @@ Compiler.prototype = {
323323

324324
Hash: function(hash) {
325325
let pairs = hash.pairs,
326-
i = 0,
326+
i = 0, splat = null,
327327
l = pairs.length;
328328

329329
this.opcode('pushHash');
330-
331330
for (; i < l; i++) {
331+
332+
if (pairs[i].type === 'Splat') {
333+
if (splat !== null) {
334+
throw new Exception('Multiple splats are not supported yet');
335+
}
336+
splat = pairs[i].value;
337+
continue;
338+
}
332339
this.pushParam(pairs[i].value);
333340
}
334341
while (i--) {
342+
if (pairs[i].type === 'Splat') {
343+
continue;
344+
}
335345
this.opcode('assignToHash', pairs[i].key);
336346
}
337-
this.opcode('popHash');
347+
this.opcode('popHash', splat);
338348
},
339349

340350
// HELPERS

lib/handlebars/compiler/javascript-compiler.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,6 @@ JavaScriptCompiler.prototype = {
421421
// it onto the stack.
422422
lookupOnContext: function(parts, falsy, strict, scoped) {
423423
let i = 0;
424-
425424
if (!scoped && this.options.compat && !this.lastContext) {
426425
// The depthed query is expected to handle the undefined logic for the root level that
427426
// is implemented below, so we evaluate that directly in compat mode
@@ -537,7 +536,7 @@ JavaScriptCompiler.prototype = {
537536
}
538537
this.hash = {values: [], types: [], contexts: [], ids: []};
539538
},
540-
popHash: function() {
539+
popHash: function(splat) {
541540
let hash = this.hash;
542541
this.hash = this.hashes.pop();
543542

@@ -550,6 +549,10 @@ JavaScriptCompiler.prototype = {
550549
}
551550

552551
this.push(this.objectLiteral(hash.values));
552+
if (splat) {
553+
let splatIdentifier = 'depth' + splat.depth + '.' + splat.original; // or parts[0] ? what's the different
554+
this.push([this.aliasable('container.splat'), '(', this.popStack(), ', ', splatIdentifier, ')']);
555+
}
553556
},
554557

555558
// [pushString]
@@ -1008,6 +1011,7 @@ JavaScriptCompiler.prototype = {
10081011
}
10091012

10101013
options.name = this.quotedString(helper);
1014+
10111015
options.hash = this.popStack();
10121016

10131017
if (this.trackIds) {

lib/handlebars/compiler/printer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,8 @@ PrintVisitor.prototype.Hash = function(hash) {
168168
PrintVisitor.prototype.HashPair = function(pair) {
169169
return pair.key + '=' + this.accept(pair.value);
170170
};
171+
172+
PrintVisitor.prototype.Splat = function(splat) {
173+
return 'SPLAT{' + this.accept(splat.value) + '}';
174+
};
171175
/* eslint-enable new-cap */

lib/handlebars/runtime.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export function template(templateSpec, env) {
8888
return typeof current === 'function' ? current.call(context) : current;
8989
},
9090

91+
splat: function(current, context) {
92+
return Utils.extend(context, current);
93+
},
94+
9195
escapeExpression: Utils.escapeExpression,
9296
invokePartial: invokePartialWrapper,
9397

spec/helpers.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,82 @@ describe('helpers', function() {
737737
shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo');
738738
});
739739
});
740+
741+
describe('splat operators', function() {
742+
743+
it('basic splat test', function() {
744+
var string = '{{hello **splat }}';
745+
var hash = {splat: {firstName: 'Guybrush', lastName: 'Threepwood'}};
746+
var helpers = {
747+
hello: function(options) {
748+
var hash = options.hash;
749+
return 'Hi, my name is ' + hash.firstName + ' ' + hash.lastName;
750+
}
751+
};
752+
753+
shouldCompileTo(string, [hash, helpers], 'Hi, my name is Guybrush Threepwood');
754+
});
755+
756+
it('fails with multiple splats', function() {
757+
var string = '{{foo **bar **baz}}';
758+
shouldThrow(function() {
759+
CompilerContext.compile(string);
760+
}, Error);
761+
762+
});
763+
764+
it('splat shadowing', function() {
765+
var string = '{{helper **splat occupation="pirate" }}';
766+
var hash = {splat: {occupation: 'cannonball'}};
767+
var helpers = {
768+
helper: function(options) {
769+
var hash = options.hash;
770+
return 'I want to be a ' + hash.occupation + '!';
771+
}
772+
};
773+
774+
shouldCompileTo(string, [hash, helpers], 'I want to be a pirate!');
775+
});
776+
777+
it('splat test', function() {
778+
var template = CompilerContext.compile('{{helper **foo.splat character=character }}');
779+
780+
var helpers = {
781+
helper: function(options) {
782+
return options.hash.character + ' and the ' + options.hash.numberOfHeads + ' monkey on ' + options.hash.island + ' Island';
783+
}
784+
};
785+
786+
var context = {foo: { splat: {numberOfHeads: '3 headed', island: 'Dinky'}}, character: 'Guybrush'};
787+
788+
var result = template(context, {helpers: helpers});
789+
equals(result, 'Guybrush and the 3 headed monkey on Dinky Island', 'Splat test');
790+
});
791+
792+
it('splat with function', function() {
793+
var template = CompilerContext.compile('{{helper **foo character=character }}');
794+
795+
var helpers = {
796+
helper: function(options) {
797+
var hash = options.hash;
798+
return hash.character + ' and the ' + hash.numberOfHeads + ' monkey on ' + hash.format(hash.island) + ' Island';
799+
}
800+
};
801+
802+
var context = {
803+
foo: {
804+
numberOfHeads: '3 headed',
805+
island: 'Dinky',
806+
format: function(str) {
807+
return str.toUpperCase();
808+
}
809+
},
810+
character: 'Guybrush'
811+
};
812+
813+
var result = template(context, {helpers: helpers});
814+
equals(result, 'Guybrush and the 3 headed monkey on DINKY Island', 'Splat function test');
815+
});
816+
817+
});
740818
});

spec/parser.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ describe('parser', function() {
9393
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
9494
});
9595

96+
it('parses splat', function() {
97+
equals(astFor('{{foo **bar}}'), '{{ PATH:foo [] HASH{SPLAT{PATH:bar}} }}\n');
98+
equals(astFor('{{foo ** bar}}'), '{{ PATH:foo [] HASH{SPLAT{PATH:bar}} }}\n');
99+
equals(astFor('{{foo **bar baz=bat}}'), '{{ PATH:foo [] HASH{SPLAT{PATH:bar}, baz=PATH:bat} }}\n');
100+
});
101+
96102
it('parses contents followed by a mustache', function() {
97103
equals(astFor('foo bar {{baz}}'), 'CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n');
98104
});
@@ -215,6 +221,10 @@ describe('parser', function() {
215221
shouldThrow(function() {
216222
astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
217223
}, Error, /goodbyes doesn't match hellos/);
224+
225+
shouldThrow(function() {
226+
astFor('{{foo **}}');
227+
}, Error, /Parse error on line 1/);
218228
});
219229

220230
it('should handle invalid paths', function() {

spec/tokenizer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ describe('Tokenizer', function() {
376376
shouldBeToken(result[2], 'ID', 'omg');
377377
});
378378

379+
it('tokenizes splat', function() {
380+
var result = tokenize('{{foo **bar}}');
381+
shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'ID', 'CLOSE']);
382+
383+
result = tokenize('{{foo ** bar}}');
384+
shouldMatchTokens(result, ['OPEN', 'ID', 'SPLAT', 'ID', 'CLOSE']);
385+
});
386+
379387
it('tokenizes special @ identifiers', function() {
380388
var result = tokenize('{{ @foo }}');
381389
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);

src/handlebars.l

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function strip(start, end) {
1212
LEFT_STRIP "~"
1313
RIGHT_STRIP "~"
1414

15-
LOOKAHEAD [=~}\s\/.)|]
15+
LOOKAHEAD [=~}\s\/.)|*]
1616
LITERAL_LOOKAHEAD [~}\s)]
1717

1818
/*
@@ -119,7 +119,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
119119
<mu>"|" return 'CLOSE_BLOCK_PARAMS';
120120
121121
<mu>{ID} return 'ID';
122-
122+
<mu>"**" return 'SPLAT'
123123
<mu>'['('\\]'|[^\]])*']' yytext = yytext.replace(/\\([\\\]])/g,'$1'); return 'ID';
124124
<mu>. return 'INVALID';
125125

src/handlebars.yy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ mustache
8989
| OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
9090
;
9191

92+
9293
partial
9394
: OPEN_PARTIAL partialName param* hash? CLOSE {
9495
$$ = {
@@ -131,6 +132,7 @@ hash
131132

132133
hashSegment
133134
: ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)}
135+
| SPLAT param -> {type: 'Splat', value: $2, loc: yy.locInfo(@$)}
134136
;
135137

136138
blockParams

0 commit comments

Comments
 (0)