summaryrefslogtreecommitdiffstats
path: root/lib/handlebars/compiler
diff options
context:
space:
mode:
Diffstat (limited to 'lib/handlebars/compiler')
-rw-r--r--lib/handlebars/compiler/ast.js25
-rw-r--r--lib/handlebars/compiler/code-gen.js151
-rw-r--r--lib/handlebars/compiler/compiler.js100
-rw-r--r--lib/handlebars/compiler/helpers.js73
-rw-r--r--lib/handlebars/compiler/javascript-compiler.js341
-rw-r--r--lib/handlebars/compiler/printer.js2
-rw-r--r--lib/handlebars/compiler/visitor.js60
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;