From 6ee9bb491cd311fc8410161bcb0d29a5f0796e51 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Tue, 15 Aug 2017 16:00:17 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Added=20number=20&=20index=20matchi?= =?UTF-8?q?ng=20to=20{{#has}}=20helper=20(#8902)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``` --- core/server/helpers/has.js | 67 ++-- core/test/unit/server_helpers/has_spec.js | 421 +++++++++++++++------- 2 files changed, 336 insertions(+), 152 deletions(-) diff --git a/core/server/helpers/has.js b/core/server/helpers/has.js index 3edf5c82d1..bd2830ae56 100644 --- a/core/server/helpers/has.js +++ b/core/server/helpers/has.js @@ -8,47 +8,66 @@ var proxy = require('./proxy'), logging = proxy.logging, i18n = proxy.i18n; +function evaluateTagList(expr, tags) { + return expr.split(',').map(function (v) { + return v.trim(); + }).reduce(function (p, c) { + return p || (_.findIndex(tags, function (item) { + // Escape regex special characters + item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); + item = new RegExp('^' + item + '$', 'i'); + return item.test(c); + }) !== -1); + }, false); +} + +function evaluateAuthorList(expr, author) { + var authorList = expr.split(',').map(function (v) { + return v.trim().toLocaleLowerCase(); + }); + + return _.includes(authorList, author.toLocaleLowerCase()); +} + +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; + authorOk, + numberOk, + indexOk; - function evaluateTagList(expr, tags) { - return expr.split(',').map(function (v) { - return v.trim(); - }).reduce(function (p, c) { - return p || (_.findIndex(tags, function (item) { - // Escape regex special characters - item = item.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'); - item = new RegExp('^' + item + '$', 'i'); - return item.test(c); - }) !== -1); - }, false); - } - - function evaluateAuthorList(expr, author) { - var authorList = expr.split(',').map(function (v) { - return v.trim().toLocaleLowerCase(); - }); - - return _.includes(authorList, author.toLocaleLowerCase()); - } - - if (!tagList && !authorList) { + if (!tagList && !authorList && !numberList && !indexList) { logging.warn(i18n.t('warnings.helpers.has.invalidAttribute')); return; } tagsOk = tagList && evaluateTagList(tagList, tags) || 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.inverse(this); diff --git a/core/test/unit/server_helpers/has_spec.js b/core/test/unit/server_helpers/has_spec.js index 9bae7c97f6..3d7c912599 100644 --- a/core/test/unit/server_helpers/has_spec.js +++ b/core/test/unit/server_helpers/has_spec.js @@ -7,176 +7,341 @@ var should = require('should'), // jshint ignore:line sandbox = sinon.sandbox.create(); describe('{{#has}} helper', function () { + var fn, inverse, thisCtx, handlebarsOptions; + afterEach(function () { sandbox.restore(); }); - it('should handle tag list that validates true', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + beforeEach(function () { + fn = sandbox.spy(); + inverse = sandbox.spy(); - helpers.has.call( - {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, - {hash: {tag: 'invalid, bar, wat'}, fn: fn, inverse: inverse} - ); + thisCtx = {}; - fn.called.should.be.true(); - inverse.called.should.be.false(); + // This object mocks out the object that handlebars helpers get passed + handlebarsOptions = { + hash: {}, + data: {}, + fn: fn, + inverse: inverse + }; }); - it('should handle tags with case-insensitivity', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + function callHasHelper(thisCtx, hash) { + // Hash is the options passed in + handlebarsOptions.hash = hash; + return helpers.has.call(thisCtx, handlebarsOptions); + } - helpers.has.call( - {tags: [{name: 'ghost'}]}, - {hash: {tag: 'GhoSt'}, fn: fn, inverse: inverse} - ); + describe('tag match', function () { + it('should handle tag list that validates true', function () { + thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; - fn.called.should.be.true(); - inverse.called.should.be.false(); + // {{#has tag="invalid, bar, wat"}} + callHasHelper(thisCtx, {tag: 'invalid, bar, wat'}); + + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should handle tags with case-insensitivity', function () { + thisCtx = {tags: [{name: 'ghost'}]}; + + // {{#has tag="GhoSt"}} + callHasHelper(thisCtx, {tag: 'GhoSt'}); + + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should match exact tags, not superstrings', function () { + thisCtx = {tags: [{name: 'magical'}]}; + + callHasHelper(thisCtx, {tag: 'magic'}); + + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); + + it('should match exact tags, not substrings', function () { + thisCtx = {tags: [{name: 'magic'}]}; + + callHasHelper(thisCtx, {tag: 'magical'}); + + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); + + it('should handle tag list that validates false', function () { + thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx, {tag: 'much, such, wow'}); + + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); + + it('should not do anything if there are no attributes', function () { + thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx); + + fn.called.should.be.false(); + inverse.called.should.be.false(); + }); + + it('should not do anything when an invalid attribute is given', function () { + thisCtx = {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx, {invalid: 'nonsense'}); + + fn.called.should.be.false(); + inverse.called.should.be.false(); + }); }); - it('should match exact tags, not superstrings', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + describe('author match', function () { + it('should handle author list that evaluates to true', function () { + thisCtx = {author: {name: 'sam'}}; - helpers.has.call( - {tags: [{name: 'magical'}]}, - {hash: {tag: 'magic'}, fn: fn, inverse: inverse} - ); + callHasHelper(thisCtx, {author: 'joe, sam, pat'}); - fn.called.should.be.false(); - inverse.called.should.be.true(); + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should handle author list that evaluates to false', function () { + thisCtx = {author: {name: 'jamie'}}; + + callHasHelper(thisCtx, {author: 'joe, sam, pat'}); + + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); + + it('should handle authors with case-insensitivity', function () { + thisCtx = {author: {name: 'Sam'}}; + + callHasHelper(thisCtx, {author: 'joe, sAm, pat'}); + + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should handle tags and authors like an OR query (pass)', function () { + thisCtx = {author: {name: 'sam'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'}); + + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should handle tags and authors like an OR query (pass)', function () { + thisCtx = {author: {name: 'sam'}, tags: [{name: 'much'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'}); + + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); + + it('should handle tags and authors like an OR query (fail)', function () { + thisCtx = {author: {name: 'fred'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}; + + callHasHelper(thisCtx, {author: 'joe, sam, pat', tag: 'much, such, wow'}); + + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); }); - it('should match exact tags, not substrings', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + describe('number match (1-based index)', function () { + it('will match on an exact number (pass)', function () { + handlebarsOptions.data = {number: 6}; - helpers.has.call( - {tags: [{name: 'magic'}]}, - {hash: {tag: 'magical'}, fn: fn, inverse: inverse} - ); + callHasHelper(thisCtx, {number: '6'}); - fn.called.should.be.false(); - inverse.called.should.be.true(); + 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); + }); }); - it('should handle tag list that validates false', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + describe('index match (0-based index)', function () { + it('will match on an exact index (pass)', function () { + handlebarsOptions.data = {index: 6}; - helpers.has.call( - {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, - {hash: {tag: 'much, such, wow'}, fn: fn, inverse: inverse} - ); + callHasHelper(thisCtx, {index: '6'}); - fn.called.should.be.false(); - inverse.called.should.be.true(); - }); + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); - it('should not do anything if there are no attributes', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + it('will match on an exact index (fail)', function () { + handlebarsOptions.data = {index: 5}; - helpers.has.call( - {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, - {fn: fn, inverse: inverse} - ); + callHasHelper(thisCtx, {index: '6'}); - fn.called.should.be.false(); - inverse.called.should.be.false(); - }); + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); - it('should not do anything when an invalid attribute is given', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + 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'}); + } - helpers.has.call( - {tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, - {hash: {invalid: 'nonsense'}, fn: fn, inverse: inverse} - ); + fn.calledOnce.should.be.true(); + inverse.called.should.be.true(); + inverse.callCount.should.eql(7); + }); - fn.called.should.be.false(); - inverse.called.should.be.false(); - }); + it('will match on an index list (pass)', function () { + handlebarsOptions.data = {index: 6}; - it('should handle author list that evaluates to true', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + callHasHelper(thisCtx, {index: '1, 3, 6,12'}); - helpers.has.call( - {author: {name: 'sam'}}, - {hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse} - ); + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); - fn.called.should.be.true(); - inverse.called.should.be.false(); - }); + it('will match on an index list (fail)', function () { + handlebarsOptions.data = {index: 5}; - it('should handle author list that evaluates to false', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + callHasHelper(thisCtx, {index: '1, 3, 6,12'}); - helpers.has.call( - {author: {name: 'jamie'}}, - {hash: {author: 'joe, sam, pat'}, fn: fn, inverse: inverse} - ); + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); - 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'}); + } - it('should handle authors with case-insensitivity', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + fn.called.should.be.true(); + fn.callCount.should.eql(3); + inverse.called.should.be.true(); + inverse.callCount.should.eql(5); + }); - helpers.has.call( - {author: {name: 'Sam'}}, - {hash: {author: 'joe, sAm, pat'}, fn: fn, inverse: inverse} - ); + it('will match on a nth pattern (pass)', function () { + handlebarsOptions.data = {index: 6}; - fn.called.should.be.true(); - inverse.called.should.be.false(); - }); + callHasHelper(thisCtx, {index: 'nth:3'}); - it('should handle tags and authors like an OR query (pass)', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + fn.called.should.be.true(); + inverse.called.should.be.false(); + }); - helpers.has.call( - {author: {name: 'sam'}, tags: [{name: 'foo'}, {name: 'bar'}, {name: 'baz'}]}, - {hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse} - ); + it('will match on a nth pattern (fail)', function () { + handlebarsOptions.data = {index: 5}; - fn.called.should.be.true(); - inverse.called.should.be.false(); - }); + callHasHelper(thisCtx, {index: 'nth:3'}); - it('should handle tags and authors like an OR query (pass)', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); + fn.called.should.be.false(); + inverse.called.should.be.true(); + }); - helpers.has.call( - {author: {name: 'sam'}, tags: [{name: 'much'}, {name: 'bar'}, {name: 'baz'}]}, - {hash: {author: 'joe, sam, pat', tag: 'much, such, wow'}, fn: fn, inverse: inverse} - ); + 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(); - inverse.called.should.be.false(); - }); - - it('should handle tags and authors like an OR query (fail)', function () { - var fn = sandbox.spy(), - inverse = sandbox.spy(); - - helpers.has.call( - {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(); - inverse.called.should.be.true(); + fn.called.should.be.true(); + fn.callCount.should.eql(3); + inverse.called.should.be.true(); + inverse.callCount.should.eql(5); + }); }); });