From 7fcd56fbea6c4d1c96748817eaf7dd64873023e7 Mon Sep 17 00:00:00 2001 From: Ivan ROGER Date: Thu, 9 Mar 2023 10:47:30 +0100 Subject: [PATCH 1/2] Add option to customize negate prefix --- README.md | 3 ++- index.d.ts | 1 + lib/search-query-parser.js | 33 +++++++++++++++++++-------------- test/test.js | 27 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index afda2ad..f3e187b 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,13 @@ var searchQueryObj = searchQuery.parse(query, options); ``` You can configure what keywords and ranges the parser should accept with the options argument. -It accepts 5 values: +It accepts 6 values: * `keywords`, that can be separated by commas (,). Accepts an array of strings. * `ranges`, that can be separated by a hyphen (-). Accepts an array of strings. * `tokenize`, that controls the behaviour of text search terms. If set to `true`, non-keyword text terms are returned as an array of strings where each term in the array is a whitespace-separated word, or a multi-word term surrounded by single- or double-quotes. * `alwaysArray`, a boolean that controls the behaviour of the returned query. If set to `true`, all matched keywords would always be arrays instead of strings. If set to `false` they will be strings if matched a single value. Defaults to `false`. * `offsets`, a boolean that controls the behaviour of the returned query. If set to `true`, the query will contain the offsets object. If set to `false`, the query will not contain the offsets object. Defaults to `true`. +* `negatePrefix`, a string that controls the behaviour of the returned query. It defines what prefix is used to mark a term as excluded. Defaults to `'-'`. If no keywords or ranges are specified, or if none are present in the given search query, then `searchQuery.parse` will return a string if `tokenize` is false, or an array of strings under the key `text` if `tokenize` is true. diff --git a/index.d.ts b/index.d.ts index 180b9d8..6d95cb9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,6 +10,7 @@ export interface SearchParserOptions { keywords?: string[]; ranges?: string[]; alwaysArray?: boolean; + negatePrefix?: string; } export interface ISearchParserDictionary { diff --git a/lib/search-query-parser.js b/lib/search-query-parser.js index 9bcea7d..ef1f6a7 100644 --- a/lib/search-query-parser.js +++ b/lib/search-query-parser.js @@ -8,10 +8,12 @@ exports.parse = function (string, options) { // Set a default options object when none is provided if (!options) { - options = {offsets: true}; + options = {offsets: true, negatePrefix: '-'}; } else { // If options offsets was't passed, set it to true options.offsets = (typeof options.offsets === 'undefined' ? true : options.offsets) + // If options negatePrefix was't passed, set it to '-' + options.negatePrefix = (typeof options.negatePrefix === 'undefined' ? '-' : options.negatePrefix) } if (!string) { @@ -69,9 +71,9 @@ exports.parse = function (string, options) { }); } else { var isExcludedTerm = false; - if (term[0] === '-') { + if (term.startsWith(options.negatePrefix)) { isExcludedTerm = true; - term = term.slice(1); + term = term.slice(options.negatePrefix.length); } // Strip surrounding quotes @@ -132,15 +134,15 @@ exports.parse = function (string, options) { options.keywords = options.keywords || []; 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; - } + if (key.startsWith(options.negatePrefix)) { + var _key = key.slice(options.negatePrefix.length); + isKeyword = !(-1 === options.keywords.indexOf(_key)) + if (isKeyword) { + key = _key; + isExclusion = true; + } + } else { + isKeyword = !(-1 === options.keywords.indexOf(key)); } // Check if the key is a registered range @@ -314,7 +316,10 @@ exports.stringify = function (queryObject, options, prefix) { // Set a default options object when none is provided if (!options) { - options = {offsets: true}; + options = {offsets: true, negatePrefix: '-'}; + } else { + // If options negatePrefix was't passed, set it to '-' + options.negatePrefix = (typeof options.negatePrefix === 'undefined' ? '-' : options.negatePrefix) } // If the query object is falsy we can just return an empty string @@ -414,7 +419,7 @@ exports.stringify = function (queryObject, options, prefix) { // Exclude if (queryObject.exclude) { if (Object.keys(queryObject.exclude).length > 0) { - parts.push(exports.stringify(queryObject.exclude, options, '-')); + parts.push(exports.stringify(queryObject.exclude, options, options.negatePrefix)); } } diff --git a/test/test.js b/test/test.js index 11a9085..8f3653d 100644 --- a/test/test.js +++ b/test/test.js @@ -73,6 +73,21 @@ describe('Search query syntax parser', function () { parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); }); + it('should return a tokenized string with custom negation prefix', function () { + var searchQuery = "fancy !pyjama !wear"; + var options = { tokenize: true, negatePrefix: '!' }; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.should.have.property('text', ['fancy']); + parsedSearchQuery.should.have.property('exclude', {text: ['pyjama', 'wear']}); + + var parsedAfterStringifySearchQuery = searchquery.parse(searchquery.stringify(parsedSearchQuery, options), options); + parsedAfterStringifySearchQuery.offsets = undefined; + parsedSearchQuery.offsets = undefined; + parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); + }); + it('should return a tokenized string with negation of single-quoted terms', function () { var searchQuery = "fancy -'pyjama -wear'"; var options = { tokenize: true }; @@ -788,4 +803,16 @@ describe('Search query syntax parser', function () { parsedSearchQuery.offsets = undefined; parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); }); + + it('should stringify properly with default negate prefix', function () { + var searchQueryObject = { + text: [ 'fancy' ], + exclude: { text: [ 'pyjama', 'wear' ] } + }; + var options = { tokenize: true }; + stringifiedSearchQuery = searchquery.stringify(searchQueryObject, options); + + stringifiedSearchQuery.should.be.a.string; + stringifiedSearchQuery.should.be.eql('fancy -pyjama -wear'); + }); }); From a28918e80ea6ef6e3ed6144b774ae64c9a20750b Mon Sep 17 00:00:00 2001 From: Ivan ROGER Date: Thu, 9 Mar 2023 16:21:23 +0100 Subject: [PATCH 2/2] Remove zero-length terms Fix term check on empty text --- lib/search-query-parser.js | 10 +++++++--- test/test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/search-query-parser.js b/lib/search-query-parser.js index ef1f6a7..af93598 100644 --- a/lib/search-query-parser.js +++ b/lib/search-query-parser.js @@ -45,8 +45,7 @@ exports.parse = function (string, options) { var term = match[0]; var sepIndex = term.indexOf(':'); if (sepIndex !== -1) { - var split = term.split(':'), - key = term.slice(0, sepIndex), + var key = term.slice(0, sepIndex), val = term.slice(sepIndex + 1); // Strip surrounding quotes val = val.replace(/^\"|\"$|^\'|\'$/g, ''); @@ -92,6 +91,11 @@ exports.parse = function (string, options) { } }); + if (term.length === 0) { + // Ignore empty strings after cleanup + continue + } + if (isExcludedTerm) { if (exclusion['text']) { if (exclusion['text'] instanceof Array) { @@ -119,7 +123,7 @@ exports.parse = function (string, options) { var term; while (term = terms.pop()) { // When just a simple term - if (term.text) { + if (term.text !== undefined) { // We add it as pure text query.text.push(term.text); // When offsets is true, push a new offset diff --git a/test/test.js b/test/test.js index 8f3653d..128df25 100644 --- a/test/test.js +++ b/test/test.js @@ -118,6 +118,35 @@ describe('Search query syntax parser', function () { parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); }); + it('should return a tokenized string without empty text terms', function () { + var searchQuery = "fancy pyjama wear ''"; + var options = { tokenize: true }; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.should.have.property('text', ['fancy', 'pyjama', 'wear']); + + var parsedAfterStringifySearchQuery = searchquery.parse(searchquery.stringify(parsedSearchQuery, options), options); + parsedAfterStringifySearchQuery.offsets = undefined; + parsedSearchQuery.offsets = undefined; + parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); + }); + + it('should return a simple string without empty text terms', function () { + var searchQuery = "key:value fancy pyjama wear ''"; + var options = { keywords: ['key'] }; + var parsedSearchQuery = searchquery.parse(searchQuery, options); + + parsedSearchQuery.should.be.an.Object; + parsedSearchQuery.should.have.property('text', 'fancy pyjama wear'); + parsedSearchQuery.should.have.property('key', 'value'); + + var parsedAfterStringifySearchQuery = searchquery.parse(searchquery.stringify(parsedSearchQuery, options), options); + parsedAfterStringifySearchQuery.offsets = undefined; + parsedSearchQuery.offsets = undefined; + parsedAfterStringifySearchQuery.should.be.eql(parsedSearchQuery); + }); + it('should parse a single keyword with no text', function () { var searchQuery = 'from:jul@foo.com'; var options = {keywords: ['from']};