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