summaryrefslogtreecommitdiffstats
path: root/lib/handlebars/compiler
diff options
context:
space:
mode:
authorKevin Decker <kpdecker@gmail.com>2014-11-29 18:02:12 -0600
committerKevin Decker <kpdecker@gmail.com>2014-11-29 18:02:12 -0600
commitd4070c36675bfecee290f20bd2d9c23a50e9e00b (patch)
tree0fdf5adfe0824f0310fe1745effcc1576d060933 /lib/handlebars/compiler
parent3a9440f954092558275cd4c05a35ba34bcbfa210 (diff)
parenta655aedb5cf523430b08ada5f8cc4730d1db3e5b (diff)
downloadhandlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.zip
handlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.tar.gz
handlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.tar.bz2
Merge pull request #915 from wycats/ast-update
Ast update
Diffstat (limited to 'lib/handlebars/compiler')
-rw-r--r--lib/handlebars/compiler/ast.js222
-rw-r--r--lib/handlebars/compiler/base.js13
-rw-r--r--lib/handlebars/compiler/code-gen.js15
-rw-r--r--lib/handlebars/compiler/compiler.js271
-rw-r--r--lib/handlebars/compiler/helpers.js271
-rw-r--r--lib/handlebars/compiler/javascript-compiler.js49
-rw-r--r--lib/handlebars/compiler/printer.js120
-rw-r--r--lib/handlebars/compiler/visitor.js63
-rw-r--r--lib/handlebars/compiler/whitespace-control.js210
9 files changed, 634 insertions, 600 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index 0bc70e9..72a56aa 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -1,195 +1,111 @@
import Exception from "../exception";
-function LocationInfo(locInfo) {
- locInfo = locInfo || {};
- this.firstLine = locInfo.first_line;
- this.firstColumn = locInfo.first_column;
- this.lastColumn = locInfo.last_column;
- this.lastLine = locInfo.last_line;
-}
-
var AST = {
- ProgramNode: function(statements, blockParams, strip, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "program";
- this.statements = statements;
+ Program: function(statements, blockParams, strip, locInfo) {
+ this.loc = locInfo;
+ this.type = 'Program';
+ this.body = statements;
+
this.blockParams = blockParams;
this.strip = strip;
},
- MustacheNode: function(rawParams, hash, open, strip, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "mustache";
- this.strip = strip;
-
- // Open may be a string parsed from the parser or a passed boolean flag
- if (open != null && open.charAt) {
- // Must use charAt to support IE pre-10
- var escapeFlag = open.charAt(3) || open.charAt(2);
- this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
- } else {
- this.escaped = !!open;
- }
-
- if (rawParams instanceof AST.SexprNode) {
- this.sexpr = rawParams;
- } else {
- // Support old AST API
- this.sexpr = new AST.SexprNode(rawParams, hash);
- }
-
- // Support old AST API that stored this info in MustacheNode
- this.id = this.sexpr.id;
- this.params = this.sexpr.params;
- this.hash = this.sexpr.hash;
- this.eligibleHelper = this.sexpr.eligibleHelper;
- this.isHelper = this.sexpr.isHelper;
- },
-
- SexprNode: function(rawParams, hash, locInfo) {
- LocationInfo.call(this, locInfo);
-
- this.type = "sexpr";
- this.hash = hash;
-
- var id = this.id = rawParams[0];
- var params = this.params = rawParams.slice(1);
+ MustacheStatement: function(sexpr, escaped, strip, locInfo) {
+ this.loc = locInfo;
+ this.type = 'MustacheStatement';
- // a mustache is definitely a helper if:
- // * it is an eligible helper, and
- // * it has at least one parameter or hash segment
- this.isHelper = !!(params.length || hash);
-
- // a mustache is an eligible helper if:
- // * its id is simple (a single part, not `this` or `..`)
- this.eligibleHelper = this.isHelper || id.isSimple;
-
- // if a mustache is an eligible helper but not a definite
- // helper, it is ambiguous, and will be resolved in a later
- // pass or at runtime.
- },
+ this.sexpr = sexpr;
+ this.escaped = escaped;
- PartialNode: function(partialName, context, hash, strip, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "partial";
- this.partialName = partialName;
- this.context = context;
- this.hash = hash;
this.strip = strip;
-
- this.strip.inlineStandalone = true;
},
- BlockNode: function(sexpr, program, inverse, strip, locInfo) {
- LocationInfo.call(this, locInfo);
+ BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
+ this.loc = locInfo;
- this.type = 'block';
+ this.type = 'BlockStatement';
this.sexpr = sexpr;
this.program = program;
this.inverse = inverse;
- this.strip = strip;
- if (inverse && !program) {
- this.isInverse = true;
- }
+ this.openStrip = openStrip;
+ this.inverseStrip = inverseStrip;
+ this.closeStrip = closeStrip;
},
- ContentNode: function(string, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "content";
- this.original = this.string = string;
+ PartialStatement: function(sexpr, strip, locInfo) {
+ this.loc = locInfo;
+ this.type = 'PartialStatement';
+ this.sexpr = sexpr;
+ this.indent = '';
+
+ this.strip = strip;
},
- HashNode: function(pairs, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "hash";
- this.pairs = pairs;
+ ContentStatement: function(string, locInfo) {
+ this.loc = locInfo;
+ this.type = 'ContentStatement';
+ this.original = this.value = string;
},
- IdNode: function(parts, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "ID";
-
- var original = "",
- dig = [],
- depth = 0,
- depthString = '';
-
- for(var i=0,l=parts.length; i<l; i++) {
- var part = parts[i].part;
- original += (parts[i].separator || '') + part;
-
- if (part === ".." || part === "." || part === "this") {
- if (dig.length > 0) {
- throw new Exception("Invalid path: " + original, this);
- } else if (part === "..") {
- depth++;
- depthString += '../';
- } else {
- this.isScoped = true;
- }
- } else {
- dig.push(part);
- }
- }
+ CommentStatement: function(comment, strip, locInfo) {
+ this.loc = locInfo;
+ this.type = 'CommentStatement';
+ this.value = comment;
- this.original = original;
- this.parts = dig;
- this.string = dig.join('.');
- this.depth = depth;
- this.idName = depthString + this.string;
+ this.strip = strip;
+ },
- // an ID is simple if it only has one part, and that part is not
- // `..` or `this`.
- this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
+ SubExpression: function(path, params, hash, locInfo) {
+ this.loc = locInfo;
- this.stringModeValue = this.string;
+ this.type = 'SubExpression';
+ this.path = path;
+ this.params = params || [];
+ this.hash = hash;
},
- PartialNameNode: function(name, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "PARTIAL_NAME";
- this.name = name.original;
- },
+ PathExpression: function(data, depth, parts, original, locInfo) {
+ this.loc = locInfo;
+ this.type = 'PathExpression';
- DataNode: function(id, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "DATA";
- this.id = id;
- this.stringModeValue = id.stringModeValue;
- this.idName = '@' + id.stringModeValue;
+ this.data = data;
+ this.original = original;
+ this.parts = parts;
+ this.depth = depth;
},
- StringNode: function(string, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "STRING";
+ StringLiteral: function(string, locInfo) {
+ this.loc = locInfo;
+ this.type = 'StringLiteral';
this.original =
- this.string =
- this.stringModeValue = string;
+ this.value = string;
},
- NumberNode: function(number, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "NUMBER";
+ NumberLiteral: function(number, locInfo) {
+ this.loc = locInfo;
+ this.type = 'NumberLiteral';
this.original =
- this.number = number;
- this.stringModeValue = Number(number);
+ this.value = Number(number);
},
- BooleanNode: function(bool, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "BOOLEAN";
- this.bool = bool;
- this.stringModeValue = bool === "true";
+ BooleanLiteral: function(bool, locInfo) {
+ this.loc = locInfo;
+ this.type = 'BooleanLiteral';
+ this.original =
+ this.value = bool === 'true';
},
- CommentNode: function(comment, strip, locInfo) {
- LocationInfo.call(this, locInfo);
- this.type = "comment";
- this.comment = comment;
-
- this.strip = strip;
- strip.inlineStandalone = true;
+ Hash: function(pairs, locInfo) {
+ this.loc = locInfo;
+ this.type = 'Hash';
+ this.pairs = pairs;
+ },
+ HashPair: function(key, value, locInfo) {
+ this.loc = locInfo;
+ this.type = 'HashPair';
+ this.key = key;
+ this.value = value;
}
};
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
index 1378463..ff237ec 100644
--- a/lib/handlebars/compiler/base.js
+++ b/lib/handlebars/compiler/base.js
@@ -1,5 +1,6 @@
import parser from "./parser";
import AST from "./ast";
+import WhitespaceControl from "./whitespace-control";
module Helpers from "./helpers";
import { extend } from "../utils";
@@ -8,11 +9,17 @@ export { parser };
var yy = {};
extend(yy, Helpers, AST);
-export function parse(input) {
+export function parse(input, options) {
// Just return if an already-compile AST was passed in.
- if (input.constructor === AST.ProgramNode) { return input; }
+ if (input.type === 'Program') { return input; }
parser.yy = yy;
- return parser.parse(input);
+ // Altering the shared object here, but this is ok as parser is a sync operation
+ yy.locInfo = function(locInfo) {
+ return new yy.SourceLocation(options && options.srcName, locInfo);
+ };
+
+ var strip = new WhitespaceControl();
+ return strip.accept(parser.parse(input));
}
diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js
index 7d1b4ca..0fddb7c 100644
--- a/lib/handlebars/compiler/code-gen.js
+++ b/lib/handlebars/compiler/code-gen.js
@@ -79,18 +79,18 @@ CodeGen.prototype = {
},
empty: function(loc) {
- loc = loc || this.currentLocation || {};
- return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile);
+ loc = loc || this.currentLocation || {start:{}};
+ return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
},
wrap: function(chunk, loc) {
if (chunk instanceof SourceNode) {
return chunk;
}
- loc = loc || this.currentLocation || {};
+ loc = loc || this.currentLocation || {start:{}};
chunk = castChunk(chunk, this, loc);
- return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk);
+ return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
},
functionCall: function(fn, type, params) {
@@ -99,7 +99,7 @@ CodeGen.prototype = {
},
quotedString: function(str) {
- return '"' + str
+ return '"' + (str + '')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
@@ -113,7 +113,10 @@ CodeGen.prototype = {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
- pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]);
+ var value = castChunk(obj[key], this);
+ if (value !== 'undefined') {
+ pairs.push([this.quotedString(key), ':', value]);
+ }
}
}
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index 1e5d07a..5ba0916 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -3,6 +3,26 @@ import {isArray} from "../utils";
var slice = [].slice;
+
+// a mustache is definitely a helper if:
+// * it is an eligible helper, and
+// * it has at least one parameter or hash segment
+function helperExpr(sexpr) {
+ return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash);
+}
+
+function scopedId(path) {
+ return (/^\.|this\b/).test(path.original);
+}
+
+// an ID is simple if it only has one part, and that part is not
+// `..` or `this`.
+function simpleId(path) {
+ var part = path.parts[0];
+
+ return path.parts.length === 1 && !scopedId(path) && !path.depth;
+}
+
export function Compiler() {}
// the foundHelper register will disambiguate helper lookup from finding a
@@ -74,12 +94,12 @@ Compiler.prototype = {
return this[node.type](node);
},
- program: function(program) {
- var statements = program.statements;
-
- for(var i=0, l=statements.length; i<l; i++) {
- this.accept(statements[i]);
+ Program: function(program) {
+ var body = program.body;
+ for(var i=0, l=body.length; i<l; i++) {
+ this.accept(body[i]);
}
+
this.isSimple = l === 1;
this.depths.list = this.depths.list.sort(function(a, b) {
@@ -107,24 +127,19 @@ Compiler.prototype = {
return guid;
},
- block: function(block) {
+ BlockStatement: function(block) {
var sexpr = block.sexpr,
program = block.program,
inverse = block.inverse;
- if (program) {
- program = this.compileProgram(program);
- }
-
- if (inverse) {
- inverse = this.compileProgram(inverse);
- }
+ program = program && this.compileProgram(program);
+ inverse = inverse && this.compileProgram(inverse);
var type = this.classifySexpr(sexpr);
- if (type === "helper") {
+ if (type === 'helper') {
this.helperSexpr(sexpr, program, inverse);
- } else if (type === "simple") {
+ } else if (type === 'simple') {
this.simpleSexpr(sexpr);
// now that the simple mustache is resolved, we need to
@@ -132,7 +147,7 @@ Compiler.prototype = {
this.opcode('pushProgram', block, program);
this.opcode('pushProgram', block, inverse);
this.opcode('emptyHash', block);
- this.opcode('blockValue', block, sexpr.id.original);
+ this.opcode('blockValue', block, sexpr.path.original);
} else {
this.ambiguousSexpr(sexpr, program, inverse);
@@ -147,54 +162,30 @@ Compiler.prototype = {
this.opcode('append', block);
},
- hash: function(hash) {
- var pairs = hash.pairs, i, l;
-
- this.opcode('pushHash', hash);
-
- for(i=0, l=pairs.length; i<l; i++) {
- this.pushParam(pairs[i][1]);
- }
- while(i--) {
- this.opcode('assignToHash', hash, pairs[i][0]);
- }
- this.opcode('popHash', hash);
- },
-
- partial: function(partial) {
- var partialName = partial.partialName;
+ PartialStatement: function(partial) {
+ var partialName = partial.sexpr.path.original;
this.usePartial = true;
- if (partial.hash) {
- this.accept(partial.hash);
- } else {
- this.opcode('pushLiteral', partial, 'undefined');
+ var params = partial.sexpr.params;
+ if (params.length > 1) {
+ throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
+ } else if (!params.length) {
+ params.push({type: 'PathExpression', parts: [], depth: 0});
}
- if (partial.context) {
- this.accept(partial.context);
- } else {
- this.opcode('getContext', partial, 0);
- this.opcode('pushContext', partial);
- }
+ this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true);
var indent = partial.indent || '';
if (this.options.preventIndent && indent) {
this.opcode('appendContent', partial, indent);
indent = '';
}
- this.opcode('invokePartial', partial, partialName.name, indent);
+ this.opcode('invokePartial', partial, partialName, indent);
this.opcode('append', partial);
},
- content: function(content) {
- if (content.string) {
- this.opcode('appendContent', content, content.string);
- }
- },
-
- mustache: function(mustache) {
- this.sexpr(mustache.sexpr);
+ MustacheStatement: function(mustache) {
+ this.accept(mustache.sexpr);
if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped', mustache);
@@ -203,106 +194,107 @@ Compiler.prototype = {
}
},
+ ContentStatement: function(content) {
+ if (content.value) {
+ this.opcode('appendContent', content, content.value);
+ }
+ },
+
+ CommentStatement: function() {},
+
+ SubExpression: function(sexpr) {
+ var type = this.classifySexpr(sexpr);
+
+ if (type === 'simple') {
+ this.simpleSexpr(sexpr);
+ } else if (type === 'helper') {
+ this.helperSexpr(sexpr);
+ } else {
+ this.ambiguousSexpr(sexpr);
+ }
+ },
ambiguousSexpr: function(sexpr, program, inverse) {
- var id = sexpr.id,
- name = id.parts[0],
+ var path = sexpr.path,
+ name = path.parts[0],
isBlock = program != null || inverse != null;
- this.opcode('getContext', sexpr, id.depth);
+ this.opcode('getContext', sexpr, path.depth);
this.opcode('pushProgram', sexpr, program);
this.opcode('pushProgram', sexpr, inverse);
- this.ID(id);
+ this.accept(path);
this.opcode('invokeAmbiguous', sexpr, name, isBlock);
},
simpleSexpr: function(sexpr) {
- var id = sexpr.id;
-
- if (id.type === 'DATA') {
- this.DATA(id);
- } else if (id.parts.length) {
- this.ID(id);
- } else {
- // Simplified ID for `this`
- this.addDepth(id.depth);
- this.opcode('getContext', sexpr, id.depth);
- this.opcode('pushContext', sexpr);
- }
-
+ this.accept(sexpr.path);
this.opcode('resolvePossibleLambda', sexpr);
},
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
- id = sexpr.id,
- name = id.parts[0];
+ path = sexpr.path,
+ name = path.parts[0];
if (this.options.knownHelpers[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;
+ path.falsy = true;
- this.ID(id);
- this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple);
- }
- },
-
- sexpr: function(sexpr) {
- var type = this.classifySexpr(sexpr);
-
- if (type === "simple") {
- this.simpleSexpr(sexpr);
- } else if (type === "helper") {
- this.helperSexpr(sexpr);
- } else {
- this.ambiguousSexpr(sexpr);
+ this.accept(path);
+ this.opcode('invokeHelper', sexpr, params.length, path.original, simpleId(path));
}
},
- ID: function(id) {
- this.addDepth(id.depth);
- this.opcode('getContext', id, id.depth);
+ PathExpression: function(path) {
+ this.addDepth(path.depth);
+ this.opcode('getContext', path, path.depth);
- var name = id.parts[0];
+ var name = path.parts[0];
if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
- this.opcode('pushContext', id);
+ this.opcode('pushContext', path);
+ } else if (path.data) {
+ this.options.data = true;
+ this.opcode('lookupData', path, path.depth, path.parts);
} else {
- this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped);
+ this.opcode('lookupOnContext', path, path.parts, path.falsy, scopedId(path));
}
},
- DATA: function(data) {
- this.options.data = true;
- this.opcode('lookupData', data, data.id.depth, data.id.parts);
+ StringLiteral: function(string) {
+ this.opcode('pushString', string, string.value);
},
- STRING: function(string) {
- this.opcode('pushString', string, string.string);
+ NumberLiteral: function(number) {
+ this.opcode('pushLiteral', number, number.value);
},
- NUMBER: function(number) {
- this.opcode('pushLiteral', number, number.number);
+ BooleanLiteral: function(bool) {
+ this.opcode('pushLiteral', bool, bool.value);
},
- BOOLEAN: function(bool) {
- this.opcode('pushLiteral', bool, bool.bool);
- },
+ Hash: function(hash) {
+ var pairs = hash.pairs, i, l;
+
+ this.opcode('pushHash', hash);
- comment: function() {},
+ for (i=0, l=pairs.length; i<l; i++) {
+ this.pushParam(pairs[i].value);
+ }
+ while (i--) {
+ this.opcode('assignToHash', hash, pairs[i].key);
+ }
+ this.opcode('popHash', hash);
+ },
// HELPERS
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 });
+ this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: node.loc });
},
addDepth: function(depth) {
@@ -315,14 +307,21 @@ Compiler.prototype = {
},
classifySexpr: function(sexpr) {
- var isHelper = sexpr.isHelper;
- var isEligible = sexpr.eligibleHelper;
- var options = this.options;
+ // a mustache is an eligible helper if:
+ // * its id is simple (a single part, not `this` or `..`)
+ var isHelper = helperExpr(sexpr);
+
+ // if a mustache is an eligible helper but not a definite
+ // helper, it is ambiguous, and will be resolved in a later
+ // pass or at runtime.
+ var isEligible = isHelper || simpleId(sexpr.path);
+
+ var options = this.options;
// if ambiguous, we can possibly resolve the ambiguity now
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
if (isEligible && !isHelper) {
- var name = sexpr.id.parts[0];
+ var name = sexpr.path.parts[0];
if (options.knownHelpers[name]) {
isHelper = true;
@@ -331,9 +330,9 @@ Compiler.prototype = {
}
}
- if (isHelper) { return "helper"; }
- else if (isEligible) { return "ambiguous"; }
- else { return "simple"; }
+ if (isHelper) { return 'helper'; }
+ else if (isEligible) { return 'ambiguous'; }
+ else { return 'simple'; }
},
pushParams: function(params) {
@@ -343,27 +342,47 @@ Compiler.prototype = {
},
pushParam: function(val) {
+ var value = val.value != null ? val.value : val.original || '';
+
+ // Force helper evaluation
+ if (val.type === 'SubExpression') {
+ val.isHelper = true;
+ }
+
if (this.stringParams) {
+ if (value.replace) {
+ value = value
+ .replace(/^(\.?\.\/)*/g, '')
+ .replace(/\//g, '.');
+ }
+
if(val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val, val.depth || 0);
- this.opcode('pushStringParam', val, val.stringModeValue, val.type);
+ this.opcode('pushStringParam', val, value, val.type);
- if (val.type === 'sexpr') {
- // Subexpressions get evaluated and passed in
+ if (val.type === 'SubExpression') {
+ // SubExpressions get evaluated and passed in
// in string params mode.
- this.sexpr(val);
+ this.accept(val);
}
} else {
if (this.trackIds) {
- this.opcode('pushId', val, val.type, val.idName || val.stringModeValue);
+ value = val.original || value;
+ if (value.replace) {
+ value = value
+ .replace(/^\.\//g, '')
+ .replace(/^\.$/g, '');
+ }
+
+ this.opcode('pushId', val, val.type, value);
}
this.accept(val);
}
},
- setupFullMustacheParams: function(sexpr, program, inverse) {
+ setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
var params = sexpr.params;
this.pushParams(params);
@@ -371,9 +390,9 @@ Compiler.prototype = {
this.opcode('pushProgram', sexpr, inverse);
if (sexpr.hash) {
- this.hash(sexpr.hash);
+ this.accept(sexpr.hash);
} else {
- this.opcode('emptyHash', sexpr);
+ this.opcode('emptyHash', sexpr, omitEmpty);
}
return params;
@@ -381,7 +400,7 @@ Compiler.prototype = {
};
export function precompile(input, options, env) {
- if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
+ if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
}
@@ -393,13 +412,13 @@ export function precompile(input, options, env) {
options.useDepths = true;
}
- var ast = env.parse(input);
+ var ast = env.parse(input, options);
var environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options, env) {
- if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
+ if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
}
@@ -415,7 +434,7 @@ export function compile(input, options, env) {
var compiled;
function compileInput() {
- var ast = env.parse(input);
+ var ast = env.parse(input, options);
var environment = new env.Compiler().compile(ast, options);
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec);
diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js
index 50a3c53..1daddf6 100644
--- a/lib/handlebars/compiler/helpers.js
+++ b/lib/handlebars/compiler/helpers.js
@@ -1,9 +1,21 @@
import Exception from "../exception";
+export function SourceLocation(source, locInfo) {
+ this.source = source;
+ this.start = {
+ line: locInfo.first_line,
+ column: locInfo.first_column
+ };
+ this.end = {
+ line: locInfo.last_line,
+ column: locInfo.last_column
+ };
+}
+
export function stripFlags(open, close) {
return {
- left: open.charAt(2) === '~',
- right: close.charAt(close.length-3) === '~'
+ open: open.charAt(2) === '~',
+ close: close.charAt(close.length-3) === '~'
};
}
@@ -12,222 +24,91 @@ export function stripComment(comment) {
.replace(/-?-?~?\}\}$/, '');
}
-export function prepareRawBlock(openRawBlock, content, close, locInfo) {
+export function preparePath(data, parts, 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);
+ locInfo = this.locInfo(locInfo);
+
+ var original = data ? '@' : '',
+ dig = [],
+ depth = 0,
+ depthString = '';
+
+ for(var i=0,l=parts.length; i<l; i++) {
+ var part = parts[i].part;
+ original += (parts[i].separator || '') + part;
+
+ if (part === '..' || part === '.' || part === 'this') {
+ if (dig.length > 0) {
+ throw new Exception('Invalid path: ' + original, {loc: locInfo});
+ } else if (part === '..') {
+ depth++;
+ depthString += '../';
+ }
+ } else {
+ dig.push(part);
+ }
}
- var program = new this.ProgramNode([content], null, {}, locInfo);
-
- return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo);
+ return new this.PathExpression(data, depth, dig, original, locInfo);
}
-export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
+export function prepareMustache(sexpr, open, strip, locInfo) {
/*jshint -W040 */
- // 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
- };
+ // Must use charAt to support IE pre-10
+ var escapeFlag = open.charAt(3) || open.charAt(2),
+ escaped = escapeFlag !== '{' && escapeFlag !== '&';
- throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode);
- }
+ return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo));
+}
- program.blockParams = openBlock.blockParams;
+export function prepareRawBlock(openRawBlock, content, close, locInfo) {
+ /*jshint -W040 */
+ if (openRawBlock.sexpr.path.original !== close) {
+ var errorNode = {loc: openRawBlock.sexpr.loc};
- // 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: {}};
+ throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode);
}
- // 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;
+ locInfo = this.locInfo(locInfo);
+ var program = new this.Program([content], null, {}, locInfo);
- // 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: openBlock.strip.left,
- right: close.strip.right,
+ return new this.BlockStatement(
+ openRawBlock.sexpr, program, undefined,
+ {}, {}, {},
+ locInfo);
+}
- // 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((firstInverse || program).statements)
- };
+export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
+ /*jshint -W040 */
+ // When we are chaining inverse calls, we will not have a close path
+ if (close && close.path && openBlock.sexpr.path.original !== close.path.original) {
+ var errorNode = {loc: openBlock.sexpr.loc};
- if (openBlock.strip.right) {
- omitRight(program.statements, null, true);
+ throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode);
}
- if (inverse) {
- var inverseStrip = inverseAndProgram.strip;
+ program.blockParams = openBlock.blockParams;
- if (inverseStrip.left) {
- omitLeft(program.statements, null, true);
- }
+ var inverse,
+ inverseStrip;
- if (inverseStrip.right) {
- omitRight(firstInverse.statements, null, true);
- }
- if (close.strip.left) {
- omitLeft(lastInverse.statements, null, true);
+ if (inverseAndProgram) {
+ if (inverseAndProgram.chain) {
+ inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip;
}
- // Find standalone else statments
- if (isPrevWhitespace(program.statements)
- && isNextWhitespace(firstInverse.statements)) {
-
- omitLeft(program.statements);
- omitRight(firstInverse.statements);
- }
- } else {
- if (close.strip.left) {
- omitLeft(program.statements, null, true);
- }
+ inverseStrip = inverseAndProgram.strip;
+ inverse = inverseAndProgram.program;
}
if (inverted) {
- return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo);
- } else {
- return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo);
- }
-}
-
-
-export function prepareProgram(statements, isRoot) {
- for (var i = 0, l = statements.length; i < l; i++) {
- var current = statements[i],
- strip = current.strip;
-
- if (!strip) {
- continue;
- }
-
- var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'),
- _isNextWhitespace = isNextWhitespace(statements, i, isRoot),
-
- openStandalone = strip.openStandalone && _isPrevWhitespace,
- closeStandalone = strip.closeStandalone && _isNextWhitespace,
- inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
-
- if (strip.right) {
- omitRight(statements, i, true);
- }
- if (strip.left) {
- omitLeft(statements, i, true);
- }
-
- if (inlineStandalone) {
- omitRight(statements, i);
-
- if (omitLeft(statements, i)) {
- // If we are on a standalone node, save the indent info for partials
- if (current.type === 'partial') {
- // Pull out the whitespace from the final line
- current.indent = (/([ \t]+$)/).exec(statements[i-1].original)[1];
- }
- }
- }
- if (openStandalone) {
- omitRight((current.program || current.inverse).statements);
-
- // Strip out the previous content node if it's whitespace only
- omitLeft(statements, i);
- }
- if (closeStandalone) {
- // Always strip the next node
- omitRight(statements, i);
-
- omitLeft((current.inverse || current.program).statements);
- }
- }
-
- return statements;
-}
-
-function isPrevWhitespace(statements, i, isRoot) {
- if (i === undefined) {
- i = statements.length;
- }
-
- // Nodes that end with newlines are considered whitespace (but are special
- // cased for strip operations)
- var prev = statements[i-1],
- sibling = statements[i-2];
- if (!prev) {
- return isRoot;
- }
-
- if (prev.type === 'content') {
- return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original);
- }
-}
-function isNextWhitespace(statements, i, isRoot) {
- if (i === undefined) {
- i = -1;
- }
-
- var next = statements[i+1],
- sibling = statements[i+2];
- if (!next) {
- return isRoot;
- }
-
- if (next.type === 'content') {
- return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original);
- }
-}
-
-// Marks the node to the right of the position as omitted.
-// I.e. {{foo}}' ' will mark the ' ' node as omitted.
-//
-// If i is undefined, then the first child will be marked as such.
-//
-// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
-// content is met.
-function omitRight(statements, i, multiple) {
- var current = statements[i == null ? 0 : i + 1];
- if (!current || current.type !== 'content' || (!multiple && current.rightStripped)) {
- return;
- }
-
- var original = current.string;
- current.string = current.string.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
- current.rightStripped = current.string !== original;
-}
-
-// Marks the node to the left of the position as omitted.
-// I.e. ' '{{foo}} will mark the ' ' node as omitted.
-//
-// If i is undefined then the last child will be marked as such.
-//
-// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
-// content is met.
-function omitLeft(statements, i, multiple) {
- var current = statements[i == null ? statements.length - 1 : i - 1];
- if (!current || current.type !== 'content' || (!multiple && current.leftStripped)) {
- return;
+ inverted = inverse;
+ inverse = program;
+ program = inverted;
}
- // We omit the last node if it's whitespace only and not preceeded by a non-content node.
- var original = current.string;
- current.string = current.string.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
- current.leftStripped = current.string !== original;
- return current.leftStripped;
+ return new this.BlockStatement(
+ openBlock.sexpr, program, inverse,
+ openBlock.strip, inverseStrip, close && (close.strip || close.openStrip),
+ this.locInfo(locInfo));
}
diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js
index ba84a3e..db19778 100644
--- a/lib/handlebars/compiler/javascript-compiler.js
+++ b/lib/handlebars/compiler/javascript-compiler.js
@@ -136,7 +136,7 @@ JavaScriptCompiler.prototype = {
if (!asObject) {
ret.compiler = JSON.stringify(ret.compiler);
- this.source.currentLocation = {firstLine: 1, firstColumn: 0};
+ this.source.currentLocation = {start: {line: 1, column: 0}};
ret = this.objectLiteral(ret);
if (options.srcName) {
@@ -275,7 +275,7 @@ JavaScriptCompiler.prototype = {
blockValue: function(name) {
var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
params = [this.contextName(0)];
- this.setupParams(name, 0, params);
+ this.setupHelperArgs(name, 0, params);
var blockName = this.popStack();
params.splice(1, 0, blockName);
@@ -293,7 +293,7 @@ JavaScriptCompiler.prototype = {
// We're being a bit cheeky and reusing the options value from the prior exec
var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
params = [this.contextName(0)];
- this.setupParams('', 0, params, true);
+ this.setupHelperArgs('', 0, params, true);
this.flushInline();
@@ -460,7 +460,7 @@ JavaScriptCompiler.prototype = {
// If it's a subexpression, the string result
// will be pushed after this opcode.
- if (type !== 'sexpr') {
+ if (type !== 'SubExpression') {
if (typeof string === 'string') {
this.pushString(string);
} else {
@@ -469,9 +469,7 @@ JavaScriptCompiler.prototype = {
}
},
- emptyHash: function() {
- this.pushStackLiteral('{}');
-
+ emptyHash: function(omitEmpty) {
if (this.trackIds) {
this.push('{}'); // hashIds
}
@@ -479,6 +477,7 @@ JavaScriptCompiler.prototype = {
this.push('{}'); // hashContexts
this.push('{}'); // hashTypes
}
+ this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
},
pushHash: function() {
if (this.hash) {
@@ -611,16 +610,22 @@ JavaScriptCompiler.prototype = {
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(name, indent) {
- var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"];
+ var params = [],
+ options = this.setupParams(name, 1, params, false);
- if (this.options.data) {
- params.push("data");
- } else if (this.options.compat) {
- params.push('undefined');
+ if (indent) {
+ options.indent = JSON.stringify(indent);
}
+ options.helpers = 'helpers';
+ options.partials = 'partials';
+
+ params.unshift(this.nameLookup('partials', name, 'partial'));
+
if (this.options.compat) {
- params.push('depths');
+ options.depths = 'depths';
}
+ options = this.objectLiteral(options);
+ params.push(options);
this.push(this.source.functionCall('this.invokePartial', '', params));
},
@@ -659,9 +664,9 @@ JavaScriptCompiler.prototype = {
},
pushId: function(type, name) {
- if (type === 'ID' || type === 'DATA') {
+ if (type === 'PathExpression') {
this.pushString(name);
- } else if (type === 'sexpr') {
+ } else if (type === 'SubExpression') {
this.pushStackLiteral('true');
} else {
this.pushStackLiteral('null');
@@ -881,7 +886,7 @@ JavaScriptCompiler.prototype = {
setupHelper: function(paramSize, name, blockHelper) {
var params = [],
- paramsInit = this.setupParams(name, paramSize, params, blockHelper);
+ paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
var foundHelper = this.nameLookup('helpers', name, 'helper');
return {
@@ -892,8 +897,8 @@ JavaScriptCompiler.prototype = {
};
},
- setupParams: function(helper, paramSize, params, useRegister) {
- var options = {}, contexts = [], types = [], ids = [], param, inverse, program;
+ setupParams: function(helper, paramSize, params) {
+ var options = {}, contexts = [], types = [], ids = [], param;
options.name = this.quotedString(helper);
options.hash = this.popStack();
@@ -906,8 +911,8 @@ JavaScriptCompiler.prototype = {
options.hashContexts = this.popStack();
}
- inverse = this.popStack();
- program = this.popStack();
+ var inverse = this.popStack(),
+ program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)`
@@ -943,7 +948,11 @@ JavaScriptCompiler.prototype = {
if (this.options.data) {
options.data = "data";
}
+ return options;
+ },
+ setupHelperArgs: function(helper, paramSize, params, useRegister) {
+ var options = this.setupParams(helper, paramSize, params, true);
options = this.objectLiteral(options);
if (useRegister) {
this.useRegister('options');
diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js
index e93652c..b549d61 100644
--- a/lib/handlebars/compiler/printer.js
+++ b/lib/handlebars/compiler/printer.js
@@ -21,22 +21,22 @@ PrintVisitor.prototype.pad = function(string) {
return out;
};
-PrintVisitor.prototype.program = function(program) {
- var out = "",
- statements = program.statements,
+PrintVisitor.prototype.Program = function(program) {
+ var out = '',
+ body = program.body,
i, l;
if (program.blockParams) {
- var blockParams = "BLOCK PARAMS: [";
+ var blockParams = 'BLOCK PARAMS: [';
for(i=0, l=program.blockParams.length; i<l; i++) {
- blockParams += " " + program.blockParams[i];
+ blockParams += ' ' + program.blockParams[i];
}
- blockParams += " ]";
+ blockParams += ' ]';
out += this.pad(blockParams);
}
- for(i=0, l=statements.length; i<l; i++) {
- out = out + this.accept(statements[i]);
+ for(i=0, l=body.length; i<l; i++) {
+ out = out + this.accept(body[i]);
}
this.padding--;
@@ -44,21 +44,25 @@ PrintVisitor.prototype.program = function(program) {
return out;
};
-PrintVisitor.prototype.block = function(block) {
+PrintVisitor.prototype.MustacheStatement = function(mustache) {
+ return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}');
+};
+
+PrintVisitor.prototype.BlockStatement = function(block) {
var out = "";
- out = out + this.pad("BLOCK:");
+ out = out + this.pad('BLOCK:');
this.padding++;
out = out + this.pad(this.accept(block.sexpr));
if (block.program) {
- out = out + this.pad("PROGRAM:");
+ out = out + this.pad('PROGRAM:');
this.padding++;
out = out + this.accept(block.program);
this.padding--;
}
if (block.inverse) {
if (block.program) { this.padding++; }
- out = out + this.pad("{{^}}");
+ out = out + this.pad('{{^}}');
this.padding++;
out = out + this.accept(block.inverse);
this.padding--;
@@ -69,7 +73,27 @@ PrintVisitor.prototype.block = function(block) {
return out;
};
-PrintVisitor.prototype.sexpr = function(sexpr) {
+PrintVisitor.prototype.PartialStatement = function(partial) {
+ var sexpr = partial.sexpr,
+ content = 'PARTIAL:' + sexpr.path.original;
+ if(sexpr.params[0]) {
+ content += ' ' + this.accept(sexpr.params[0]);
+ }
+ if (sexpr.hash) {
+ content += ' ' + this.accept(sexpr.hash);
+ }
+ return this.pad('{{> ' + content + ' }}');
+};
+
+PrintVisitor.prototype.ContentStatement = function(content) {
+ return this.pad("CONTENT[ '" + content.value + "' ]");
+};
+
+PrintVisitor.prototype.CommentStatement = function(comment) {
+ return this.pad("{{! '" + comment.value + "' }}");
+};
+
+PrintVisitor.prototype.SubExpression = function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;
for(var i=0, l=params.length; i<l; i++) {
@@ -80,71 +104,37 @@ PrintVisitor.prototype.sexpr = function(sexpr) {
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";
- return this.accept(sexpr.id) + " " + params + hash;
+ return this.accept(sexpr.path) + " " + params + hash;
};
-PrintVisitor.prototype.mustache = function(mustache) {
- return this.pad("{{ " + this.accept(mustache.sexpr) + " }}");
-};
-
-PrintVisitor.prototype.partial = function(partial) {
- var content = this.accept(partial.partialName);
- if(partial.context) {
- content += " " + this.accept(partial.context);
- }
- if (partial.hash) {
- content += " " + this.accept(partial.hash);
- }
- return this.pad("{{> " + content + " }}");
+PrintVisitor.prototype.PathExpression = function(id) {
+ var path = id.parts.join('/');
+ return (id.data ? '@' : '') + 'PATH:' + path;
};
-PrintVisitor.prototype.hash = function(hash) {
- var pairs = hash.pairs;
- var joinedPairs = [], left, right;
-
- for(var i=0, l=pairs.length; i<l; i++) {
- left = pairs[i][0];
- right = this.accept(pairs[i][1]);
- joinedPairs.push( left + "=" + right );
- }
- return "HASH{" + joinedPairs.join(", ") + "}";
+PrintVisitor.prototype.StringLiteral = function(string) {
+ return '"' + string.value + '"';
};
-PrintVisitor.prototype.STRING = function(string) {
- return '"' + string.string + '"';
+PrintVisitor.prototype.NumberLiteral = function(number) {
+ return "NUMBER{" + number.value + "}";
};
-PrintVisitor.prototype.NUMBER = function(number) {
- return "NUMBER{" + number.number + "}";
+PrintVisitor.prototype.BooleanLiteral = function(bool) {
+ return "BOOLEAN{" + bool.value + "}";
};
-PrintVisitor.prototype.BOOLEAN = function(bool) {
- return "BOOLEAN{" + bool.bool + "}";
-};
+PrintVisitor.prototype.Hash = function(hash) {
+ var pairs = hash.pairs;
+ var joinedPairs = [], left, right;
-PrintVisitor.prototype.ID = function(id) {
- var path = id.parts.join("/");
- if(id.parts.length > 1) {
- return "PATH:" + path;
- } else {
- return "ID:" + path;
+ for (var i=0, l=pairs.length; i<l; i++) {
+ joinedPairs.push(this.accept(pairs[i]));
}
-};
-
-PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
- return "PARTIAL:" + partialName.name;
-};
-PrintVisitor.prototype.DATA = function(data) {
- return "@" + this.accept(data.id);
+ return 'HASH{' + joinedPairs.join(', ') + '}';
};
-
-PrintVisitor.prototype.content = function(content) {
- return this.pad("CONTENT[ '" + content.string + "' ]");
+PrintVisitor.prototype.HashPair = function(pair) {
+ return pair.key + '=' + this.accept(pair.value);
};
-
-PrintVisitor.prototype.comment = function(comment) {
- return this.pad("{{! '" + comment.comment + "' }}");
-};
-
diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js
index a4eb2b4..c0cfab6 100644
--- a/lib/handlebars/compiler/visitor.js
+++ b/lib/handlebars/compiler/visitor.js
@@ -4,64 +4,63 @@ Visitor.prototype = {
constructor: Visitor,
accept: function(object) {
- return object && this[object.type] && this[object.type](object);
+ return object && this[object.type](object);
},
- program: function(program) {
- var statements = program.statements,
+ Program: function(program) {
+ var body = program.body,
i, l;
- for(i=0, l=statements.length; i<l; i++) {
- this.accept(statements[i]);
+ for(i=0, l=body.length; i<l; i++) {
+ this.accept(body[i]);
}
},
- block: function(block) {
- this.accept(block.mustache);
+ MustacheStatement: function(mustache) {
+ this.accept(mustache.sexpr);
+ },
+
+ BlockStatement: function(block) {
+ this.accept(block.sexpr);
this.accept(block.program);
this.accept(block.inverse);
},
- mustache: function(mustache) {
- this.accept(mustache.sexpr);
+ PartialStatement: function(partial) {
+ this.accept(partial.partialName);
+ this.accept(partial.context);
+ this.accept(partial.hash);
},
- sexpr: function(sexpr) {
+ ContentStatement: function(content) {},
+ CommentStatement: function(comment) {},
+
+ SubExpression: function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;
- this.accept(sexpr.id);
+ this.accept(sexpr.path);
for(var i=0, l=params.length; i<l; i++) {
this.accept(params[i]);
}
this.accept(sexpr.hash);
},
- hash: function(hash) {
+ PathExpression: function(path) {},
+
+ StringLiteral: function(string) {},
+ NumberLiteral: function(number) {},
+ BooleanLiteral: function(bool) {},
+
+ Hash: function(hash) {
var pairs = hash.pairs;
for(var i=0, l=pairs.length; i<l; i++) {
- this.accept(pairs[i][1]);
+ this.accept(pairs[i]);
}
},
-
- 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) {}
+ HashPair: function(pair) {
+ this.accept(pair.value);
+ }
};
export default Visitor;
diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js
new file mode 100644
index 0000000..e10eb15
--- /dev/null
+++ b/lib/handlebars/compiler/whitespace-control.js
@@ -0,0 +1,210 @@
+import Visitor from "./visitor";
+
+function WhitespaceControl() {
+}
+WhitespaceControl.prototype = new Visitor();
+
+WhitespaceControl.prototype.Program = function(program) {
+ var isRoot = !this.isRootSeen;
+ this.isRootSeen = true;
+
+ var body = program.body;
+ for (var i = 0, l = body.length; i < l; i++) {
+ var current = body[i],
+ strip = this.accept(current);
+
+ if (!strip) {
+ continue;
+ }
+
+ var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
+ _isNextWhitespace = isNextWhitespace(body, i, isRoot),
+
+ openStandalone = strip.openStandalone && _isPrevWhitespace,
+ closeStandalone = strip.closeStandalone && _isNextWhitespace,
+ inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
+
+ if (strip.close) {
+ omitRight(body, i, true);
+ }
+ if (strip.open) {
+ omitLeft(body, i, true);
+ }
+
+ if (inlineStandalone) {
+ omitRight(body, i);
+
+ if (omitLeft(body, i)) {
+ // If we are on a standalone node, save the indent info for partials
+ if (current.type === 'PartialStatement') {
+ // Pull out the whitespace from the final line
+ current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1];
+ }
+ }
+ }
+ if (openStandalone) {
+ omitRight((current.program || current.inverse).body);
+
+ // Strip out the previous content node if it's whitespace only
+ omitLeft(body, i);
+ }
+ if (closeStandalone) {
+ // Always strip the next node
+ omitRight(body, i);
+
+ omitLeft((current.inverse || current.program).body);
+ }
+ }
+
+ return program;
+};
+WhitespaceControl.prototype.BlockStatement = function(block) {
+ this.accept(block.program);
+ this.accept(block.inverse);
+
+ // Find the inverse program that is involed with whitespace stripping.
+ var program = block.program || block.inverse,
+ inverse = block.program && block.inverse,
+ firstInverse = inverse,
+ lastInverse = inverse;
+
+ if (inverse && inverse.chained) {
+ firstInverse = inverse.body[0].program;
+
+ // Walk the inverse chain to find the last inverse that is actually in the chain.
+ while (lastInverse.chained) {
+ lastInverse = lastInverse.body[lastInverse.body.length-1].program;
+ }
+ }
+
+ var strip = {
+ open: block.openStrip.open,
+ close: block.closeStrip.close,
+
+ // 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.body),
+ closeStandalone: isPrevWhitespace((firstInverse || program).body)
+ };
+
+ if (block.openStrip.close) {
+ omitRight(program.body, null, true);
+ }
+
+ if (inverse) {
+ var inverseStrip = block.inverseStrip;
+
+ if (inverseStrip.open) {
+ omitLeft(program.body, null, true);
+ }
+
+ if (inverseStrip.close) {
+ omitRight(firstInverse.body, null, true);
+ }
+ if (block.closeStrip.open) {
+ omitLeft(lastInverse.body, null, true);
+ }
+
+ // Find standalone else statments
+ if (isPrevWhitespace(program.body)
+ && isNextWhitespace(firstInverse.body)) {
+
+ omitLeft(program.body);
+ omitRight(firstInverse.body);
+ }
+ } else {
+ if (block.closeStrip.open) {
+ omitLeft(program.body, null, true);
+ }
+ }
+
+ return strip;
+};
+
+WhitespaceControl.prototype.MustacheStatement = function(mustache) {
+ return mustache.strip;
+};
+
+WhitespaceControl.prototype.PartialStatement =
+ WhitespaceControl.prototype.CommentStatement = function(node) {
+ var strip = node.strip || {};
+ return {
+ inlineStandalone: true,
+ open: strip.open,
+ close: strip.close
+ };
+};
+
+
+function isPrevWhitespace(body, i, isRoot) {
+ if (i === undefined) {
+ i = body.length;
+ }
+
+ // Nodes that end with newlines are considered whitespace (but are special
+ // cased for strip operations)
+ var prev = body[i-1],
+ sibling = body[i-2];
+ if (!prev) {
+ return isRoot;
+ }
+
+ if (prev.type === 'ContentStatement') {
+ return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original);
+ }
+}
+function isNextWhitespace(body, i, isRoot) {
+ if (i === undefined) {
+ i = -1;
+ }
+
+ var next = body[i+1],
+ sibling = body[i+2];
+ if (!next) {
+ return isRoot;
+ }
+
+ if (next.type === 'ContentStatement') {
+ return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original);
+ }
+}
+
+// Marks the node to the right of the position as omitted.
+// I.e. {{foo}}' ' will mark the ' ' node as omitted.
+//
+// If i is undefined, then the first child will be marked as such.
+//
+// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
+// content is met.
+function omitRight(body, i, multiple) {
+ var current = body[i == null ? 0 : i + 1];
+ if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) {
+ return;
+ }
+
+ var original = current.value;
+ current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
+ current.rightStripped = current.value !== original;
+}
+
+// Marks the node to the left of the position as omitted.
+// I.e. ' '{{foo}} will mark the ' ' node as omitted.
+//
+// If i is undefined then the last child will be marked as such.
+//
+// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
+// content is met.
+function omitLeft(body, i, multiple) {
+ var current = body[i == null ? body.length - 1 : i - 1];
+ if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) {
+ return;
+ }
+
+ // We omit the last node if it's whitespace only and not preceeded by a non-content node.
+ var original = current.value;
+ current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
+ current.leftStripped = current.value !== original;
+ return current.leftStripped;
+}
+
+export default WhitespaceControl;