Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/handlebars/compiler/code-gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, ')']);
},

Expand Down
64 changes: 45 additions & 19 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -199,6 +199,7 @@ Compiler.prototype = {
this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append');
},

PartialBlockStatement: function(partialBlock) {
this.PartialStatement(partialBlock);
},
Expand All @@ -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);
Expand Down Expand Up @@ -261,20 +262,22 @@ 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 {
path.strict = true;
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);
}
},

Expand Down Expand Up @@ -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');
},

Expand Down Expand Up @@ -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);
Expand All @@ -407,7 +433,7 @@ Compiler.prototype = {
this.opcode('emptyHash', omitEmpty);
}

return params;
return { params, splatMap };
},

blockParamIndex: function(name) {
Expand Down
92 changes: 74 additions & 18 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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, ' || '] : '';
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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 = {};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on performance/code size of this vs. array with missing values notation? I.e. [,,1] vs. {"2":1}.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, don't have the perf wizard knowledge myself.

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, ...
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
};
},

Expand All @@ -930,6 +985,7 @@ JavaScriptCompiler.prototype = {
}

options.name = this.quotedString(helper);

options.hash = this.popStack();

let inverse = this.popStack(),
Expand Down
8 changes: 7 additions & 1 deletion lib/handlebars/compiler/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
};


Expand Down Expand Up @@ -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 */
16 changes: 16 additions & 0 deletions lib/handlebars/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the first argument passed here? Are we ok with it being modified in all cases?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw this so please disregard :) Do we have test coverage asserting that the first object is not mutated?

},

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,

Expand Down
Loading