1 /* eslint-disable new-cap */
3 import Exception from '../exception';
4 import {isArray, indexOf} from '../utils';
5 import AST from './ast';
7 const slice = [].slice;
9 export function Compiler() {}
11 // the foundHelper register will disambiguate helper lookup from finding a
12 // function in a context. This is necessary for mustache compatibility, which
13 // requires that context functions in blocks are evaluated by blockHelperMissing,
14 // and then proceed as if the resulting value was provided to blockHelperMissing.
16 Compiler.prototype = {
19 equals: function(other) {
20 let len = this.opcodes.length;
21 if (other.opcodes.length !== len) {
25 for (let i = 0; i < len; i++) {
26 let opcode = this.opcodes[i],
27 otherOpcode = other.opcodes[i];
28 if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
33 // We know that length is the same between the two arrays because they are directly tied
34 // to the opcode behavior above.
35 len = this.children.length;
36 for (let i = 0; i < len; i++) {
37 if (!this.children[i].equals(other.children[i])) {
47 compile: function(program, options) {
51 this.options = options;
52 this.stringParams = options.stringParams;
53 this.trackIds = options.trackIds;
55 options.blockParams = options.blockParams || [];
57 // These changes will propagate to the other compiler components
58 let knownHelpers = options.knownHelpers;
59 options.knownHelpers = {
60 'helperMissing': true,
61 'blockHelperMissing': true,
70 for (let name in knownHelpers) {
71 /* istanbul ignore else */
72 if (name in knownHelpers) {
73 options.knownHelpers[name] = knownHelpers[name];
78 return this.accept(program);
81 compileProgram: function(program) {
82 let childCompiler = new this.compiler(), // eslint-disable-line new-cap
83 result = childCompiler.compile(program, this.options),
86 this.usePartial = this.usePartial || result.usePartial;
88 this.children[guid] = result;
89 this.useDepths = this.useDepths || result.useDepths;
94 accept: function(node) {
95 /* istanbul ignore next: Sanity code */
96 if (!this[node.type]) {
97 throw new Exception('Unknown type: ' + node.type, node);
100 this.sourceNode.unshift(node);
101 let ret = this[node.type](node);
102 this.sourceNode.shift();
106 Program: function(program) {
107 this.options.blockParams.unshift(program.blockParams);
109 let body = program.body,
110 bodyLength = body.length;
111 for (let i = 0; i < bodyLength; i++) {
112 this.accept(body[i]);
115 this.options.blockParams.shift();
117 this.isSimple = bodyLength === 1;
118 this.blockParams = program.blockParams ? program.blockParams.length : 0;
123 BlockStatement: function(block) {
124 transformLiteralToPath(block);
126 let program = block.program,
127 inverse = block.inverse;
129 program = program && this.compileProgram(program);
130 inverse = inverse && this.compileProgram(inverse);
132 let type = this.classifySexpr(block);
134 if (type === 'helper') {
135 this.helperSexpr(block, program, inverse);
136 } else if (type === 'simple') {
137 this.simpleSexpr(block);
139 // now that the simple mustache is resolved, we need to
140 // evaluate it by executing `blockHelperMissing`
141 this.opcode('pushProgram', program);
142 this.opcode('pushProgram', inverse);
143 this.opcode('emptyHash');
144 this.opcode('blockValue', block.path.original);
146 this.ambiguousSexpr(block, program, inverse);
148 // now that the simple mustache is resolved, we need to
149 // evaluate it by executing `blockHelperMissing`
150 this.opcode('pushProgram', program);
151 this.opcode('pushProgram', inverse);
152 this.opcode('emptyHash');
153 this.opcode('ambiguousBlockValue');
156 this.opcode('append');
159 DecoratorBlock(decorator) {
160 let program = decorator.program && this.compileProgram(decorator.program);
161 let params = this.setupFullMustacheParams(decorator, program, undefined),
162 path = decorator.path;
164 this.useDecorators = true;
165 this.opcode('registerDecorator', params.length, path.original);
168 PartialStatement: function(partial) {
169 this.usePartial = true;
171 let program = partial.program;
173 program = this.compileProgram(partial.program);
176 let params = partial.params;
177 if (params.length > 1) {
178 throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
179 } else if (!params.length) {
180 if (this.options.explicitPartialContext) {
181 this.opcode('pushLiteral', 'undefined');
183 params.push({type: 'PathExpression', parts: [], depth: 0});
187 let partialName = partial.name.original,
188 isDynamic = partial.name.type === 'SubExpression';
190 this.accept(partial.name);
193 this.setupFullMustacheParams(partial, program, undefined, true);
195 let indent = partial.indent || '';
196 if (this.options.preventIndent && indent) {
197 this.opcode('appendContent', indent);
201 this.opcode('invokePartial', isDynamic, partialName, indent);
202 this.opcode('append');
204 PartialBlockStatement: function(partialBlock) {
205 this.PartialStatement(partialBlock);
208 MustacheStatement: function(mustache) {
209 this.SubExpression(mustache);
211 if (mustache.escaped && !this.options.noEscape) {
212 this.opcode('appendEscaped');
214 this.opcode('append');
217 Decorator(decorator) {
218 this.DecoratorBlock(decorator);
222 ContentStatement: function(content) {
224 this.opcode('appendContent', content.value);
228 CommentStatement: function() {},
230 SubExpression: function(sexpr) {
231 transformLiteralToPath(sexpr);
232 let type = this.classifySexpr(sexpr);
234 if (type === 'simple') {
235 this.simpleSexpr(sexpr);
236 } else if (type === 'helper') {
237 this.helperSexpr(sexpr);
239 this.ambiguousSexpr(sexpr);
242 ambiguousSexpr: function(sexpr, program, inverse) {
243 let path = sexpr.path,
244 name = path.parts[0],
245 isBlock = program != null || inverse != null;
247 this.opcode('getContext', path.depth);
249 this.opcode('pushProgram', program);
250 this.opcode('pushProgram', inverse);
255 this.opcode('invokeAmbiguous', name, isBlock);
258 simpleSexpr: function(sexpr) {
259 let path = sexpr.path;
262 this.opcode('resolvePossibleLambda');
265 helperSexpr: function(sexpr, program, inverse) {
266 let params = this.setupFullMustacheParams(sexpr, program, inverse),
268 name = path.parts[0];
270 if (this.options.knownHelpers[name]) {
271 this.opcode('invokeKnownHelper', params.length, name);
272 } else if (this.options.knownHelpersOnly) {
273 throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr);
279 this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
283 PathExpression: function(path) {
284 this.addDepth(path.depth);
285 this.opcode('getContext', path.depth);
287 let name = path.parts[0],
288 scoped = AST.helpers.scopedId(path),
289 blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
292 this.opcode('lookupBlockParam', blockParamId, path.parts);
294 // Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
295 this.opcode('pushContext');
296 } else if (path.data) {
297 this.options.data = true;
298 this.opcode('lookupData', path.depth, path.parts, path.strict);
300 this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped);
304 StringLiteral: function(string) {
305 this.opcode('pushString', string.value);
308 NumberLiteral: function(number) {
309 this.opcode('pushLiteral', number.value);
312 BooleanLiteral: function(bool) {
313 this.opcode('pushLiteral', bool.value);
316 UndefinedLiteral: function() {
317 this.opcode('pushLiteral', 'undefined');
320 NullLiteral: function() {
321 this.opcode('pushLiteral', 'null');
324 Hash: function(hash) {
325 let pairs = hash.pairs,
329 this.opcode('pushHash');
332 this.pushParam(pairs[i].value);
335 this.opcode('assignToHash', pairs[i].key);
337 this.opcode('popHash');
341 opcode: function(name) {
342 this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
345 addDepth: function(depth) {
350 this.useDepths = true;
353 classifySexpr: function(sexpr) {
354 let isSimple = AST.helpers.simpleId(sexpr.path);
356 let isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
358 // a mustache is an eligible helper if:
359 // * its id is simple (a single part, not `this` or `..`)
360 let isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
362 // if a mustache is an eligible helper but not a definite
363 // helper, it is ambiguous, and will be resolved in a later
364 // pass or at runtime.
365 let isEligible = !isBlockParam && (isHelper || isSimple);
367 // if ambiguous, we can possibly resolve the ambiguity now
368 // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
369 if (isEligible && !isHelper) {
370 let name = sexpr.path.parts[0],
371 options = this.options;
373 if (options.knownHelpers[name]) {
375 } else if (options.knownHelpersOnly) {
382 } else if (isEligible) {
389 pushParams: function(params) {
390 for (let i = 0, l = params.length; i < l; i++) {
391 this.pushParam(params[i]);
395 pushParam: function(val) {
396 let value = val.value != null ? val.value : val.original || '';
398 if (this.stringParams) {
401 .replace(/^(\.?\.\/)*/g, '')
402 .replace(/\//g, '.');
406 this.addDepth(val.depth);
408 this.opcode('getContext', val.depth || 0);
409 this.opcode('pushStringParam', value, val.type);
411 if (val.type === 'SubExpression') {
412 // SubExpressions get evaluated and passed in
413 // in string params mode.
419 if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
420 blockParamIndex = this.blockParamIndex(val.parts[0]);
422 if (blockParamIndex) {
423 let blockParamChild = val.parts.slice(1).join('.');
424 this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
426 value = val.original || value;
429 .replace(/^this(?:\.|$)/, '')
430 .replace(/^\.\//, '')
431 .replace(/^\.$/, '');
434 this.opcode('pushId', val.type, value);
441 setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
442 let params = sexpr.params;
443 this.pushParams(params);
445 this.opcode('pushProgram', program);
446 this.opcode('pushProgram', inverse);
449 this.accept(sexpr.hash);
451 this.opcode('emptyHash', omitEmpty);
457 blockParamIndex: function(name) {
458 for (let depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
459 let blockParams = this.options.blockParams[depth],
460 param = blockParams && indexOf(blockParams, name);
461 if (blockParams && param >= 0) {
462 return [depth, param];
468 export function precompile(input, options, env) {
469 if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
470 throw new Exception('You must pass a string or Handlebars AST to Handlebars.precompile. You passed ' + input);
473 options = options || {};
474 if (!('data' in options)) {
477 if (options.compat) {
478 options.useDepths = true;
481 let ast = env.parse(input, options),
482 environment = new env.Compiler().compile(ast, options);
483 return new env.JavaScriptCompiler().compile(environment, options);
486 export function compile(input, options = {}, env) {
487 if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
488 throw new Exception('You must pass a string or Handlebars AST to Handlebars.compile. You passed ' + input);
491 if (!('data' in options)) {
494 if (options.compat) {
495 options.useDepths = true;
500 function compileInput() {
501 let ast = env.parse(input, options),
502 environment = new env.Compiler().compile(ast, options),
503 templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
504 return env.template(templateSpec);
507 // Template is only compiled on first use and cached after that point.
508 function ret(context, execOptions) {
510 compiled = compileInput();
512 return compiled.call(this, context, execOptions);
514 ret._setup = function(setupOptions) {
516 compiled = compileInput();
518 return compiled._setup(setupOptions);
520 ret._child = function(i, data, blockParams, depths) {
522 compiled = compileInput();
524 return compiled._child(i, data, blockParams, depths);
529 function argEquals(a, b) {
534 if (isArray(a) && isArray(b) && a.length === b.length) {
535 for (let i = 0; i < a.length; i++) {
536 if (!argEquals(a[i], b[i])) {
544 function transformLiteralToPath(sexpr) {
545 if (!sexpr.path.parts) {
546 let literal = sexpr.path;
547 // Casting to string here to make false and 0 literal values play nicely with the rest
550 type: 'PathExpression',
553 parts: [literal.original + ''],
554 original: literal.original + '',