diff --git a/.eslintrc b/.eslintrc index 253a1bb29..237b5ee6d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "objectLiteralDuplicateProperties": true, "objectLiteralShorthandMethods": true, "objectLiteralShorthandProperties": true, + "restParams": true, "spread": true, "templateStrings": true }, @@ -44,7 +45,7 @@ "no-extra-boolean-cast": 2, "no-extra-parens": 0, "no-extra-semi": 2, - "no-func-assign": 2, + "no-func-assign": 0, // Stylistic... might consider disallowing in the future "no-inner-declarations": 0, @@ -128,7 +129,7 @@ "no-catch-shadow": 2, "no-delete-var": 2, "no-label-var": 2, - "no-shadow": 2, + "no-shadow": 0, "no-shadow-restricted-names": 2, "no-undef": 2, "no-undef-init": 2, diff --git a/.travis.yml b/.travis.yml index ecbc1a435..b9ecde273 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,3 +21,6 @@ matrix: cache: directories: - node_modules + +git: + depth: 100 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b84fdb005..79db3ba81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,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/26/ +[jsfiddle]: http://jsfiddle.net/9D88g/46/ diff --git a/Gruntfile.js b/Gruntfile.js index 70239bf8b..2850a9052 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,6 +9,8 @@ module.exports = function(grunt) { }, files: [ '*.js', + 'bench/**/*.js', + 'tasks/**/*.js', 'lib/**/!(*.min|parser).js', 'spec/**/!(*.amd|json2|require).js' ] @@ -43,7 +45,9 @@ module.exports = function(grunt) { babel: { options: { - loose: ['es6.modules'] + sourceMaps: 'inline', + loose: ['es6.modules'], + auxiliaryCommentBefore: 'istanbul ignore next' }, amd: { options: { @@ -75,7 +79,7 @@ module.exports = function(grunt) { module: { loaders: [ // the optional 'runtime' transformer tells babel to require the runtime instead of inlining it. - { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules' } + { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next' } ] }, output: { @@ -158,10 +162,10 @@ module.exports = function(grunt) { build: process.env.TRAVIS_JOB_ID, urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'], detailedError: true, - concurrency: 2, + concurrency: 4, browsers: [ {browserName: 'chrome'}, - {browserName: 'firefox'}, + {browserName: 'firefox', platform: 'Linux'}, {browserName: 'safari', version: 7, platform: 'OS X 10.9'}, {browserName: 'safari', version: 6, platform: 'OS X 10.8'}, {browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'}, diff --git a/LICENSE b/LICENSE index a2d22cbb4..4effa3916 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2011-2014 by Yehuda Katz +Copyright (C) 2011-2015 by Yehuda Katz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.markdown b/README.markdown index d6aa6bd5c..61c0bdcf9 100644 --- a/README.markdown +++ b/README.markdown @@ -10,24 +10,12 @@ Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be. Checkout the official Handlebars docs site at -[http://www.handlebarsjs.com](http://www.handlebarsjs.com). +[http://www.handlebarsjs.com](http://www.handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/). 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 -a `handlebars-latest.js` file from the [builds page][builds-page]. When the -build is published, it is also available as a `handlebars-gitSHA.js` file on -the builds page if you need a version to refer to others. -`handlebars-runtime.js` builds are also available. - -**Note**: The S3 builds page is provided as a convenience for the community, -but you should not use it for hosting Handlebars in production. +See our [installation documentation](http://handlebarsjs.com/installation.html). Usage ----- @@ -57,294 +45,33 @@ var result = template(data); // ``` +Full documentation and more examples are at [handlebarsjs.com](http://handlebarsjs.com/). -Registering Helpers -------------------- - -You can register helpers that Handlebars will use when evaluating your -template. Here's an example, which assumes that your objects have a URL -embedded in them, as well as the text for a link: - -```js -Handlebars.registerHelper('link_to', function() { - return new Handlebars.SafeString("" + Handlebars.Utils.escapeExpression(this.body) + ""); -}); - -var context = { posts: [{url: "/hello-world", body: "Hello World!"}] }; -var source = "" - -var template = Handlebars.compile(source); -template(context); - -// Would render: -// -// -``` - -Helpers take precedence over fields defined on the context. To access a field -that is masked by a helper, a path reference may be used. In the example above -a field named `link_to` on the `context` object would be referenced using: - -``` -{{./link_to}} -``` - -Escaping --------- - -By default, the `{{expression}}` syntax will escape its contents. This -helps to protect you against accidental XSS problems caused by malicious -data passed from the server as JSON. - -To explicitly *not* escape the contents, use the triple-mustache -(`{{{}}}`). You have seen this used in the above example. +Precompiling Templates +---------------------- +Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](http://handlebarsjs.com/precompilation.html). Differences Between Handlebars.js and Mustache ---------------------------------------------- Handlebars.js adds a couple of additional features to make writing 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 -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. - -To display data from descendant contexts, use the `.` character. So, for -example, if your data were structured like: - -```js -var data = {"person": { "name": "Alan" }, "company": {"name": "Rad, Inc." } }; -``` - -You could display the person's name from the top-level context with the -following expression: - -``` -{{person.name}} -``` - -You can backtrack using `../`. For example, if you've already traversed -into the person object you could still display the company's name with -an expression like `{{../company.name}}`, so: - -``` -{{#with person}}{{name}} - {{../company.name}}{{/with}} -``` - -would render: - -``` -Alan - Rad, Inc. -``` - -### Strings - -When calling a helper, you can pass paths or Strings as parameters. For -instance: - -```js -Handlebars.registerHelper('link_to', function(title, options) { - return "" + title + "!" -}); - -var context = { posts: [{url: "/hello-world", body: "Hello World!"}] }; -var source = '' - -var template = Handlebars.compile(source); -template(context); - -// Would render: -// -// -``` - -When you pass a String as a parameter to a helper, the literal String -gets passed to the helper function. - - -### Block Helpers - -Handlebars.js also adds the ability to define block helpers. Block -helpers are functions that can be called from anywhere in the template. -Here's an example: - -```js -var source = ""; -Handlebars.registerHelper('link', function(options) { - return '' + options.fn(this) + ''; -}); -var template = Handlebars.compile(source); - -var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; -template(data); - -// Should render: -// -``` - -Whenever the block helper is called it is given one or more parameters, -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`. - -Block helpers have the same syntax as mustache sections but should not be -confused with one another. Sections are akin to an implicit `each` or -`with` statement depending on the input data and helpers are explicit -pieces of code that are free to implement whatever behavior they like. -The [mustache spec](http://mustache.github.io/mustache.5.html) -defines the exact behavior of sections. In the case of name conflicts, -helpers are given priority. - -### Partials - -You can register additional templates as partials, which will be used by -Handlebars when it encounters a partial (`{{> partialName}}`). Partials -can either be String templates or compiled template functions. Here's an -example: - -```js -var source = ""; - -Handlebars.registerPartial('link', '{{name}}') -var template = Handlebars.compile(source); - -var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; - -template(data); - -// Should render: -// -``` - -Partials can also accept parameters - -```js -var source = "
{{> roster rosterProperties people=listOfPeople}}
"; - -Handlebars.registerPartial('roster', '

{{rosterName}}

{{#people}}{{id}}: {{name}}{{/people}}') -var template = Handlebars.compile(source); - -var data = { - "listOfPeople": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ], - "rosterProperties": { - "rosterName": "Cool People" - } -}; - -template(data); - -// Should render: -//
-//

Cool People

-// 1: Alan -// 2: Yehuda -//
- -``` - -### Comments - -You can add comments to your templates with the following syntax: - -```js -{{! This is a comment }} -``` - -You can also use real html comments if you want them to end up in the output. - -```html -
- {{! This comment will not end up in the output }} - -
-``` +- [Nested Paths](http://handlebarsjs.com/#paths) +- [Helpers](http://handlebarsjs.com/#helpers) +- [Block Expressions](http://handlebarsjs.com/#block-expressions) +- [Literal Values](http://handlebarsjs.com/#literals) +- [Delimited Comments](http://handlebarsjs.com/#comments) +Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](http://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority. ### Compatibility There are a few Mustache behaviors that Handlebars does not implement. - Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references. -- The optional Mustache-style lambdas are not supported. Instead Handlebars provides it's own lambda resolution that follows the behaviors of helpers. -- Alternative delimeters are not supported. - - -Precompiling Templates ----------------------- +- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers. +- Alternative delimiters are not supported. -Handlebars allows templates to be precompiled and included as javascript -code rather than the handlebars template allowing for faster startup time. - -### Installation -The precompiler script may be installed via npm using the `npm install -g handlebars` -command. - -### Usage - -
-Precompile handlebar templates.
-Usage: handlebars template...
-
-Options:
-  -a, --amd            Create an AMD format function (allows loading with RequireJS)          [boolean]
-  -f, --output         Output File                                                            [string]
-  -k, --known          Known helpers                                                          [string]
-  -o, --knownOnly      Known helpers only                                                     [boolean]
-  -m, --min            Minimize output                                                        [boolean]
-  -s, --simple         Output template function only.                                         [boolean]
-  -r, --root           Template root. Base value that will be stripped from template names.   [string]
-  -c, --commonjs       Exports CommonJS style, path to Handlebars module                      [string]
-  -h, --handlebarPath  Path to handlebar.js (only valid for amd-style)                        [string]
-  -n, --namespace      Template namespace                                                     [string]
-  -p, --partial        Compiling a partial template                                           [boolean]
-  -d, --data           Include data when compiling                                            [boolean]
-  -e, --extension      Template extension.                                                    [string]
-  -b, --bom            Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-
- -If using the precompiler's normal mode, the resulting templates will be -stored to the `Handlebars.templates` object using the relative template -name sans the extension. These templates may be executed in the same -manner as templates. - -If using the simple mode the precompiler will generate a single -javascript method. To execute this method it must be passed to -the `Handlebars.template` method and the resulting object may be used as normal. - -### Optimizations - -- Rather than using the full _handlebars.js_ library, implementations that - do not need to compile templates at runtime may include _handlebars.runtime.js_ - whose min+gzip size is approximately 1k. -- If a helper is known to exist in the target environment they may be defined - using the `--known name` argument may be used to optimize accesses to these - helpers for size and speed. -- When all helpers are known in advance the `--knownOnly` argument may be used - to optimize all block helper references. -- Implementations that do not use `@data` variables can improve performance of - iteration centric templates by specifying `{data: false}` in the compiler options. Supported Environments ---------------------- @@ -414,6 +141,7 @@ Handlebars in the Wild Handlebars.js with [jQuery](http://jquery.com/). * [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for handlebars projects. +* [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette. * [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins. * [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main @@ -433,6 +161,4 @@ License ------- Handlebars.js is released under the MIT license. -[bower-repo]: https://github.com/components/handlebars.js -[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/bucket-listing.html?sort=lastmod&sortdir=desc [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master diff --git a/bench/.eslintrc b/bench/.eslintrc new file mode 100644 index 000000000..e03f181ed --- /dev/null +++ b/bench/.eslintrc @@ -0,0 +1,14 @@ +{ + "globals": { + "require": true + }, + "rules": { + // Disabling for tests, for now. + "no-path-concat": 0, + + "no-var": 0, + "no-shadow": 0, + "handle-callback-err": 0, + "no-console": 0 + } +} \ No newline at end of file diff --git a/bench/dist-size.js b/bench/dist-size.js index 9e5fdc0f5..9176054d5 100644 --- a/bench/dist-size.js +++ b/bench/dist-size.js @@ -1,5 +1,4 @@ -var _ = require('underscore'), - async = require('async'), +var async = require('async'), fs = require('fs'), zlib = require('zlib'); diff --git a/bench/index.js b/bench/index.js index 462b046f5..3e357e54a 100644 --- a/bench/index.js +++ b/bench/index.js @@ -2,7 +2,7 @@ var fs = require('fs'); var metrics = fs.readdirSync(__dirname); metrics.forEach(function(metric) { - if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) { + if (metric === 'index.js' || !(/(.*)\.js$/.test(metric))) { return; } diff --git a/bench/templates/arguments.js b/bench/templates/arguments.js index 5480c8d8e..aaa034686 100644 --- a/bench/templates/arguments.js +++ b/bench/templates/arguments.js @@ -1,6 +1,6 @@ module.exports = { helpers: { - foo: function(options) { + foo: function() { return ''; } }, diff --git a/bench/templates/array-each.js b/bench/templates/array-each.js index f1eb1e8e9..50e1c02b8 100644 --- a/bench/templates/array-each.js +++ b/bench/templates/array-each.js @@ -1,7 +1,7 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#each names}}{{name}}{{/each}}", - dust: "{#names}{name}{/names}", - mustache: "{{#names}}{{name}}{{/names}}", - eco: "<% for item in @names: %><%= item.name %><% end %>" + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#each names}}{{name}}{{/each}}', + dust: '{#names}{name}{/names}', + mustache: '{{#names}}{{name}}{{/names}}', + eco: '<% for item in @names: %><%= item.name %><% end %>' }; diff --git a/bench/templates/array-mustache.js b/bench/templates/array-mustache.js index 908f805ea..220c6fe47 100644 --- a/bench/templates/array-mustache.js +++ b/bench/templates/array-mustache.js @@ -1,4 +1,4 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#names}}{{name}}{{/names}}" -} + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#names}}{{name}}{{/names}}' +}; diff --git a/bench/templates/complex.js b/bench/templates/complex.js index ddf361b59..feba874dd 100644 --- a/bench/templates/complex.js +++ b/bench/templates/complex.js @@ -3,13 +3,13 @@ var fs = require('fs'); module.exports = { context: { header: function() { - return "Colors"; + return 'Colors'; }, hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays items: [ - {name: "red", current: true, url: "#Red"}, - {name: "green", current: false, url: "#Green"}, - {name: "blue", current: false, url: "#Blue"} + {name: 'red', current: true, url: '#Red'}, + {name: 'green', current: false, url: '#Green'}, + {name: 'blue', current: false, url: '#Blue'} ] }, diff --git a/bench/templates/data.js b/bench/templates/data.js index f532decd5..be10e8399 100644 --- a/bench/templates/data.js +++ b/bench/templates/data.js @@ -1,4 +1,4 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}" -} + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, + handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}' +}; diff --git a/bench/templates/depth-1.js b/bench/templates/depth-1.js index 74809bca8..0f2576f43 100644 --- a/bench/templates/depth-1.js +++ b/bench/templates/depth-1.js @@ -1,6 +1,6 @@ module.exports = { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' }, - handlebars: "{{#each names}}{{../foo}}{{/each}}", - mustache: "{{#names}}{{foo}}{{/names}}", - eco: "<% for item in @names: %><%= @foo %><% end %>" + context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}], foo: 'bar' }, + handlebars: '{{#each names}}{{../foo}}{{/each}}', + mustache: '{{#names}}{{foo}}{{/names}}', + eco: '<% for item in @names: %><%= @foo %><% end %>' }; diff --git a/bench/templates/depth-2.js b/bench/templates/depth-2.js index 1d38baa4e..bff6ce850 100644 --- a/bench/templates/depth-2.js +++ b/bench/templates/depth-2.js @@ -1,6 +1,6 @@ module.exports = { - context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' }, - handlebars: "{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}", - mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}", - eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>" + context: { names: [{bat: 'foo', name: ['Moe']}, {bat: 'foo', name: ['Larry']}, {bat: 'foo', name: ['Curly']}, {bat: 'foo', name: ['Shemp']}], foo: 'bar' }, + handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}', + mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}', + eco: '<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>' }; diff --git a/bench/templates/index.js b/bench/templates/index.js index a718ea388..943f9cdfe 100644 --- a/bench/templates/index.js +++ b/bench/templates/index.js @@ -2,7 +2,7 @@ var fs = require('fs'); var templates = fs.readdirSync(__dirname); templates.forEach(function(template) { - if (template === 'index.js' || !/(.*)\.js$/.test(template)) { + if (template === 'index.js' || !(/(.*)\.js$/.test(template))) { return; } module.exports[RegExp.$1] = require('./' + RegExp.$1); diff --git a/bench/templates/object-mustache.js b/bench/templates/object-mustache.js index 52dbc26e3..41774b73a 100644 --- a/bench/templates/object-mustache.js +++ b/bench/templates/object-mustache.js @@ -1,4 +1,4 @@ module.exports = { - context: { person: { name: "Larry", age: 45 } }, - handlebars: "{{#person}}{{name}}{{age}}{{/person}}" + context: { person: { name: 'Larry', age: 45 } }, + handlebars: '{{#person}}{{name}}{{age}}{{/person}}' }; diff --git a/bench/templates/object.js b/bench/templates/object.js index fef127ada..084c070ad 100644 --- a/bench/templates/object.js +++ b/bench/templates/object.js @@ -1,7 +1,7 @@ module.exports = { - context: { person: { name: "Larry", age: 45 } }, - handlebars: "{{#with person}}{{name}}{{age}}{{/with}}", - dust: "{#person}{name}{age}{/person}", - eco: "<%= @person.name %><%= @person.age %>", - mustache: "{{#person}}{{name}}{{age}}{{/person}}" + context: { person: { name: 'Larry', age: 45 } }, + handlebars: '{{#with person}}{{name}}{{age}}{{/with}}', + dust: '{#person}{name}{age}{/person}', + eco: '<%= @person.name %><%= @person.age %>', + mustache: '{{#person}}{{name}}{{age}}{{/person}}' }; diff --git a/bench/templates/partial-recursion.js b/bench/templates/partial-recursion.js index 9d604fd02..b903553b5 100644 --- a/bench/templates/partial-recursion.js +++ b/bench/templates/partial-recursion.js @@ -1,10 +1,10 @@ module.exports = { context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, partials: { - mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" }, - handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" } + mustache: { recursion: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' }, + handlebars: { recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}' } }, - handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}", - dust: "{name}{#kids}{>recursion:./}{/kids}", - mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" + handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}', + dust: '{name}{#kids}{>recursion:./}{/kids}', + mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' }; diff --git a/bench/templates/partial.js b/bench/templates/partial.js index a6e663156..949e9c075 100644 --- a/bench/templates/partial.js +++ b/bench/templates/partial.js @@ -1,11 +1,11 @@ module.exports = { - context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] }, + context: { peeps: [{name: 'Moe', count: 15}, {name: 'Larry', count: 5}, {name: 'Curly', count: 1}] }, partials: { - mustache: { variables: "Hello {{name}}! You have {{count}} new messages." }, - handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." } + mustache: { variables: 'Hello {{name}}! You have {{count}} new messages.' }, + handlebars: { variables: 'Hello {{name}}! You have {{count}} new messages.' } }, - handlebars: "{{#each peeps}}{{>variables}}{{/each}}", - dust: "{#peeps}{>variables/}{/peeps}", - mustache: "{{#peeps}}{{>variables}}{{/peeps}}" + handlebars: '{{#each peeps}}{{>variables}}{{/each}}', + dust: '{#peeps}{>variables/}{/peeps}', + mustache: '{{#peeps}}{{>variables}}{{/peeps}}' }; diff --git a/bench/templates/paths.js b/bench/templates/paths.js index d84e06152..fed039d51 100644 --- a/bench/templates/paths.js +++ b/bench/templates/paths.js @@ -1,7 +1,7 @@ module.exports = { - context: { person: { name: {bar: {baz: "Larry"}}, age: 45 } }, - handlebars: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}", - dust: "{person.name.bar.baz}{person.age}{person.foo}{animal.age}", - eco: "<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>", - mustache: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}" + context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } }, + handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}', + dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}', + eco: '<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>', + mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}' }; diff --git a/bench/templates/string.js b/bench/templates/string.js index 335e37cf9..6b0e94a74 100644 --- a/bench/templates/string.js +++ b/bench/templates/string.js @@ -1,7 +1,7 @@ module.exports = { context: {}, - handlebars: "Hello world", - dust: "Hello world", - mustache: "Hello world", - eco: "Hello world" + handlebars: 'Hello world', + dust: 'Hello world', + mustache: 'Hello world', + eco: 'Hello world' }; diff --git a/bench/templates/subexpression.js b/bench/templates/subexpression.js index 261c22d01..659b53041 100644 --- a/bench/templates/subexpression.js +++ b/bench/templates/subexpression.js @@ -4,11 +4,11 @@ module.exports = { return 'foo ' + value; }, header: function() { - return "Colors"; + return 'Colors'; } }, - handlebars: "{{echo (header)}}", - eco: "<%= @echo(@header()) %>" + handlebars: '{{echo (header)}}', + eco: '<%= @echo(@header()) %>' }; module.exports.context = module.exports.helpers; diff --git a/bench/templates/variables.js b/bench/templates/variables.js index d354238b1..41e4feafe 100644 --- a/bench/templates/variables.js +++ b/bench/templates/variables.js @@ -1,8 +1,8 @@ module.exports = { - context: {name: "Mick", count: 30}, - handlebars: "Hello {{name}}! You have {{count}} new messages.", - dust: "Hello {name}! You have {count} new messages.", - mustache: "Hello {{name}}! You have {{count}} new messages.", - eco: "Hello <%= @name %>! You have <%= @count %> new messages." + context: {name: 'Mick', count: 30}, + handlebars: 'Hello {{name}}! You have {{count}} new messages.', + dust: 'Hello {name}! You have {count} new messages.', + mustache: 'Hello {{name}}! You have {{count}} new messages.', + eco: 'Hello <%= @name %>! You have <%= @count %> new messages.' }; diff --git a/bench/throughput.js b/bench/throughput.js index d27a94d31..9f1f79858 100644 --- a/bench/throughput.js +++ b/bench/throughput.js @@ -1,23 +1,22 @@ var _ = require('underscore'), runner = require('./util/template-runner'), - templates = require('./templates'), eco, dust, Handlebars, Mustache, eco; try { - dust = require("dustjs-linkedin"); + dust = require('dustjs-linkedin'); } catch (err) { /* NOP */ } try { - Mustache = require("mustache"); + Mustache = require('mustache'); } catch (err) { /* NOP */ } try { - eco = require("eco"); + eco = require('eco'); } catch (err) { /* NOP */ } function error() { - throw new Error("EWOT"); + throw new Error('EWOT'); } function makeSuite(bench, name, template, handlebarsOnly) { @@ -34,19 +33,19 @@ function makeSuite(bench, name, template, handlebarsOnly) { mustacheOut; var handlebar = Handlebars.compile(template.handlebars, {data: false}), - compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), + compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), options = {helpers: template.helpers}; - _.each(template.partials && template.partials.handlebars, function(partial, name) { - Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); + _.each(template.partials && template.partials.handlebars, function(partial, partialName) { + Handlebars.registerPartial(partialName, Handlebars.compile(partial, {data: false})); }); handlebarsOut = handlebar(context, options); - bench("handlebars", function() { + bench('handlebars', function() { handlebar(context, options); }); compatOut = compat(context, options); - bench("compat", function() { + bench('compat', function() { compat(context, options); }); @@ -61,8 +60,8 @@ function makeSuite(bench, name, template, handlebarsOnly) { dust.render(templateName, context, function(err, out) { dustOut = out; }); - bench("dust", function() { - dust.render(templateName, context, function(err, out) { }); + bench('dust', function() { + dust.render(templateName, context, function() {}); }); } else { bench('dust', error); @@ -75,11 +74,11 @@ function makeSuite(bench, name, template, handlebarsOnly) { ecoOut = ecoTemplate(context); - bench("eco", function() { + bench('eco', function() { ecoTemplate(context); }); } else { - bench("eco", error); + bench('eco', error); } } @@ -90,11 +89,11 @@ function makeSuite(bench, name, template, handlebarsOnly) { if (mustacheSource) { mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials); - bench("mustache", function() { + bench('mustache', function() { Mustache.to_html(mustacheSource, context, mustachePartials); }); } else { - bench("mustache", error); + bench('mustache', error); } } diff --git a/bench/util/benchwarmer.js b/bench/util/benchwarmer.js index 7496a3e9e..78b1a347e 100644 --- a/bench/util/benchwarmer.js +++ b/bench/util/benchwarmer.js @@ -1,7 +1,7 @@ var _ = require('underscore'), - Benchmark = require("benchmark"); + Benchmark = require('benchmark'); -var BenchWarmer = function(names) { +function BenchWarmer() { this.benchmarks = []; this.currentBenches = []; this.names = []; @@ -9,9 +9,9 @@ var BenchWarmer = function(names) { this.minimum = Infinity; this.maximum = -Infinity; this.errors = {}; -}; +} -var print = require("sys").print; +var print = require('sys').print; BenchWarmer.prototype = { winners: function(benches) { @@ -29,7 +29,7 @@ BenchWarmer.prototype = { }); }, push: function(name, fn) { - if(this.names.indexOf(name) == -1) { + if (this.names.indexOf(name) == -1) { this.names.push(name); } @@ -37,9 +37,9 @@ BenchWarmer.prototype = { this.first = false; var bench = new Benchmark(fn, { - name: this.suiteName + ": " + name, + name: this.suiteName + ': ' + name, onComplete: function() { - if(first) { self.startLine(suiteName); } + if (first) { self.startLine(suiteName); } self.writeBench(bench); self.currentBenches.push(bench); }, onError: function() { @@ -58,7 +58,7 @@ BenchWarmer.prototype = { this.printHeader('ops/msec', true); Benchmark.invoke(this.benchmarks, { - name: "run", + name: 'run', onComplete: function() { self.scaleTimes(); @@ -76,7 +76,7 @@ BenchWarmer.prototype = { print('\n'); var errors = false, prop, bench; - for(prop in self.errors) { + for (prop in self.errors) { if (self.errors.hasOwnProperty(prop) && self.errors[prop].error.message !== 'EWOT') { errors = true; @@ -84,18 +84,18 @@ BenchWarmer.prototype = { } } - if(errors) { - print("\n\nErrors:\n"); - for(prop in self.errors) { + if (errors) { + print('\n\nErrors:\n'); + for (prop in self.errors) { if (self.errors.hasOwnProperty(prop) && self.errors[prop].error.message !== 'EWOT') { bench = self.errors[prop]; - print("\n" + bench.name + ":\n"); + print('\n' + bench.name + ':\n'); print(bench.error.message); - if(bench.error.stack) { - print(bench.error.stack.join("\n")); + if (bench.error.stack) { + print(bench.error.stack.join('\n')); } - print("\n"); + print('\n'); } } } @@ -104,7 +104,7 @@ BenchWarmer.prototype = { } }); - print("\n"); + print('\n'); }, scaleTimes: function() { @@ -121,10 +121,10 @@ BenchWarmer.prototype = { printHeader: function(title, winners) { var benchSize = 0, names = this.names, i, l; - for(i=0, l=names.length; i handlebars.js - 3.0.3 + 4.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 index c09414f0b..29382191e 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -83,10 +83,23 @@ interface PartialStatement <: Statement { name: PathExpression | SubExpression; params: [ Expression ]; hash: Hash; - + indent: string; strip: StripFlags | null; } + +interface PartialBlockStatement <: Statement { + type: "PartialBlockStatement"; + name: PathExpression | SubExpression; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + + indent: string; + openStrip: StripFlags | null; + closeStrip: 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. @@ -107,6 +120,33 @@ interface CommentStatement <: Statement { } ``` + +```java +interface Decorator <: Statement { + type: "Decorator"; + + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + strip: StripFlags | null; +} + +interface DecoratorBlock <: Statement { + type: "DecoratorBlock"; + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + + openStrip: StripFlags | null; + closeStrip: StripFlags | null; +} +``` + +Decorator paths only utilize the `path.original` value and as a consequence do not support depthed evaluation. + ### Expressions ```java @@ -236,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c - `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`. + - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, 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. diff --git a/docs/decorators-api.md b/docs/decorators-api.md new file mode 100644 index 000000000..e14a33ff5 --- /dev/null +++ b/docs/decorators-api.md @@ -0,0 +1,19 @@ +# Decorators + +Decorators allow for blocks to be annotated with metadata or wrapped in functionality prior to execution of the block. This may be used to communicate with the containing helper or to setup a particular state in the system prior to running the block. + +Decorators are registered through similar methods as helpers, `registerDecorators` and `unregisterDecorators`. These can then be referenced via the friendly name in the template using the `{{* decorator}}` and `{{#* decorator}}{/decorator}}` syntaxes. These syntaxs are derivitives of the normal mustache syntax and as such have all of the same argument and whitespace behaviors. + +Decorators are executed when the block program is instantiated and are passed `(program, props, container, context, data, blockParams, depths)` + +- `program`: The block to wrap +- `props`: Object used to set metadata on the final function. Any values set on this object will be set on the function, regardless of if the original function is replaced or not. Metadata should be applied using this object as values applied to `program` may be masked by subsequent decorators that may wrap `program`. +- `container`: The current runtime container +- `context`: The current context. Since the decorator is run before the block that contains it, this is the parent context. +- `data`: The current `@data` values +- `blockParams`: The current block parameters stack +- `depths`: The current context stack + +Decorators may set values on `props` or return a modified function that wraps `program` in particular behaviors. If the decorator returns nothing, then `program` is left unaltered. + +The [inline partial](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors. diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index cfe1e917c..e68031ee6 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,8 +1,11 @@ -import * as Utils from './utils'; +import {createFrame, extend, toString} from './utils'; import Exception from './exception'; +import {registerDefaultHelpers} from './helpers'; +import {registerDefaultDecorators} from './decorators'; +import logger from './logger'; -export const VERSION = '3.0.1'; -export const COMPILER_REVISION = 6; +export const VERSION = '4.0.0'; +export const COMPILER_REVISION = 7; export const REVISION_CHANGES = { 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it @@ -10,31 +13,31 @@ export const REVISION_CHANGES = { 3: '== 1.0.0-rc.4', 4: '== 1.x.x', 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' + 6: '>= 2.0.0-beta.1', + 7: '>= 4.0.0' }; -const isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; +const objectType = '[object Object]'; -export function HandlebarsEnvironment(helpers, partials) { +export function HandlebarsEnvironment(helpers, partials, decorators) { this.helpers = helpers || {}; this.partials = partials || {}; + this.decorators = decorators || {}; registerDefaultHelpers(this); + registerDefaultDecorators(this); } HandlebarsEnvironment.prototype = { constructor: HandlebarsEnvironment, logger: logger, - log: log, + log: logger.log, registerHelper: function(name, fn) { if (toString.call(name) === objectType) { if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); + extend(this.helpers, name); } else { this.helpers[name] = fn; } @@ -45,7 +48,7 @@ HandlebarsEnvironment.prototype = { registerPartial: function(name, partial) { if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); + extend(this.partials, name); } else { if (typeof partial === 'undefined') { throw new Exception('Attempting to register a partial as undefined'); @@ -55,190 +58,21 @@ HandlebarsEnvironment.prototype = { }, unregisterPartial: function(name) { delete this.partials[name]; - } -}; - -function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if (arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"'); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - let inverse = options.inverse, - fn = options.fn; - - if (context === true) { - return fn(this); - } else if (context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if (context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - let data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - let fn = options.fn, - inverse = options.inverse, - i = 0, - ret = '', - data, - contextPath; - - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - function execIteration(field, index, last) { - if (data) { - data.key = field; - data.index = index; - data.first = index === 0; - data.last = !!last; - - if (contextPath) { - data.contextPath = contextPath + field; - } - } - - ret = ret + fn(context[field], { - data: data, - blockParams: Utils.blockParams([context[field], field], [contextPath + field, null]) - }); - } - - if (context && typeof context === 'object') { - if (isArray(context)) { - for (let j = context.length; i < j; i++) { - execIteration(i, i, i === context.length - 1); - } - } else { - let priorKey; - - for (let key in context) { - if (context.hasOwnProperty(key)) { - // We're running the iterations one step out of sync so we can detect - // the last iteration without have to scan the object twice and create - // an itermediate keys array. - if (priorKey) { - execIteration(priorKey, i - 1); - } - priorKey = key; - i++; - } - } - if (priorKey) { - execIteration(priorKey, i - 1, true); - } - } - } - - if (i === 0) { - ret = inverse(this); - } - - return ret; - }); - - instance.registerHelper('if', function(conditional, options) { - if (isFunction(conditional)) { conditional = conditional.call(this); } - - // Default behavior is to render the positive path if the value is truthy and not empty. - // The `includeZero` option may be set to treat the condtional as purely not empty based on the - // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. - if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) { - return options.inverse(this); - } else { - return options.fn(this); - } - }); - - instance.registerHelper('unless', function(conditional, options) { - return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); - }); - - instance.registerHelper('with', function(context, options) { - if (isFunction(context)) { context = context.call(this); } - - let fn = options.fn; - - if (!Utils.isEmpty(context)) { - if (options.data && options.ids) { - let data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]); - options = {data: data}; - } + }, - return fn(context, options); + registerDecorator: function(name, fn) { + if (toString.call(name) === objectType) { + if (fn) { throw new Exception('Arg not supported with multiple decorators'); } + extend(this.decorators, name); } else { - return options.inverse(this); - } - }); - - instance.registerHelper('log', function(message, options) { - let level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; - instance.log(level, message); - }); - - instance.registerHelper('lookup', function(obj, field) { - return obj && obj[field]; - }); -} - -export let logger = { - methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, - - // State enum - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - level: 1, - - // Can be overridden in the host environment - log: function(level, message) { - if (typeof console !== 'undefined' && logger.level <= level) { - let method = logger.methodMap[level]; - (console[method] || console.log).call(console, message); // eslint-disable-line no-console + this.decorators[name] = fn; } + }, + unregisterDecorator: function(name) { + delete this.decorators[name]; } }; export let log = logger.log; -export function createFrame(object) { - let frame = Utils.extend({}, object); - frame._parent = object; - return frame; -} +export {createFrame, logger}; diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 08b127f77..1699569ba 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,137 +1,13 @@ let AST = { - Program: function(statements, blockParams, strip, locInfo) { - this.loc = locInfo; - this.type = 'Program'; - this.body = statements; - - this.blockParams = blockParams; - this.strip = strip; - }, - - MustacheStatement: function(path, params, hash, escaped, strip, locInfo) { - this.loc = locInfo; - this.type = 'MustacheStatement'; - - this.path = path; - this.params = params || []; - this.hash = hash; - this.escaped = escaped; - - this.strip = strip; - }, - - BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { - this.loc = locInfo; - this.type = 'BlockStatement'; - - this.path = path; - this.params = params || []; - this.hash = hash; - this.program = program; - this.inverse = inverse; - - this.openStrip = openStrip; - this.inverseStrip = inverseStrip; - this.closeStrip = closeStrip; - }, - - PartialStatement: function(name, params, hash, strip, locInfo) { - this.loc = locInfo; - this.type = 'PartialStatement'; - - this.name = name; - this.params = params || []; - this.hash = hash; - - this.indent = ''; - this.strip = strip; - }, - - ContentStatement: function(string, locInfo) { - this.loc = locInfo; - this.type = 'ContentStatement'; - this.original = this.value = string; - }, - - CommentStatement: function(comment, strip, locInfo) { - this.loc = locInfo; - this.type = 'CommentStatement'; - this.value = comment; - - this.strip = strip; - }, - - SubExpression: function(path, params, hash, locInfo) { - this.loc = locInfo; - - this.type = 'SubExpression'; - this.path = path; - this.params = params || []; - this.hash = hash; - }, - - PathExpression: function(data, depth, parts, original, locInfo) { - this.loc = locInfo; - this.type = 'PathExpression'; - - this.data = data; - this.original = original; - this.parts = parts; - this.depth = depth; - }, - - StringLiteral: function(string, locInfo) { - this.loc = locInfo; - this.type = 'StringLiteral'; - this.original = - this.value = string; - }, - - NumberLiteral: function(number, locInfo) { - this.loc = locInfo; - this.type = 'NumberLiteral'; - this.original = - this.value = Number(number); - }, - - BooleanLiteral: function(bool, locInfo) { - this.loc = locInfo; - this.type = 'BooleanLiteral'; - this.original = - this.value = bool === 'true'; - }, - - UndefinedLiteral: function(locInfo) { - this.loc = locInfo; - this.type = 'UndefinedLiteral'; - this.original = this.value = undefined; - }, - - NullLiteral: function(locInfo) { - this.loc = locInfo; - this.type = 'NullLiteral'; - this.original = this.value = null; - }, - - 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; - }, - // 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 helperExpression: function(node) { - return !!(node.type === 'SubExpression' || node.params.length || node.hash); + return (node.type === 'SubExpression') + || ((node.type === 'MustacheStatement' || node.type === 'BlockStatement') + && !!((node.params && node.params.length) || node.hash)); }, scopedId: function(path) { diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 7075d9bd8..c6871d399 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,5 +1,4 @@ import parser from './parser'; -import AST from './ast'; import WhitespaceControl from './whitespace-control'; import * as Helpers from './helpers'; import { extend } from '../utils'; @@ -7,7 +6,7 @@ import { extend } from '../utils'; export { parser }; let yy = {}; -extend(yy, Helpers, AST); +extend(yy, Helpers); export function parse(input, options) { // Just return if an already-compiled AST was passed in. @@ -20,6 +19,6 @@ export function parse(input, options) { return new yy.SourceLocation(options && options.srcName, locInfo); }; - let strip = new WhitespaceControl(); + let strip = new WhitespaceControl(options); return strip.accept(parser.parse(input)); } diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index bc7bc0703..6541fe873 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -69,6 +69,9 @@ function CodeGen(srcFile) { } CodeGen.prototype = { + isEmpty() { + return !this.source.length; + }, prepend: function(source, loc) { this.source.unshift(this.wrap(source, loc)); }, @@ -90,7 +93,8 @@ CodeGen.prototype = { } }, - empty: function(loc = this.currentLocation || {start: {}}) { + empty: function() { + let loc = this.currentLocation || {start: {}}; return new SourceNode(loc.start.line, loc.start.column, this.srcFile); }, wrap: function(chunk, loc = this.currentLocation || {start: {}}) { @@ -137,22 +141,22 @@ CodeGen.prototype = { }, - generateList: function(entries, loc) { - let ret = this.empty(loc); + generateList: function(entries) { + let ret = this.empty(); for (let i = 0, len = entries.length; i < len; i++) { if (i) { ret.add(','); } - ret.add(castChunk(entries[i], this, loc)); + ret.add(castChunk(entries[i], this)); } return ret; }, - generateArray: function(entries, loc) { - let ret = this.generateList(entries, loc); + generateArray: function(entries) { + let ret = this.generateList(entries); ret.prepend('['); ret.add(']'); diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 457542162..987d0d459 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,3 +1,5 @@ +/* eslint-disable new-cap */ + import Exception from '../exception'; import {isArray, indexOf} from '../utils'; import AST from './ast'; @@ -66,6 +68,7 @@ Compiler.prototype = { }; if (knownHelpers) { for (let name in knownHelpers) { + /* istanbul ignore else */ if (name in knownHelpers) { options.knownHelpers[name] = knownHelpers[name]; } @@ -89,6 +92,11 @@ Compiler.prototype = { }, accept: function(node) { + /* istanbul ignore next: Sanity code */ + if (!this[node.type]) { + throw new Exception('Unknown type: ' + node.type, node); + } + this.sourceNode.unshift(node); let ret = this[node.type](node); this.sourceNode.shift(); @@ -148,14 +156,32 @@ Compiler.prototype = { this.opcode('append'); }, + DecoratorBlock(decorator) { + let program = decorator.program && this.compileProgram(decorator.program); + let params = this.setupFullMustacheParams(decorator, program, undefined), + path = decorator.path; + + this.useDecorators = true; + this.opcode('registerDecorator', params.length, path.original); + }, + PartialStatement: function(partial) { this.usePartial = true; + let program = partial.program; + if (program) { + program = this.compileProgram(partial.program); + } + let params = partial.params; if (params.length > 1) { throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); } else if (!params.length) { - params.push({type: 'PathExpression', parts: [], depth: 0}); + if (this.options.explicitPartialContext) { + this.opcode('pushLiteral', 'undefined'); + } else { + params.push({type: 'PathExpression', parts: [], depth: 0}); + } } let partialName = partial.name.original, @@ -164,7 +190,7 @@ Compiler.prototype = { this.accept(partial.name); } - this.setupFullMustacheParams(partial, undefined, undefined, true); + this.setupFullMustacheParams(partial, program, undefined, true); let indent = partial.indent || ''; if (this.options.preventIndent && indent) { @@ -175,9 +201,12 @@ Compiler.prototype = { this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('append'); }, + PartialBlockStatement: function(partialBlock) { + this.PartialStatement(partialBlock); + }, MustacheStatement: function(mustache) { - this.SubExpression(mustache); // eslint-disable-line new-cap + this.SubExpression(mustache); if (mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped'); @@ -185,6 +214,10 @@ Compiler.prototype = { this.opcode('append'); } }, + Decorator(decorator) { + this.DecoratorBlock(decorator); + }, + ContentStatement: function(content) { if (content.value) { @@ -216,13 +249,16 @@ Compiler.prototype = { this.opcode('pushProgram', program); this.opcode('pushProgram', inverse); + path.strict = true; this.accept(path); this.opcode('invokeAmbiguous', name, isBlock); }, simpleSexpr: function(sexpr) { - this.accept(sexpr.path); + let path = sexpr.path; + path.strict = true; + this.accept(path); this.opcode('resolvePossibleLambda'); }, @@ -236,6 +272,7 @@ Compiler.prototype = { } else if (this.options.knownHelpersOnly) { throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); } else { + path.strict = true; path.falsy = true; this.accept(path); @@ -258,9 +295,9 @@ Compiler.prototype = { this.opcode('pushContext'); } else if (path.data) { this.options.data = true; - this.opcode('lookupData', path.depth, path.parts); + this.opcode('lookupData', path.depth, path.parts, path.strict); } else { - this.opcode('lookupOnContext', path.parts, path.falsy, scoped); + this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped); } }, @@ -389,8 +426,9 @@ Compiler.prototype = { value = val.original || value; if (value.replace) { value = value - .replace(/^\.\//g, '') - .replace(/^\.$/g, ''); + .replace(/^this(?:\.|$)/, '') + .replace(/^\.\//, '') + .replace(/^\.$/, ''); } this.opcode('pushId', val.type, value); @@ -508,6 +546,13 @@ function transformLiteralToPath(sexpr) { let 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.loc); + sexpr.path = { + type: 'PathExpression', + data: false, + depth: 0, + parts: [literal.original + ''], + original: literal.original + '', + loc: literal.loc + }; } } diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index fc0120c4f..e09a08df9 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,5 +1,15 @@ import Exception from '../exception'; +function validateClose(open, close) { + close = close.path ? close.path.original : close; + + if (open.path.original !== close) { + let errorNode = {loc: open.path.loc}; + + throw new Exception(open.path.original + " doesn't match " + close, errorNode); + } +} + export function SourceLocation(source, locInfo) { this.source = source; this.start = { @@ -32,8 +42,8 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } -export function preparePath(data, parts, locInfo) { - locInfo = this.locInfo(locInfo); +export function preparePath(data, parts, loc) { + loc = this.locInfo(loc); let original = data ? '@' : '', dig = [], @@ -49,7 +59,7 @@ export function preparePath(data, parts, locInfo) { if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { if (dig.length > 0) { - throw new Exception('Invalid path: ' + original, {loc: locInfo}); + throw new Exception('Invalid path: ' + original, {loc}); } else if (part === '..') { depth++; depthString += '../'; @@ -59,7 +69,14 @@ export function preparePath(data, parts, locInfo) { } } - return new this.PathExpression(data, depth, dig, original, locInfo); + return { + type: 'PathExpression', + data, + depth, + parts: dig, + original, + loc + }; } export function prepareMustache(path, params, hash, open, strip, locInfo) { @@ -67,40 +84,59 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { let escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; - return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo)); + let decorator = (/\*/.test(open)); + return { + type: decorator ? 'Decorator' : 'MustacheStatement', + path, + params, + hash, + escaped, + strip, + loc: this.locInfo(locInfo) + }; } -export function prepareRawBlock(openRawBlock, content, close, locInfo) { - if (openRawBlock.path.original !== close) { - let errorNode = {loc: openRawBlock.path.loc}; - - throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); - } +export function prepareRawBlock(openRawBlock, contents, close, locInfo) { + validateClose(openRawBlock, close); locInfo = this.locInfo(locInfo); - let program = new this.Program([content], null, {}, locInfo); + let program = { + type: 'Program', + body: contents, + strip: {}, + loc: locInfo + }; - return new this.BlockStatement( - openRawBlock.path, openRawBlock.params, openRawBlock.hash, - program, undefined, - {}, {}, {}, - locInfo); + return { + type: 'BlockStatement', + path: openRawBlock.path, + params: openRawBlock.params, + hash: openRawBlock.hash, + program, + openStrip: {}, + inverseStrip: {}, + closeStrip: {}, + loc: locInfo + }; } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { - // When we are chaining inverse calls, we will not have a close path - if (close && close.path && openBlock.path.original !== close.path.original) { - let errorNode = {loc: openBlock.path.loc}; - - throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); + if (close && close.path) { + validateClose(openBlock, close); } + let decorator = (/\*/.test(openBlock.open)); + program.blockParams = openBlock.blockParams; let inverse, inverseStrip; if (inverseAndProgram) { + if (decorator) { + throw new Exception('Unexpected inverse block on decorator', inverseAndProgram); + } + if (inverseAndProgram.chain) { inverseAndProgram.program.body[0].closeStrip = close.strip; } @@ -115,9 +151,62 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver program = inverted; } - return new this.BlockStatement( - openBlock.path, openBlock.params, openBlock.hash, - program, inverse, - openBlock.strip, inverseStrip, close && close.strip, - this.locInfo(locInfo)); + return { + type: decorator ? 'DecoratorBlock' : 'BlockStatement', + path: openBlock.path, + params: openBlock.params, + hash: openBlock.hash, + program, + inverse, + openStrip: openBlock.strip, + inverseStrip, + closeStrip: close && close.strip, + loc: this.locInfo(locInfo) + }; } + +export function prepareProgram(statements, loc) { + if (!loc && statements.length) { + const firstLoc = statements[0].loc, + lastLoc = statements[statements.length - 1].loc; + + /* istanbul ignore else */ + if (firstLoc && lastLoc) { + loc = { + source: firstLoc.source, + start: { + line: firstLoc.start.line, + column: firstLoc.start.column + }, + end: { + line: lastLoc.end.line, + column: lastLoc.end.column + } + }; + } + } + + return { + type: 'Program', + body: statements, + strip: {}, + loc: loc + }; +} + + +export function preparePartialBlock(open, program, close, locInfo) { + validateClose(open, close); + + return { + type: 'PartialBlockStatement', + name: open.path, + params: open.params, + hash: open.hash, + program, + openStrip: open.strip, + closeStrip: close && close.strip, + loc: this.locInfo(locInfo) + }; +} + diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 883066165..ede0b5e49 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -16,11 +16,11 @@ JavaScriptCompiler.prototype = { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { return [parent, '.', name]; } else { - return [parent, "['", name, "']"]; + return [parent, '[', JSON.stringify(name), ']']; } }, depthedLookup: function(name) { - return [this.aliasable('this.lookup'), '(depths, "', name, '")']; + return [this.aliasable('container.lookup'), '(depths, "', name, '")']; }, compilerInfo: function() { @@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = { this.name = this.environment.name; this.isChild = !!context; this.context = context || { + decorators: [], programs: [], environments: [] }; @@ -81,7 +82,7 @@ JavaScriptCompiler.prototype = { this.compileChildren(environment, options); - this.useDepths = this.useDepths || environment.useDepths || this.options.compat; + this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; let opcodes = environment.opcodes, @@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = { throw new Exception('Compile completed with content left on stack'); } + if (!this.decorators.isEmpty()) { + this.useDecorators = true; + + this.decorators.prepend('var decorators = container.decorators;\n'); + this.decorators.push('return fn;'); + + if (asObject) { + this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]); + } else { + this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n'); + this.decorators.push('}\n'); + this.decorators = this.decorators.merge(); + } + } else { + this.decorators = undefined; + } + let fn = this.createFunctionContext(asObject); if (!this.isChild) { let ret = { compiler: this.compilerInfo(), main: fn }; - let programs = this.context.programs; + + if (this.decorators) { + ret.main_d = this.decorators; // eslint-disable-line camelcase + ret.useDecorators = true; + } + + let {programs, decorators} = this.context; for (i = 0, l = programs.length; i < l; i++) { if (programs[i]) { ret[i] = programs[i]; + if (decorators[i]) { + ret[i + '_d'] = decorators[i]; + ret.useDecorators = true; + } } } @@ -163,6 +191,7 @@ JavaScriptCompiler.prototype = { // getContext opcode when it would be a noop this.lastContext = 0; this.source = new CodeGen(this.options.srcName); + this.decorators = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -189,7 +218,7 @@ JavaScriptCompiler.prototype = { } } - let params = ['depth0', 'helpers', 'partials', 'data']; + let params = ['container', 'depth0', 'helpers', 'partials', 'data']; if (this.useBlockParams || this.useDepths) { params.push('blockParams'); @@ -359,7 +388,7 @@ JavaScriptCompiler.prototype = { // Escape `value` and append it to the buffer appendEscaped: function() { this.pushSource(this.appendToBuffer( - [this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); + [this.aliasable('container.escapeExpression'), '(', this.popStack(), ')'])); }, // [getContext] @@ -390,7 +419,7 @@ JavaScriptCompiler.prototype = { // // Looks up the value of `name` on the current context and pushes // it onto the stack. - lookupOnContext: function(parts, falsy, scoped) { + lookupOnContext: function(parts, falsy, strict, scoped) { let i = 0; if (!scoped && this.options.compat && !this.lastContext) { @@ -401,7 +430,7 @@ JavaScriptCompiler.prototype = { this.pushContext(); } - this.resolvePath('context', parts, i, falsy); + this.resolvePath('context', parts, i, falsy, strict); }, // [lookupBlockParam] @@ -424,19 +453,19 @@ JavaScriptCompiler.prototype = { // On stack, after: data, ... // // Push the data lookup operator - lookupData: function(depth, parts) { + lookupData: function(depth, parts, strict) { if (!depth) { this.pushStackLiteral('data'); } else { - this.pushStackLiteral('this.data(data, ' + depth + ')'); + this.pushStackLiteral('container.data(data, ' + depth + ')'); } - this.resolvePath('data', parts, 0, true); + this.resolvePath('data', parts, 0, true, strict); }, - resolvePath: function(type, parts, i, falsy) { + resolvePath: function(type, parts, i, falsy, strict) { if (this.options.strict || this.options.assumeObjects) { - this.push(strictLookup(this.options.strict, this, parts, type)); + this.push(strictLookup(this.options.strict && strict, this, parts, type)); return; } @@ -466,7 +495,7 @@ JavaScriptCompiler.prototype = { // If the `value` is a lambda, replace it on the stack by // the return value of the lambda resolvePossibleLambda: function() { - this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); + this.push([this.aliasable('container.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); }, // [pushStringParam] @@ -561,6 +590,24 @@ JavaScriptCompiler.prototype = { } }, + // [registerDecorator] + // + // On stack, before: hash, program, params..., ... + // On stack, after: ... + // + // Pops off the decorator's parameters, invokes the decorator, + // and inserts the decorator into the decorators list. + registerDecorator(paramSize, name) { + let foundDecorator = this.nameLookup('decorators', name, 'decorator'), + options = this.setupHelperArgs(name, paramSize); + + this.decorators.push([ + 'fn = ', + this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]), + ' || fn;' + ]); + }, + // [invokeHelper] // // On stack, before: hash, inverse, program, params..., ... @@ -644,7 +691,7 @@ JavaScriptCompiler.prototype = { // and pushes the result of the invocation back. invokePartial: function(isDynamic, name, indent) { let params = [], - options = this.setupParams(name, 1, params, false); + options = this.setupParams(name, 1, params); if (isDynamic) { name = this.popStack(); @@ -669,7 +716,7 @@ JavaScriptCompiler.prototype = { options = this.objectLiteral(options); params.push(options); - this.push(this.source.functionCall('this.invokePartial', '', params)); + this.push(this.source.functionCall('container.invokePartial', '', params)); }, // [assignToHash] @@ -738,6 +785,7 @@ JavaScriptCompiler.prototype = { child.index = index; child.name = 'program' + index; this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); + this.context.decorators[index] = compiler.decorators; this.context.environments[index] = child; this.useDepths = this.useDepths || compiler.useDepths; @@ -771,7 +819,7 @@ JavaScriptCompiler.prototype = { programParams.push('depths'); } - return 'this.program(' + programParams.join(', ') + ')'; + return 'container.program(' + programParams.join(', ') + ')'; }, useRegister: function(name) { @@ -946,7 +994,16 @@ JavaScriptCompiler.prototype = { }, setupParams: function(helper, paramSize, params) { - let options = {}, contexts = [], types = [], ids = [], param; + let options = {}, + contexts = [], + types = [], + ids = [], + objectArgs = !params, + param; + + if (objectArgs) { + params = []; + } options.name = this.quotedString(helper); options.hash = this.popStack(); @@ -965,8 +1022,8 @@ JavaScriptCompiler.prototype = { // Avoid setting fn and inverse if neither are set. This allows // helpers to do a check for `if (options.fn)` if (program || inverse) { - options.fn = program || 'this.noop'; - options.inverse = inverse || 'this.noop'; + options.fn = program || 'container.noop'; + options.inverse = inverse || 'container.noop'; } // The parameters go on to the stack in order (making sure that they are evaluated in order) @@ -985,6 +1042,10 @@ JavaScriptCompiler.prototype = { } } + if (objectArgs) { + options.args = this.source.generateArray(params); + } + if (this.trackIds) { options.ids = this.source.generateArray(ids); } @@ -1003,15 +1064,17 @@ JavaScriptCompiler.prototype = { }, setupHelperArgs: function(helper, paramSize, params, useRegister) { - let options = this.setupParams(helper, paramSize, params, true); + let options = this.setupParams(helper, paramSize, params); options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); params.push('options'); return ['options=', options]; - } else { + } else if (params) { params.push(options); return ''; + } else { + return options; } } }; @@ -1061,7 +1124,7 @@ function strictLookup(requireTerminal, compiler, parts, type) { } if (requireTerminal) { - return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; + return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; } else { return stack; } diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 691a3567c..66e7c7d4b 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -15,10 +15,10 @@ PrintVisitor.prototype.pad = function(string) { let out = ''; for (let i = 0, l = this.padding; i < l; i++) { - out = out + ' '; + out += ' '; } - out = out + string + '\n'; + out += string + '\n'; return out; }; @@ -37,7 +37,7 @@ PrintVisitor.prototype.Program = function(program) { } for (i = 0, l = body.length; i < l; i++) { - out = out + this.accept(body[i]); + out += this.accept(body[i]); } this.padding--; @@ -48,24 +48,28 @@ PrintVisitor.prototype.Program = function(program) { PrintVisitor.prototype.MustacheStatement = function(mustache) { return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; +PrintVisitor.prototype.Decorator = function(mustache) { + return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}'); +}; -PrintVisitor.prototype.BlockStatement = function(block) { +PrintVisitor.prototype.BlockStatement = +PrintVisitor.prototype.DecoratorBlock = function(block) { let out = ''; - out = out + this.pad('BLOCK:'); + out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'); this.padding++; - out = out + this.pad(this.SubExpression(block)); + out += this.pad(this.SubExpression(block)); if (block.program) { - out = out + this.pad('PROGRAM:'); + out += this.pad('PROGRAM:'); this.padding++; - out = out + this.accept(block.program); + out += this.accept(block.program); this.padding--; } if (block.inverse) { if (block.program) { this.padding++; } - out = out + this.pad('{{^}}'); + out += this.pad('{{^}}'); this.padding++; - out = out + this.accept(block.inverse); + out += this.accept(block.inverse); this.padding--; if (block.program) { this.padding--; } } @@ -84,6 +88,22 @@ PrintVisitor.prototype.PartialStatement = function(partial) { } return this.pad('{{> ' + content + ' }}'); }; +PrintVisitor.prototype.PartialBlockStatement = function(partial) { + let content = 'PARTIAL BLOCK:' + partial.name.original; + if (partial.params[0]) { + content += ' ' + this.accept(partial.params[0]); + } + if (partial.hash) { + content += ' ' + this.accept(partial.hash); + } + + content += ' ' + this.pad('PROGRAM:'); + this.padding++; + content += this.accept(partial.program); + this.padding--; + + return this.pad('{{> ' + content + ' }}'); +}; PrintVisitor.prototype.ContentStatement = function(content) { return this.pad("CONTENT[ '" + content.value + "' ]"); diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index ba7b3760c..2c504d1b2 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,5 +1,4 @@ import Exception from '../exception'; -import AST from './ast'; function Visitor() { this.parents = []; @@ -13,8 +12,9 @@ Visitor.prototype = { acceptKey: function(node, name) { let value = this.accept(node[name]); if (this.mutating) { - // Hacky sanity check: - if (value && (!value.type || !AST[value.type])) { + // Hacky sanity check: This may have a few false positives for type for the helper + // methods but will generally do the right thing without a lot of overhead. + if (value && !Visitor.prototype[value.type]) { throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); } node[name] = value; @@ -50,6 +50,11 @@ Visitor.prototype = { return; } + /* istanbul ignore next: Sanity code */ + if (!this[object.type]) { + throw new Exception('Unknown type: ' + object.type, object); + } + if (this.current) { this.parents.unshift(this.current); } @@ -70,35 +75,23 @@ Visitor.prototype = { this.acceptArray(program.body); }, - MustacheStatement: function(mustache) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params); - this.acceptKey(mustache, 'hash'); - }, + MustacheStatement: visitSubExpression, + Decorator: visitSubExpression, - BlockStatement: function(block) { - this.acceptRequired(block, 'path'); - this.acceptArray(block.params); - this.acceptKey(block, 'hash'); + BlockStatement: visitBlock, + DecoratorBlock: visitBlock, - this.acceptKey(block, 'program'); - this.acceptKey(block, 'inverse'); - }, + PartialStatement: visitPartial, + PartialBlockStatement: function(partial) { + visitPartial.call(this, partial); - PartialStatement: function(partial) { - this.acceptRequired(partial, 'name'); - this.acceptArray(partial.params); - this.acceptKey(partial, 'hash'); + this.acceptKey(partial, 'program'); }, ContentStatement: function(/* content */) {}, CommentStatement: function(/* comment */) {}, - SubExpression: function(sexpr) { - this.acceptRequired(sexpr, 'path'); - this.acceptArray(sexpr.params); - this.acceptKey(sexpr, 'hash'); - }, + SubExpression: visitSubExpression, PathExpression: function(/* path */) {}, @@ -116,4 +109,21 @@ Visitor.prototype = { } }; +function visitSubExpression(mustache) { + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params); + this.acceptKey(mustache, 'hash'); +} +function visitBlock(block) { + visitSubExpression.call(this, block); + + this.acceptKey(block, 'program'); + this.acceptKey(block, 'inverse'); +} +function visitPartial(partial) { + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params); + this.acceptKey(partial, 'hash'); +} + export default Visitor; diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index 5b76944dc..e11483c91 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -1,10 +1,13 @@ import Visitor from './visitor'; -function WhitespaceControl() { +function WhitespaceControl(options = {}) { + this.options = options; } WhitespaceControl.prototype = new Visitor(); WhitespaceControl.prototype.Program = function(program) { + const doStandalone = !this.options.ignoreStandalone; + let isRoot = !this.isRootSeen; this.isRootSeen = true; @@ -31,7 +34,7 @@ WhitespaceControl.prototype.Program = function(program) { omitLeft(body, i, true); } - if (inlineStandalone) { + if (doStandalone && inlineStandalone) { omitRight(body, i); if (omitLeft(body, i)) { @@ -42,13 +45,13 @@ WhitespaceControl.prototype.Program = function(program) { } } } - if (openStandalone) { + if (doStandalone && openStandalone) { omitRight((current.program || current.inverse).body); // Strip out the previous content node if it's whitespace only omitLeft(body, i); } - if (closeStandalone) { + if (doStandalone && closeStandalone) { // Always strip the next node omitRight(body, i); @@ -58,7 +61,10 @@ WhitespaceControl.prototype.Program = function(program) { return program; }; -WhitespaceControl.prototype.BlockStatement = function(block) { + +WhitespaceControl.prototype.BlockStatement = +WhitespaceControl.prototype.DecoratorBlock = +WhitespaceControl.prototype.PartialBlockStatement = function(block) { this.accept(block.program); this.accept(block.inverse); @@ -106,7 +112,8 @@ WhitespaceControl.prototype.BlockStatement = function(block) { } // Find standalone else statments - if (isPrevWhitespace(program.body) + if (!this.options.ignoreStandalone + && isPrevWhitespace(program.body) && isNextWhitespace(firstInverse.body)) { omitLeft(program.body); omitRight(firstInverse.body); @@ -118,6 +125,7 @@ WhitespaceControl.prototype.BlockStatement = function(block) { return strip; }; +WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(mustache) { return mustache.strip; }; diff --git a/lib/handlebars/decorators.js b/lib/handlebars/decorators.js new file mode 100644 index 000000000..6f5a61525 --- /dev/null +++ b/lib/handlebars/decorators.js @@ -0,0 +1,6 @@ +import registerInline from './decorators/inline'; + +export function registerDefaultDecorators(instance) { + registerInline(instance); +} + diff --git a/lib/handlebars/decorators/inline.js b/lib/handlebars/decorators/inline.js new file mode 100644 index 000000000..214246620 --- /dev/null +++ b/lib/handlebars/decorators/inline.js @@ -0,0 +1,22 @@ +import {extend} from '../utils'; + +export default function(instance) { + instance.registerDecorator('inline', function(fn, props, container, options) { + let ret = fn; + if (!props.partials) { + props.partials = {}; + ret = function(context, options) { + // Create a new partials stack frame prior to exec. + let original = container.partials; + container.partials = extend({}, original, props.partials); + let ret = fn(context, options); + container.partials = original; + return ret; + }; + } + + props.partials[options.args[0]] = options.fn; + + return ret; + }); +} diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js index 46ce18eae..52499c0ca 100644 --- a/lib/handlebars/exception.js +++ b/lib/handlebars/exception.js @@ -19,6 +19,7 @@ function Exception(message, node) { this[errorProps[idx]] = tmp[errorProps[idx]]; } + /* istanbul ignore else */ if (Error.captureStackTrace) { Error.captureStackTrace(this, Exception); } diff --git a/lib/handlebars/helpers.js b/lib/handlebars/helpers.js new file mode 100644 index 000000000..7a4365aea --- /dev/null +++ b/lib/handlebars/helpers.js @@ -0,0 +1,17 @@ +import registerBlockHelperMissing from './helpers/block-helper-missing'; +import registerEach from './helpers/each'; +import registerHelperMissing from './helpers/helper-missing'; +import registerIf from './helpers/if'; +import registerLog from './helpers/log'; +import registerLookup from './helpers/lookup'; +import registerWith from './helpers/with'; + +export function registerDefaultHelpers(instance) { + registerBlockHelperMissing(instance); + registerEach(instance); + registerHelperMissing(instance); + registerIf(instance); + registerLog(instance); + registerLookup(instance); + registerWith(instance); +} diff --git a/lib/handlebars/helpers/block-helper-missing.js b/lib/handlebars/helpers/block-helper-missing.js new file mode 100644 index 000000000..6639ddb9d --- /dev/null +++ b/lib/handlebars/helpers/block-helper-missing.js @@ -0,0 +1,32 @@ +import {appendContextPath, createFrame, isArray} from '../utils'; + +export default function(instance) { + instance.registerHelper('blockHelperMissing', function(context, options) { + let inverse = options.inverse, + fn = options.fn; + + if (context === true) { + return fn(this); + } else if (context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if (context.length > 0) { + if (options.ids) { + options.ids = [options.name]; + } + + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + if (options.data && options.ids) { + let data = createFrame(options.data); + data.contextPath = appendContextPath(options.data.contextPath, options.name); + options = {data: data}; + } + + return fn(context, options); + } + }); +} diff --git a/lib/handlebars/helpers/each.js b/lib/handlebars/helpers/each.js new file mode 100644 index 000000000..9b1629b40 --- /dev/null +++ b/lib/handlebars/helpers/each.js @@ -0,0 +1,83 @@ +import {appendContextPath, blockParams, createFrame, isArray, isFunction} from '../utils'; +import Exception from '../exception'; + +export default function(instance) { + instance.registerHelper('each', function(context, options) { + if (!options) { + throw new Exception('Must pass iterator to #each'); + } + + let fn = options.fn, + inverse = options.inverse, + i = 0, + ret = '', + data, + contextPath; + + if (options.data && options.ids) { + contextPath = appendContextPath(options.data.contextPath, options.ids[0]) + '.'; + } + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + function execIteration(field, index, last) { + // Don't iterate over undefined values since we can't execute blocks against them + // in non-strict (js) mode. + if (context[field] == null) { + return; + } + + if (data) { + data.key = field; + data.index = index; + data.first = index === 0; + data.last = !!last; + + if (contextPath) { + data.contextPath = contextPath + field; + } + } + + ret = ret + fn(context[field], { + data: data, + blockParams: blockParams([context[field], field], [contextPath + field, null]) + }); + } + + if (context && typeof context === 'object') { + if (isArray(context)) { + for (let j = context.length; i < j; i++) { + execIteration(i, i, i === context.length - 1); + } + } else { + let priorKey; + + for (let key in context) { + if (context.hasOwnProperty(key)) { + // We're running the iterations one step out of sync so we can detect + // the last iteration without have to scan the object twice and create + // an itermediate keys array. + if (priorKey !== undefined) { + execIteration(priorKey, i - 1); + } + priorKey = key; + i++; + } + } + if (priorKey !== undefined) { + execIteration(priorKey, i - 1, true); + } + } + } + + if (i === 0) { + ret = inverse(this); + } + + return ret; + }); +} diff --git a/lib/handlebars/helpers/helper-missing.js b/lib/handlebars/helpers/helper-missing.js new file mode 100644 index 000000000..ec32e8245 --- /dev/null +++ b/lib/handlebars/helpers/helper-missing.js @@ -0,0 +1,13 @@ +import Exception from '../exception'; + +export default function(instance) { + instance.registerHelper('helperMissing', function(/* [args, ]options */) { + if (arguments.length === 1) { + // A missing field in a {{foo}} construct. + return undefined; + } else { + // Someone is actually trying to call something, blow up. + throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"'); + } + }); +} diff --git a/lib/handlebars/helpers/if.js b/lib/handlebars/helpers/if.js new file mode 100644 index 000000000..11d08df91 --- /dev/null +++ b/lib/handlebars/helpers/if.js @@ -0,0 +1,20 @@ +import {isEmpty, isFunction} from '../utils'; + +export default function(instance) { + instance.registerHelper('if', function(conditional, options) { + if (isFunction(conditional)) { conditional = conditional.call(this); } + + // Default behavior is to render the positive path if the value is truthy and not empty. + // The `includeZero` option may be set to treat the condtional as purely not empty based on the + // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. + if ((!options.hash.includeZero && !conditional) || isEmpty(conditional)) { + return options.inverse(this); + } else { + return options.fn(this); + } + }); + + instance.registerHelper('unless', function(conditional, options) { + return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); + }); +} diff --git a/lib/handlebars/helpers/log.js b/lib/handlebars/helpers/log.js new file mode 100644 index 000000000..4bde4a10d --- /dev/null +++ b/lib/handlebars/helpers/log.js @@ -0,0 +1,19 @@ +export default function(instance) { + instance.registerHelper('log', function(/* message, options */) { + let args = [undefined], + options = arguments[arguments.length - 1]; + for (let i = 0; i < arguments.length - 1; i++) { + args.push(arguments[i]); + } + + let level = 1; + if (options.hash.level != null) { + level = options.hash.level; + } else if (options.data && options.data.level != null) { + level = options.data.level; + } + args[0] = level; + + instance.log(... args); + }); +} diff --git a/lib/handlebars/helpers/lookup.js b/lib/handlebars/helpers/lookup.js new file mode 100644 index 000000000..a52e77a04 --- /dev/null +++ b/lib/handlebars/helpers/lookup.js @@ -0,0 +1,5 @@ +export default function(instance) { + instance.registerHelper('lookup', function(obj, field) { + return obj && obj[field]; + }); +} diff --git a/lib/handlebars/helpers/with.js b/lib/handlebars/helpers/with.js new file mode 100644 index 000000000..7418cd066 --- /dev/null +++ b/lib/handlebars/helpers/with.js @@ -0,0 +1,24 @@ +import {appendContextPath, blockParams, createFrame, isEmpty, isFunction} from '../utils'; + +export default function(instance) { + instance.registerHelper('with', function(context, options) { + if (isFunction(context)) { context = context.call(this); } + + let fn = options.fn; + + if (!isEmpty(context)) { + let data = options.data; + if (options.data && options.ids) { + data = createFrame(options.data); + data.contextPath = appendContextPath(options.data.contextPath, options.ids[0]); + } + + return fn(context, { + data: data, + blockParams: blockParams([context], [data && data.contextPath]) + }); + } else { + return options.inverse(this); + } + }); +} diff --git a/lib/handlebars/logger.js b/lib/handlebars/logger.js new file mode 100644 index 000000000..1d583ddb9 --- /dev/null +++ b/lib/handlebars/logger.js @@ -0,0 +1,33 @@ +let logger = { + methodMap: ['debug', 'info', 'warn', 'error'], + level: 'info', + + // Maps a given level value to the `methodMap` indexes above. + lookupLevel: function(level) { + if (typeof level === 'string') { + let levelMap = logger.methodMap.indexOf(level.toLowerCase()); + if (levelMap >= 0) { + level = levelMap; + } else { + level = parseInt(level, 10); + } + } + + return level; + }, + + // Can be overridden in the host environment + log: function(level, ...message) { + level = logger.lookupLevel(level); + + if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) { + let method = logger.methodMap[level]; + if (!console[method]) { // eslint-disable-line no-console + method = 'log'; + } + console[method](...message); // eslint-disable-line no-console + } + } +}; + +export default logger; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 874728fb5..6b31a7ba4 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -20,8 +20,6 @@ export function checkRevision(compilerInfo) { } } -// TODO: Remove this line and break up compilePartial - export function template(templateSpec, env) { /* istanbul ignore next */ if (!env) { @@ -31,6 +29,8 @@ export function template(templateSpec, env) { throw new Exception('Unknown template object: ' + typeof templateSpec); } + templateSpec.main.decorator = templateSpec.main_d; + // Note: Using env.VM references rather than local var references throughout this section to allow // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); @@ -38,6 +38,9 @@ export function template(templateSpec, env) { function invokePartialWrapper(partial, context, options) { if (options.hash) { context = Utils.extend({}, context, options.hash); + if (options.ids) { + options.ids[0] = true; + } } partial = env.VM.resolvePartial.call(this, partial, context, options); @@ -89,7 +92,9 @@ export function template(templateSpec, env) { invokePartial: invokePartialWrapper, fn: function(i) { - return templateSpec[i]; + let ret = templateSpec[i]; + ret.decorator = templateSpec[i + '_d']; + return ret; }, programs: [], @@ -134,10 +139,18 @@ export function template(templateSpec, env) { let depths, blockParams = templateSpec.useBlockParams ? [] : undefined; if (templateSpec.useDepths) { - depths = options.depths ? [context].concat(options.depths) : [context]; + if (options.depths) { + depths = context !== options.depths[0] ? [context].concat(options.depths) : options.depths; + } else { + depths = [context]; + } } - return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths); + function main(context/*, options*/) { + return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); + } + main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams); + return main(context, options); } ret.isTop = true; @@ -148,9 +161,13 @@ export function template(templateSpec, env) { if (templateSpec.usePartial) { container.partials = container.merge(options.partials, env.partials); } + if (templateSpec.useDecorators) { + container.decorators = container.merge(options.decorators, env.decorators); + } } else { container.helpers = options.helpers; container.partials = options.partials; + container.decorators = options.decorators; } }; @@ -169,13 +186,21 @@ export function template(templateSpec, env) { export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) { function prog(context, options = {}) { - return fn.call(container, + let currentDepths = depths; + if (depths && context !== depths[0]) { + currentDepths = [context].concat(depths); + } + + return fn(container, context, container.helpers, container.partials, options.data || data, blockParams && [options.blockParams].concat(blockParams), - depths && [context].concat(depths)); + currentDepths); } + + prog = executeDecorators(fn, prog, container, depths, data, blockParams); + prog.program = i; prog.depth = depths ? depths.length : 0; prog.blockParams = declaredBlockParams || 0; @@ -184,7 +209,11 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa export function resolvePartial(partial, context, options) { if (!partial) { - partial = options.partials[options.name]; + if (options.name === '@partial-block') { + partial = options.data['partial-block']; + } else { + partial = options.partials[options.name]; + } } else if (!partial.call && !options.name) { // This is a dynamic partial that returned a string options.name = partial; @@ -195,6 +224,22 @@ export function resolvePartial(partial, context, options) { export function invokePartial(partial, context, options) { options.partial = true; + if (options.ids) { + options.data.contextPath = options.ids[0] || options.data.contextPath; + } + + let partialBlock; + if (options.fn && options.fn !== noop) { + partialBlock = options.data['partial-block'] = options.fn; + + if (partialBlock.partials) { + options.partials = Utils.extend({}, options.partials, partialBlock.partials); + } + } + + if (partial === undefined && partialBlock) { + partial = partialBlock; + } if (partial === undefined) { throw new Exception('The partial ' + options.name + ' could not be found'); @@ -212,3 +257,12 @@ function initData(context, data) { } return data; } + +function executeDecorators(fn, prog, container, depths, data, blockParams) { + if (fn.decorator) { + let props = {}; + prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); + Utils.extend(prog, props); + } + return prog; +} diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index c5223947d..d34646b7d 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -4,11 +4,12 @@ const escape = { '>': '>', '"': '"', "'": ''', - '`': '`' + '`': '`', + '=': '=' }; -const badChars = /[&<>"'`]/g, - possible = /[&<>"'`]/; +const badChars = /[&<>"'`=]/g, + possible = /[&<>"'`=]/; function escapeChar(chr) { return escape[chr]; @@ -30,8 +31,8 @@ export let toString = Object.prototype.toString; // Sourced from lodash // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt -/*eslint-disable func-style, no-var */ -var isFunction = function(value) { +/*eslint-disable func-style */ +let isFunction = function(value) { return typeof value === 'function'; }; // fallback for older versions of Chrome and Safari @@ -41,8 +42,8 @@ if (isFunction(/x/)) { return typeof value === 'function' && toString.call(value) === '[object Function]'; }; } -export var isFunction; -/*eslint-enable func-style, no-var */ +export {isFunction}; +/*eslint-enable func-style */ /* istanbul ignore next */ export const isArray = Array.isArray || function(value) { @@ -91,6 +92,12 @@ export function isEmpty(value) { } } +export function createFrame(object) { + let frame = extend({}, object); + frame._parent = object; + return frame; +} + export function blockParams(params, ids) { params.path = ids; return params; diff --git a/lib/precompiler.js b/lib/precompiler.js index 48cfebd1a..9f23ef661 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -1,35 +1,161 @@ /*eslint-disable no-console */ +import Async from 'async'; import fs from 'fs'; import * as Handlebars from './handlebars'; import {basename} from 'path'; import {SourceMapConsumer, SourceNode} from 'source-map'; import uglify from 'uglify-js'; +module.exports.loadTemplates = function(opts, callback) { + loadStrings(opts, function(err, strings) { + if (err) { + callback(err); + } else { + loadFiles(opts, function(err, files) { + if (err) { + callback(err); + } else { + opts.templates = strings.concat(files); + callback(undefined, opts); + } + }); + } + }); +}; + +function loadStrings(opts, callback) { + let strings = arrayCast(opts.string), + names = arrayCast(opts.name); + + if (names.length !== strings.length + && strings.length > 1) { + return callback(new Handlebars.Exception('Number of names did not match the number of string inputs')); + } + + Async.map(strings, function(string, callback) { + if (string !== '-') { + callback(undefined, string); + } else { + // Load from stdin + let buffer = ''; + process.stdin.setEncoding('utf8'); + + process.stdin.on('data', function(chunk) { + buffer += chunk; + }); + process.stdin.on('end', function() { + callback(undefined, buffer); + }); + } + }, + function(err, strings) { + strings = strings.map((string, index) => ({ + name: names[index], + path: names[index], + source: string + })); + callback(err, strings); + }); +} + +function loadFiles(opts, callback) { + // Build file extension pattern + let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); + extension = new RegExp('\\.' + extension + '$'); + + let ret = [], + queue = (opts.files || []).map((template) => ({template, root: opts.root})); + Async.whilst(() => queue.length, function(callback) { + let {template: path, root} = queue.shift(); + + fs.stat(path, function(err, stat) { + if (err) { + return callback(new Handlebars.Exception(`Unable to open template file "${path}"`)); + } + + if (stat.isDirectory()) { + opts.hasDirectory = true; + + fs.readdir(path, function(err, children) { + /* istanbul ignore next : Race condition that being too lazy to test */ + if (err) { + return callback(err); + } + children.forEach(function(file) { + let childPath = path + '/' + file; + + if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { + queue.push({template: childPath, root: root || path}); + } + }); + + callback(); + }); + } else { + fs.readFile(path, 'utf8', function(err, data) { + /* istanbul ignore next : Race condition that being too lazy to test */ + if (err) { + return callback(err); + } + + if (opts.bom && data.indexOf('\uFEFF') === 0) { + data = data.substring(1); + } + + // Clean the template name + let name = path; + if (!root) { + name = basename(name); + } else if (name.indexOf(root) === 0) { + name = name.substring(root.length + 1); + } + name = name.replace(extension, ''); + + ret.push({ + path: path, + name: name, + source: data + }); + + callback(); + }); + } + }); + }, + function(err) { + if (err) { + callback(err); + } else { + callback(undefined, ret); + } + }); +} + module.exports.cli = function(opts) { if (opts.version) { console.log(Handlebars.VERSION); return; } - if (!opts.templates.length) { + if (!opts.templates.length && !opts.hasDirectory) { throw new Handlebars.Exception('Must define at least one template or directory.'); } - opts.templates.forEach(function(template) { - try { - fs.statSync(template); - } catch (err) { - throw new Handlebars.Exception(`Unable to open template file "${template}"`); - } - }); - if (opts.simple && opts.min) { throw new Handlebars.Exception('Unable to minimize simple output'); } - if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) { + + const multiple = opts.templates.length !== 1 || opts.hasDirectory; + if (opts.simple && multiple) { throw new Handlebars.Exception('Unable to output multiple templates in simple mode'); } + // Force simple mode if we have only one template and it's unnamed. + if (!opts.amd && !opts.commonjs && opts.templates.length === 1 + && !opts.templates[0].name) { + opts.simple = true; + } + // Convert the known list into a hash let known = {}; if (opts.known && !Array.isArray(opts.known)) { @@ -41,9 +167,7 @@ module.exports.cli = function(opts) { } } - // Build file extension pattern - let extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; }); - extension = new RegExp('\\.' + extension + '$'); + const objectName = opts.partial ? 'Handlebars.partials' : 'templates'; let output = new SourceNode(); if (!opts.simple) { @@ -63,81 +187,47 @@ module.exports.cli = function(opts) { } output.add('{};\n'); } - function processTemplate(template, root) { - let path = template, - stat = fs.statSync(path); - if (stat.isDirectory()) { - fs.readdirSync(template).map(function(file) { - let childPath = template + '/' + file; - - if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) { - processTemplate(childPath, root || template); - } - }); - } else { - let data = fs.readFileSync(path, 'utf8'); - if (opts.bom && data.indexOf('\uFEFF') === 0) { - data = data.substring(1); - } - - let options = { - knownHelpers: known, - knownHelpersOnly: opts.o - }; + opts.templates.forEach(function(template) { + let options = { + knownHelpers: known, + knownHelpersOnly: opts.o + }; - if (opts.map) { - options.srcName = path; - } - if (opts.data) { - options.data = true; - } + if (opts.map) { + options.srcName = template.path; + } + if (opts.data) { + options.data = true; + } - // Clean the template name - if (!root) { - template = basename(template); - } else if (template.indexOf(root) === 0) { - template = template.substring(root.length + 1); - } - template = template.replace(extension, ''); + let precompiled = Handlebars.precompile(template.source, options); - let precompiled = Handlebars.precompile(data, options); + // If we are generating a source map, we have to reconstruct the SourceNode object + if (opts.map) { + let consumer = new SourceMapConsumer(precompiled.map); + precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer); + } - // If we are generating a source map, we have to reconstruct the SourceNode object - if (opts.map) { - let consumer = new SourceMapConsumer(precompiled.map); - precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer); + if (opts.simple) { + output.add([precompiled, '\n']); + } else { + if (!template.name) { + throw new Handlebars.Exception('Name missing for template'); } - if (opts.simple) { - output.add([precompiled, '\n']); - } else if (opts.partial) { - if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.add('return '); - } - output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']); - } else { - if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) { - output.add('return '); - } - output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']); + if (opts.amd && !multiple) { + output.add('return '); } + output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']); } - } - - opts.templates.forEach(function(template) { - processTemplate(template, opts.root); }); // Output the content if (!opts.simple) { if (opts.amd) { - if (opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) { - if (opts.partial) { - output.add('return Handlebars.partials;\n'); - } else { - output.add('return templates;\n'); - } + if (multiple) { + output.add(['return ', objectName, ';\n']); } output.add('});'); } else if (!opts.commonjs) { @@ -176,3 +266,11 @@ module.exports.cli = function(opts) { console.log(output); } }; + +function arrayCast(value) { + value = value != null ? value : []; + if (!Array.isArray(value)) { + value = [value]; + } + return value; +} diff --git a/package.json b/package.json index 83f428d0f..814e5277e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handlebars", "barename": "handlebars", - "version": "3.0.3", + "version": "4.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,11 +21,12 @@ "node": ">=0.4.7" }, "dependencies": { + "async": "^1.4.0", "optimist": "^0.6.1", "source-map": "^0.1.40" }, "optionalDependencies": { - "uglify-js": "~2.3" + "uglify-js": "~2.4" }, "devDependencies": { "async": "^0.9.0", @@ -52,6 +53,7 @@ "jison": "~0.3.0", "keen.io": "0.0.3", "mocha": "~1.20.0", + "mock-stdin": "^0.3.0", "mustache": "0.x", "semver": "^4.0.0", "underscore": "^1.5.1" diff --git a/release-notes.md b/release-notes.md index 21a5346e9..dba8a55ec 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,93 @@ ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...master) +[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...master) + +## v4.0.0 - September 1st, 2015 +- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski)) +- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley)) +- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13)) +- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn)) +- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch)) +- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin)) +- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski)) +- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick)) +- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia)) +- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other? +- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks. +- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions +- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals +- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers +- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84) +- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba) +- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6) +- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1) +- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a) +- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac) +- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc) +- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760) +- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab) +- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da) +- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038) +- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35) +- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7) +- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b) +- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106) +- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c) +- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa) + +Compatibility notes: +- Depthed paths are now conditional pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior the better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028). +- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces. +- AST constructors have been dropped in favor of plain old javascript objects +- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0. + +[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...v4.0.0) + +## v4.0.0 - September 1st, 2015 +- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski)) +- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley)) +- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13)) +- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn)) +- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch)) +- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin)) +- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski)) +- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick)) +- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia)) +- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other? +- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks. +- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions +- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals +- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers +- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84) +- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba) +- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6) +- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1) +- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a) +- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac) +- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc) +- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760) +- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab) +- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da) +- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038) +- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35) +- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7) +- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b) +- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106) +- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c) +- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa) + +Compatibility notes: +- Depthed paths are now conditional pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior the better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028). +- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces. +- AST constructors have been dropped in favor of plain old javascript objects +- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0. + +[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0) ## v3.0.3 - April 28th, 2015 - [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://api.github.com/users/boskee)) diff --git a/spec/amd.html b/spec/amd.html index 5de33c1cf..1149dc706 100644 --- a/spec/amd.html +++ b/spec/amd.html @@ -1,3 +1,4 @@ + Mocha diff --git a/spec/ast.js b/spec/ast.js index 6f492fddd..8f346d882 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -3,113 +3,49 @@ describe('ast', function() { return; } - var LOCATION_INFO = { - start: { - line: 1, - column: 1 - }, - end: { - line: 1, - column: 1 - } - }; - - function testLocationInfoStorage(node) { - equals(node.loc.start.line, 1); - equals(node.loc.start.column, 1); - equals(node.loc.end.line, 1); - equals(node.loc.end.column, 1); - } + var AST = Handlebars.AST; - describe('MustacheStatement', function() { - it('should store args', function() { - var mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO); - equals(mustache.type, 'MustacheStatement'); - equals(mustache.escaped, true); - testLocationInfoStorage(mustache); - }); - }); 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:5"); }); - - it('stores location info', function() { - 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('PathExpression', function() { - it('stores location info', function() { - var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); - testLocationInfoStorage(idNode); - }); }); - describe('Hash', function() { - it('stores location info', function() { - var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO); - testLocationInfoStorage(hash); - }); - }); - - describe('ContentStatement', function() { - it('stores location info', function() { - var content = new handlebarsEnv.AST.ContentStatement('HI', LOCATION_INFO); - testLocationInfoStorage(content); - }); - }); - - describe('CommentStatement', function() { - it('stores location info', function() { - var comment = new handlebarsEnv.AST.CommentStatement('HI', {}, LOCATION_INFO); - testLocationInfoStorage(comment); - }); - }); - - describe('NumberLiteral', function() { - it('stores location info', function() { - var integer = new handlebarsEnv.AST.NumberLiteral('6', LOCATION_INFO); - testLocationInfoStorage(integer); - }); - }); + describe('helpers', function() { + describe('#helperExpression', function() { + it('should handle mustache statements', function() { + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: undefined}), false); + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [1], hash: undefined}), true); + equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: {}}), true); + }); + it('should handle block statements', function() { + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: undefined}), false); + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [1], hash: undefined}), true); + equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: {}}), true); + }); + it('should handle subexpressions', function() { + equals(AST.helpers.helperExpression({type: 'SubExpression'}), true); + }); + it('should work with non-helper nodes', function() { + equals(AST.helpers.helperExpression({type: 'Program'}), false); - describe('StringLiteral', function() { - it('stores location info', function() { - var string = new handlebarsEnv.AST.StringLiteral('6', LOCATION_INFO); - testLocationInfoStorage(string); - }); - }); + equals(AST.helpers.helperExpression({type: 'PartialStatement'}), false); + equals(AST.helpers.helperExpression({type: 'ContentStatement'}), false); + equals(AST.helpers.helperExpression({type: 'CommentStatement'}), false); - describe('BooleanLiteral', function() { - it('stores location info', function() { - var bool = new handlebarsEnv.AST.BooleanLiteral('true', LOCATION_INFO); - testLocationInfoStorage(bool); - }); - }); + equals(AST.helpers.helperExpression({type: 'PathExpression'}), false); - describe('PartialStatement', function() { - it('stores location info', function() { - var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO); - testLocationInfoStorage(pn); - }); - }); + equals(AST.helpers.helperExpression({type: 'StringLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'NumberLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'BooleanLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'UndefinedLiteral'}), false); + equals(AST.helpers.helperExpression({type: 'NullLiteral'}), false); - describe('Program', function() { - it('storing location info', function() { - var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO); - testLocationInfoStorage(pn); + equals(AST.helpers.helperExpression({type: 'Hash'}), false); + equals(AST.helpers.helperExpression({type: 'HashPair'}), false); + }); }); }); @@ -123,8 +59,18 @@ describe('ast', function() { 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}}'); + ast = Handlebars.parse( + 'line 1 {{line1Token}}\n' // 1 + + ' line 2 {{line2token}}\n' // 2 + + ' line 3 {{#blockHelperOnLine3}}\n' // 3 + + 'line 4{{line4token}}\n' // 4 + + 'line5{{else}}\n' // 5 + + '{{line6Token}}\n' // 6 + + '{{/blockHelperOnLine3}}\n' // 7 + + '{{#open}}\n' // 8 + + '{{else inverse}}\n' // 9 + + '{{else}}\n' // 10 + + '{{/open}}'); // 11 body = ast.body; it('gets ContentNode line numbers', function() { @@ -155,14 +101,23 @@ describe('ast', function() { var blockHelperNode = body[5], program = blockHelperNode.program; - testColumns(program, 3, 5, 8, 5); + testColumns(program, 3, 5, 31, 5); }); it('correctly records the line numbers of an inverse of a block helper', function() { var blockHelperNode = body[5], inverse = blockHelperNode.inverse; - testColumns(inverse, 5, 7, 5, 0); + testColumns(inverse, 5, 7, 13, 0); + }); + + it('correctly records the line number of chained inverses', function() { + var chainInverseNode = body[7]; + + testColumns(chainInverseNode.program, 8, 9, 9, 0); + testColumns(chainInverseNode.inverse, 9, 10, 16, 0); + testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0); + testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0); }); }); diff --git a/spec/basic.js b/spec/basic.js index f9b781b4a..8859545d9 100644 --- a/spec/basic.js +++ b/spec/basic.js @@ -22,6 +22,10 @@ describe('basic context', function() { 'It works if all the required keys are provided'); }); + it('compiling with a string context', function() { + shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3'); + }); + it('compiling with an undefined context', function() { shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!'); @@ -203,8 +207,12 @@ describe('basic context', function() { }); it('literal references', function() { - shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, - 'Goodbye beautiful world!', 'Literal paths can be used'); + shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo bar"}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo("Goodbye {{'foo bar'}} world!", {'foo bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo[bar"}} world!', {'foo[bar': 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo('Goodbye {{"foo\'bar"}} world!', {"foo'bar": 'beautiful'}, 'Goodbye beautiful world!'); + shouldCompileTo("Goodbye {{'foo\"bar'}} world!", {'foo"bar': 'beautiful'}, 'Goodbye beautiful world!'); }); it("that current context path ({{.}}) doesn't hit helpers", function() { diff --git a/spec/blocks.js b/spec/blocks.js index 80f1580e6..2fbaee7cc 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -65,6 +65,13 @@ describe('blocks', function() { shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!'); }); + it('works with cached blocks', function() { + var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false}); + + var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]}); + equals(result, 'Alan JohnsonAlan Johnson'); + }); + describe('inverted sections', function() { it('inverted sections with unset value', function() { var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'; @@ -118,6 +125,16 @@ describe('blocks', function() { shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 'No people\n'); }); + it('block standalone else sections can be disabled', function() { + shouldCompileTo( + '{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', + [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], + '\nNo people\n\n'); + shouldCompileTo( + '{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n', + [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], + '\nNo people\n\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'); @@ -149,4 +166,182 @@ describe('blocks', function() { shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel '); }); }); + + describe('decorators', function() { + it('should apply mustache decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'success'; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should apply allow undefined return', function() { + var helpers = { + helper: function(options) { + return options.fn() + options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'cess'; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}suc{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply block decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should support nested decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn.nested + options.fn(); + return fn; + }, + nested: function(fn, props, container, options) { + props.nested = options.fn(); + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply multiple decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = (fn.run || '') + options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should access parent variables', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.args; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator foo}}{{/helper}}', + {hash: {'foo': 'success'}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should work with root program', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], 'success'); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator "success"}}', + {hash: {'foo': 'success'}, decorators: decorators}, + ''); + equals(run, true); + }); + it('should fail when accessing variables from root', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], undefined); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator foo}}', + {hash: {'foo': 'fail'}, decorators: decorators}, + ''); + equals(run, true); + }); + + describe('registration', function() { + it('unregisters', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator('foo', function() { + return 'fail'; + }); + + equals(!!handlebarsEnv.decorators.foo, true); + handlebarsEnv.unregisterDecorator('foo'); + equals(handlebarsEnv.decorators.foo, undefined); + }); + + it('allows multiple globals', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator({ + foo: function() {}, + bar: function() {} + }); + + equals(!!handlebarsEnv.decorators.foo, true); + equals(!!handlebarsEnv.decorators.bar, true); + handlebarsEnv.unregisterDecorator('foo'); + handlebarsEnv.unregisterDecorator('bar'); + equals(handlebarsEnv.decorators.foo, undefined); + equals(handlebarsEnv.decorators.bar, undefined); + }); + it('fails with multiple and args', function() { + shouldThrow(function() { + handlebarsEnv.registerDecorator({ + world: function() { return 'world!'; }, + testHelper: function() { return 'found it!'; } + }, {}); + }, Error, 'Arg not supported with multiple decorators'); + }); + }); + }); }); diff --git a/spec/builtins.js b/spec/builtins.js index 46d70baac..f06a1ad23 100644 --- a/spec/builtins.js +++ b/spec/builtins.js @@ -32,6 +32,11 @@ describe('builtin helpers', function() { shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!', 'if with function does not show the contents when returns undefined'); }); + + it('should not change the depth list', function() { + var string = '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}'; + shouldCompileTo(string, {foo: {goodbye: true}, world: 'world'}, 'GOODBYE cruel world!'); + }); }); describe('#with', function() { @@ -47,6 +52,16 @@ describe('builtin helpers', function() { var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}'; shouldCompileTo(string, {}, 'Person is not present'); }); + it('with provides block parameter', function() { + var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}'; + shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson'); + }); + it('works when data is disabled', function() { + var template = CompilerContext.compile('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}', {data: false}); + + var result = template({person: {first: 'Alan', last: 'Johnson'}}); + equals(result, 'Alan Johnson'); + }); }); describe('#each', function() { @@ -210,6 +225,16 @@ describe('builtin helpers', function() { 'each with array function argument ignores the contents when empty'); }); + it('each object when last key is an empty string', function() { + var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'; + var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, '': {text: 'GOODBYE'}}, world: 'world'}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'Empty string key is not skipped'); + }); + it('data passed to helpers', function() { var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}'; var hash = {letters: ['a', 'b', 'c']}; @@ -276,7 +301,7 @@ describe('builtin helpers', function() { }; shouldCompileTo(string, [hash,,,, {level: '03'}], ''); - equals(3, levelArg); + equals('03', levelArg); equals('whee', logArg); }); it('should output to info', function() { @@ -311,11 +336,77 @@ describe('builtin helpers', function() { }); it('should handle missing logger', function() { var string = '{{log blah}}'; - var hash = { blah: 'whee' }; + var hash = { blah: 'whee' }, + called = false; console.error = undefined; + console.log = function(log) { + equals('whee', log); + called = true; + }; shouldCompileTo(string, [hash,,,, {level: '03'}], ''); + equals(true, called); + }); + + it('should handle string log levels', function() { + var string = '{{log blah}}'; + var hash = { blah: 'whee' }; + var called; + + console.error = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, [hash,,,, {level: 'error'}], ''); + equals(true, called); + + called = false; + + shouldCompileTo(string, [hash,,,, {level: 'ERROR'}], ''); + equals(true, called); + }); + it('should handle hash log levels', function() { + var string = '{{log blah level="error"}}'; + var hash = { blah: 'whee' }; + var called; + + console.error = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(true, called); + }); + it('should handle hash log levels', function() { + var string = '{{log blah level="debug"}}'; + var hash = { blah: 'whee' }; + var called = false; + + console.info = console.log = console.error = console.debug = function(log) { + equals('whee', log); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(false, called); + }); + it('should pass multiple log arguments', function() { + var string = '{{log blah "foo" 1}}'; + var hash = { blah: 'whee' }; + var called; + + console.info = console.log = function(log1, log2, log3) { + equals('whee', log1); + equals('foo', log2); + equals(1, log3); + called = true; + }; + + shouldCompileTo(string, hash, ''); + equals(true, called); }); /*eslint-enable no-console */ }); diff --git a/spec/compiler.js b/spec/compiler.js index fe4b63a30..be1fb007d 100644 --- a/spec/compiler.js +++ b/spec/compiler.js @@ -39,7 +39,10 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')], null, {}))(), 'Hello'); + equal(Handlebars.compile({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + })(), 'Hello'); }); it('can pass through an empty string', function() { @@ -58,7 +61,10 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')]), null, {})), true); + equal(/return "Hello"/.test(Handlebars.precompile({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + })), true); }); it('can pass through an empty string', function() { diff --git a/spec/env/common.js b/spec/env/common.js index e4c8ae537..111294c68 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -1,11 +1,27 @@ +var AssertError; +if (Error.captureStackTrace) { + AssertError = function AssertError(message, caller) { + Error.prototype.constructor.call(this, message); + this.message = message; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, caller || AssertError); + } + }; + + AssertError.prototype = new Error(); +} else { + AssertError = Error; +} + global.shouldCompileTo = function(string, hashOrArray, expected, message) { shouldCompileToWithPartials(string, hashOrArray, false, expected, message); }; -global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { +global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { var result = compileWithPartials(string, hashOrArray, partials); if (result !== expected) { - throw new Error("'" + result + "' should === '" + expected + "': " + message); + throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials); } }; @@ -13,7 +29,10 @@ global.compileWithPartials = function(string, hashOrArray, partials) { var template, ary, options; - if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { + if (hashOrArray && hashOrArray.hash) { + ary = [hashOrArray.hash, hashOrArray]; + delete hashOrArray.hash; + } else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { ary = []; ary.push(hashOrArray[0]); ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); @@ -31,9 +50,9 @@ global.compileWithPartials = function(string, hashOrArray, partials) { }; -global.equals = global.equal = function(a, b, msg) { +global.equals = global.equal = function equals(a, b, msg) { if (a !== b) { - throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : '')); + throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals); } }; @@ -44,13 +63,13 @@ global.shouldThrow = function(callback, type, msg) { failed = true; } catch (err) { if (type && !(err instanceof type)) { - throw new Error('Type failure: ' + err); + throw new AssertError('Type failure: ' + err); } if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { equal(msg, err.message); } } if (failed) { - throw new Error('It failed to throw'); + throw new AssertError('It failed to throw', shouldThrow); } }; diff --git a/spec/env/runner.js b/spec/env/runner.js index 56fc8d443..98d2482e8 100644 --- a/spec/env/runner.js +++ b/spec/env/runner.js @@ -13,9 +13,9 @@ var files = fs.readdirSync(testDir) .filter(function(name) { return (/.*\.js$/).test(name); }) .map(function(name) { return testDir + '/' + name; }); -run('./node', function() { +run('./runtime', function() { run('./browser', function() { - run('./runtime', function() { + run('./node', function() { /*eslint-disable no-process-exit */ process.exit(errors); /*eslint-enable no-process-exit */ diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js index 852733b2f..1cf1eabc9 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 templates['empty'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { return ""; },"useData":true}); }); diff --git a/spec/helpers.js b/spec/helpers.js index 54ef0f288..94e503f12 100644 --- a/spec/helpers.js +++ b/spec/helpers.js @@ -28,6 +28,29 @@ describe('helpers', function() { 'raw block helper gets raw content'); }); + it('helper for nested raw block gets raw content', function() { + var string = '{{{{a}}}} {{{{b}}}} {{{{/b}}}} {{{{/a}}}}'; + var helpers = { + a: function(options) { + return options.fn(); + } + }; + shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} ', 'raw block helper should get nested raw block as raw content'); + }); + + it('helper block with identical context', function() { + var string = '{{#goodbyes}}{{name}}{{/goodbyes}}'; + var hash = {name: 'Alan'}; + var helpers = {goodbyes: function(options) { + var out = ''; + var byes = ['Goodbye', 'goodbye', 'GOODBYE']; + for (var i = 0, j = byes.length; i < j; i++) { + out += byes[i] + ' ' + options.fn(this) + '! '; + } + return out; + }}; + shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! '); + }); it('helper block with complex lookup expression', function() { var string = '{{#goodbyes}}{{../name}}{{/goodbyes}}'; var hash = {name: 'Alan'}; @@ -35,7 +58,7 @@ describe('helpers', function() { var out = ''; var byes = ['Goodbye', 'goodbye', 'GOODBYE']; for (var i = 0, j = byes.length; i < j; i++) { - out += byes[i] + ' ' + options.fn(this) + '! '; + out += byes[i] + ' ' + options.fn({}) + '! '; } return out; }}; diff --git a/spec/parser.js b/spec/parser.js index c37887414..3b7e3e45f 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -65,7 +65,7 @@ describe('parser', function() { equals(astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n'); }); - it('parses mutaches with DATA parameters', function() { + it('parses mustaches with DATA parameters', function() { equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n'); }); @@ -113,6 +113,18 @@ describe('parser', function() { equals(astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n'); }); + it('parsers partial blocks', function() { + equals(astFor('{{#> foo}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n'); + }); + it('should handle parser block mismatch', function() { + shouldThrow(function() { + astFor('{{#> goodbyes}}{{/hellos}}'); + }, Error, (/goodbyes doesn't match hellos/)); + }); + it('parsers partial blocks with arguments', function() { + equals(astFor('{{#> foo context hash=value}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n'); + }); + it('parses a comment', function() { equals(astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n"); }); @@ -228,7 +240,48 @@ describe('parser', function() { describe('externally compiled AST', function() { it('can pass through an already-compiled AST', function() { - equals(astFor(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement('Hello')], null)), 'CONTENT[ \'Hello\' ]\n'); + equals(astFor({ + type: 'Program', + body: [ {type: 'ContentStatement', value: 'Hello'}] + }), 'CONTENT[ \'Hello\' ]\n'); + }); + }); + + describe('directives', function() { + it('should parse block directives', function() { + equals(astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'); + }); + it('should parse directives', function() { + equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n'); }); + it('should fail if directives have inverse', function() { + shouldThrow(function() { + astFor('{{#* foo}}{{^}}{{/foo}}'); + }, Error, /Unexpected inverse/); + }); + }); + + it('GH1024 - should track program location properly', function() { + var p = Handlebars.parse('\n' + + ' {{#if foo}}\n' + + ' {{bar}}\n' + + ' {{else}} {{baz}}\n' + + '\n' + + ' {{/if}}\n' + + ' '); + + // We really need a deep equals but for now this should be stable... + equals(JSON.stringify(p.loc), JSON.stringify({ + start: { line: 1, column: 0 }, + end: { line: 7, column: 4 } + })); + equals(JSON.stringify(p.body[1].program.loc), JSON.stringify({ + start: { line: 2, column: 13 }, + end: { line: 4, column: 7 } + })); + equals(JSON.stringify(p.body[1].inverse.loc), JSON.stringify({ + start: { line: 4, column: 15 }, + end: { line: 6, column: 5 } + })); }); }); diff --git a/spec/partials.js b/spec/partials.js index 22d2dc959..cc2c266e7 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -41,6 +41,28 @@ describe('partials', function() { 'Partials can be passed a context'); }); + it('partials with no context', function() { + var partial = '{{name}} ({{url}}) '; + var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials( + 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}', + [hash, {}, {dude: partial}, {explicitPartialContext: true}], + true, + 'Dudes: () () '); + shouldCompileToWithPartials( + 'Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}', + [hash, {}, {dude: partial}, {explicitPartialContext: true}], + true, + 'Dudes: foo () foo () '); + }); + + it('partials with string context', function() { + var string = 'Dudes: {{>dude "dudes"}}'; + var partial = '{{.}}'; + var hash = {}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: dudes'); + }); + it('partials with undefined context', function() { var string = 'Dudes: {{>dude dudes}}'; var partial = '{{foo}} Empty'; @@ -189,6 +211,106 @@ describe('partials', function() { handlebarsEnv.compile = compile; }); + describe('partial blocks', function() { + it('should render partial block as default', function() { + shouldCompileToWithPartials( + '{{#> dude}}success{{/dude}}', + [{}, {}, {}], + true, + 'success'); + }); + it('should execute default block with proper context', function() { + shouldCompileToWithPartials( + '{{#> dude context}}{{value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {}], + true, + 'success'); + }); + it('should propagate block parameters to default block', function() { + shouldCompileToWithPartials( + '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}', + [{context: {value: 'success'}}, {}, {}], + true, + 'success'); + }); + + it('should not use partial block if partial exists', function() { + shouldCompileToWithPartials( + '{{#> dude}}fail{{/dude}}', + [{}, {}, {dude: 'success'}], + true, + 'success'); + }); + + it('should render block from partial', function() { + shouldCompileToWithPartials( + '{{#> dude}}success{{/dude}}', + [{}, {}, {dude: '{{> @partial-block }}'}], + true, + 'success'); + }); + it('should render block from partial with context', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}], + true, + 'success'); + }); + it('should render block from partial with context', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{../context/value}}{{/dude}}', + [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}], + true, + 'success'); + }); + it('should render block from partial with block params', function() { + shouldCompileToWithPartials( + '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}', + [{context: {value: 'success'}}, {}, {dude: '{{> @partial-block }}'}], + true, + 'success'); + }); + }); + + describe('inline partials', function() { + it('should define inline partials for template', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should overwrite multiple partials in the same template', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should define inline partials for block', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + shouldThrow(function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}', {}, 'success'); + }, Error, /myPartial could not/); + }); + it('should override global partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {hash: {}, partials: {myPartial: function() { return 'fail'; }}}, 'success'); + }); + it('should override template partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + }); + it('should override partials down the entire stack', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}', {}, 'success'); + }); + + it('should define inline partials for partial call', function() { + shouldCompileToWithPartials( + '{{#*inline "myPartial"}}success{{/inline}}{{> dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + it('should define inline partials in partial block call', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + }); + it('should pass compiler flags', function() { if (Handlebars.compile) { var env = Handlebars.create(); @@ -231,6 +353,12 @@ describe('partials', function() { var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); }); + it('partials can access parents with custom context', function() { + var string = 'Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}'; + var partial = '{{name}} ({{url}}) {{root}} '; + var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '); + }); it('partials can access parents without data', function() { var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; var partial = '{{name}} ({{url}}) {{root}} '; diff --git a/spec/precompiler.js b/spec/precompiler.js index 21e25b06e..e1ad5ade9 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -16,6 +16,12 @@ describe('precompiler', function() { precompile, minify, + emptyTemplate = { + path: __dirname + '/artifacts/empty.handlebars', + name: 'empty', + source: '' + }, + file, content, writeFileSync; @@ -51,10 +57,9 @@ describe('precompiler', function() { Precompiler.cli({templates: []}); }, Handlebars.Exception, 'Must define at least one template or directory.'); }); - it('should throw on missing template', function() { - shouldThrow(function() { - Precompiler.cli({templates: ['foo']}); - }, Handlebars.Exception, 'Unable to open template file "foo"'); + it('should handle empty/filtered directories', function() { + Precompiler.cli({hasDirectory: true, templates: []}); + // Success is not throwing }); it('should throw when combining simple and minimized', function() { shouldThrow(function() { @@ -66,107 +71,183 @@ describe('precompiler', function() { Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true}); }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); }); + it('should throw when missing name', function() { + shouldThrow(function() { + Precompiler.cli({templates: [{source: ''}], amd: true}); + }, Handlebars.Exception, 'Name missing for template'); + }); it('should throw when combining simple and directories', function() { shouldThrow(function() { - Precompiler.cli({templates: [__dirname], simple: true}); + Precompiler.cli({hasDirectory: true, templates: [1], simple: true}); }, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); }); - it('should enumerate directories by extension', function() { - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'}); - equal(/'example_2'/.test(log), true); - log = ''; - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'}); - equal(/'empty'/.test(log), true); - equal(/'example_1'/.test(log), true); - }); - it('should protect from regexp patterns', function() { - Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'}); - // Success is not throwing - }); it('should output simple templates', function() { Handlebars.precompile = function() { return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], simple: true}); + equal(log, 'simple\n'); + }); + it('should default to simple templates', function() { + Handlebars.precompile = function() { return 'simple'; }; + Precompiler.cli({templates: [{source: ''}]}); equal(log, 'simple\n'); }); it('should output amd templates', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], amd: true}); equal(/template\(amd\)/.test(log), true); }); it('should output multiple amd', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'}); + Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, namespace: 'foo'}); equal(/templates = foo = foo \|\|/.test(log), true); equal(/return templates/.test(log), true); equal(/template\(amd\)/.test(log), true); }); it('should output amd partials', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], amd: true, partial: true}); equal(/return Handlebars\.partials\['empty'\]/.test(log), true); equal(/template\(amd\)/.test(log), true); }); it('should output multiple amd partials', function() { Handlebars.precompile = function() { return 'amd'; }; - Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, partial: true}); equal(/return Handlebars\.partials\[/.test(log), false); equal(/template\(amd\)/.test(log), true); }); it('should output commonjs templates', function() { Handlebars.precompile = function() { return 'commonjs'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], commonjs: true}); equal(/template\(commonjs\)/.test(log), true); }); it('should set data flag', function() { Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true}); + Precompiler.cli({templates: [emptyTemplate], simple: true, data: true}); equal(log, 'simple\n'); }); it('should set known helpers', function() { Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; }; - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'}); - 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/'}); + Precompiler.cli({templates: [emptyTemplate], simple: true, known: '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!'}); + Precompiler.cli({templates: [emptyTemplate], simple: true, 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'}); + Precompiler.cli({templates: [emptyTemplate], min: true}); equal(log, 'min'); }); it('should output map', function() { - Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'}); + Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'}); 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'}); + Precompiler.cli({templates: [emptyTemplate], min: true, map: 'foo.js.map'}); equal(file, 'foo.js.map'); equal(/sourceMappingURL=/.test(log), true); }); + + describe('#loadTemplates', function() { + it('should throw on missing template', function(done) { + Precompiler.loadTemplates({files: ['foo']}, function(err) { + equal(err.message, 'Unable to open template file "foo"'); + done(); + }); + }); + it('should enumerate directories by extension', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hbs'}, function(err, opts) { + equal(opts.templates.length, 1); + equal(opts.templates[0].name, 'example_2'); + done(err); + }); + }); + it('should enumerate all templates by extension', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'handlebars'}, function(err, opts) { + equal(opts.templates.length, 3); + equal(opts.templates[0].name, 'bom'); + equal(opts.templates[1].name, 'empty'); + equal(opts.templates[2].name, 'example_1'); + done(err); + }); + }); + it('should handle regular expression characters in extensions', function(done) { + Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hb(s'}, function(err) { + // Success is not throwing + done(err); + }); + }); + it('should handle BOM', function(done) { + var opts = {files: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].source, 'a'); + done(err); + }); + }); + + it('should handle different root', function(done) { + var opts = {files: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, __dirname + '/artifacts/empty'); + done(err); + }); + }); + + it('should accept string inputs', function(done) { + var opts = {string: ''}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, undefined); + equal(opts.templates[0].source, ''); + done(err); + }); + }); + it('should accept string array inputs', function(done) { + var opts = {string: ['', 'bar'], name: ['beep', 'boop']}; + Precompiler.loadTemplates(opts, function(err, opts) { + equal(opts.templates[0].name, 'beep'); + equal(opts.templates[0].source, ''); + equal(opts.templates[1].name, 'boop'); + equal(opts.templates[1].source, 'bar'); + done(err); + }); + }); + it('should accept stdin input', function(done) { + var stdin = require('mock-stdin').stdin(); + Precompiler.loadTemplates({string: '-'}, function(err, opts) { + equal(opts.templates[0].source, 'foo'); + done(err); + }); + stdin.send('fo'); + stdin.send('o'); + stdin.end(); + }); + it('error on name missing', function(done) { + var opts = {string: ['', 'bar']}; + Precompiler.loadTemplates(opts, function(err) { + equal(err.message, 'Number of names did not match the number of string inputs'); + done(); + }); + }); + + it('should complete when no args are passed', function(done) { + Precompiler.loadTemplates({}, function(err, opts) { + equal(opts.templates.length, 0); + done(err); + }); + }); + }); }); diff --git a/spec/regressions.js b/spec/regressions.js index 247c1c9b3..e8942a484 100644 --- a/spec/regressions.js +++ b/spec/regressions.js @@ -172,4 +172,35 @@ describe('Regressions', function() { var result = template(context); equals(result, 'foo'); }); + + it('GH-1021: Each empty string key', function() { + var data = { + '': 'foo', + 'name': 'Chris', + 'value': 10000 + }; + + shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n'); + }); + + it('GH-1054: Should handle simple safe string responses', function() { + var root = '{{#wrap}}{{>partial}}{{/wrap}}'; + var partials = { + partial: '{{#wrap}}{{/wrap}}' + }; + var helpers = { + wrap: function(options) { + return new Handlebars.SafeString(options.fn()); + } + }; + + shouldCompileToWithPartials(root, [{}, helpers, partials], true, ''); + }); + + it('GH-1065: Sparse arrays', function() { + var array = []; + array[1] = 'foo'; + array[3] = 'bar'; + shouldCompileTo('{{#each array}}{{@index}}{{.}}{{/each}}', {array: array}, '1foo3bar'); + }); }); diff --git a/spec/runtime.js b/spec/runtime.js index 502a8436b..a4830ad0c 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -14,19 +14,19 @@ describe('runtime', function() { it('should throw on version mismatch', function() { shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION + 1] }); }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION - 1] }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true + main: {} }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); }); diff --git a/spec/strict.js b/spec/strict.js index 2aef13442..05ce35d9e 100644 --- a/spec/strict.js +++ b/spec/strict.js @@ -78,6 +78,23 @@ describe('strict', function() { template({hello: {}}); }, Exception, /"bar" not defined in/); }); + + it('should allow undefined parameters when passed to helpers', function() { + var template = CompilerContext.compile('{{#unless foo}}success{{/unless}}', {strict: true}); + equals(template({}), 'success'); + }); + + it('should allow undefined hash when passed to helpers', function() { + var template = CompilerContext.compile('{{helper value=@foo}}', {strict: true}); + var helpers = { + helper: function(options) { + equals('value' in options.hash, true); + equals(options.hash.value, undefined); + return 'success'; + } + }; + equals(template({}, {helpers: helpers}), 'success'); + }); }); describe('assume objects', function() { diff --git a/spec/tokenizer.js b/spec/tokenizer.js index ad71dc9b2..dc077ce72 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -214,6 +214,10 @@ describe('Tokenizer', function() { shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); }); + it('tokenizes partial block declarations', function() { + var result = tokenize('{{#> foo}}'); + shouldMatchTokens(result, ['OPEN_PARTIAL_BLOCK', 'ID', 'CLOSE']); + }); 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']); @@ -237,6 +241,15 @@ describe('Tokenizer', function() { shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); }); + it('tokenizes directives', function() { + shouldMatchTokens( + tokenize('{{#*foo}}content{{/foo}}'), + ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); + shouldMatchTokens( + tokenize('{{*foo}}'), + ['OPEN', 'ID', 'CLOSE']); + }); + it('tokenizes inverse sections as "INVERSE"', function() { shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']); shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']); @@ -264,7 +277,7 @@ describe('Tokenizer', function() { }); it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() { - var result = tokenize('{{ foo bar \'baz\' }}'); + var result = tokenize('{{ foo bar \"baz\" }}'); shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']); shouldBeToken(result[3], 'STRING', 'baz'); }); diff --git a/spec/track-ids.js b/spec/track-ids.js index 7a8b59ee4..30a46617d 100644 --- a/spec/track-ids.js +++ b/spec/track-ids.js @@ -47,12 +47,14 @@ describe('track ids', function() { equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar'); }); it('should note ../ and ./ references', function() { - var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver}}', {trackIds: true}); + var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver this.is.a this}}', {trackIds: true}); var helpers = { - wycats: function(passiveVoice, noun, options) { + wycats: function(passiveVoice, noun, thiz, thiz2, options) { equal(options.ids[0], 'is.a'); equal(options.ids[1], '../slave.driver'); + equal(options.ids[2], 'is.a'); + equal(options.ids[3], ''); return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun; } @@ -188,4 +190,48 @@ describe('track ids', function() { }); }); }); + + describe('partials', function() { + var helpers = { + blockParams: function(name, options) { + return name + ':' + options.ids[0] + '\n'; + }, + wycats: function(name, options) { + return name + ':' + options.data.contextPath + '\n'; + } + }; + + it('should pass track id for basic partial', function() { + var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude}}{{/dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{wycats name}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes.0\nAlan:dudes.1\n'); + }); + + it('should pass track id for context partial', function() { + var template = CompilerContext.compile('Dudes: {{> dude dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{#each this}}{{wycats name}}{{/each}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes..0\nAlan:dudes..1\n'); + }); + + it('should invalidate context for partials with parameters', function() { + var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude . bar="foo"}}{{/dudes}}', {trackIds: true}), + hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; + + var partials = { + dude: CompilerContext.compile('{{wycats name}}', {trackIds: true}) + }; + + equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:true\nAlan:true\n'); + }); + }); }); diff --git a/spec/utils.js b/spec/utils.js index 81732c5e7..7248ac447 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -18,6 +18,7 @@ describe('utils', function() { describe('#escapeExpression', function() { it('shouhld escape html', function() { equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>'); + equals(Handlebars.Utils.escapeExpression('foo='), 'foo='); }); it('should not escape SafeString', function() { var string = new Handlebars.SafeString('foo<&"\'>'); diff --git a/spec/visitor.js b/spec/visitor.js index 1f50d79c1..d3fb795e2 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -8,6 +8,9 @@ describe('Visitor', function() { // stub methods are executed var visitor = new Handlebars.Visitor(); visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); + visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{* bar }}')); }); it('should traverse to stubs', function() { @@ -40,8 +43,6 @@ describe('Visitor', function() { 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() { @@ -49,7 +50,7 @@ describe('Visitor', function() { visitor.mutating = true; visitor.StringLiteral = function(string) { - return new Handlebars.AST.NumberLiteral(42, string.locInfo); + return {type: 'NumberLiteral', value: 42, loc: string.loc}; }; var ast = Handlebars.parse('{{foo foo="foo"}}'); @@ -109,7 +110,7 @@ describe('Visitor', function() { visitor.mutating = true; visitor.StringLiteral = function(string) { - return new Handlebars.AST.NumberLiteral(42, string.locInfo); + return {type: 'NumberLiteral', value: 42, loc: string.locInfo}; }; var ast = Handlebars.parse('{{foo "foo"}}'); diff --git a/src/handlebars.l b/src/handlebars.l index ff2128355..4c3c30421 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -49,12 +49,21 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} return 'CONTENT'; } +// nested raw block will create stacked 'raw' condition +"{{{{"/[^/] this.begin('raw'); return 'CONTENT'; "{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" { - yytext = yytext.substr(5, yyleng-9); this.popState(); - return 'END_RAW_BLOCK'; + // Should be using `this.topState()` below, but it currently + // returns the second top instead of the first top. Opened an + // issue about it at https://github.com/zaach/jison/issues/291 + if (this.conditionStack[this.conditionStack.length-1] === 'raw') { + return 'CONTENT'; + } else { + yytext = yytext.substr(5, yyleng-9); + return 'END_RAW_BLOCK'; + } } -[^\x00]*?/("{{{{/") { return 'CONTENT'; } +[^\x00]*?/("{{{{") { return 'CONTENT'; } [\s\S]*?"--"{RIGHT_STRIP}?"}}" { this.popState(); @@ -71,7 +80,8 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} return 'CLOSE_RAW_BLOCK'; } "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; -"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; +"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK'; +"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK'; "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; "{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; @@ -88,7 +98,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} this.popState(); return 'COMMENT'; } -"{{"{LEFT_STRIP}? return 'OPEN'; +"{{"{LEFT_STRIP}?"*"? return 'OPEN'; "=" return 'EQUALS'; ".." return 'ID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index d67a7da7e..ce0649838 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$)) + : statement* -> yy.prepareProgram($1) ; statement @@ -17,16 +17,29 @@ statement | block -> $1 | rawBlock -> $1 | partial -> $1 + | partialBlock -> $1 | content -> $1 - | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) - ; + | COMMENT { + $$ = { + type: 'CommentStatement', + value: yy.stripComment($1), + strip: yy.stripFlags($1, $1), + loc: yy.locInfo(@$) + }; + }; content - : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$)) - ; + : CONTENT { + $$ = { + type: 'ContentStatement', + original: $1, + value: $1, + loc: yy.locInfo(@$) + }; + }; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) + : openRawBlock content+ END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock @@ -39,7 +52,7 @@ block ; openBlock - : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse @@ -57,7 +70,7 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = new yy.Program([inverse], null, {}, yy.locInfo(@$)); + program = yy.prepareProgram([inverse], $2.loc); program.chained = true; $$ = { strip: $1.strip, program: program, chain: true }; @@ -77,7 +90,23 @@ mustache ; partial - : OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) + : OPEN_PARTIAL partialName param* hash? CLOSE { + $$ = { + type: 'PartialStatement', + name: $2, + params: $3, + hash: $4, + indent: '', + strip: yy.stripFlags($1, $5), + loc: yy.locInfo(@$) + }; + } + ; +partialBlock + : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$) + ; +openPartialBlock + : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } ; param @@ -86,15 +115,22 @@ param ; sexpr - : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$)) - ; + : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR { + $$ = { + type: 'SubExpression', + path: $2, + params: $3, + hash: $4, + loc: yy.locInfo(@$) + }; + }; hash - : hashSegment+ -> new yy.Hash($1, yy.locInfo(@$)) + : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)} ; hashSegment - : ID EQUALS param -> new yy.HashPair(yy.id($1), $3, yy.locInfo(@$)) + : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)} ; blockParams @@ -104,11 +140,11 @@ blockParams 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(@$)) - | UNDEFINED -> new yy.UndefinedLiteral(yy.locInfo(@$)) - | NULL -> new yy.NullLiteral(yy.locInfo(@$)) + | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)} + | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)} + | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)} + | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)} + | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)} ; partialName diff --git a/src/parser-suffix.js b/src/parser-suffix.js index 6e4aa20d6..1f69f7a44 100644 --- a/src/parser-suffix.js +++ b/src/parser-suffix.js @@ -1 +1,2 @@ -export default handlebars; +exports.__esModule = true; +exports['default'] = handlebars; diff --git a/tasks/.eslintrc b/tasks/.eslintrc new file mode 100644 index 000000000..346150294 --- /dev/null +++ b/tasks/.eslintrc @@ -0,0 +1,16 @@ +{ + "globals": { + "require": true + }, + "rules": { + // Disabling for tests, for now. + "no-path-concat": 0, + + "no-var": 0, + "no-shadow": 0, + "handle-callback-err": 0, + "no-console": 0, + "no-process-env": 0, + "dot-notation": [2, {"allowKeywords": true}] + } +} \ No newline at end of file diff --git a/tasks/metrics.js b/tasks/metrics.js index c4a202b15..9044306cf 100644 --- a/tasks/metrics.js +++ b/tasks/metrics.js @@ -36,7 +36,7 @@ module.exports = function(grunt) { return done(); } - emit(keen, events, function(err, res) { + emit(keen, events, function(err) { if (err) { throw err; } diff --git a/tasks/parser.js b/tasks/parser.js index 47533cedf..7ff725850 100644 --- a/tasks/parser.js +++ b/tasks/parser.js @@ -6,7 +6,7 @@ module.exports = function(grunt) { var cmd = './node_modules/.bin/jison'; - if(process.platform === 'win32'){ + if (process.platform === 'win32') { cmd = 'node_modules\\.bin\\jison.cmd'; } diff --git a/tasks/publish.js b/tasks/publish.js index 68b3157aa..55ea20a71 100644 --- a/tasks/publish.js +++ b/tasks/publish.js @@ -60,7 +60,7 @@ module.exports = function(grunt) { async.forEach(_.keys(files), function(file, callback) { var params = {Bucket: bucket, Key: file, Body: grunt.file.read(files[file])}; - s3.putObject(params, function(err, data) { + s3.putObject(params, function(err) { if (err) { throw err; } else { diff --git a/tasks/test.js b/tasks/test.js index ad8a911d3..74473244a 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -40,5 +40,17 @@ module.exports = function(grunt) { done(); }); }); - grunt.registerTask('test', ['test:bin', 'test:cov']); + + grunt.registerTask('test:check-cov', function() { + var done = this.async(); + + var runner = childProcess.fork('node_modules/.bin/istanbul', ['check-coverage', '--statements', '100', '--functions', '100', '--branches', '100', '--lines 100'], {stdio: 'inherit'}); + runner.on('close', function(code) { + if (code != 0) { + grunt.fatal('Coverage check failed: ' + code); + } + done(); + }); + }); + grunt.registerTask('test', ['test:bin', 'test:cov', 'test:check-cov']); }; diff --git a/tasks/util/git.js b/tasks/util/git.js index a6c9ec1dc..03802630d 100644 --- a/tasks/util/git.js +++ b/tasks/util/git.js @@ -50,7 +50,7 @@ module.exports = { childProcess.exec('git rev-parse --short origin/master', {}, function(err, stdout) { // This will error if master was not checked out but in this case we know we are not master // so we can ignore. - if (err && !/Needed a single revision/.test(err.message)) { + if (err && !(/Needed a single revision/.test(err.message))) { throw new Error('git.master: ' + err.message); } @@ -59,7 +59,7 @@ module.exports = { }, add: function(path, callback) { - childProcess.exec('git add -f ' + path, {}, function(err, stdout) { + childProcess.exec('git add -f ' + path, {}, function(err) { if (err) { throw new Error('git.add: ' + err.message); } @@ -68,7 +68,7 @@ module.exports = { }); }, commit: function(name, callback) { - childProcess.exec('git commit --message=' + name, {}, function(err, stdout) { + childProcess.exec('git commit --message=' + name, {}, function(err) { if (err) { throw new Error('git.commit: ' + err.message); } @@ -77,7 +77,7 @@ module.exports = { }); }, tag: function(name, callback) { - childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err, stdout, stderr) { + childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err) { if (err) { throw new Error('git.tag: ' + err.message); } @@ -98,7 +98,7 @@ module.exports = { }); var versionTags = tags.filter(function(info) { - return /^v/.test(info[0]); + return (/^v/.test(info[0])); }); callback(undefined, versionTags[0] || tags[0]); diff --git a/tasks/version.js b/tasks/version.js index e6bfe5943..8bc4d8250 100644 --- a/tasks/version.js +++ b/tasks/version.js @@ -18,9 +18,9 @@ module.exports = function(grunt) { grunt.log.writeln('Updating to version ' + version); async.each([ - ['lib/handlebars/base.js', /var VERSION = ['"](.*)['"];/, 'var VERSION = "' + version + '";'], - ['components/bower.json', /"version":.*/, '"version": "' + version + '",'], - ['components/handlebars.js.nuspec', /.*<\/version>/, '' + version + ''] + ['lib/handlebars/base.js', (/const VERSION = ['"](.*)['"];/), 'const VERSION = \'' + version + '\';'], + ['components/bower.json', (/"version":.*/), '"version": "' + version + '",'], + ['components/handlebars.js.nuspec', (/.*<\/version>/), '' + version + ''] ], function(args, callback) { replace.apply(undefined, args); @@ -33,9 +33,9 @@ module.exports = function(grunt) { }); }); - function replace(path, regex, replace) { + function replace(path, regex, value) { var content = grunt.file.read(path); - content = content.replace(regex, replace); + content = content.replace(regex, value); grunt.file.write(path, content); } };