diff --git a/.gitignore b/.gitignore
index 0e32d042b..3c6d099f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ vendor
lib/handlebars/compiler/parser.js
/dist/
/tmp/
+/coverage/
node_modules
*.sublime-project
*.sublime-workspace
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..1739275a8
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "spec/mustache"]
+ path = spec/mustache
+ url = git://github.com/mustache/spec.git
diff --git a/.istanbul.yml b/.istanbul.yml
new file mode 100644
index 000000000..e6911f190
--- /dev/null
+++ b/.istanbul.yml
@@ -0,0 +1,2 @@
+instrumentation:
+ excludes: ['**/spec/**']
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..75aa9f553
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,77 @@
+# How to Contribute
+
+## Reporting Issues
+
+Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
+
+Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here)
+
+Pull requests containing only failing thats demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
+
+## Pull Requests
+
+We also accept [pull requests][pull-request]!
+
+Generally we like to see pull requests that
+- Maintain the existing code style
+- Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
+- Have [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+- Have tests
+- Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
+
+## Building
+
+To build Handlebars.js you'll need a few things installed.
+
+* Node.js
+* [Grunt](http://gruntjs.com/getting-started)
+
+Project dependencies may be installed via `npm install`.
+
+To build Handlebars.js from scratch, you'll want to run `grunt`
+in the root of the project. That will build Handlebars and output the
+results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
+You can also run our set of benchmarks with `grunt bench`.
+
+The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
+
+If you notice any problems, please report them to the GitHub issue tracker at
+[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
+
+## Ember testing
+
+The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
+
+```sh
+npm link
+grunt build release
+cp dist/*.js $emberRepoDir/bower_components/handlebars/
+
+cd $emberRepoDir
+npm link handlebars
+npm test
+```
+
+## Releasing
+
+Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
+
+A full release may be completed with the following:
+
+```
+yo release
+npm publish
+yo release:publish cdnjs handlebars.js dist/cdnjs/
+yo release:publish components handlebars.js dist/components/
+
+cd dist/components/
+gem build handlebars-source.gemspec
+gem push handlebars-source-*.gem
+```
+
+After this point the handlebars site needs to be updated to point to the new version numbers. The jsfiddle link should be updated to point to the most recent distribution for all instances in our documentation.
+
+[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/11/
diff --git a/FAQ.md b/FAQ.md
new file mode 100644
index 000000000..0bd0997d6
--- /dev/null
+++ b/FAQ.md
@@ -0,0 +1,60 @@
+# Frequently Asked Questions
+
+1. How can I file a bug report:
+
+ See our guidelines on [reporting issues](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
+
+1. Why isn't my Mustache template working?
+
+ Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/wycats/handlebars.js#differences-between-handlebarsjs-and-mustache).
+
+1. Why is it slower when compiling?
+
+ The Handlebars compiler must parse the template and construct a JavaScript program which can then be run. Under some environments such as older mobile devices this can have a performance impact which can be avoided by precompiling. Generally it's recommended that precompilation and the runtime library be used on all clients.
+
+1. Why doesn't this work with Content Security Policy restrictions?
+
+ When not using the precompiler, Handlebars generates a dynamic function for each template which can cause issues with pages that have enabled Content Policy. It's recommended that templates are precompiled or the `unsafe-eval` policy is enabled for sites that must generate dynamic templates at runtime.
+
+1. How can I include script tags in my template?
+
+ If loading the template via an inlined `
+ ```
+
+ It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue.
+
+1. Why are my precompiled scripts throwing exceptions?
+
+ When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via:
+
+ ```sh
+ handlebars --version
+ ```
+ If using the integrated precompiler and
+
+ ```javascript
+ console.log(Handlebars.VERSION);
+ ```
+ On the client side.
+
+ We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler.
+
+ Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
+
+1. Why doesn't IE like the `default` name in the AMD module?
+
+ Some browsers such as particular versions of IE treat `default` as a reserved word in JavaScript source files. To safely use this you need to reference this via the `Handlebars['default']` lookup method. This is an unfortunate side effect of the shims necessary to backport the Handlebars ES6 code to all current browsers.
+
+1. How do I load the runtime library when using AMD?
+
+ There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/wycats/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
+
+ The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
+
+ If not using ES6 transpilers or accessing submodules in the build the former option should be sufficent for most use cases.
diff --git a/Gruntfile.js b/Gruntfile.js
index 4e12ef734..b73974059 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -43,7 +43,7 @@ module.exports = function(grunt) {
packager: {
global: {
- type: 'global',
+ type: 'umd',
export: 'Handlebars',
files: [{
cwd: 'lib/',
@@ -138,13 +138,22 @@ module.exports = function(grunt) {
browsers: [
{browserName: 'chrome'},
{browserName: 'firefox'},
- {browserName: 'firefox', version: '3.6'},
{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'},
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'},
- {browserName: 'internet explorer', version: 9, platform: 'Windows 7'},
- {browserName: 'internet explorer', version: 6, platform: 'XP'}
+ {browserName: 'internet explorer', version: 9, platform: 'Windows 7'}
+ ]
+ }
+ },
+ sanity: {
+ options: {
+ build: process.env.TRAVIS_JOB_ID,
+ urls: ['http://localhost:9999/spec/umd.html?headless=true', 'http://localhost:9999/spec/amd-runtime.html?headless=true', 'http://localhost:9999/spec/umd-runtime.html?headless=true'],
+ detailedError: true,
+ concurrency: 2,
+ browsers: [
+ {browserName: 'chrome'}
]
}
}
diff --git a/README.markdown b/README.markdown
index a675c0062..77552860d 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,4 +1,4 @@
-[](https://travis-ci.org/wycats/handlebars.js)
+[](https://travis-ci.org/wycats/handlebars.js)
[](https://saucelabs.com/u/handlebars)
Handlebars.js
@@ -252,6 +252,14 @@ You can also use real html comments if you want them to end up in the output.
```
+### 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
----------------------
@@ -291,9 +299,8 @@ 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 using
-the `Handlebars.template` method and the resulting object may be as
-normal.
+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
@@ -336,13 +343,7 @@ does have some big performance advantages. Justin Marney, a.k.a.
[gotascii](http://github.com/gotascii), confirmed that with an
[independent test](http://sorescode.com/2010/09/12/benchmarks.html). The
rewritten Handlebars (current version) is faster than the old version,
-and we will have some benchmarks in the near future.
-
-
-Building
---------
-
-To build handlebars, just run `grunt build`, and the build will output to the `dist` directory.
+with many [performance tests](https://travis-ci.org/wycats/handlebars.js/builds/33392182#L538) being 5 to 7 times faster than the Mustache equivalent.
Upgrading
@@ -352,16 +353,9 @@ See [release-notes.md](https://github.com/wycats/handlebars.js/blob/master/relea
Known Issues
------------
-* Runtime/precompiler mismatches: Often result in errors like "can not find method match of object" or similar. Please verify the version of the runtime and the version used to precompile templates if odd issues occur after upgrading one component or another.
-* Handlebars.js can be cryptic when there's an error while rendering.
-* Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`)
-Reporting Issues
-----------------
+See [FAQ.md](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
-Should you run into other issues with the project, please file an [issue][issue]. When filing issues a repo case running against the latest version of the code is appreciated. A [jsfiddle template][jsfiddle] is available for this purpose. As new versions are released the bitly link will be updated to point to a fiddle template with the latest version.
-
-We also accept [pull requests][pull-request]!
Handlebars in the Wild
----------------------
@@ -402,68 +396,10 @@ External Resources
Have a project using Handlebars? Send us a [pull request][pull-request]!
-Helping Out
------------
-
-To build Handlebars.js you'll need a few things installed.
-
-* Node.js
-* [Grunt](http://gruntjs.com/getting-started)
-
-Project dependencies may be installed via `npm install`.
-
-To build Handlebars.js from scratch, you'll want to run `grunt`
-in the root of the project. That will build Handlebars and output the
-results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
-You can also run our set of benchmarks with `grunt bench`.
-
-The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
-
-If you notice any problems, please report them to the GitHub issue tracker at
-[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
-Feel free to contact commondream or wycats through GitHub with any other
-questions or feature requests. To submit changes fork the project and
-send a pull request.
-
-### Ember testing
-
-The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
-
-```sh
-grunt build release
-export HANDLEBARS_PATH=`pwd`
-
-cd $emberRepoDir
-bundle exec rake clean
-bundle exec rake test
-```
-
-### Releasing
-
-Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
-
-A full release may be completed with the following:
-
-```
-yo release
-npm publish
-yo release:publish cdnjs handlebars.js dist/cdnjs/
-yo release:publish components handlebars.js dist/components/
-
-cd dist/components/
-gem build handlebars-source.gemspec
-gem push handlebars-source-*.gem
-```
-
-After this point the handlebars site needs to be updated to point to the new version numbers. The jsfiddle bitly link should be updated to point to the most recent distribution.
-
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
-[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://l.kde.cc/hbs-bug
diff --git a/bench/templates/paths.js b/bench/templates/paths.js
index 0a426dd83..d84e06152 100644
--- a/bench/templates/paths.js
+++ b/bench/templates/paths.js
@@ -1,7 +1,7 @@
module.exports = {
- context: { person: { name: "Larry", age: 45 } },
- handlebars: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}",
- dust: "{person.name}{person.age}{person.foo}{animal.age}",
- eco: "<%= @person.name %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
- mustache: "{{person.name}}{{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/throughput.js b/bench/throughput.js
index 308446a09..d27a94d31 100644
--- a/bench/throughput.js
+++ b/bench/throughput.js
@@ -28,11 +28,13 @@ function makeSuite(bench, name, template, handlebarsOnly) {
partials = template.partials,
handlebarsOut,
+ compatOut,
dustOut,
ecoOut,
mustacheOut;
var handlebar = Handlebars.compile(template.handlebars, {data: false}),
+ 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}));
@@ -43,6 +45,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
handlebar(context, options);
});
+ compatOut = compat(context, options);
+ bench("compat", function() {
+ compat(context, options);
+ });
+
if (handlebarsOnly) {
return;
}
@@ -107,6 +114,7 @@ function makeSuite(bench, name, template, handlebarsOnly) {
}
}
+ compare(compatOut, 'compat');
compare(dustOut, 'dust');
compare(ecoOut, 'eco');
compare(mustacheOut, 'mustache');
diff --git a/bin/handlebars b/bin/handlebars
index 8794faa60..4ea329661 100755
--- a/bin/handlebars
+++ b/bin/handlebars
@@ -22,7 +22,7 @@ var optimist = require('optimist')
'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath',
- 'default': ''
+ 'default': ''
},
'k': {
'type': 'string',
@@ -87,149 +87,9 @@ var optimist = require('optimist')
if (argv.version) {
return;
}
-
- var template = [0];
- if (!argv._.length) {
- throw 'Must define at least one template or directory.';
- }
-
- argv._.forEach(function(template) {
- try {
- fs.statSync(template);
- } catch (err) {
- throw 'Unable to open template file "' + template + '"';
- }
- });
- })
- .check(function(argv) {
- if (argv.simple && argv.min) {
- throw 'Unable to minimze simple output';
- }
- if (argv.simple && (argv._.length !== 1 || fs.statSync(argv._[0]).isDirectory())) {
- throw 'Unable to output multiple templates in simple mode';
- }
});
-var fs = require('fs'),
- handlebars = require('../lib'),
- basename = require('path').basename,
- uglify = require('uglify-js');
-
-var argv = optimist.argv,
- template = argv._[0];
-
-if (argv.version) {
- return console.log(handlebars.VERSION);
-}
-
-// Convert the known list into a hash
-var known = {};
-if (argv.known && !Array.isArray(argv.known)) {
- argv.known = [argv.known];
-}
-if (argv.known) {
- for (var i = 0, len = argv.known.length; i < len; i++) {
- known[argv.known[i]] = true;
- }
-}
-
-// Build file extension pattern
-var extension = argv.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
-extension = new RegExp('\\.' + extension + '$');
-
-var output = [];
-if (!argv.simple) {
- if (argv.amd) {
- output.push('define([\'' + argv.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
- } else if (argv.commonjs) {
- output.push('var Handlebars = require("' + argv.commonjs + '");');
- } else {
- output.push('(function() {\n');
- }
- output.push(' var template = Handlebars.template, templates = ');
- output.push(argv.namespace);
- output.push(' = ');
- output.push(argv.namespace);
- output.push(' || {};\n');
-}
-function processTemplate(template, root, explicit) {
- var path = template,
- stat = fs.statSync(path);
- if (stat.isDirectory()) {
- fs.readdirSync(template).map(function(file) {
- var path = template + '/' + file;
-
- if (extension.test(path) || fs.statSync(path).isDirectory()) {
- processTemplate(path, root || template);
- }
- });
- } else if (explicit || extension.test(path)) {
- var data = fs.readFileSync(path, 'utf8');
-
- if (argv.bom && data.indexOf('\uFEFF') === 0) {
- data = data.substring(1);
- }
-
- var options = {
- knownHelpers: known,
- knownHelpersOnly: argv.o
- };
-
- if (argv.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, '');
-
- if (argv.simple) {
- output.push(handlebars.precompile(data, options) + '\n');
- } else if (argv.partial) {
- if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
- output.push('return ');
- }
- output.push('Handlebars.partials[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
- } else {
- if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
- output.push('return ');
- }
- output.push('templates[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
- }
- }
-}
-
-argv._.forEach(function(template) {
- processTemplate(template, argv.root, true);
-});
-
-// Output the content
-if (!argv.simple) {
- if (argv.amd) {
- if(argv._.length > 1 || (argv._.length == 1 && fs.statSync(argv._[0]).isDirectory())) {
- if(argv.partial){
- output.push('return Handlebars.partials;\n');
- } else {
- output.push('return templates;\n');
- }
- }
- output.push('});');
- } else if (!argv.commonjs) {
- output.push('})();');
- }
-}
-output = output.join('');
-
-if (argv.min) {
- output = uglify.minify(output, {fromString: true}).code;
-}
-
-if (argv.output) {
- fs.writeFileSync(argv.output, output, 'utf8');
-} else {
- console.log(output);
-}
+var argv = optimist.argv;
+argv.templates = argv._;
+delete argv._;
+return require('../lib/precompiler').cli(argv);
diff --git a/components/bower.json b/components/bower.json
index efcfbf584..d91be56b0 100644
--- a/components/bower.json
+++ b/components/bower.json
@@ -1,6 +1,6 @@
{
"name": "handlebars",
- "version": "2.0.0-alpha.4",
+ "version": "2.0.0-beta.1",
"main": "handlebars.js",
"dependencies": {}
}
diff --git a/components/handlebars.js.nuspec b/components/handlebars.js.nuspec
index 7cf788646..8e676c7ab 100644
--- a/components/handlebars.js.nuspec
+++ b/components/handlebars.js.nuspec
@@ -2,7 +2,7 @@
handlebars.js
- 2.0.0-alpha.4
+ 2.0.0-beta.1
handlebars.js Authors
https://github.com/wycats/handlebars.js/blob/master/LICENSE
https://github.com/wycats/handlebars.js/
diff --git a/lib/handlebars.js b/lib/handlebars.js
index ffa9c7ab2..039ab3e17 100644
--- a/lib/handlebars.js
+++ b/lib/handlebars.js
@@ -30,4 +30,6 @@ var create = function() {
Handlebars = create();
Handlebars.create = create;
+Handlebars['default'] = Handlebars;
+
export default Handlebars;
diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js
index e77aae000..bc07714f2 100644
--- a/lib/handlebars.runtime.js
+++ b/lib/handlebars.runtime.js
@@ -16,6 +16,7 @@ var create = function() {
hb.SafeString = SafeString;
hb.Exception = Exception;
hb.Utils = Utils;
+ hb.escapeExpression = Utils.escapeExpression;
hb.VM = runtime;
hb.template = function(spec) {
@@ -28,4 +29,6 @@ var create = function() {
var Handlebars = create();
Handlebars.create = create;
+Handlebars['default'] = Handlebars;
+
export default Handlebars;
diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js
index 3cb432352..9ecf6f4f1 100644
--- a/lib/handlebars/base.js
+++ b/lib/handlebars/base.js
@@ -1,15 +1,16 @@
module Utils from "./utils";
import Exception from "./exception";
-export var VERSION = "2.0.0-alpha.4";
-export var COMPILER_REVISION = 5;
+export var VERSION = "2.0.0-beta.1";
+export var COMPILER_REVISION = 6;
export var REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '== 1.x.x',
- 5: '>= 2.0.0'
+ 5: '== 2.0.0-alpha.x',
+ 6: '>= 2.0.0-beta.1'
};
var isArray = Utils.isArray,
@@ -30,12 +31,11 @@ HandlebarsEnvironment.prototype = {
logger: logger,
log: log,
- registerHelper: function(name, fn, inverse) {
+ registerHelper: function(name, fn) {
if (toString.call(name) === objectType) {
- if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); }
+ if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name);
} else {
- if (inverse) { fn.not = inverse; }
this.helpers[name] = fn;
}
},
@@ -67,9 +67,8 @@ function registerDefaultHelpers(instance) {
});
instance.registerHelper('blockHelperMissing', function(context, options) {
- var inverse = options.inverse || function() {}, fn = options.fn;
-
- if (isFunction(context)) { context = context.call(this); }
+ var inverse = options.inverse,
+ fn = options.fn;
if(context === true) {
return fn(this);
@@ -97,10 +96,8 @@ function registerDefaultHelpers(instance) {
});
instance.registerHelper('each', function(context, options) {
- // Allow for {{#each}}
if (!options) {
- options = context;
- context = this;
+ throw new Exception('Must pass iterator to #each');
}
var fn = options.fn, inverse = options.inverse;
@@ -187,6 +184,8 @@ function registerDefaultHelpers(instance) {
}
return fn(context, options);
+ } else {
+ return options.inverse(this);
}
});
@@ -195,7 +194,7 @@ function registerDefaultHelpers(instance) {
instance.log(level, context);
});
- instance.registerHelper('lookup', function(obj, field, options) {
+ instance.registerHelper('lookup', function(obj, field) {
return obj && obj[field];
});
}
@@ -221,7 +220,7 @@ export var logger = {
}
};
-export function log(level, obj) { logger.log(level, obj); }
+export var log = logger.log;
export var createFrame = function(object) {
var frame = Utils.extend({}, object);
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index e388e54e1..49bdc3337 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -1,6 +1,6 @@
import Exception from "../exception";
-function LocationInfo(locInfo){
+function LocationInfo(locInfo) {
locInfo = locInfo || {};
this.firstLine = locInfo.first_line;
this.firstColumn = locInfo.first_column;
@@ -9,38 +9,11 @@ function LocationInfo(locInfo){
}
var AST = {
- ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
- var inverseLocationInfo, firstInverseNode;
- if (arguments.length === 3) {
- locInfo = inverse;
- inverse = null;
- } else if (arguments.length === 2) {
- locInfo = inverseStrip;
- inverseStrip = null;
- }
-
+ ProgramNode: function(statements, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "program";
this.statements = statements;
- this.strip = {};
-
- if(inverse) {
- firstInverseNode = inverse[0];
- if (firstInverseNode) {
- inverseLocationInfo = {
- first_line: firstInverseNode.firstLine,
- last_line: firstInverseNode.lastLine,
- last_column: firstInverseNode.lastColumn,
- first_column: firstInverseNode.firstColumn
- };
- this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
- } else {
- this.inverse = new AST.ProgramNode(inverse, inverseStrip);
- }
- this.strip.right = inverseStrip.left;
- } else if (inverseStrip) {
- this.strip.left = inverseStrip.right;
- }
+ this.strip = strip;
},
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
@@ -64,8 +37,6 @@ var AST = {
this.sexpr = new AST.SexprNode(rawParams, hash);
}
- this.sexpr.isRoot = true;
-
// Support old AST API that stored this info in MustacheNode
this.id = this.sexpr.id;
this.params = this.sexpr.params;
@@ -104,27 +75,18 @@ var AST = {
this.context = context;
this.hash = hash;
this.strip = strip;
+
+ this.strip.inlineStandalone = true;
},
- BlockNode: function(mustache, program, inverse, close, locInfo) {
+ BlockNode: function(mustache, program, inverse, strip, locInfo) {
LocationInfo.call(this, locInfo);
- if(mustache.sexpr.id.original !== close.path.original) {
- throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this);
- }
-
this.type = 'block';
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
-
- this.strip = {
- left: mustache.strip.left,
- right: close.strip.right
- };
-
- (program || inverse).strip.left = mustache.strip.right;
- (inverse || program).strip.right = close.strip.left;
+ this.strip = strip;
if (inverse && !program) {
this.isInverse = true;
@@ -142,13 +104,13 @@ var AST = {
this.type = 'block';
this.mustache = mustache;
- this.program = new AST.ProgramNode([content], locInfo);
+ this.program = new AST.ProgramNode([content], {}, locInfo);
},
ContentNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "content";
- this.string = string;
+ this.original = this.string = string;
},
HashNode: function(pairs, locInfo) {
@@ -238,9 +200,14 @@ var AST = {
LocationInfo.call(this, locInfo);
this.type = "comment";
this.comment = comment;
+
+ this.strip = {
+ inlineStandalone: true
+ };
}
};
+
// Must be exported as an object rather than the root of the module as the jison lexer
// most modify the object to operate properly.
export default AST;
diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js
index 722f09af5..13784637e 100644
--- a/lib/handlebars/compiler/base.js
+++ b/lib/handlebars/compiler/base.js
@@ -1,12 +1,18 @@
import parser from "./parser";
import AST from "./ast";
+module Helpers from "./helpers";
+import { extend } from "../utils";
export { parser };
+var yy = {};
+extend(yy, Helpers, AST);
+
export function parse(input) {
// Just return if an already-compile AST was passed in.
- if(input.constructor === AST.ProgramNode) { return input; }
+ if (input.constructor === AST.ProgramNode) { return input; }
+
+ parser.yy = yy;
- parser.yy = AST;
return parser.parse(input);
}
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index e5f2280ac..1aba34b4e 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -1,4 +1,7 @@
import Exception from "../exception";
+import {isArray} from "../utils";
+
+var slice = [].slice;
export function Compiler() {}
@@ -10,30 +13,6 @@ export function Compiler() {}
Compiler.prototype = {
compiler: Compiler,
- disassemble: function() {
- var opcodes = this.opcodes, opcode, out = [], params, param;
-
- for (var i=0, l=opcodes.length; i"'`]/g;
var possible = /[&<>"'`]/;
function escapeChar(chr) {
- return escape[chr] || "&";
+ return escape[chr];
}
export function extend(obj /* , ...source */) {
@@ -37,6 +37,7 @@ var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
+/* istanbul ignore next */
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
@@ -44,6 +45,7 @@ if (isFunction(/x/)) {
}
export var isFunction;
+/* istanbul ignore next */
export var isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
@@ -53,8 +55,10 @@ export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof SafeString) {
return string.toString();
- } else if (!string && string !== 0) {
+ } else if (string == null) {
return "";
+ } else if (!string) {
+ return string + '';
}
// Force a string conversion as this will be done by the append regardless and
diff --git a/lib/index.js b/lib/index.js
index e150524bd..790aab73a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -14,6 +14,7 @@ handlebars.print = printer.print;
module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files
+/* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) {
var extension = function(module, filename) {
var fs = require("fs");
diff --git a/lib/precompiler.js b/lib/precompiler.js
new file mode 100644
index 000000000..676402226
--- /dev/null
+++ b/lib/precompiler.js
@@ -0,0 +1,144 @@
+
+var fs = require('fs'),
+ Handlebars = require('./index'),
+ basename = require('path').basename,
+ uglify = require('uglify-js');
+
+module.exports.cli = function(opts) {
+ if (opts.version) {
+ console.log(Handlebars.VERSION);
+ return;
+ }
+
+ var template = [0];
+ if (!opts.templates.length) {
+ 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 minimze simple output');
+ }
+ if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) {
+ throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
+ }
+
+ // Convert the known list into a hash
+ var known = {};
+ if (opts.known && !Array.isArray(opts.known)) {
+ opts.known = [opts.known];
+ }
+ if (opts.known) {
+ for (var i = 0, len = opts.known.length; i < len; i++) {
+ known[opts.known[i]] = true;
+ }
+ }
+
+ // Build file extension pattern
+ var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
+ extension = new RegExp('\\.' + extension + '$');
+
+ var output = [];
+ if (!opts.simple) {
+ if (opts.amd) {
+ output.push('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
+ } else if (opts.commonjs) {
+ output.push('var Handlebars = require("' + opts.commonjs + '");');
+ } else {
+ output.push('(function() {\n');
+ }
+ output.push(' var template = Handlebars.template, templates = ');
+ output.push(opts.namespace);
+ output.push(' = ');
+ output.push(opts.namespace);
+ output.push(' || {};\n');
+ }
+ function processTemplate(template, root) {
+ var path = template,
+ stat = fs.statSync(path);
+ if (stat.isDirectory()) {
+ fs.readdirSync(template).map(function(file) {
+ var path = template + '/' + file;
+
+ if (extension.test(path) || fs.statSync(path).isDirectory()) {
+ processTemplate(path, root || template);
+ }
+ });
+ } else {
+ var data = fs.readFileSync(path, 'utf8');
+
+ if (opts.bom && data.indexOf('\uFEFF') === 0) {
+ data = data.substring(1);
+ }
+
+ var options = {
+ knownHelpers: known,
+ knownHelpersOnly: opts.o
+ };
+
+ 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, '');
+
+ if (opts.simple) {
+ output.push(Handlebars.precompile(data, options) + '\n');
+ } else if (opts.partial) {
+ if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
+ output.push('return ');
+ }
+ output.push('Handlebars.partials[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n');
+ } else {
+ if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
+ output.push('return ');
+ }
+ output.push('templates[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\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.push('return Handlebars.partials;\n');
+ } else {
+ output.push('return templates;\n');
+ }
+ }
+ output.push('});');
+ } else if (!opts.commonjs) {
+ output.push('})();');
+ }
+ }
+ output = output.join('');
+
+ if (opts.min) {
+ output = uglify.minify(output, {fromString: true}).code;
+ }
+
+ if (opts.output) {
+ fs.writeFileSync(opts.output, output, 'utf8');
+ } else {
+ console.log(output);
+ }
+};
diff --git a/package.json b/package.json
index b8b3b3bca..1759b1c60 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
- "version": "2.0.0-alpha.4",
+ "version": "2.0.0-beta.1",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
@@ -32,6 +32,7 @@
"benchmark": "~1.0",
"dustjs-linkedin": "~2.0.2",
"eco": "~1.1.0-rc-3",
+ "es6-module-packager": "1.x",
"grunt": "~0.4.1",
"grunt-cli": "~0.1.10",
"grunt-contrib-clean": "~0.4.1",
@@ -42,11 +43,11 @@
"grunt-contrib-requirejs": "~0.4.1",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-watch": "~0.5.3",
- "grunt-saucelabs": "~5.0.1",
- "es6-module-packager": "1.x",
+ "grunt-saucelabs": "8.x",
+ "istanbul": "^0.3.0",
"jison": "~0.3.0",
"keen.io": "0.0.3",
- "mocha": "*",
+ "mocha": "~1.20.0",
"mustache": "~0.7.2",
"semver": "~2.1.0",
"underscore": "~1.5.1"
diff --git a/release-notes.md b/release-notes.md
index 981231e17..48cf7cf0d 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -2,7 +2,50 @@
## Development
-[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.4...master)
+[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-beta.1...master)
+
+## v2.0.0-beta.1 - August 26th, 2014
+- [#787](https://github.com/wycats/handlebars.js/pull/787) - Remove whitespace surrounding standalone statements ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#827](https://github.com/wycats/handlebars.js/issues/827) - Render false literal as “false” ([@scoot557](https://api.github.com/users/scoot557))
+- [#767](https://github.com/wycats/handlebars.js/issues/767) - Subexpressions bug with hash and context ([@evensoul](https://api.github.com/users/evensoul))
+- Changes to 0/undefined handling
+ - [#731](https://github.com/wycats/handlebars.js/pull/731) - Strange behavior for {{#foo}} {{bar}} {{/foo}} when foo is 0 ([@kpdecker](https://api.github.com/users/kpdecker))
+ - [#820](https://github.com/wycats/handlebars.js/issues/820) - strange behavior for {{foo.bar}} when foo is 0 or null or false ([@zordius](https://api.github.com/users/zordius))
+ - [#837](https://github.com/wycats/handlebars.js/issues/837) - Strange input for custom helper ( foo.bar == false when foo is undefined ) ([@zordius](https://api.github.com/users/zordius))
+- [#819](https://github.com/wycats/handlebars.js/pull/819) - Implement recursive field lookup ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#764](https://github.com/wycats/handlebars.js/issues/764) - This reference not working for helpers ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#773](https://github.com/wycats/handlebars.js/issues/773) - Implicit parameters in {{#each}} introduces a peculiarity in helpers calling convention ([@Bertrand](https://api.github.com/users/Bertrand))
+- [#783](https://github.com/wycats/handlebars.js/issues/783) - helperMissing and consistency for different expression types ([@ErisDS](https://api.github.com/users/ErisDS))
+- [#795](https://github.com/wycats/handlebars.js/pull/795) - Turn the precompile script into a wrapper around a module. ([@jwietelmann](https://api.github.com/users/jwietelmann))
+- [#823](https://github.com/wycats/handlebars.js/pull/823) - Support inverse sections on the with helper ([@dan-manges](https://api.github.com/users/dan-manges))
+- [#834](https://github.com/wycats/handlebars.js/pull/834) - Refactor blocks, programs and inverses ([@mmun](https://api.github.com/users/mmun))
+- [#852](https://github.com/wycats/handlebars.js/issues/852) - {{foo~}} space control behavior is different from older version ([@zordius](https://api.github.com/users/zordius))
+- [#835](https://github.com/wycats/handlebars.js/issues/835) - Templates overwritten if file is loaded twice
+
+- Expose escapeExpression on the root object - 980c38c
+- Remove nested function eval in blockHelperMissing - 6f22ec1
+- Fix compiler program de-duping - 9e3f824
+
+Compatibility notes:
+- The default build now outputs a generic UMD wrapper. This should be transparent change but may cause issues in some environments.
+- Runtime compatibility breaks in both directions. Ensure that both compiler and client are upgraded to 2.0.0-beta.1 or higher at the same time.
+ - `programWithDepth` has been removed an instead an array of context values is passed to fields needing depth lookups.
+- `false` values are now printed to output rather than silently dropped
+- Lines containing only block statements and whitespace are now removed. This matches the Mustache spec but may cause issues with code that expects whitespace to exist but would not otherwise.
+- Partials that are standalone will now indent their rendered content
+- `AST.ProgramNode`'s signature has changed.
+- Numerious methods/features removed from psuedo-API classes
+ - `JavaScriptCompiler.register`
+ - `JavaScriptCompiler.replaceStack` no longer supports non-inline replace
+ - `Compiler.disassemble`
+ - `DECLARE` opcode
+ - `strip` opcode
+ - `lookup` opcode
+ - Content nodes may have their `string` values mutated over time. `original` field provides the unmodified value.
+- Removed unused `Handlebars.registerHelper` `inverse` parameter
+- `each` helper requires iterator parameter
+
+[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.4...v2.0.0-beta.1)
## v2.0.0-alpha.4 - May 19th, 2014
- Expose setup wrappers for compiled templates - 3638874
diff --git a/spec/amd-runtime.html b/spec/amd-runtime.html
new file mode 100644
index 000000000..19bccdbf5
--- /dev/null
+++ b/spec/amd-runtime.html
@@ -0,0 +1,89 @@
+
+
+ Mocha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/ast.js b/spec/ast.js
index 17a982c41..c28e876e8 100644
--- a/spec/ast.js
+++ b/spec/ast.js
@@ -68,17 +68,9 @@ describe('ast', function() {
});
});
describe('BlockNode', function() {
- it('should throw on mustache mismatch (old sexpr-less version)', function() {
- shouldThrow(function() {
- var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
- new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
- }, Handlebars.Exception, "foo doesn't match bar");
- });
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
- var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
- var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
- new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}, {first_line: 2, first_column: 2});
+ handlebarsEnv.parse("\n {{#foo}}{{/bar}}");
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
});
@@ -86,7 +78,7 @@ describe('ast', function() {
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
var block = new handlebarsEnv.AST.BlockNode(mustacheNode,
- {strip: {}}, {strip: {}},
+ {statements: [], strip: {}}, {statements: [], strip: {}},
{
strip: {},
path: {original: 'foo'}
@@ -197,32 +189,11 @@ describe('ast', function() {
testLocationInfoStorage(pn);
});
});
- describe("ProgramNode", function(){
-
- describe("storing location info", function(){
- it("stores when `inverse` argument isn't passed", function(){
- var pn = new handlebarsEnv.AST.ProgramNode([], LOCATION_INFO);
- testLocationInfoStorage(pn);
- });
- it("stores when `inverse` or `stripInverse` arguments passed", function(){
- var pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, undefined, LOCATION_INFO);
- testLocationInfoStorage(pn);
-
- var clone = {
- strip: {},
- firstLine: 0,
- lastLine: 0,
- firstColumn: 0,
- lastColumn: 0
- };
- pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, [ clone ], LOCATION_INFO);
- testLocationInfoStorage(pn);
-
- // Assert that the newly created ProgramNode has the same location
- // information as the inverse
- testLocationInfoStorage(pn.inverse);
- });
+ describe('ProgramNode', function(){
+ it('storing location info', function(){
+ var pn = new handlebarsEnv.AST.ProgramNode([], {}, LOCATION_INFO);
+ testLocationInfoStorage(pn);
});
});
@@ -251,8 +222,7 @@ describe('ast', function() {
});
it('gets line numbers correct when newlines appear', function(){
- var secondContentNode = statements[2];
- testColumns(secondContentNode, 1, 2, 21, 8);
+ testColumns(statements[2], 1, 2, 21, 8);
});
it('gets MustacheNode line numbers correct across newlines', function(){
@@ -265,12 +235,139 @@ describe('ast', function() {
testColumns(blockHelperNode, 3, 7, 8, 23);
});
+ it('correctly records the line numbers the program of a block helper', function(){
+ var blockHelperNode = statements[5],
+ program = blockHelperNode.program;
+
+ testColumns(program, 3, 5, 8, 5);
+ });
+
it('correctly records the line numbers of an inverse of a block helper', function(){
var blockHelperNode = statements[5],
inverse = blockHelperNode.inverse;
- testColumns(inverse, 5, 6, 13, 0);
+ testColumns(inverse, 5, 7, 5, 0);
});
});
+
+ describe('standalone flags', function(){
+ describe('mustache', function() {
+ it('does not mark mustaches as standalone', function() {
+ var ast = Handlebars.parse(' {{comment}} ');
+ equals(!!ast.statements[0].string, true);
+ equals(!!ast.statements[2].string, true);
+ });
+ });
+ describe('blocks', function() {
+ it('marks block mustaches as standalone', function() {
+ var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
+ block = ast.statements[1];
+
+ equals(ast.statements[0].string, '');
+
+ equals(block.program.statements[0].string, 'foo\n');
+ equals(block.inverse.statements[0].string, ' bar \n');
+
+ equals(ast.statements[2].string, '');
+ });
+ it('marks initial block mustaches as standalone', function() {
+ var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
+ block = ast.statements[0];
+
+ equals(block.program.statements[0].string, 'foo\n');
+ });
+ it('marks mustaches with children as standalone', function() {
+ var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
+ block = ast.statements[0];
+
+ equals(block.program.statements[0].string, '');
+ equals(block.program.statements[1].id.original, 'foo');
+ equals(block.program.statements[2].string, '\n');
+ });
+ it('marks nested block mustaches as standalone', function() {
+ var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
+ statements = ast.statements[0].program.statements,
+ block = statements[1];
+
+ equals(statements[0].string, '');
+
+ equals(block.program.statements[0].string, 'foo\n');
+ equals(block.inverse.statements[0].string, ' bar \n');
+
+ equals(statements[0].string, '');
+ });
+ it('does not mark nested block mustaches as standalone', function() {
+ var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'),
+ statements = ast.statements[0].program.statements,
+ block = statements[1];
+
+ equals(statements[0].omit, undefined);
+
+ equals(block.program.statements[0].string, ' \nfoo\n');
+ equals(block.inverse.statements[0].string, ' bar \n ');
+
+ equals(statements[0].omit, undefined);
+ });
+ it('does not mark nested initial block mustaches as standalone', function() {
+ var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'),
+ statements = ast.statements[0].program.statements,
+ block = statements[0];
+
+ equals(block.program.statements[0].string, ' \nfoo\n');
+ equals(block.inverse.statements[0].string, ' bar \n ');
+
+ equals(statements[0].omit, undefined);
+ });
+
+ it('marks column 0 block mustaches as standalone', function() {
+ var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
+ block = ast.statements[1];
+
+ equals(ast.statements[0].omit, undefined);
+
+ equals(block.program.statements[0].string, 'foo\n');
+ equals(block.inverse.statements[0].string, ' bar \n');
+
+ equals(ast.statements[2].string, '');
+ });
+ });
+ describe('partials', function() {
+ it('marks partial as standalone', function() {
+ var ast = Handlebars.parse('{{> partial }} ');
+ equals(ast.statements[1].string, '');
+ });
+ it('marks indented partial as standalone', function() {
+ var ast = Handlebars.parse(' {{> partial }} ');
+ equals(ast.statements[0].string, '');
+ equals(ast.statements[1].indent, ' ');
+ equals(ast.statements[2].string, '');
+ });
+ it('marks those around content as not standalone', function() {
+ var ast = Handlebars.parse('a{{> partial }}');
+ equals(ast.statements[0].omit, undefined);
+
+ ast = Handlebars.parse('{{> partial }}a');
+ equals(ast.statements[1].omit, undefined);
+ });
+ });
+ describe('comments', function() {
+ it('marks comment as standalone', function() {
+ var ast = Handlebars.parse('{{! comment }} ');
+ equals(ast.statements[1].string, '');
+ });
+ it('marks indented comment as standalone', function() {
+ var ast = Handlebars.parse(' {{! comment }} ');
+ equals(ast.statements[0].string, '');
+ equals(ast.statements[2].string, '');
+ });
+ it('marks those around content as not standalone', function() {
+ var ast = Handlebars.parse('a{{! comment }}');
+ equals(ast.statements[0].omit, undefined);
+
+ ast = Handlebars.parse('{{! comment }}a');
+ equals(ast.statements[1].omit, undefined);
+ });
+ });
+ });
});
diff --git a/spec/basic.js b/spec/basic.js
index 8aa54b98c..8a9c116cf 100644
--- a/spec/basic.js
+++ b/spec/basic.js
@@ -50,6 +50,14 @@ describe("basic context", function() {
shouldCompileTo("num: {{.}}", 0, "num: 0");
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
});
+ it('false', function() {
+ shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
+ shouldCompileTo('val: {{.}}', false, 'val: false');
+ shouldCompileTo('val: {{val1/val2}}', {val1: {val2: false}}, 'val: false');
+
+ shouldCompileTo('val1: {{{val1}}}, val2: {{{val2}}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
+ shouldCompileTo('val: {{{val1/val2}}}', {val1: {val2: false}}, 'val: false');
+ });
it("newlines", function() {
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
diff --git a/spec/blocks.js b/spec/blocks.js
index 8f7c242fa..a172970d8 100644
--- a/spec/blocks.js
+++ b/spec/blocks.js
@@ -1,4 +1,4 @@
-/*global CompilerContext, shouldCompileTo */
+/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('blocks', function() {
it("array", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
@@ -8,7 +8,12 @@ describe('blocks', function() {
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"Arrays ignore the contents when empty");
+ });
+ it('array without data', function() {
+ var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
+ var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
+ shouldCompileTo(string, [hash,,,,false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
});
it("array with @index", function() {
@@ -39,6 +44,13 @@ describe('blocks', function() {
"Templates can access variables in contexts up the stack with relative path syntax");
});
+ it('multiple blocks with complex lookup', function() {
+ var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}';
+ var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
+
+ shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan');
+ });
+
it("block with complex lookup using nested context", function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
@@ -48,10 +60,10 @@ describe('blocks', function() {
});
it("block with deep nested complex lookup", function() {
- var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
- var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
+ var string = "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}";
+ var hash = {omg: "OMG!", outer: [{ sibling: 'sad', inner: [{ text: "goodbye" }] }] };
- shouldCompileTo(string, hash, "Goodbye cruel OMG!");
+ shouldCompileTo(string, hash, "Goodbye cruel sad OMG!");
});
describe('inverted sections', function() {
@@ -83,4 +95,39 @@ describe('blocks', function() {
"No people");
});
});
+
+ describe('standalone sections', function() {
+ it('block standalone else sections', function() {
+ shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
+ 'No people\n');
+ shouldCompileTo('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n', {none: 'No people'},
+ 'No people\n');
+ shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
+ 'No people\n');
+ });
+ it('should handle nesting', function() {
+ shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.');
+ });
+ });
+
+ describe('compat mode', function() {
+ it("block with deep recursive lookup lookup", function() {
+ var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}";
+ var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
+
+ shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
+ });
+ it("block with deep recursive pathed lookup", function() {
+ var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
+ var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
+
+ shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
+ });
+ it("block with missed recursive lookup", function() {
+ var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
+ var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
+
+ shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel ");
+ });
+ });
});
diff --git a/spec/builtins.js b/spec/builtins.js
index bbe494e08..2cd6bacfc 100644
--- a/spec/builtins.js
+++ b/spec/builtins.js
@@ -1,4 +1,4 @@
-/*global CompilerContext, shouldCompileTo, compileWithPartials, handlebarsEnv */
+/*global CompilerContext, shouldCompileTo, shouldThrow, compileWithPartials, handlebarsEnv */
describe('builtin helpers', function() {
describe('#if', function() {
it("if", function() {
@@ -44,6 +44,10 @@ describe('builtin helpers', function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
});
+ it("with with else", function() {
+ var string = "{{#with person}}Person is present{{else}}Person is not present{{/with}}";
+ shouldCompileTo(string, {}, "Person is not present");
+ });
});
describe('#each', function() {
@@ -62,9 +66,29 @@ describe('builtin helpers', function() {
"each with array argument ignores the contents when empty");
});
+ it('each without data', function() {
+ var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
+ var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
+ shouldCompileTo(string, [hash,,,,false], 'goodbye! Goodbye! GOODBYE! cruel world!');
+
+ hash = {goodbyes: 'cruel', world: 'world'};
+ shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,,false], 'cruelworld');
+ });
+
+ it('each without context', function() {
+ var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
+ shouldCompileTo(string, [,,,,], 'cruel !');
+ });
+
it("each with an object and @key", function() {
var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
- var hash = {goodbyes: {"#1 ": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"};
+
+ function Clazz() {
+ this['#1 '] = {text: 'goodbye'};
+ this[2] = {text: 'GOODBYE'};
+ }
+ Clazz.prototype.foo = 'fail';
+ var hash = {goodbyes: new Clazz(), world: 'world'};
// Object property iteration order is undefined according to ECMA spec,
// so we need to check both possible orders
@@ -182,25 +206,88 @@ describe('builtin helpers', function() {
});
it("each on implicit context", function() {
- var string = "{{#each}}{{text}}! {{/each}}cruel world!";
- var hash = [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];
- shouldCompileTo(string, [hash], "goodbye! Goodbye! GOODBYE! cruel world!");
+ shouldThrow(function() {
+ var template = CompilerContext.compile("{{#each}}{{text}}! {{/each}}cruel world!");
+ template({});
+ }, handlebarsEnv.Exception, 'Must pass iterator to #each');
});
});
- it("#log", function() {
- var string = "{{log blah}}";
- var hash = { blah: "whee" };
+ describe("#log", function() {
+ if (typeof console === 'undefined') {
+ return;
+ }
+
+ var info,
+ error;
+ beforeEach(function() {
+ info = console.info;
+ error = console.error;
+ });
+ afterEach(function() {
+ console.info = info;
+ console.error = error;
+ });
- var levelArg, logArg;
- handlebarsEnv.log = function(level, arg){
- levelArg = level;
- logArg = arg;
- };
+ it('should call logger at default level', function() {
+ var string = "{{log blah}}";
+ var hash = { blah: "whee" };
- shouldCompileTo(string, hash, "", "log should not display");
- equals(1, levelArg, "should call log with 1");
- equals("whee", logArg, "should call log with 'whee'");
+ var levelArg, logArg;
+ handlebarsEnv.log = function(level, arg){
+ levelArg = level;
+ logArg = arg;
+ };
+
+ shouldCompileTo(string, hash, "", "log should not display");
+ equals(1, levelArg, "should call log with 1");
+ equals("whee", logArg, "should call log with 'whee'");
+ });
+ it('should call logger at data level', function() {
+ var string = "{{log blah}}";
+ var hash = { blah: "whee" };
+
+ var levelArg, logArg;
+ handlebarsEnv.log = function(level, arg){
+ levelArg = level;
+ logArg = arg;
+ };
+
+ shouldCompileTo(string, [hash,,,,{level: '03'}], "");
+ equals(3, levelArg);
+ equals("whee", logArg);
+ });
+ it('should not output to console', function() {
+ var string = "{{log blah}}";
+ var hash = { blah: "whee" };
+
+ console.info = function() {
+ throw new Error();
+ };
+
+ shouldCompileTo(string, hash, "", "log should not display");
+ });
+ it('should log at data level', function() {
+ var string = "{{log blah}}";
+ var hash = { blah: "whee" };
+ var called;
+
+ console.error = function(log) {
+ equals("whee", log);
+ called = true;
+ };
+
+ shouldCompileTo(string, [hash,,,,{level: '03'}], "");
+ equals(true, called);
+ });
+ it('should handle missing logger', function() {
+ var string = "{{log blah}}";
+ var hash = { blah: "whee" };
+
+ console.error = undefined;
+
+ shouldCompileTo(string, [hash,,,,{level: '03'}], "");
+ });
});
diff --git a/spec/compiler.js b/spec/compiler.js
new file mode 100644
index 000000000..250dbc74b
--- /dev/null
+++ b/spec/compiler.js
@@ -0,0 +1,70 @@
+/*global Handlebars, shouldThrow */
+
+describe('compiler', function() {
+ if (!Handlebars.compile) {
+ return;
+ }
+
+ describe('#equals', function() {
+ function compile(string) {
+ var ast = Handlebars.parse(string);
+ return new Handlebars.Compiler().compile(ast, {});
+ }
+
+ it('should treat as equal', function() {
+ equal(compile('foo').equals(compile('foo')), true);
+ equal(compile('{{foo}}').equals(compile('{{foo}}')), true);
+ equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true);
+ equal(compile('{{foo.bar baz "foo" true false bat=1}}').equals(compile('{{foo.bar baz "foo" true false bat=1}}')), true);
+ equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (baz bat=1)}}')), true);
+ equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), true);
+ });
+ it('should treat as not equal', function() {
+ equal(compile('foo').equals(compile('bar')), false);
+ equal(compile('{{foo}}').equals(compile('{{bar}}')), false);
+ equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false);
+ equal(compile('{{foo.bar baz bat=1}}').equals(compile('{{foo.bar bar bat=1}}')), false);
+ equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (bar bat=1)}}')), false);
+ equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), false);
+ equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')), false);
+ });
+ });
+
+ describe('#compile', function() {
+ it('should fail with invalid input', function() {
+ shouldThrow(function() {
+ Handlebars.compile(null);
+ }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null');
+ shouldThrow(function() {
+ Handlebars.compile({});
+ }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]');
+ });
+
+ it('can utilize AST instance', function() {
+ equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], {}))(), 'Hello');
+ });
+
+ it("can pass through an empty string", function() {
+ equal(Handlebars.compile('')(), '');
+ });
+ });
+
+ describe('#precompile', function() {
+ it('should fail with invalid input', function() {
+ shouldThrow(function() {
+ Handlebars.precompile(null);
+ }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
+ shouldThrow(function() {
+ Handlebars.precompile({});
+ }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]');
+ });
+
+ it('can utilize AST instance', function() {
+ equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), {})), true);
+ });
+
+ it("can pass through an empty string", function() {
+ equal(/return ""/.test(Handlebars.precompile('')), true);
+ });
+ });
+});
diff --git a/spec/env/common.js b/spec/env/common.js
index 53bf977da..92cc61123 100644
--- a/spec/env/common.js
+++ b/spec/env/common.js
@@ -1,3 +1,4 @@
+/*global CompilerContext, compileWithPartials, shouldCompileToWithPartials */
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
@@ -5,27 +6,35 @@ global.shouldCompileTo = function(string, hashOrArray, expected, message) {
global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
- throw new Error("'" + expected + "' should === '" + result + "': " + message);
+ throw new Error("'" + result + "' should === '" + expected + "': " + message);
}
};
global.compileWithPartials = function(string, hashOrArray, partials) {
- var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
+ var template,
+ ary,
+ options;
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
+ options = {compat: hashOrArray[3]};
+ if (hashOrArray[4] != null) {
+ options.data = !!hashOrArray[4];
+ ary[1].data = hashOrArray[4];
+ }
} else {
ary = [hashOrArray];
}
+ template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string, options);
return template.apply(this, ary);
};
global.equals = global.equal = function(a, b, msg) {
if (a !== b) {
- throw new Error("'" + b + "' should === '" + a + "'" + (msg ? ": " + msg : ''));
+ throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ": " + msg : ''));
}
};
@@ -39,7 +48,7 @@ global.shouldThrow = function(callback, type, msg) {
throw new Error('Type failure');
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
- throw new Error('Message failure');
+ equal(msg, err.message);
}
}
if (failed) {
diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js
index e9616c344..1cf3298eb 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":[5,">= 2.0.0"],"main":function(depth0,helpers,partials,data) {
+return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
return "";
},"useData":true});
});
diff --git a/spec/helpers.js b/spec/helpers.js
index 6ba368e4d..e604e91c8 100644
--- a/spec/helpers.js
+++ b/spec/helpers.js
@@ -158,6 +158,22 @@ describe('helpers', function() {
shouldCompileTo(messageString, [rootMessage, { list: list }], "Nobody's here
", "the context of an inverse is the parent of the block");
});
+ it('pathed lambas with parameters', function() {
+ var hash = {
+ helper: function() {
+ return 'winning';
+ }
+ };
+ hash.hash = hash;
+ var helpers = {
+ './helper': function() {
+ return 'fail';
+ }
+ };
+ shouldCompileTo('{{./helper 1}}', [hash, helpers], 'winning');
+ shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning');
+ });
+
describe("helpers hash", function() {
it("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
@@ -192,20 +208,41 @@ describe('helpers', function() {
});
});
- it("Multiple global helper registration", function() {
- var helpers = handlebarsEnv.helpers;
- handlebarsEnv.helpers = {};
+ describe('registration', function() {
+ it('unregisters', function() {
+ var helpers = handlebarsEnv.helpers;
+ handlebarsEnv.helpers = {};
- handlebarsEnv.registerHelper({
- 'if': helpers['if'],
- world: function() { return "world!"; },
- test_helper: function() { return 'found it!'; }
+ handlebarsEnv.registerHelper('foo', function() {
+ return 'fail';
+ });
+ handlebarsEnv.unregisterHelper('foo');
+ equals(handlebarsEnv.helpers.foo, undefined);
});
- shouldCompileTo(
- "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
- [{cruel: "cruel"}],
- "found it! Goodbye cruel world!!");
+ it('allows multiple globals', function() {
+ var helpers = handlebarsEnv.helpers;
+ handlebarsEnv.helpers = {};
+
+ handlebarsEnv.registerHelper({
+ 'if': helpers['if'],
+ world: function() { return "world!"; },
+ test_helper: function() { return 'found it!'; }
+ });
+
+ shouldCompileTo(
+ "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
+ [{cruel: "cruel"}],
+ "found it! Goodbye cruel world!!");
+ });
+ it('fails with multiple and args', function() {
+ shouldThrow(function() {
+ handlebarsEnv.registerHelper({
+ world: function() { return "world!"; },
+ test_helper: function() { return 'found it!'; }
+ }, {});
+ }, Error, 'Arg not supported with multiple helpers');
+ });
});
it("decimal number literals work", function() {
@@ -403,6 +440,21 @@ describe('helpers', function() {
shouldCompileTo(string, [context, helpers], "Hello world ");
});
+
+ it("if a value is not found, custom helperMissing is used", function() {
+ var string = "{{hello}} {{link_to}}";
+ var context = { hello: "Hello", world: "world" };
+
+ var helpers = {
+ helperMissing: function(options) {
+ if(options.name === "link_to") {
+ return new Handlebars.SafeString("winning ");
+ }
+ }
+ };
+
+ shouldCompileTo(string, [context, helpers], "Hello winning ");
+ });
});
describe("knownHelpers", function() {
diff --git a/spec/mustache b/spec/mustache
new file mode 160000
index 000000000..72233f3ff
--- /dev/null
+++ b/spec/mustache
@@ -0,0 +1 @@
+Subproject commit 72233f3ffda9e33915fd3022d0a9ebbcce265acd
diff --git a/spec/parser.js b/spec/parser.js
index ebde17196..131160a74 100644
--- a/spec/parser.js
+++ b/spec/parser.js
@@ -1,4 +1,4 @@
-/*global Handlebars */
+/*global Handlebars, shouldThrow */
describe('parser', function() {
if (!Handlebars.print) {
return;
@@ -121,11 +121,11 @@ describe('parser', function() {
});
it('parses empty blocks with empty inverse section', function() {
- equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
+ equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
});
it('parses empty blocks with empty inverse (else-style) section', function() {
- equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
+ equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse section', function() {
@@ -147,6 +147,9 @@ describe('parser', function() {
it('parses a standalone inverse section', function() {
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
});
+ it('parses a standalone inverse section', function() {
+ equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
+ });
it("raises if there's a Parse error", function() {
shouldThrow(function() {
diff --git a/spec/partials.js b/spec/partials.js
index 732436af0..20187f81d 100644
--- a/spec/partials.js
+++ b/spec/partials.js
@@ -1,11 +1,11 @@
-/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
+/*global CompilerContext, Handlebars, handlebarsEnv, shouldCompileTo, shouldCompileToWithPartials, shouldThrow */
describe('partials', function() {
- it("basic partials", function() {
- var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
- var partial = "{{name}} ({{url}}) ";
- var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
- "Basic partials output based on current context.");
+ it('basic partials', function() {
+ var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
+ var partial = '{{name}} ({{url}}) ';
+ var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
});
it("partials with context", function() {
@@ -91,6 +91,9 @@ describe('partials', function() {
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
+
+ handlebarsEnv.unregisterPartial('global_test');
+ equals(handlebarsEnv.partials.global_test, undefined);
});
it("Multiple partial registration", function() {
@@ -136,5 +139,54 @@ describe('partials', function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = "";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: "); });
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: ");
+ });
+
+ it("throw on missing partial", function() {
+ var compile = handlebarsEnv.compile;
+ handlebarsEnv.compile = undefined;
+ shouldThrow(function() {
+ shouldCompileTo('{{> dude}}', [{}, {}, {dude: 'fail'}], '');
+ }, Error, /The partial dude could not be compiled/);
+ handlebarsEnv.compile = compile;
+ });
+
+ describe('standalone partials', function() {
+ it("indented partials", function() {
+ var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
+ var dude = "{{name}}\n";
+ var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true,
+ "Dudes:\n Yehuda\n Alan\n");
+ });
+ it("nested indented partials", function() {
+ var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
+ var dude = "{{name}}\n {{> url}}";
+ var url = "{{url}}!\n";
+ var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true,
+ "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n");
+ });
+ });
+
+ describe('compat mode', function() {
+ it('partials can access parents', function() {
+ var string = 'Dudes: {{#dudes}}{{> dude}}{{/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}} ';
+ var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true, false], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
+ });
+ it('partials inherit compat', function() {
+ var string = 'Dudes: {{> dude}}';
+ var partial = '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}';
+ 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 ');
+ });
+ });
});
diff --git a/spec/precompiler.js b/spec/precompiler.js
new file mode 100644
index 000000000..bd19fbf85
--- /dev/null
+++ b/spec/precompiler.js
@@ -0,0 +1,130 @@
+/*global shouldThrow */
+
+
+describe('precompiler', function() {
+ // NOP Under non-node environments
+ if (typeof process === 'undefined') {
+ return;
+ }
+
+ var Handlebars = require('../lib'),
+ Precompiler = require('../lib/precompiler'),
+ uglify = require('uglify-js');
+
+ var log,
+ logFunction,
+
+ precompile,
+ minify;
+
+ beforeEach(function() {
+ precompile = Handlebars.precompile;
+ minify = uglify.minify;
+
+ logFunction = console.log;
+ log = '';
+ console.log = function() {
+ log += Array.prototype.join.call(arguments, '');
+ };
+ });
+ afterEach(function() {
+ Handlebars.precompile = precompile;
+ uglify.minify = minify;
+ console.log = logFunction;
+ });
+
+ it('should output version', function() {
+ Precompiler.cli({templates: [], version: true});
+ equals(log, Handlebars.VERSION);
+ });
+ it('should throw if lacking templates', function() {
+ shouldThrow(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 throw when combining simple and minimized', function() {
+ shouldThrow(function() {
+ Precompiler.cli({templates: [__dirname], simple: true, min: true});
+ }, Handlebars.Exception, 'Unable to minimze simple output');
+ });
+ it('should throw when combining simple and multiple templates', function() {
+ shouldThrow(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 combining simple and directories', function() {
+ shouldThrow(function() {
+ Precompiler.cli({templates: [__dirname], 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'});
+ 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'});
+ 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'});
+ 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'});
+ 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'});
+ 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'});
+ 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});
+ 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 output minimized templates', function() {
+ Handlebars.precompile = function() { return 'amd'; };
+ uglify.minify = function() { return {code: 'min'}; };
+ Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'});
+ equal(log, 'min');
+ });
+});
diff --git a/spec/regressions.js b/spec/regressions.js
index 7412cd339..11207fce3 100644
--- a/spec/regressions.js
+++ b/spec/regressions.js
@@ -24,7 +24,19 @@ describe('Regressions', function() {
});
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
- var string = "This is a slightly more complicated {{thing}}. .\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n\n{{#things}}\n{{word}} \n{{/things}} .\n{{/hasThings}}\n{{^hasThings}}\n\nNothing to check out... \n{{/hasThings}}";
+ var string = 'This is a slightly more complicated {{thing}}. .\n'
+ + '{{! Just ignore this business. }}\n'
+ + 'Check this out:\n'
+ + '{{#hasThings}}\n'
+ + '\n'
+ + '{{#things}}\n'
+ + '{{word}} \n'
+ + '{{/things}} .\n'
+ + '{{/hasThings}}\n'
+ + '{{^hasThings}}\n'
+ + '\n'
+ + 'Nothing to check out... \n'
+ + '{{/hasThings}}';
var data = {
thing: function() {
return "blah";
@@ -39,7 +51,13 @@ describe('Regressions', function() {
}
};
- var output = "This is a slightly more complicated blah. .\n\nCheck this out:\n\n\n\n@fat \n\n@dhg \n\n@sayrer \n .\n\n";
+ var output = 'This is a slightly more complicated blah. .\n'
+ + 'Check this out:\n'
+ + '\n'
+ + '@fat \n'
+ + '@dhg \n'
+ + '@sayrer \n'
+ + ' .\n';
shouldCompileTo(string, data, output);
});
@@ -112,19 +130,19 @@ describe('Regressions', function() {
shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
});
- it("Passing falsy values to Handlebars.compile throws an error", function() {
- shouldThrow(function() {
- CompilerContext.compile(null);
- }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
+ it('GH-731: zero context rendering', function() {
+ shouldCompileTo('{{#foo}} This is {{bar}} ~ {{/foo}}', {foo: 0, bar: 'OK'}, ' This is ~ ');
+ });
+
+ it('GH-820: zero pathed rendering', function() {
+ shouldCompileTo('{{foo.bar}}', {foo: 0}, '');
});
- if (Handlebars.AST) {
- it("can pass through an already-compiled AST via compile/precompile", function() {
- equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello');
- });
+ it('GH-837: undefined values for helpers', function() {
+ var helpers = {
+ str: function(value) { return value + ''; }
+ };
- it("can pass through an empty string", function() {
- equal(Handlebars.compile('')(), '');
- });
- }
+ shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined');
+ });
});
diff --git a/spec/runtime.js b/spec/runtime.js
new file mode 100644
index 000000000..d33dd747c
--- /dev/null
+++ b/spec/runtime.js
@@ -0,0 +1,63 @@
+/*globals Handlebars, shouldThrow */
+
+describe('runtime', function() {
+ describe('#template', function() {
+ it('should throw on invalid templates', function() {
+ shouldThrow(function() {
+ Handlebars.template({});
+ }, Error, 'Unknown template object: object');
+ shouldThrow(function() {
+ Handlebars.template();
+ }, Error, 'Unknown template object: undefined');
+ shouldThrow(function() {
+ Handlebars.template('');
+ }, Error, 'Unknown template object: string');
+ });
+ it('should throw on version mismatch', function() {
+ shouldThrow(function() {
+ Handlebars.template({
+ main: true,
+ 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,
+ 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
+ });
+ }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
+ });
+ });
+
+ describe('#child', function() {
+ if (!Handlebars.compile) {
+ return;
+ }
+
+ it('should throw for depthed methods without depths', function() {
+ shouldThrow(function() {
+ var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
+ // Calling twice to hit the non-compiled case.
+ template._setup({});
+ template._setup({});
+ template._child(1);
+ }, Error, 'must pass parent depths');
+ });
+ it('should expose child template', function() {
+ var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
+ // Calling twice to hit the non-compiled case.
+ equal(template._child(1)(), 'bar');
+ equal(template._child(1)(), 'bar');
+ });
+ it('should render depthed content', function() {
+ var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
+ // Calling twice to hit the non-compiled case.
+ equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz');
+ });
+ });
+});
diff --git a/spec/spec.js b/spec/spec.js
new file mode 100644
index 000000000..2dd2bd8f0
--- /dev/null
+++ b/spec/spec.js
@@ -0,0 +1,49 @@
+describe('spec', function() {
+ // NOP Under non-node environments
+ if (typeof process === 'undefined') {
+ return;
+ }
+
+ var _ = require('underscore'),
+ Handlebars = require('../lib'),
+ fs = require('fs');
+
+ var specDir =__dirname + '/mustache/specs/';
+ var specs = _.filter(fs.readdirSync(specDir), function(name) {
+ return /.*\.json$/.test(name);
+ });
+
+ _.each(specs, function(name) {
+ var spec = require(specDir + name);
+ _.each(spec.tests, function(test) {
+ // Our lambda implementation knowingly deviates from the optional Mustace lambda spec
+ // We also do not support alternative delimeters
+ if (name === '~lambdas.json'
+
+ // We also choose to throw if paritals are not found
+ || (name === 'partials.json' && test.name === 'Failed Lookup')
+
+ // We nest the entire response from partials, not just the literals
+ || (name === 'partials.json' && test.name === 'Standalone Indentation')
+
+ || /\{\{\=/.test(test.template)
+ || _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) {
+ it.skip(name + ' - ' + test.name);
+ return;
+ }
+
+ var data = _.clone(test.data);
+ if (data.lambda) {
+ // Blergh
+ data.lambda = eval('(' + data.lambda.js + ')');
+ }
+ it(name + ' - ' + test.name, function() {
+ if (test.partials) {
+ shouldCompileToWithPartials(test.template, [data, {}, test.partials, true], true, test.expected, test.desc + ' "' + test.template + '"');
+ } else {
+ shouldCompileTo(test.template, [data, {}, {}, true], test.expected, test.desc + ' "' + test.template + '"');
+ }
+ });
+ });
+ });
+});
diff --git a/spec/subexpressions.js b/spec/subexpressions.js
index 5d11473bc..5c9fdfc30 100644
--- a/spec/subexpressions.js
+++ b/spec/subexpressions.js
@@ -59,6 +59,24 @@ describe('subexpressions', function() {
shouldCompileTo(string, [context, helpers], "val is true");
});
+ it('GH-800 : Complex subexpressions', function() {
+ var context = {a: 'a', b:'b', c:{c:'c'}, d:'d', e: {e: 'e'}};
+ var helpers = {
+ dash: function(a, b) {
+ return a + "-" + b;
+ },
+ concat: function(a, b) {
+ return a + b;
+ }
+ };
+
+ shouldCompileTo('{{dash "abc" (concat a b)}}', [context, helpers], 'abc-ab');
+ shouldCompileTo('{{dash d (concat a b)}}', [context, helpers], 'd-ab');
+ shouldCompileTo('{{dash c.c (concat a b)}}', [context, helpers], 'c-ab');
+ shouldCompileTo('{{dash (concat a b) c.c}}', [context, helpers], 'ab-c');
+ shouldCompileTo('{{dash (concat a e.e) c.c}}', [context, helpers], 'ae-c');
+ });
+
it("provides each nested helper invocation its own options hash", function() {
var string = '{{equal (equal true true) true}}';
@@ -121,6 +139,30 @@ describe('subexpressions', function() {
shouldCompileTo(string, [{}, helpers], ' ');
});
+ it("multiple subexpressions in a hash with context", function() {
+ var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}';
+
+ var context = {
+ item: {
+ field: "Name",
+ placeholder: "Example User"
+ }
+ };
+
+ var helpers = {
+ input: function(options) {
+ var hash = options.hash;
+ var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
+ var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
+ return new Handlebars.SafeString(' ');
+ },
+ t: function(defaultString) {
+ return new Handlebars.SafeString(defaultString);
+ }
+ }
+ shouldCompileTo(string, [context, helpers], ' ');
+ });
+
it("in string params mode,", function() {
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
diff --git a/spec/tokenizer.js b/spec/tokenizer.js
index 36a632b8a..0f0dc0b88 100644
--- a/spec/tokenizer.js
+++ b/spec/tokenizer.js
@@ -237,10 +237,10 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});
- it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
- shouldMatchTokens(tokenize("{{^}}"), ['OPEN_INVERSE', 'CLOSE']);
- shouldMatchTokens(tokenize("{{else}}"), ['OPEN_INVERSE', 'CLOSE']);
- shouldMatchTokens(tokenize("{{ else }}"), ['OPEN_INVERSE', 'CLOSE']);
+ it('tokenizes inverse sections as "INVERSE"', function() {
+ shouldMatchTokens(tokenize("{{^}}"), ['INVERSE']);
+ shouldMatchTokens(tokenize("{{else}}"), ['INVERSE']);
+ shouldMatchTokens(tokenize("{{ else }}"), ['INVERSE']);
});
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
diff --git a/spec/umd-runtime.html b/spec/umd-runtime.html
new file mode 100644
index 000000000..90b91448a
--- /dev/null
+++ b/spec/umd-runtime.html
@@ -0,0 +1,89 @@
+
+
+ Mocha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/umd.html b/spec/umd.html
new file mode 100644
index 000000000..5f3863fc8
--- /dev/null
+++ b/spec/umd.html
@@ -0,0 +1,109 @@
+
+
+ Mocha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/utils.js b/spec/utils.js
index 390ad0533..0216c8de8 100644
--- a/spec/utils.js
+++ b/spec/utils.js
@@ -30,8 +30,8 @@ describe('utils', function() {
equals(Handlebars.Utils.escapeExpression(''), '');
equals(Handlebars.Utils.escapeExpression(undefined), '');
equals(Handlebars.Utils.escapeExpression(null), '');
- equals(Handlebars.Utils.escapeExpression(false), '');
+ equals(Handlebars.Utils.escapeExpression(false), 'false');
equals(Handlebars.Utils.escapeExpression(0), '0');
});
it('should handle empty objects', function() {
@@ -56,4 +56,20 @@ describe('utils', function() {
equals(Handlebars.Utils.isEmpty({bar: 1}), false);
});
});
+
+ describe('#extend', function() {
+ it('should ignore prototype values', function() {
+ function A() {
+ this.a = 1;
+ }
+ A.prototype.b = 4;
+
+ var b = {b: 2};
+
+ Handlebars.Utils.extend(b, new A());
+
+ equals(b.a, 1);
+ equals(b.b, 2);
+ });
+ });
});
diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js
index 2088ed8b7..86364292e 100644
--- a/spec/whitespace-control.js
+++ b/spec/whitespace-control.js
@@ -1,3 +1,5 @@
+/*global shouldCompileTo, shouldCompileToWithPartials */
+
describe('whitespace control', function() {
it('should strip whitespace around mustache calls', function() {
var hash = {foo: 'bar<'};
@@ -8,6 +10,8 @@ describe('whitespace control', function() {
shouldCompileTo(' {{~&foo~}} ', hash, 'bar<');
shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<');
+
+ shouldCompileTo('1\n{{foo~}} \n\n 23\n{{bar}}4', {}, '1\n23\n4');
});
describe('blocks', function() {
@@ -18,6 +22,9 @@ describe('whitespace control', function() {
shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, ' bar ');
+
+ shouldCompileTo(' \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar');
+ shouldCompileTo(' a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ', hash, ' abara ');
});
it('should strip whitespace around inverse block calls', function() {
var hash = {};
@@ -26,6 +33,8 @@ describe('whitespace control', function() {
shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, ' bar ');
+
+ shouldCompileTo(' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar');
});
it('should strip whitespace around complex block calls', function() {
var hash = {foo: 'bar<'};
@@ -37,6 +46,9 @@ describe('whitespace control', function() {
shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar');
+ shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar');
+ shouldCompileTo('\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar<');
+
hash = {};
shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz');
@@ -45,6 +57,8 @@ describe('whitespace control', function() {
shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz ');
shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz');
+
+ shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'baz');
});
});
diff --git a/src/handlebars.l b/src/handlebars.l
index cafdd72c7..0f420e7ee 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -28,7 +28,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
%%
-[^\x00]*?/("{{") {
+[^\x00]*?/("{{") {
if(yytext.slice(-2) === "\\\\") {
strip(0,1);
this.begin("mu");
@@ -67,14 +67,11 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
this.begin('raw');
return 'CLOSE_RAW_BLOCK';
}
-"{{{{"[^\x00]*"}}}}" {
- yytext = yytext.substr(4, yyleng-8);
- this.popState();
- return 'RAW_BLOCK';
- }
"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
"{{"{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';
"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE';
"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE';
"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index 51796ec6d..a8d288f68 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -2,78 +2,64 @@
%ebnf
-%{
-
-function stripFlags(open, close) {
- return {
- left: open.charAt(2) === '~',
- right: close.charAt(0) === '~' || close.charAt(1) === '~'
- };
-}
-
-%}
-
%%
root
- : statements EOF { return new yy.ProgramNode($1, @$); }
- | EOF { return new yy.ProgramNode([], @$); }
+ : program EOF { yy.prepareProgram($1.statements, true); return $1; }
;
program
- : simpleInverse statements -> new yy.ProgramNode([], $1, $2, @$)
- | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3, @$)
- | statements simpleInverse -> new yy.ProgramNode($1, $2, [], @$)
- | statements -> new yy.ProgramNode($1, @$)
- | simpleInverse -> new yy.ProgramNode([], @$)
- | "" -> new yy.ProgramNode([], @$)
- ;
-
-statements
- : statement -> [$1]
- | statements statement { $1.push($2); $$ = $1; }
+ : statement* -> new yy.ProgramNode(yy.prepareProgram($1), {}, @$)
;
statement
- : openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
- | openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3, @$)
- | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3, @$)
- | mustache -> $1
+ : mustache -> $1
+ | block -> $1
+ | rawBlock -> $1
| partial -> $1
| CONTENT -> new yy.ContentNode($1, @$)
| COMMENT -> new yy.CommentNode($1, @$)
;
+rawBlock
+ : openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
+ ;
+
openRawBlock
: OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$)
;
+block
+ : openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
+ | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$)
+ ;
+
openBlock
- : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
+ : OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;
openInverse
- : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
+ : OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
+ ;
+
+inverseAndProgram
+ : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
;
closeBlock
- : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)}
+ : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
;
mustache
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
// This also allows for handler unification as all mustache node instances can utilize the same handler
- : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
- | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
+ : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
+ | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;
partial
- : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, stripFlags($1, $5), @$)
- | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, stripFlags($1, $4), @$)
- ;
-
-simpleInverse
- : OPEN_INVERSE CLOSE -> stripFlags($1, $2)
+ : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$)
+ | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$)
;
sexpr
diff --git a/src/parser-prefix.js b/src/parser-prefix.js
index 685b6ec3b..a4ba487c6 100644
--- a/src/parser-prefix.js
+++ b/src/parser-prefix.js
@@ -1 +1,2 @@
/* jshint ignore:start */
+/* istanbul ignore next */
diff --git a/tasks/test.js b/tasks/test.js
index 664af60d8..ad8a911d3 100644
--- a/tasks/test.js
+++ b/tasks/test.js
@@ -29,5 +29,16 @@ module.exports = function(grunt) {
done();
});
});
- grunt.registerTask('test', ['test:bin', 'test:mocha']);
+ grunt.registerTask('test:cov', function() {
+ var done = this.async();
+
+ var runner = childProcess.fork('node_modules/.bin/istanbul', ['cover', '--', './spec/env/runner.js'], {stdio: 'inherit'});
+ runner.on('close', function(code) {
+ if (code != 0) {
+ grunt.fatal(code + ' tests failed');
+ }
+ done();
+ });
+ });
+ grunt.registerTask('test', ['test:bin', 'test:cov']);
};