diff --git a/README.md b/README.md index 5e4d8ff..a2b8884 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,23 @@ var parsedQueryWithOptions = searchQuery.parse(query, options); // } ``` -The offsets object could become pretty huge with long search queries which could be an unnecessary use of space if no functionality depends on it. It can simply be turned off using the option `offsets: false` +The offsets object could become pretty huge with long search queries which could be an unnecessary use of space if no functionality depends on it. It can simply be turned off using the option `offsets: false`. + +Anytime, you can go back and stringify the parsed search query. This could be handy if you would like to manipulate the parsed search query object. + +```javascript +var searchQuery = require('search-query-parser'); + +var query = 'from:hi@retrace.io,foo@gmail.com to:me subject:vacations date:1/10/2013-15/04/2014 photos'; +var options = {keywords: ['from', 'to', 'subject'], ranges: ['date']} + +var searchQueryObj = searchQuery.parse(query, options); + +searchQueryObj.to = 'you'; +var newQuery = searchQuery.stringify(query, options); + +// newQuery is now: photos from:hi@retrace.io,foo@gmail.com to:you subject:vacations date:1/10/2013-15/04/2014 +``` ## Typescript @@ -152,3 +168,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/index.d.ts b/index.d.ts index 3430a5f..9fdff57 100644 --- a/index.d.ts +++ b/index.d.ts @@ -29,4 +29,12 @@ export interface SearchParserResult extends ISearchParserDictionary { exclude?: ISearchParserDictionary; } -export function parse(string: string, options?: SearchParserOptions): string | SearchParserResult; +export function parse( + string: string, + options?: SearchParserOptions +): string | SearchParserResult; + +export function stringify( + searchParserResult: string | SearchParserResult, + options?: SearchParserOptions +): string; diff --git a/index.js b/index.js index d9697ee..6dccb4e 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./lib/search-query-parser'); +module.exports = require("./lib/search-query-parser"); diff --git a/lib/search-query-parser.js b/lib/search-query-parser.js index 8e9fc80..75b2437 100644 --- a/lib/search-query-parser.js +++ b/lib/search-query-parser.js @@ -5,31 +5,31 @@ */ exports.parse = function (string, options) { - // Set a default options object when none is provided if (!options) { - options = {offsets: true}; + options = { offsets: true }; } else { // If options offsets was't passed, set it to true - options.offsets = (typeof options.offsets === 'undefined' ? true : options.offsets) + options.offsets = + typeof options.offsets === "undefined" ? true : options.offsets; } if (!string) { - string = ''; + string = ""; } // When a simple string, return it - if (-1 === string.indexOf(':') && !options.tokenize) { + if (-1 === string.indexOf(":") && !options.tokenize) { return string; } // When no keywords or ranges set, treat as a simple string - else if (!options.keywords && !options.ranges && !options.tokenize){ + else if (!options.keywords && !options.ranges && !options.tokenize) { return string; } // Otherwise parse the advanced query syntax else { // Our object to store the query object - var query = {text: []}; + var query = { text: [] }; // When offsets is true, create their array if (options.offsets) { query.offsets = []; @@ -41,72 +41,72 @@ exports.parse = function (string, options) { var match; while ((match = regex.exec(string)) !== null) { var term = match[0]; - var sepIndex = term.indexOf(':'); + var sepIndex = term.indexOf(":"); if (sepIndex !== -1) { - var split = term.split(':'), - key = term.slice(0, sepIndex), - val = term.slice(sepIndex + 1); + var split = term.split(":"), + key = term.slice(0, sepIndex), + val = term.slice(sepIndex + 1); // Strip surrounding quotes - val = val.replace(/^\"|\"$|^\'|\'$/g, ''); + val = val.replace(/^\"|\"$|^\'|\'$/g, ""); // Strip backslashes respecting escapes - val = (val + '').replace(/\\(.?)/g, function (s, n1) { + val = (val + "").replace(/\\(.?)/g, function (s, n1) { switch (n1) { - case '\\': - return '\\'; - case '0': - return '\u0000'; - case '': - return ''; - default: - return n1; + case "\\": + return "\\"; + case "0": + return "\u0000"; + case "": + return ""; + default: + return n1; } }); terms.push({ keyword: key, value: val, offsetStart: match.index, - offsetEnd: match.index + term.length + offsetEnd: match.index + term.length, }); } else { var isExcludedTerm = false; - if (term[0] === '-') { + if (term[0] === "-") { isExcludedTerm = true; term = term.slice(1); } // Strip surrounding quotes - term = term.replace(/^\"|\"$|^\'|\'$/g, ''); + term = term.replace(/^\"|\"$|^\'|\'$/g, ""); // Strip backslashes respecting escapes - term = (term + '').replace(/\\(.?)/g, function (s, n1) { + term = (term + "").replace(/\\(.?)/g, function (s, n1) { switch (n1) { - case '\\': - return '\\'; - case '0': - return '\u0000'; - case '': - return ''; - default: - return n1; + case "\\": + return "\\"; + case "0": + return "\u0000"; + case "": + return ""; + default: + return n1; } }); if (isExcludedTerm) { - if (exclusion['text']) { - if (exclusion['text'] instanceof Array) { - exclusion['text'].push(term); + if (exclusion["text"]) { + if (exclusion["text"] instanceof Array) { + exclusion["text"].push(term); } else { - exclusion['text'] = [exclusion['text']]; - exclusion['text'].push(term); + exclusion["text"] = [exclusion["text"]]; + exclusion["text"].push(term); } } else { // First time seeing an excluded text term - exclusion['text'] = term; + exclusion["text"] = term; } } else { terms.push({ text: term, offsetStart: match.index, - offsetEnd: match.index + term.length + offsetEnd: match.index + term.length, }); } } @@ -115,7 +115,7 @@ exports.parse = function (string, options) { terms.reverse(); // For each search term var term; - while (term = terms.pop()) { + while ((term = terms.pop())) { // When just a simple term if (term.text) { // We add it as pure text @@ -133,14 +133,14 @@ exports.parse = function (string, options) { 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; - } + 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 @@ -153,8 +153,10 @@ exports.parse = function (string, options) { query.offsets.push({ keyword: key, value: term.value, - offsetStart: isExclusion ? term.offsetStart + 1 : term.offsetStart, - offsetEnd: term.offsetEnd + offsetStart: isExclusion + ? term.offsetStart + 1 + : term.offsetStart, + offsetEnd: term.offsetEnd, }); } @@ -162,7 +164,7 @@ exports.parse = function (string, options) { // When value is a thing if (value.length) { // Get an array of values when several are there - var values = value.split(','); + var values = value.split(","); if (isExclusion) { if (exclusion[key]) { // ...many times... @@ -171,8 +173,7 @@ exports.parse = function (string, options) { if (values.length > 1) { // ... concatenate both arrays. exclusion[key] = exclusion[key].concat(values); - } - else { + } else { // ... append the current single value. exclusion[key].push(value); } @@ -213,8 +214,7 @@ exports.parse = function (string, options) { if (values.length > 1) { // ... concatenate both arrays. query[key] = query[key].concat(values); - } - else { + } else { // ... append the current single value. query[key].push(value); } @@ -257,7 +257,7 @@ exports.parse = function (string, options) { var value = term.value; // Range are separated with a dash - var rangeValues = value.split('-'); + var rangeValues = value.split("-"); // When both end of the range are specified // keyword:XXXX-YYYY query[key] = {}; @@ -274,10 +274,9 @@ exports.parse = function (string, options) { else { query[key].from = value; } - } - else { + } else { // We add it as pure text - var text = term.keyword + ':' + term.value; + var text = term.keyword + ":" + term.value; query.text.push(text); // When offsets is true, push a new offset @@ -285,7 +284,7 @@ exports.parse = function (string, options) { query.offsets.push({ text: text, offsetStart: term.offsetStart, - offsetEnd: term.offsetEnd + offsetEnd: term.offsetEnd, }); } } @@ -295,7 +294,7 @@ exports.parse = function (string, options) { // Concatenate all text terms if any if (query.text.length) { if (!options.tokenize) { - query.text = query.text.join(' ').trim(); + query.text = query.text.join(" ").trim(); } } // Just remove the attribute text when it's empty @@ -307,5 +306,120 @@ exports.parse = function (string, options) { query.exclude = exclusion; return query; } - +}; + +exports.stringify = function (queryObject, options, prefix) { + // Set a default options object when none is provided + if (!options) { + options = { offsets: true }; + } + + // If the query object is falsy we can just return an empty string + if (!queryObject) { + return ""; + } + + // If the query object is already a string, we can return it immediately + if (typeof queryObject === "string") { + return queryObject; + } + + // If the query object is an array, we can return it concatenated with a space + if (Array.isArray(queryObject)) { + return queryObject.join(" "); + } + + // If the query object does not have any keys, we can return an empty string + if (!Object.keys(queryObject).length) { + return ""; + } + + // If the query object contains only text which is a string, we can return it immediately + if ( + Object.keys(queryObject).length === 3 && + !!queryObject.text && + !!queryObject.offsets && + !!queryObject.exclude && + typeof queryObject.text === "string" + ) { + return queryObject.text; + } + + // We will use a prefix for the exclude syntax later one + if (!prefix) { + prefix = ""; + } + + // Helpers + var addQuotes = function (string) { + return string.indexOf(" ") > -1 ? JSON.stringify(string) : string; + }; + var addPrefix = function (string) { + return prefix + string; + }; + + // Keep track of all single parts in this array + var parts = []; + + // Text + if (queryObject.text) { + var value = []; + if (typeof queryObject.text === "string") { + value.push(queryObject.text); + } else { + value.push.apply(value, queryObject.text); + } + + if (value.length > 0) { + parts.push(value.map(addQuotes).map(addPrefix).join(" ")); + } + } + + // Keywords + if (options.keywords) { + options.keywords.forEach(function (keyword) { + if (!queryObject[keyword]) { + return; + } + + var value = []; + if (typeof queryObject[keyword] === "string") { + value.push(queryObject[keyword]); + } else { + value.push.apply(value, queryObject[keyword]); + } + + if (value.length > 0) { + parts.push(addPrefix(keyword + ":" + value.map(addQuotes).join(","))); + } + }); + } + + // Ranges + if (options.ranges) { + options.ranges.forEach(function (range) { + if (!queryObject[range]) { + return; + } + + var value = queryObject[range].from; + var to = queryObject[range].to; + if (to) { + value = value + "-" + to; + } + + if (value) { + parts.push(addPrefix(range + ":" + value)); + } + }); + } + + // Exclude + if (queryObject.exclude) { + if (Object.keys(queryObject.exclude).length > 0) { + parts.push(exports.stringify(queryObject.exclude, options, "-")); + } + } + + return parts.join(" "); }; diff --git a/test/index.html b/test/index.html index 0590d4a..bbcb6ac 100644 --- a/test/index.html +++ b/test/index.html @@ -2,14 +2,16 @@