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