@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 + "' }}");
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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!';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user