|
|
|
const addWeightWithBoost = (option, query) => {
|
|
|
|
option.weight = calculateWeight(option.clean, query) * option.clean.boost;
|
|
|
|
|
|
|
|
return option;
|
|
|
|
};
|
|
|
|
|
|
|
|
const clean = (text) =>
|
|
|
|
text
|
|
|
|
.trim()
|
|
|
|
.replace(/['’]/g, "")
|
|
|
|
.replace(/[.,"/#!$%^&*;:{}=\-_`~()]/g, " ")
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
|
|
const cleanseOption = (option) => {
|
|
|
|
option.clean = {
|
|
|
|
name: clean(option.name),
|
|
|
|
nameWithoutStopWords: removeStopWords(option.name),
|
|
|
|
boost: option.boost || 1,
|
|
|
|
};
|
|
|
|
|
|
|
|
return option;
|
|
|
|
};
|
|
|
|
|
|
|
|
const hasWeight = (option) => option.weight > 0;
|
|
|
|
|
|
|
|
const byWeightThenAlphabetically = (a, b) => {
|
|
|
|
if (a.weight > b.weight) return -1;
|
|
|
|
if (a.weight < b.weight) return 1;
|
|
|
|
if (a.name < b.name) return -1;
|
|
|
|
if (a.name > b.name) return 1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
const optionName = (option) => option.name;
|
|
|
|
const exactMatch = (word, query) => word === query;
|
|
|
|
|
|
|
|
const startsWithRegExp = (query) => new RegExp("\\b" + query, "i");
|
|
|
|
const startsWith = (word, query) => word.search(startsWithRegExp(query)) === 0;
|
|
|
|
|
|
|
|
const wordsStartsWithQuery = (word, regExps) =>
|
|
|
|
regExps.every((regExp) => word.search(regExp) >= 0);
|
|
|
|
|
|
|
|
const calculateWeight = ({ name, nameWithoutStopWords }, query) => {
|
|
|
|
const queryWithoutStopWords = removeStopWords(query);
|
|
|
|
|
|
|
|
if (exactMatch(name, query)) return 100;
|
|
|
|
if (exactMatch(nameWithoutStopWords, queryWithoutStopWords)) return 95;
|
|
|
|
|
|
|
|
if (startsWith(name, query)) return 60;
|
|
|
|
if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55;
|
|
|
|
const startsWithRegExps = queryWithoutStopWords
|
|
|
|
.split(/\s+/)
|
|
|
|
.map(startsWithRegExp);
|
|
|
|
|
|
|
|
if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
const stopWords = ["the", "of", "in", "and", "at", "&"];
|
|
|
|
|
|
|
|
const removeStopWords = (text) => {
|
|
|
|
const isAllStopWords = text
|
|
|
|
.trim()
|
|
|
|
.split(" ")
|
|
|
|
.every((word) => stopWords.includes(word));
|
|
|
|
|
|
|
|
if (isAllStopWords) {
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
const regex = new RegExp(
|
|
|
|
stopWords.map((word) => `(\\s+)?${word}(\\s+)?`).join("|"),
|
|
|
|
"gi"
|
|
|
|
);
|
|
|
|
return text.replace(regex, " ").trim();
|
|
|
|
};
|
|
|
|
|
|
|
|
export const sort = (query, options) => {
|
|
|
|
const cleanQuery = clean(query);
|
|
|
|
|
|
|
|
return options
|
|
|
|
.map(cleanseOption)
|
|
|
|
.map((option) => addWeightWithBoost(option, cleanQuery))
|
|
|
|
.filter(hasWeight)
|
|
|
|
.sort(byWeightThenAlphabetically)
|
|
|
|
.map(optionName);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const suggestion = (value, options) => {
|
|
|
|
const option = options.find((o) => o.name === value);
|
|
|
|
if (option) {
|
|
|
|
const html = `<span>${value}</span>`;
|
|
|
|
return html;
|
|
|
|
} else {
|
|
|
|
return `<span>No results found</span>`;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const enhanceOption = (option) => {
|
|
|
|
return {
|
|
|
|
name: option.label,
|
|
|
|
boost: parseFloat(option.getAttribute("data-boost")) || 1,
|
|
|
|
};
|
|
|
|
};
|