0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Added number & index matching to {{#has}} helper (#8902)

refs #8901

- Adds support for:

    ```
    {{#has number="3"}} // A single number
    {{#has number="3, 6, 9"}} // list the numbers you want to match against
    {{#has number="nth:3"}} // special syntax for nth item
    ```

    And

    ```
    {{#has index="3"}} // A single number
    {{#has index="3, 6, 9"}} // list the numbers you want to match against
    {{#has index="nth:3"}} // special syntax for nth item
    ```
This commit is contained in:
Hannah Wolfe 2017-08-15 16:00:17 +01:00 committed by Kevin Ansfield
parent d064eda229
commit 6ee9bb491c
2 changed files with 336 additions and 152 deletions

View file

@ -8,17 +8,6 @@ var proxy = require('./proxy'),
logging = proxy.logging, logging = proxy.logging,
i18n = proxy.i18n; i18n = proxy.i18n;
module.exports = function has(options) {
options = options || {};
options.hash = options.hash || {};
var tags = _.map(this.tags, 'name'),
author = this.author ? this.author.name : null,
tagList = options.hash.tag || false,
authorList = options.hash.author || false,
tagsOk,
authorOk;
function evaluateTagList(expr, tags) { function evaluateTagList(expr, tags) {
return expr.split(',').map(function (v) { return expr.split(',').map(function (v) {
return v.trim(); return v.trim();
@ -40,15 +29,45 @@ module.exports = function has(options) {
return _.includes(authorList, author.toLocaleLowerCase()); return _.includes(authorList, author.toLocaleLowerCase());
} }
if (!tagList && !authorList) { function evaluateIntegerMatch(expr, integer) {
var nthMatch = expr.match(/^nth:(\d+)/);
if (nthMatch) {
return integer % parseInt(nthMatch[1], 10) === 0;
}
return expr.split(',').reduce(function (bool, _integer) {
return bool || parseInt(_integer, 10) === integer;
}, false);
}
module.exports = function has(options) {
options = options || {};
options.hash = options.hash || {};
var tags = _.map(this.tags, 'name'),
author = this.author ? this.author.name : null,
number = options.data.number,
index = options.data.index,
tagList = options.hash.tag || false,
authorList = options.hash.author || false,
numberList = options.hash.number || false,
indexList = options.hash.index || false,
tagsOk,
authorOk,
numberOk,
indexOk;
if (!tagList && !authorList && !numberList && !indexList) {
logging.warn(i18n.t('warnings.helpers.has.invalidAttribute')); logging.warn(i18n.t('warnings.helpers.has.invalidAttribute'));
return; return;
} }
tagsOk = tagList && evaluateTagList(tagList, tags) || false; tagsOk = tagList && evaluateTagList(tagList, tags) || false;
authorOk = authorList && evaluateAuthorList(authorList, author) || false; authorOk = authorList && evaluateAuthorList(authorList, author) || false;
numberOk = numberList && evaluateIntegerMatch(numberList, number) || false;
indexOk = indexList && evaluateIntegerMatch(indexList, index) || false;
if (tagsOk || authorOk) { if (tagsOk || authorOk || numberOk || indexOk) {
return options.fn(this); return options.fn(this);
} }
return options.inverse(this); return options.inverse(this);

View file

@ -7,176 +7,341 @@ var should = require('should'), // jshint ignore:line
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
describe('{{#has}} helper', function () { describe('{{#has}} helper', function () {
var fn, inverse, thisCtx, handlebarsOptions;
afterEach(function () { afterEach(function () {
sandbox.restore(); sandbox.restore();
}); });
it('should handle tag list that validates true', function () { beforeEach(function () {
var fn = sandbox.spy(), fn = sandbox.spy();
inverse = sandbox.spy(); inverse = sandbox.spy();
helpers.has.call( thisCtx = {};
{tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {tag: 'invalid, bar, wat'}, fn: fn, inverse: inverse} // This object mocks out the object that handlebars helpers get passed
); handlebarsOptions = {
hash: {},
data: {},
fn: fn,
inverse: inverse
};
});
function callHasHelper(thisCtx, hash) {
// Hash is the options passed in
handlebarsOptions.hash = hash;
return helpers.has.call(thisCtx, handlebarsOptions);
}
describe('tag match', function () {
it('should handle tag list that validates true', function () {
thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
// {{#has tag="invalid, bar, wat"}}
callHasHelper(thisCtx, {tag: 'invalid, bar, wat'});
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should handle tags with case-insensitivity', function () { it('should handle tags with case-insensitivity', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'ghost'}]};
inverse = sandbox.spy();
helpers.has.call( // {{#has tag="GhoSt"}}
{tags: [{name: 'ghost'}]}, callHasHelper(thisCtx, {tag: 'GhoSt'});
{hash: {tag: 'GhoSt'}, fn: fn, inverse: inverse}
);
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should match exact tags, not superstrings', function () { it('should match exact tags, not superstrings', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'magical'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {tag: 'magic'});
{tags: [{name: 'magical'}]},
{hash: {tag: 'magic'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.true(); inverse.called.should.be.true();
}); });
it('should match exact tags, not substrings', function () { it('should match exact tags, not substrings', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'magic'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {tag: 'magical'});
{tags: [{name: 'magic'}]},
{hash: {tag: 'magical'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.true(); inverse.called.should.be.true();
}); });
it('should handle tag list that validates false', function () { it('should handle tag list that validates false', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {tag: 'much, such, wow'});
{tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {tag: 'much, such, wow'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.true(); inverse.called.should.be.true();
}); });
it('should not do anything if there are no attributes', function () { it('should not do anything if there are no attributes', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx);
{tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should not do anything when an invalid attribute is given', function () { it('should not do anything when an invalid attribute is given', function () {
var fn = sandbox.spy(), thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {invalid: 'nonsense'});
{tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {invalid: 'nonsense'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
});
describe('author match', function () {
it('should handle author list that evaluates to true', function () { it('should handle author list that evaluates to true', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'sam'}};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sam, pat'});
{author: {name: 'sam'}},
{hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse}
);
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should handle author list that evaluates to false', function () { it('should handle author list that evaluates to false', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'jamie'}};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sam, pat'});
{author: {name: 'jamie'}},
{hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.true(); inverse.called.should.be.true();
}); });
it('should handle authors with case-insensitivity', function () { it('should handle authors with case-insensitivity', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'Sam'}};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sAm, pat'});
{author: {name: 'Sam'}},
{hash: {author: 'joe, sAm, pat'}, fn: fn, inverse: inverse}
);
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should handle tags and authors like an OR query (pass)', function () { it('should handle tags and authors like an OR query (pass)', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'sam'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'});
{author: {name: 'sam'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse}
);
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should handle tags and authors like an OR query (pass)', function () { it('should handle tags and authors like an OR query (pass)', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'sam'}, tags: [{name: 'much'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'});
{author: {name: 'sam'}, tags: [{name: 'much'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse}
);
fn.called.should.be.true(); fn.called.should.be.true();
inverse.called.should.be.false(); inverse.called.should.be.false();
}); });
it('should handle tags and authors like an OR query (fail)', function () { it('should handle tags and authors like an OR query (fail)', function () {
var fn = sandbox.spy(), thisCtx = {author: {name: 'fred'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]};
inverse = sandbox.spy();
helpers.has.call( callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'});
{author: {name: 'fred'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]},
{hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse}
);
fn.called.should.be.false(); fn.called.should.be.false();
inverse.called.should.be.true(); inverse.called.should.be.true();
}); });
}); });
describe('number match (1-based index)', function () {
it('will match on an exact number (pass)', function () {
handlebarsOptions.data = {number: 6};
callHasHelper(thisCtx, {number: '6'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on an exact number (fail)', function () {
handlebarsOptions.data = {number: 5};
callHasHelper(thisCtx, {number: '6'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on an exact number (loop)', function () {
for (var number = 1; number < 9; number += 1) {
handlebarsOptions.data = {number: number};
// Will match 6
callHasHelper(thisCtx, {number: '6'});
}
fn.calledOnce.should.be.true();
inverse.called.should.be.true();
inverse.callCount.should.eql(7);
});
it('will match on a number list (pass)', function () {
handlebarsOptions.data = {number: 6};
callHasHelper(thisCtx, {number: '1, 3, 6,12'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on a number list (fail)', function () {
handlebarsOptions.data = {number: 5};
callHasHelper(thisCtx, {number: '1, 3, 6,12'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on a number list (loop)', function () {
for (var number = 1; number < 9; number += 1) {
handlebarsOptions.data = {number: number};
// Will match 1, 3, 6
callHasHelper(thisCtx, {number: '1, 3, 6,12'});
}
fn.called.should.be.true();
fn.callCount.should.eql(3);
inverse.called.should.be.true();
inverse.callCount.should.eql(5);
});
it('will match on a nth pattern (pass)', function () {
handlebarsOptions.data = {number: 6};
callHasHelper(thisCtx, {number: 'nth:3'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on a nth pattern (fail)', function () {
handlebarsOptions.data = {number: 5};
callHasHelper(thisCtx, {number: 'nth:3'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on a nth pattern (loop)', function () {
for (var number = 1; number < 9; number += 1) {
handlebarsOptions.data = {number: number};
// Will match 3 & 6
callHasHelper(thisCtx, {number: 'nth:3'});
}
fn.called.should.be.true();
fn.callCount.should.eql(2);
inverse.called.should.be.true();
inverse.callCount.should.eql(6);
});
});
describe('index match (0-based index)', function () {
it('will match on an exact index (pass)', function () {
handlebarsOptions.data = {index: 6};
callHasHelper(thisCtx, {index: '6'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on an exact index (fail)', function () {
handlebarsOptions.data = {index: 5};
callHasHelper(thisCtx, {index: '6'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on an exact index (loop)', function () {
for (var index = 0; index < 8; index += 1) {
handlebarsOptions.data = {index: index};
// Will match 6
callHasHelper(thisCtx, {index: '6'});
}
fn.calledOnce.should.be.true();
inverse.called.should.be.true();
inverse.callCount.should.eql(7);
});
it('will match on an index list (pass)', function () {
handlebarsOptions.data = {index: 6};
callHasHelper(thisCtx, {index: '1, 3, 6,12'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on an index list (fail)', function () {
handlebarsOptions.data = {index: 5};
callHasHelper(thisCtx, {index: '1, 3, 6,12'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on an index list (loop)', function () {
for (var index = 0; index < 8; index += 1) {
handlebarsOptions.data = {index: index};
// Will match 1, 3, 6
callHasHelper(thisCtx, {index: '1, 3, 6,12'});
}
fn.called.should.be.true();
fn.callCount.should.eql(3);
inverse.called.should.be.true();
inverse.callCount.should.eql(5);
});
it('will match on a nth pattern (pass)', function () {
handlebarsOptions.data = {index: 6};
callHasHelper(thisCtx, {index: 'nth:3'});
fn.called.should.be.true();
inverse.called.should.be.false();
});
it('will match on a nth pattern (fail)', function () {
handlebarsOptions.data = {index: 5};
callHasHelper(thisCtx, {index: 'nth:3'});
fn.called.should.be.false();
inverse.called.should.be.true();
});
it('will match on a nth pattern (loop)', function () {
for (var index = 0; index < 8; index += 1) {
handlebarsOptions.data = {index: index};
// Will match 0, 3, 6
callHasHelper(thisCtx, {index: 'nth:3'});
}
fn.called.should.be.true();
fn.callCount.should.eql(3);
inverse.called.should.be.true();
inverse.callCount.should.eql(5);
});
});
});