d03b6ecfc4
Disallow access to prototype properties and methods by default.
Access to properties is always checked via
`Object.prototype.hasOwnProperty.call(parent, propertyName)`.
New runtime options:
- **allowedProtoMethods**: a string-to-boolean map of property-names that are allowed if they are methods of the parent object.
- **allowedProtoProperties**: a string-to-boolean map of property-names that are allowed if they are properties but not methods of the parent object.
```js
const template = handlebars.compile('{{aString.trim}}')
const result = template({ aString: ' abc ' })
// result is empty, because trim is defined at String prototype
```
```js
const template = handlebars.compile('{{aString.trim}}')
const result = template({ aString: ' abc ' }, {
allowedProtoMethods: {
trim: true
}
})
// result = 'abc'
```
Implementation details: The method now "container.lookupProperty"
handles the prototype-checks and the white-lists. It is used in
- JavaScriptCompiler#nameLookup
- The "lookup"-helper (passed to all helpers as "options.lookupProperty")
- The "lookup" function at the container, which is used for recursive lookups in "compat" mode
Compatibility:
- **Old precompiled templates work with new runtimes**: The "options.lookupPropery"-function is passed to the helper by a wrapper, not by the compiled templated.
- **New templates work with old runtimes**: The template contains a function that is used as fallback if the "lookupProperty"-function cannot be found at the container. However, the runtime-options "allowedProtoProperties" and "allowedProtoMethods" only work with the newest runtime.
BREAKING CHANGE:
- access to prototype properties is forbidden completely by default
1343 lines
36 KiB
JavaScript
1343 lines
36 KiB
JavaScript
describe('helpers', function() {
|
|
it('helper with complex lookup$', function() {
|
|
var string = '{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}';
|
|
var hash = {
|
|
prefix: '/root',
|
|
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
|
|
};
|
|
var helpers = {
|
|
link: function(prefix) {
|
|
return (
|
|
'<a href="' + prefix + '/' + this.url + '">' + this.text + '</a>'
|
|
);
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
'<a href="/root/goodbye">Goodbye</a>'
|
|
);
|
|
});
|
|
|
|
it('helper for raw block gets raw content', function() {
|
|
var string = '{{{{raw}}}} {{test}} {{{{/raw}}}}';
|
|
var hash = { test: 'hello' };
|
|
var helpers = {
|
|
raw: function(options) {
|
|
return options.fn();
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
' {{test}} ',
|
|
'raw block helper gets raw content'
|
|
);
|
|
});
|
|
|
|
it('helper for raw block gets parameters', function() {
|
|
var string = '{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}';
|
|
var hash = { test: 'hello' };
|
|
var helpers = {
|
|
raw: function(a, b, c, options) {
|
|
return options.fn() + a + b + c;
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
' {{test}} 123',
|
|
'raw block helper gets raw content'
|
|
);
|
|
});
|
|
|
|
describe('raw block parsing (with identity helper-function)', function() {
|
|
function runWithIdentityHelper(template, expected) {
|
|
var helpers = {
|
|
identity: function(options) {
|
|
return options.fn();
|
|
}
|
|
};
|
|
shouldCompileTo(template, [{}, helpers], expected);
|
|
}
|
|
|
|
it('helper for nested raw block gets raw content', function() {
|
|
runWithIdentityHelper(
|
|
'{{{{identity}}}} {{{{b}}}} {{{{/b}}}} {{{{/identity}}}}',
|
|
' {{{{b}}}} {{{{/b}}}} '
|
|
);
|
|
});
|
|
|
|
it('helper for nested raw block works with empty content', function() {
|
|
runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', '');
|
|
});
|
|
|
|
xit('helper for nested raw block works if nested raw blocks are broken', function() {
|
|
// This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release
|
|
// The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3
|
|
// If anyone can make this template work without breaking everything else, then go for it,
|
|
// but for now, this is just a known bug, that will be documented.
|
|
runWithIdentityHelper(
|
|
'{{{{identity}}}} {{{{a}}}} {{{{ {{{{/ }}}} }}}} {{{{/identity}}}}',
|
|
' {{{{a}}}} {{{{ {{{{/ }}}} }}}} '
|
|
);
|
|
});
|
|
|
|
it('helper for nested raw block closes after first matching close', function() {
|
|
runWithIdentityHelper(
|
|
'{{{{identity}}}}abc{{{{/identity}}}} {{{{identity}}}}abc{{{{/identity}}}}',
|
|
'abc abc'
|
|
);
|
|
});
|
|
|
|
it('helper for nested raw block throw exception when with missing closing braces', function() {
|
|
var string = '{{{{a}}}} {{{{/a';
|
|
shouldThrow(function() {
|
|
Handlebars.compile(string)();
|
|
});
|
|
});
|
|
});
|
|
|
|
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' };
|
|
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({}) + '! ';
|
|
}
|
|
return out;
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
'Goodbye Alan! goodbye Alan! GOODBYE Alan! '
|
|
);
|
|
});
|
|
|
|
it('helper with complex lookup and nested template', function() {
|
|
var string =
|
|
'{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
|
|
var hash = {
|
|
prefix: '/root',
|
|
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
|
|
};
|
|
var helpers = {
|
|
link: function(prefix, options) {
|
|
return (
|
|
'<a href="' +
|
|
prefix +
|
|
'/' +
|
|
this.url +
|
|
'">' +
|
|
options.fn(this) +
|
|
'</a>'
|
|
);
|
|
}
|
|
};
|
|
shouldCompileToWithPartials(
|
|
string,
|
|
[hash, helpers],
|
|
false,
|
|
'<a href="/root/goodbye">Goodbye</a>'
|
|
);
|
|
});
|
|
|
|
it('helper with complex lookup and nested template in VM+Compiler', function() {
|
|
var string =
|
|
'{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
|
|
var hash = {
|
|
prefix: '/root',
|
|
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
|
|
};
|
|
var helpers = {
|
|
link: function(prefix, options) {
|
|
return (
|
|
'<a href="' +
|
|
prefix +
|
|
'/' +
|
|
this.url +
|
|
'">' +
|
|
options.fn(this) +
|
|
'</a>'
|
|
);
|
|
}
|
|
};
|
|
shouldCompileToWithPartials(
|
|
string,
|
|
[hash, helpers],
|
|
true,
|
|
'<a href="/root/goodbye">Goodbye</a>'
|
|
);
|
|
});
|
|
it('helper returning undefined value', function() {
|
|
shouldCompileTo(' {{nothere}}', [{}, { nothere: function() {} }], ' ');
|
|
shouldCompileTo(
|
|
' {{#nothere}}{{/nothere}}',
|
|
[{}, { nothere: function() {} }],
|
|
' '
|
|
);
|
|
});
|
|
|
|
it('block helper', function() {
|
|
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
|
|
var template = CompilerContext.compile(string);
|
|
|
|
var result = template(
|
|
{ world: 'world' },
|
|
{
|
|
helpers: {
|
|
goodbyes: function(options) {
|
|
return options.fn({ text: 'GOODBYE' });
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(result, 'GOODBYE! cruel world!', 'Block helper executed');
|
|
});
|
|
|
|
it('block helper staying in the same context', function() {
|
|
var string = '{{#form}}<p>{{name}}</p>{{/form}}';
|
|
var template = CompilerContext.compile(string);
|
|
|
|
var result = template(
|
|
{ name: 'Yehuda' },
|
|
{
|
|
helpers: {
|
|
form: function(options) {
|
|
return '<form>' + options.fn(this) + '</form>';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(
|
|
result,
|
|
'<form><p>Yehuda</p></form>',
|
|
'Block helper executed with current context'
|
|
);
|
|
});
|
|
|
|
it('block helper should have context in this', function() {
|
|
var source =
|
|
'<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>';
|
|
function link(options) {
|
|
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
|
|
}
|
|
var data = {
|
|
people: [
|
|
{ name: 'Alan', id: 1 },
|
|
{ name: 'Yehuda', id: 2 }
|
|
]
|
|
};
|
|
|
|
shouldCompileTo(
|
|
source,
|
|
[data, { link: link }],
|
|
'<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>'
|
|
);
|
|
});
|
|
|
|
it('block helper for undefined value', function() {
|
|
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, '');
|
|
});
|
|
|
|
it('block helper passing a new context', function() {
|
|
var string = '{{#form yehuda}}<p>{{name}}</p>{{/form}}';
|
|
var template = CompilerContext.compile(string);
|
|
|
|
var result = template(
|
|
{ yehuda: { name: 'Yehuda' } },
|
|
{
|
|
helpers: {
|
|
form: function(context, options) {
|
|
return '<form>' + options.fn(context) + '</form>';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(result, '<form><p>Yehuda</p></form>', 'Context variable resolved');
|
|
});
|
|
|
|
it('block helper passing a complex path context', function() {
|
|
var string = '{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}';
|
|
var template = CompilerContext.compile(string);
|
|
|
|
var result = template(
|
|
{ yehuda: { name: 'Yehuda', cat: { name: 'Harold' } } },
|
|
{
|
|
helpers: {
|
|
form: function(context, options) {
|
|
return '<form>' + options.fn(context) + '</form>';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(
|
|
result,
|
|
'<form><p>Harold</p></form>',
|
|
'Complex path variable resolved'
|
|
);
|
|
});
|
|
|
|
it('nested block helpers', function() {
|
|
var string =
|
|
'{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}';
|
|
var template = CompilerContext.compile(string);
|
|
|
|
var result = template(
|
|
{
|
|
yehuda: { name: 'Yehuda' }
|
|
},
|
|
{
|
|
helpers: {
|
|
link: function(options) {
|
|
return '<a href="' + this.name + '">' + options.fn(this) + '</a>';
|
|
},
|
|
form: function(context, options) {
|
|
return '<form>' + options.fn(context) + '</form>';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(
|
|
result,
|
|
'<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>',
|
|
'Both blocks executed'
|
|
);
|
|
});
|
|
|
|
it('block helper inverted sections', function() {
|
|
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
|
|
function list(context, options) {
|
|
if (context.length > 0) {
|
|
var out = '<ul>';
|
|
for (var i = 0, j = context.length; i < j; i++) {
|
|
out += '<li>';
|
|
out += options.fn(context[i]);
|
|
out += '</li>';
|
|
}
|
|
out += '</ul>';
|
|
return out;
|
|
} else {
|
|
return '<p>' + options.inverse(this) + '</p>';
|
|
}
|
|
}
|
|
|
|
var hash = { people: [{ name: 'Alan' }, { name: 'Yehuda' }] };
|
|
var empty = { people: [] };
|
|
var rootMessage = {
|
|
people: [],
|
|
message: "Nobody's here"
|
|
};
|
|
|
|
var messageString = '{{#list people}}Hello{{^}}{{message}}{{/list}}';
|
|
|
|
// the meaning here may be kind of hard to catch, but list.not is always called,
|
|
// so we should see the output of both
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, { list: list }],
|
|
'<ul><li>Alan</li><li>Yehuda</li></ul>',
|
|
'an inverse wrapper is passed in as a new context'
|
|
);
|
|
shouldCompileTo(
|
|
string,
|
|
[empty, { list: list }],
|
|
"<p><em>Nobody's here</em></p>",
|
|
'an inverse wrapper can be optionally called'
|
|
);
|
|
shouldCompileTo(
|
|
messageString,
|
|
[rootMessage, { list: list }],
|
|
'<p>Nobody's here</p>',
|
|
'the context of an inverse is the parent of the block'
|
|
);
|
|
});
|
|
|
|
it('pathed lambas with parameters', function() {
|
|
var hash = {
|
|
helper: function() {
|
|
return 'winning';
|
|
}
|
|
};
|
|
hash.hash = hash;
|
|
var helpers = {
|
|
'./helper': function() {
|
|
return 'fail';
|
|
}
|
|
};
|
|
shouldCompileTo('{{./helper 1}}', [hash, helpers], 'winning');
|
|
shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning');
|
|
});
|
|
|
|
describe('helpers hash', function() {
|
|
it('providing a helpers hash', function() {
|
|
shouldCompileTo(
|
|
'Goodbye {{cruel}} {{world}}!',
|
|
[
|
|
{ cruel: 'cruel' },
|
|
{
|
|
world: function() {
|
|
return 'world';
|
|
}
|
|
}
|
|
],
|
|
'Goodbye cruel world!',
|
|
'helpers hash is available'
|
|
);
|
|
|
|
shouldCompileTo(
|
|
'Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!',
|
|
[
|
|
{ iter: [{ cruel: 'cruel' }] },
|
|
{
|
|
world: function() {
|
|
return 'world';
|
|
}
|
|
}
|
|
],
|
|
'Goodbye cruel world!',
|
|
'helpers hash is available inside other blocks'
|
|
);
|
|
});
|
|
|
|
it('in cases of conflict, helpers win', function() {
|
|
shouldCompileTo(
|
|
'{{{lookup}}}',
|
|
[
|
|
{ lookup: 'Explicit' },
|
|
{
|
|
lookup: function() {
|
|
return 'helpers';
|
|
}
|
|
}
|
|
],
|
|
'helpers',
|
|
'helpers hash has precedence escaped expansion'
|
|
);
|
|
shouldCompileTo(
|
|
'{{lookup}}',
|
|
[
|
|
{ lookup: 'Explicit' },
|
|
{
|
|
lookup: function() {
|
|
return 'helpers';
|
|
}
|
|
}
|
|
],
|
|
'helpers',
|
|
'helpers hash has precedence simple expansion'
|
|
);
|
|
});
|
|
|
|
it('the helpers hash is available is nested contexts', function() {
|
|
shouldCompileTo(
|
|
'{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}',
|
|
[
|
|
{ outer: { inner: { unused: [] } } },
|
|
{
|
|
helper: function() {
|
|
return 'helper';
|
|
}
|
|
}
|
|
],
|
|
'helper',
|
|
'helpers hash is available in nested contexts.'
|
|
);
|
|
});
|
|
|
|
it('the helper hash should augment the global hash', function() {
|
|
handlebarsEnv.registerHelper('test_helper', function() {
|
|
return 'found it!';
|
|
});
|
|
|
|
shouldCompileTo(
|
|
'{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}',
|
|
[
|
|
{ cruel: 'cruel' },
|
|
{
|
|
world: function() {
|
|
return 'world!';
|
|
}
|
|
}
|
|
],
|
|
'found it! Goodbye cruel world!!'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('registration', function() {
|
|
it('unregisters', function() {
|
|
handlebarsEnv.helpers = {};
|
|
|
|
handlebarsEnv.registerHelper('foo', function() {
|
|
return 'fail';
|
|
});
|
|
handlebarsEnv.unregisterHelper('foo');
|
|
equals(handlebarsEnv.helpers.foo, undefined);
|
|
});
|
|
|
|
it('allows multiple globals', function() {
|
|
var helpers = handlebarsEnv.helpers;
|
|
handlebarsEnv.helpers = {};
|
|
|
|
handlebarsEnv.registerHelper({
|
|
if: helpers['if'],
|
|
world: function() {
|
|
return 'world!';
|
|
},
|
|
testHelper: function() {
|
|
return 'found it!';
|
|
}
|
|
});
|
|
|
|
shouldCompileTo(
|
|
'{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}',
|
|
[{ cruel: 'cruel' }],
|
|
'found it! Goodbye cruel world!!'
|
|
);
|
|
});
|
|
it('fails with multiple and args', function() {
|
|
shouldThrow(
|
|
function() {
|
|
handlebarsEnv.registerHelper(
|
|
{
|
|
world: function() {
|
|
return 'world!';
|
|
},
|
|
testHelper: function() {
|
|
return 'found it!';
|
|
}
|
|
},
|
|
{}
|
|
);
|
|
},
|
|
Error,
|
|
'Arg not supported with multiple helpers'
|
|
);
|
|
});
|
|
});
|
|
|
|
it('decimal number literals work', function() {
|
|
var string = 'Message: {{hello -1.2 1.2}}';
|
|
var helpers = {
|
|
hello: function(times, times2) {
|
|
if (typeof times !== 'number') {
|
|
times = 'NaN';
|
|
}
|
|
if (typeof times2 !== 'number') {
|
|
times2 = 'NaN';
|
|
}
|
|
return 'Hello ' + times + ' ' + times2 + ' times';
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
'Message: Hello -1.2 1.2 times',
|
|
'template with a negative integer literal'
|
|
);
|
|
});
|
|
|
|
it('negative number literals work', function() {
|
|
var string = 'Message: {{hello -12}}';
|
|
var helpers = {
|
|
hello: function(times) {
|
|
if (typeof times !== 'number') {
|
|
times = 'NaN';
|
|
}
|
|
return 'Hello ' + times + ' times';
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
'Message: Hello -12 times',
|
|
'template with a negative integer literal'
|
|
);
|
|
});
|
|
|
|
describe('String literal parameters', function() {
|
|
it('simple literals work', function() {
|
|
var string = 'Message: {{hello "world" 12 true false}}';
|
|
var helpers = {
|
|
hello: function(param, times, bool1, bool2) {
|
|
if (typeof times !== 'number') {
|
|
times = 'NaN';
|
|
}
|
|
if (typeof bool1 !== 'boolean') {
|
|
bool1 = 'NaB';
|
|
}
|
|
if (typeof bool2 !== 'boolean') {
|
|
bool2 = 'NaB';
|
|
}
|
|
return (
|
|
'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2
|
|
);
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
'Message: Hello world 12 times: true false',
|
|
'template with a simple String literal'
|
|
);
|
|
});
|
|
|
|
it('using a quote in the middle of a parameter raises an error', function() {
|
|
var string = 'Message: {{hello wo"rld"}}';
|
|
shouldThrow(function() {
|
|
CompilerContext.compile(string);
|
|
}, Error);
|
|
});
|
|
|
|
it('escaping a String is possible', function() {
|
|
var string = 'Message: {{{hello "\\"world\\""}}}';
|
|
var helpers = {
|
|
hello: function(param) {
|
|
return 'Hello ' + param;
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
'Message: Hello "world"',
|
|
'template with an escaped String literal'
|
|
);
|
|
});
|
|
|
|
it("it works with ' marks", function() {
|
|
var string = 'Message: {{{hello "Alan\'s world"}}}';
|
|
var helpers = {
|
|
hello: function(param) {
|
|
return 'Hello ' + param;
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
"Message: Hello Alan's world",
|
|
"template with a ' mark"
|
|
);
|
|
});
|
|
});
|
|
|
|
it('negative number literals work', function() {
|
|
var string = 'Message: {{hello -12}}';
|
|
var helpers = {
|
|
hello: function(times) {
|
|
if (typeof times !== 'number') {
|
|
times = 'NaN';
|
|
}
|
|
return 'Hello ' + times + ' times';
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[{}, helpers],
|
|
'Message: Hello -12 times',
|
|
'template with a negative integer literal'
|
|
);
|
|
});
|
|
|
|
describe('multiple parameters', function() {
|
|
it('simple multi-params work', function() {
|
|
var string = 'Message: {{goodbye cruel world}}';
|
|
var hash = { cruel: 'cruel', world: 'world' };
|
|
var helpers = {
|
|
goodbye: function(cruel, world) {
|
|
return 'Goodbye ' + cruel + ' ' + world;
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
'Message: Goodbye cruel world',
|
|
'regular helpers with multiple params'
|
|
);
|
|
});
|
|
|
|
it('block multi-params work', function() {
|
|
var string =
|
|
'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
|
|
var hash = { cruel: 'cruel', world: 'world' };
|
|
var helpers = {
|
|
goodbye: function(cruel, world, options) {
|
|
return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world });
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
string,
|
|
[hash, helpers],
|
|
'Message: Goodbye cruel world',
|
|
'block helpers with multiple params'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('hash', function() {
|
|
it('helpers can take an optional hash', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{goodbye cruel="CRUEL" world="WORLD" times=12}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
return (
|
|
'GOODBYE ' +
|
|
options.hash.cruel +
|
|
' ' +
|
|
options.hash.world +
|
|
' ' +
|
|
options.hash.times +
|
|
' TIMES'
|
|
);
|
|
}
|
|
};
|
|
|
|
var context = {};
|
|
|
|
var result = template(context, { helpers: helpers });
|
|
equals(result, 'GOODBYE CRUEL WORLD 12 TIMES', 'Helper output hash');
|
|
});
|
|
|
|
it('helpers can take an optional hash with booleans', function() {
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
if (options.hash.print === true) {
|
|
return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
|
|
} else if (options.hash.print === false) {
|
|
return 'NOT PRINTING';
|
|
} else {
|
|
return 'THIS SHOULD NOT HAPPEN';
|
|
}
|
|
}
|
|
};
|
|
|
|
var context = {};
|
|
|
|
var template = CompilerContext.compile(
|
|
'{{goodbye cruel="CRUEL" world="WORLD" print=true}}'
|
|
);
|
|
var result = template(context, { helpers: helpers });
|
|
equals(result, 'GOODBYE CRUEL WORLD', 'Helper output hash');
|
|
|
|
template = CompilerContext.compile(
|
|
'{{goodbye cruel="CRUEL" world="WORLD" print=false}}'
|
|
);
|
|
result = template(context, { helpers: helpers });
|
|
equals(result, 'NOT PRINTING', 'Boolean helper parameter honored');
|
|
});
|
|
|
|
it('block helpers can take an optional hash', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
return (
|
|
'GOODBYE ' +
|
|
options.hash.cruel +
|
|
' ' +
|
|
options.fn(this) +
|
|
' ' +
|
|
options.hash.times +
|
|
' TIMES'
|
|
);
|
|
}
|
|
};
|
|
|
|
var result = template({}, { helpers: helpers });
|
|
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
|
|
});
|
|
|
|
it('block helpers can take an optional hash with single quoted stings', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
return (
|
|
'GOODBYE ' +
|
|
options.hash.cruel +
|
|
' ' +
|
|
options.fn(this) +
|
|
' ' +
|
|
options.hash.times +
|
|
' TIMES'
|
|
);
|
|
}
|
|
};
|
|
|
|
var result = template({}, { helpers: helpers });
|
|
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
|
|
});
|
|
|
|
it('block helpers can take an optional hash with booleans', function() {
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
if (options.hash.print === true) {
|
|
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
|
|
} else if (options.hash.print === false) {
|
|
return 'NOT PRINTING';
|
|
} else {
|
|
return 'THIS SHOULD NOT HAPPEN';
|
|
}
|
|
}
|
|
};
|
|
|
|
var template = CompilerContext.compile(
|
|
'{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}'
|
|
);
|
|
var result = template({}, { helpers: helpers });
|
|
equals(result, 'GOODBYE CRUEL world', 'Boolean hash parameter honored');
|
|
|
|
template = CompilerContext.compile(
|
|
'{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'
|
|
);
|
|
result = template({}, { helpers: helpers });
|
|
equals(result, 'NOT PRINTING', 'Boolean hash parameter honored');
|
|
});
|
|
});
|
|
|
|
describe('helperMissing', function() {
|
|
it('if a context is not found, helperMissing is used', function() {
|
|
shouldThrow(
|
|
function() {
|
|
var template = CompilerContext.compile('{{hello}} {{link_to world}}');
|
|
template({});
|
|
},
|
|
undefined,
|
|
/Missing helper: "link_to"/
|
|
);
|
|
});
|
|
|
|
it('if a context is not found, custom helperMissing is used', function() {
|
|
var string = '{{hello}} {{link_to world}}';
|
|
var context = { hello: 'Hello', world: 'world' };
|
|
|
|
var helpers = {
|
|
helperMissing: function(mesg, options) {
|
|
if (options.name === 'link_to') {
|
|
return new Handlebars.SafeString('<a>' + mesg + '</a>');
|
|
}
|
|
}
|
|
};
|
|
|
|
shouldCompileTo(string, [context, helpers], 'Hello <a>world</a>');
|
|
});
|
|
|
|
it('if a value is not found, custom helperMissing is used', function() {
|
|
var string = '{{hello}} {{link_to}}';
|
|
var context = { hello: 'Hello', world: 'world' };
|
|
|
|
var helpers = {
|
|
helperMissing: function(options) {
|
|
if (options.name === 'link_to') {
|
|
return new Handlebars.SafeString('<a>winning</a>');
|
|
}
|
|
}
|
|
};
|
|
|
|
shouldCompileTo(string, [context, helpers], 'Hello <a>winning</a>');
|
|
});
|
|
});
|
|
|
|
describe('knownHelpers', function() {
|
|
it('Known helper should render helper', function() {
|
|
var template = CompilerContext.compile('{{hello}}', {
|
|
knownHelpers: { hello: true }
|
|
});
|
|
|
|
var result = template(
|
|
{},
|
|
{
|
|
helpers: {
|
|
hello: function() {
|
|
return 'foo';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(result, 'foo', "'foo' should === '" + result);
|
|
});
|
|
|
|
it('Unknown helper in knownHelpers only mode should be passed as undefined', function() {
|
|
var template = CompilerContext.compile('{{typeof hello}}', {
|
|
knownHelpers: { typeof: true },
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
var result = template(
|
|
{},
|
|
{
|
|
helpers: {
|
|
typeof: function(arg) {
|
|
return typeof arg;
|
|
},
|
|
hello: function() {
|
|
return 'foo';
|
|
}
|
|
}
|
|
}
|
|
);
|
|
equal(result, 'undefined', "'undefined' should === '" + result);
|
|
});
|
|
it('Builtin helpers available in knownHelpers only mode', function() {
|
|
var template = CompilerContext.compile('{{#unless foo}}bar{{/unless}}', {
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
var result = template({});
|
|
equal(result, 'bar', "'bar' should === '" + result);
|
|
});
|
|
it('Field lookup works in knownHelpers only mode', function() {
|
|
var template = CompilerContext.compile('{{foo}}', {
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
var result = template({ foo: 'bar' });
|
|
equal(result, 'bar', "'bar' should === '" + result);
|
|
});
|
|
it('Conditional blocks work in knownHelpers only mode', function() {
|
|
var template = CompilerContext.compile('{{#foo}}bar{{/foo}}', {
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
var result = template({ foo: 'baz' });
|
|
equal(result, 'bar', "'bar' should === '" + result);
|
|
});
|
|
it('Invert blocks work in knownHelpers only mode', function() {
|
|
var template = CompilerContext.compile('{{^foo}}bar{{/foo}}', {
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
var result = template({ foo: false });
|
|
equal(result, 'bar', "'bar' should === '" + result);
|
|
});
|
|
it('Functions are bound to the context in knownHelpers only mode', function() {
|
|
var template = CompilerContext.compile('{{foo}}', {
|
|
knownHelpersOnly: true
|
|
});
|
|
var result = template({
|
|
foo: function() {
|
|
return this.bar;
|
|
},
|
|
bar: 'bar'
|
|
});
|
|
equal(result, 'bar', "'bar' should === '" + result);
|
|
});
|
|
it('Unknown helper call in knownHelpers only mode should throw', function() {
|
|
shouldThrow(function() {
|
|
CompilerContext.compile('{{typeof hello}}', { knownHelpersOnly: true });
|
|
}, Error);
|
|
});
|
|
});
|
|
|
|
describe('blockHelperMissing', function() {
|
|
it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() {
|
|
var string = '{{#truthy}}yep{{/truthy}}';
|
|
var data = {
|
|
truthy: function() {
|
|
return true;
|
|
}
|
|
};
|
|
shouldCompileTo(string, data, 'yep');
|
|
});
|
|
it('lambdas resolved by blockHelperMissing are bound to the context', function() {
|
|
var string = '{{#truthy}}yep{{/truthy}}';
|
|
var boundData = {
|
|
truthy: function() {
|
|
return this.truthiness();
|
|
},
|
|
truthiness: function() {
|
|
return false;
|
|
}
|
|
};
|
|
shouldCompileTo(string, boundData, '');
|
|
});
|
|
});
|
|
|
|
describe('name field', function() {
|
|
var context = {};
|
|
var helpers = {
|
|
blockHelperMissing: function() {
|
|
return 'missing: ' + arguments[arguments.length - 1].name;
|
|
},
|
|
helperMissing: function() {
|
|
return 'helper missing: ' + arguments[arguments.length - 1].name;
|
|
},
|
|
helper: function() {
|
|
return 'ran: ' + arguments[arguments.length - 1].name;
|
|
}
|
|
};
|
|
|
|
it('should include in ambiguous mustache calls', function() {
|
|
shouldCompileTo('{{helper}}', [context, helpers], 'ran: helper');
|
|
});
|
|
it('should include in helper mustache calls', function() {
|
|
shouldCompileTo('{{helper 1}}', [context, helpers], 'ran: helper');
|
|
});
|
|
it('should include in ambiguous block calls', function() {
|
|
shouldCompileTo(
|
|
'{{#helper}}{{/helper}}',
|
|
[context, helpers],
|
|
'ran: helper'
|
|
);
|
|
});
|
|
it('should include in simple block calls', function() {
|
|
shouldCompileTo(
|
|
'{{#./helper}}{{/./helper}}',
|
|
[context, helpers],
|
|
'missing: ./helper'
|
|
);
|
|
});
|
|
it('should include in helper block calls', function() {
|
|
shouldCompileTo(
|
|
'{{#helper 1}}{{/helper}}',
|
|
[context, helpers],
|
|
'ran: helper'
|
|
);
|
|
});
|
|
it('should include in known helper calls', function() {
|
|
var template = CompilerContext.compile('{{helper}}', {
|
|
knownHelpers: { helper: true },
|
|
knownHelpersOnly: true
|
|
});
|
|
|
|
equal(template({}, { helpers: helpers }), 'ran: helper');
|
|
});
|
|
|
|
it('should include full id', function() {
|
|
shouldCompileTo(
|
|
'{{#foo.helper}}{{/foo.helper}}',
|
|
[{ foo: {} }, helpers],
|
|
'missing: foo.helper'
|
|
);
|
|
});
|
|
|
|
it('should include full id if a hash is passed', function() {
|
|
shouldCompileTo(
|
|
'{{#foo.helper bar=baz}}{{/foo.helper}}',
|
|
[{ foo: {} }, helpers],
|
|
'helper missing: foo.helper'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('name conflicts', function() {
|
|
it('helpers take precedence over same-named context properties', function() {
|
|
var template = CompilerContext.compile('{{goodbye}} {{cruel world}}');
|
|
|
|
var helpers = {
|
|
goodbye: function() {
|
|
return this.goodbye.toUpperCase();
|
|
},
|
|
|
|
cruel: function(world) {
|
|
return 'cruel ' + world.toUpperCase();
|
|
}
|
|
};
|
|
|
|
var context = {
|
|
goodbye: 'goodbye',
|
|
world: 'world'
|
|
};
|
|
|
|
var result = template(context, { helpers: helpers });
|
|
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
|
|
});
|
|
|
|
it('helpers take precedence over same-named context properties$', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#goodbye}} {{cruel world}}{{/goodbye}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
return this.goodbye.toUpperCase() + options.fn(this);
|
|
},
|
|
|
|
cruel: function(world) {
|
|
return 'cruel ' + world.toUpperCase();
|
|
}
|
|
};
|
|
|
|
var context = {
|
|
goodbye: 'goodbye',
|
|
world: 'world'
|
|
};
|
|
|
|
var result = template(context, { helpers: helpers });
|
|
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
|
|
});
|
|
|
|
it('Scoped names take precedence over helpers', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function() {
|
|
return this.goodbye.toUpperCase();
|
|
},
|
|
|
|
cruel: function(world) {
|
|
return 'cruel ' + world.toUpperCase();
|
|
}
|
|
};
|
|
|
|
var context = {
|
|
goodbye: 'goodbye',
|
|
world: 'world'
|
|
};
|
|
|
|
var result = template(context, { helpers: helpers });
|
|
equals(
|
|
result,
|
|
'goodbye cruel WORLD cruel GOODBYE',
|
|
'Helper not executed'
|
|
);
|
|
});
|
|
|
|
it('Scoped names take precedence over block helpers', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}'
|
|
);
|
|
|
|
var helpers = {
|
|
goodbye: function(options) {
|
|
return this.goodbye.toUpperCase() + options.fn(this);
|
|
},
|
|
|
|
cruel: function(world) {
|
|
return 'cruel ' + world.toUpperCase();
|
|
}
|
|
};
|
|
|
|
var context = {
|
|
goodbye: 'goodbye',
|
|
world: 'world'
|
|
};
|
|
|
|
var result = template(context, { helpers: helpers });
|
|
equals(result, 'GOODBYE cruel WORLD goodbye', 'Helper executed');
|
|
});
|
|
});
|
|
|
|
describe('block params', function() {
|
|
it('should take presedence over context values', function() {
|
|
var hash = { value: 'foo' };
|
|
var helpers = {
|
|
goodbyes: function(options) {
|
|
equals(options.fn.blockParams, 1);
|
|
return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
'{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}',
|
|
[hash, helpers],
|
|
'1foo'
|
|
);
|
|
});
|
|
it('should take presedence over helper values', function() {
|
|
var hash = {};
|
|
var helpers = {
|
|
value: function() {
|
|
return 'foo';
|
|
},
|
|
goodbyes: function(options) {
|
|
equals(options.fn.blockParams, 1);
|
|
return options.fn({}, { blockParams: [1, 2] });
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
'{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}',
|
|
[hash, helpers],
|
|
'1foo'
|
|
);
|
|
});
|
|
it('should not take presedence over pathed values', function() {
|
|
var hash = { value: 'bar' };
|
|
var helpers = {
|
|
value: function() {
|
|
return 'foo';
|
|
},
|
|
goodbyes: function(options) {
|
|
equals(options.fn.blockParams, 1);
|
|
return options.fn(this, { blockParams: [1, 2] });
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
'{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}',
|
|
[hash, helpers],
|
|
'barfoo'
|
|
);
|
|
});
|
|
it('should take presednece over parent block params', function() {
|
|
var hash = { value: 'foo' },
|
|
value = 1;
|
|
var helpers = {
|
|
goodbyes: function(options) {
|
|
return options.fn(
|
|
{ value: 'bar' },
|
|
{
|
|
blockParams:
|
|
options.fn.blockParams === 1 ? [value++, value++] : undefined
|
|
}
|
|
);
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
'{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}',
|
|
[hash, helpers],
|
|
'13foo'
|
|
);
|
|
});
|
|
|
|
it('should allow block params on chained helpers', function() {
|
|
var hash = { value: 'foo' };
|
|
var helpers = {
|
|
goodbyes: function(options) {
|
|
equals(options.fn.blockParams, 1);
|
|
return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
|
|
}
|
|
};
|
|
shouldCompileTo(
|
|
'{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}',
|
|
[hash, helpers],
|
|
'1foo'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('built-in helpers malformed arguments ', function() {
|
|
it('if helper - too few arguments', function() {
|
|
var template = CompilerContext.compile('{{#if}}{{/if}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#if requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('if helper - too many arguments, string', function() {
|
|
var template = CompilerContext.compile('{{#if test "string"}}{{/if}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#if requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('if helper - too many arguments, undefined', function() {
|
|
var template = CompilerContext.compile('{{#if test undefined}}{{/if}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#if requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('if helper - too many arguments, null', function() {
|
|
var template = CompilerContext.compile('{{#if test null}}{{/if}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#if requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('unless helper - too few arguments', function() {
|
|
var template = CompilerContext.compile('{{#unless}}{{/unless}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#unless requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('unless helper - too many arguments', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#unless test null}}{{/unless}}'
|
|
);
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#unless requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('with helper - too few arguments', function() {
|
|
var template = CompilerContext.compile('{{#with}}{{/with}}');
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#with requires exactly one argument/
|
|
);
|
|
});
|
|
|
|
it('with helper - too many arguments', function() {
|
|
var template = CompilerContext.compile(
|
|
'{{#with test "string"}}{{/with}}'
|
|
);
|
|
shouldThrow(
|
|
function() {
|
|
template({});
|
|
},
|
|
undefined,
|
|
/#with requires exactly one argument/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('the lookupProperty-option', function() {
|
|
it('should be passed to custom helpers', function() {
|
|
expectTemplate('{{testHelper}}')
|
|
.withHelper('testHelper', function testHelper(options) {
|
|
return options.lookupProperty(this, 'testProperty');
|
|
})
|
|
.withInput({ testProperty: 'abc' })
|
|
.toCompileTo('abc');
|
|
});
|
|
});
|
|
});
|