1import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
2import Exception from '../exception';
3import { isArray } from '../utils';
4import CodeGen from './code-gen';
5
6function Literal(value) {
7  this.value = value;
8}
9
10function JavaScriptCompiler() {}
11
12JavaScriptCompiler.prototype = {
13  // PUBLIC API: You can override these methods in a subclass to provide
14  // alternative compiled forms for name lookup and buffering semantics
15  nameLookup: function(parent, name /*,  type */) {
16    return this.internalNameLookup(parent, name);
17  },
18  depthedLookup: function(name) {
19    return [
20      this.aliasable('container.lookup'),
21      '(depths, ',
22      JSON.stringify(name),
23      ')'
24    ];
25  },
26
27  compilerInfo: function() {
28    const revision = COMPILER_REVISION,
29      versions = REVISION_CHANGES[revision];
30    return [revision, versions];
31  },
32
33  appendToBuffer: function(source, location, explicit) {
34    // Force a source as this simplifies the merge logic.
35    if (!isArray(source)) {
36      source = [source];
37    }
38    source = this.source.wrap(source, location);
39
40    if (this.environment.isSimple) {
41      return ['return ', source, ';'];
42    } else if (explicit) {
43      // This is a case where the buffer operation occurs as a child of another
44      // construct, generally braces. We have to explicitly output these buffer
45      // operations to ensure that the emitted code goes in the correct location.
46      return ['buffer += ', source, ';'];
47    } else {
48      source.appendToBuffer = true;
49      return source;
50    }
51  },
52
53  initializeBuffer: function() {
54    return this.quotedString('');
55  },
56  // END PUBLIC API
57  internalNameLookup: function(parent, name) {
58    this.lookupPropertyFunctionIsUsed = true;
59    return ['lookupProperty(', parent, ',', JSON.stringify(name), ')'];
60  },
61
62  lookupPropertyFunctionIsUsed: false,
63
64  compile: function(environment, options, context, asObject) {
65    this.environment = environment;
66    this.options = options;
67    this.stringParams = this.options.stringParams;
68    this.trackIds = this.options.trackIds;
69    this.precompile = !asObject;
70
71    this.name = this.environment.name;
72    this.isChild = !!context;
73    this.context = context || {
74      decorators: [],
75      programs: [],
76      environments: []
77    };
78
79    this.preamble();
80
81    this.stackSlot = 0;
82    this.stackVars = [];
83    this.aliases = {};
84    this.registers = { list: [] };
85    this.hashes = [];
86    this.compileStack = [];
87    this.inlineStack = [];
88    this.blockParams = [];
89
90    this.compileChildren(environment, options);
91
92    this.useDepths =
93      this.useDepths ||
94      environment.useDepths ||
95      environment.useDecorators ||
96      this.options.compat;
97    this.useBlockParams = this.useBlockParams || environment.useBlockParams;
98
99    let opcodes = environment.opcodes,
100      opcode,
101      firstLoc,
102      i,
103      l;
104
105    for (i = 0, l = opcodes.length; i < l; i++) {
106      opcode = opcodes[i];
107
108      this.source.currentLocation = opcode.loc;
109      firstLoc = firstLoc || opcode.loc;
110      this[opcode.opcode].apply(this, opcode.args);
111    }
112
113    // Flush any trailing content that might be pending.
114    this.source.currentLocation = firstLoc;
115    this.pushSource('');
116
117    /* istanbul ignore next */
118    if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
119      throw new Exception('Compile completed with content left on stack');
120    }
121
122    if (!this.decorators.isEmpty()) {
123      this.useDecorators = true;
124
125      this.decorators.prepend([
126        'var decorators = container.decorators, ',
127        this.lookupPropertyFunctionVarDeclaration(),
128        ';\n'
129      ]);
130      this.decorators.push('return fn;');
131
132      if (asObject) {
133        this.decorators = Function.apply(this, [
134          'fn',
135          'props',
136          'container',
137          'depth0',
138          'data',
139          'blockParams',
140          'depths',
141          this.decorators.merge()
142        ]);
143      } else {
144        this.decorators.prepend(
145          'function(fn, props, container, depth0, data, blockParams, depths) {\n'
146        );
147        this.decorators.push('}\n');
148        this.decorators = this.decorators.merge();
149      }
150    } else {
151      this.decorators = undefined;
152    }
153
154    let fn = this.createFunctionContext(asObject);
155    if (!this.isChild) {
156      let ret = {
157        compiler: this.compilerInfo(),
158        main: fn
159      };
160
161      if (this.decorators) {
162        ret.main_d = this.decorators; // eslint-disable-line camelcase
163        ret.useDecorators = true;
164      }
165
166      let { programs, decorators } = this.context;
167      for (i = 0, l = programs.length; i < l; i++) {
168        if (programs[i]) {
169          ret[i] = programs[i];
170          if (decorators[i]) {
171            ret[i + '_d'] = decorators[i];
172            ret.useDecorators = true;
173          }
174        }
175      }
176
177      if (this.environment.usePartial) {
178        ret.usePartial = true;
179      }
180      if (this.options.data) {
181        ret.useData = true;
182      }
183      if (this.useDepths) {
184        ret.useDepths = true;
185      }
186      if (this.useBlockParams) {
187        ret.useBlockParams = true;
188      }
189      if (this.options.compat) {
190        ret.compat = true;
191      }
192
193      if (!asObject) {
194        ret.compiler = JSON.stringify(ret.compiler);
195
196        this.source.currentLocation = { start: { line: 1, column: 0 } };
197        ret = this.objectLiteral(ret);
198
199        if (options.srcName) {
200          ret = ret.toStringWithSourceMap({ file: options.destName });
201          ret.map = ret.map && ret.map.toString();
202        } else {
203          ret = ret.toString();
204        }
205      } else {
206        ret.compilerOptions = this.options;
207      }
208
209      return ret;
210    } else {
211      return fn;
212    }
213  },
214
215  preamble: function() {
216    // track the last context pushed into place to allow skipping the
217    // getContext opcode when it would be a noop
218    this.lastContext = 0;
219    this.source = new CodeGen(this.options.srcName);
220    this.decorators = new CodeGen(this.options.srcName);
221  },
222
223  createFunctionContext: function(asObject) {
224    let varDeclarations = '';
225
226    let locals = this.stackVars.concat(this.registers.list);
227    if (locals.length > 0) {
228      varDeclarations += ', ' + locals.join(', ');
229    }
230
231    // Generate minimizer alias mappings
232    //
233    // When using true SourceNodes, this will update all references to the given alias
234    // as the source nodes are reused in situ. For the non-source node compilation mode,
235    // aliases will not be used, but this case is already being run on the client and
236    // we aren't concern about minimizing the template size.
237    let aliasCount = 0;
238    Object.keys(this.aliases).forEach(alias => {
239      let node = this.aliases[alias];
240      if (node.children && node.referenceCount > 1) {
241        varDeclarations += ', alias' + ++aliasCount + '=' + alias;
242        node.children[0] = 'alias' + aliasCount;
243      }
244    });
245
246    if (this.lookupPropertyFunctionIsUsed) {
247      varDeclarations += ', ' + this.lookupPropertyFunctionVarDeclaration();
248    }
249
250    let params = ['container', 'depth0', 'helpers', 'partials', 'data'];
251
252    if (this.useBlockParams || this.useDepths) {
253      params.push('blockParams');
254    }
255    if (this.useDepths) {
256      params.push('depths');
257    }
258
259    // Perform a second pass over the output to merge content when possible
260    let source = this.mergeSource(varDeclarations);
261
262    if (asObject) {
263      params.push(source);
264
265      return Function.apply(this, params);
266    } else {
267      return this.source.wrap([
268        'function(',
269        params.join(','),
270        ') {\n  ',
271        source,
272        '}'
273      ]);
274    }
275  },
276  mergeSource: function(varDeclarations) {
277    let isSimple = this.environment.isSimple,
278      appendOnly = !this.forceBuffer,
279      appendFirst,
280      sourceSeen,
281      bufferStart,
282      bufferEnd;
283    this.source.each(line => {
284      if (line.appendToBuffer) {
285        if (bufferStart) {
286          line.prepend('  + ');
287        } else {
288          bufferStart = line;
289        }
290        bufferEnd = line;
291      } else {
292        if (bufferStart) {
293          if (!sourceSeen) {
294            appendFirst = true;
295          } else {
296            bufferStart.prepend('buffer += ');
297          }
298          bufferEnd.add(';');
299          bufferStart = bufferEnd = undefined;
300        }
301
302        sourceSeen = true;
303        if (!isSimple) {
304          appendOnly = false;
305        }
306      }
307    });
308
309    if (appendOnly) {
310      if (bufferStart) {
311        bufferStart.prepend('return ');
312        bufferEnd.add(';');
313      } else if (!sourceSeen) {
314        this.source.push('return "";');
315      }
316    } else {
317      varDeclarations +=
318        ', buffer = ' + (appendFirst ? '' : this.initializeBuffer());
319
320      if (bufferStart) {
321        bufferStart.prepend('return buffer + ');
322        bufferEnd.add(';');
323      } else {
324        this.source.push('return buffer;');
325      }
326    }
327
328    if (varDeclarations) {
329      this.source.prepend(
330        'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n')
331      );
332    }
333
334    return this.source.merge();
335  },
336
337  lookupPropertyFunctionVarDeclaration: function() {
338    return `
339      lookupProperty = container.lookupProperty || function(parent, propertyName) {
340        if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
341          return parent[propertyName];
342        }
343        return undefined
344    }
345    `.trim();
346  },
347
348  // [blockValue]
349  //
350  // On stack, before: hash, inverse, program, value
351  // On stack, after: return value of blockHelperMissing
352  //
353  // The purpose of this opcode is to take a block of the form
354  // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
355  // replace it on the stack with the result of properly
356  // invoking blockHelperMissing.
357  blockValue: function(name) {
358    let blockHelperMissing = this.aliasable(
359        'container.hooks.blockHelperMissing'
360      ),
361      params = [this.contextName(0)];
362    this.setupHelperArgs(name, 0, params);
363
364    let blockName = this.popStack();
365    params.splice(1, 0, blockName);
366
367    this.push(this.source.functionCall(blockHelperMissing, 'call', params));
368  },
369
370  // [ambiguousBlockValue]
371  //
372  // On stack, before: hash, inverse, program, value
373  // Compiler value, before: lastHelper=value of last found helper, if any
374  // On stack, after, if no lastHelper: same as [blockValue]
375  // On stack, after, if lastHelper: value
376  ambiguousBlockValue: function() {
377    // We're being a bit cheeky and reusing the options value from the prior exec
378    let blockHelperMissing = this.aliasable(
379        'container.hooks.blockHelperMissing'
380      ),
381      params = [this.contextName(0)];
382    this.setupHelperArgs('', 0, params, true);
383
384    this.flushInline();
385
386    let current = this.topStack();
387    params.splice(1, 0, current);
388
389    this.pushSource([
390      'if (!',
391      this.lastHelper,
392      ') { ',
393      current,
394      ' = ',
395      this.source.functionCall(blockHelperMissing, 'call', params),
396      '}'
397    ]);
398  },
399
400  // [appendContent]
401  //
402  // On stack, before: ...
403  // On stack, after: ...
404  //
405  // Appends the string value of `content` to the current buffer
406  appendContent: function(content) {
407    if (this.pendingContent) {
408      content = this.pendingContent + content;
409    } else {
410      this.pendingLocation = this.source.currentLocation;
411    }
412
413    this.pendingContent = content;
414  },
415
416  // [append]
417  //
418  // On stack, before: value, ...
419  // On stack, after: ...
420  //
421  // Coerces `value` to a String and appends it to the current buffer.
422  //
423  // If `value` is truthy, or 0, it is coerced into a string and appended
424  // Otherwise, the empty string is appended
425  append: function() {
426    if (this.isInline()) {
427      this.replaceStack(current => [' != null ? ', current, ' : ""']);
428
429      this.pushSource(this.appendToBuffer(this.popStack()));
430    } else {
431      let local = this.popStack();
432      this.pushSource([
433        'if (',
434        local,
435        ' != null) { ',
436        this.appendToBuffer(local, undefined, true),
437        ' }'
438      ]);
439      if (this.environment.isSimple) {
440        this.pushSource([
441          'else { ',
442          this.appendToBuffer("''", undefined, true),
443          ' }'
444        ]);
445      }
446    }
447  },
448
449  // [appendEscaped]
450  //
451  // On stack, before: value, ...
452  // On stack, after: ...
453  //
454  // Escape `value` and append it to the buffer
455  appendEscaped: function() {
456    this.pushSource(
457      this.appendToBuffer([
458        this.aliasable('container.escapeExpression'),
459        '(',
460        this.popStack(),
461        ')'
462      ])
463    );
464  },
465
466  // [getContext]
467  //
468  // On stack, before: ...
469  // On stack, after: ...
470  // Compiler value, after: lastContext=depth
471  //
472  // Set the value of the `lastContext` compiler value to the depth
473  getContext: function(depth) {
474    this.lastContext = depth;
475  },
476
477  // [pushContext]
478  //
479  // On stack, before: ...
480  // On stack, after: currentContext, ...
481  //
482  // Pushes the value of the current context onto the stack.
483  pushContext: function() {
484    this.pushStackLiteral(this.contextName(this.lastContext));
485  },
486
487  // [lookupOnContext]
488  //
489  // On stack, before: ...
490  // On stack, after: currentContext[name], ...
491  //
492  // Looks up the value of `name` on the current context and pushes
493  // it onto the stack.
494  lookupOnContext: function(parts, falsy, strict, scoped) {
495    let i = 0;
496
497    if (!scoped && this.options.compat && !this.lastContext) {
498      // The depthed query is expected to handle the undefined logic for the root level that
499      // is implemented below, so we evaluate that directly in compat mode
500      this.push(this.depthedLookup(parts[i++]));
501    } else {
502      this.pushContext();
503    }
504
505    this.resolvePath('context', parts, i, falsy, strict);
506  },
507
508  // [lookupBlockParam]
509  //
510  // On stack, before: ...
511  // On stack, after: blockParam[name], ...
512  //
513  // Looks up the value of `parts` on the given block param and pushes
514  // it onto the stack.
515  lookupBlockParam: function(blockParamId, parts) {
516    this.useBlockParams = true;
517
518    this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']);
519    this.resolvePath('context', parts, 1);
520  },
521
522  // [lookupData]
523  //
524  // On stack, before: ...
525  // On stack, after: data, ...
526  //
527  // Push the data lookup operator
528  lookupData: function(depth, parts, strict) {
529    if (!depth) {
530      this.pushStackLiteral('data');
531    } else {
532      this.pushStackLiteral('container.data(data, ' + depth + ')');
533    }
534
535    this.resolvePath('data', parts, 0, true, strict);
536  },
537
538  resolvePath: function(type, parts, i, falsy, strict) {
539    if (this.options.strict || this.options.assumeObjects) {
540      this.push(
541        strictLookup(this.options.strict && strict, this, parts, i, type)
542      );
543      return;
544    }
545
546    let len = parts.length;
547    for (; i < len; i++) {
548      /* eslint-disable no-loop-func */
549      this.replaceStack(current => {
550        let lookup = this.nameLookup(current, parts[i], type);
551        // We want to ensure that zero and false are handled properly if the context (falsy flag)
552        // needs to have the special handling for these values.
553        if (!falsy) {
554          return [' != null ? ', lookup, ' : ', current];
555        } else {
556          // Otherwise we can use generic falsy handling
557          return [' && ', lookup];
558        }
559      });
560      /* eslint-enable no-loop-func */
561    }
562  },
563
564  // [resolvePossibleLambda]
565  //
566  // On stack, before: value, ...
567  // On stack, after: resolved value, ...
568  //
569  // If the `value` is a lambda, replace it on the stack by
570  // the return value of the lambda
571  resolvePossibleLambda: function() {
572    this.push([
573      this.aliasable('container.lambda'),
574      '(',
575      this.popStack(),
576      ', ',
577      this.contextName(0),
578      ')'
579    ]);
580  },
581
582  // [pushStringParam]
583  //
584  // On stack, before: ...
585  // On stack, after: string, currentContext, ...
586  //
587  // This opcode is designed for use in string mode, which
588  // provides the string value of a parameter along with its
589  // depth rather than resolving it immediately.
590  pushStringParam: function(string, type) {
591    this.pushContext();
592    this.pushString(type);
593
594    // If it's a subexpression, the string result
595    // will be pushed after this opcode.
596    if (type !== 'SubExpression') {
597      if (typeof string === 'string') {
598        this.pushString(string);
599      } else {
600        this.pushStackLiteral(string);
601      }
602    }
603  },
604
605  emptyHash: function(omitEmpty) {
606    if (this.trackIds) {
607      this.push('{}'); // hashIds
608    }
609    if (this.stringParams) {
610      this.push('{}'); // hashContexts
611      this.push('{}'); // hashTypes
612    }
613    this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
614  },
615  pushHash: function() {
616    if (this.hash) {
617      this.hashes.push(this.hash);
618    }
619    this.hash = { values: {}, types: [], contexts: [], ids: [] };
620  },
621  popHash: function() {
622    let hash = this.hash;
623    this.hash = this.hashes.pop();
624
625    if (this.trackIds) {
626      this.push(this.objectLiteral(hash.ids));
627    }
628    if (this.stringParams) {
629      this.push(this.objectLiteral(hash.contexts));
630      this.push(this.objectLiteral(hash.types));
631    }
632
633    this.push(this.objectLiteral(hash.values));
634  },
635
636  // [pushString]
637  //
638  // On stack, before: ...
639  // On stack, after: quotedString(string), ...
640  //
641  // Push a quoted version of `string` onto the stack
642  pushString: function(string) {
643    this.pushStackLiteral(this.quotedString(string));
644  },
645
646  // [pushLiteral]
647  //
648  // On stack, before: ...
649  // On stack, after: value, ...
650  //
651  // Pushes a value onto the stack. This operation prevents
652  // the compiler from creating a temporary variable to hold
653  // it.
654  pushLiteral: function(value) {
655    this.pushStackLiteral(value);
656  },
657
658  // [pushProgram]
659  //
660  // On stack, before: ...
661  // On stack, after: program(guid), ...
662  //
663  // Push a program expression onto the stack. This takes
664  // a compile-time guid and converts it into a runtime-accessible
665  // expression.
666  pushProgram: function(guid) {
667    if (guid != null) {
668      this.pushStackLiteral(this.programExpression(guid));
669    } else {
670      this.pushStackLiteral(null);
671    }
672  },
673
674  // [registerDecorator]
675  //
676  // On stack, before: hash, program, params..., ...
677  // On stack, after: ...
678  //
679  // Pops off the decorator's parameters, invokes the decorator,
680  // and inserts the decorator into the decorators list.
681  registerDecorator(paramSize, name) {
682    let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
683      options = this.setupHelperArgs(name, paramSize);
684
685    this.decorators.push([
686      'fn = ',
687      this.decorators.functionCall(foundDecorator, '', [
688        'fn',
689        'props',
690        'container',
691        options
692      ]),
693      ' || fn;'
694    ]);
695  },
696
697  // [invokeHelper]
698  //
699  // On stack, before: hash, inverse, program, params..., ...
700  // On stack, after: result of helper invocation
701  //
702  // Pops off the helper's parameters, invokes the helper,
703  // and pushes the helper's return value onto the stack.
704  //
705  // If the helper is not found, `helperMissing` is called.
706  invokeHelper: function(paramSize, name, isSimple) {
707    let nonHelper = this.popStack(),
708      helper = this.setupHelper(paramSize, name);
709
710    let possibleFunctionCalls = [];
711
712    if (isSimple) {
713      // direct call to helper
714      possibleFunctionCalls.push(helper.name);
715    }
716    // call a function from the input object
717    possibleFunctionCalls.push(nonHelper);
718    if (!this.options.strict) {
719      possibleFunctionCalls.push(
720        this.aliasable('container.hooks.helperMissing')
721      );
722    }
723
724    let functionLookupCode = [
725      '(',
726      this.itemsSeparatedBy(possibleFunctionCalls, '||'),
727      ')'
728    ];
729    let functionCall = this.source.functionCall(
730      functionLookupCode,
731      'call',
732      helper.callParams
733    );
734    this.push(functionCall);
735  },
736
737  itemsSeparatedBy: function(items, separator) {
738    let result = [];
739    result.push(items[0]);
740    for (let i = 1; i < items.length; i++) {
741      result.push(separator, items[i]);
742    }
743    return result;
744  },
745  // [invokeKnownHelper]
746  //
747  // On stack, before: hash, inverse, program, params..., ...
748  // On stack, after: result of helper invocation
749  //
750  // This operation is used when the helper is known to exist,
751  // so a `helperMissing` fallback is not required.
752  invokeKnownHelper: function(paramSize, name) {
753    let helper = this.setupHelper(paramSize, name);
754    this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
755  },
756
757  // [invokeAmbiguous]
758  //
759  // On stack, before: hash, inverse, program, params..., ...
760  // On stack, after: result of disambiguation
761  //
762  // This operation is used when an expression like `{{foo}}`
763  // is provided, but we don't know at compile-time whether it
764  // is a helper or a path.
765  //
766  // This operation emits more code than the other options,
767  // and can be avoided by passing the `knownHelpers` and
768  // `knownHelpersOnly` flags at compile-time.
769  invokeAmbiguous: function(name, helperCall) {
770    this.useRegister('helper');
771
772    let nonHelper = this.popStack();
773
774    this.emptyHash();
775    let helper = this.setupHelper(0, name, helperCall);
776
777    let helperName = (this.lastHelper = this.nameLookup(
778      'helpers',
779      name,
780      'helper'
781    ));
782
783    let lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')'];
784    if (!this.options.strict) {
785      lookup[0] = '(helper = ';
786      lookup.push(
787        ' != null ? helper : ',
788        this.aliasable('container.hooks.helperMissing')
789      );
790    }
791
792    this.push([
793      '(',
794      lookup,
795      helper.paramsInit ? ['),(', helper.paramsInit] : [],
796      '),',
797      '(typeof helper === ',
798      this.aliasable('"function"'),
799      ' ? ',
800      this.source.functionCall('helper', 'call', helper.callParams),
801      ' : helper))'
802    ]);
803  },
804
805  // [invokePartial]
806  //
807  // On stack, before: context, ...
808  // On stack after: result of partial invocation
809  //
810  // This operation pops off a context, invokes a partial with that context,
811  // and pushes the result of the invocation back.
812  invokePartial: function(isDynamic, name, indent) {
813    let params = [],
814      options = this.setupParams(name, 1, params);
815
816    if (isDynamic) {
817      name = this.popStack();
818      delete options.name;
819    }
820
821    if (indent) {
822      options.indent = JSON.stringify(indent);
823    }
824    options.helpers = 'helpers';
825    options.partials = 'partials';
826    options.decorators = 'container.decorators';
827
828    if (!isDynamic) {
829      params.unshift(this.nameLookup('partials', name, 'partial'));
830    } else {
831      params.unshift(name);
832    }
833
834    if (this.options.compat) {
835      options.depths = 'depths';
836    }
837    options = this.objectLiteral(options);
838    params.push(options);
839
840    this.push(this.source.functionCall('container.invokePartial', '', params));
841  },
842
843  // [assignToHash]
844  //
845  // On stack, before: value, ..., hash, ...
846  // On stack, after: ..., hash, ...
847  //
848  // Pops a value off the stack and assigns it to the current hash
849  assignToHash: function(key) {
850    let value = this.popStack(),
851      context,
852      type,
853      id;
854
855    if (this.trackIds) {
856      id = this.popStack();
857    }
858    if (this.stringParams) {
859      type = this.popStack();
860      context = this.popStack();
861    }
862
863    let hash = this.hash;
864    if (context) {
865      hash.contexts[key] = context;
866    }
867    if (type) {
868      hash.types[key] = type;
869    }
870    if (id) {
871      hash.ids[key] = id;
872    }
873    hash.values[key] = value;
874  },
875
876  pushId: function(type, name, child) {
877    if (type === 'BlockParam') {
878      this.pushStackLiteral(
879        'blockParams[' +
880          name[0] +
881          '].path[' +
882          name[1] +
883          ']' +
884          (child ? ' + ' + JSON.stringify('.' + child) : '')
885      );
886    } else if (type === 'PathExpression') {
887      this.pushString(name);
888    } else if (type === 'SubExpression') {
889      this.pushStackLiteral('true');
890    } else {
891      this.pushStackLiteral('null');
892    }
893  },
894
895  // HELPERS
896
897  compiler: JavaScriptCompiler,
898
899  compileChildren: function(environment, options) {
900    let children = environment.children,
901      child,
902      compiler;
903
904    for (let i = 0, l = children.length; i < l; i++) {
905      child = children[i];
906      compiler = new this.compiler(); // eslint-disable-line new-cap
907
908      let existing = this.matchExistingProgram(child);
909
910      if (existing == null) {
911        this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
912        let index = this.context.programs.length;
913        child.index = index;
914        child.name = 'program' + index;
915        this.context.programs[index] = compiler.compile(
916          child,
917          options,
918          this.context,
919          !this.precompile
920        );
921        this.context.decorators[index] = compiler.decorators;
922        this.context.environments[index] = child;
923
924        this.useDepths = this.useDepths || compiler.useDepths;
925        this.useBlockParams = this.useBlockParams || compiler.useBlockParams;
926        child.useDepths = this.useDepths;
927        child.useBlockParams = this.useBlockParams;
928      } else {
929        child.index = existing.index;
930        child.name = 'program' + existing.index;
931
932        this.useDepths = this.useDepths || existing.useDepths;
933        this.useBlockParams = this.useBlockParams || existing.useBlockParams;
934      }
935    }
936  },
937  matchExistingProgram: function(child) {
938    for (let i = 0, len = this.context.environments.length; i < len; i++) {
939      let environment = this.context.environments[i];
940      if (environment && environment.equals(child)) {
941        return environment;
942      }
943    }
944  },
945
946  programExpression: function(guid) {
947    let child = this.environment.children[guid],
948      programParams = [child.index, 'data', child.blockParams];
949
950    if (this.useBlockParams || this.useDepths) {
951      programParams.push('blockParams');
952    }
953    if (this.useDepths) {
954      programParams.push('depths');
955    }
956
957    return 'container.program(' + programParams.join(', ') + ')';
958  },
959
960  useRegister: function(name) {
961    if (!this.registers[name]) {
962      this.registers[name] = true;
963      this.registers.list.push(name);
964    }
965  },
966
967  push: function(expr) {
968    if (!(expr instanceof Literal)) {
969      expr = this.source.wrap(expr);
970    }
971
972    this.inlineStack.push(expr);
973    return expr;
974  },
975
976  pushStackLiteral: function(item) {
977    this.push(new Literal(item));
978  },
979
980  pushSource: function(source) {
981    if (this.pendingContent) {
982      this.source.push(
983        this.appendToBuffer(
984          this.source.quotedString(this.pendingContent),
985          this.pendingLocation
986        )
987      );
988      this.pendingContent = undefined;
989    }
990
991    if (source) {
992      this.source.push(source);
993    }
994  },
995
996  replaceStack: function(callback) {
997    let prefix = ['('],
998      stack,
999      createdStack,
1000      usedLiteral;
1001
1002    /* istanbul ignore next */
1003    if (!this.isInline()) {
1004      throw new Exception('replaceStack on non-inline');
1005    }
1006
1007    // We want to merge the inline statement into the replacement statement via ','
1008    let top = this.popStack(true);
1009
1010    if (top instanceof Literal) {
1011      // Literals do not need to be inlined
1012      stack = [top.value];
1013      prefix = ['(', stack];
1014      usedLiteral = true;
1015    } else {
1016      // Get or create the current stack name for use by the inline
1017      createdStack = true;
1018      let name = this.incrStack();
1019
1020      prefix = ['((', this.push(name), ' = ', top, ')'];
1021      stack = this.topStack();
1022    }
1023
1024    let item = callback.call(this, stack);
1025
1026    if (!usedLiteral) {
1027      this.popStack();
1028    }
1029    if (createdStack) {
1030      this.stackSlot--;
1031    }
1032    this.push(prefix.concat(item, ')'));
1033  },
1034
1035  incrStack: function() {
1036    this.stackSlot++;
1037    if (this.stackSlot > this.stackVars.length) {
1038      this.stackVars.push('stack' + this.stackSlot);
1039    }
1040    return this.topStackName();
1041  },
1042  topStackName: function() {
1043    return 'stack' + this.stackSlot;
1044  },
1045  flushInline: function() {
1046    let inlineStack = this.inlineStack;
1047    this.inlineStack = [];
1048    for (let i = 0, len = inlineStack.length; i < len; i++) {
1049      let entry = inlineStack[i];
1050      /* istanbul ignore if */
1051      if (entry instanceof Literal) {
1052        this.compileStack.push(entry);
1053      } else {
1054        let stack = this.incrStack();
1055        this.pushSource([stack, ' = ', entry, ';']);
1056        this.compileStack.push(stack);
1057      }
1058    }
1059  },
1060  isInline: function() {
1061    return this.inlineStack.length;
1062  },
1063
1064  popStack: function(wrapped) {
1065    let inline = this.isInline(),
1066      item = (inline ? this.inlineStack : this.compileStack).pop();
1067
1068    if (!wrapped && item instanceof Literal) {
1069      return item.value;
1070    } else {
1071      if (!inline) {
1072        /* istanbul ignore next */
1073        if (!this.stackSlot) {
1074          throw new Exception('Invalid stack pop');
1075        }
1076        this.stackSlot--;
1077      }
1078      return item;
1079    }
1080  },
1081
1082  topStack: function() {
1083    let stack = this.isInline() ? this.inlineStack : this.compileStack,
1084      item = stack[stack.length - 1];
1085
1086    /* istanbul ignore if */
1087    if (item instanceof Literal) {
1088      return item.value;
1089    } else {
1090      return item;
1091    }
1092  },
1093
1094  contextName: function(context) {
1095    if (this.useDepths && context) {
1096      return 'depths[' + context + ']';
1097    } else {
1098      return 'depth' + context;
1099    }
1100  },
1101
1102  quotedString: function(str) {
1103    return this.source.quotedString(str);
1104  },
1105
1106  objectLiteral: function(obj) {
1107    return this.source.objectLiteral(obj);
1108  },
1109
1110  aliasable: function(name) {
1111    let ret = this.aliases[name];
1112    if (ret) {
1113      ret.referenceCount++;
1114      return ret;
1115    }
1116
1117    ret = this.aliases[name] = this.source.wrap(name);
1118    ret.aliasable = true;
1119    ret.referenceCount = 1;
1120
1121    return ret;
1122  },
1123
1124  setupHelper: function(paramSize, name, blockHelper) {
1125    let params = [],
1126      paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
1127    let foundHelper = this.nameLookup('helpers', name, 'helper'),
1128      callContext = this.aliasable(
1129        `${this.contextName(0)} != null ? ${this.contextName(
1130          0
1131        )} : (container.nullContext || {})`
1132      );
1133
1134    return {
1135      params: params,
1136      paramsInit: paramsInit,
1137      name: foundHelper,
1138      callParams: [callContext].concat(params)
1139    };
1140  },
1141
1142  setupParams: function(helper, paramSize, params) {
1143    let options = {},
1144      contexts = [],
1145      types = [],
1146      ids = [],
1147      objectArgs = !params,
1148      param;
1149
1150    if (objectArgs) {
1151      params = [];
1152    }
1153
1154    options.name = this.quotedString(helper);
1155    options.hash = this.popStack();
1156
1157    if (this.trackIds) {
1158      options.hashIds = this.popStack();
1159    }
1160    if (this.stringParams) {
1161      options.hashTypes = this.popStack();
1162      options.hashContexts = this.popStack();
1163    }
1164
1165    let inverse = this.popStack(),
1166      program = this.popStack();
1167
1168    // Avoid setting fn and inverse if neither are set. This allows
1169    // helpers to do a check for `if (options.fn)`
1170    if (program || inverse) {
1171      options.fn = program || 'container.noop';
1172      options.inverse = inverse || 'container.noop';
1173    }
1174
1175    // The parameters go on to the stack in order (making sure that they are evaluated in order)
1176    // so we need to pop them off the stack in reverse order
1177    let i = paramSize;
1178    while (i--) {
1179      param = this.popStack();
1180      params[i] = param;
1181
1182      if (this.trackIds) {
1183        ids[i] = this.popStack();
1184      }
1185      if (this.stringParams) {
1186        types[i] = this.popStack();
1187        contexts[i] = this.popStack();
1188      }
1189    }
1190
1191    if (objectArgs) {
1192      options.args = this.source.generateArray(params);
1193    }
1194
1195    if (this.trackIds) {
1196      options.ids = this.source.generateArray(ids);
1197    }
1198    if (this.stringParams) {
1199      options.types = this.source.generateArray(types);
1200      options.contexts = this.source.generateArray(contexts);
1201    }
1202
1203    if (this.options.data) {
1204      options.data = 'data';
1205    }
1206    if (this.useBlockParams) {
1207      options.blockParams = 'blockParams';
1208    }
1209    return options;
1210  },
1211
1212  setupHelperArgs: function(helper, paramSize, params, useRegister) {
1213    let options = this.setupParams(helper, paramSize, params);
1214    options.loc = JSON.stringify(this.source.currentLocation);
1215    options = this.objectLiteral(options);
1216    if (useRegister) {
1217      this.useRegister('options');
1218      params.push('options');
1219      return ['options=', options];
1220    } else if (params) {
1221      params.push(options);
1222      return '';
1223    } else {
1224      return options;
1225    }
1226  }
1227};
1228
1229(function() {
1230  const reservedWords = (
1231    'break else new var' +
1232    ' case finally return void' +
1233    ' catch for switch while' +
1234    ' continue function this with' +
1235    ' default if throw' +
1236    ' delete in try' +
1237    ' do instanceof typeof' +
1238    ' abstract enum int short' +
1239    ' boolean export interface static' +
1240    ' byte extends long super' +
1241    ' char final native synchronized' +
1242    ' class float package throws' +
1243    ' const goto private transient' +
1244    ' debugger implements protected volatile' +
1245    ' double import public let yield await' +
1246    ' null true false'
1247  ).split(' ');
1248
1249  const compilerWords = (JavaScriptCompiler.RESERVED_WORDS = {});
1250
1251  for (let i = 0, l = reservedWords.length; i < l; i++) {
1252    compilerWords[reservedWords[i]] = true;
1253  }
1254})();
1255
1256/**
1257 * @deprecated May be removed in the next major version
1258 */
1259JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
1260  return (
1261    !JavaScriptCompiler.RESERVED_WORDS[name] &&
1262    /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name)
1263  );
1264};
1265
1266function strictLookup(requireTerminal, compiler, parts, i, type) {
1267  let stack = compiler.popStack(),
1268    len = parts.length;
1269  if (requireTerminal) {
1270    len--;
1271  }
1272
1273  for (; i < len; i++) {
1274    stack = compiler.nameLookup(stack, parts[i], type);
1275  }
1276
1277  if (requireTerminal) {
1278    return [
1279      compiler.aliasable('container.strict'),
1280      '(',
1281      stack,
1282      ', ',
1283      compiler.quotedString(parts[i]),
1284      ', ',
1285      JSON.stringify(compiler.source.currentLocation),
1286      ' )'
1287    ];
1288  } else {
1289    return stack;
1290  }
1291}
1292
1293export default JavaScriptCompiler;
1294