diff options
author | kpdecker <kpdecker@gmail.com> | 2014-11-06 09:56:00 -0600 |
---|---|---|
committer | kpdecker <kpdecker@gmail.com> | 2014-11-08 17:50:01 -0600 |
commit | 3ee0682247a1be883810d7251a75a95a5aa7e943 (patch) | |
tree | c90a6ca50f4d4f3fc2dbf1982ff450ec3221a256 /lib/handlebars/compiler/javascript-compiler.js | |
parent | 249f559104cb7f85736e7e83e38ccc67b9b84bf6 (diff) | |
download | handlebars.js-3ee0682247a1be883810d7251a75a95a5aa7e943.zip handlebars.js-3ee0682247a1be883810d7251a75a95a5aa7e943.tar.gz handlebars.js-3ee0682247a1be883810d7251a75a95a5aa7e943.tar.bz2 |
Generate source maps
Allow the precompiler to generate source maps when the srcFile parameter is passed.
This refactors large chunks of the code generation pipeline, allowing metadata to be associated with code chunks as well as breaking out much of the code generation logic into a separate helper.
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" + |