Merge pull request #915 from wycats/ast-update

Ast update
This commit is contained in:
Kevin Decker
2014-11-29 18:02:12 -06:00
20 changed files with 1141 additions and 944 deletions
+228
View File
@@ -0,0 +1,228 @@
# Handlebars Compiler APIs
There are a number of formal APIs that tool implementors may interact with.
## AST
Other tools may interact with the formal AST as defined below. Any JSON structure matching this pattern may be used and passed into the `compile` and `precompile` methods in the same way as the text for a template.
AST structures may be generated either with the `Handlebars.parse` method and then manipulated, via the `Handlebars.AST` objects of the same name, or constructed manually as a generic JavaScript object matching the structure defined below.
```javascript
var ast = Handlebars.parse(myTemplate);
// Modify ast
Handlebars.precompile(ast);
```
### Basic
```java
interface Node {
type: string;
loc: SourceLocation | null;
}
interface SourceLocation {
source: string | null;
start: Position;
end: Position;
}
interface Position {
line: uint >= 1;
column: uint >= 0;
}
```
### Programs
```java
interface Program <: Node {
type: "Program";
body: [ Statement ];
blockParams: [ string ];
}
```
### Statements
```java
interface Statement <: Node { }
interface MustacheStatement <: Statement {
type: "MustacheStatement";
sexpr: SubExpression;
escaped: boolean;
strip: StripFlags | null;
}
interface BlockStatement <: Statement {
type: "BlockStatement";
sexpr: SubExpression;
program: Program | null;
inverse: Program | null;
openStrip: StripFlags | null;
inverseStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
interface PartialStatement <: Statement {
type: "PartialStatement";
sexpr: SubExpression;
indent: string;
strip: StripFlags | null;
}
interface ContentStatement <: Statement {
type: "ContentStatement";
value: string;
original: string;
}
interface CommentStatement <: Statement {
type: "CommentStatement";
value: string;
strip: StripFlags | null;
}
```
### Expressions
```java
interface Expression <: Node { }
```
##### SubExpressions
```java
interface SubExpression <: Expression {
type: "SubExpression";
path: PathExpression;
params: [ Expression ];
hash: Hash;
isHelper: true | null;
}
```
`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors.
##### Paths
```java
interface PathExpression <: Expression {
type: "PathExpression";
data: boolean;
depth: uint >= 0;
parts: [ string ];
original: string;
}
```
- `data` is true when the given expression is a `@data` reference.
- `depth` is an integer representation of which context the expression references. `0` represents the current context, `1` would be `../`, etc.
- `parts` is an array of the names in the path. `foo.bar` would be `['foo', 'bar']`. Scope references, `.`, `..`, and `this` should be omitted from this array.
- `original` is the path as entered by the user. Separator and scope references are left untouched.
##### Literals
```java
interface Literal <: Expression { }
interface StringLiteral <: Literal {
type: "StringLiteral";
value: string;
original: string;
}
interface BooleanLiteral <: Literal {
type: "BooleanLiteral";
value: boolean;
original: boolean;
}
interface NumberLiteral <: Literal {
type: "NumberLiteral";
value: number;
original: number;
}
```
### Miscellaneous
```java
interface Hash <: Node {
type: "Hash";
pairs: [ HashPair ];
}
interface HashPair <: Node {
type: "HashPair";
key: string;
value: Expression;
}
interface StripFlags {
open: boolean;
close: boolean;
}
```
`StripFlags` are used to signify whitespace control character that may have been entered on a given statement.
## AST Visitor
`Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes.
Recording all referenced partial names:
```javascript
var Visitor = Handlebars.Visitor;
function ImportScanner() {
this.partials = [];
}
ImportScanner.prototype = new Visitor();
ImportScanner.prototype.PartialStatement = function(partial) {
this.partials.push({request: partial.sexpr.original});
Visitor.prototype.PartialStatement.call(this, partial);
};
var scanner = new ImportScanner();
scanner.accept(ast);
```
## JavaScript Compiler
The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler:
```javascript
function MyCompiler() {
Handlebars.JavaScriptCompiler.apply(this, arguments);
}
MyCompiler.prototype = Object.create(Handlebars.JavaScriptCompiler);
MyCompiler.nameLookup = function(parent, name, type) {
if (type === 'partial') {
return 'MyPartialList[' + JSON.stringify(name) ']';
} else {
return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(this, parent, name, type);
}
};
var env = Handlebars.create();
env.JavaScriptCompiler = MyCompiler;
env.compile('my template');
```
+87 -171
View File
@@ -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";
MustacheStatement: function(sexpr, escaped, strip, locInfo) {
this.loc = locInfo;
this.type = 'MustacheStatement';
this.sexpr = sexpr;
this.escaped = escaped;
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);
BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
this.loc = locInfo;
this.type = "sexpr";
this.hash = hash;
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// 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.
},
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);
this.type = 'block';
this.type = 'BlockStatement';
this.sexpr = sexpr;
this.program = program;
this.inverse = inverse;
this.openStrip = openStrip;
this.inverseStrip = inverseStrip;
this.closeStrip = closeStrip;
},
PartialStatement: function(sexpr, strip, locInfo) {
this.loc = locInfo;
this.type = 'PartialStatement';
this.sexpr = sexpr;
this.indent = '';
this.strip = strip;
if (inverse && !program) {
this.isInverse = true;
}
},
ContentNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "content";
this.original = this.string = string;
ContentStatement: function(string, locInfo) {
this.loc = locInfo;
this.type = 'ContentStatement';
this.original = this.value = string;
},
HashNode: function(pairs, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "hash";
CommentStatement: function(comment, strip, locInfo) {
this.loc = locInfo;
this.type = 'CommentStatement';
this.value = comment;
this.strip = strip;
},
SubExpression: function(path, params, hash, locInfo) {
this.loc = locInfo;
this.type = 'SubExpression';
this.path = path;
this.params = params || [];
this.hash = hash;
},
PathExpression: function(data, depth, parts, original, locInfo) {
this.loc = locInfo;
this.type = 'PathExpression';
this.data = data;
this.original = original;
this.parts = parts;
this.depth = depth;
},
StringLiteral: function(string, locInfo) {
this.loc = locInfo;
this.type = 'StringLiteral';
this.original =
this.value = string;
},
NumberLiteral: function(number, locInfo) {
this.loc = locInfo;
this.type = 'NumberLiteral';
this.original =
this.value = Number(number);
},
BooleanLiteral: function(bool, locInfo) {
this.loc = locInfo;
this.type = 'BooleanLiteral';
this.original =
this.value = bool === 'true';
},
Hash: function(pairs, locInfo) {
this.loc = locInfo;
this.type = 'Hash';
this.pairs = pairs;
},
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);
}
}
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.idName = depthString + this.string;
// 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;
this.stringModeValue = id.stringModeValue;
this.idName = '@' + id.stringModeValue;
},
StringNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
},
NumberNode: function(number, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "NUMBER";
this.original =
this.number = number;
this.stringModeValue = Number(number);
},
BooleanNode: function(bool, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
},
CommentNode: function(comment, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "comment";
this.comment = comment;
this.strip = strip;
strip.inlineStandalone = true;
HashPair: function(key, value, locInfo) {
this.loc = locInfo;
this.type = 'HashPair';
this.key = key;
this.value = value;
}
};
+10 -3
View File
@@ -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));
}
+9 -6
View File
@@ -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]);
}
}
}
+145 -126
View File
@@ -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);
this.accept(path);
this.opcode('invokeHelper', sexpr, params.length, path.original, simpleId(path));
}
},
sexpr: function(sexpr) {
var type = this.classifySexpr(sexpr);
PathExpression: function(path) {
this.addDepth(path.depth);
this.opcode('getContext', path, path.depth);
if (type === "simple") {
this.simpleSexpr(sexpr);
} else if (type === "helper") {
this.helperSexpr(sexpr);
} else {
this.ambiguousSexpr(sexpr);
}
},
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id, id.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;
comment: function() {},
this.opcode('pushHash', hash);
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);
+79 -198
View File
@@ -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
};
locInfo = this.locInfo(locInfo);
throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode);
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.PathExpression(data, depth, dig, original, locInfo);
}
return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo);
export function prepareMustache(sexpr, open, strip, locInfo) {
/*jshint -W040 */
// Must use charAt to support IE pre-10
var escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';
return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo));
}
export function prepareRawBlock(openRawBlock, content, close, locInfo) {
/*jshint -W040 */
if (openRawBlock.sexpr.path.original !== close) {
var errorNode = {loc: openRawBlock.sexpr.loc};
throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode);
}
locInfo = this.locInfo(locInfo);
var program = new this.Program([content], null, {}, locInfo);
return new this.BlockStatement(
openRawBlock.sexpr, program, undefined,
{}, {}, {},
locInfo);
}
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.id.original !== close.path.original) {
var errorNode = {
firstLine: openBlock.sexpr.firstLine,
firstColumn: openBlock.sexpr.firstColumn
};
if (close && close.path && openBlock.sexpr.path.original !== close.path.original) {
var errorNode = {loc: openBlock.sexpr.loc};
throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode);
throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode);
}
program.blockParams = openBlock.blockParams;
// 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,
inverseStrip;
// 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: 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((firstInverse || program).statements)
};
if (openBlock.strip.right) {
omitRight(program.statements, null, true);
}
if (inverse) {
var inverseStrip = inverseAndProgram.strip;
if (inverseStrip.left) {
omitLeft(program.statements, null, true);
if (inverseAndProgram) {
if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip;
}
if (inverseStrip.right) {
omitRight(firstInverse.statements, null, true);
}
if (close.strip.left) {
omitLeft(lastInverse.statements, null, true);
}
// 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);
inverted = inverse;
inverse = program;
program = inverted;
}
}
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;
}
// 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));
}
+29 -20
View File
@@ -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');
+58 -68
View File
@@ -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.PathExpression = function(id) {
var path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path;
};
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.StringLiteral = function(string) {
return '"' + string.value + '"';
};
PrintVisitor.prototype.hash = function(hash) {
PrintVisitor.prototype.NumberLiteral = function(number) {
return "NUMBER{" + number.value + "}";
};
PrintVisitor.prototype.BooleanLiteral = function(bool) {
return "BOOLEAN{" + bool.value + "}";
};
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 );
for (var i=0, l=pairs.length; i<l; i++) {
joinedPairs.push(this.accept(pairs[i]));
}
return "HASH{" + joinedPairs.join(", ") + "}";
return 'HASH{' + joinedPairs.join(', ') + '}';
};
PrintVisitor.prototype.STRING = function(string) {
return '"' + string.string + '"';
PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value);
};
PrintVisitor.prototype.NUMBER = function(number) {
return "NUMBER{" + number.number + "}";
};
PrintVisitor.prototype.BOOLEAN = function(bool) {
return "BOOLEAN{" + bool.bool + "}";
};
PrintVisitor.prototype.ID = function(id) {
var path = id.parts.join("/");
if(id.parts.length > 1) {
return "PATH:" + path;
} else {
return "ID:" + path;
}
};
PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
return "PARTIAL:" + partialName.name;
};
PrintVisitor.prototype.DATA = function(data) {
return "@" + this.accept(data.id);
};
PrintVisitor.prototype.content = function(content) {
return this.pad("CONTENT[ '" + content.string + "' ]");
};
PrintVisitor.prototype.comment = function(comment) {
return this.pad("{{! '" + comment.comment + "' }}");
};
+31 -32
View File
@@ -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;
@@ -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;
+9 -6
View File
@@ -2,11 +2,14 @@
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(message, node) {
var line;
if (node && node.firstLine) {
line = node.firstLine;
var loc = node && node.loc,
line,
column;
if (loc) {
line = loc.start.line;
column = loc.start.column;
message += ' - ' + line + ':' + node.firstColumn;
message += ' - ' + line + ':' + column;
}
var tmp = Error.prototype.constructor.call(this, message);
@@ -16,9 +19,9 @@ function Exception(message, node) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
if (line) {
if (loc) {
this.lineNumber = line;
this.column = node.firstColumn;
this.column = column;
}
}
+13 -14
View File
@@ -35,36 +35,35 @@ export function template(templateSpec, env) {
// for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler);
var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) {
if (hash) {
context = Utils.extend({}, context, hash);
var invokePartialWrapper = function(partial, context, options) {
if (options.hash) {
context = Utils.extend({}, context, options.hash);
}
if (!partial) {
partial = partials[name];
partial = options.partials[options.name];
}
var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths);
var result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
var options = { helpers: helpers, partials: partials, data: data, depths: depths };
partials[name] = env.compile(partial, templateSpec.compilerOptions, env);
result = partials[name](context, options);
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
result = options.partials[options.name](context, options);
}
if (result != null) {
if (indent) {
if (options.indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}
lines[i] = indent + lines[i];
lines[i] = options.indent + lines[i];
}
result = lines.join('\n');
}
return result;
} else {
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
throw new Exception("The partial " + options.name + " could not be compiled when running in runtime-only mode");
}
};
@@ -172,11 +171,11 @@ export function program(container, i, fn, data, depths) {
return prog;
}
export function invokePartial(partial, name, context, helpers, partials, data, depths) {
var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths };
export function invokePartial(partial, context, options) {
options.partial = true;
if(partial === undefined) {
throw new Exception("The partial " + name + " could not be found");
throw new Exception("The partial " + options.name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
}
+106 -187
View File
@@ -5,69 +5,34 @@ describe('ast', function() {
}
var LOCATION_INFO = {
last_line: 0,
first_line: 0,
first_column: 0,
last_column: 0
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 1
}
};
function testLocationInfoStorage(node){
var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ],
property,
propertiesLen = properties.length,
i;
for (i = 0; i < propertiesLen; i++){
property = properties[0];
equals(node[property], 0);
}
equals(node.loc.start.line, 1);
equals(node.loc.start.column, 1);
equals(node.loc.end.line, 1);
equals(node.loc.end.column, 1);
}
describe('MustacheNode', function() {
function testEscape(open, expected) {
var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false);
equals(mustache.escaped, expected);
}
describe('MustacheStatement', function() {
it('should store args', function() {
var id = {isSimple: true},
hash = {},
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO);
equals(mustache.type, 'mustache');
equals(mustache.hash, hash);
mustache = new handlebarsEnv.AST.MustacheStatement({}, true, {}, LOCATION_INFO);
equals(mustache.type, 'MustacheStatement');
equals(mustache.escaped, true);
equals(mustache.id, id);
equals(mustache.params.length, 1);
equals(mustache.params[0], 'param1');
equals(!!mustache.isHelper, true);
testLocationInfoStorage(mustache);
});
it('should accept token for escape', function() {
testEscape('{{', true);
testEscape('{{~', true);
testEscape('{{#', true);
testEscape('{{~#', true);
testEscape('{{/', true);
testEscape('{{~/', true);
testEscape('{{^', true);
testEscape('{{~^', true);
testEscape('{', true);
testEscape('{', true);
testEscape('{{&', false);
testEscape('{{~&', false);
testEscape('{{{', false);
testEscape('{{~{', false);
});
it('should accept boolean for escape', function() {
testEscape(true, true);
testEscape({}, true);
testEscape(false, false);
testEscape(undefined, false);
});
});
describe('BlockNode', function() {
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
handlebarsEnv.parse("\n {{#foo}}{{/bar}}");
@@ -75,175 +40,129 @@ describe('ast', function() {
});
it('stores location info', function(){
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
var block = new handlebarsEnv.AST.BlockNode(mustacheNode,
{statements: [], strip: {}}, {statements: [], strip: {}},
{
strip: {},
path: {original: 'foo'}
},
LOCATION_INFO);
var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {});
var block = new handlebarsEnv.AST.BlockStatement(
mustacheNode,
{body: []},
{body: []},
{},
{},
{},
LOCATION_INFO);
testLocationInfoStorage(block);
});
});
describe('IdNode', function() {
it('should throw on invalid path', function() {
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '..'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo.. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '.'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: 'this'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foothis - 1:1");
});
describe('PathExpression', function() {
it('stores location info', function(){
var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO);
var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO);
testLocationInfoStorage(idNode);
});
});
describe("HashNode", function(){
describe('Hash', function(){
it('stores location info', function(){
var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO);
var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO);
testLocationInfoStorage(hash);
});
});
describe("ContentNode", function(){
describe('ContentStatement', function(){
it('stores location info', function(){
var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO);
var content = new handlebarsEnv.AST.ContentStatement("HI", LOCATION_INFO);
testLocationInfoStorage(content);
});
});
describe("CommentNode", function(){
describe('CommentStatement', function(){
it('stores location info', function(){
var comment = new handlebarsEnv.AST.CommentNode("HI", {}, LOCATION_INFO);
var comment = new handlebarsEnv.AST.CommentStatement("HI", {}, LOCATION_INFO);
testLocationInfoStorage(comment);
});
});
describe("NumberNode", function(){
describe('NumberLiteral', function(){
it('stores location info', function(){
var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO);
var integer = new handlebarsEnv.AST.NumberLiteral("6", LOCATION_INFO);
testLocationInfoStorage(integer);
});
});
describe("StringNode", function(){
describe('StringLiteral', function(){
it('stores location info', function(){
var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO);
var string = new handlebarsEnv.AST.StringLiteral("6", LOCATION_INFO);
testLocationInfoStorage(string);
});
});
describe("BooleanNode", function(){
describe('BooleanLiteral', function(){
it('stores location info', function(){
var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO);
var bool = new handlebarsEnv.AST.BooleanLiteral("true", LOCATION_INFO);
testLocationInfoStorage(bool);
});
});
describe("DataNode", function(){
describe('PartialStatement', function(){
it('stores location info', function(){
var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO);
testLocationInfoStorage(data);
});
});
describe("PartialNameNode", function(){
it('stores location info', function(){
var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO);
testLocationInfoStorage(pnn);
});
});
describe("PartialNode", function(){
it('stores location info', function(){
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO);
var pn = new handlebarsEnv.AST.PartialStatement('so_partial', {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe('ProgramNode', function(){
describe('Program', function(){
it('storing location info', function(){
var pn = new handlebarsEnv.AST.ProgramNode([], null, {}, LOCATION_INFO);
var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe("Line Numbers", function(){
var ast, statements;
var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){
equals(node.firstLine, firstLine);
equals(node.lastLine, lastLine);
equals(node.firstColumn, firstColumn);
equals(node.lastColumn, lastColumn);
equals(node.loc.start.line, firstLine);
equals(node.loc.start.column, firstColumn);
equals(node.loc.end.line, lastLine);
equals(node.loc.end.column, lastColumn);
}
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" +
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}");
statements = ast.statements;
body = ast.body;
it('gets ContentNode line numbers', function(){
var contentNode = statements[0];
var contentNode = body[0];
testColumns(contentNode, 1, 1, 0, 7);
});
it('gets MustacheNode line numbers', function(){
var mustacheNode = statements[1];
it('gets MustacheStatement line numbers', function(){
var mustacheNode = body[1];
testColumns(mustacheNode, 1, 1, 7, 21);
});
it('gets line numbers correct when newlines appear', function(){
testColumns(statements[2], 1, 2, 21, 8);
testColumns(body[2], 1, 2, 21, 8);
});
it('gets MustacheNode line numbers correct across newlines', function(){
var secondMustacheNode = statements[3];
testColumns(secondMustacheNode, 2, 2, 8, 22);
it('gets MustacheStatement line numbers correct across newlines', function(){
var secondMustacheStatement = body[3];
testColumns(secondMustacheStatement, 2, 2, 8, 22);
});
it('gets the block helper information correct', function(){
var blockHelperNode = statements[5];
var blockHelperNode = body[5];
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('correctly records the line numbers the program of a block helper', function(){
var blockHelperNode = statements[5],
var blockHelperNode = body[5],
program = blockHelperNode.program;
testColumns(program, 3, 5, 8, 5);
});
it('correctly records the line numbers of an inverse of a block helper', function(){
var blockHelperNode = statements[5],
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 7, 5, 0);
@@ -254,118 +173,118 @@ describe('ast', function() {
describe('mustache', function() {
it('does not mark mustaches as standalone', function() {
var ast = Handlebars.parse(' {{comment}} ');
equals(!!ast.statements[0].string, true);
equals(!!ast.statements[2].string, true);
equals(!!ast.body[0].value, true);
equals(!!ast.body[2].value, true);
});
});
describe('blocks', function() {
it('marks block mustaches as standalone', function() {
var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.statements[1];
block = ast.body[1];
equals(ast.statements[0].string, '');
equals(ast.body[0].value, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.statements[2].string, '');
equals(ast.body[2].value, '');
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
block = ast.statements[0];
block = ast.body[0];
equals(block.program.statements[0].string, 'foo\n');
equals(block.program.body[0].value, 'foo\n');
});
it('marks mustaches with children as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
block = ast.statements[0];
block = ast.body[0];
equals(block.program.statements[0].string, '');
equals(block.program.statements[1].id.original, 'foo');
equals(block.program.statements[2].string, '\n');
equals(block.program.body[0].value, '');
equals(block.program.body[1].sexpr.path.original, 'foo');
equals(block.program.body[2].value, '\n');
});
it('marks nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[1];
body = ast.body[0].program.body,
block = body[1];
equals(statements[0].string, '');
equals(body[0].value, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(statements[0].string, '');
equals(body[0].value, '');
});
it('does not mark nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[1];
body = ast.body[0].program.body,
block = body[1];
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
});
it('does not mark nested initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[0];
body = ast.body[0].program.body,
block = body[0];
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
});
it('marks column 0 block mustaches as standalone', function() {
var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.statements[1];
block = ast.body[1];
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.statements[2].string, '');
equals(ast.body[2].value, '');
});
});
describe('partials', function() {
it('marks partial as standalone', function() {
var ast = Handlebars.parse('{{> partial }} ');
equals(ast.statements[1].string, '');
equals(ast.body[1].value, '');
});
it('marks indented partial as standalone', function() {
var ast = Handlebars.parse(' {{> partial }} ');
equals(ast.statements[0].string, '');
equals(ast.statements[1].indent, ' ');
equals(ast.statements[2].string, '');
equals(ast.body[0].value, '');
equals(ast.body[1].indent, ' ');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{> partial }}');
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{> partial }}a');
equals(ast.statements[1].omit, undefined);
equals(ast.body[1].omit, undefined);
});
});
describe('comments', function() {
it('marks comment as standalone', function() {
var ast = Handlebars.parse('{{! comment }} ');
equals(ast.statements[1].string, '');
equals(ast.body[1].value, '');
});
it('marks indented comment as standalone', function() {
var ast = Handlebars.parse(' {{! comment }} ');
equals(ast.statements[0].string, '');
equals(ast.statements[2].string, '');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{! comment }}');
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{! comment }}a');
equals(ast.statements[1].omit, undefined);
equals(ast.body[1].omit, undefined);
});
});
});
+2 -2
View File
@@ -41,7 +41,7 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], null, {}))(), 'Hello');
equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")], null, {}))(), 'Hello');
});
it("can pass through an empty string", function() {
@@ -60,7 +60,7 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), null, {})), true);
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")]), null, {})), true);
});
it("can pass through an empty string", function() {
+59 -46
View File
@@ -10,19 +10,19 @@ describe('parser', function() {
}
it('parses simple mustaches', function() {
equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n");
equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n");
equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n");
equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n");
equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n");
equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n");
equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n");
equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n");
equals(ast_for('{{foo-}}'), "{{ PATH:foo- [] }}\n");
equals(ast_for('{{foo:}}'), "{{ PATH:foo: [] }}\n");
});
it('parses simple mustaches with data', function() {
equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n");
equals(ast_for("{{@foo}}"), "{{ @PATH:foo [] }}\n");
});
it('parses simple mustaches with data paths', function() {
equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n");
equals(ast_for("{{@../foo}}"), "{{ @PATH:foo [] }}\n");
});
it('parses mustaches with paths', function() {
@@ -30,70 +30,72 @@ describe('parser', function() {
});
it('parses mustaches with this/foo', function() {
equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n");
equals(ast_for("{{this/foo}}"), "{{ PATH:foo [] }}\n");
});
it('parses mustaches with - in a path', function() {
equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n");
equals(ast_for("{{foo-bar}}"), "{{ PATH:foo-bar [] }}\n");
});
it('parses mustaches with parameters', function() {
equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n");
equals(ast_for("{{foo bar}}"), "{{ PATH:foo [PATH:bar] }}\n");
});
it('parses mustaches with string parameters', function() {
equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n');
equals(ast_for("{{foo bar \"baz\" }}"), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
});
it('parses mustaches with NUMBER parameters', function() {
equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n");
equals(ast_for("{{foo 1}}"), "{{ PATH:foo [NUMBER{1}] }}\n");
});
it('parses mustaches with BOOLEAN parameters', function() {
equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n");
equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n");
equals(ast_for("{{foo true}}"), "{{ PATH:foo [BOOLEAN{true}] }}\n");
equals(ast_for("{{foo false}}"), "{{ PATH:foo [BOOLEAN{false}] }}\n");
});
it('parses mutaches with DATA parameters', function() {
equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n");
equals(ast_for("{{foo @bar}}"), "{{ PATH:foo [@PATH:bar] }}\n");
});
it('parses mustaches with hash arguments', function() {
equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n");
equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=NUMBER{1}} }}\n");
equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
equals(ast_for("{{foo bar=baz}}"), "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n");
equals(ast_for("{{foo bar=1}}"), "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n");
equals(ast_for("{{foo bar=true}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n");
equals(ast_for("{{foo bar=false}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n");
equals(ast_for("{{foo bar=@baz}}"), "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n");
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n");
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(ast_for("{{foo bat='bam'}}"), '{{ ID:foo [] HASH{bat="bam"} }}\n');
equals(ast_for("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
});
it('parses contents followed by a mustache', function() {
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n");
});
it('parses a partial', function() {
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n");
equals(ast_for("{{> 'foo' }}"), "{{> PARTIAL:foo }}\n");
equals(ast_for("{{> 1 }}"), "{{> PARTIAL:1 }}\n");
});
it('parses a partial with context', function() {
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n");
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo PATH:bar }}\n");
});
it('parses a partial with hash', function() {
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID:bat} }}\n");
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n");
});
it('parses a partial with context and hash', function() {
equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo ID:bar HASH{bat=ID:baz} }}\n");
equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n");
});
it('parses a partial with a complex name', function() {
@@ -109,47 +111,47 @@ describe('parser', function() {
});
it('parses an inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses an inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses multiple inverse sections', function() {
equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n ID:if [ID:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses empty blocks', function() {
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n");
});
it('parses empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n");
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n");
});
it('parses empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n");
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses empty blocks with non-empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses a standalone inverse section', function() {
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n CONTENT[ 'bar' ]\n");
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n");
});
it('throws on old inverse section', function() {
shouldThrow(function() {
@@ -158,13 +160,12 @@ describe('parser', function() {
});
it('parses block with block params', function() {
equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('parses inverse block with block params', function() {
equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it("raises if there's a Parse error", function() {
shouldThrow(function() {
ast_for("foo{{^}}bar");
@@ -184,6 +185,18 @@ describe('parser', function() {
}, Error, /goodbyes doesn't match hellos/);
});
it('should handle invalid paths', function() {
shouldThrow(function() {
ast_for("{{foo/../bar}}");
}, Error, /Invalid path: foo\/\.\. - 1:2/);
shouldThrow(function() {
ast_for("{{foo/./bar}}");
}, Error, /Invalid path: foo\/\. - 1:2/);
shouldThrow(function() {
ast_for("{{foo/this/bar}}");
}, Error, /Invalid path: foo\/this - 1:2/);
});
it('knows how to report the correct line number in errors', function() {
shouldThrow(function() {
ast_for("hello\nmy\n{{foo}");
@@ -201,7 +214,7 @@ describe('parser', function() {
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
equals(ast_for(new Handlebars.AST.ProgramNode([new Handlebars.AST.ContentNode("Hello")], null)), "CONTENT[ \'Hello\' ]\n");
equals(ast_for(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement("Hello")], null)), "CONTENT[ \'Hello\' ]\n");
});
});
});
+6
View File
@@ -23,6 +23,12 @@ describe('partials', function() {
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty");
});
it('partials with duplicate parameters', function() {
shouldThrow(function() {
CompilerContext.compile('Dudes: {{>dude dudes foo bar=baz}}');
}, Error, 'Unsupported number of partial arguments: 2 - 1:7');
});
it("partials with parameters", function() {
var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}";
var partial = "{{others.foo}}{{name}} ({{url}}) ";
+16 -15
View File
@@ -1,3 +1,4 @@
/*global CompilerContext */
describe('string params mode', function() {
it("arguments to helpers can be retrieved from options hash in string form", function() {
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
@@ -56,9 +57,9 @@ describe('string params mode', function() {
var helpers = {
tomdale: function(desire, noun, trueBool, falseBool, options) {
equal(options.types[0], 'STRING', "the string type is passed");
equal(options.types[1], 'ID', "the expression type is passed");
equal(options.types[2], 'BOOLEAN', "the expression type is passed");
equal(options.types[0], 'StringLiteral', "the string type is passed");
equal(options.types[1], 'PathExpression', "the expression type is passed");
equal(options.types[2], 'BooleanLiteral', "the expression type is passed");
equal(desire, "need", "the string form is passed for strings");
equal(noun, "dad.joke", "the string form is passed for expressions");
equal(trueBool, true, "raw booleans are passed through");
@@ -76,21 +77,21 @@ describe('string params mode', function() {
var helpers = {
tomdale: function(exclamation, options) {
equal(exclamation, "he.says");
equal(options.types[0], "ID");
equal(exclamation, 'he.says');
equal(options.types[0], 'PathExpression');
equal(options.hashTypes.desire, "STRING");
equal(options.hashTypes.noun, "ID");
equal(options.hashTypes.bool, "BOOLEAN");
equal(options.hash.desire, "need");
equal(options.hash.noun, "dad.joke");
equal(options.hashTypes.desire, 'StringLiteral');
equal(options.hashTypes.noun, 'PathExpression');
equal(options.hashTypes.bool, 'BooleanLiteral');
equal(options.hash.desire, 'need');
equal(options.hash.noun, 'dad.joke');
equal(options.hash.bool, true);
return "Helper called";
return 'Helper called';
}
};
var result = template({}, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("hash parameters get context information", function() {
@@ -101,7 +102,7 @@ describe('string params mode', function() {
var helpers = {
tomdale: function(exclamation, options) {
equal(exclamation, "he.says");
equal(options.types[0], "ID");
equal(options.types[0], 'PathExpression');
equal(options.contexts.length, 1);
equal(options.hashContexts.noun, context);
@@ -164,8 +165,8 @@ describe('string params mode', function() {
var helpers = {
foo: function(bar, options) {
equal(bar, 'bar');
equal(options.types[0], 'DATA');
equal(bar, '@bar');
equal(options.types[0], 'PathExpression');
return 'Foo!';
}
};
+7 -7
View File
@@ -1,4 +1,4 @@
/*global CompilerContext, shouldCompileTo */
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
describe('subexpressions', function() {
it("arg-less helper", function() {
var string = "{{foo (bar)}}!";
@@ -135,7 +135,7 @@ describe('subexpressions', function() {
t: function(defaultString) {
return new Handlebars.SafeString(defaultString);
}
}
};
shouldCompileTo(string, [{}, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
@@ -159,7 +159,7 @@ describe('subexpressions', function() {
t: function(defaultString) {
return new Handlebars.SafeString(defaultString);
}
}
};
shouldCompileTo(string, [context, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
@@ -170,14 +170,14 @@ describe('subexpressions', function() {
snog: function(a, b, options) {
equals(a, 'foo');
equals(options.types.length, 2, "string params for outer helper processed correctly");
equals(options.types[0], 'sexpr', "string params for outer helper processed correctly");
equals(options.types[1], 'ID', "string params for outer helper processed correctly");
equals(options.types[0], 'SubExpression', "string params for outer helper processed correctly");
equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly");
return a + b;
},
blorg: function(a, options) {
equals(options.types.length, 1, "string params for inner helper processed correctly");
equals(options.types[0], 'ID', "string params for inner helper processed correctly");
equals(options.types[0], 'PathExpression', "string params for inner helper processed correctly");
return a;
}
};
@@ -196,7 +196,7 @@ describe('subexpressions', function() {
var helpers = {
blog: function(options) {
equals(options.hashTypes.fun, 'sexpr');
equals(options.hashTypes.fun, 'SubExpression');
return "val is " + options.hash.fun;
},
bork: function() {
+13 -17
View File
@@ -14,33 +14,29 @@ describe('Visitor', function() {
// Simply run the thing and make sure it does not fail and that all of the
// stub methods are executed
var visitor = new Handlebars.Visitor();
visitor.accept(Handlebars.parse('{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'));
visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'));
});
it('should traverse to stubs', function() {
var visitor = new Handlebars.Visitor();
visitor.PARTIAL_NAME = function(partialName) {
equal(partialName.name, 'bar');
visitor.StringLiteral = function(string) {
equal(string.value, '2');
};
visitor.STRING = function(string) {
equal(string.string, '2');
visitor.NumberLiteral = function(number) {
equal(number.value, 1);
};
visitor.NUMBER = function(number) {
equal(number.stringModeValue, 1);
visitor.BooleanLiteral = function(bool) {
equal(bool.value, true);
};
visitor.BOOLEAN = function(bool) {
equal(bool.stringModeValue, true);
visitor.PathExpression = function(id) {
equal(/foo\.bar$/.test(id.original), true);
};
visitor.ID = function(id) {
equal(id.original, 'foo.bar');
visitor.ContentStatement = function(content) {
equal(content.value, ' ');
};
visitor.content = function(content) {
equal(content.string, ' ');
};
visitor.comment = function(comment) {
equal(comment.comment, 'comment');
visitor.CommentStatement = function(comment) {
equal(comment.value, 'comment');
};
visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}'));
+24 -26
View File
@@ -5,11 +5,11 @@
%%
root
: program EOF { yy.prepareProgram($1.statements, true); return $1; }
: program EOF { return $1; }
;
program
: statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, @$)
: statement* -> new yy.Program($1, null, {}, yy.locInfo(@$))
;
statement
@@ -18,11 +18,11 @@ statement
| rawBlock -> $1
| partial -> $1
| content -> $1
| COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), @$)
| COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$))
;
content
: CONTENT -> new yy.ContentNode($1, @$)
: CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$))
;
rawBlock
@@ -47,7 +47,7 @@ openInverse
;
openInverseChain
: OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
: OPEN_INVERSE_CHAIN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$)
;
inverseAndProgram
@@ -57,9 +57,8 @@ inverseAndProgram
inverseChain
: openInverseChain program inverseChain? {
var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$),
program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, @$);
program.inverse = inverse;
program = new yy.Program([inverse], null, {}, yy.locInfo(@$));
program.chained = true;
$$ = { strip: $1.strip, program: program, chain: true };
}
@@ -73,53 +72,52 @@ closeBlock
mustache
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
// This also allows for handler unification as all mustache node instances can utilize the same handler
: OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
: OPEN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$)
;
partial
: OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$)
| OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$)
: OPEN_PARTIAL sexpr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$))
;
sexpr
: path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$)
| dataName -> new yy.SexprNode([$1], null, @$)
: helperName param* hash? -> new yy.SubExpression($1, $2, $3, yy.locInfo(@$))
| dataName -> new yy.SubExpression($1, null, null, yy.locInfo(@$))
;
param
: path -> $1
| STRING -> new yy.StringNode($1, @$)
| NUMBER -> new yy.NumberNode($1, @$)
| BOOLEAN -> new yy.BooleanNode($1, @$)
| STRING -> new yy.StringLiteral($1, yy.locInfo(@$))
| NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$))
| BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$))
| dataName -> $1
| OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;}
| OPEN_SEXPR sexpr CLOSE_SEXPR -> $2
;
hash
: hashSegment+ -> new yy.HashNode($1, @$)
: hashSegment+ -> new yy.Hash($1, yy.locInfo(@$))
;
hashSegment
: ID EQUALS param -> [$1, $3]
: ID EQUALS param -> new yy.HashPair($1, $3, yy.locInfo(@$))
;
blockParams
: OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> $2
;
partialName
: path -> new yy.PartialNameNode($1, @$)
| STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$)
| NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$))
helperName
: path -> $1
| STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$)
| NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$))
;
dataName
: DATA path -> new yy.DataNode($2, @$)
: DATA pathSegments -> yy.preparePath(true, $2, @$)
;
path
: pathSegments -> new yy.IdNode($1, @$)
: pathSegments -> yy.preparePath(false, $1, @$)
;
pathSegments