diff options
Diffstat (limited to 'lib/handlebars/compiler')
-rw-r--r-- | lib/handlebars/compiler/ast.js | 25 | ||||
-rw-r--r-- | lib/handlebars/compiler/code-gen.js | 151 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 100 | ||||
-rw-r--r-- | lib/handlebars/compiler/helpers.js | 73 | ||||
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 341 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 2 | ||||
-rw-r--r-- | lib/handlebars/compiler/visitor.js | 60 |
7 files changed, 512 insertions, 240 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 49bdc33..35a60db 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -79,11 +79,11 @@ var AST = { this.strip.inlineStandalone = true; }, - BlockNode: function(mustache, program, inverse, strip, locInfo) { + BlockNode: function(sexpr, program, inverse, strip, locInfo) { LocationInfo.call(this, locInfo); this.type = 'block'; - this.mustache = mustache; + this.sexpr = sexpr; this.program = program; this.inverse = inverse; this.strip = strip; @@ -93,20 +93,6 @@ var AST = { } }, - RawBlockNode: function(mustache, content, close, locInfo) { - LocationInfo.call(this, locInfo); - - if (mustache.sexpr.id.original !== close) { - throw new Exception(mustache.sexpr.id.original + " doesn't match " + close, this); - } - - content = new AST.ContentNode(content, locInfo); - - this.type = 'block'; - this.mustache = mustache; - this.program = new AST.ProgramNode([content], {}, locInfo); - }, - ContentNode: function(string, locInfo) { LocationInfo.call(this, locInfo); this.type = "content"; @@ -196,14 +182,13 @@ var AST = { this.stringModeValue = bool === "true"; }, - CommentNode: function(comment, locInfo) { + CommentNode: function(comment, strip, locInfo) { LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; - this.strip = { - inlineStandalone: true - }; + this.strip = strip; + strip.inlineStandalone = true; } }; diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js new file mode 100644 index 0000000..7d1b4ca --- /dev/null +++ b/lib/handlebars/compiler/code-gen.js @@ -0,0 +1,151 @@ +import {isArray} from "../utils"; + +try { + var SourceMap = require('source-map'), + SourceNode = SourceMap.SourceNode; +} catch (err) { + /* istanbul ignore next: tested but not covered in istanbul due to dist build */ + SourceNode = function(line, column, srcFile, chunks) { + this.src = ''; + if (chunks) { + this.add(chunks); + } + }; + /* istanbul ignore next */ + SourceNode.prototype = { + add: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src += chunks; + }, + prepend: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src = chunks + this.src; + }, + toStringWithSourceMap: function() { + return {code: this.toString()}; + }, + toString: function() { + return this.src; + } + }; +} + + +function castChunk(chunk, codeGen, loc) { + if (isArray(chunk)) { + var ret = []; + + for (var i = 0, len = chunk.length; i < len; i++) { + ret.push(codeGen.wrap(chunk[i], loc)); + } + return ret; + } else if (typeof chunk === 'boolean' || typeof chunk === 'number') { + // Handle primitives that the SourceNode will throw up on + return chunk+''; + } + return chunk; +} + + +function CodeGen(srcFile) { + this.srcFile = srcFile; + this.source = []; +} + +CodeGen.prototype = { + prepend: function(source, loc) { + this.source.unshift(this.wrap(source, loc)); + }, + push: function(source, loc) { + this.source.push(this.wrap(source, loc)); + }, + + merge: function() { + var source = this.empty(); + this.each(function(line) { + source.add([' ', line, '\n']); + }); + return source; + }, + + each: function(iter) { + for (var i = 0, len = this.source.length; i < len; i++) { + iter(this.source[i]); + } + }, + + empty: function(loc) { + loc = loc || this.currentLocation || {}; + return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile); + }, + wrap: function(chunk, loc) { + if (chunk instanceof SourceNode) { + return chunk; + } + + loc = loc || this.currentLocation || {}; + chunk = castChunk(chunk, this, loc); + + return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk); + }, + + functionCall: function(fn, type, params) { + params = this.generateList(params); + return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); + }, + + 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') + '"'; + }, + + objectLiteral: function(obj) { + var pairs = []; + + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]); + } + } + + var ret = this.generateList(pairs); + ret.prepend('{'); + ret.add('}'); + return ret; + }, + + + generateList: function(entries, loc) { + var ret = this.empty(loc); + + for (var i = 0, len = entries.length; i < len; i++) { + if (i) { + ret.add(','); + } + + ret.add(castChunk(entries[i], this, loc)); + } + + return ret; + }, + + generateArray: function(entries, loc) { + var ret = this.generateList(entries, loc); + ret.prepend('['); + ret.add(']'); + + return ret; + } +}; + +export default CodeGen; + diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1aba34b..1e5d07a 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -108,7 +108,7 @@ Compiler.prototype = { }, block: function(block) { - var mustache = block.mustache, + var sexpr = block.sexpr, program = block.program, inverse = block.inverse; @@ -120,7 +120,6 @@ Compiler.prototype = { inverse = this.compileProgram(inverse); } - var sexpr = mustache.sexpr; var type = this.classifySexpr(sexpr); if (type === "helper") { @@ -130,36 +129,36 @@ Compiler.prototype = { // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('blockValue', sexpr.id.original); + this.opcode('pushProgram', block, program); + this.opcode('pushProgram', block, inverse); + this.opcode('emptyHash', block); + this.opcode('blockValue', block, sexpr.id.original); } else { this.ambiguousSexpr(sexpr, program, inverse); // now that the simple mustache is resolved, we need to // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('ambiguousBlockValue'); + this.opcode('pushProgram', block, program); + this.opcode('pushProgram', block, inverse); + this.opcode('emptyHash', block); + this.opcode('ambiguousBlockValue', block); } - this.opcode('append'); + this.opcode('append', block); }, hash: function(hash) { var pairs = hash.pairs, i, l; - this.opcode('pushHash'); + this.opcode('pushHash', hash); for(i=0, l=pairs.length; i<l; i++) { this.pushParam(pairs[i][1]); } while(i--) { - this.opcode('assignToHash', pairs[i][0]); + this.opcode('assignToHash', hash, pairs[i][0]); } - this.opcode('popHash'); + this.opcode('popHash', hash); }, partial: function(partial) { @@ -169,23 +168,28 @@ Compiler.prototype = { if (partial.hash) { this.accept(partial.hash); } else { - this.opcode('push', 'undefined'); + this.opcode('pushLiteral', partial, 'undefined'); } if (partial.context) { this.accept(partial.context); } else { - this.opcode('getContext', 0); - this.opcode('pushContext'); + this.opcode('getContext', partial, 0); + this.opcode('pushContext', partial); } - this.opcode('invokePartial', partialName.name, partial.indent || ''); - this.opcode('append'); + var indent = partial.indent || ''; + if (this.options.preventIndent && indent) { + this.opcode('appendContent', partial, indent); + indent = ''; + } + this.opcode('invokePartial', partial, partialName.name, indent); + this.opcode('append', partial); }, content: function(content) { if (content.string) { - this.opcode('appendContent', content.string); + this.opcode('appendContent', content, content.string); } }, @@ -193,9 +197,9 @@ Compiler.prototype = { this.sexpr(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { - this.opcode('appendEscaped'); + this.opcode('appendEscaped', mustache); } else { - this.opcode('append'); + this.opcode('append', mustache); } }, @@ -204,14 +208,14 @@ Compiler.prototype = { name = id.parts[0], isBlock = program != null || inverse != null; - this.opcode('getContext', id.depth); + this.opcode('getContext', sexpr, id.depth); - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); + this.opcode('pushProgram', sexpr, program); + this.opcode('pushProgram', sexpr, inverse); this.ID(id); - this.opcode('invokeAmbiguous', name, isBlock); + this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, simpleSexpr: function(sexpr) { @@ -224,11 +228,11 @@ Compiler.prototype = { } else { // Simplified ID for `this` this.addDepth(id.depth); - this.opcode('getContext', id.depth); - this.opcode('pushContext'); + this.opcode('getContext', sexpr, id.depth); + this.opcode('pushContext', sexpr); } - this.opcode('resolvePossibleLambda'); + this.opcode('resolvePossibleLambda', sexpr); }, helperSexpr: function(sexpr, program, inverse) { @@ -237,14 +241,14 @@ Compiler.prototype = { name = id.parts[0]; if (this.options.knownHelpers[name]) { - this.opcode('invokeKnownHelper', params.length, name); + this.opcode('invokeKnownHelper', sexpr, params.length, name); } else if (this.options.knownHelpersOnly) { throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr); } else { id.falsy = true; this.ID(id); - this.opcode('invokeHelper', params.length, id.original, id.isSimple); + this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); } }, @@ -262,39 +266,43 @@ Compiler.prototype = { ID: function(id) { this.addDepth(id.depth); - this.opcode('getContext', id.depth); + this.opcode('getContext', id, id.depth); var name = id.parts[0]; if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` - this.opcode('pushContext'); + this.opcode('pushContext', id); } else { - this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); } }, DATA: function(data) { this.options.data = true; - this.opcode('lookupData', data.id.depth, data.id.parts); + this.opcode('lookupData', data, data.id.depth, data.id.parts); }, STRING: function(string) { - this.opcode('pushString', string.string); + this.opcode('pushString', string, string.string); }, NUMBER: function(number) { - this.opcode('pushLiteral', number.number); + this.opcode('pushLiteral', number, number.number); }, BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool.bool); + this.opcode('pushLiteral', bool, bool.bool); }, comment: function() {}, // HELPERS - opcode: function(name) { - this.opcodes.push({ opcode: name, args: slice.call(arguments, 1) }); + opcode: function(name, node) { + var loc = { + firstLine: node.firstLine, firstColumn: node.firstColumn, + lastLine: node.lastLine, lastColumn: node.lastColumn + }; + this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: loc }); }, addDepth: function(depth) { @@ -339,8 +347,8 @@ Compiler.prototype = { if(val.depth) { this.addDepth(val.depth); } - this.opcode('getContext', val.depth || 0); - this.opcode('pushStringParam', val.stringModeValue, val.type); + this.opcode('getContext', val, val.depth || 0); + this.opcode('pushStringParam', val, val.stringModeValue, val.type); if (val.type === 'sexpr') { // Subexpressions get evaluated and passed in @@ -349,7 +357,7 @@ Compiler.prototype = { } } else { if (this.trackIds) { - this.opcode('pushId', val.type, val.idName || val.stringModeValue); + this.opcode('pushId', val, val.type, val.idName || val.stringModeValue); } this.accept(val); } @@ -359,13 +367,13 @@ Compiler.prototype = { var params = sexpr.params; this.pushParams(params); - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); + this.opcode('pushProgram', sexpr, program); + this.opcode('pushProgram', sexpr, inverse); if (sexpr.hash) { this.hash(sexpr.hash); } else { - this.opcode('emptyHash'); + this.opcode('emptyHash', sexpr); } return params; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 758c740..d9b7b14 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -7,26 +7,69 @@ export function stripFlags(open, close) { }; } +export function stripComment(comment) { + return comment.replace(/^\{\{~?\!-?-?/, '') + .replace(/-?-?~?\}\}$/, ''); +} + +export function prepareRawBlock(openRawBlock, content, close, locInfo) { + /*jshint -W040 */ + if (openRawBlock.sexpr.id.original !== close) { + var errorNode = { + firstLine: openRawBlock.sexpr.firstLine, + firstColumn: openRawBlock.sexpr.firstColumn + }; + + throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); + } + + var program = new this.ProgramNode([content], {}, locInfo); + + return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); +} -export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { +export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); + // When we are chaining inverse calls, we will not have a close path + if (close && close.path && openBlock.sexpr.id.original !== close.path.original) { + var errorNode = { + firstLine: openBlock.sexpr.firstLine, + firstColumn: openBlock.sexpr.firstColumn + }; + + throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); + } + + // Safely handle a chained inverse that does not have a non-conditional inverse + // (i.e. both inverseAndProgram AND close are undefined) + if (!close) { + close = {strip: {}}; } - var inverse = inverseAndProgram && inverseAndProgram.program; + // Find the inverse program that is involed with whitespace stripping. + var inverse = inverseAndProgram && inverseAndProgram.program, + firstInverse = inverse, + lastInverse = inverse; + if (inverse && inverse.inverse) { + firstInverse = inverse.statements[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.inverse) { + lastInverse = lastInverse.statements[lastInverse.statements.length-1].program; + } + } var strip = { - left: mustache.strip.left, + left: openBlock.strip.left, right: close.strip.right, // Determine the standalone candiacy. Basically flag our content as being possibly standalone // so our parent can determine if we actually are standalone openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) + closeStandalone: isPrevWhitespace((firstInverse || program).statements) }; - if (mustache.strip.right) { + if (openBlock.strip.right) { omitRight(program.statements, null, true); } @@ -36,19 +79,20 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert if (inverseStrip.left) { omitLeft(program.statements, null, true); } + if (inverseStrip.right) { - omitRight(inverse.statements, null, true); + omitRight(firstInverse.statements, null, true); } if (close.strip.left) { - omitLeft(inverse.statements, null, true); + omitLeft(lastInverse.statements, null, true); } // Find standalone else statments if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { + && isNextWhitespace(firstInverse.statements)) { omitLeft(program.statements); - omitRight(inverse.statements); + omitRight(firstInverse.statements); } } else { if (close.strip.left) { @@ -57,9 +101,9 @@ export function prepareBlock(mustache, program, inverseAndProgram, close, invert } if (inverted) { - return new this.BlockNode(mustache, inverse, program, strip, locInfo); + return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo); } else { - return new this.BlockNode(mustache, program, inverse, strip, locInfo); + return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo); } } @@ -93,7 +137,8 @@ export function prepareProgram(statements, isRoot) { if (omitLeft(statements, i)) { // If we are on a standalone node, save the indent info for partials if (current.type === 'partial') { - current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : ''; + // Pull out the whitespace from the final line + current.indent = (/([ \t]+$)/).exec(statements[i-1].original)[1]; } } } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index d41cacd..af9c5de 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,13 @@ 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 [this.aliasable('this.lookup'), '(depths, "', name, '")']; }, compilerInfo: function() { @@ -29,15 +29,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; } }, @@ -78,16 +86,20 @@ JavaScriptCompiler.prototype = { var opcodes = environment.opcodes, opcode, + firstLoc, i, l; for (i = 0, l = opcodes.length; i < l; i++) { opcode = opcodes[i]; + this.source.currentLocation = opcode.loc; + firstLoc = firstLoc || opcode.loc; this[opcode.opcode].apply(this, opcode.args); } // Flush any trailing content that might be pending. + this.source.currentLocation = firstLoc; this.pushSource(''); /* istanbul ignore next */ @@ -123,7 +135,18 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); + + this.source.currentLocation = {firstLine: 1, firstColumn: 0}; ret = this.objectLiteral(ret); + + if (options.srcName) { + ret = ret.toStringWithSourceMap({file: options.destName}); + ret.map = ret.map && ret.map.toString(); + } else { + ret = ret.toString(); + } + } else { + ret.compilerOptions = this.options; } return ret; @@ -136,7 +159,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) { @@ -148,9 +171,18 @@ JavaScriptCompiler.prototype = { } // Generate minimizer alias mappings + // + // When using true SourceNodes, this will update all references to the given alias + // as the source nodes are reused in situ. For the non-source node compilation mode, + // aliases will not be used, but this case is already being run on the client and + // we aren't concern about minimizing the template size. + var aliasCount = 0; for (var alias in this.aliases) { - if (this.aliases.hasOwnProperty(alias)) { - varDeclarations += ', ' + alias + '=' + this.aliases[alias]; + var node = this.aliases[alias]; + + if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { + varDeclarations += ', alias' + (++aliasCount) + '=' + alias; + node.children[0] = 'alias' + aliasCount; } } @@ -168,59 +200,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] @@ -233,15 +273,14 @@ JavaScriptCompiler.prototype = { // replace it on the stack with the result of properly // invoking blockHelperMissing. blockValue: function(name) { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = [this.contextName(0)]; + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; this.setupParams(name, 0, params); var blockName = this.popStack(); params.splice(1, 0, blockName); - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); + this.push(this.source.functionCall(blockHelperMissing, 'call', params)); }, // [ambiguousBlockValue] @@ -251,10 +290,9 @@ JavaScriptCompiler.prototype = { // On stack, after, if no lastHelper: same as [blockValue] // On stack, after, if lastHelper: value ambiguousBlockValue: function() { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - // We're being a bit cheeky and reusing the options value from the prior exec - var params = [this.contextName(0)]; + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; this.setupParams('', 0, params, true); this.flushInline(); @@ -262,7 +300,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] @@ -274,6 +315,8 @@ JavaScriptCompiler.prototype = { appendContent: function(content) { if (this.pendingContent) { content = this.pendingContent + content; + } else { + this.pendingLocation = this.source.currentLocation; } this.pendingContent = content; @@ -289,13 +332,18 @@ JavaScriptCompiler.prototype = { // If `value` is truthy, or 0, it is coerced into a string and appended // Otherwise, the empty string is appended append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); - if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); + if (this.isInline()) { + this.replaceStack(function(current) { + return [' != null ? ', current, ' : ""']; + }); + + this.pushSource(this.appendToBuffer(this.popStack())); + } else { + var local = this.popStack(); + this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); + if (this.environment.isSimple) { + this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); + } } }, @@ -306,9 +354,8 @@ JavaScriptCompiler.prototype = { // // Escape `value` and append it to the buffer appendEscaped: function() { - this.aliases.escapeExpression = 'this.escapeExpression'; - - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); + this.pushSource(this.appendToBuffer( + [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); }, // [getContext] @@ -358,10 +405,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]; } }); } @@ -384,7 +431,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')]; }); } }, @@ -397,9 +444,7 @@ JavaScriptCompiler.prototype = { // If the `value` is a lambda, replace it on the stack by // the return value of the lambda resolvePossibleLambda: function() { - this.aliases.lambda = 'this.lambda'; - - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); + this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -447,14 +492,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] @@ -467,17 +512,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: ... @@ -516,13 +550,15 @@ JavaScriptCompiler.prototype = { // // If the helper is not found, `helperMissing` is called. invokeHelper: function(paramSize, name, isSimple) { - this.aliases.helperMissing = 'helpers.helperMissing'; - 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, ' || ', this.aliasable('helpers.helperMissing'), ')'), + 'call', + helper.callParams)); }, // [invokeKnownHelper] @@ -534,7 +570,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] @@ -550,8 +586,6 @@ JavaScriptCompiler.prototype = { // and can be avoided by passing the `knownHelpers` and // `knownHelpersOnly` flags at compile-time. invokeAmbiguous: function(name, helperCall) { - this.aliases.functionType = '"function"'; - this.aliases.helperMissing = 'helpers.helperMissing'; this.useRegister('helper'); var nonHelper = this.popStack(); @@ -561,10 +595,13 @@ 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 : ', + this.aliasable('helpers.helperMissing'), + (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', + '(typeof helper === ', this.aliasable('"function"'), ' ? ', + this.source.functionCall('helper','call', helper.callParams), ' : helper))' + ]); }, // [invokePartial] @@ -586,7 +623,7 @@ JavaScriptCompiler.prototype = { params.push('depths'); } - this.push("this.invokePartial(" + params.join(", ") + ")"); + this.push(this.source.functionCall('this.invokePartial', '', params)); }, // [assignToHash] @@ -611,15 +648,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) { @@ -691,13 +728,23 @@ JavaScriptCompiler.prototype = { } }, + push: function(expr) { + if (!(expr instanceof Literal)) { + expr = this.source.wrap(expr); + } + + this.inlineStack.push(expr); + return expr; + }, + pushStackLiteral: function(item) { - return this.push(new Literal(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; } @@ -706,17 +753,8 @@ JavaScriptCompiler.prototype = { } }, - pushStack: function(item) { - this.flushInline(); - - var stack = this.incrStack(); - this.pushSource(stack + " = " + item + ";"); - this.compileStack.push(stack); - return stack; - }, - replaceStack: function(callback) { - var prefix = '', + var prefix = ['('], inline = this.isInline(), stack, createdStack, @@ -732,14 +770,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 = !this.stackSlot; - var name = !createdStack ? this.topStackName() : this.incrStack(); + createdStack = true; + var name = this.incrStack(); - prefix = '(' + this.push(name) + ' = ' + top + ')'; + prefix = ['((', this.push(name), ' = ', top, ')']; stack = this.topStack(); } @@ -751,7 +790,7 @@ JavaScriptCompiler.prototype = { if (createdStack) { this.stackSlot--; } - this.push('(' + prefix + item + ')'); + this.push(prefix.concat(item, ')')); }, incrStack: function() { @@ -764,15 +803,16 @@ JavaScriptCompiler.prototype = { }, flushInline: function() { var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + /* istanbul ignore if */ + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + var stack = this.incrStack(); + this.pushSource([stack, ' = ', entry, ';']); + this.compileStack.push(stack); } } }, @@ -802,6 +842,7 @@ JavaScriptCompiler.prototype = { var stack = (this.isInline() ? this.inlineStack : this.compileStack), item = stack[stack.length - 1]; + /* istanbul ignore if */ if (item instanceof Literal) { return item.value; } else { @@ -818,25 +859,25 @@ 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 = []; + return this.source.objectLiteral(obj); + }, - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } + aliasable: function(name) { + var ret = this.aliases[name]; + if (ret) { + ret.referenceCount++; + return ret; } - return '{' + pairs.join(',') + '}'; + ret = this.aliases[name] = this.source.wrap(name); + ret.aliasable = true; + ret.referenceCount = 1; + + return ret; }, setupHelper: function(paramSize, name, blockHelper) { @@ -848,11 +889,11 @@ JavaScriptCompiler.prototype = { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") + callParams: [this.contextName(0)].concat(params) }; }, - setupOptions: function(helper, paramSize, params) { + setupParams: function(helper, paramSize, params, useRegister) { var options = {}, contexts = [], types = [], ids = [], param, inverse, program; options.name = this.quotedString(helper); @@ -872,16 +913,8 @@ JavaScriptCompiler.prototype = { // Avoid setting fn and inverse if neither are set. This allows // helpers to do a check for `if (options.fn)` if (program || inverse) { - if (!program) { - program = 'this.noop'; - } - - if (!inverse) { - inverse = 'this.noop'; - } - - options.fn = program; - options.inverse = inverse; + options.fn = program || 'this.noop'; + options.inverse = inverse || 'this.noop'; } // The parameters go on to the stack in order (making sure that they are evaluated in order) @@ -901,29 +934,22 @@ 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) { options.data = "data"; } - return options; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(helperName, paramSize, params, useRegister) { - var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params)); - + options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); params.push('options'); - return 'options=' + options; + return ['options=', options]; } else { params.push(options); return ''; @@ -931,6 +957,7 @@ JavaScriptCompiler.prototype = { } }; + var reservedWords = ( "break else new var" + " case finally return void" + diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 7654245..c329373 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -40,7 +40,7 @@ PrintVisitor.prototype.block = function(block) { out = out + this.pad("BLOCK:"); this.padding++; - out = out + this.accept(block.mustache); + out = out + this.pad(this.accept(block.sexpr)); if (block.program) { out = out + this.pad("PROGRAM:"); this.padding++; diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 6a0373e..a4eb2b4 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -4,8 +4,64 @@ Visitor.prototype = { constructor: Visitor, accept: function(object) { - return this[object.type](object); - } + return object && this[object.type] && this[object.type](object); + }, + + program: function(program) { + var statements = program.statements, + i, l; + + for(i=0, l=statements.length; i<l; i++) { + this.accept(statements[i]); + } + }, + + block: function(block) { + this.accept(block.mustache); + this.accept(block.program); + this.accept(block.inverse); + }, + + mustache: function(mustache) { + this.accept(mustache.sexpr); + }, + + sexpr: function(sexpr) { + var params = sexpr.params, paramStrings = [], hash; + + this.accept(sexpr.id); + for(var i=0, l=params.length; i<l; i++) { + this.accept(params[i]); + } + this.accept(sexpr.hash); + }, + + hash: function(hash) { + var pairs = hash.pairs; + + for(var i=0, l=pairs.length; i<l; i++) { + this.accept(pairs[i][1]); + } + }, + + partial: function(partial) { + this.accept(partial.partialName); + this.accept(partial.context); + this.accept(partial.hash); + }, + PARTIAL_NAME: function(partialName) {}, + + DATA: function(data) { + this.accept(data.id); + }, + + STRING: function(string) {}, + NUMBER: function(number) {}, + BOOLEAN: function(bool) {}, + ID: function(id) {}, + + content: function(content) {}, + comment: function(comment) {} }; export default Visitor; |