`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces.
+- AST constructors have been dropped in favor of plain old javascript objects
+- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0.
+
+[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...v4.0.0)
+
+## v4.0.0 - September 1st, 2015
+- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker))
+- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski))
+- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley))
+- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13))
+- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn))
+- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch))
+- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin))
+- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski))
+- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick))
+- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia))
+- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other?
+- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks.
+- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions
+- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals
+- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers
+- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84)
+- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba)
+- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6)
+- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1)
+- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a)
+- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac)
+- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc)
+- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760)
+- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab)
+- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da)
+- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038)
+- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35)
+- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7)
+- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b)
+- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106)
+- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c)
+- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa)
+
+Compatibility notes:
+- Depthed paths are now conditional pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior the better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028).
+- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces.
+- AST constructors have been dropped in favor of plain old javascript objects
+- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0.
+
+[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0)
## v3.0.3 - April 28th, 2015
- [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://api.github.com/users/boskee))
diff --git a/spec/amd.html b/spec/amd.html
index 5de33c1cf..1149dc706 100644
--- a/spec/amd.html
+++ b/spec/amd.html
@@ -1,3 +1,4 @@
+
Mocha
diff --git a/spec/ast.js b/spec/ast.js
index 6f492fddd..8f346d882 100644
--- a/spec/ast.js
+++ b/spec/ast.js
@@ -3,113 +3,49 @@ describe('ast', function() {
return;
}
- var LOCATION_INFO = {
- start: {
- line: 1,
- column: 1
- },
- end: {
- line: 1,
- column: 1
- }
- };
-
- function testLocationInfoStorage(node) {
- equals(node.loc.start.line, 1);
- equals(node.loc.start.column, 1);
- equals(node.loc.end.line, 1);
- equals(node.loc.end.column, 1);
- }
+ var AST = Handlebars.AST;
- describe('MustacheStatement', function() {
- it('should store args', function() {
- var mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO);
- equals(mustache.type, 'MustacheStatement');
- equals(mustache.escaped, true);
- testLocationInfoStorage(mustache);
- });
- });
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}, Handlebars.Exception, "foo doesn't match bar - 2:5");
});
-
- it('stores location info', function() {
- var mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {});
- var block = new handlebarsEnv.AST.BlockStatement(
- mustacheNode,
- null, null,
- {body: []},
- {body: []},
- {},
- {},
- {},
- LOCATION_INFO);
- testLocationInfoStorage(block);
- });
- });
- describe('PathExpression', function() {
- it('stores location info', function() {
- var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO);
- testLocationInfoStorage(idNode);
- });
});
- describe('Hash', function() {
- it('stores location info', function() {
- var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO);
- testLocationInfoStorage(hash);
- });
- });
-
- describe('ContentStatement', function() {
- it('stores location info', function() {
- var content = new handlebarsEnv.AST.ContentStatement('HI', LOCATION_INFO);
- testLocationInfoStorage(content);
- });
- });
-
- describe('CommentStatement', function() {
- it('stores location info', function() {
- var comment = new handlebarsEnv.AST.CommentStatement('HI', {}, LOCATION_INFO);
- testLocationInfoStorage(comment);
- });
- });
-
- describe('NumberLiteral', function() {
- it('stores location info', function() {
- var integer = new handlebarsEnv.AST.NumberLiteral('6', LOCATION_INFO);
- testLocationInfoStorage(integer);
- });
- });
+ describe('helpers', function() {
+ describe('#helperExpression', function() {
+ it('should handle mustache statements', function() {
+ equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: undefined}), false);
+ equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [1], hash: undefined}), true);
+ equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: {}}), true);
+ });
+ it('should handle block statements', function() {
+ equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: undefined}), false);
+ equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [1], hash: undefined}), true);
+ equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: {}}), true);
+ });
+ it('should handle subexpressions', function() {
+ equals(AST.helpers.helperExpression({type: 'SubExpression'}), true);
+ });
+ it('should work with non-helper nodes', function() {
+ equals(AST.helpers.helperExpression({type: 'Program'}), false);
- describe('StringLiteral', function() {
- it('stores location info', function() {
- var string = new handlebarsEnv.AST.StringLiteral('6', LOCATION_INFO);
- testLocationInfoStorage(string);
- });
- });
+ equals(AST.helpers.helperExpression({type: 'PartialStatement'}), false);
+ equals(AST.helpers.helperExpression({type: 'ContentStatement'}), false);
+ equals(AST.helpers.helperExpression({type: 'CommentStatement'}), false);
- describe('BooleanLiteral', function() {
- it('stores location info', function() {
- var bool = new handlebarsEnv.AST.BooleanLiteral('true', LOCATION_INFO);
- testLocationInfoStorage(bool);
- });
- });
+ equals(AST.helpers.helperExpression({type: 'PathExpression'}), false);
- describe('PartialStatement', function() {
- it('stores location info', function() {
- var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO);
- testLocationInfoStorage(pn);
- });
- });
+ equals(AST.helpers.helperExpression({type: 'StringLiteral'}), false);
+ equals(AST.helpers.helperExpression({type: 'NumberLiteral'}), false);
+ equals(AST.helpers.helperExpression({type: 'BooleanLiteral'}), false);
+ equals(AST.helpers.helperExpression({type: 'UndefinedLiteral'}), false);
+ equals(AST.helpers.helperExpression({type: 'NullLiteral'}), false);
- describe('Program', function() {
- it('storing location info', function() {
- var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO);
- testLocationInfoStorage(pn);
+ equals(AST.helpers.helperExpression({type: 'Hash'}), false);
+ equals(AST.helpers.helperExpression({type: 'HashPair'}), false);
+ });
});
});
@@ -123,8 +59,18 @@ describe('ast', function() {
equals(node.loc.end.column, lastColumn);
}
- ast = Handlebars.parse('line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n' +
- 'line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}');
+ ast = Handlebars.parse(
+ 'line 1 {{line1Token}}\n' // 1
+ + ' line 2 {{line2token}}\n' // 2
+ + ' line 3 {{#blockHelperOnLine3}}\n' // 3
+ + 'line 4{{line4token}}\n' // 4
+ + 'line5{{else}}\n' // 5
+ + '{{line6Token}}\n' // 6
+ + '{{/blockHelperOnLine3}}\n' // 7
+ + '{{#open}}\n' // 8
+ + '{{else inverse}}\n' // 9
+ + '{{else}}\n' // 10
+ + '{{/open}}'); // 11
body = ast.body;
it('gets ContentNode line numbers', function() {
@@ -155,14 +101,23 @@ describe('ast', function() {
var blockHelperNode = body[5],
program = blockHelperNode.program;
- testColumns(program, 3, 5, 8, 5);
+ testColumns(program, 3, 5, 31, 5);
});
it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
- testColumns(inverse, 5, 7, 5, 0);
+ testColumns(inverse, 5, 7, 13, 0);
+ });
+
+ it('correctly records the line number of chained inverses', function() {
+ var chainInverseNode = body[7];
+
+ testColumns(chainInverseNode.program, 8, 9, 9, 0);
+ testColumns(chainInverseNode.inverse, 9, 10, 16, 0);
+ testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0);
+ testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
});
});
diff --git a/spec/basic.js b/spec/basic.js
index f9b781b4a..8859545d9 100644
--- a/spec/basic.js
+++ b/spec/basic.js
@@ -22,6 +22,10 @@ describe('basic context', function() {
'It works if all the required keys are provided');
});
+ it('compiling with a string context', function() {
+ shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3');
+ });
+
it('compiling with an undefined context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
@@ -203,8 +207,12 @@ describe('basic context', function() {
});
it('literal references', function() {
- shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'},
- 'Goodbye beautiful world!', 'Literal paths can be used');
+ shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
+ shouldCompileTo('Goodbye {{"foo bar"}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
+ shouldCompileTo("Goodbye {{'foo bar'}} world!", {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
+ shouldCompileTo('Goodbye {{"foo[bar"}} world!', {'foo[bar': 'beautiful'}, 'Goodbye beautiful world!');
+ shouldCompileTo('Goodbye {{"foo\'bar"}} world!', {"foo'bar": 'beautiful'}, 'Goodbye beautiful world!');
+ shouldCompileTo("Goodbye {{'foo\"bar'}} world!", {'foo"bar': 'beautiful'}, 'Goodbye beautiful world!');
});
it("that current context path ({{.}}) doesn't hit helpers", function() {
diff --git a/spec/blocks.js b/spec/blocks.js
index 80f1580e6..2fbaee7cc 100644
--- a/spec/blocks.js
+++ b/spec/blocks.js
@@ -65,6 +65,13 @@ describe('blocks', function() {
shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!');
});
+ it('works with cached blocks', function() {
+ var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false});
+
+ var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]});
+ equals(result, 'Alan JohnsonAlan Johnson');
+ });
+
describe('inverted sections', function() {
it('inverted sections with unset value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
@@ -118,6 +125,16 @@ describe('blocks', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
});
+ it('block standalone else sections can be disabled', function() {
+ shouldCompileTo(
+ '{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n',
+ [{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
+ '\nNo people\n\n');
+ shouldCompileTo(
+ '{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n',
+ [{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
+ '\nNo people\n\n');
+ });
it('block standalone chained else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
@@ -149,4 +166,182 @@ describe('blocks', function() {
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel ');
});
});
+
+ describe('decorators', function() {
+ it('should apply mustache decorators', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn) {
+ fn.run = 'success';
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{*decorator}}{{/helper}}',
+ {hash: {}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+ it('should apply allow undefined return', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn() + options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn) {
+ fn.run = 'cess';
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{*decorator}}suc{{/helper}}',
+ {hash: {}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+
+ it('should apply block decorators', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ fn.run = options.fn();
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}',
+ {hash: {}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+ it('should support nested decorators', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ fn.run = options.fn.nested + options.fn();
+ return fn;
+ },
+ nested: function(fn, props, container, options) {
+ props.nested = options.fn();
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}',
+ {hash: {}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+
+ it('should apply multiple decorators', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ fn.run = (fn.run || '') + options.fn();
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}',
+ {hash: {}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+
+ it('should access parent variables', function() {
+ var helpers = {
+ helper: function(options) {
+ return options.fn.run;
+ }
+ };
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ fn.run = options.args;
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{#helper}}{{*decorator foo}}{{/helper}}',
+ {hash: {'foo': 'success'}, helpers: helpers, decorators: decorators},
+ 'success');
+ });
+ it('should work with root program', function() {
+ var run;
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ equals(options.args[0], 'success');
+ run = true;
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{*decorator "success"}}',
+ {hash: {'foo': 'success'}, decorators: decorators},
+ '');
+ equals(run, true);
+ });
+ it('should fail when accessing variables from root', function() {
+ var run;
+ var decorators = {
+ decorator: function(fn, props, container, options) {
+ equals(options.args[0], undefined);
+ run = true;
+ return fn;
+ }
+ };
+ shouldCompileTo(
+ '{{*decorator foo}}',
+ {hash: {'foo': 'fail'}, decorators: decorators},
+ '');
+ equals(run, true);
+ });
+
+ describe('registration', function() {
+ it('unregisters', function() {
+ handlebarsEnv.decorators = {};
+
+ handlebarsEnv.registerDecorator('foo', function() {
+ return 'fail';
+ });
+
+ equals(!!handlebarsEnv.decorators.foo, true);
+ handlebarsEnv.unregisterDecorator('foo');
+ equals(handlebarsEnv.decorators.foo, undefined);
+ });
+
+ it('allows multiple globals', function() {
+ handlebarsEnv.decorators = {};
+
+ handlebarsEnv.registerDecorator({
+ foo: function() {},
+ bar: function() {}
+ });
+
+ equals(!!handlebarsEnv.decorators.foo, true);
+ equals(!!handlebarsEnv.decorators.bar, true);
+ handlebarsEnv.unregisterDecorator('foo');
+ handlebarsEnv.unregisterDecorator('bar');
+ equals(handlebarsEnv.decorators.foo, undefined);
+ equals(handlebarsEnv.decorators.bar, undefined);
+ });
+ it('fails with multiple and args', function() {
+ shouldThrow(function() {
+ handlebarsEnv.registerDecorator({
+ world: function() { return 'world!'; },
+ testHelper: function() { return 'found it!'; }
+ }, {});
+ }, Error, 'Arg not supported with multiple decorators');
+ });
+ });
+ });
});
diff --git a/spec/builtins.js b/spec/builtins.js
index 46d70baac..f06a1ad23 100644
--- a/spec/builtins.js
+++ b/spec/builtins.js
@@ -32,6 +32,11 @@ describe('builtin helpers', function() {
shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns undefined');
});
+
+ it('should not change the depth list', function() {
+ var string = '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}';
+ shouldCompileTo(string, {foo: {goodbye: true}, world: 'world'}, 'GOODBYE cruel world!');
+ });
});
describe('#with', function() {
@@ -47,6 +52,16 @@ describe('builtin helpers', function() {
var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}';
shouldCompileTo(string, {}, 'Person is not present');
});
+ it('with provides block parameter', function() {
+ var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}';
+ shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
+ });
+ it('works when data is disabled', function() {
+ var template = CompilerContext.compile('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}', {data: false});
+
+ var result = template({person: {first: 'Alan', last: 'Johnson'}});
+ equals(result, 'Alan Johnson');
+ });
});
describe('#each', function() {
@@ -210,6 +225,16 @@ describe('builtin helpers', function() {
'each with array function argument ignores the contents when empty');
});
+ it('each object when last key is an empty string', function() {
+ var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
+ var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, '': {text: 'GOODBYE'}}, world: 'world'};
+
+ var template = CompilerContext.compile(string);
+ var result = template(hash);
+
+ equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'Empty string key is not skipped');
+ });
+
it('data passed to helpers', function() {
var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}';
var hash = {letters: ['a', 'b', 'c']};
@@ -276,7 +301,7 @@ describe('builtin helpers', function() {
};
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
- equals(3, levelArg);
+ equals('03', levelArg);
equals('whee', logArg);
});
it('should output to info', function() {
@@ -311,11 +336,77 @@ describe('builtin helpers', function() {
});
it('should handle missing logger', function() {
var string = '{{log blah}}';
- var hash = { blah: 'whee' };
+ var hash = { blah: 'whee' },
+ called = false;
console.error = undefined;
+ console.log = function(log) {
+ equals('whee', log);
+ called = true;
+ };
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
+ equals(true, called);
+ });
+
+ it('should handle string log levels', function() {
+ var string = '{{log blah}}';
+ var hash = { blah: 'whee' };
+ var called;
+
+ console.error = function(log) {
+ equals('whee', log);
+ called = true;
+ };
+
+ shouldCompileTo(string, [hash,,,, {level: 'error'}], '');
+ equals(true, called);
+
+ called = false;
+
+ shouldCompileTo(string, [hash,,,, {level: 'ERROR'}], '');
+ equals(true, called);
+ });
+ it('should handle hash log levels', function() {
+ var string = '{{log blah level="error"}}';
+ var hash = { blah: 'whee' };
+ var called;
+
+ console.error = function(log) {
+ equals('whee', log);
+ called = true;
+ };
+
+ shouldCompileTo(string, hash, '');
+ equals(true, called);
+ });
+ it('should handle hash log levels', function() {
+ var string = '{{log blah level="debug"}}';
+ var hash = { blah: 'whee' };
+ var called = false;
+
+ console.info = console.log = console.error = console.debug = function(log) {
+ equals('whee', log);
+ called = true;
+ };
+
+ shouldCompileTo(string, hash, '');
+ equals(false, called);
+ });
+ it('should pass multiple log arguments', function() {
+ var string = '{{log blah "foo" 1}}';
+ var hash = { blah: 'whee' };
+ var called;
+
+ console.info = console.log = function(log1, log2, log3) {
+ equals('whee', log1);
+ equals('foo', log2);
+ equals(1, log3);
+ called = true;
+ };
+
+ shouldCompileTo(string, hash, '');
+ equals(true, called);
});
/*eslint-enable no-console */
});
diff --git a/spec/compiler.js b/spec/compiler.js
index fe4b63a30..be1fb007d 100644
--- a/spec/compiler.js
+++ b/spec/compiler.js
@@ -39,7 +39,10 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
- equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')], null, {}))(), 'Hello');
+ equal(Handlebars.compile({
+ type: 'Program',
+ body: [ {type: 'ContentStatement', value: 'Hello'}]
+ })(), 'Hello');
});
it('can pass through an empty string', function() {
@@ -58,7 +61,10 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
- equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')]), null, {})), true);
+ equal(/return "Hello"/.test(Handlebars.precompile({
+ type: 'Program',
+ body: [ {type: 'ContentStatement', value: 'Hello'}]
+ })), true);
});
it('can pass through an empty string', function() {
diff --git a/spec/env/common.js b/spec/env/common.js
index e4c8ae537..111294c68 100644
--- a/spec/env/common.js
+++ b/spec/env/common.js
@@ -1,11 +1,27 @@
+var AssertError;
+if (Error.captureStackTrace) {
+ AssertError = function AssertError(message, caller) {
+ Error.prototype.constructor.call(this, message);
+ this.message = message;
+
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, caller || AssertError);
+ }
+ };
+
+ AssertError.prototype = new Error();
+} else {
+ AssertError = Error;
+}
+
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
-global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
+global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
- throw new Error("'" + result + "' should === '" + expected + "': " + message);
+ throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials);
}
};
@@ -13,7 +29,10 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
var template,
ary,
options;
- if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
+ if (hashOrArray && hashOrArray.hash) {
+ ary = [hashOrArray.hash, hashOrArray];
+ delete hashOrArray.hash;
+ } else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
@@ -31,9 +50,9 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
};
-global.equals = global.equal = function(a, b, msg) {
+global.equals = global.equal = function equals(a, b, msg) {
if (a !== b) {
- throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''));
+ throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals);
}
};
@@ -44,13 +63,13 @@ global.shouldThrow = function(callback, type, msg) {
failed = true;
} catch (err) {
if (type && !(err instanceof type)) {
- throw new Error('Type failure: ' + err);
+ throw new AssertError('Type failure: ' + err);
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
equal(msg, err.message);
}
}
if (failed) {
- throw new Error('It failed to throw');
+ throw new AssertError('It failed to throw', shouldThrow);
}
};
diff --git a/spec/env/runner.js b/spec/env/runner.js
index 56fc8d443..98d2482e8 100644
--- a/spec/env/runner.js
+++ b/spec/env/runner.js
@@ -13,9 +13,9 @@ var files = fs.readdirSync(testDir)
.filter(function(name) { return (/.*\.js$/).test(name); })
.map(function(name) { return testDir + '/' + name; });
-run('./node', function() {
+run('./runtime', function() {
run('./browser', function() {
- run('./runtime', function() {
+ run('./node', function() {
/*eslint-disable no-process-exit */
process.exit(errors);
/*eslint-enable no-process-exit */
diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js
index 852733b2f..1cf1eabc9 100644
--- a/spec/expected/empty.amd.js
+++ b/spec/expected/empty.amd.js
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
+return templates['empty'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
diff --git a/spec/helpers.js b/spec/helpers.js
index 54ef0f288..94e503f12 100644
--- a/spec/helpers.js
+++ b/spec/helpers.js
@@ -28,6 +28,29 @@ describe('helpers', function() {
'raw block helper gets raw content');
});
+ it('helper for nested raw block gets raw content', function() {
+ var string = '{{{{a}}}} {{{{b}}}} {{{{/b}}}} {{{{/a}}}}';
+ var helpers = {
+ a: function(options) {
+ return options.fn();
+ }
+ };
+ shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} ', 'raw block helper should get nested raw block as raw content');
+ });
+
+ it('helper block with identical context', function() {
+ var string = '{{#goodbyes}}{{name}}{{/goodbyes}}';
+ var hash = {name: 'Alan'};
+ var helpers = {goodbyes: function(options) {
+ var out = '';
+ var byes = ['Goodbye', 'goodbye', 'GOODBYE'];
+ for (var i = 0, j = byes.length; i < j; i++) {
+ out += byes[i] + ' ' + options.fn(this) + '! ';
+ }
+ return out;
+ }};
+ shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
+ });
it('helper block with complex lookup expression', function() {
var string = '{{#goodbyes}}{{../name}}{{/goodbyes}}';
var hash = {name: 'Alan'};
@@ -35,7 +58,7 @@ describe('helpers', function() {
var out = '';
var byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (var i = 0, j = byes.length; i < j; i++) {
- out += byes[i] + ' ' + options.fn(this) + '! ';
+ out += byes[i] + ' ' + options.fn({}) + '! ';
}
return out;
}};
diff --git a/spec/parser.js b/spec/parser.js
index c37887414..3b7e3e45f 100644
--- a/spec/parser.js
+++ b/spec/parser.js
@@ -65,7 +65,7 @@ describe('parser', function() {
equals(astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n');
});
- it('parses mutaches with DATA parameters', function() {
+ it('parses mustaches with DATA parameters', function() {
equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
});
@@ -113,6 +113,18 @@ describe('parser', function() {
equals(astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n');
});
+ it('parsers partial blocks', function() {
+ equals(astFor('{{#> foo}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n');
+ });
+ it('should handle parser block mismatch', function() {
+ shouldThrow(function() {
+ astFor('{{#> goodbyes}}{{/hellos}}');
+ }, Error, (/goodbyes doesn't match hellos/));
+ });
+ it('parsers partial blocks with arguments', function() {
+ equals(astFor('{{#> foo context hash=value}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n');
+ });
+
it('parses a comment', function() {
equals(astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n");
});
@@ -228,7 +240,48 @@ describe('parser', function() {
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
- equals(astFor(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement('Hello')], null)), 'CONTENT[ \'Hello\' ]\n');
+ equals(astFor({
+ type: 'Program',
+ body: [ {type: 'ContentStatement', value: 'Hello'}]
+ }), 'CONTENT[ \'Hello\' ]\n');
+ });
+ });
+
+ describe('directives', function() {
+ it('should parse block directives', function() {
+ equals(astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n');
+ });
+ it('should parse directives', function() {
+ equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
});
+ it('should fail if directives have inverse', function() {
+ shouldThrow(function() {
+ astFor('{{#* foo}}{{^}}{{/foo}}');
+ }, Error, /Unexpected inverse/);
+ });
+ });
+
+ it('GH1024 - should track program location properly', function() {
+ var p = Handlebars.parse('\n'
+ + ' {{#if foo}}\n'
+ + ' {{bar}}\n'
+ + ' {{else}} {{baz}}\n'
+ + '\n'
+ + ' {{/if}}\n'
+ + ' ');
+
+ // We really need a deep equals but for now this should be stable...
+ equals(JSON.stringify(p.loc), JSON.stringify({
+ start: { line: 1, column: 0 },
+ end: { line: 7, column: 4 }
+ }));
+ equals(JSON.stringify(p.body[1].program.loc), JSON.stringify({
+ start: { line: 2, column: 13 },
+ end: { line: 4, column: 7 }
+ }));
+ equals(JSON.stringify(p.body[1].inverse.loc), JSON.stringify({
+ start: { line: 4, column: 15 },
+ end: { line: 6, column: 5 }
+ }));
});
});
diff --git a/spec/partials.js b/spec/partials.js
index 22d2dc959..cc2c266e7 100644
--- a/spec/partials.js
+++ b/spec/partials.js
@@ -41,6 +41,28 @@ describe('partials', function() {
'Partials can be passed a context');
});
+ it('partials with no context', function() {
+ var partial = '{{name}} ({{url}}) ';
+ var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+ shouldCompileToWithPartials(
+ 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}',
+ [hash, {}, {dude: partial}, {explicitPartialContext: true}],
+ true,
+ 'Dudes: () () ');
+ shouldCompileToWithPartials(
+ 'Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}',
+ [hash, {}, {dude: partial}, {explicitPartialContext: true}],
+ true,
+ 'Dudes: foo () foo () ');
+ });
+
+ it('partials with string context', function() {
+ var string = 'Dudes: {{>dude "dudes"}}';
+ var partial = '{{.}}';
+ var hash = {};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: dudes');
+ });
+
it('partials with undefined context', function() {
var string = 'Dudes: {{>dude dudes}}';
var partial = '{{foo}} Empty';
@@ -189,6 +211,106 @@ describe('partials', function() {
handlebarsEnv.compile = compile;
});
+ describe('partial blocks', function() {
+ it('should render partial block as default', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}success{{/dude}}',
+ [{}, {}, {}],
+ true,
+ 'success');
+ });
+ it('should execute default block with proper context', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude context}}{{value}}{{/dude}}',
+ [{context: {value: 'success'}}, {}, {}],
+ true,
+ 'success');
+ });
+ it('should propagate block parameters to default block', function() {
+ shouldCompileToWithPartials(
+ '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}',
+ [{context: {value: 'success'}}, {}, {}],
+ true,
+ 'success');
+ });
+
+ it('should not use partial block if partial exists', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}fail{{/dude}}',
+ [{}, {}, {dude: 'success'}],
+ true,
+ 'success');
+ });
+
+ it('should render block from partial', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}success{{/dude}}',
+ [{}, {}, {dude: '{{> @partial-block }}'}],
+ true,
+ 'success');
+ });
+ it('should render block from partial with context', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}{{value}}{{/dude}}',
+ [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}],
+ true,
+ 'success');
+ });
+ it('should render block from partial with context', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}{{../context/value}}{{/dude}}',
+ [{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}],
+ true,
+ 'success');
+ });
+ it('should render block from partial with block params', function() {
+ shouldCompileToWithPartials(
+ '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}',
+ [{context: {value: 'success'}}, {}, {dude: '{{> @partial-block }}'}],
+ true,
+ 'success');
+ });
+ });
+
+ describe('inline partials', function() {
+ it('should define inline partials for template', function() {
+ shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success');
+ });
+ it('should overwrite multiple partials in the same template', function() {
+ shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success');
+ });
+ it('should define inline partials for block', function() {
+ shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success');
+ shouldThrow(function() {
+ shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}', {}, 'success');
+ }, Error, /myPartial could not/);
+ });
+ it('should override global partials', function() {
+ shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {hash: {}, partials: {myPartial: function() { return 'fail'; }}}, 'success');
+ });
+ it('should override template partials', function() {
+ shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success');
+ });
+ it('should override partials down the entire stack', function() {
+ shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}', {}, 'success');
+ });
+
+ it('should define inline partials for partial call', function() {
+ shouldCompileToWithPartials(
+ '{{#*inline "myPartial"}}success{{/inline}}{{> dude}}',
+ [{}, {}, {dude: '{{> myPartial }}'}],
+ true,
+ 'success');
+ });
+ it('should define inline partials in partial block call', function() {
+ shouldCompileToWithPartials(
+ '{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}',
+ [{}, {}, {dude: '{{> myPartial }}'}],
+ true,
+ 'success');
+ });
+ });
+
it('should pass compiler flags', function() {
if (Handlebars.compile) {
var env = Handlebars.create();
@@ -231,6 +353,12 @@ describe('partials', function() {
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
+ it('partials can access parents with custom context', function() {
+ var string = 'Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}';
+ var partial = '{{name}} ({{url}}) {{root}} ';
+ var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+ shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
+ });
it('partials can access parents without data', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} ';
diff --git a/spec/precompiler.js b/spec/precompiler.js
index 21e25b06e..e1ad5ade9 100644
--- a/spec/precompiler.js
+++ b/spec/precompiler.js
@@ -16,6 +16,12 @@ describe('precompiler', function() {
precompile,
minify,
+ emptyTemplate = {
+ path: __dirname + '/artifacts/empty.handlebars',
+ name: 'empty',
+ source: ''
+ },
+
file,
content,
writeFileSync;
@@ -51,10 +57,9 @@ describe('precompiler', function() {
Precompiler.cli({templates: []});
}, Handlebars.Exception, 'Must define at least one template or directory.');
});
- it('should throw on missing template', function() {
- shouldThrow(function() {
- Precompiler.cli({templates: ['foo']});
- }, Handlebars.Exception, 'Unable to open template file "foo"');
+ it('should handle empty/filtered directories', function() {
+ Precompiler.cli({hasDirectory: true, templates: []});
+ // Success is not throwing
});
it('should throw when combining simple and minimized', function() {
shouldThrow(function() {
@@ -66,107 +71,183 @@ describe('precompiler', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
+ it('should throw when missing name', function() {
+ shouldThrow(function() {
+ Precompiler.cli({templates: [{source: ''}], amd: true});
+ }, Handlebars.Exception, 'Name missing for template');
+ });
it('should throw when combining simple and directories', function() {
shouldThrow(function() {
- Precompiler.cli({templates: [__dirname], simple: true});
+ Precompiler.cli({hasDirectory: true, templates: [1], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
- it('should enumerate directories by extension', function() {
- Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'});
- equal(/'example_2'/.test(log), true);
- log = '';
- Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'});
- equal(/'empty'/.test(log), true);
- equal(/'example_1'/.test(log), true);
- });
- it('should protect from regexp patterns', function() {
- Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'});
- // Success is not throwing
- });
it('should output simple templates', function() {
Handlebars.precompile = function() { return 'simple'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], simple: true});
+ equal(log, 'simple\n');
+ });
+ it('should default to simple templates', function() {
+ Handlebars.precompile = function() { return 'simple'; };
+ Precompiler.cli({templates: [{source: ''}]});
equal(log, 'simple\n');
});
it('should output amd templates', function() {
Handlebars.precompile = function() { return 'amd'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], amd: true});
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; };
- Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'});
+ Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, namespace: 'foo'});
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
- Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
});
it('should output commonjs templates', function() {
Handlebars.precompile = function() { return 'commonjs'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], commonjs: true});
equal(/template\(commonjs\)/.test(log), true);
});
it('should set data flag', function() {
Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true});
+ Precompiler.cli({templates: [emptyTemplate], simple: true, data: true});
equal(log, 'simple\n');
});
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'});
- equal(log, 'simple\n');
- });
-
- it('should handle different root', function() {
- Handlebars.precompile = function() { return 'simple'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', root: 'foo/'});
+ Precompiler.cli({templates: [emptyTemplate], simple: true, known: 'foo'});
equal(log, 'simple\n');
});
it('should output to file system', function() {
Handlebars.precompile = function() { return 'simple'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'});
+ Precompiler.cli({templates: [emptyTemplate], simple: true, output: 'file!'});
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
});
- it('should handle BOM', function() {
- Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; };
- Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true});
- equal(log, 'simple\n');
- });
it('should output minimized templates', function() {
Handlebars.precompile = function() { return 'amd'; };
uglify.minify = function() { return {code: 'min'}; };
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], min: true});
equal(log, 'min');
});
it('should output map', function() {
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
it('should output map', function() {
- Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'});
+ Precompiler.cli({templates: [emptyTemplate], min: true, map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
+
+ describe('#loadTemplates', function() {
+ it('should throw on missing template', function(done) {
+ Precompiler.loadTemplates({files: ['foo']}, function(err) {
+ equal(err.message, 'Unable to open template file "foo"');
+ done();
+ });
+ });
+ it('should enumerate directories by extension', function(done) {
+ Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hbs'}, function(err, opts) {
+ equal(opts.templates.length, 1);
+ equal(opts.templates[0].name, 'example_2');
+ done(err);
+ });
+ });
+ it('should enumerate all templates by extension', function(done) {
+ Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'handlebars'}, function(err, opts) {
+ equal(opts.templates.length, 3);
+ equal(opts.templates[0].name, 'bom');
+ equal(opts.templates[1].name, 'empty');
+ equal(opts.templates[2].name, 'example_1');
+ done(err);
+ });
+ });
+ it('should handle regular expression characters in extensions', function(done) {
+ Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hb(s'}, function(err) {
+ // Success is not throwing
+ done(err);
+ });
+ });
+ it('should handle BOM', function(done) {
+ var opts = {files: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true};
+ Precompiler.loadTemplates(opts, function(err, opts) {
+ equal(opts.templates[0].source, 'a');
+ done(err);
+ });
+ });
+
+ it('should handle different root', function(done) {
+ var opts = {files: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'};
+ Precompiler.loadTemplates(opts, function(err, opts) {
+ equal(opts.templates[0].name, __dirname + '/artifacts/empty');
+ done(err);
+ });
+ });
+
+ it('should accept string inputs', function(done) {
+ var opts = {string: ''};
+ Precompiler.loadTemplates(opts, function(err, opts) {
+ equal(opts.templates[0].name, undefined);
+ equal(opts.templates[0].source, '');
+ done(err);
+ });
+ });
+ it('should accept string array inputs', function(done) {
+ var opts = {string: ['', 'bar'], name: ['beep', 'boop']};
+ Precompiler.loadTemplates(opts, function(err, opts) {
+ equal(opts.templates[0].name, 'beep');
+ equal(opts.templates[0].source, '');
+ equal(opts.templates[1].name, 'boop');
+ equal(opts.templates[1].source, 'bar');
+ done(err);
+ });
+ });
+ it('should accept stdin input', function(done) {
+ var stdin = require('mock-stdin').stdin();
+ Precompiler.loadTemplates({string: '-'}, function(err, opts) {
+ equal(opts.templates[0].source, 'foo');
+ done(err);
+ });
+ stdin.send('fo');
+ stdin.send('o');
+ stdin.end();
+ });
+ it('error on name missing', function(done) {
+ var opts = {string: ['', 'bar']};
+ Precompiler.loadTemplates(opts, function(err) {
+ equal(err.message, 'Number of names did not match the number of string inputs');
+ done();
+ });
+ });
+
+ it('should complete when no args are passed', function(done) {
+ Precompiler.loadTemplates({}, function(err, opts) {
+ equal(opts.templates.length, 0);
+ done(err);
+ });
+ });
+ });
});
diff --git a/spec/regressions.js b/spec/regressions.js
index 247c1c9b3..e8942a484 100644
--- a/spec/regressions.js
+++ b/spec/regressions.js
@@ -172,4 +172,35 @@ describe('Regressions', function() {
var result = template(context);
equals(result, 'foo');
});
+
+ it('GH-1021: Each empty string key', function() {
+ var data = {
+ '': 'foo',
+ 'name': 'Chris',
+ 'value': 10000
+ };
+
+ shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n');
+ });
+
+ it('GH-1054: Should handle simple safe string responses', function() {
+ var root = '{{#wrap}}{{>partial}}{{/wrap}}';
+ var partials = {
+ partial: '{{#wrap}}
{{/wrap}}'
+ };
+ var helpers = {
+ wrap: function(options) {
+ return new Handlebars.SafeString(options.fn());
+ }
+ };
+
+ shouldCompileToWithPartials(root, [{}, helpers, partials], true, '');
+ });
+
+ it('GH-1065: Sparse arrays', function() {
+ var array = [];
+ array[1] = 'foo';
+ array[3] = 'bar';
+ shouldCompileTo('{{#each array}}{{@index}}{{.}}{{/each}}', {array: array}, '1foo3bar');
+ });
});
diff --git a/spec/runtime.js b/spec/runtime.js
index 502a8436b..a4830ad0c 100644
--- a/spec/runtime.js
+++ b/spec/runtime.js
@@ -14,19 +14,19 @@ describe('runtime', function() {
it('should throw on version mismatch', function() {
shouldThrow(function() {
Handlebars.template({
- main: true,
+ main: {},
compiler: [Handlebars.COMPILER_REVISION + 1]
});
}, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
- main: true,
+ main: {},
compiler: [Handlebars.COMPILER_REVISION - 1]
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
- main: true
+ main: {}
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
});
diff --git a/spec/strict.js b/spec/strict.js
index 2aef13442..05ce35d9e 100644
--- a/spec/strict.js
+++ b/spec/strict.js
@@ -78,6 +78,23 @@ describe('strict', function() {
template({hello: {}});
}, Exception, /"bar" not defined in/);
});
+
+ it('should allow undefined parameters when passed to helpers', function() {
+ var template = CompilerContext.compile('{{#unless foo}}success{{/unless}}', {strict: true});
+ equals(template({}), 'success');
+ });
+
+ it('should allow undefined hash when passed to helpers', function() {
+ var template = CompilerContext.compile('{{helper value=@foo}}', {strict: true});
+ var helpers = {
+ helper: function(options) {
+ equals('value' in options.hash, true);
+ equals(options.hash.value, undefined);
+ return 'success';
+ }
+ };
+ equals(template({}, {helpers: helpers}), 'success');
+ });
});
describe('assume objects', function() {
diff --git a/spec/tokenizer.js b/spec/tokenizer.js
index ad71dc9b2..dc077ce72 100644
--- a/spec/tokenizer.js
+++ b/spec/tokenizer.js
@@ -214,6 +214,10 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
});
+ it('tokenizes partial block declarations', function() {
+ var result = tokenize('{{#> foo}}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL_BLOCK', 'ID', 'CLOSE']);
+ });
it('tokenizes a comment as "COMMENT"', function() {
var result = tokenize('foo {{! this is a comment }} bar {{ baz }}');
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
@@ -237,6 +241,15 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});
+ it('tokenizes directives', function() {
+ shouldMatchTokens(
+ tokenize('{{#*foo}}content{{/foo}}'),
+ ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
+ shouldMatchTokens(
+ tokenize('{{*foo}}'),
+ ['OPEN', 'ID', 'CLOSE']);
+ });
+
it('tokenizes inverse sections as "INVERSE"', function() {
shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']);
shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']);
@@ -264,7 +277,7 @@ describe('Tokenizer', function() {
});
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() {
- var result = tokenize('{{ foo bar \'baz\' }}');
+ var result = tokenize('{{ foo bar \"baz\" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], 'STRING', 'baz');
});
diff --git a/spec/track-ids.js b/spec/track-ids.js
index 7a8b59ee4..30a46617d 100644
--- a/spec/track-ids.js
+++ b/spec/track-ids.js
@@ -47,12 +47,14 @@ describe('track ids', function() {
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar');
});
it('should note ../ and ./ references', function() {
- var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver}}', {trackIds: true});
+ var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver this.is.a this}}', {trackIds: true});
var helpers = {
- wycats: function(passiveVoice, noun, options) {
+ wycats: function(passiveVoice, noun, thiz, thiz2, options) {
equal(options.ids[0], 'is.a');
equal(options.ids[1], '../slave.driver');
+ equal(options.ids[2], 'is.a');
+ equal(options.ids[3], '');
return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
}
@@ -188,4 +190,48 @@ describe('track ids', function() {
});
});
});
+
+ describe('partials', function() {
+ var helpers = {
+ blockParams: function(name, options) {
+ return name + ':' + options.ids[0] + '\n';
+ },
+ wycats: function(name, options) {
+ return name + ':' + options.data.contextPath + '\n';
+ }
+ };
+
+ it('should pass track id for basic partial', function() {
+ var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude}}{{/dudes}}', {trackIds: true}),
+ hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+
+ var partials = {
+ dude: CompilerContext.compile('{{wycats name}}', {trackIds: true})
+ };
+
+ equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes.0\nAlan:dudes.1\n');
+ });
+
+ it('should pass track id for context partial', function() {
+ var template = CompilerContext.compile('Dudes: {{> dude dudes}}', {trackIds: true}),
+ hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+
+ var partials = {
+ dude: CompilerContext.compile('{{#each this}}{{wycats name}}{{/each}}', {trackIds: true})
+ };
+
+ equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:dudes..0\nAlan:dudes..1\n');
+ });
+
+ it('should invalidate context for partials with parameters', function() {
+ var template = CompilerContext.compile('Dudes: {{#dudes}}{{> dude . bar="foo"}}{{/dudes}}', {trackIds: true}),
+ hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
+
+ var partials = {
+ dude: CompilerContext.compile('{{wycats name}}', {trackIds: true})
+ };
+
+ equals(template(hash, {helpers: helpers, partials: partials}), 'Dudes: Yehuda:true\nAlan:true\n');
+ });
+ });
});
diff --git a/spec/utils.js b/spec/utils.js
index 81732c5e7..7248ac447 100644
--- a/spec/utils.js
+++ b/spec/utils.js
@@ -18,6 +18,7 @@ describe('utils', function() {
describe('#escapeExpression', function() {
it('shouhld escape html', function() {
equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>');
+ equals(Handlebars.Utils.escapeExpression('foo='), 'foo=');
});
it('should not escape SafeString', function() {
var string = new Handlebars.SafeString('foo<&"\'>');
diff --git a/spec/visitor.js b/spec/visitor.js
index 1f50d79c1..d3fb795e2 100644
--- a/spec/visitor.js
+++ b/spec/visitor.js
@@ -8,6 +8,9 @@ describe('Visitor', function() {
// stub methods are executed
var visitor = new Handlebars.Visitor();
visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'));
+ visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}'));
+ visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}'));
+ visitor.accept(Handlebars.parse('{{* bar }}'));
});
it('should traverse to stubs', function() {
@@ -40,8 +43,6 @@ describe('Visitor', function() {
visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}'));
});
- it('should return undefined');
-
describe('mutating', function() {
describe('fields', function() {
it('should replace value', function() {
@@ -49,7 +50,7 @@ describe('Visitor', function() {
visitor.mutating = true;
visitor.StringLiteral = function(string) {
- return new Handlebars.AST.NumberLiteral(42, string.locInfo);
+ return {type: 'NumberLiteral', value: 42, loc: string.loc};
};
var ast = Handlebars.parse('{{foo foo="foo"}}');
@@ -109,7 +110,7 @@ describe('Visitor', function() {
visitor.mutating = true;
visitor.StringLiteral = function(string) {
- return new Handlebars.AST.NumberLiteral(42, string.locInfo);
+ return {type: 'NumberLiteral', value: 42, loc: string.locInfo};
};
var ast = Handlebars.parse('{{foo "foo"}}');
diff --git a/src/handlebars.l b/src/handlebars.l
index ff2128355..4c3c30421 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -49,12 +49,21 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
return 'CONTENT';
}
+// nested raw block will create stacked 'raw' condition
+"{{{{"/[^/] this.begin('raw'); return 'CONTENT';
"{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" {
- yytext = yytext.substr(5, yyleng-9);
this.popState();
- return 'END_RAW_BLOCK';
+ // Should be using `this.topState()` below, but it currently
+ // returns the second top instead of the first top. Opened an
+ // issue about it at https://github.com/zaach/jison/issues/291
+ if (this.conditionStack[this.conditionStack.length-1] === 'raw') {
+ return 'CONTENT';
+ } else {
+ yytext = yytext.substr(5, yyleng-9);
+ return 'END_RAW_BLOCK';
+ }
}
-[^\x00]*?/("{{{{/") { return 'CONTENT'; }
+[^\x00]*?/("{{{{") { return 'CONTENT'; }
[\s\S]*?"--"{RIGHT_STRIP}?"}}" {
this.popState();
@@ -71,7 +80,8 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
return 'CLOSE_RAW_BLOCK';
}
"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
-"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
+"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK';
+"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK';
"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
@@ -88,7 +98,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
this.popState();
return 'COMMENT';
}
-"{{"{LEFT_STRIP}? return 'OPEN';
+"{{"{LEFT_STRIP}?"*"? return 'OPEN';
"=" return 'EQUALS';
".." return 'ID';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index d67a7da7e..ce0649838 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -9,7 +9,7 @@ root
;
program
- : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$))
+ : statement* -> yy.prepareProgram($1)
;
statement
@@ -17,16 +17,29 @@ statement
| block -> $1
| rawBlock -> $1
| partial -> $1
+ | partialBlock -> $1
| content -> $1
- | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$))
- ;
+ | COMMENT {
+ $$ = {
+ type: 'CommentStatement',
+ value: yy.stripComment($1),
+ strip: yy.stripFlags($1, $1),
+ loc: yy.locInfo(@$)
+ };
+ };
content
- : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$))
- ;
+ : CONTENT {
+ $$ = {
+ type: 'ContentStatement',
+ original: $1,
+ value: $1,
+ loc: yy.locInfo(@$)
+ };
+ };
rawBlock
- : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$)
+ : openRawBlock content+ END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$)
;
openRawBlock
@@ -39,7 +52,7 @@ block
;
openBlock
- : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
+ : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
openInverse
@@ -57,7 +70,7 @@ inverseAndProgram
inverseChain
: openInverseChain program inverseChain? {
var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$),
- program = new yy.Program([inverse], null, {}, yy.locInfo(@$));
+ program = yy.prepareProgram([inverse], $2.loc);
program.chained = true;
$$ = { strip: $1.strip, program: program, chain: true };
@@ -77,7 +90,23 @@ mustache
;
partial
- : OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$))
+ : OPEN_PARTIAL partialName param* hash? CLOSE {
+ $$ = {
+ type: 'PartialStatement',
+ name: $2,
+ params: $3,
+ hash: $4,
+ indent: '',
+ strip: yy.stripFlags($1, $5),
+ loc: yy.locInfo(@$)
+ };
+ }
+ ;
+partialBlock
+ : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$)
+ ;
+openPartialBlock
+ : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) }
;
param
@@ -86,15 +115,22 @@ param
;
sexpr
- : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$))
- ;
+ : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR {
+ $$ = {
+ type: 'SubExpression',
+ path: $2,
+ params: $3,
+ hash: $4,
+ loc: yy.locInfo(@$)
+ };
+ };
hash
- : hashSegment+ -> new yy.Hash($1, yy.locInfo(@$))
+ : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)}
;
hashSegment
- : ID EQUALS param -> new yy.HashPair(yy.id($1), $3, yy.locInfo(@$))
+ : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)}
;
blockParams
@@ -104,11 +140,11 @@ blockParams
helperName
: path -> $1
| dataName -> $1
- | STRING -> new yy.StringLiteral($1, yy.locInfo(@$))
- | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$))
- | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$))
- | UNDEFINED -> new yy.UndefinedLiteral(yy.locInfo(@$))
- | NULL -> new yy.NullLiteral(yy.locInfo(@$))
+ | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)}
+ | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)}
+ | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)}
+ | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)}
+ | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)}
;
partialName
diff --git a/src/parser-suffix.js b/src/parser-suffix.js
index 6e4aa20d6..1f69f7a44 100644
--- a/src/parser-suffix.js
+++ b/src/parser-suffix.js
@@ -1 +1,2 @@
-export default handlebars;
+exports.__esModule = true;
+exports['default'] = handlebars;
diff --git a/tasks/.eslintrc b/tasks/.eslintrc
new file mode 100644
index 000000000..346150294
--- /dev/null
+++ b/tasks/.eslintrc
@@ -0,0 +1,16 @@
+{
+ "globals": {
+ "require": true
+ },
+ "rules": {
+ // Disabling for tests, for now.
+ "no-path-concat": 0,
+
+ "no-var": 0,
+ "no-shadow": 0,
+ "handle-callback-err": 0,
+ "no-console": 0,
+ "no-process-env": 0,
+ "dot-notation": [2, {"allowKeywords": true}]
+ }
+}
\ No newline at end of file
diff --git a/tasks/metrics.js b/tasks/metrics.js
index c4a202b15..9044306cf 100644
--- a/tasks/metrics.js
+++ b/tasks/metrics.js
@@ -36,7 +36,7 @@ module.exports = function(grunt) {
return done();
}
- emit(keen, events, function(err, res) {
+ emit(keen, events, function(err) {
if (err) {
throw err;
}
diff --git a/tasks/parser.js b/tasks/parser.js
index 47533cedf..7ff725850 100644
--- a/tasks/parser.js
+++ b/tasks/parser.js
@@ -6,7 +6,7 @@ module.exports = function(grunt) {
var cmd = './node_modules/.bin/jison';
- if(process.platform === 'win32'){
+ if (process.platform === 'win32') {
cmd = 'node_modules\\.bin\\jison.cmd';
}
diff --git a/tasks/publish.js b/tasks/publish.js
index 68b3157aa..55ea20a71 100644
--- a/tasks/publish.js
+++ b/tasks/publish.js
@@ -60,7 +60,7 @@ module.exports = function(grunt) {
async.forEach(_.keys(files), function(file, callback) {
var params = {Bucket: bucket, Key: file, Body: grunt.file.read(files[file])};
- s3.putObject(params, function(err, data) {
+ s3.putObject(params, function(err) {
if (err) {
throw err;
} else {
diff --git a/tasks/test.js b/tasks/test.js
index ad8a911d3..74473244a 100644
--- a/tasks/test.js
+++ b/tasks/test.js
@@ -40,5 +40,17 @@ module.exports = function(grunt) {
done();
});
});
- grunt.registerTask('test', ['test:bin', 'test:cov']);
+
+ grunt.registerTask('test:check-cov', function() {
+ var done = this.async();
+
+ var runner = childProcess.fork('node_modules/.bin/istanbul', ['check-coverage', '--statements', '100', '--functions', '100', '--branches', '100', '--lines 100'], {stdio: 'inherit'});
+ runner.on('close', function(code) {
+ if (code != 0) {
+ grunt.fatal('Coverage check failed: ' + code);
+ }
+ done();
+ });
+ });
+ grunt.registerTask('test', ['test:bin', 'test:cov', 'test:check-cov']);
};
diff --git a/tasks/util/git.js b/tasks/util/git.js
index a6c9ec1dc..03802630d 100644
--- a/tasks/util/git.js
+++ b/tasks/util/git.js
@@ -50,7 +50,7 @@ module.exports = {
childProcess.exec('git rev-parse --short origin/master', {}, function(err, stdout) {
// This will error if master was not checked out but in this case we know we are not master
// so we can ignore.
- if (err && !/Needed a single revision/.test(err.message)) {
+ if (err && !(/Needed a single revision/.test(err.message))) {
throw new Error('git.master: ' + err.message);
}
@@ -59,7 +59,7 @@ module.exports = {
},
add: function(path, callback) {
- childProcess.exec('git add -f ' + path, {}, function(err, stdout) {
+ childProcess.exec('git add -f ' + path, {}, function(err) {
if (err) {
throw new Error('git.add: ' + err.message);
}
@@ -68,7 +68,7 @@ module.exports = {
});
},
commit: function(name, callback) {
- childProcess.exec('git commit --message=' + name, {}, function(err, stdout) {
+ childProcess.exec('git commit --message=' + name, {}, function(err) {
if (err) {
throw new Error('git.commit: ' + err.message);
}
@@ -77,7 +77,7 @@ module.exports = {
});
},
tag: function(name, callback) {
- childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err, stdout, stderr) {
+ childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err) {
if (err) {
throw new Error('git.tag: ' + err.message);
}
@@ -98,7 +98,7 @@ module.exports = {
});
var versionTags = tags.filter(function(info) {
- return /^v/.test(info[0]);
+ return (/^v/.test(info[0]));
});
callback(undefined, versionTags[0] || tags[0]);
diff --git a/tasks/version.js b/tasks/version.js
index e6bfe5943..8bc4d8250 100644
--- a/tasks/version.js
+++ b/tasks/version.js
@@ -18,9 +18,9 @@ module.exports = function(grunt) {
grunt.log.writeln('Updating to version ' + version);
async.each([
- ['lib/handlebars/base.js', /var VERSION = ['"](.*)['"];/, 'var VERSION = "' + version + '";'],
- ['components/bower.json', /"version":.*/, '"version": "' + version + '",'],
- ['components/handlebars.js.nuspec', /.*<\/version>/, '' + version + '']
+ ['lib/handlebars/base.js', (/const VERSION = ['"](.*)['"];/), 'const VERSION = \'' + version + '\';'],
+ ['components/bower.json', (/"version":.*/), '"version": "' + version + '",'],
+ ['components/handlebars.js.nuspec', (/.*<\/version>/), '' + version + '']
],
function(args, callback) {
replace.apply(undefined, args);
@@ -33,9 +33,9 @@ module.exports = function(grunt) {
});
});
- function replace(path, regex, replace) {
+ function replace(path, regex, value) {
var content = grunt.file.read(path);
- content = content.replace(regex, replace);
+ content = content.replace(regex, value);
grunt.file.write(path, content);
}
};