diff options
Diffstat (limited to 'lib/handlebars/compiler/javascript-compiler.js')
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 224 |
1 files changed, 124 insertions, 100 deletions
diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 4256b1c..cb9b1ca 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,5 +1,7 @@ import { COMPILER_REVISION, REVISION_CHANGES } from "../base"; import Exception from "../exception"; +import {isArray} from "../utils"; +import CodeGen from "./code-gen"; function Literal(value) { this.value = value; @@ -12,15 +14,15 @@ JavaScriptCompiler.prototype = { // alternative compiled forms for name lookup and buffering semantics nameLookup: function(parent, name /* , type*/) { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return parent + "." + name; + return [parent, ".", name]; } else { - return parent + "['" + name + "']"; + return [parent, "['", name, "']"]; } }, depthedLookup: function(name) { this.aliases.lookup = 'this.lookup'; - return 'lookup(depths, "' + name + '")'; + return ['lookup(depths, "', name, '")']; }, compilerInfo: function() { @@ -29,15 +31,23 @@ JavaScriptCompiler.prototype = { return [revision, versions]; }, - appendToBuffer: function(string) { + appendToBuffer: function(string, location, explicit) { + // Force a string as this simplifies the merge logic. + if (!isArray(string)) { + string = [string]; + } + string = this.source.wrap(string, location); + if (this.environment.isSimple) { - return "return " + string + ";"; + return ['return ', string, ';']; + } else if (explicit) { + // This is a case where the buffer operation occurs as a child of another + // construct, generally braces. We have to explicitly output these buffer + // operations to ensure that the emitted code goes in the correct location. + return ['buffer += ', string, ';']; } else { - return { - appendToBuffer: true, - content: string, - toString: function() { return "buffer += " + string + ";"; } - }; + string.appendToBuffer = true; + return string; } }, @@ -84,11 +94,12 @@ JavaScriptCompiler.prototype = { for (i = 0, l = opcodes.length; i < l; i++) { opcode = opcodes[i]; - this.currentLoc = opcode.loc; + this.source.currentLocation = opcode.loc; this[opcode.opcode].apply(this, opcode.args); } // Flush any trailing content that might be pending. + this.source.currentLocation = undefined; this.pushSource(''); /* istanbul ignore next */ @@ -125,6 +136,13 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); ret = this.objectLiteral(ret); + + if (options.srcName) { + ret = ret.toStringWithSourceMap({file: options.destName}); + ret.map = ret.map && ret.map.toString(); + } else { + ret = ret.toString(); + } } return ret; @@ -137,7 +155,7 @@ JavaScriptCompiler.prototype = { // track the last context pushed into place to allow skipping the // getContext opcode when it would be a noop this.lastContext = 0; - this.source = []; + this.source = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -169,59 +187,67 @@ JavaScriptCompiler.prototype = { return Function.apply(this, params); } else { - return 'function(' + params.join(',') + ') {\n ' + source + '}'; + return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']); } }, mergeSource: function(varDeclarations) { - var source = '', - buffer, + var isSimple = this.environment.isSimple, appendOnly = !this.forceBuffer, - appendFirst; + appendFirst, - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; + sourceSeen, + bufferStart, + bufferEnd; + this.source.each(function(line) { if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; + if (bufferStart) { + line.prepend(' + '); } else { - buffer = line.content; + bufferStart = line; } + bufferEnd = line; } else { - if (buffer) { - if (!source) { + if (bufferStart) { + if (!sourceSeen) { appendFirst = true; - source = buffer + ';\n '; } else { - source += 'buffer += ' + buffer + ';\n '; + bufferStart.prepend('buffer += '); } - buffer = undefined; + bufferEnd.add(';'); + bufferStart = bufferEnd = undefined; } - source += line + '\n '; - if (!this.environment.isSimple) { + sourceSeen = true; + if (!isSimple) { appendOnly = false; } } - } + }); + if (appendOnly) { - if (buffer || !source) { - source += 'return ' + (buffer || '""') + ';\n'; + if (bufferStart) { + bufferStart.prepend('return '); + bufferEnd.add(';'); + } else { + this.source.push('return "";'); } } else { varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); - if (buffer) { - source += 'return buffer + ' + buffer + ';\n'; + + if (bufferStart) { + bufferStart.prepend('return buffer + '); + bufferEnd.add(';'); } else { - source += 'return buffer;\n'; + this.source.push('return buffer;'); } } if (varDeclarations) { - source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source; + this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ')); } - return source; + return this.source.merge(); }, // [blockValue] @@ -242,7 +268,7 @@ JavaScriptCompiler.prototype = { var blockName = this.popStack(); params.splice(1, 0, blockName); - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); + this.push(this.source.functionCall('blockHelperMissing', 'call', params)); }, // [ambiguousBlockValue] @@ -263,7 +289,10 @@ JavaScriptCompiler.prototype = { var current = this.topStack(); params.splice(1, 0, current); - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); + this.pushSource([ + 'if (!', this.lastHelper, ') { ', + current, ' = ', this.source.functionCall('blockHelperMissing', 'call', params), + '}']); }, // [appendContent] @@ -275,6 +304,8 @@ JavaScriptCompiler.prototype = { appendContent: function(content) { if (this.pendingContent) { content = this.pendingContent + content; + } else { + this.pendingLocation = this.source.currentLocation; } this.pendingContent = content; @@ -292,15 +323,15 @@ JavaScriptCompiler.prototype = { append: function() { if (this.isInline()) { this.replaceStack(function(current) { - return ' != null ? ' + current + ' : ""'; + return [' != null ? ', current, ' : ""']; }); this.pushSource(this.appendToBuffer(this.popStack())); } else { var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); + this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); + this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); } } }, @@ -314,7 +345,7 @@ JavaScriptCompiler.prototype = { appendEscaped: function() { this.aliases.escapeExpression = 'this.escapeExpression'; - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); + this.pushSource(this.appendToBuffer(['escapeExpression(', this.popStack(), ')'])); }, // [getContext] @@ -364,10 +395,10 @@ JavaScriptCompiler.prototype = { // We want to ensure that zero and false are handled properly if the context (falsy flag) // needs to have the special handling for these values. if (!falsy) { - return ' != null ? ' + lookup + ' : ' + current; + return [' != null ? ', lookup, ' : ', current]; } else { // Otherwise we can use generic falsy handling - return ' && ' + lookup; + return [' && ', lookup]; } }); } @@ -390,7 +421,7 @@ JavaScriptCompiler.prototype = { var len = parts.length; for (var i = 0; i < len; i++) { this.replaceStack(function(current) { - return ' && ' + this.nameLookup(current, parts[i], 'data'); + return [' && ', this.nameLookup(current, parts[i], 'data')]; }); } }, @@ -405,7 +436,7 @@ JavaScriptCompiler.prototype = { resolvePossibleLambda: function() { this.aliases.lambda = 'this.lambda'; - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); + this.push(['lambda(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -453,14 +484,14 @@ JavaScriptCompiler.prototype = { this.hash = this.hashes.pop(); if (this.trackIds) { - this.push('{' + hash.ids.join(',') + '}'); + this.push(this.objectLiteral(hash.ids)); } if (this.stringParams) { - this.push('{' + hash.contexts.join(',') + '}'); - this.push('{' + hash.types.join(',') + '}'); + this.push(this.objectLiteral(hash.contexts)); + this.push(this.objectLiteral(hash.types)); } - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); + this.push(this.objectLiteral(hash.values)); }, // [pushString] @@ -473,17 +504,6 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral(this.quotedString(string)); }, - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, - // [pushLiteral] // // On stack, before: ... @@ -526,9 +546,13 @@ JavaScriptCompiler.prototype = { var nonHelper = this.popStack(); var helper = this.setupHelper(paramSize, name); + var simple = isSimple ? [helper.name, ' || '] : ''; - var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - this.push('((' + lookup + ').call(' + helper.callParams + '))'); + this.push( + this.source.functionCall( + ['('].concat(simple, nonHelper, ' || ', 'helperMissing)'), + 'call', + helper.callParams)); }, // [invokeKnownHelper] @@ -540,7 +564,7 @@ JavaScriptCompiler.prototype = { // so a `helperMissing` fallback is not required. invokeKnownHelper: function(paramSize, name) { var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); + this.push(this.source.functionCall(helper.name, 'call', helper.callParams)); }, // [invokeAmbiguous] @@ -567,10 +591,12 @@ JavaScriptCompiler.prototype = { var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - this.push( - '((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing' - + (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),' - + '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))'); + this.push([ + '((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : helperMissing', + (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', + '(typeof helper === functionType ? ', + this.source.functionCall('helper','call', helper.callParams), ' : helper))' + ]); }, // [invokePartial] @@ -592,7 +618,7 @@ JavaScriptCompiler.prototype = { params.push('depths'); } - this.push("this.invokePartial(" + params.join(", ") + ")"); + this.push(this.source.functionCall('this.invokePartial', '', params)); }, // [assignToHash] @@ -617,15 +643,15 @@ JavaScriptCompiler.prototype = { var hash = this.hash; if (context) { - hash.contexts.push("'" + key + "': " + context); + hash.contexts[key] = context; } if (type) { - hash.types.push("'" + key + "': " + type); + hash.types[key] = type; } if (id) { - hash.ids.push("'" + key + "': " + id); + hash.ids[key] = id; } - hash.values.push("'" + key + "': (" + value + ")"); + hash.values[key] = value; }, pushId: function(type, name) { @@ -697,13 +723,23 @@ JavaScriptCompiler.prototype = { } }, + push: function(expr) { + if (!(expr instanceof Literal)) { + expr = this.source.wrap(expr); + } + + this.inlineStack.push(expr); + return expr; + }, + pushStackLiteral: function(item) { this.push(new Literal(item)); }, pushSource: function(source) { if (this.pendingContent) { - this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent))); + this.source.push( + this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation)); this.pendingContent = undefined; } @@ -713,7 +749,7 @@ JavaScriptCompiler.prototype = { }, replaceStack: function(callback) { - var prefix = '', + var prefix = ['('], inline = this.isInline(), stack, createdStack, @@ -729,14 +765,15 @@ JavaScriptCompiler.prototype = { if (top instanceof Literal) { // Literals do not need to be inlined - prefix = stack = top.value; + stack = [top.value]; + prefix = ['(', stack]; usedLiteral = true; } else { // Get or create the current stack name for use by the inline createdStack = true; var name = this.incrStack(); - prefix = '(' + this.push(name) + ' = ' + top + ')'; + prefix = ['((', this.push(name), ' = ', top, ')']; stack = this.topStack(); } @@ -748,7 +785,7 @@ JavaScriptCompiler.prototype = { if (createdStack) { this.stackSlot--; } - this.push('(' + prefix + item + ')'); + this.push(prefix.concat(item, ')')); }, incrStack: function() { @@ -769,7 +806,7 @@ JavaScriptCompiler.prototype = { this.compileStack.push(entry); } else { var stack = this.incrStack(); - this.pushSource(stack + " = " + entry + ";"); + this.pushSource([stack, ' = ', entry, ';']); this.compileStack.push(stack); } } @@ -817,25 +854,11 @@ JavaScriptCompiler.prototype = { }, quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; + return this.source.quotedString(str); }, objectLiteral: function(obj) { - var pairs = []; - - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } - } - - return '{' + pairs.join(',') + '}'; + return this.source.objectLiteral(obj); }, setupHelper: function(paramSize, name, blockHelper) { @@ -847,7 +870,7 @@ JavaScriptCompiler.prototype = { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") + callParams: [this.contextName(0)].concat(params) }; }, @@ -892,11 +915,11 @@ JavaScriptCompiler.prototype = { } if (this.trackIds) { - options.ids = "[" + ids.join(",") + "]"; + options.ids = this.source.generateArray(ids); } if (this.stringParams) { - options.types = "[" + types.join(",") + "]"; - options.contexts = "[" + contexts.join(",") + "]"; + options.types = this.source.generateArray(types); + options.contexts = this.source.generateArray(contexts); } if (this.options.data) { @@ -907,7 +930,7 @@ JavaScriptCompiler.prototype = { if (useRegister) { this.useRegister('options'); params.push('options'); - return 'options=' + options; + return ['options=', options]; } else { params.push(options); return ''; @@ -915,6 +938,7 @@ JavaScriptCompiler.prototype = { } }; + var reservedWords = ( "break else new var" + " case finally return void" + |