Compare commits

...

61 Commits

Author SHA1 Message Date
kpdecker 096b8ccd2e v1.2.0 2013-12-23 21:39:46 -06:00
kpdecker 923993dfb1 Update release notes 2013-12-23 21:39:23 -06:00
kpdecker fcf4e4ef0e Update release documentation 2013-12-23 21:30:34 -06:00
kpdecker ae1795f82c Update sauce tests for supported versions 2013-12-23 21:24:11 -06:00
kpdecker 2666553383 Cleanup jshint globals a bit 2013-12-23 21:16:25 -06:00
kpdecker 15af03c8bf Remove unused var 2013-12-23 21:16:14 -06:00
kpdecker 97c7d65931 Do not minimize multiple times 2013-12-23 21:16:07 -06:00
kpdecker bbc7c7d080 Expose compilerInfo and checkRevision as APIs
Fixes #656
2013-12-23 21:00:59 -06:00
kpdecker 1a751b2f6e Add JavascriptCompiler public API tests 2013-12-23 20:59:16 -06:00
kpdecker abe9c82e75 Use env.VM to lookup runtime methods
Allows for overrides by 3rd parties

Fixes #679
2013-12-23 19:53:11 -06:00
kpdecker 956ac95e7a Add browserify runtime export
Fixes #668
2013-12-23 19:51:10 -06:00
kpdecker 40e1837b14 Merge branch 'fix-escapes' of github.com:dmarcotte/handlebars.js into dmarcotte-fix-escapes
Conflicts:
	spec/tokenizer.js
2013-12-23 19:22:30 -06:00
kpdecker ddea5be2a4 Add AST test coverage for more complex paths 2013-12-23 19:02:45 -06:00
kpdecker b4968bb8d4 Fix handling of boolean escape in MustacheNode
Fixes issue with Ember compatibility due to direct instantiation of MustacheNode.
2013-12-23 19:02:24 -06:00
Kevin Decker ca8e34c94d Merge pull request #641 from wycats/ember-testing
Document ember testing process
2013-12-23 16:13:22 -08:00
kpdecker 4a94340f42 Add docs on testing ember integration 2013-12-23 18:12:14 -06:00
kpdecker 7d69becd2e Add notes on bower install
Fixes #655
2013-12-23 15:50:07 -06:00
kpdecker 1a9b5d4ac6 Add test case to cover #676
Fixes #676
2013-12-23 15:42:38 -06:00
kpdecker bdc0e6507e Provide default sort for builds link 2013-12-23 15:34:40 -06:00
kpdecker 93fb25c702 Handle empty responses from partials
Fixes #675
2013-12-23 15:33:33 -06:00
kpdecker 23d41e5617 Use watch atBegin command to handle failing tests 2013-12-23 14:26:56 -06:00
kpdecker d62620a282 Implement grunt dev task 2013-12-23 13:13:25 -06:00
Kevin Decker 62fdd541f5 Merge pull request #681 from wycats/sauce-tests
Adds in-browser testing and Saucelabs CI
2013-12-23 11:09:57 -08:00
kpdecker 7f53c917d9 Skip leak check under FF 2013-12-23 12:32:48 -06:00
kpdecker 015eba6687 Hide success messages when running in sauce
This should help debug the firefox issues that only occur when executing against sauce.
2013-12-23 11:57:11 -06:00
kpdecker 5b8b54b774 Add travis build caching 2013-12-23 03:20:13 -06:00
kpdecker efcedf2f24 Optimize travis build with publish option 2013-12-23 03:12:03 -06:00
kpdecker 512daa3189 Create matrix target for 0.10 only 2013-12-23 02:58:02 -06:00
kpdecker ad3727b3fc (Attempt to) Limit sauce tests to one travis build 2013-12-23 02:52:46 -06:00
kpdecker e2a81fdd60 Add selenium badge 2013-12-23 02:46:35 -06:00
kpdecker fe7bc0d4bd Add sauce labs testing support 2013-12-23 02:32:46 -06:00
kpdecker 9ad8b86ece Concat tests together for inbrowser exec 2013-12-23 02:19:09 -06:00
kpdecker 96b42dedea Fix tokenizer tests under IE 2013-12-23 02:19:09 -06:00
kpdecker 4942324250 Move away from should asserts to internal
This is needed as neither Sinon nor Chai support in-browser testing under IE.
2013-12-23 02:19:09 -06:00
kpdecker 0cf5657c43 Use shouldThrow helper for test asserts 2013-12-23 02:19:09 -06:00
kpdecker 3daef9d308 Use charAt rather than string index
Older versions of IE do not support [] access to string contents so charAt must be used.

Fixes #677
2013-12-23 02:18:57 -06:00
kpdecker 7f0ded4926 Relax packager dependency 2013-12-22 18:50:18 -06:00
kpdecker 1041c1d533 Add tests for object @index and @first handling 2013-12-22 18:46:13 -06:00
kpdecker f06a9dc1f0 Use strict comparison for first 2013-12-22 18:45:57 -06:00
kpdecker 45ab2bb218 Merge branch 'patch-1' of github.com:cgp/handlebars.js into cgp-patch-1 2013-12-22 18:27:10 -06:00
kpdecker f17cb3ecac Run jshint on output
Allows us to execute jshint in non-forced mode.
2013-12-01 15:40:08 -06:00
kpdecker 86974b2660 jshint 2013-12-01 14:59:44 -06:00
kpdecker 57b4ef08b6 Use module packager for all transpile tasks 2013-12-01 14:59:38 -06:00
kpdecker cc16bc47dd Add missing semicolon 2013-12-01 11:37:32 -06:00
kpdecker 078c1eecba Export AST as an object rather than module
The parser expects the AST object to be mutable, which modules are not.
2013-12-01 11:37:25 -06:00
kpdecker eb53f2e844 Allow extend to work with non-prototyped objects
ES6 modules do not extend the Object prototype so this blows up under the latest version of the transpiler.
2013-12-01 11:36:28 -06:00
Chris Pall e8954b3488 Update base.js
Give users the index for properties. When I am rendering a radio button I use key-value pairs, but I can't use either as unique identifiers because they likely contain invalid identifier characters. I added "first" as an index as well, but have no particular use case for first. Since there is no way to detect "last", I didn't add that property.
2013-11-22 00:41:25 -05:00
Kevin Decker 20b9c3829e Merge pull request #653 from k-j-kleist/patch-1
Update README.markdown
2013-11-20 09:35:53 -08:00
Kevin Decker 709153f7f5 Merge pull request #650 from thomasboyt/patch-1
Handlebars is MIT-licensed
2013-11-20 09:34:25 -08:00
Kevin Decker d5cc3fbcf1 Merge pull request #657 from chrishoage/master
Fixes issue where cli compiles non handlebars templates
2013-11-20 09:32:48 -08:00
choage d3cff5c1cd Fixes issue where cli compiles non handlebars templates 2013-11-17 16:35:01 -08:00
Karl Johan Kleist 83106e7a5e Update README.markdown
Added "Ghost" (Just a blogging platform) to "Handlebars in the Wild".
2013-11-12 10:41:56 +01:00
Thomas Boyt 525e58d318 Handlebars is MIT-licensed
The package.json list Handlebars as having a BSD license, but the readme says it's MIT licensed
2013-11-08 11:48:31 -05:00
Daniel Marcotte 9d353bd3dd Fix "\\{{" immediately following "\{{"
Escaped-escape mustaches ("\\{{") immediately following escaped
mustaches ("\{{") were being handled incorrectly.

Fix the lookahead to make sure yytext still contains the appropriate
slashes when we pop out of <emu> so they can be handled consistently
by the initial state.
2013-11-06 08:20:49 -08:00
kpdecker 47d13cb23c v1.1.2 2013-11-05 18:10:06 -06:00
kpdecker 7c13753bb5 Update release notes 2013-11-05 18:09:26 -06:00
kpdecker affbcbb79e Unify isArray/isFunction/toString implementations
Restores Array.isArray polyfill for all use cases.

Fixes #645
2013-11-05 18:07:33 -06:00
kpdecker 96a45a4a96 Add simple binary utility tests 2013-11-05 18:02:25 -06:00
kpdecker 6f6eb89bd8 Use handlebars runtime from precompiled amd files
Fixes #644
2013-11-05 18:02:05 -06:00
kpdecker eea708a18d Fix empty string compilation 2013-11-04 22:11:53 -06:00
Stefan Penner 508bb2d55d local grunt-cli 2013-11-04 21:18:57 -05:00
46 changed files with 976 additions and 539 deletions
+1
View File
@@ -8,3 +8,4 @@ node_modules
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
+3 -9
View File
@@ -1,14 +1,6 @@
{
"predef": [
"console",
"Ember",
"DS",
"Handlebars",
"Metamorph",
"ember_assert",
"ember_warn",
"ember_deprecate",
"ember_deprecateFunc",
"require",
"suite",
"equal",
@@ -22,10 +14,12 @@
"ok",
"strictEqual",
"module",
"define",
"describe",
"it",
"afterEach"
"afterEach",
"beforeEach"
],
"node" : true,
+1
View File
@@ -18,5 +18,6 @@ dist/components/*
spec/*
src/*
tasks/*
tmp/*
publish/*
vendor/*
+13 -10
View File
@@ -1,13 +1,12 @@
language: node_js
node_js:
- "0.8"
- "0.10"
before_install:
- npm install -g grunt-cli
script:
- grunt --stack default metrics publish:latest
- grunt --stack travis
email:
on_failure: change
@@ -15,13 +14,17 @@ email:
env:
global:
- S3_BUCKET_NAME=builds.handlebarsjs.com
- secure: ! 'PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62Lrq
- secure: PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62LrqtJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg=
- secure: mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuTMRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJV7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s=
tJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+
matrix:
include:
- node_js: "0.10"
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg='
- secure: ! 'mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuT
MRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJ
V7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s='
cache:
directories:
- node_modules
+88 -47
View File
@@ -7,15 +7,14 @@ module.exports = function(grunt) {
jshint: {
options: {
jshintrc: '.jshintrc',
force: true
jshintrc: '.jshintrc'
},
files: [
'lib/**/!(parser).js'
'dist/**/!(*.min|parser).js'
]
},
clean: ["dist", "lib/handlebars/compiler/parser.js"],
clean: ['tmp', 'dist', 'lib/handlebars/compiler/parser.js'],
copy: {
dist: {
@@ -42,9 +41,20 @@ module.exports = function(grunt) {
}
},
transpile: {
packager: {
global: {
type: 'global',
export: 'Handlebars',
files: [{
cwd: 'lib/',
expand: true,
src: ['handlebars*.js'],
dest: 'dist/'
}]
},
amd: {
type: "amd",
type: 'amd',
anonymous: true,
files: [{
expand: true,
@@ -64,21 +74,6 @@ module.exports = function(grunt) {
}]
}
},
packager: {
options: {
export: 'Handlebars'
},
global: {
files: [{
cwd: 'lib/',
expand: true,
src: ['handlebars*.js'],
dest: 'dist/'
}]
}
},
requirejs: {
options: {
optimize: "none",
@@ -108,57 +103,103 @@ module.exports = function(grunt) {
files: [{
cwd: 'dist/',
expand: true,
src: ['handlebars*.js'],
src: ['handlebars*.js', '!*.min.js'],
dest: 'dist/',
rename: function(dest, src) {
return dest + src.replace(/\.js$/, '.min.js');
}
}]
}
},
concat: {
tests: {
src: ['spec/!(require).js'],
dest: 'tmp/tests.js'
}
},
connect: {
server: {
options: {
base: '.',
hostname: '*',
port: 9999
}
}
},
'saucelabs-mocha': {
all: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/?headless=true'],
detailedError: true,
concurrency: 2,
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: 'safari', version: 5},
{browserName: 'opera', version: 12},
{browserName: 'opera', version: 11},
{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: 8, platform: 'XP'},
{browserName: 'internet explorer', version: 7, platform: 'XP'},
{browserName: 'internet explorer', version: 6, platform: 'XP'}
]
}
}
},
watch: {
scripts: {
options: {
atBegin: true
},
files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'],
tasks: ['build', 'tests', 'test']
}
}
});
// Build a new version of the library
this.registerTask('build', "Builds a distributable version of the current project", [
'jshint',
'clean',
'parser',
'node',
'globals']);
'globals',
'jshint']);
this.registerTask('amd', ['transpile:amd', 'requirejs']);
this.registerTask('node', ['transpile:cjs']);
this.registerTask('globals', ['packager-fork']);
this.registerTask('amd', ['packager:amd', 'requirejs']);
this.registerTask('node', ['packager:cjs']);
this.registerTask('globals', ['packager:global']);
this.registerTask('tests', ['concat:tests']);
this.registerTask('release', 'Build final packages', ['amd', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
this.registerTask('release', 'Build final packages', ['amd', 'jshint', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-es6-module-transpiler');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-saucelabs');
grunt.loadNpmTasks('es6-module-packager');
grunt.task.loadTasks('tasks');
grunt.registerTask('packager-fork', function() {
// Allows us to run the packager task out of process to work around the multiple
// traceur exec issues
grunt.util.spawn({grunt: true, args: ['--stack', 'packager'], opts: {stdio: 'inherit'}}, this.async());
});
grunt.registerTask('test', function() {
var done = this.async();
var runner = childProcess.fork('./spec/env/runner', [], {stdio: 'inherit'});
runner.on('close', function(code) {
if (code != 0) {
grunt.fatal(code + ' tests failed');
}
done();
});
});
grunt.registerTask('bench', ['metrics']);
grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []);
grunt.registerTask('default', ['build', 'test', 'release']);
grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'sauce', 'metrics', 'publish:latest'] : ['default']);
grunt.registerTask('dev', ['clean', 'connect', 'watch']);
grunt.registerTask('default', ['clean', 'build', 'test', 'release']);
};
+29 -7
View File
@@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js)
[![Travis Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js)
[![Selenium Test Status](https://saucelabs.com/buildstatus/handlebars)](https://saucelabs.com/u/handlebars)
Handlebars.js
=============
@@ -14,9 +14,7 @@ Checkout the official Handlebars docs site at
Installing
----------
Installing Handlebars is easy. Simply download the package [from the
official site](http://handlebarsjs.com/) and add it to your web pages
(you should usually use the most recent version).
Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version).
Alternatively, if you prefer having the latest version of handlebars from
the 'master' branch, passing builds of the 'master' branch are automatically
@@ -325,6 +323,8 @@ Handlebars has been designed to work in any ECMAScript 3 environment. This inclu
Older versions and other runtimes are likely to work but have not been formally
tested.
[![Selenium Test Status](https://saucelabs.com/browser-matrix/handlebars.svg)](https://saucelabs.com/u/handlebars)
Performance
-----------
@@ -364,6 +364,7 @@ Handlebars in the Wild
* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
structure your views, also with automatic data binding support.
* [Ghost](https://ghost.org/) Just a blogging platform.
* [handlebars_assets](http://github.com/leshill/handlebars_assets): A Rails Asset Pipeline gem
from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
@@ -388,7 +389,7 @@ External Resources
* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
Have a project using Handlebars? Send us a [pull request](https://github.com/wycats/handlebars.js/pull/new/master)!
Have a project using Handlebars? Send us a [pull request][pull-request]!
Helping Out
-----------
@@ -405,12 +406,27 @@ 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.
@@ -423,6 +439,10 @@ yo release:release patch
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.
@@ -431,5 +451,7 @@ License
-------
Handlebars.js is released under the MIT license.
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
[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
+3 -3
View File
@@ -127,7 +127,7 @@ extension = new RegExp('\\.' + extension + '$');
var output = [];
if (!argv.simple) {
if (argv.amd) {
output.push('define([\'' + argv.handlebarPath + 'handlebars\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
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 {
@@ -150,9 +150,9 @@ function processTemplate(template, root) {
processTemplate(path, root || template);
}
});
} else {
} else if (extension.test(path)) {
var data = fs.readFileSync(path, 'utf8');
if (argv.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "1.1.1",
"version": "1.2.0",
"main": "handlebars.js",
"dependencies": {}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>1.1.1</version>
<version>1.2.0</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
+2 -1
View File
@@ -1,7 +1,8 @@
/*globals Handlebars: true */
import Handlebars from "./handlebars.runtime";
// Compiler imports
module AST from "./handlebars/compiler/ast";
import AST from "./handlebars/compiler/ast";
import { parser as Parser, parse } from "./handlebars/compiler/base";
import { Compiler, compile, precompile } from "./handlebars/compiler/compiler";
import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler";
+1
View File
@@ -1,3 +1,4 @@
/*globals Handlebars: true */
module base from "./handlebars/base";
// Each of these augment the Handlebars object. No need to setup here.
+10 -21
View File
@@ -1,8 +1,7 @@
/*globals Exception, Utils */
module Utils from "./utils";
import Exception from "./exception";
export var VERSION = "1.1.1";
export var VERSION = "1.2.0";
export var COMPILER_REVISION = 4;
export var REVISION_CHANGES = {
@@ -12,25 +11,11 @@ export var REVISION_CHANGES = {
4: '>= 1.0.0'
};
var toString = Object.prototype.toString,
var isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
};
}
function isArray(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
}
export function HandlebarsEnvironment(helpers, partials) {
this.helpers = helpers || {};
this.partials = partials || {};
@@ -107,7 +92,7 @@ function registerDefaultHelpers(instance) {
for(var j = context.length; i<j; i++) {
if (data) {
data.index = i;
data.first = (i === 0)
data.first = (i === 0);
data.last = (i === (context.length-1));
}
ret = ret + fn(context[i], { data: data });
@@ -115,7 +100,11 @@ function registerDefaultHelpers(instance) {
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) { data.key = key; }
if(data) {
data.key = key;
data.index = i;
data.first = (i === 0);
}
ret = ret + fn(context[key], {data: data});
i++;
}
+151 -139
View File
@@ -1,145 +1,157 @@
import Exception from "../exception";
export function ProgramNode(statements, inverseStrip, inverse) {
this.type = "program";
this.statements = statements;
this.strip = {};
var AST = {
ProgramNode: function(statements, inverseStrip, inverse) {
this.type = "program";
this.statements = statements;
this.strip = {};
if(inverse) {
this.inverse = new ProgramNode(inverse, inverseStrip);
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
}
export function MustacheNode(rawParams, hash, open, strip) {
this.type = "mustache";
this.hash = hash;
this.strip = strip;
var escapeFlag = open[3] || open[2];
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var eligibleHelper = this.eligibleHelper = id.isSimple;
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = eligibleHelper && (params.length || hash);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
}
export function PartialNode(partialName, context, strip) {
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.strip = strip;
}
export function BlockNode(mustache, program, inverse, close) {
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
}
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;
if (inverse && !program) {
this.isInverse = true;
}
}
export function ContentNode(string) {
this.type = "content";
this.string = string;
}
export function HashNode(pairs) {
this.type = "hash";
this.pairs = pairs;
}
export function IdNode(parts) {
this.type = "ID";
var original = "",
dig = [],
depth = 0;
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) { throw new Exception("Invalid path: " + original); }
else if (part === "..") { depth++; }
else { this.isScoped = true; }
if(inverse) {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
else { dig.push(part); }
},
MustacheNode: function(rawParams, hash, open, strip) {
this.type = "mustache";
this.hash = hash;
this.strip = strip;
// Open may be a string parsed from the parser or a passed boolean flag
if (open != null && open.charAt) {
// Must use charAt to support IE pre-10
var escapeFlag = open.charAt(3) || open.charAt(2);
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
} else {
this.escaped = !!open;
}
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var eligibleHelper = this.eligibleHelper = id.isSimple;
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = eligibleHelper && (params.length || hash);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
},
PartialNode: function(partialName, context, strip) {
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.strip = strip;
},
BlockNode: function(mustache, program, inverse, close) {
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
}
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;
if (inverse && !program) {
this.isInverse = true;
}
},
ContentNode: function(string) {
this.type = "content";
this.string = string;
},
HashNode: function(pairs) {
this.type = "hash";
this.pairs = pairs;
},
IdNode: function(parts) {
this.type = "ID";
var original = "",
dig = [],
depth = 0;
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) { throw new Exception("Invalid path: " + original); }
else if (part === "..") { depth++; }
else { this.isScoped = true; }
}
else { dig.push(part); }
}
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
this.stringModeValue = this.string;
},
PartialNameNode: function(name) {
this.type = "PARTIAL_NAME";
this.name = name.original;
},
DataNode: function(id) {
this.type = "DATA";
this.id = id;
},
StringNode: function(string) {
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
},
IntegerNode: function(integer) {
this.type = "INTEGER";
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
},
BooleanNode: function(bool) {
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
},
CommentNode: function(comment) {
this.type = "comment";
this.comment = comment;
}
};
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
this.stringModeValue = this.string;
}
export function PartialNameNode(name) {
this.type = "PARTIAL_NAME";
this.name = name.original;
}
export function DataNode(id) {
this.type = "DATA";
this.id = id;
}
export function StringNode(string) {
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
}
export function IntegerNode(integer) {
this.type = "INTEGER";
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
}
export function BooleanNode(bool) {
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
}
export function CommentNode(comment) {
this.type = "comment";
this.comment = comment;
}
// 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;
+1 -1
View File
@@ -1,5 +1,5 @@
import parser from "./parser";
module AST from "./ast";
import AST from "./ast";
export { parser };
+1 -1
View File
@@ -1,7 +1,7 @@
import Exception from "../exception";
import { parse } from "./base";
import JavaScriptCompiler from "./javascript-compiler";
module AST from "./ast";
import AST from "./ast";
export function Compiler() {}
@@ -32,6 +32,12 @@ JavaScriptCompiler.prototype = {
}
},
compilerInfo: function() {
var revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
return "this.compilerInfo = ["+revision+",'"+versions+"'];\n";
},
appendToBuffer: function(string) {
if (this.environment.isSimple) {
return "return " + string + ";";
@@ -165,9 +171,7 @@ JavaScriptCompiler.prototype = {
var source = this.mergeSource();
if (!this.isChild) {
var revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
source = this.compilerInfo()+source;
}
if (asObject) {
-1
View File
@@ -26,7 +26,6 @@ PrintVisitor.prototype.pad = function(string, newline) {
PrintVisitor.prototype.program = function(program) {
var out = "",
statements = program.statements,
inverse = program.inverse,
i, l;
for(i=0, l=statements.length; i<l; i++) {
+13 -19
View File
@@ -1,9 +1,8 @@
/*global Utils */
module Utils from "./utils";
import Exception from "./exception";
import { COMPILER_REVISION, REVISION_CHANGES } from "./base";
function checkRevision(compilerInfo) {
export function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
@@ -28,25 +27,20 @@ export function template(templateSpec, env) {
throw new Error("No environment passed to template");
}
var invokePartialWrapper;
if (env.compile) {
invokePartialWrapper = function(partial, name, context, helpers, partials, data) {
// TODO : Check this for all inputs and the options handling (partial flag, etc). This feels
// like there should be a common exec path
var result = invokePartial.apply(this, arguments);
if (result) { return result; }
// Note: Using env.VM references rather than local var references throughout this section to allow
// for external users to override these as psuedo-supported APIs.
var invokePartialWrapper = function(partial, name, context, helpers, partials, data) {
var result = env.VM.invokePartial.apply(this, arguments);
if (result != null) { return result; }
if (env.compile) {
var options = { helpers: helpers, partials: partials, data: data };
partials[name] = env.compile(partial, { data: data !== undefined }, env);
return partials[name](context, options);
};
} else {
invokePartialWrapper = function(partial, name /* , context, helpers, partials, data */) {
var result = invokePartial.apply(this, arguments);
if (result) { return result; }
} else {
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
};
}
}
};
// Just add water
var container = {
@@ -72,8 +66,8 @@ export function template(templateSpec, env) {
}
return ret;
},
programWithDepth: programWithDepth,
noop: noop,
programWithDepth: env.VM.programWithDepth,
noop: env.VM.noop,
compilerInfo: null
};
@@ -95,7 +89,7 @@ export function template(templateSpec, env) {
options.data);
if (!options.partial) {
checkRevision(container.compilerInfo);
env.VM.checkRevision(container.compilerInfo);
}
return result;
+22 -3
View File
@@ -1,7 +1,6 @@
/*jshint -W004 */
import SafeString from "./safe-string";
var isArray = Array.isArray;
var escape = {
"&": "&amp;",
"<": "&lt;",
@@ -20,12 +19,32 @@ function escapeChar(chr) {
export function extend(obj, value) {
for(var key in value) {
if(value.hasOwnProperty(key)) {
if(Object.prototype.hasOwnProperty.call(value, key)) {
obj[key] = value[key];
}
}
}
export var toString = Object.prototype.toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
};
}
export var isFunction;
export var isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof SafeString) {
+9 -6
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "1.1.1",
"version": "1.2.0",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
@@ -15,7 +15,7 @@
"url": "https://github.com/wycats/handlebars.js.git"
},
"author": "Yehuda Katz",
"license": "BSD",
"license": "MIT",
"readmeFilename": "README.md",
"engines": {
"node": ">=0.4.7"
@@ -33,19 +33,22 @@
"dustjs-linkedin": "~2.0.2",
"eco": "~1.1.0-rc-3",
"grunt": "~0.4.1",
"grunt-cli": "~0.1.10",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-jshint": "~0.6.3",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-requirejs": "~0.4.1",
"grunt-contrib-uglify": "~0.2.2",
"grunt-es6-module-transpiler": "joefiorini/grunt-es6-module-transpiler",
"es6-module-packager": "*",
"grunt-contrib-watch": "~0.5.3",
"grunt-saucelabs": "~4.1.2",
"es6-module-packager": "0.x",
"jison": "~0.3.0",
"keen.io": "0.0.3",
"mocha": "*",
"mustache": "~0.7.2",
"semver": "~2.1.0",
"should": "~1.2.2",
"underscore": "~1.5.1"
},
"main": "lib/index.js",
+37 -1
View File
@@ -2,7 +2,43 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.1...master)
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.0...master)
## v1.2.0 - December 23rd, 2013
- [#675](https://github.com/wycats/handlebars.js/issues/675) - Cannot compile empty template for partial ([@erwinw](https://api.github.com/users/erwinw))
- [#677](https://github.com/wycats/handlebars.js/issues/677) - Triple brace statements fail under IE ([@hamzaCM](https://api.github.com/users/hamzaCM))
- [#655](https://github.com/wycats/handlebars.js/issues/655) - Loading Handlebars using bower ([@niki4810](https://api.github.com/users/niki4810))
- [#657](https://github.com/wycats/handlebars.js/pull/657) - Fixes issue where cli compiles non handlebars templates ([@chrishoage](https://api.github.com/users/chrishoage))
- [#681](https://github.com/wycats/handlebars.js/pull/681) - Adds in-browser testing and Saucelabs CI ([@kpdecker](https://api.github.com/users/kpdecker))
- [#661](https://github.com/wycats/handlebars.js/pull/661) - Add @first and @index to #each object iteration ([@cgp](https://api.github.com/users/cgp))
- [#650](https://github.com/wycats/handlebars.js/pull/650) - Handlebars is MIT-licensed ([@thomasboyt](https://api.github.com/users/thomasboyt))
- [#641](https://github.com/wycats/handlebars.js/pull/641) - Document ember testing process ([@kpdecker](https://api.github.com/users/kpdecker))
- [#662](https://github.com/wycats/handlebars.js/issues/662) - handlebars-source 1.1.2 is missing from RubyGems.
- [#656](https://github.com/wycats/handlebars.js/issues/656) - Expose COMPILER_REVISION checks as a hook ([@machty](https://api.github.com/users/machty))
- [#668](https://github.com/wycats/handlebars.js/issues/668) - Consider publishing handlebars-runtime as a separate module on npm ([@dlmanning](https://api.github.com/users/dlmanning))
- [#679](https://github.com/wycats/handlebars.js/issues/679) - Unable to override invokePartial ([@mattbrailsford](https://api.github.com/users/mattbrailsford))
- [#646](https://github.com/wycats/handlebars.js/pull/646) - Fix "\\{{" immediately following "\{{" ([@dmarcotte](https://api.github.com/users/dmarcotte))
- Allow extend to work with non-prototyped objects - eb53f2e
- Add JavascriptCompiler public API tests - 1a751b2
- Add AST test coverage for more complex paths - ddea5be
- Fix handling of boolean escape in MustacheNode - b4968bb
Compatibility notes:
- `@index` and `@first` are now supported for `each` iteration on objects
- `Handlebars.VM.checkRevision` and `Handlebars.JavaScriptCompiler.prototype.compilerInfo` now available to modify the version checking behavior.
- Browserify users may link to the runtime library via `require('handlebars/runtime')`
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.2...v1.2.0)
## v1.1.2 - November 5th, 2013
- [#645](https://github.com/wycats/handlebars.js/issues/645) - 1.1.1 fails under IE8 ([@kpdecker](https://api.github.com/users/kpdecker))
- [#644](https://github.com/wycats/handlebars.js/issues/644) - Using precompiled templates (AMD mode) with handlebars.runtime 1.1.1 ([@fddima](https://api.github.com/users/fddima))
- Add simple binary utility tests - 96a45a4
- Fix empty string compilation - eea708a
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.1...v1.1.2)
## v1.1.1 - November 4th, 2013
+3
View File
@@ -0,0 +1,3 @@
// Create a simple path alias to allow browserify to resolve
// the runtime on a supported path.
module.exports = require('./dist/cjs/handlebars.runtime');
View File
+82
View File
@@ -0,0 +1,82 @@
/*global Handlebars, handlebarsEnv, shouldThrow */
describe('ast', function() {
if (!Handlebars.AST) {
return;
}
describe('MustacheNode', function() {
function testEscape(open, expected) {
var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false);
equals(mustache.escaped, expected);
}
it('should store args', function() {
var id = {isSimple: true},
hash = {},
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false);
equals(mustache.type, 'mustache');
equals(mustache.hash, hash);
equals(mustache.escaped, true);
equals(mustache.id, id);
equals(mustache.params.length, 1);
equals(mustache.params[0], 'param1');
equals(!!mustache.isHelper, true);
});
it('should accept token for escape', function() {
testEscape('{{', true);
testEscape('{{~', true);
testEscape('{{#', true);
testEscape('{{~#', true);
testEscape('{{/', true);
testEscape('{{~/', true);
testEscape('{{^', true);
testEscape('{{~^', true);
testEscape('{', true);
testEscape('{', true);
testEscape('{{&', false);
testEscape('{{~&', false);
testEscape('{{{', false);
testEscape('{{~{', false);
});
it('should accept boolean for escape', function() {
testEscape(true, true);
testEscape({}, true);
testEscape(false, false);
testEscape(undefined, false);
});
});
describe('BlockNode', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
new handlebarsEnv.AST.BlockNode({id: {original: 'foo'}}, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});
});
describe('IdNode', function() {
it('should throw on invalid path', function() {
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '..'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foo..");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '.'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foo.");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: 'this'},
{part: 'bar'}
]);
}, Handlebars.Exception, "Invalid path: foothis");
});
});
});
+5 -4
View File
@@ -1,3 +1,4 @@
/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo */
global.handlebarsEnv = null;
beforeEach(function() {
@@ -160,9 +161,9 @@ describe("basic context", function() {
it("this keyword nested inside path", function() {
var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("this keyword in helpers", function() {
@@ -181,8 +182,8 @@ describe("basic context", function() {
it("this keyword nested inside helpers param", function() {
var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
});
+2 -2
View File
@@ -42,9 +42,9 @@ describe('blocks', function() {
it("block with complex lookup using nested context", function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("block with deep nested complex lookup", function() {
+22 -1
View File
@@ -73,7 +73,7 @@ describe('builtin helpers', function() {
var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!";
var expected2 = "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!";
(actual === expected1 || actual === expected2).should.equal(true, "each with object argument iterates over the contents when not empty");
equals(actual === expected1 || actual === expected2, true, "each with object argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with object argument ignores the contents when empty");
});
@@ -98,6 +98,17 @@ describe('builtin helpers', function() {
equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
});
it("each object with @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
});
it("each with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
@@ -118,6 +129,16 @@ describe('builtin helpers', function() {
equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used");
});
it("each object with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "goodbye! cruel world!", "The @first variable is used");
});
it("each with @last", function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
+7 -7
View File
@@ -1,4 +1,4 @@
/*global CompilerContext */
/*global CompilerContext, Handlebars, handlebarsEnv, shouldThrow */
describe('data', function() {
it("passing in data to a compiled function that expects data - works with helpers", function() {
var template = CompilerContext.compile("{{hello}}", {data: true});
@@ -88,25 +88,25 @@ describe('data', function() {
it("parameter data throws when using this scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("parameter data throws when using parent scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("parameter data throws when using complex scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
(function() {
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("data is inherited downstream", function() {
+24 -4
View File
@@ -1,12 +1,12 @@
global.should = require('should');
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
var result = compileWithPartials(string, hashOrArray, partials);
result.should.equal(expected, "'" + expected + "' should === '" + result + "': " + message);
if (result !== expected) {
throw new Error("'" + expected + "' should === '" + result + "': " + message);
}
};
global.compileWithPartials = function(string, hashOrArray, partials) {
@@ -24,5 +24,25 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
global.equals = global.equal = function(a, b, msg) {
a.should.equal(b, msg || '');
if (a !== b) {
throw new Error("'" + b + "' should === '" + a + "'" + (msg ? ": " + msg : ''));
}
};
global.shouldThrow = function(callback, type, msg) {
var failed;
try {
callback();
failed = true;
} catch (err) {
if (type && !(err instanceof type)) {
throw new Error('Type failure');
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
throw new Error('Message failure');
}
}
if (failed) {
throw new Error('It failed to throw');
}
};
+11
View File
@@ -0,0 +1,11 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "";
return buffer;
});
});
+10 -10
View File
@@ -1,4 +1,4 @@
/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
/*global CompilerContext, Handlebars, shouldCompileTo, shouldCompileToWithPartials, shouldThrow, handlebarsEnv */
describe('helpers', function() {
it("helper with complex lookup$", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
@@ -212,10 +212,10 @@ describe('helpers', function() {
});
it("using a quote in the middle of a parameter raises an error", function() {
(function() {
var string = 'Message: {{hello wo"rld"}}';
var string = 'Message: {{hello wo"rld"}}';
shouldThrow(function() {
CompilerContext.compile(string);
}).should.throw(Error);
}, Error);
});
it("escaping a String is possible", function(){
@@ -351,10 +351,10 @@ describe('helpers', function() {
describe("helperMissing", function() {
it("if a context is not found, helperMissing is used", function() {
(function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{hello}} {{link_to world}}");
template({});
}).should.throw(/Missing helper: 'link_to'/);
}, undefined, /Missing helper: 'link_to'/);
});
it("if a context is not found, custom helperMissing is used", function() {
@@ -417,9 +417,9 @@ describe('helpers', function() {
equal(result, "bar", "'bar' should === '" + result);
});
it("Unknown helper call in knownHelpers only mode should throw", function() {
(function() {
shouldThrow(function() {
CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
}).should.throw(Error);
}, Error);
});
});
@@ -491,7 +491,7 @@ describe('helpers', function() {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
}
};
var context = {
@@ -513,7 +513,7 @@ describe('helpers', function() {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
}
};
var context = {
+59
View File
@@ -0,0 +1,59 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="/dist/handlebars.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
var ret;
eval('ret = ' + templateSpec);
return ret;
}
</script>
<script src="/tmp/tests.js"></script>
<script>
onload = function(){
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
mocha.run();
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
+69
View File
@@ -0,0 +1,69 @@
/*global Handlebars, beforeEach, handlebarsEnv, shouldCompileTo */
describe('javascript-compiler api', function() {
if (!Handlebars.JavaScriptCompiler) {
return;
}
describe('#nameLookup', function() {
var $superName;
beforeEach(function() {
$superName = handlebarsEnv.JavaScriptCompiler.prototype.nameLookup;
});
afterEach(function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = $superName;
});
it('should allow override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
return parent + '.bar_' + name;
};
shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food");
});
});
describe('#compilerInfo', function() {
var $superCheck, $superInfo;
beforeEach(function() {
$superCheck = handlebarsEnv.VM.checkRevision;
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
});
afterEach(function() {
handlebarsEnv.VM.checkRevision = $superCheck;
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
});
it('should allow compilerInfo override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function() {
return 'this.compilerInfo = "crazy";';
};
handlebarsEnv.VM.checkRevision = function(compilerInfo) {
if (compilerInfo !== 'crazy') {
throw new Error('It didn\'t work');
}
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "food ");
});
});
describe('buffer', function() {
var $superAppend, $superCreate;
beforeEach(function() {
$superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer;
$superCreate = handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer;
});
afterEach(function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend;
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = $superCreate;
});
it('should allow init buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_');
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "foo_food ");
});
it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return $superAppend.call(this, string + ' + "_foo"');
};
shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo");
});
});
});
+59 -58
View File
@@ -1,3 +1,4 @@
/*global Handlebars */
describe('parser', function() {
if (!Handlebars.print) {
return;
@@ -9,165 +10,165 @@ describe('parser', function() {
}
it('parses simple mustaches', function() {
ast_for('{{foo}}').should.equal("{{ ID:foo [] }}\n");
ast_for('{{foo?}}').should.equal("{{ ID:foo? [] }}\n");
ast_for('{{foo_}}').should.equal("{{ ID:foo_ [] }}\n");
ast_for('{{foo-}}').should.equal("{{ ID:foo- [] }}\n");
ast_for('{{foo:}}').should.equal("{{ ID:foo: [] }}\n");
equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n");
equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n");
equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n");
equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n");
equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n");
});
it('parses simple mustaches with data', function() {
ast_for("{{@foo}}").should.equal("{{ @ID:foo [] }}\n");
equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n");
});
it('parses mustaches with paths', function() {
ast_for("{{foo/bar}}").should.equal("{{ PATH:foo/bar [] }}\n");
equals(ast_for("{{foo/bar}}"), "{{ PATH:foo/bar [] }}\n");
});
it('parses mustaches with this/foo', function() {
ast_for("{{this/foo}}").should.equal("{{ ID:foo [] }}\n");
equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n");
});
it('parses mustaches with - in a path', function() {
ast_for("{{foo-bar}}").should.equal("{{ ID:foo-bar [] }}\n");
equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n");
});
it('parses mustaches with parameters', function() {
ast_for("{{foo bar}}").should.equal("{{ ID:foo [ID:bar] }}\n");
equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n");
});
it('parses mustaches with string parameters', function() {
ast_for("{{foo bar \"baz\" }}").should.equal('{{ ID:foo [ID:bar, "baz"] }}\n');
equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n');
});
it('parses mustaches with INTEGER parameters', function() {
ast_for("{{foo 1}}").should.equal("{{ ID:foo [INTEGER{1}] }}\n");
equals(ast_for("{{foo 1}}"), "{{ ID:foo [INTEGER{1}] }}\n");
});
it('parses mustaches with BOOLEAN parameters', function() {
ast_for("{{foo true}}").should.equal("{{ ID:foo [BOOLEAN{true}] }}\n");
ast_for("{{foo false}}").should.equal("{{ ID:foo [BOOLEAN{false}] }}\n");
equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n");
equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n");
});
it('parses mutaches with DATA parameters', function() {
ast_for("{{foo @bar}}").should.equal("{{ ID:foo [@ID:bar] }}\n");
equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n");
});
it('parses mustaches with hash arguments', function() {
ast_for("{{foo bar=baz}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz} }}\n");
ast_for("{{foo bar=1}}").should.equal("{{ ID:foo [] HASH{bar=INTEGER{1}} }}\n");
ast_for("{{foo bar=true}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
ast_for("{{foo bar=false}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
ast_for("{{foo bar=@baz}}").should.equal("{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n");
equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=INTEGER{1}} }}\n");
equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
ast_for("{{foo bar=baz bat=bam}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
ast_for("{{foo bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
ast_for("{{foo bat='bam'}}").should.equal('{{ ID:foo [] HASH{bat="bam"} }}\n');
equals(ast_for("{{foo bat='bam'}}"), '{{ ID:foo [] HASH{bat="bam"} }}\n');
ast_for("{{foo omg bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=INTEGER{1}} }}\n');
ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=INTEGER{1}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
});
it('parses contents followed by a mustache', function() {
ast_for("foo bar {{baz}}").should.equal("CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
});
it('parses a partial', function() {
ast_for("{{> foo }}").should.equal("{{> PARTIAL:foo }}\n");
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n");
});
it('parses a partial with context', function() {
ast_for("{{> foo bar}}").should.equal("{{> PARTIAL:foo ID:bar }}\n");
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n");
});
it('parses a partial with a complex name', function() {
ast_for("{{> shared/partial?.bar}}").should.equal("{{> PARTIAL:shared/partial?.bar }}\n");
equals(ast_for("{{> shared/partial?.bar}}"), "{{> PARTIAL:shared/partial?.bar }}\n");
});
it('parses a comment', function() {
ast_for("{{! this is a comment }}").should.equal("{{! ' this is a comment ' }}\n");
equals(ast_for("{{! this is a comment }}"), "{{! ' this is a comment ' }}\n");
});
it('parses a multi-line comment', function() {
ast_for("{{!\nthis is a multi-line comment\n}}").should.equal("{{! \'\nthis is a multi-line comment\n\' }}\n");
equals(ast_for("{{!\nthis is a multi-line comment\n}}"), "{{! \'\nthis is a multi-line comment\n\' }}\n");
});
it('parses an inverse section', function() {
ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses an inverse (else-style) section', function() {
ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses empty blocks', function() {
ast_for("{{#foo}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
});
it('parses empty blocks with empty inverse section', function() {
ast_for("{{#foo}}{{^}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
});
it('parses empty blocks with empty inverse (else-style) section', function() {
ast_for("{{#foo}}{{else}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
});
it('parses non-empty blocks with empty inverse section', function() {
ast_for("{{#foo}} bar {{^}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
ast_for("{{#foo}} bar {{else}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses empty blocks with non-empty inverse section', function() {
ast_for("{{#foo}}{{^}} bar {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
ast_for("{{#foo}}{{else}} bar {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses a standalone inverse section', function() {
ast_for("{{^foo}}bar{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
});
it("raises if there's a Parse error", function() {
(function() {
shouldThrow(function() {
ast_for("foo{{^}}bar");
}).should.throw(/Parse error on line 1/);
(function() {
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo}");
}).should.throw(/Parse error on line 1/);
(function() {
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo &}}");
}).should.throw(/Parse error on line 1/);
(function() {
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{#goodbyes}}{{/hellos}}");
}).should.throw(/goodbyes doesn't match hellos/);
}, Error, /goodbyes doesn't match hellos/);
});
it('knows how to report the correct line number in errors', function() {
(function() {
shouldThrow(function() {
ast_for("hello\nmy\n{{foo}");
}).should.throw(/Parse error on line 3/);
(function() {
}, Error, /Parse error on line 3/);
shouldThrow(function() {
ast_for("hello\n\nmy\n\n{{foo}");
}).should.throw(/Parse error on line 5/);
}, Error, /Parse error on line 5/);
});
it('knows how to report the correct line number in errors when the first character is a newline', function() {
(function() {
shouldThrow(function() {
ast_for("\n\nhello\n\nmy\n\n{{foo}");
}).should.throw(/Parse error on line 7/);
}, Error, /Parse error on line 7/);
});
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])).should.equal("CONTENT[ \'Hello\' ]\n");
equals(ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])), "CONTENT[ \'Hello\' ]\n");
});
});
});
+10 -4
View File
@@ -32,17 +32,17 @@ describe('partials', function() {
});
it("rendering undefined partial throws an exception", function() {
(function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
template();
}).should.throw(Handlebars.Exception, 'The partial whatever could not be found');
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it("rendering template partial in vm mode throws an exception", function() {
(function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
template();
}).should.throw(Handlebars.Exception, 'The partial whatever could not be found');
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it("rendering function partial in vm mode", function() {
@@ -123,4 +123,10 @@ describe('partials', function() {
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
});
it('should handle empty partial', 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: "); });
});
+18 -7
View File
@@ -1,4 +1,4 @@
/*global CompilerContext, shouldCompileTo */
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
describe('Regressions', function() {
it("GH-94: Cannot read property of undefined", function() {
var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
@@ -85,12 +85,19 @@ describe('Regressions', function() {
});
it('GH-437: Matching escaping', function() {
(function() {
shouldThrow(function() {
CompilerContext.compile('{{{a}}');
}).should.throw(Error);
(function() {
}, Error);
shouldThrow(function() {
CompilerContext.compile('{{a}}}');
}).should.throw(Error);
}, Error);
});
it("GH-676: Using array in escaping mustache fails", function() {
var string = "{{arr}}";
var data = { "arr": [1,2] };
shouldCompileTo(string, data, data.arr.toString(), "it works as expected");
});
it("Mustache man page", function() {
@@ -106,14 +113,18 @@ describe('Regressions', function() {
});
it("Passing falsy values to Handlebars.compile throws an error", function() {
(function() {
shouldThrow(function() {
CompilerContext.compile(null);
}).should.throw("You must pass a string or Handlebars AST to Handlebars.precompile. You passed null");
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
});
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("can pass through an empty string", function() {
equal(Handlebars.compile('')(), '');
});
}
});
+4 -4
View File
@@ -2,22 +2,22 @@ if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) {
describe('Require', function() {
it('Load .handlebars files with require()', function() {
var template = require("./artifacts/example_1");
template.should.eql(require("./artifacts/example_1.handlebars"));
equal(template, require("./artifacts/example_1.handlebars"));
var expected = 'foo\n';
var result = template({foo: "foo"});
result.should.equal(expected);
equal(result, expected);
});
it('Load .hbs files with require()', function() {
var template = require("./artifacts/example_2");
template.should.eql(require("./artifacts/example_2.hbs"));
equal(template, require("./artifacts/example_2.hbs"));
var expected = 'Hello, World!\n';
var result = template({name: "World"});
result.should.equal(expected);
equal(result, expected);
});
});
}
+132 -123
View File
@@ -1,13 +1,12 @@
var should = require('should');
should.Assertion.prototype.match_tokens = function(tokens) {
this.obj.forEach(function(value, index) {
value.name.should.equal(tokens[index]);
});
};
should.Assertion.prototype.be_token = function(name, text) {
this.obj.should.eql({name: name, text: text});
};
function shouldMatchTokens(result, tokens) {
for (var index = 0; index < result.length; index++) {
equals(result[index].name, tokens[index]);
}
}
function shouldBeToken(result, name, text) {
equals(result.name, name);
equals(result.text, text);
}
describe('Tokenizer', function() {
if (!Handlebars.Parser) {
@@ -35,324 +34,334 @@ describe('Tokenizer', function() {
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
var result = tokenize("{{foo}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
});
it('supports unescaping with &', function() {
var result = tokenize("{{&bar}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
result[0].should.be_token("OPEN", "{{&");
result[1].should.be_token("ID", "bar");
shouldBeToken(result[0], "OPEN", "{{&");
shouldBeToken(result[1], "ID", "bar");
});
it('supports unescaping with {{{', function() {
var result = tokenize("{{{bar}}}");
result.should.match_tokens(['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
result[1].should.be_token("ID", "bar");
shouldBeToken(result[1], "ID", "bar");
});
it('supports escaping delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} {{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[3].should.be_token("CONTENT", " ");
result[4].should.be_token("CONTENT", "{{bar}} ");
shouldBeToken(result[3], "CONTENT", " ");
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
});
it('supports escaping multiple delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
result[3].should.be_token("CONTENT", " ");
result[4].should.be_token("CONTENT", "{{bar}} ");
result[5].should.be_token("CONTENT", "{{baz}}");
shouldBeToken(result[3], "CONTENT", " ");
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
shouldBeToken(result[5], "CONTENT", "{{baz}}");
});
it('supports escaping a triple stash', function() {
var result = tokenize("{{foo}} \\{{{bar}}} {{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[4].should.be_token("CONTENT", "{{{bar}}} ");
shouldBeToken(result[4], "CONTENT", "{{{bar}}} ");
});
it('supports escaping escape character', function() {
var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[3].should.be_token("CONTENT", " \\");
result[5].should.be_token("ID", "bar");
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
});
it('supports escaping multiple escape characters', function() {
var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[3].should.be_token("CONTENT", " \\");
result[5].should.be_token("ID", "bar");
result[7].should.be_token("CONTENT", " \\");
result[9].should.be_token("ID", "baz");
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[7], "CONTENT", " \\");
shouldBeToken(result[9], "ID", "baz");
});
it('supports mixed escaped delimiters and escaped escape characters', function() {
it('supports escaped mustaches after escaped escape characters', function() {
var result = tokenize("{{foo}} \\\\{{bar}} \\{{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
result[3].should.be_token("CONTENT", " \\");
result[4].should.be_token("OPEN", "{{");
result[5].should.be_token("ID", "bar");
result[7].should.be_token("CONTENT", " ");
result[8].should.be_token("CONTENT", "{{baz}}");
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[4], "OPEN", "{{");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[7], "CONTENT", " ");
shouldBeToken(result[8], "CONTENT", "{{baz}}");
});
it('supports escaped escape characters after escaped mustaches', function() {
var result = tokenize("{{foo}} \\{{bar}} \\\\{{baz}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
shouldBeToken(result[5], "CONTENT", "\\");
shouldBeToken(result[6], "OPEN", "{{");
shouldBeToken(result[7], "ID", "baz");
});
it('supports escaped escape character on a triple stash', function() {
var result = tokenize("{{foo}} \\\\{{{bar}}} {{baz}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[3].should.be_token("CONTENT", " \\");
result[5].should.be_token("ID", "bar");
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
});
it('tokenizes a simple path', function() {
var result = tokenize("{{foo/bar}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows dot notation', function() {
var result = tokenize("{{foo.bar}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
tokenize("{{foo.bar.baz}}").should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(tokenize("{{foo.bar.baz}}"), ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows path literals with []', function() {
var result = tokenize("{{foo.[bar]}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows multiple path literals on a line with []', function() {
var result = tokenize("{{foo.[bar]}}{{foo.[baz]}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('tokenizes {{.}} as OPEN ID CLOSE', function() {
var result = tokenize("{{.}}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
});
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() {
var result = tokenize("{{../foo/bar}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "..");
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
});
it('tokenizes a path with .. as a parent path', function() {
var result = tokenize("{{../foo.bar}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "..");
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
});
it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() {
var result = tokenize("{{this/foo}}");
result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "this");
result[3].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "this");
shouldBeToken(result[3], "ID", "foo");
});
it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() {
var result = tokenize("{{ foo }}");
result.should.match_tokens(['OPEN', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
});
it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function() {
var result = tokenize("{{ foo \n bar }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
});
it('tokenizes raw content as "CONTENT"', function() {
var result = tokenize("foo {{ bar }} baz");
result.should.match_tokens(['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
result[0].should.be_token("CONTENT", "foo ");
result[4].should.be_token("CONTENT", " baz");
shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
shouldBeToken(result[0], "CONTENT", "foo ");
shouldBeToken(result[4], "CONTENT", " baz");
});
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{> foo}}");
result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() {
var result = tokenize("{{> foo bar }}");
result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
});
it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo}}");
result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo }}");
result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo/bar.baz }}");
result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('tokenizes a comment as "COMMENT"', function() {
var result = tokenize("foo {{! this is a comment }} bar {{ baz }}");
result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[1].should.be_token("COMMENT", " this is a comment ");
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a comment ");
});
it('tokenizes a block comment as "COMMENT"', function() {
var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}");
result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[1].should.be_token("COMMENT", " this is a {{comment}} ");
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a {{comment}} ");
});
it('tokenizes a block comment with whitespace as "COMMENT"', function() {
var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}");
result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
result[1].should.be_token("COMMENT", " this is a\n{{comment}}\n");
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a\n{{comment}}\n");
});
it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() {
var result = tokenize("{{#foo}}content{{/foo}}");
result.should.match_tokens(['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});
it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
tokenize("{{^}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
tokenize("{{else}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
tokenize("{{ else }}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{^}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{else}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['OPEN_INVERSE', 'CLOSE']);
});
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
var result = tokenize("{{^foo}}");
result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
});
it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() {
var result = tokenize("{{^ foo }}");
result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
});
it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() {
var result = tokenize("{{ foo bar baz }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
result[1].should.be_token("ID", "foo");
result[2].should.be_token("ID", "bar");
result[3].should.be_token("ID", "baz");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[2], "ID", "bar");
shouldBeToken(result[3], "ID", "baz");
});
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() {
var result = tokenize("{{ foo bar \"baz\" }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
result[3].should.be_token("STRING", "baz");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz");
});
it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function() {
var result = tokenize("{{ foo bar \'baz\' }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
result[3].should.be_token("STRING", "baz");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz");
});
it('tokenizes String params with spaces inside as "STRING"', function() {
var result = tokenize("{{ foo bar \"baz bat\" }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
result[3].should.be_token("STRING", "baz bat");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz bat");
});
it('tokenizes String params with escapes quotes as STRING', function() {
var result = tokenize('{{ foo "bar\\"baz" }}');
result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']);
result[2].should.be_token("STRING", 'bar"baz');
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "STRING", 'bar"baz');
});
it('tokenizes String params using single quotes with escapes quotes as STRING', function() {
var result = tokenize("{{ foo 'bar\\'baz' }}");
result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']);
result[2].should.be_token("STRING", "bar'baz");
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "STRING", "bar'baz");
});
it('tokenizes numbers', function() {
var result = tokenize('{{ foo 1 }}');
result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']);
result[2].should.be_token("INTEGER", "1");
shouldMatchTokens(result, ['OPEN', 'ID', 'INTEGER', 'CLOSE']);
shouldBeToken(result[2], "INTEGER", "1");
result = tokenize('{{ foo -1 }}');
result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']);
result[2].should.be_token("INTEGER", "-1");
shouldMatchTokens(result, ['OPEN', 'ID', 'INTEGER', 'CLOSE']);
shouldBeToken(result[2], "INTEGER", "-1");
});
it('tokenizes booleans', function() {
var result = tokenize('{{ foo true }}');
result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
result[2].should.be_token("BOOLEAN", "true");
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "true");
result = tokenize('{{ foo false }}');
result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
result[2].should.be_token("BOOLEAN", "false");
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "false");
});
it('tokenizes hash arguments', function() {
var result = tokenize("{{ foo bar=baz }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=bat }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=1 }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'INTEGER', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'INTEGER', 'CLOSE']);
result = tokenize("{{ foo bar baz=true }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar baz=false }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar\n baz=bat }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{foo omg bar=baz bat=\"bam\"}}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
result[2].should.be_token("ID", "omg");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "ID", "omg");
});
it('tokenizes special @ identifiers', function() {
var result = tokenize("{{ @foo }}");
result.should.match_tokens(['OPEN', 'DATA', 'ID', 'CLOSE']);
result[2].should.be_token("ID", "foo");
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[2], "ID", "foo");
result = tokenize("{{ foo @bar }}");
result.should.match_tokens(['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
result[3].should.be_token("ID", "bar");
shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[3], "ID", "bar");
result = tokenize("{{ foo bar=@baz }}");
result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
result[5].should.be_token("ID", "baz");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[5], "ID", "baz");
});
it('does not time out in a mustache with a single } followed by EOF', function() {
tokenize("{{foo}").should.match_tokens(['OPEN', 'ID']);
shouldMatchTokens(tokenize("{{foo}"), ['OPEN', 'ID']);
});
it('does not time out in a mustache when invalid ID characters are used', function() {
tokenize("{{foo & }}").should.match_tokens(['OPEN', 'ID']);
shouldMatchTokens(tokenize("{{foo & }}"), ['OPEN', 'ID']);
});
});
+23 -21
View File
@@ -1,11 +1,13 @@
/*global shouldCompileTo */
/*global Handlebars, shouldCompileTo */
describe('utils', function() {
describe('#SafeString', function() {
it("constructing a safestring from a string and checking its type", function() {
var safe = new Handlebars.SafeString("testing 1, 2, 3");
safe.should.be.instanceof(Handlebars.SafeString);
(safe == "testing 1, 2, 3").should.equal(true, "SafeString is equivalent to its underlying string");
if (!(safe instanceof Handlebars.SafeString)) {
throw new Error('Must be instance of SafeString');
}
equals(safe == 'testing 1, 2, 3', true, 'SafeString is equivalent to its underlying string');
});
it("it should not escape SafeString properties", function() {
@@ -17,41 +19,41 @@ describe('utils', function() {
describe('#escapeExpression', function() {
it('shouhld escape html', function() {
Handlebars.Utils.escapeExpression('foo<&"\'>').should.equal('foo&lt;&amp;&quot;&#x27;&gt;');
equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo&lt;&amp;&quot;&#x27;&gt;');
});
it('should not escape SafeString', function() {
var string = new Handlebars.SafeString('foo<&"\'>');
Handlebars.Utils.escapeExpression(string).should.equal('foo<&"\'>');
equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>');
});
it('should handle falsy', function() {
Handlebars.Utils.escapeExpression('').should.equal('');
Handlebars.Utils.escapeExpression(undefined).should.equal('');
Handlebars.Utils.escapeExpression(null).should.equal('');
Handlebars.Utils.escapeExpression(false).should.equal('');
equals(Handlebars.Utils.escapeExpression(''), '');
equals(Handlebars.Utils.escapeExpression(undefined), '');
equals(Handlebars.Utils.escapeExpression(null), '');
equals(Handlebars.Utils.escapeExpression(false), '');
Handlebars.Utils.escapeExpression(0).should.equal('0');
equals(Handlebars.Utils.escapeExpression(0), '0');
});
it('should handle empty objects', function() {
Handlebars.Utils.escapeExpression({}).should.equal({}.toString());
Handlebars.Utils.escapeExpression([]).should.equal([].toString());
equals(Handlebars.Utils.escapeExpression({}), {}.toString());
equals(Handlebars.Utils.escapeExpression([]), [].toString());
});
});
describe('#isEmpty', function() {
it('should not be empty', function() {
Handlebars.Utils.isEmpty(undefined).should.equal(true);
Handlebars.Utils.isEmpty(null).should.equal(true);
Handlebars.Utils.isEmpty(false).should.equal(true);
Handlebars.Utils.isEmpty('').should.equal(true);
Handlebars.Utils.isEmpty([]).should.equal(true);
equals(Handlebars.Utils.isEmpty(undefined), true);
equals(Handlebars.Utils.isEmpty(null), true);
equals(Handlebars.Utils.isEmpty(false), true);
equals(Handlebars.Utils.isEmpty(''), true);
equals(Handlebars.Utils.isEmpty([]), true);
});
it('should be empty', function() {
Handlebars.Utils.isEmpty(0).should.equal(false);
Handlebars.Utils.isEmpty([1]).should.equal(false);
Handlebars.Utils.isEmpty('foo').should.equal(false);
Handlebars.Utils.isEmpty({bar: 1}).should.equal(false);
equals(Handlebars.Utils.isEmpty(0), false);
equals(Handlebars.Utils.isEmpty([1]), false);
equals(Handlebars.Utils.isEmpty('foo'), false);
equals(Handlebars.Utils.isEmpty({bar: 1}), false);
});
});
});
+4 -4
View File
@@ -43,9 +43,9 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
[^\x00]+ return 'CONTENT';
<emu>[^\x00]{2,}?/("{{"|<<EOF>>) {
if(yytext.slice(-1) !== "\\") this.popState();
if(yytext.slice(-1) === "\\") strip(0,1);
// marks CONTENT up to the next mustache or escaped mustache
<emu>[^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<<EOF>>) {
this.popState();
return 'CONTENT';
}
@@ -66,7 +66,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<mu>".." return 'ID';
<mu>"."/{LOOKAHEAD} return 'ID';
<mu>[\/.] return 'SEP';
<mu>\s+ /*ignore whitespace*/
<mu>\s+ // ignore whitespace
<mu>"}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED';
<mu>{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE';
<mu>'"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING';
+3 -2
View File
@@ -6,8 +6,8 @@
function stripFlags(open, close) {
return {
left: open[2] === '~',
right: close[0] === '~' || close[1] === '~'
left: open.charAt(2) === '~',
right: close.charAt(0) === '~' || close.charAt(1) === '~'
};
}
@@ -17,6 +17,7 @@ function stripFlags(open, close) {
root
: statements EOF { return new yy.ProgramNode($1); }
| EOF { return new yy.ProgramNode([]); }
;
program
+1
View File
@@ -0,0 +1 @@
/* jshint ignore:start */
+1
View File
@@ -1 +1,2 @@
export default handlebars;
/* jshint ignore:end */
-13
View File
@@ -1,13 +0,0 @@
module.exports = function(grunt) {
grunt.registerMultiTask('packager', 'Transpiles scripts written using ES6 to ES5.', function() {
// Execute in here to prevent traceur private var blowup
var Packager = require('es6-module-packager').default,
fs = require('fs');
var options = this.options();
this.files.forEach(function(file) {
var packager = new Packager(file.src[0], {export: options.export});
fs.writeFileSync(file.dest, packager.toLocals());
});
});
};
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports = function(grunt) {
return;
}
var src = ['handlebars.js', 'src/parser-suffix.js'].map(grunt.file.read).join('');
var src = ['src/parser-prefix.js', 'handlebars.js', 'src/parser-suffix.js'].map(grunt.file.read).join('');
grunt.file.delete('handlebars.js');
grunt.file.write('lib/handlebars/compiler/parser.js', src);
+32
View File
@@ -0,0 +1,32 @@
var childProcess = require('child_process'),
fs = require('fs');
module.exports = function(grunt) {
grunt.registerTask('test:bin', function() {
var done = this.async();
childProcess.exec('./bin/handlebars -a spec/artifacts/empty.handlebars', function(err, stdout) {
if (err) {
throw err;
}
if (stdout.toString() !== fs.readFileSync('./spec/expected/empty.amd.js').toString()) {
throw new Error('Expected binary output differed');
}
done();
});
});
grunt.registerTask('test:mocha', function() {
var done = this.async();
var runner = childProcess.fork('./spec/env/runner', [], {stdio: 'inherit'});
runner.on('close', function(code) {
if (code != 0) {
grunt.fatal(code + ' tests failed');
}
done();
});
});
grunt.registerTask('test', ['test:bin', 'test:mocha']);
};