diff --git a/lib/search-query-parser.js b/lib/search-query-parser.js index 278b7fe..690eefc 100644 --- a/lib/search-query-parser.js +++ b/lib/search-query-parser.js @@ -31,6 +31,7 @@ exports.parse = function (string, options) { else { // Our object to store the query object var query = {text: []}; + var exclusion = {}; // Get a list of search terms respecting single and double quotes var terms = string.match(/(\S+:'(?:[^'\\]|\\.)*')|(\S+:"(?:[^"\\]|\\.)*")|\S+|\S+:\S+/g); for (var i = 0; i < terms.length; i++) { @@ -74,7 +75,18 @@ exports.parse = function (string, options) { var key = term.slice(0, sepIdx); // Check if the key is a registered keyword options.keywords = options.keywords || []; - var isKeyword = !(-1 === options.keywords.indexOf(key)); + var isKeyword = false; + var isExclusion = false; + if (!/^-/.test(key)) { + isKeyword = !(-1 === options.keywords.indexOf(key)); + } else if (key[0] === '-') { + var _key = key.slice(1); + isKeyword = !(-1 === options.keywords.indexOf(_key)) + if (isKeyword) { + key = _key; + isExclusion = true; + } + } // Check if the key is a registered range options.ranges = options.ranges || []; var isRange = !(-1 === options.ranges.indexOf(key)); @@ -85,42 +97,79 @@ exports.parse = function (string, options) { if (value.length) { // Get an array of values when several are there var values = value.split(','); - // If we already have seen that keyword... - if (query[key]) { - // ...many times... - if (query[key] instanceof Array) { + if (isExclusion) { + if (exclusion[key]) { + // ...many times... + if (exclusion[key] instanceof Array) { + // ...and got several values this time... + if (values.length > 1) { + // ... concatenate both arrays. + exclusion[key] = exclusion[key].concat(values); + } + else { + // ... append the current single value. + exclusion[key].push(value); + } + } + // We saw that keyword only once before + else { + // Put both the current value and the new + // value in an array + exclusion[key] = [exclusion[key]]; + exclusion[key].push(value); + } + } + // First time we see that keyword + else { // ...and got several values this time... if (values.length > 1) { - // ... concatenate both arrays. - query[key] = query[key].concat(values); + // ...add all values seen. + exclusion[key] = values; } + // Got only a single value this time else { - // ... append the current single value. + // Record its value as a string + exclusion[key] = value; + } + } + } else { + // If we already have seen that keyword... + if (query[key]) { + // ...many times... + if (query[key] instanceof Array) { + // ...and got several values this time... + if (values.length > 1) { + // ... concatenate both arrays. + query[key] = query[key].concat(values); + } + else { + // ... append the current single value. + query[key].push(value); + } + } + // We saw that keyword only once before + else { + // Put both the current value and the new + // value in an array + query[key] = [query[key]]; query[key].push(value); } } - // We saw that keyword only once before + // First time we see that keyword else { - // Put both the current value and the new - // value in an array - query[key] = [query[key]]; - query[key].push(value); + // ...and got several values this time... + if (values.length > 1) { + // ...add all values seen. + query[key] = values; + } + // Got only a single value this time + else { + // Record its value as a string + query[key] = value; + } } } - // First time we see that keyword - else { - // ...and got several values this time... - if (values.length > 1) { - // ...add all values seen. - query[key] = values; - } - // Got only a single value this time - else { - // Record its value as a string - query[key] = value; - } - } - } + } } // The key allows a range else if (isRange) { @@ -161,8 +210,8 @@ exports.parse = function (string, options) { } // Return forged query object + query.exclude = exclusion; return query; - } }; diff --git a/test/test.js b/test/test.js index 14385a6..529b97a 100644 --- a/test/test.js +++ b/test/test.js @@ -223,4 +223,39 @@ describe('Search query syntax parser', function () { }); + it('should parse a single keyword query in exclusion syntax', function() { + var searchQuery = '-from:jul@foo.com'; + var options = {keywords: ['from']}; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.exclude.should.be.an.Object; + parsedSearchQuery.exclude.should.have.property('from', 'jul@foo.com'); + parsedSearchQuery.should.not.have.property('text'); + }); + + it('should concatenate a keyword multiple values in exclusion syntax', function() { + var searchQuery = '-from:jul@foo.com,mar@foo.com'; + var options = {keywords: ['from']}; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.exclude.should.be.an.Object; + parsedSearchQuery.exclude.from.should.containEql('jul@foo.com'); + parsedSearchQuery.exclude.from.should.containEql('mar@foo.com'); + parsedSearchQuery.should.not.have.property('text'); + }); + + it('should support keywords which appear multiple times with exclusion syntax', function() { + var searchQuery = '-from:jul@foo.com,mar@foo.com -from:jan@foo.com'; + var options = {keywords: ['from']}; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.exclude.should.be.an.Object; + parsedSearchQuery.exclude.from.should.containEql('jul@foo.com'); + parsedSearchQuery.exclude.from.should.containEql('mar@foo.com'); + parsedSearchQuery.exclude.from.should.containEql('jan@foo.com'); + parsedSearchQuery.should.not.have.property('text'); + }); });