advanced-search-query
Version:
Another parser for advanced search query syntax.
341 lines • 11.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdvancedSearchQuery = void 0;
const lodash_groupby_1 = __importDefault(require("lodash.groupby"));
const utils_1 = require("./utils");
const RESET = 'RESET';
const IN_OPERAND = 'IN_OPERAND';
const IN_TEXT = 'IN_TEXT';
const SINGLE_QUOTE = 'SINGLE_QUOTE';
const DOUBLE_QUOTE = 'DOUBLE_QUOTE';
class AdvancedSearchQuery {
/**
* Not intended for public use. API could change.
*/
constructor(keywords, texts) {
this.keywords = keywords;
this.texts = texts;
this.input = '';
this.isDirty = true;
}
getText() {
return this.texts
? this.texts
.map(({ value, isNegated }) => (isNegated ? `-${value}` : value))
.join(' ')
: '';
}
getTexts() {
return this.texts;
}
getKeywords() {
const keywords = lodash_groupby_1.default(this.keywords, 'name');
return Object.entries(keywords)
.map(([name, keywords]) => {
return [
name,
keywords.map((keyword) => {
return Object.assign(Object.assign({}, keyword), { name: undefined });
}),
];
})
.reduce((cumulatedKeywords, [name, keywords]) => {
return Object.assign(Object.assign({}, cumulatedKeywords), { [name]: keywords });
}, {});
}
getKeyword(name) {
const keyword = this.getKeywords()[name];
if (!keyword) {
return [];
}
return keyword;
}
toObject() {
const parsedQuery = {
text: {
include: this.getTexts()
.filter((text) => {
return text.isNegated === false;
})
.map((text) => {
return text.value;
}),
exclude: this.getTexts()
.filter((text) => {
return text.isNegated === true;
})
.map((text) => {
return text.value;
}),
},
keywords: {},
};
this.keywords.forEach((keyword) => {
parsedQuery.keywords[keyword.name] = parsedQuery.keywords[keyword.name] || {
include: [],
exclude: [],
};
if (keyword.isNegated) {
parsedQuery.keywords[keyword.name].exclude.push(keyword.value);
}
else {
parsedQuery.keywords[keyword.name].include.push(keyword.value);
}
});
return parsedQuery;
}
addKeyword(name, value, isNegated = false) {
this.keywords.push({
name,
value,
isNegated,
});
this.isDirty = true;
return this;
}
removeKeywords() {
this.keywords = [];
this.isDirty = true;
return this;
}
removeKeyword(name, value, isNegated) {
const keywords = this.keywords.filter((keyword) => {
if (name !== keyword.name) {
return true;
}
if (typeof value !== 'undefined' && value !== keyword.value) {
return true;
}
if (typeof isNegated !== 'undefined' && isNegated !== keyword.isNegated) {
return true;
}
return false;
});
this.isDirty = keywords.length !== this.keywords.length;
this.keywords = keywords;
return this;
}
addText(value, isNegated = false) {
this.texts.push({
value,
isNegated,
});
this.isDirty = true;
return this;
}
removeText(value, isNegated) {
const texts = this.texts.filter((text) => {
if (value !== text.value) {
return true;
}
if (typeof isNegated !== 'undefined' && isNegated !== text.isNegated) {
return true;
}
return false;
});
this.isDirty = texts.length !== this.texts.length;
this.texts = texts;
return this;
}
removeTexts() {
this.texts = [];
this.isDirty = true;
return this;
}
clone() {
return new AdvancedSearchQuery(this.keywords.slice(0), this.texts.slice(0));
}
toString() {
if (!this.isDirty && this.output) {
return this.output;
}
// Group keyword, isNegated pairs as keys
const conditionGroups = {};
this.keywords.forEach(({ name, value, isNegated }) => {
const isNegatedPrefix = isNegated ? '-' : '';
const key = `${isNegatedPrefix}${name}`;
conditionGroups[key] = conditionGroups[key] || [];
conditionGroups[key].push(value);
});
// Build condition
let condition = '';
Object.keys(conditionGroups).forEach((conditionGroupKey) => {
const values = conditionGroups[conditionGroupKey];
const safeValues = values.filter(Boolean).map((value) => {
let newV = '';
let shouldQuote = false;
for (let i = 0; i < value.length; i++) {
const char = value[i];
if (char === '"') {
newV += '\\"';
}
else {
if (char === ' ' || char === ',') {
shouldQuote = true;
}
newV += char;
}
}
return shouldQuote ? `"${newV}"` : newV;
});
if (safeValues.length > 0) {
condition += ` ${conditionGroupKey}:${safeValues.join(',')}`;
}
});
this.output = `${condition} ${this.getText()}`.trim();
this.isDirty = false;
return this.output;
}
}
exports.AdvancedSearchQuery = AdvancedSearchQuery;
/**
* @param {String} string to parse e.g. 'to:me -from:joe@acme.com foobar'.
* @param {Array} transformTextToConditions Array of functions to transform text into conditions
* @returns {AdvancedSearchQuery} An instance of class AdvancedSearchQuery.
*/
function default_1(input = '', transformTextToKeywords = []) {
const keywords = [];
const texts = [];
const addCondition = (name, value, isNegated) => {
keywords.push({
name,
value,
isNegated,
});
};
const addTextSegment = (value, isNegated) => {
let hasTransform = false;
transformTextToKeywords.forEach((transform) => {
const transformed = transform(value);
if (transformed) {
const { name, value } = transformed;
if (name && value) {
addCondition(name, value, isNegated);
hasTransform = true;
}
}
});
if (!hasTransform) {
texts.push({ value, isNegated });
}
};
let state = RESET;
let currentOperand = '';
let isNegated = false;
let currentText = '';
let quoteState;
let prevChar = '';
const performReset = () => {
state = RESET;
quoteState = RESET;
currentOperand = '';
currentText = '';
isNegated = false;
prevChar = '';
};
// Terminology, in this example: 'to:joe@acme.com'
// 'to' is the operator
// 'joe@acme.com' is the operand
// 'to:joe@acme.com' is the condition
// Possible states:
const inText = () => state === IN_TEXT; // could be inside raw text or operator
const inOperand = () => state === IN_OPERAND;
const inSingleQuote = () => quoteState === SINGLE_QUOTE;
const inDoubleQuote = () => quoteState === DOUBLE_QUOTE;
const inQuote = () => inSingleQuote() || inDoubleQuote();
performReset();
const quotePairMap = utils_1.getQuotePairMap(input);
for (let i = 0; i < input.length; i++) {
const char = input[i];
if (char === ' ') {
if (inOperand()) {
if (inQuote()) {
currentOperand += char;
}
else {
addCondition(currentText, currentOperand, isNegated);
performReset();
}
}
else if (inText()) {
if (inQuote()) {
currentText += char;
}
else {
addTextSegment(currentText, isNegated);
performReset();
}
}
}
else if (char === ',' && inOperand() && !inQuote()) {
addCondition(currentText, currentOperand, isNegated);
// No reset here because we are still evaluating operands for the same operator
currentOperand = '';
}
else if (char === '-' && !inOperand() && !inText()) {
isNegated = true;
}
else if (char === ':' && !inQuote()) {
if (inOperand()) {
// If we're in an operand, just push the string on.
currentOperand += char;
}
else if (inText()) {
// Skip this char, move states into IN_OPERAND,
state = IN_OPERAND;
}
}
else if (char === '"' && prevChar !== '\\' && !inSingleQuote()) {
if (inDoubleQuote()) {
quoteState = RESET;
}
else if (quotePairMap.double[i]) {
quoteState = DOUBLE_QUOTE;
}
else if (inOperand()) {
currentOperand += char;
}
else {
currentText += char;
}
}
else if (char === "'" && prevChar !== '\\' && !inDoubleQuote()) {
if (inSingleQuote()) {
quoteState = RESET;
}
else if (quotePairMap.single[i]) {
quoteState = SINGLE_QUOTE;
}
else if (inOperand()) {
currentOperand += char;
}
else {
currentText += char;
}
}
else if (char !== '\\') {
// Regular character..
if (inOperand()) {
currentOperand += char;
}
else {
currentText += char;
state = IN_TEXT;
}
}
prevChar = char;
}
// End of string, add any last entries
if (inText()) {
addTextSegment(currentText, isNegated);
}
else if (inOperand()) {
addCondition(currentText, currentOperand, isNegated);
}
return new AdvancedSearchQuery(keywords, texts);
}
exports.default = default_1;
//# sourceMappingURL=index.js.map