From 2167fac836ad9977e20a1327c830bc8af6133be0 Mon Sep 17 00:00:00 2001 From: yangchen Date: Tue, 27 Sep 2016 12:17:11 +0800 Subject: [PATCH 1/2] add exclusion syntax --- lib/search-query-parser.js | 105 +++++++++++++++++++++++++++---------- test/test.js | 35 +++++++++++++ 2 files changed, 112 insertions(+), 28 deletions(-) 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'); + }); }); From b0f164fa2b30fb19366170bc522d2dde83d30a3a Mon Sep 17 00:00:00 2001 From: yangchen Date: Wed, 28 Sep 2016 10:53:06 +0800 Subject: [PATCH 2/2] add exclusion syntax examples in README --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fbf22ff..46c4fc8 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ var searchQueryObj = searchQuery.parse(query, options); You can configure what keywords and ranges the parser should accept with the options argument. It accepts 2 values: - * `keywords`, that can be separated by commas (,) - * `ranges`, that can be separated by a hyphen (-) +* `keywords`, that can be separated by commas (,) +* `ranges`, that can be separated by a hyphen (-) Both values take an array of strings, as in the example just above. @@ -67,9 +67,20 @@ var parsedQueryWithOptions = searchQuery.parse(query, options); // parsedQueryWithOptions is now 'a query with just text' ``` +You can also use exclusion syntax, like `-from:sep@foobar.io name:hello,world` . And it will return : + +```javascript +{ + name: ['hello', 'world'], + exclusion: { + from: ['sep@foobar.io'] + } +} +``` + ## Testing -The 17 tests are written using the BDD testing framework should.js, and run with mocha. +The 20 tests are written using the BDD testing framework should.js, and run with mocha. Run `npm install should` and `npm install -g mocha` to install them both.