b09333db79
Handlebars now supports subexpressions.
{{foo (bar 3)}}
Subexpressions are always evaluated as helpers; if
`3` were omitted from the above example, `bar`
would be invoked as a param-less helper, even
though a top-levell `{{bar}}` would be considered
ambiguous.
The return value of a subexpression helper is
passed in as a parameter of a parent subexpression
helper, even in string params mode. Their type,
as listed in `options.types` or `options.hashTypes`
in string params mode, is "sexpr".
The main conceptual change in the Handlebars code
is that there is a new AST.SexprNode that manages
the params/hash passed into a mustache, as well
as the logic that governs whether that mustache
is a helper invocation, property lookup, etc.
MustacheNode, which used to manage this stuff,
still exists, but only manages things like
white-space stripping and whether the mustache
is escaped or not. So, a MustacheNode _has_
a SexprNode.
The introduction of subexpressions is fully
backwards compatible, but a few things needed
to change about the compiled output of a template
in order to support subexpressions. The main one
is that the options hash is no longer stashed in
a local `options` var before being passed to
either the helper being invoked or the
`helperMissing` fallback. Rather, the options
object is inlined in these cases. This does
mean compiled template sizes will be a little
bit larger, even those that don't make use of
subexpressions, but shouldn't have any noticeable
impact on performance when actually rendering
templates as only one of these inlined objects
will actually get evaluated.
218 lines
6.1 KiB
JavaScript
218 lines
6.1 KiB
JavaScript
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, inverseStrip, inverse, locInfo) {
|
|
var inverseLocationInfo, firstInverseNode;
|
|
if (arguments.length === 3) {
|
|
locInfo = inverse;
|
|
inverse = null;
|
|
} else if (arguments.length === 2) {
|
|
locInfo = inverseStrip;
|
|
inverseStrip = null;
|
|
}
|
|
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "program";
|
|
this.statements = statements;
|
|
this.strip = {};
|
|
|
|
if(inverse) {
|
|
firstInverseNode = inverse[0];
|
|
if (firstInverseNode) {
|
|
inverseLocationInfo = {
|
|
first_line: firstInverseNode.firstLine,
|
|
last_line: firstInverseNode.lastLine,
|
|
last_column: firstInverseNode.lastColumn,
|
|
first_column: firstInverseNode.firstColumn
|
|
};
|
|
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
|
|
} else {
|
|
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
|
|
}
|
|
this.strip.right = inverseStrip.left;
|
|
} else if (inverseStrip) {
|
|
this.strip.left = inverseStrip.right;
|
|
}
|
|
},
|
|
|
|
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) {
|
|
this.type = "sexpr";
|
|
this.hash = hash;
|
|
|
|
var id = this.id = rawParams[0];
|
|
var params = this.params = rawParams.slice(1);
|
|
|
|
// a mustache is an eligible helper if:
|
|
// * its id is simple (a single part, not `this` or `..`)
|
|
var eligibleHelper = this.eligibleHelper = id.isSimple;
|
|
|
|
// a mustache is definitely a helper if:
|
|
// * it is an eligible helper, and
|
|
// * it has at least one parameter or hash segment
|
|
this.isHelper = eligibleHelper && (params.length || hash);
|
|
|
|
// 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.
|
|
},
|
|
|
|
PartialNode: function(partialName, context, strip, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "partial";
|
|
this.partialName = partialName;
|
|
this.context = context;
|
|
this.strip = strip;
|
|
},
|
|
|
|
BlockNode: function(mustache, program, inverse, close, locInfo) {
|
|
if(mustache.sexpr.id.original !== close.path.original) {
|
|
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original);
|
|
}
|
|
|
|
LocationInfo.call(this, locInfo);
|
|
|
|
this.type = 'block';
|
|
this.mustache = mustache;
|
|
this.program = program;
|
|
this.inverse = inverse;
|
|
|
|
this.strip = {
|
|
left: mustache.strip.left,
|
|
right: close.strip.right
|
|
};
|
|
|
|
(program || inverse).strip.left = mustache.strip.right;
|
|
(inverse || program).strip.right = close.strip.left;
|
|
|
|
if (inverse && !program) {
|
|
this.isInverse = true;
|
|
}
|
|
},
|
|
|
|
ContentNode: function(string, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "content";
|
|
this.string = string;
|
|
},
|
|
|
|
HashNode: function(pairs, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "hash";
|
|
this.pairs = pairs;
|
|
},
|
|
|
|
IdNode: function(parts, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "ID";
|
|
|
|
var original = "",
|
|
dig = [],
|
|
depth = 0;
|
|
|
|
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); }
|
|
else if (part === "..") { depth++; }
|
|
else { this.isScoped = true; }
|
|
}
|
|
else { dig.push(part); }
|
|
}
|
|
|
|
this.original = original;
|
|
this.parts = dig;
|
|
this.string = dig.join('.');
|
|
this.depth = depth;
|
|
|
|
// 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;
|
|
|
|
this.stringModeValue = this.string;
|
|
},
|
|
|
|
PartialNameNode: function(name, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "PARTIAL_NAME";
|
|
this.name = name.original;
|
|
},
|
|
|
|
DataNode: function(id, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "DATA";
|
|
this.id = id;
|
|
},
|
|
|
|
StringNode: function(string, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "STRING";
|
|
this.original =
|
|
this.string =
|
|
this.stringModeValue = string;
|
|
},
|
|
|
|
IntegerNode: function(integer, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "INTEGER";
|
|
this.original =
|
|
this.integer = integer;
|
|
this.stringModeValue = Number(integer);
|
|
},
|
|
|
|
BooleanNode: function(bool, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "BOOLEAN";
|
|
this.bool = bool;
|
|
this.stringModeValue = bool === "true";
|
|
},
|
|
|
|
CommentNode: function(comment, locInfo) {
|
|
LocationInfo.call(this, locInfo);
|
|
this.type = "comment";
|
|
this.comment = comment;
|
|
}
|
|
};
|
|
|
|
// Must be exported as an object rather than the root of the module as the jison lexer
|
|
// most modify the object to operate properly.
|
|
export default AST;
|