diff --git a/.jshintrc b/.jshintrc index bb29c76e8..313015ee7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -45,6 +45,7 @@ "plusplus": false, "regexp": false, "undef": true, + "unused": true, "sub": true, "strict": false, "white": false diff --git a/.npmignore b/.npmignore index 366b45408..6b8c8bfce 100644 --- a/.npmignore +++ b/.npmignore @@ -13,6 +13,7 @@ Gruntfile.js bench/* configurations/* components/* +coverage/* dist/cdnjs/* dist/components/* spec/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6db72a75..b84fdb005 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,8 @@ To build Handlebars.js you'll need a few things installed. * Node.js * [Grunt](http://gruntjs.com/getting-started) +Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`. + Project dependencies may be installed via `npm install`. To build Handlebars.js from scratch, you'll want to run `grunt` @@ -75,4 +77,4 @@ After this point the handlebars site needs to be updated to point to the new ver [generator-release]: https://github.com/walmartlabs/generator-release [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master [issue]: https://github.com/wycats/handlebars.js/issues/new -[jsfiddle]: http://jsfiddle.net/9D88g/25/ +[jsfiddle]: http://jsfiddle.net/9D88g/26/ diff --git a/FAQ.md b/FAQ.md index 0bd0997d6..108e839af 100644 --- a/FAQ.md +++ b/FAQ.md @@ -57,4 +57,4 @@ The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility. - If not using ES6 transpilers or accessing submodules in the build the former option should be sufficent for most use cases. + If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases. diff --git a/README.markdown b/README.markdown index 77552860d..5bfa47c72 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,8 @@ Installing ---------- Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version). +For web browsers, a free CDN is available at [jsDelivr](http://www.jsdelivr.com/#!handlebarsjs). Advanced usage, such as [version aliasing & concocting](https://github.com/jsdelivr/jsdelivr#usage), is available. + Alternatively, if you prefer having the latest version of handlebars from the 'master' branch, passing builds of the 'master' branch are automatically published to S3. You may download the latest passing master build by grabbing @@ -108,7 +110,7 @@ templates easier and also changes a tiny detail of how partials work. ### Paths Handlebars.js supports an extended expression syntax that we call paths. -Paths are made up of typical expressions and . characters. Expressions +Paths are made up of typical expressions and `.` characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendants and ancestors of the current context. @@ -132,7 +134,7 @@ into the person object you could still display the company's name with an expression like `{{../company.name}}`, so: ``` -{{#with person}}{{name}} - {{../company.name}}{{/person}} +{{#with person}}{{name}} - {{../company.name}}{{/with}} ``` would render: @@ -195,7 +197,7 @@ template(data); ``` Whenever the block helper is called it is given one or more parameters, -any arguments that are passed in the helper in the call and an `options` +any arguments that are passed into the helper in the call, and an `options` object containing the `fn` function which executes the block's child. The block's current context may be accessed through `this`. diff --git a/bin/handlebars b/bin/handlebars index 4ea329661..79dfa6fb2 100755 --- a/bin/handlebars +++ b/bin/handlebars @@ -1,12 +1,16 @@ #!/usr/bin/env node var optimist = require('optimist') - .usage('Precompile handlebar templates.\nUsage: $0 template...', { + .usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...', { 'f': { 'type': 'string', 'description': 'Output File', 'alias': 'output' }, + 'map': { + 'type': 'string', + 'description': 'Source Map File' + }, 'a': { 'type': 'boolean', 'description': 'Exports amd style (require.js)', @@ -80,6 +84,11 @@ var optimist = require('optimist') 'type': 'boolean', 'description': 'Prints the current compiler version', 'alias': 'version' + }, + + 'help': { + 'type': 'boolean', + 'description': 'Outputs this message' } }) @@ -89,7 +98,14 @@ var optimist = require('optimist') } }); + var argv = optimist.argv; argv.templates = argv._; delete argv._; + +if (argv.help || !argv.templates.length) { + optimist.showHelp(); + return; +} + return require('../lib/precompiler').cli(argv); diff --git a/components/bower.json b/components/bower.json index ec9280310..0e1f0a12b 100644 --- a/components/bower.json +++ b/components/bower.json @@ -1,6 +1,6 @@ { "name": "handlebars", - "version": "2.0.0", + "version": "3.0.0", "main": "handlebars.js", "dependencies": {} } diff --git a/components/handlebars-source.gemspec b/components/handlebars-source.gemspec index f9e1a4bc4..7b5d59566 100644 --- a/components/handlebars-source.gemspec +++ b/components/handlebars-source.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |gem| gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.} gem.summary = %q{Handlebars.js source code wrapper} gem.homepage = "https://github.com/wycats/handlebars.js/" - gem.version = package["version"].sub! "-", "." + gem.version = package["version"].sub "-", "." gem.license = "MIT" gem.files = [ diff --git a/components/handlebars.js.nuspec b/components/handlebars.js.nuspec index 671a32fba..8af75b07c 100644 --- a/components/handlebars.js.nuspec +++ b/components/handlebars.js.nuspec @@ -2,7 +2,7 @@ handlebars.js - 2.0.0 + 3.0.0 handlebars.js Authors https://github.com/wycats/handlebars.js/blob/master/LICENSE https://github.com/wycats/handlebars.js/ diff --git a/docs/compiler-api.md b/docs/compiler-api.md new file mode 100644 index 000000000..40ded8fd2 --- /dev/null +++ b/docs/compiler-api.md @@ -0,0 +1,268 @@ +# 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"; + + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + escaped: boolean; + strip: StripFlags | null; +} + +interface BlockStatement <: Statement { + type: "BlockStatement"; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + inverse: Program | null; + + openStrip: StripFlags | null; + inverseStrip: StripFlags | null; + closeStrip: StripFlags | null; +} + +interface PartialStatement <: Statement { + type: "PartialStatement"; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; + + indent: string; + strip: StripFlags | null; +} +``` + +`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial. + + +```java +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; +} +``` + +##### 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.name.original}); + + Visitor.prototype.PartialStatement.call(this, partial); +}; + +var scanner = new ImportScanner(); +scanner.accept(ast); +``` + +The current node's ancestors will be maintained in the `parents` array, with the most recent parent listed first. + +The visitor may also be configured to operate in mutation mode by setting the `mutation` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node in tact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish. + +Implementors that may need to support mutation mode are encouraged to utilize the `acceptKey`, `acceptRequired` and `acceptArray` helpers which provide the conditional overwrite behavior as well as implement sanity checks where pertinent. + +## JavaScript Compiler + +The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: + +- `nameLookup(parent, name, type)` + Used to generate the code to resolve a give path component. + + - `parent` is the existing code in the path resolution + - `name` is the current path component + - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`. + + Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases. + +- `depthedLookup(name)` + Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode. + +- `compilerInfo()` + Allows for custom compiler flags used in the runtime version checking logic. + +- `appendToBuffer(source, location, explicit)` + Allows for code buffer emitting code. Defaults behavior is string concatenation. + + - `source` is the source code whose result is to be appending + - `location` is the location of the source in the source map. + - `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise. + +- `initializeBuffer()` + Allows for buffers other than the default string buffer to be used. Generally needs to be paired with a custom `appendToBuffer` implementation. + +```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.js b/lib/handlebars.js index 039ab3e17..81b7cfbd2 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -30,6 +30,17 @@ var create = function() { Handlebars = create(); Handlebars.create = create; +/*jshint -W040 */ +/* istanbul ignore next */ +var root = typeof global !== 'undefined' ? global : window, + $Handlebars = root.Handlebars; +/* istanbul ignore next */ +Handlebars.noConflict = function() { + if (root.Handlebars === Handlebars) { + root.Handlebars = $Handlebars; + } +}; + Handlebars['default'] = Handlebars; export default Handlebars; diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js index bc07714f2..ef00e2856 100644 --- a/lib/handlebars.runtime.js +++ b/lib/handlebars.runtime.js @@ -29,6 +29,17 @@ var create = function() { var Handlebars = create(); Handlebars.create = create; +/*jshint -W040 */ +/* istanbul ignore next */ +var root = typeof global !== 'undefined' ? global : window, + $Handlebars = root.Handlebars; +/* istanbul ignore next */ +Handlebars.noConflict = function() { + if (root.Handlebars === Handlebars) { + root.Handlebars = $Handlebars; + } +}; + Handlebars['default'] = Handlebars; export default Handlebars; diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 76f53e276..67f624cad 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,7 +1,7 @@ module Utils from "./utils"; import Exception from "./exception"; -export var VERSION = "2.0.0"; +export var VERSION = "3.0.0"; export var COMPILER_REVISION = 6; export var REVISION_CHANGES = { @@ -47,6 +47,9 @@ HandlebarsEnvironment.prototype = { if (toString.call(name) === objectType) { Utils.extend(this.partials, name); } else { + if (typeof partial === 'undefined') { + throw new Exception('Attempting to register a partial as undefined'); + } this.partials[name] = partial; } }, @@ -114,36 +117,47 @@ function registerDefaultHelpers(instance) { data = createFrame(options.data); } + function execIteration(key, i, last) { + if (data) { + data.key = key; + data.index = i; + data.first = i === 0; + data.last = !!last; + + if (contextPath) { + data.contextPath = contextPath + key; + } + } + + ret = ret + fn(context[key], { + data: data, + blockParams: Utils.blockParams([context[key], key], [contextPath + key, null]) + }); + } + if(context && typeof context === 'object') { if (isArray(context)) { for(var j = context.length; i 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { - depth++; - depthString += '../'; - } else { - this.isScoped = true; - } - } else { - dig.push(part); - } - } + PathExpression: function(data, depth, parts, original, locInfo) { + this.loc = locInfo; + this.type = 'PathExpression'; + this.data = data; this.original = original; - this.parts = dig; - this.string = dig.join('.'); + this.parts = parts; this.depth = depth; - this.idName = depthString + this.string; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; }, - PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, - - DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; + StringLiteral: function(string, locInfo) { + this.loc = locInfo; + this.type = 'StringLiteral'; + this.original = + this.value = string; }, - StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "STRING"; + NumberLiteral: function(number, locInfo) { + this.loc = locInfo; + this.type = 'NumberLiteral'; this.original = - this.string = - this.stringModeValue = string; + this.value = Number(number); }, - NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "NUMBER"; + BooleanLiteral: function(bool, locInfo) { + this.loc = locInfo; + this.type = 'BooleanLiteral'; this.original = - this.number = number; - this.stringModeValue = Number(number); + this.value = bool === 'true'; }, - BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "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; }, - CommentNode: function(comment, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "comment"; - this.comment = comment; + // Public API used to evaluate derived attributes regarding AST nodes + helpers: { + // a mustache is definitely a helper if: + // * it is an eligible helper, and + // * it has at least one parameter or hash segment + // TODO: Make these public utility methods + helperExpression: function(node) { + return !!(node.type === 'SubExpression' || node.params.length || node.hash); + }, + + scopedId: function(path) { + return (/^\.|this\b/).test(path.original); + }, - this.strip = { - inlineStandalone: true - }; + // an ID is simple if it only has one part, and that part is not + // `..` or `this`. + simpleId: function(path) { + return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth; + } } }; // Must be exported as an object rather than the root of the module as the jison lexer -// most modify the object to operate properly. +// must modify the object to operate properly. export default AST; diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 13784637e..167b84438 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) { - // Just return if an already-compile AST was passed in. - if (input.constructor === AST.ProgramNode) { return input; } +export function parse(input, options) { + // Just return if an already-compiled AST was passed in. + 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 new file mode 100644 index 000000000..0fddb7c65 --- /dev/null +++ b/lib/handlebars/compiler/code-gen.js @@ -0,0 +1,154 @@ +import {isArray} from "../utils"; + +try { + var SourceMap = require('source-map'), + SourceNode = SourceMap.SourceNode; +} catch (err) { + /* istanbul ignore next: tested but not covered in istanbul due to dist build */ + SourceNode = function(line, column, srcFile, chunks) { + this.src = ''; + if (chunks) { + this.add(chunks); + } + }; + /* istanbul ignore next */ + SourceNode.prototype = { + add: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src += chunks; + }, + prepend: function(chunks) { + if (isArray(chunks)) { + chunks = chunks.join(''); + } + this.src = chunks + this.src; + }, + toStringWithSourceMap: function() { + return {code: this.toString()}; + }, + toString: function() { + return this.src; + } + }; +} + + +function castChunk(chunk, codeGen, loc) { + if (isArray(chunk)) { + var ret = []; + + for (var i = 0, len = chunk.length; i < len; i++) { + ret.push(codeGen.wrap(chunk[i], loc)); + } + return ret; + } else if (typeof chunk === 'boolean' || typeof chunk === 'number') { + // Handle primitives that the SourceNode will throw up on + return chunk+''; + } + return chunk; +} + + +function CodeGen(srcFile) { + this.srcFile = srcFile; + this.source = []; +} + +CodeGen.prototype = { + prepend: function(source, loc) { + this.source.unshift(this.wrap(source, loc)); + }, + push: function(source, loc) { + this.source.push(this.wrap(source, loc)); + }, + + merge: function() { + var source = this.empty(); + this.each(function(line) { + source.add([' ', line, '\n']); + }); + return source; + }, + + each: function(iter) { + for (var i = 0, len = this.source.length; i < len; i++) { + iter(this.source[i]); + } + }, + + empty: function(loc) { + 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 || {start:{}}; + chunk = castChunk(chunk, this, loc); + + return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); + }, + + functionCall: function(fn, type, params) { + params = this.generateList(params); + return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); + }, + + quotedString: function(str) { + return '"' + (str + '') + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, + + objectLiteral: function(obj) { + var pairs = []; + + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + var value = castChunk(obj[key], this); + if (value !== 'undefined') { + pairs.push([this.quotedString(key), ':', value]); + } + } + } + + var ret = this.generateList(pairs); + ret.prepend('{'); + ret.add('}'); + return ret; + }, + + + generateList: function(entries, loc) { + var ret = this.empty(loc); + + for (var i = 0, len = entries.length; i < len; i++) { + if (i) { + ret.add(','); + } + + ret.add(castChunk(entries[i], this, loc)); + } + + return ret; + }, + + generateArray: function(entries, loc) { + var ret = this.generateList(entries, loc); + ret.prepend('['); + ret.add(']'); + + return ret; + } +}; + +export default CodeGen; + diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1aba34b4e..21de99ca4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,8 +1,10 @@ import Exception from "../exception"; -import {isArray} from "../utils"; +import {isArray, indexOf} from "../utils"; +import AST from "./ast"; var slice = [].slice; + export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a @@ -42,16 +44,18 @@ Compiler.prototype = { guid: 0, compile: function(program, options) { + this.sourceNode = []; this.opcodes = []; this.children = []; - this.depths = {list: []}; this.options = options; this.stringParams = options.stringParams; this.trackIds = options.trackIds; + options.blockParams = options.blockParams || []; + // These changes will propagate to the other compiler components - var knownHelpers = this.options.knownHelpers; - this.options.knownHelpers = { + var knownHelpers = options.knownHelpers; + options.knownHelpers = { 'helperMissing': true, 'blockHelperMissing': true, 'each': true, @@ -63,79 +67,72 @@ Compiler.prototype = { }; if (knownHelpers) { for (var name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; + options.knownHelpers[name] = knownHelpers[name]; } } return this.accept(program); }, - accept: function(node) { - return this[node.type](node); - }, - - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i 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', 0); - this.opcode('pushContext'); + var partialName = partial.name.original, + isDynamic = partial.name.type === 'SubExpression'; + if (isDynamic) { + this.accept(partial.name); } - this.opcode('invokePartial', partialName.name, partial.indent || ''); - this.opcode('append'); - }, + this.setupFullMustacheParams(partial, undefined, undefined, true); - content: function(content) { - if (content.string) { - this.opcode('appendContent', content.string); + var indent = partial.indent || ''; + if (this.options.preventIndent && indent) { + this.opcode('appendContent', indent); + indent = ''; } + + this.opcode('invokePartial', isDynamic, partialName, indent); + this.opcode('append'); }, - mustache: function(mustache) { - this.sexpr(mustache.sexpr); + MustacheStatement: function(mustache) { + this.SubExpression(mustache); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); @@ -199,122 +183,143 @@ Compiler.prototype = { } }, + ContentStatement: function(content) { + if (content.value) { + this.opcode('appendContent', content.value); + } + }, + + CommentStatement: function() {}, + + SubExpression: function(sexpr) { + transformLiteralToPath(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', id.depth); + this.opcode('getContext', path.depth); this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); - this.ID(id); + this.accept(path); this.opcode('invokeAmbiguous', 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', id.depth); - this.opcode('pushContext'); - } - + this.accept(sexpr.path); this.opcode('resolvePossibleLambda'); }, 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', 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', params.length, id.original, id.isSimple); + this.accept(path); + this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path)); } }, - 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); - } - }, + PathExpression: function(path) { + this.addDepth(path.depth); + this.opcode('getContext', path.depth); - ID: function(id) { - this.addDepth(id.depth); - this.opcode('getContext', id.depth); + var name = path.parts[0], + scoped = AST.helpers.scopedId(path), + blockParamId = !path.depth && !scoped && this.blockParamIndex(name); - var name = id.parts[0]; - if (!name) { + if (blockParamId) { + this.opcode('lookupBlockParam', blockParamId, path.parts); + } else if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` this.opcode('pushContext'); + } else if (path.data) { + this.options.data = true; + this.opcode('lookupData', path.depth, path.parts); } else { - this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', path.parts, path.falsy, scoped); } }, - DATA: function(data) { - this.options.data = true; - this.opcode('lookupData', data.id.depth, data.id.parts); + StringLiteral: function(string) { + this.opcode('pushString', string.value); }, - STRING: function(string) { - this.opcode('pushString', string.string); + NumberLiteral: function(number) { + this.opcode('pushLiteral', number.value); }, - NUMBER: function(number) { - this.opcode('pushLiteral', number.number); + BooleanLiteral: function(bool) { + this.opcode('pushLiteral', bool.value); }, - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool.bool); - }, + Hash: function(hash) { + var pairs = hash.pairs, i, l; + + this.opcode('pushHash'); - comment: function() {}, + for (i=0, l=pairs.length; i= 0) { + return [depth, param]; + } + } } }; 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); } @@ -385,13 +424,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); } @@ -407,7 +446,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); @@ -426,11 +465,11 @@ export function compile(input, options, env) { } return compiled._setup(options); }; - ret._child = function(i, data, depths) { + ret._child = function(i, data, blockParams, depths) { if (!compiled) { compiled = compileInput(); } - return compiled._child(i, data, depths); + return compiled._child(i, data, blockParams, depths); }; return ret; } @@ -449,3 +488,12 @@ function argEquals(a, b) { return true; } } + +function transformLiteralToPath(sexpr) { + if (!sexpr.path.parts) { + var literal = sexpr.path; + // Casting to string here to make false and 0 literal values play nicely with the rest + // of the system. + sexpr.path = new AST.PathExpression(false, 0, [literal.original+''], literal.original+'', literal.log); + } +} diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 758c740d5..beaf98869 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,186 +1,116 @@ 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) === '~' }; } +export function stripComment(comment) { + return comment.replace(/^\{\{~?\!-?-?/, '') + .replace(/-?-?~?\}\}$/, ''); +} -export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { +export function preparePath(data, parts, locInfo) { /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); - } - - var inverse = inverseAndProgram && inverseAndProgram.program; - - var strip = { - left: mustache.strip.left, - right: close.strip.right, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) - }; - - if (mustache.strip.right) { - omitRight(program.statements, null, true); - } - - if (inverse) { - var inverseStrip = inverseAndProgram.strip; - - if (inverseStrip.left) { - omitLeft(program.statements, null, true); - } - if (inverseStrip.right) { - omitRight(inverse.statements, null, true); - } - if (close.strip.left) { - omitLeft(inverse.statements, null, true); - } - - // Find standalone else statments - if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { - - omitLeft(program.statements); - omitRight(inverse.statements); - } - } else { - if (close.strip.left) { - omitLeft(program.statements, null, true); + locInfo = this.locInfo(locInfo); + + var original = data ? '@' : '', + dig = [], + depth = 0, + depthString = ''; + + for(var i=0,l=parts.length; i 0) { + throw new Exception('Invalid path: ' + original, {loc: locInfo}); + } else if (part === '..') { + depth++; + depthString += '../'; + } + } else { + dig.push(part); } } - if (inverted) { - return new this.BlockNode(mustache, inverse, program, strip, locInfo); - } else { - return new this.BlockNode(mustache, program, inverse, strip, locInfo); - } + return new this.PathExpression(data, depth, dig, original, locInfo); } +export function prepareMustache(path, params, hash, open, strip, locInfo) { + /*jshint -W040 */ + // Must use charAt to support IE pre-10 + var escapeFlag = open.charAt(3) || open.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; -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') { - current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : ''; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).statements); + return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); +} - // Strip out the previous content node if it's whitespace only - omitLeft(statements, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(statements, i); +export function prepareRawBlock(openRawBlock, content, close, locInfo) { + /*jshint -W040 */ + if (openRawBlock.path.original !== close) { + var errorNode = {loc: openRawBlock.path.loc}; - omitLeft((current.inverse || current.program).statements); - } + throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); } - return statements; + locInfo = this.locInfo(locInfo); + var program = new this.Program([content], null, {}, locInfo); + + return new this.BlockStatement( + openRawBlock.path, openRawBlock.params, openRawBlock.hash, + program, undefined, + {}, {}, {}, + locInfo); } -function isPrevWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = statements.length; - } +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.path.original !== close.path.original) { + var errorNode = {loc: openBlock.path.loc}; - // 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; + throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); } - 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; - } + program.blockParams = openBlock.blockParams; - var next = statements[i+1], - sibling = statements[i+2]; - if (!next) { - return isRoot; - } + var inverse, + inverseStrip; - if (next.type === 'content') { - return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); - } -} + if (inverseAndProgram) { + if (inverseAndProgram.chain) { + inverseAndProgram.program.body[0].closeStrip = close.strip; + } -// 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; + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; } - 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; + if (inverted) { + 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.path, openBlock.params, openBlock.hash, + program, inverse, + openBlock.strip, inverseStrip, close && close.strip, + this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index d41cacd73..a027edb91 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -1,5 +1,7 @@ import { COMPILER_REVISION, REVISION_CHANGES } from "../base"; import Exception from "../exception"; +import {isArray} from "../utils"; +import CodeGen from "./code-gen"; function Literal(value) { this.value = value; @@ -12,15 +14,13 @@ JavaScriptCompiler.prototype = { // alternative compiled forms for name lookup and buffering semantics nameLookup: function(parent, name /* , type*/) { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return parent + "." + name; + return [parent, ".", name]; } else { - return parent + "['" + name + "']"; + return [parent, "['", name, "']"]; } }, depthedLookup: function(name) { - this.aliases.lookup = 'this.lookup'; - - return 'lookup(depths, "' + name + '")'; + return [this.aliasable('this.lookup'), '(depths, "', name, '")']; }, compilerInfo: function() { @@ -29,23 +29,29 @@ JavaScriptCompiler.prototype = { return [revision, versions]; }, - appendToBuffer: function(string) { + appendToBuffer: function(source, location, explicit) { + // Force a source as this simplifies the merge logic. + if (!isArray(source)) { + source = [source]; + } + source = this.source.wrap(source, location); + if (this.environment.isSimple) { - return "return " + string + ";"; + return ['return ', source, ';']; + } else if (explicit) { + // This is a case where the buffer operation occurs as a child of another + // construct, generally braces. We have to explicitly output these buffer + // operations to ensure that the emitted code goes in the correct location. + return ['buffer += ', source, ';']; } else { - return { - appendToBuffer: true, - content: string, - toString: function() { return "buffer += " + string + ";"; } - }; + source.appendToBuffer = true; + return source; } }, initializeBuffer: function() { return this.quotedString(""); }, - - namespace: "Handlebars", // END PUBLIC API compile: function(environment, options, context, asObject) { @@ -71,23 +77,29 @@ JavaScriptCompiler.prototype = { this.hashes = []; this.compileStack = []; this.inlineStack = []; + this.blockParams = []; this.compileChildren(environment, options); - this.useDepths = this.useDepths || environment.depths.list.length || this.options.compat; + this.useDepths = this.useDepths || environment.useDepths || this.options.compat; + this.useBlockParams = this.useBlockParams || environment.useBlockParams; var opcodes = environment.opcodes, opcode, + firstLoc, i, l; for (i = 0, l = opcodes.length; i < l; i++) { opcode = opcodes[i]; + this.source.currentLocation = opcode.loc; + firstLoc = firstLoc || opcode.loc; this[opcode.opcode].apply(this, opcode.args); } // Flush any trailing content that might be pending. + this.source.currentLocation = firstLoc; this.pushSource(''); /* istanbul ignore next */ @@ -117,13 +129,27 @@ JavaScriptCompiler.prototype = { if (this.useDepths) { ret.useDepths = true; } + if (this.useBlockParams) { + ret.useBlockParams = true; + } if (this.options.compat) { ret.compat = true; } if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); + + this.source.currentLocation = {start: {line: 1, column: 0}}; ret = this.objectLiteral(ret); + + if (options.srcName) { + ret = ret.toStringWithSourceMap({file: options.destName}); + ret.map = ret.map && ret.map.toString(); + } else { + ret = ret.toString(); + } + } else { + ret.compilerOptions = this.options; } return ret; @@ -136,7 +162,7 @@ JavaScriptCompiler.prototype = { // track the last context pushed into place to allow skipping the // getContext opcode when it would be a noop this.lastContext = 0; - this.source = []; + this.source = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -148,14 +174,26 @@ JavaScriptCompiler.prototype = { } // Generate minimizer alias mappings + // + // When using true SourceNodes, this will update all references to the given alias + // as the source nodes are reused in situ. For the non-source node compilation mode, + // aliases will not be used, but this case is already being run on the client and + // we aren't concern about minimizing the template size. + var aliasCount = 0; for (var alias in this.aliases) { - if (this.aliases.hasOwnProperty(alias)) { - varDeclarations += ', ' + alias + '=' + this.aliases[alias]; + var node = this.aliases[alias]; + + if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { + varDeclarations += ', alias' + (++aliasCount) + '=' + alias; + node.children[0] = 'alias' + aliasCount; } } var params = ["depth0", "helpers", "partials", "data"]; + if (this.useBlockParams || this.useDepths) { + params.push('blockParams'); + } if (this.useDepths) { params.push('depths'); } @@ -168,59 +206,67 @@ JavaScriptCompiler.prototype = { return Function.apply(this, params); } else { - return 'function(' + params.join(',') + ') {\n ' + source + '}'; + return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']); } }, mergeSource: function(varDeclarations) { - var source = '', - buffer, + var isSimple = this.environment.isSimple, appendOnly = !this.forceBuffer, - appendFirst; + appendFirst, - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; + sourceSeen, + bufferStart, + bufferEnd; + this.source.each(function(line) { if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; + if (bufferStart) { + line.prepend(' + '); } else { - buffer = line.content; + bufferStart = line; } + bufferEnd = line; } else { - if (buffer) { - if (!source) { + if (bufferStart) { + if (!sourceSeen) { appendFirst = true; - source = buffer + ';\n '; } else { - source += 'buffer += ' + buffer + ';\n '; + bufferStart.prepend('buffer += '); } - buffer = undefined; + bufferEnd.add(';'); + bufferStart = bufferEnd = undefined; } - source += line + '\n '; - if (!this.environment.isSimple) { + sourceSeen = true; + if (!isSimple) { appendOnly = false; } } - } + }); + if (appendOnly) { - if (buffer || !source) { - source += 'return ' + (buffer || '""') + ';\n'; + if (bufferStart) { + bufferStart.prepend('return '); + bufferEnd.add(';'); + } else if (!sourceSeen) { + this.source.push('return "";'); } } else { varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); - if (buffer) { - source += 'return buffer + ' + buffer + ';\n'; + + if (bufferStart) { + bufferStart.prepend('return buffer + '); + bufferEnd.add(';'); } else { - source += 'return buffer;\n'; + this.source.push('return buffer;'); } } if (varDeclarations) { - source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source; + this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n')); } - return source; + return this.source.merge(); }, // [blockValue] @@ -233,15 +279,14 @@ JavaScriptCompiler.prototype = { // replace it on the stack with the result of properly // invoking blockHelperMissing. blockValue: function(name) { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = [this.contextName(0)]; - this.setupParams(name, 0, params); + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; + this.setupHelperArgs(name, 0, params); var blockName = this.popStack(); params.splice(1, 0, blockName); - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); + this.push(this.source.functionCall(blockHelperMissing, 'call', params)); }, // [ambiguousBlockValue] @@ -251,18 +296,20 @@ JavaScriptCompiler.prototype = { // On stack, after, if no lastHelper: same as [blockValue] // On stack, after, if lastHelper: value ambiguousBlockValue: function() { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - // We're being a bit cheeky and reusing the options value from the prior exec - var params = [this.contextName(0)]; - this.setupParams('', 0, params, true); + var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + params = [this.contextName(0)]; + this.setupHelperArgs('', 0, params, true); this.flushInline(); var current = this.topStack(); params.splice(1, 0, current); - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); + this.pushSource([ + 'if (!', this.lastHelper, ') { ', + current, ' = ', this.source.functionCall(blockHelperMissing, 'call', params), + '}']); }, // [appendContent] @@ -274,6 +321,8 @@ JavaScriptCompiler.prototype = { appendContent: function(content) { if (this.pendingContent) { content = this.pendingContent + content; + } else { + this.pendingLocation = this.source.currentLocation; } this.pendingContent = content; @@ -289,13 +338,18 @@ JavaScriptCompiler.prototype = { // If `value` is truthy, or 0, it is coerced into a string and appended // Otherwise, the empty string is appended append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); - if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); + if (this.isInline()) { + this.replaceStack(function(current) { + return [' != null ? ', current, ' : ""']; + }); + + this.pushSource(this.appendToBuffer(this.popStack())); + } else { + var local = this.popStack(); + this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); + if (this.environment.isSimple) { + this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); + } } }, @@ -306,9 +360,8 @@ JavaScriptCompiler.prototype = { // // Escape `value` and append it to the buffer appendEscaped: function() { - this.aliases.escapeExpression = 'this.escapeExpression'; - - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); + this.pushSource(this.appendToBuffer( + [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); }, // [getContext] @@ -340,9 +393,7 @@ JavaScriptCompiler.prototype = { // Looks up the value of `name` on the current context and pushes // it onto the stack. lookupOnContext: function(parts, falsy, scoped) { - /*jshint -W083 */ - var i = 0, - len = parts.length; + var i = 0; if (!scoped && this.options.compat && !this.lastContext) { // The depthed query is expected to handle the undefined logic for the root level that @@ -352,19 +403,21 @@ JavaScriptCompiler.prototype = { this.pushContext(); } - for (; i < len; i++) { - this.replaceStack(function(current) { - var lookup = this.nameLookup(current, parts[i], 'context'); - // We want to ensure that zero and false are handled properly if the context (falsy flag) - // needs to have the special handling for these values. - if (!falsy) { - return ' != null ? ' + lookup + ' : ' + current; - } else { - // Otherwise we can use generic falsy handling - return ' && ' + lookup; - } - }); - } + this.resolvePath('context', parts, i, falsy); + }, + + // [lookupBlockParam] + // + // On stack, before: ... + // On stack, after: blockParam[name], ... + // + // Looks up the value of `parts` on the given block param and pushes + // it onto the stack. + lookupBlockParam: function(blockParamId, parts) { + this.useBlockParams = true; + + this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']); + this.resolvePath('context', parts, 1); }, // [lookupData] @@ -381,10 +434,28 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral('this.data(data, ' + depth + ')'); } + this.resolvePath('data', parts, 0, true); + }, + + resolvePath: function(type, parts, i, falsy) { + /*jshint -W083 */ + if (this.options.strict || this.options.assumeObjects) { + this.push(strictLookup(this.options.strict, this, parts, type)); + return; + } + var len = parts.length; - for (var i = 0; i < len; i++) { + for (; i < len; i++) { this.replaceStack(function(current) { - return ' && ' + this.nameLookup(current, parts[i], 'data'); + var lookup = this.nameLookup(current, parts[i], type); + // We want to ensure that zero and false are handled properly if the context (falsy flag) + // needs to have the special handling for these values. + if (!falsy) { + return [' != null ? ', lookup, ' : ', current]; + } else { + // Otherwise we can use generic falsy handling + return [' && ', lookup]; + } }); } }, @@ -397,9 +468,7 @@ JavaScriptCompiler.prototype = { // If the `value` is a lambda, replace it on the stack by // the return value of the lambda resolvePossibleLambda: function() { - this.aliases.lambda = 'this.lambda'; - - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); + this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -416,7 +485,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 { @@ -425,9 +494,7 @@ JavaScriptCompiler.prototype = { } }, - emptyHash: function() { - this.pushStackLiteral('{}'); - + emptyHash: function(omitEmpty) { if (this.trackIds) { this.push('{}'); // hashIds } @@ -435,6 +502,7 @@ JavaScriptCompiler.prototype = { this.push('{}'); // hashContexts this.push('{}'); // hashTypes } + this.pushStackLiteral(omitEmpty ? 'undefined' : '{}'); }, pushHash: function() { if (this.hash) { @@ -447,14 +515,14 @@ JavaScriptCompiler.prototype = { this.hash = this.hashes.pop(); if (this.trackIds) { - this.push('{' + hash.ids.join(',') + '}'); + this.push(this.objectLiteral(hash.ids)); } if (this.stringParams) { - this.push('{' + hash.contexts.join(',') + '}'); - this.push('{' + hash.types.join(',') + '}'); + this.push(this.objectLiteral(hash.contexts)); + this.push(this.objectLiteral(hash.types)); } - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); + this.push(this.objectLiteral(hash.values)); }, // [pushString] @@ -467,17 +535,6 @@ JavaScriptCompiler.prototype = { this.pushStackLiteral(this.quotedString(string)); }, - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, - // [pushLiteral] // // On stack, before: ... @@ -516,13 +573,17 @@ JavaScriptCompiler.prototype = { // // If the helper is not found, `helperMissing` is called. invokeHelper: function(paramSize, name, isSimple) { - this.aliases.helperMissing = 'helpers.helperMissing'; - var nonHelper = this.popStack(); var helper = this.setupHelper(paramSize, name); + var simple = isSimple ? [helper.name, ' || '] : ''; + + var lookup = ['('].concat(simple, nonHelper); + if (!this.options.strict) { + lookup.push(' || ', this.aliasable('helpers.helperMissing')); + } + lookup.push(')'); - var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - this.push('((' + lookup + ').call(' + helper.callParams + '))'); + this.push(this.source.functionCall(lookup, 'call', helper.callParams)); }, // [invokeKnownHelper] @@ -534,7 +595,7 @@ JavaScriptCompiler.prototype = { // so a `helperMissing` fallback is not required. invokeKnownHelper: function(paramSize, name) { var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); + this.push(this.source.functionCall(helper.name, 'call', helper.callParams)); }, // [invokeAmbiguous] @@ -550,8 +611,6 @@ JavaScriptCompiler.prototype = { // and can be avoided by passing the `knownHelpers` and // `knownHelpersOnly` flags at compile-time. invokeAmbiguous: function(name, helperCall) { - this.aliases.functionType = '"function"'; - this.aliases.helperMissing = 'helpers.helperMissing'; this.useRegister('helper'); var nonHelper = this.popStack(); @@ -561,10 +620,21 @@ JavaScriptCompiler.prototype = { var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - this.push( - '((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing' - + (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),' - + '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))'); + var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')']; + if (!this.options.strict) { + lookup[0] = '(helper = '; + lookup.push( + ' != null ? helper : ', + this.aliasable('helpers.helperMissing') + ); + } + + this.push([ + '(', lookup, + (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', + '(typeof helper === ', this.aliasable('"function"'), ' ? ', + this.source.functionCall('helper','call', helper.callParams), ' : helper))' + ]); }, // [invokePartial] @@ -574,19 +644,34 @@ 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"]; + invokePartial: function(isDynamic, name, indent) { + 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 (isDynamic) { + name = this.popStack(); + delete options.name; + } + + if (indent) { + options.indent = JSON.stringify(indent); } + options.helpers = 'helpers'; + options.partials = 'partials'; + + if (!isDynamic) { + params.unshift(this.nameLookup('partials', name, 'partial')); + } else { + params.unshift(name); + } + if (this.options.compat) { - params.push('depths'); + options.depths = 'depths'; } + options = this.objectLiteral(options); + params.push(options); - this.push("this.invokePartial(" + params.join(", ") + ")"); + this.push(this.source.functionCall('this.invokePartial', '', params)); }, // [assignToHash] @@ -611,21 +696,25 @@ JavaScriptCompiler.prototype = { var hash = this.hash; if (context) { - hash.contexts.push("'" + key + "': " + context); + hash.contexts[key] = context; } if (type) { - hash.types.push("'" + key + "': " + type); + hash.types[key] = type; } if (id) { - hash.ids.push("'" + key + "': " + id); + hash.ids[key] = id; } - hash.values.push("'" + key + "': (" + value + ")"); + hash.values[key] = value; }, - pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { + pushId: function(type, name, child) { + if (type === 'BlockParam') { + this.pushStackLiteral( + 'blockParams[' + name[0] + '].path[' + name[1] + ']' + + (child ? ' + ' + JSON.stringify('.' + child) : '')); + } else if (type === 'PathExpression') { this.pushString(name); - } else if (type === 'sexpr') { + } else if (type === 'SubExpression') { this.pushStackLiteral('true'); } else { this.pushStackLiteral('null'); @@ -654,9 +743,13 @@ JavaScriptCompiler.prototype = { this.context.environments[index] = child; this.useDepths = this.useDepths || compiler.useDepths; + this.useBlockParams = this.useBlockParams || compiler.useBlockParams; } else { child.index = index; child.name = 'program' + index; + + this.useDepths = this.useDepths || child.useDepths; + this.useBlockParams = this.useBlockParams || child.useBlockParams; } } }, @@ -671,13 +764,12 @@ JavaScriptCompiler.prototype = { programExpression: function(guid) { var child = this.environment.children[guid], - depths = child.depths.list, - useDepths = this.useDepths, - depth; - - var programParams = [child.index, 'data']; + programParams = [child.index, 'data', child.blockParams]; - if (useDepths) { + if (this.useBlockParams || this.useDepths) { + programParams.push('blockParams'); + } + if (this.useDepths) { programParams.push('depths'); } @@ -691,13 +783,23 @@ JavaScriptCompiler.prototype = { } }, + push: function(expr) { + if (!(expr instanceof Literal)) { + expr = this.source.wrap(expr); + } + + this.inlineStack.push(expr); + return expr; + }, + pushStackLiteral: function(item) { - return this.push(new Literal(item)); + this.push(new Literal(item)); }, pushSource: function(source) { if (this.pendingContent) { - this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent))); + this.source.push( + this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation)); this.pendingContent = undefined; } @@ -706,18 +808,8 @@ JavaScriptCompiler.prototype = { } }, - pushStack: function(item) { - this.flushInline(); - - var stack = this.incrStack(); - this.pushSource(stack + " = " + item + ";"); - this.compileStack.push(stack); - return stack; - }, - replaceStack: function(callback) { - var prefix = '', - inline = this.isInline(), + var prefix = ['('], stack, createdStack, usedLiteral; @@ -732,14 +824,15 @@ JavaScriptCompiler.prototype = { if (top instanceof Literal) { // Literals do not need to be inlined - prefix = stack = top.value; + stack = [top.value]; + prefix = ['(', stack]; usedLiteral = true; } else { // Get or create the current stack name for use by the inline - createdStack = !this.stackSlot; - var name = !createdStack ? this.topStackName() : this.incrStack(); + createdStack = true; + var name = this.incrStack(); - prefix = '(' + this.push(name) + ' = ' + top + ')'; + prefix = ['((', this.push(name), ' = ', top, ')']; stack = this.topStack(); } @@ -751,7 +844,7 @@ JavaScriptCompiler.prototype = { if (createdStack) { this.stackSlot--; } - this.push('(' + prefix + item + ')'); + this.push(prefix.concat(item, ')')); }, incrStack: function() { @@ -764,15 +857,16 @@ JavaScriptCompiler.prototype = { }, flushInline: function() { var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + /* istanbul ignore if */ + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + var stack = this.incrStack(); + this.pushSource([stack, ' = ', entry, ';']); + this.compileStack.push(stack); } } }, @@ -802,6 +896,7 @@ JavaScriptCompiler.prototype = { var stack = (this.isInline() ? this.inlineStack : this.compileStack), item = stack[stack.length - 1]; + /* istanbul ignore if */ if (item instanceof Literal) { return item.value; } else { @@ -818,42 +913,42 @@ JavaScriptCompiler.prototype = { }, quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; + return this.source.quotedString(str); }, objectLiteral: function(obj) { - var pairs = []; + return this.source.objectLiteral(obj); + }, - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } + aliasable: function(name) { + var ret = this.aliases[name]; + if (ret) { + ret.referenceCount++; + return ret; } - return '{' + pairs.join(',') + '}'; + ret = this.aliases[name] = this.source.wrap(name); + ret.aliasable = true; + ret.referenceCount = 1; + + return ret; }, 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 { params: params, paramsInit: paramsInit, name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") + callParams: [this.contextName(0)].concat(params) }; }, - setupOptions: function(helper, paramSize, params) { - 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(); @@ -866,22 +961,14 @@ 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)` if (program || inverse) { - if (!program) { - program = 'this.noop'; - } - - if (!inverse) { - inverse = 'this.noop'; - } - - options.fn = program; - options.inverse = inverse; + options.fn = program || 'this.noop'; + options.inverse = inverse || 'this.noop'; } // The parameters go on to the stack in order (making sure that they are evaluated in order) @@ -901,29 +988,29 @@ JavaScriptCompiler.prototype = { } if (this.trackIds) { - options.ids = "[" + ids.join(",") + "]"; + options.ids = this.source.generateArray(ids); } if (this.stringParams) { - options.types = "[" + types.join(",") + "]"; - options.contexts = "[" + contexts.join(",") + "]"; + options.types = this.source.generateArray(types); + options.contexts = this.source.generateArray(contexts); } if (this.options.data) { - options.data = "data"; + options.data = 'data'; + } + if (this.useBlockParams) { + options.blockParams = 'blockParams'; } - return options; }, - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(helperName, paramSize, params, useRegister) { - var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params)); - + setupHelperArgs: function(helper, paramSize, params, useRegister) { + var options = this.setupParams(helper, paramSize, params, true); + options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); params.push('options'); - return 'options=' + options; + return ['options=', options]; } else { params.push(options); return ''; @@ -931,6 +1018,7 @@ JavaScriptCompiler.prototype = { } }; + var reservedWords = ( "break else new var" + " case finally return void" + @@ -946,7 +1034,8 @@ var reservedWords = ( " class float package throws" + " const goto private transient" + " debugger implements protected volatile" + - " double import public let yield" + " double import public let yield await" + + " null true false" ).split(" "); var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; @@ -959,4 +1048,24 @@ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); }; +function strictLookup(requireTerminal, compiler, parts, type) { + var stack = compiler.popStack(); + + var i = 0, + len = parts.length; + if (requireTerminal) { + len--; + } + + for (; i < len; i++) { + stack = compiler.nameLookup(stack, parts[i], type); + } + + if (requireTerminal) { + return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; + } else { + return stack; + } +} + export default JavaScriptCompiler; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 7654245c5..55232cc45 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -21,13 +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; - for(i=0, l=statements.length; i ' + 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 " + 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 1) { - return "PATH:" + path; - } else { - return "ID:" + path; + for (var i=0, l=pairs.length; i 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) { if(opts.partial){ - output.push('return Handlebars.partials;\n'); + output.add('return Handlebars.partials;\n'); } else { - output.push('return templates;\n'); + output.add('return templates;\n'); } } - output.push('});'); + output.add('});'); } else if (!opts.commonjs) { - output.push('})();'); + output.add('})();'); } } - output = output.join(''); + + + if (opts.map) { + output.add('\n//# sourceMappingURL=' + opts.map + '\n'); + } + + output = output.toStringWithSourceMap(); + output.map = output.map + ''; if (opts.min) { - output = uglify.minify(output, {fromString: true}).code; + output = uglify.minify(output.code, { + fromString: true, + + outSourceMap: opts.map, + inSourceMap: JSON.parse(output.map) + }); + if (opts.map) { + output.code += '\n//# sourceMappingURL=' + opts.map + '\n'; + } + } + + if (opts.map) { + fs.writeFileSync(opts.map, output.map, 'utf8'); } + output = output.code; if (opts.output) { fs.writeFileSync(opts.output, output, 'utf8'); diff --git a/package.json b/package.json index 6ed9a5526..a0265507f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handlebars", "barename": "handlebars", - "version": "2.0.0", + "version": "3.0.0", "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", "homepage": "http://www.handlebarsjs.com/", "keywords": [ @@ -21,36 +21,37 @@ "node": ">=0.4.7" }, "dependencies": { - "optimist": "~0.3" + "optimist": "^0.6.1", + "source-map": "^0.1.40" }, "optionalDependencies": { "uglify-js": "~2.3" }, "devDependencies": { - "async": "~0.2.9", + "async": "^0.9.0", "aws-sdk": "~1.5.0", "benchmark": "~1.0", - "dustjs-linkedin": "~2.0.2", + "dustjs-linkedin": "^2.0.2", "eco": "~1.1.0-rc-3", - "es6-module-packager": "1.x", + "es6-module-packager": "^2.0.0", "grunt": "~0.4.1", "grunt-cli": "~0.1.10", - "grunt-contrib-clean": "~0.4.1", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-clean": "0.x", + "grunt-contrib-concat": "0.x", + "grunt-contrib-connect": "0.x", + "grunt-contrib-copy": "0.x", "grunt-contrib-jshint": "0.x", - "grunt-contrib-requirejs": "~0.4.1", - "grunt-contrib-uglify": "~0.2.2", - "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-requirejs": "0.x", + "grunt-contrib-uglify": "0.x", + "grunt-contrib-watch": "0.x", "grunt-saucelabs": "8.x", "istanbul": "^0.3.0", "jison": "~0.3.0", "keen.io": "0.0.3", "mocha": "~1.20.0", - "mustache": "~0.7.2", - "semver": "~2.1.0", - "underscore": "~1.5.1" + "mustache": "0.x", + "semver": "^4.0.0", + "underscore": "^1.5.1" }, "main": "lib/index.js", "bin": { @@ -58,5 +59,14 @@ }, "scripts": { "test": "grunt" + }, + "jspm": { + "main": "handlebars", + "directories": { + "lib": "dist/amd" + }, + "buildConfig": { + "minify": true + } } } diff --git a/release-notes.md b/release-notes.md index 8d9b233ce..ad7b7f026 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,57 @@ ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...master) +[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...master) + +## v3.0.0 - February 10th, 2015 +- [#941](https://github.com/wycats/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#940](https://github.com/wycats/handlebars.js/pull/940) - Add missing reserved words so compiler knows to use array syntax: ([@mattflaschen](https://api.github.com/users/mattflaschen)) +- [#938](https://github.com/wycats/handlebars.js/pull/938) - Fix example using #with helper ([@diwo](https://api.github.com/users/diwo)) +- [#930](https://github.com/wycats/handlebars.js/pull/930) - Add parent tracking and mutation to AST visitors ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#926](https://github.com/wycats/handlebars.js/issues/926) - Depthed lookups fail when program duplicator runs ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#918](https://github.com/wycats/handlebars.js/pull/918) - Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos ([@oneeman](https://api.github.com/users/oneeman)) +- [#915](https://github.com/wycats/handlebars.js/pull/915) - Ast update ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#910](https://github.com/wycats/handlebars.js/issues/910) - Different behavior of {{@last}} when {{#each}} in {{#each}} ([@zordius](https://api.github.com/users/zordius)) +- [#907](https://github.com/wycats/handlebars.js/issues/907) - Implement named helper variable references ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#906](https://github.com/wycats/handlebars.js/pull/906) - Add parser support for block params ([@mmun](https://api.github.com/users/mmun)) +- [#903](https://github.com/wycats/handlebars.js/issues/903) - Only provide aliases for multiple use calls ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#902](https://github.com/wycats/handlebars.js/pull/902) - Generate Source Maps ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#901](https://github.com/wycats/handlebars.js/issues/901) - Still escapes with noEscape enabled on isolated Handlebars environment ([@zedknight](https://api.github.com/users/zedknight)) +- [#896](https://github.com/wycats/handlebars.js/pull/896) - Simplify BlockNode by removing intermediate MustacheNode ([@mmun](https://api.github.com/users/mmun)) +- [#892](https://github.com/wycats/handlebars.js/pull/892) - Implement parser for else chaining of helpers ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#889](https://github.com/wycats/handlebars.js/issues/889) - Consider extensible parser API ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#887](https://github.com/wycats/handlebars.js/issues/887) - Handlebars.noConflict() option? ([@bradvogel](https://api.github.com/users/bradvogel)) +- [#886](https://github.com/wycats/handlebars.js/issues/886) - Add SafeString to context (or use duck-typing) ([@dominicbarnes](https://api.github.com/users/dominicbarnes)) +- [#870](https://github.com/wycats/handlebars.js/pull/870) - Registering undefined partial throws exception. ([@max-b](https://api.github.com/users/max-b)) +- [#866](https://github.com/wycats/handlebars.js/issues/866) - comments don't respect whitespace control ([@75lb](https://api.github.com/users/75lb)) +- [#863](https://github.com/wycats/handlebars.js/pull/863) - + jsDelivr CDN info ([@tomByrer](https://api.github.com/users/tomByrer)) +- [#858](https://github.com/wycats/handlebars.js/issues/858) - Disable new default auto-indent at included partials ([@majodev](https://api.github.com/users/majodev)) +- [#856](https://github.com/wycats/handlebars.js/pull/856) - jspm compatibility ([@MajorBreakfast](https://api.github.com/users/MajorBreakfast)) +- [#805](https://github.com/wycats/handlebars.js/issues/805) - Request: "strict" lookups ([@nzakas](https://api.github.com/users/nzakas)) + +- Export the default object for handlebars/runtime - 5594416 +- Lookup partials when undefined - 617dd57 + +Compatibility notes: +- Runtime breaking changes. Must match 3.x runtime and precompiler. +- The AST has been upgraded to a public API. + - There are a number of changes to this, but the format is now documented in docs/compiler-api.md + - The Visitor API has been expanded to support mutation and provide a base implementation +- The `JavaScriptCompiler` APIs have been formalized and documented. As part of the sourcemap handling these should be updated to return arrays for concatenation. +- `JavaScriptCompiler.namespace` has been removed as it was unused. +- `SafeString` is now duck typed on `toHTML` + +New Features: +- noConflict +- Source Maps +- Block Params +- Strict Mode +- @last and other each changes +- Chained else blocks +- @data methods can now have helper parameters passed to them +- Dynamic partials + +[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...v3.0.0) ## v2.0.0 - September 1st, 2014 - Update jsfiddle to 2.0.0-beta.1 - 0670f65 diff --git a/runtime.js b/runtime.js index b7a7b12eb..1896fa9db 100644 --- a/runtime.js +++ b/runtime.js @@ -1,3 +1,3 @@ // Create a simple path alias to allow browserify to resolve // the runtime on a supported path. -module.exports = require('./dist/cjs/handlebars.runtime'); +module.exports = require('./dist/cjs/handlebars.runtime').default; diff --git a/spec/artifacts/bom.handlebars b/spec/artifacts/bom.handlebars new file mode 100644 index 000000000..548d71419 --- /dev/null +++ b/spec/artifacts/bom.handlebars @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/spec/ast.js b/spec/ast.js index c28e876e8..3b2a4ae20 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -5,245 +5,164 @@ 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({}, null, null, 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}}"); - }, Handlebars.Exception, "foo doesn't match bar - 2:2"); + }, Handlebars.Exception, "foo doesn't match bar - 2:5"); }); 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 mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {}); + var block = new handlebarsEnv.AST.BlockStatement( + mustacheNode, + null, null, + {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([], {}, 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].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/basic.js b/spec/basic.js index 8a9c116cf..9b6678a5a 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -1,4 +1,4 @@ -/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo */ +/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo, shouldThrow */ global.handlebarsEnv = null; beforeEach(function() { @@ -33,6 +33,13 @@ describe("basic context", function() { shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", "comments are ignored"); + + shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah'); + shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah'); + shouldCompileTo(' {{! comment ~}} blah', {}, ' blah'); + shouldCompileTo(' {{!-- long-comment --~}} blah', {}, ' blah'); + shouldCompileTo(' {{~! comment}} blah', {}, ' blah'); + shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah'); }); it("boolean", function() { @@ -137,12 +144,12 @@ describe("basic context", function() { }); it("pathed block functions without context argument", function() { shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}", - {foo: {awesome: function(options) { return this; }}}, + {foo: {awesome: function() { return this; }}}, "inner", "block functions are called with options"); }); it("depthed block functions without context argument", function() { shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}", - {value: true, awesome: function(options) { return this; }}, + {value: true, awesome: function() { return this; }}, "inner", "block functions are called with options"); }); @@ -222,4 +229,33 @@ describe("basic context", function() { CompilerContext.compile(string); }, Error); }); + + it('pass string literals', function() { + shouldCompileTo('{{"foo"}}', {}, ''); + shouldCompileTo('{{"foo"}}', { foo: 'bar' }, 'bar'); + shouldCompileTo('{{#"foo"}}{{.}}{{/"foo"}}', { foo: ['bar', 'baz'] }, 'barbaz'); + }); + + it('pass number literals', function() { + shouldCompileTo('{{12}}', {}, ''); + shouldCompileTo('{{12}}', { '12': 'bar' }, 'bar'); + shouldCompileTo('{{12.34}}', {}, ''); + shouldCompileTo('{{12.34}}', { '12.34': 'bar' }, 'bar'); + shouldCompileTo('{{12.34 1}}', { '12.34': function(arg) { return 'bar' + arg; } }, 'bar1'); + }); + + it('pass boolean literals', function() { + shouldCompileTo('{{true}}', {}, ''); + shouldCompileTo('{{true}}', { '': 'foo' }, ''); + shouldCompileTo('{{false}}', { 'false': 'foo' }, 'foo'); + }); + + it('should handle literals in subexpression', function() { + var helpers = { + foo: function(arg) { + return arg; + } + }; + shouldCompileTo('{{foo (false)}}', [{ 'false': function() { return 'bar'; } }, helpers], 'bar'); + }); }); diff --git a/spec/blocks.js b/spec/blocks.js index a172970d8..21fb71842 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -89,6 +89,20 @@ describe('blocks', function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, "No people"); }); + it("chained inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", {none: "No people"}, + "No people"); + }); + it("chained inverted sections with mismatch", function() { + shouldThrow(function() { + shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/if}}", {none: "No people"}, + "No people"); + }, Error); + }); it("block inverted sections with empty arrays", function() { shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, @@ -105,6 +119,12 @@ describe('blocks', function() { shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 'No people\n'); }); + it('block standalone chained else sections', function() { + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'}, + 'No people\n'); + }); it('should handle nesting', function() { shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.'); }); diff --git a/spec/builtins.js b/spec/builtins.js index 2cd6bacfc..f3b4baaba 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -98,8 +98,7 @@ describe('builtin helpers', function() { var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; equals(actual === expected1 || actual === expected2, true, "each with object argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with object argument ignores the contents when empty"); + shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!'); }); it("each with @index", function() { @@ -122,6 +121,16 @@ describe('builtin helpers', function() { equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used"); }); + it('each with block params', function() { + var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!'; + var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'); + }); + it("each object with @index", function() { var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"}; @@ -173,6 +182,16 @@ describe('builtin helpers', function() { equal(result, "GOODBYE! cruel world!", "The @last variable is used"); }); + it("each object with @last", function() { + var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; + var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "Goodbye! cruel world!", "The @last variable is used"); + }); + it("each with nested @last", function() { var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!"; var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; @@ -218,13 +237,16 @@ describe('builtin helpers', function() { return; } - var info, + var log, + info, error; beforeEach(function() { + log = console.log; info = console.info; error = console.error; }); afterEach(function() { + console.log = log; console.info = info; console.error = error; }); @@ -257,15 +279,22 @@ describe('builtin helpers', function() { equals(3, levelArg); equals("whee", logArg); }); - it('should not output to console', function() { + it('should output to info', function() { var string = "{{log blah}}"; var hash = { blah: "whee" }; + var called; - console.info = function() { - throw new Error(); + console.info = function(log) { + equals("whee", log); + called = true; + }; + console.log = function(log) { + equals("whee", log); + called = true; }; - shouldCompileTo(string, hash, "", "log should not display"); + shouldCompileTo(string, hash, ""); + equals(true, called); }); it('should log at data level', function() { var string = "{{log blah}}"; diff --git a/spec/compiler.js b/spec/compiler.js index 250dbc74b..f9eba28fb 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")], {}))(), '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")]), {})), 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/data.js b/spec/data.js index bb90df5a8..1678eea85 100644 --- a/spec/data.js +++ b/spec/data.js @@ -93,6 +93,17 @@ describe('data', function() { }, Error); }); + it('data can be functions', function() { + var template = CompilerContext.compile('{{@hello}}'); + var result = template({}, { data: { hello: function() { return 'hello'; } } }); + equals('hello', result); + }); + it('data can be functions with params', function() { + var template = CompilerContext.compile('{{@hello "hello"}}'); + var result = template({}, { data: { hello: function(arg) { return arg; } } }); + equals('hello', result); + }); + it("data is inherited downstream", function() { var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true }); var helpers = { diff --git a/spec/env/browser.js b/spec/env/browser.js index bcf4259d3..a9042e3fb 100644 --- a/spec/env/browser.js +++ b/spec/env/browser.js @@ -5,10 +5,12 @@ var _ = require('underscore'), fs = require('fs'), vm = require('vm'); -global.Handlebars = undefined; +global.Handlebars = 'no-conflict'; vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js'); global.CompilerContext = { + browser: true, + compile: function(template, options) { var templateSpec = handlebarsEnv.precompile(template, options); return handlebarsEnv.template(safeEval(templateSpec)); diff --git a/spec/env/common.js b/spec/env/common.js index 92cc61123..9ffbc1462 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -18,7 +18,7 @@ global.compileWithPartials = function(string, hashOrArray, partials) { ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); - options = {compat: hashOrArray[3]}; + options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]}; if (hashOrArray[4] != null) { options.data = !!hashOrArray[4]; ary[1].data = hashOrArray[4]; @@ -45,7 +45,7 @@ global.shouldThrow = function(callback, type, msg) { failed = true; } catch (err) { if (type && !(err instanceof type)) { - throw new Error('Type failure'); + throw new Error('Type failure: ' + err); } if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { equal(msg, err.message); diff --git a/spec/env/runtime.js b/spec/env/runtime.js index 5a2dcd9b3..0c144064e 100644 --- a/spec/env/runtime.js +++ b/spec/env/runtime.js @@ -5,7 +5,7 @@ var _ = require('underscore'), fs = require('fs'), vm = require('vm'); -global.Handlebars = undefined; +global.Handlebars = 'no-conflict'; vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js'); var parse = require('../../dist/cjs/handlebars/compiler/base').parse; @@ -13,6 +13,8 @@ var compiler = require('../../dist/cjs/handlebars/compiler/compiler'); var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default']; global.CompilerContext = { + browser: true, + compile: function(template, options) { // Hack the compiler on to the environment for these specific tests handlebarsEnv.precompile = function(template, options) { diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js index 1cf3298eb..852733b2f 100644 --- a/spec/expected/empty.amd.js +++ b/spec/expected/empty.amd.js @@ -1,6 +1,6 @@ define(['handlebars.runtime'], function(Handlebars) { Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { - return ""; + return ""; },"useData":true}); }); diff --git a/spec/helpers.js b/spec/helpers.js index e604e91c8..712bb00a0 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -60,6 +60,10 @@ describe('helpers', function() { }}; shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye"); }); + it('helper returning undefined value', function() { + shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' '); + shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' '); + }); it("block helper", function() { var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; @@ -653,4 +657,64 @@ describe('helpers', function() { equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); }); }); + + describe('block params', function() { + it('should take presedence over context values', function() { + var hash = {value: 'foo'}; + var helpers = { + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({value: 'bar'}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo'); + }); + it('should take presedence over helper values', function() { + var hash = {}; + var helpers = { + value: function() { + return 'foo'; + }, + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo'); + }); + it('should not take presedence over pathed values', function() { + var hash = {value: 'bar'}; + var helpers = { + value: function() { + return 'foo'; + }, + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn(this, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}', [hash, helpers], 'barfoo'); + }); + it('should take presednece over parent block params', function() { + var hash = {value: 'foo'}, + value = 1; + var helpers = { + goodbyes: function(options) { + return options.fn({value: 'bar'}, {blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined}); + } + }; + shouldCompileTo('{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', [hash, helpers], '13foo'); + }); + + it('should allow block params on chained helpers', function() { + var hash = {value: 'foo'}; + var helpers = { + goodbyes: function(options) { + equals(options.fn.blockParams, 1); + return options.fn({value: 'bar'}, {blockParams: [1, 2]}); + } + }; + shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo'); + }); + }); }); diff --git a/spec/javascript-compiler.js b/spec/javascript-compiler.js index 16058680b..fb7865854 100644 --- a/spec/javascript-compiler.js +++ b/spec/javascript-compiler.js @@ -19,6 +19,12 @@ describe('javascript-compiler api', function() { }; shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food"); }); + + // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases + // to avoid errors in older browsers. + it('should handle reserved words', function() { + shouldCompileTo("{{foo}} {{~null~}}", { foo: "food" }, "food"); + }); }); describe('#compilerInfo', function() { var $superCheck, $superInfo; @@ -63,7 +69,7 @@ describe('javascript-compiler api', function() { }); it('should allow append buffer override', function() { handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) { - return $superAppend.call(this, string + ' + "_foo"'); + return $superAppend.call(this, [string, ' + "_foo"']); }; shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo"); }); diff --git a/spec/parser.js b/spec/parser.js index 131160a74..3d3dccff3 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -10,19 +10,23 @@ 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('{{123}}'), "{{ NUMBER{123} [] }}\n"); + equals(ast_for('{{"foo"}}'), '{{ "foo" [] }}\n'); + equals(ast_for('{{false}}'), '{{ BOOLEAN{false} [] }}\n'); + equals(ast_for('{{true}}'), '{{ BOOLEAN{true} [] }}\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 +34,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,48 +115,64 @@ 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 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('parses a standalone inverse section', function() { - equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + it('throws on old inverse section', function() { + shouldThrow(function() { + ast_for("{{else foo}}bar{{/foo}}"); + }, Error); }); + it('parses block with block params', function() { + 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 PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + }); + it('parses chained inverse block with block params', function() { + equals(ast_for("{{#foo}}{{else foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + }); it("raises if there's a Parse error", function() { shouldThrow(function() { ast_for("foo{{^}}bar"); @@ -170,6 +192,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}"); @@ -187,7 +221,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")])), "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 20187f81d..0c9e0f6dc 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -8,6 +8,32 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); }); + it('dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'dude'; + } + }; + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }); + it('failing dynamic partials', function() { + var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + var helpers = { + partial: function() { + return 'missing'; + } + }; + shouldThrow(function() { + shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); + }, Handlebars.Exception, 'The partial missing could not be found'); + }); + it("partials with context", function() { var string = "Dudes: {{>dude dudes}}"; var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; @@ -23,6 +49,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}}) "; @@ -41,11 +73,18 @@ describe('partials', function() { it("rendering undefined partial throws an exception", function() { shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); + var template = CompilerContext.compile("{{> whatever}}"); + template(); }, Handlebars.Exception, 'The partial whatever could not be found'); }); + it("registering undefined partial throws an exception", function() { + shouldThrow(function() { + var undef; + handlebarsEnv.registerPartial('undefined_test', undef); + }, Handlebars.Exception, 'Attempting to register a partial as undefined'); + }); + it("rendering template partial in vm mode throws an exception", function() { shouldThrow(function() { var template = CompilerContext.compile("{{> whatever}}"); @@ -64,10 +103,10 @@ describe('partials', function() { }); it("GH-14: a partial preceding a selector", function() { - var string = "Dudes: {{>dude}} {{another_dude}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); + var string = "Dudes: {{>dude}} {{another_dude}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); }); it("Partials with slash paths", function() { @@ -151,6 +190,15 @@ describe('partials', function() { handlebarsEnv.compile = compile; }); + it('should pass compiler flags', function() { + if (Handlebars.compile) { + var env = Handlebars.create(); + env.registerPartial('partial', '{{foo}}'); + var template = env.compile('{{foo}} {{> partial}}', {noEscape: true}); + equal(template({foo: '<'}), '< <'); + } + }); + describe('standalone partials', function() { it("indented partials", function() { var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; @@ -167,6 +215,14 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); }); + it("prevent nested indented partials", function() { + var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; + var dude = "{{name}}\n {{> url}}"; + var url = "{{url}}!\n"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}, {preventIndent: true}], true, + "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n"); + }); }); describe('compat mode', function() { diff --git a/spec/precompiler.js b/spec/precompiler.js index bd19fbf85..cbbfdf7ea 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -9,27 +9,38 @@ describe('precompiler', function() { var Handlebars = require('../lib'), Precompiler = require('../lib/precompiler'), + fs = require('fs'), uglify = require('uglify-js'); var log, logFunction, precompile, - minify; + minify, + + file, + content, + writeFileSync; beforeEach(function() { precompile = Handlebars.precompile; minify = uglify.minify; + writeFileSync = fs.writeFileSync; logFunction = console.log; log = ''; console.log = function() { log += Array.prototype.join.call(arguments, ''); }; + fs.writeFileSync = function(_file, _content) { + file = _file; + content = _content; + }; }); afterEach(function() { Handlebars.precompile = precompile; uglify.minify = minify; + fs.writeFileSync = writeFileSync; console.log = logFunction; }); @@ -50,7 +61,7 @@ describe('precompiler', function() { it('should throw when combining simple and minimized', function() { shouldThrow(function() { Precompiler.cli({templates: [__dirname], simple: true, min: true}); - }, Handlebars.Exception, 'Unable to minimze simple output'); + }, Handlebars.Exception, 'Unable to minimize simple output'); }); it('should throw when combining simple and multiple templates', function() { shouldThrow(function() { @@ -87,7 +98,8 @@ describe('precompiler', function() { }); it('should output multiple amd', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars'}); + Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'}); + equal(/templates = foo = foo \|\|/.test(log), true); equal(/return templates/.test(log), true); equal(/template\(amd\)/.test(log), true); }); @@ -121,10 +133,42 @@ describe('precompiler', function() { equal(log, 'simple\n'); }); + it('should handle different root', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', root: 'foo/'}); + equal(log, 'simple\n'); + }); + it('should output to file system', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'}); + equal(file, 'file!'); + equal(content, 'simple\n'); + equal(log, ''); + }); + it('should handle BOM', function() { + Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; }; + Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true}); + equal(log, 'simple\n'); + }); + it('should output minimized templates', function() { Handlebars.precompile = function() { return 'amd'; }; uglify.minify = function() { return {code: 'min'}; }; Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'}); equal(log, 'min'); }); + + it('should output map', function() { + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'}); + + equal(file, 'foo.js.map'); + equal(/sourceMappingURL=/.test(log), true); + }); + + it('should output map', function() { + Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'}); + + equal(file, 'foo.js.map'); + equal(/sourceMappingURL=/.test(log), true); + }); }); diff --git a/spec/regressions.js b/spec/regressions.js index 11207fce3..84b9d0c52 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -1,4 +1,4 @@ -/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */ +/*global CompilerContext, shouldCompileTo, shouldThrow */ describe('Regressions', function() { it("GH-94: Cannot read property of undefined", function() { var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]}; @@ -145,4 +145,21 @@ describe('Regressions', function() { shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined'); }); + + it('GH-926: Depths and de-dupe', function() { + var context = { + name: 'foo', + data: [ + 1 + ], + notData: [ + 1 + ] + }; + + var template = CompilerContext.compile('{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}'); + + var result = template(context); + equals(result, 'foo'); + }); }); diff --git a/spec/runtime.js b/spec/runtime.js index d33dd747c..ce8b14c56 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -48,6 +48,16 @@ describe('runtime', function() { template._child(1); }, Error, 'must pass parent depths'); }); + + it('should throw for block param methods without params', function() { + shouldThrow(function() { + var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}'); + // Calling twice to hit the non-compiled case. + template._setup({}); + template._setup({}); + template._child(1); + }, Error, 'must pass block params'); + }); it('should expose child template', function() { var template = Handlebars.compile('{{#foo}}bar{{/foo}}'); // Calling twice to hit the non-compiled case. @@ -57,7 +67,25 @@ describe('runtime', function() { it('should render depthed content', function() { var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}'); // Calling twice to hit the non-compiled case. - equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz'); + equal(template._child(1, undefined, [], [{bar: 'baz'}])(), 'baz'); + }); + }); + + describe('#noConflict', function() { + if (!CompilerContext.browser) { + return; + } + + it('should reset on no conflict', function() { + var reset = Handlebars; + Handlebars.noConflict(); + equal(Handlebars, 'no-conflict'); + + Handlebars = 'really, none'; + reset.noConflict(); + equal(Handlebars, 'really, none'); + + Handlebars = reset; }); }); }); diff --git a/spec/source-map.js b/spec/source-map.js new file mode 100644 index 000000000..6eced9ca0 --- /dev/null +++ b/spec/source-map.js @@ -0,0 +1,49 @@ +/*global CompilerContext, Handlebars */ +try { + var SourceMap = require('source-map'), + SourceMapConsumer = SourceMap.SourceMapConsumer; +} catch (err) { + /* NOP for in browser */ +} + +describe('source-map', function() { + if (!Handlebars.precompile || !SourceMap) { + return; + } + + it('should safely include source map info', function() { + var template = Handlebars.precompile('{{hello}}', {destName: 'dest.js', srcName: 'src.hbs'}); + + equal(!!template.code, true); + equal(!!template.map, !CompilerContext.browser); + }); + it('should map source properly', function() { + var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', + template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'}); + + if (template.map) { + var consumer = new SourceMapConsumer(template.map), + lines = template.code.split('\n'), + srcLines = source.split('\n'), + + generated = grepLine('" b"', lines), + source = grepLine(' b', srcLines); + + var mapped = consumer.originalPositionFor(generated); + equal(mapped.line, source.line); + equal(mapped.column, source.column); + } + }); +}); + +function grepLine(token, lines) { + for (var i = 0; i < lines.length; i++) { + var column = lines[i].indexOf(token); + if (column >= 0) { + return { + line: i+1, + column: column + }; + } + } +} diff --git a/spec/strict.js b/spec/strict.js new file mode 100644 index 000000000..f701866fe --- /dev/null +++ b/spec/strict.js @@ -0,0 +1,124 @@ +/*global CompilerContext, Handlebars, shouldThrow */ +var Exception = Handlebars.Exception; + +describe('strict', function() { + describe('strict mode', function() { + it('should error on missing property lookup', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + }); + it('should error on missing child', function() { + var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); + equals(template({hello: {bar: 'foo'}}), 'foo'); + + shouldThrow(function() { + template({hello: {}}); + }, Exception, /"bar" not defined in/); + }); + it('should handle explicit undefined', function() { + var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); + + equals(template({hello: {bar: undefined}}), ''); + }); + it('should error on missing property lookup in known helpers mode', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true, knownHelpersOnly: true}); + + template({}); + }, Exception, /"hello" not defined in/); + }); + it('should error on missing context', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {strict: true}); + + template(); + }, Error); + }); + + it('should error on missing data lookup', function() { + var template = CompilerContext.compile('{{@hello}}', {strict: true}); + equals(template(undefined, {data: {hello: 'foo'}}), 'foo'); + + shouldThrow(function() { + template(); + }, Error); + }); + + it('should not run helperMissing for helper calls', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello foo}}', {strict: true}); + + template({foo: true}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello foo}}{{/hello}}', {strict: true}); + + template({foo: true}); + }, Exception, /"hello" not defined in/); + }); + it('should throw on ambiguous blocks', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello}}{{/hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{^hello}}{{/hello}}', {strict: true}); + + template({}); + }, Exception, /"hello" not defined in/); + + shouldThrow(function() { + var template = CompilerContext.compile('{{#hello.bar}}{{/hello.bar}}', {strict: true}); + + template({hello: {}}); + }, Exception, /"bar" not defined in/); + }); + }); + + describe('assume objects', function() { + it('should ignore missing property', function() { + var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); + + equal(template({}), ''); + }); + it('should ignore missing child', function() { + var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); + + equal(template({hello: {}}), ''); + }); + it('should error on missing object', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); + + template({}); + }, Error); + }); + it('should error on missing context', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); + + template(); + }, Error); + }); + + it('should error on missing data lookup', function() { + shouldThrow(function() { + var template = CompilerContext.compile('{{@hello.bar}}', {assumeObjects: true}); + + template(); + }, Error); + }); + + it('should execute blockHelperMissing', function() { + var template = CompilerContext.compile('{{^hello}}foo{{/hello}}', {assumeObjects: true}); + + equals(template({}), 'foo'); + }); + }); +}); diff --git a/spec/string-params.js b/spec/string-params.js index 2e88cf15c..4704a84f4 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 5c9fdfc30..1fb877551 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], ''); }); @@ -159,7 +159,7 @@ describe('subexpressions', function() { t: function(defaultString) { return new Handlebars.SafeString(defaultString); } - } + }; shouldCompileTo(string, [context, helpers], ''); }); @@ -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/tokenizer.js b/spec/tokenizer.js index 0f0dc0b88..0a5143bef 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -1,3 +1,4 @@ +/*global Handlebars */ function shouldMatchTokens(result, tokens) { for (var index = 0; index < result.length; index++) { equals(result[index].name, tokens[index]); @@ -217,19 +218,19 @@ describe('Tokenizer', function() { it('tokenizes a comment as "COMMENT"', function() { var result = tokenize("foo {{! this is a comment }} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a comment "); + shouldBeToken(result[1], "COMMENT", "{{! this is a comment }}"); }); it('tokenizes a block comment as "COMMENT"', function() { var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a {{comment}} "); + shouldBeToken(result[1], "COMMENT", "{{!-- this is a {{comment}} --}}"); }); it('tokenizes a block comment with whitespace as "COMMENT"', function() { var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}"); shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); - shouldBeToken(result[1], "COMMENT", " this is a\n{{comment}}\n"); + shouldBeToken(result[1], "COMMENT", "{{!-- this is a\n{{comment}}\n--}}"); }); it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() { @@ -399,4 +400,21 @@ describe('Tokenizer', function() { var result = tokenize("{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg \"c\")}}"); shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'BOOLEAN', 'CLOSE_SEXPR', 'BOOLEAN', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'NUMBER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']); }); + + it('tokenizes block params', function() { + var result = tokenize("{{#foo as |bar|}}"); + shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + + result = tokenize("{{#foo as |bar baz|}}"); + shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + + result = tokenize("{{#foo as | bar baz |}}"); + shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + + result = tokenize("{{#foo as as | bar baz |}}"); + shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + + result = tokenize("{{else foo as |bar baz|}}"); + shouldMatchTokens(result, ['OPEN_INVERSE_CHAIN', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']); + }); }); diff --git a/spec/track-ids.js b/spec/track-ids.js index 938f98bb5..f337fbe11 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -106,6 +106,27 @@ describe('track ids', function() { equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1'); }); + it('should use block param paths', function() { + var template = CompilerContext.compile('{{#doIt as |is|}}{{wycats is.a slave.driver is}}{{/doIt}}', {trackIds: true}); + + var helpers = { + doIt: function(options) { + var blockParams = [this.is]; + blockParams.path = ['zomg']; + return options.fn(this, {blockParams: blockParams}); + }, + wycats: function(passiveVoice, noun, blah, options) { + equal(options.ids[0], 'zomg.a'); + equal(options.ids[1], 'slave.driver'); + equal(options.ids[2], 'zomg'); + + return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun; + } + }; + + equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS zomg.a:foo slave.driver:bar'); + }); + describe('builtin helpers', function() { var helpers = { wycats: function(name, options) { @@ -129,6 +150,17 @@ describe('track ids', function() { equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n'); }); + it('should handle block params', function() { + var helpers = { + wycats: function(name, options) { + return name + ':' + options.ids[0] + '\n'; + } + }; + + var template = CompilerContext.compile('{{#each array as |value|}}{{wycats value.name}}{{/each}}', {trackIds: true}); + + equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0.name\nbar:array.1.name\n'); + }); }); describe('#with', function() { it('should track contextPath', function() { diff --git a/spec/utils.js b/spec/utils.js index 0216c8de8..4582e2496 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -25,6 +25,12 @@ describe('utils', function() { var string = new Handlebars.SafeString('foo<&"\'>'); equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>'); + var obj = { + toHTML: function() { + return 'foo<&"\'>'; + } + }; + equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>'); }); it('should handle falsy', function() { equals(Handlebars.Utils.escapeExpression(''), ''); diff --git a/spec/visitor.js b/spec/visitor.js new file mode 100644 index 000000000..32172309c --- /dev/null +++ b/spec/visitor.js @@ -0,0 +1,143 @@ +/*global Handlebars, shouldThrow */ + +describe('Visitor', function() { + if (!Handlebars.Visitor || !Handlebars.print) { + return; + } + + it('should provide coverage', 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}}{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); + }); + + it('should traverse to stubs', function() { + var visitor = new Handlebars.Visitor(); + + visitor.StringLiteral = function(string) { + equal(string.value, '2'); + }; + visitor.NumberLiteral = function(number) { + equal(number.value, 1); + }; + visitor.BooleanLiteral = function(bool) { + equal(bool.value, true); + + equal(this.parents.length, 3); + equal(this.parents[0].type, 'SubExpression'); + equal(this.parents[1].type, 'BlockStatement'); + equal(this.parents[2].type, 'Program'); + }; + visitor.PathExpression = function(id) { + equal(/(foo\.)?bar$/.test(id.original), true); + }; + visitor.ContentStatement = function(content) { + equal(content.value, ' '); + }; + 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}}')); + }); + + it('should return undefined'); + + describe('mutating', function() { + describe('fields', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo foo="foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.Hash = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + it('should throw when removing required values', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.PathExpression = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'MustacheStatement requires path'); + }); + it('should throw when returning non-node responses', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.PathExpression = function() { + return {}; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting path on MustacheStatement'); + }); + }); + describe('arrays', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo "foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.NumberLiteral = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + }); + }); +}); diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js index 86364292e..cce9405dd 100644 --- a/spec/whitespace-control.js +++ b/spec/whitespace-control.js @@ -66,6 +66,9 @@ describe('whitespace control', function() { shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar'); shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar '); + + shouldCompileToWithPartials('foo\n {{~> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); + shouldCompileToWithPartials('foo\n {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo\n bar'); }); it('should only strip whitespace once', function() { diff --git a/src/handlebars.l b/src/handlebars.l index 0f420e7ee..ace0263fc 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -12,7 +12,7 @@ function strip(start, end) { LEFT_STRIP "~" RIGHT_STRIP "~" -LOOKAHEAD [=~}\s\/.)] +LOOKAHEAD [=~}\s\/.)|] LITERAL_LOOKAHEAD [~}\s)] /* @@ -56,7 +56,10 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} } [^\x00]*?/("{{{{/") { return 'CONTENT'; } -[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; +[\s\S]*?"--"{RIGHT_STRIP}?"}}" { + this.popState(); + return 'COMMENT'; +} "(" return 'OPEN_SEXPR'; ")" return 'CLOSE_SEXPR'; @@ -73,11 +76,18 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; -"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE'; +"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN'; "{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; "{{"{LEFT_STRIP}?"&" return 'OPEN'; -"{{!--" this.popState(); this.begin('com'); -"{{!"[\s\S]*?"}}" strip(3,5); this.popState(); return 'COMMENT'; +"{{"{LEFT_STRIP}?"!--" { + this.unput(yytext); + this.popState(); + this.begin('com'); +} +"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" { + this.popState(); + return 'COMMENT'; +} "{{"{LEFT_STRIP}? return 'OPEN'; "=" return 'EQUALS'; @@ -93,6 +103,8 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} "true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; "false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; \-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER'; +"as"\s+"|" return 'OPEN_BLOCK_PARAMS'; +"|" return 'CLOSE_BLOCK_PARAMS'; {ID} return 'ID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index a8d288f68..18f03b902 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), {}, @$) + : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$)) ; statement @@ -17,89 +17,112 @@ statement | block -> $1 | rawBlock -> $1 | partial -> $1 - | CONTENT -> new yy.ContentNode($1, @$) - | COMMENT -> new yy.CommentNode($1, @$) + | content -> $1 + | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) + ; + +content + : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$)) ; rawBlock - : openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock - : OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$) + : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 } ; block - : openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) ; openBlock - : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse - : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } + ; + +openInverseChain + : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; inverseAndProgram : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } ; +inverseChain + : openInverseChain program inverseChain? { + var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), + program = new yy.Program([inverse], null, {}, yy.locInfo(@$)); + program.chained = true; + + $$ = { strip: $1.strip, program: program, chain: true }; + } + | inverseAndProgram -> $1 + ; + closeBlock - : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} + : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} ; 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 helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) + | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) ; 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 partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) ; -sexpr - : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$) - | dataName -> new yy.SexprNode([$1], null, @$) +param + : helperName -> $1 + | sexpr -> $1 ; -param - : path -> $1 - | STRING -> new yy.StringNode($1, @$) - | NUMBER -> new yy.NumberNode($1, @$) - | BOOLEAN -> new yy.BooleanNode($1, @$) - | dataName -> $1 - | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} +sexpr + : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$)) ; 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 + ; + +helperName + : path -> $1 + | dataName -> $1 + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) + | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) ; partialName - : path -> new yy.PartialNameNode($1, @$) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) - | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$)) + : helperName -> $1 + | sexpr -> $1 ; 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 : pathSegments SEP ID { $1.push({part: $3, separator: $2}); $$ = $1; } | ID -> [{part: $1}] ; -