summaryrefslogtreecommitdiffstats
path: root/lib/handlebars/compiler/compiler.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/handlebars/compiler/compiler.js')
-rw-r--r--lib/handlebars/compiler/compiler.js271
1 files changed, 145 insertions, 126 deletions
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);