|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS002: Fix invalid constructor
|
|
|
|
* DS101: Remove unnecessary use of Array.from
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* DS104: Avoid inline assignments
|
|
|
|
* DS202: Simplify dynamic range loops
|
|
|
|
* DS206: Consider reworking classes to avoid initClass
|
|
|
|
* DS207: Consider shorter variations of null checks
|
|
|
|
* DS209: Avoid top-level return
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
|
|
|
*/
|
|
|
|
//
|
|
|
|
// Match functions
|
|
|
|
//
|
|
|
|
|
|
|
|
let fuzzyRegexp, i, index, lastIndex, match, matcher, matchIndex, matchLength, queryLength, score, separators, value, valueLength;
|
|
|
|
const SEPARATOR = '.';
|
|
|
|
|
|
|
|
let query =
|
|
|
|
(queryLength =
|
|
|
|
(value =
|
|
|
|
(valueLength =
|
|
|
|
(matcher = // current match function
|
|
|
|
(fuzzyRegexp = // query fuzzy regexp
|
|
|
|
(index = // position of the query in the string being matched
|
|
|
|
(lastIndex = // last position of the query in the string being matched
|
|
|
|
(match = // regexp match data
|
|
|
|
(matchIndex =
|
|
|
|
(matchLength =
|
|
|
|
(score = // score for the current match
|
|
|
|
(separators = // counter
|
|
|
|
(i = null))))))))))))); // cursor
|
|
|
|
|
|
|
|
function exactMatch() {
|
|
|
|
index = value.indexOf(query);
|
|
|
|
if (!(index >= 0)) { return; }
|
|
|
|
|
|
|
|
lastIndex = value.lastIndexOf(query);
|
|
|
|
|
|
|
|
if (index !== lastIndex) {
|
|
|
|
return Math.max(scoreExactMatch(), ((index = lastIndex) && scoreExactMatch()) || 0);
|
|
|
|
} else {
|
|
|
|
return scoreExactMatch();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function scoreExactMatch() {
|
|
|
|
// Remove one point for each unmatched character.
|
|
|
|
score = 100 - (valueLength - queryLength);
|
|
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
// If the character preceding the query is a dot, assign the same score
|
|
|
|
// as if the query was found at the beginning of the string, minus one.
|
|
|
|
if (value.charAt(index - 1) === SEPARATOR) {
|
|
|
|
score += index - 1;
|
|
|
|
// Don't match a single-character query unless it's found at the beginning
|
|
|
|
// of the string or is preceded by a dot.
|
|
|
|
} else if (queryLength === 1) {
|
|
|
|
return;
|
|
|
|
// (1) Remove one point for each unmatched character up to the nearest
|
|
|
|
// preceding dot or the beginning of the string.
|
|
|
|
// (2) Remove one point for each unmatched character following the query.
|
|
|
|
} else {
|
|
|
|
i = index - 2;
|
|
|
|
while ((i >= 0) && (value.charAt(i) !== SEPARATOR)) { i--; }
|
|
|
|
score -= (index - i) + // (1)
|
|
|
|
(valueLength - queryLength - index); // (2)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove one point for each dot preceding the query, except for the one
|
|
|
|
// immediately before the query.
|
|
|
|
separators = 0;
|
|
|
|
i = index - 2;
|
|
|
|
while (i >= 0) {
|
|
|
|
if (value.charAt(i) === SEPARATOR) { separators++; }
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
score -= separators;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove five points for each dot following the query.
|
|
|
|
separators = 0;
|
|
|
|
i = valueLength - queryLength - index - 1;
|
|
|
|
while (i >= 0) {
|
|
|
|
if (value.charAt(index + queryLength + i) === SEPARATOR) { separators++; }
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
score -= separators * 5;
|
|
|
|
|
|
|
|
return Math.max(1, score);
|
|
|
|
}
|
|
|
|
|
|
|
|
function fuzzyMatch() {
|
|
|
|
if ((valueLength <= queryLength) || (value.indexOf(query) >= 0)) { return; }
|
|
|
|
if (!(match = fuzzyRegexp.exec(value))) { return; }
|
|
|
|
matchIndex = match.index;
|
|
|
|
matchLength = match[0].length;
|
|
|
|
score = scoreFuzzyMatch();
|
|
|
|
if (match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1))) {
|
|
|
|
matchIndex = i + match.index;
|
|
|
|
matchLength = match[0].length;
|
|
|
|
return Math.max(score, scoreFuzzyMatch());
|
|
|
|
} else {
|
|
|
|
return score;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function scoreFuzzyMatch() {
|
|
|
|
// When the match is at the beginning of the string or preceded by a dot.
|
|
|
|
if ((matchIndex === 0) || (value.charAt(matchIndex - 1) === SEPARATOR)) {
|
|
|
|
return Math.max(66, 100 - matchLength);
|
|
|
|
// When the match is at the end of the string.
|
|
|
|
} else if ((matchIndex + matchLength) === valueLength) {
|
|
|
|
return Math.max(33, 67 - matchLength);
|
|
|
|
// When the match is in the middle of the string.
|
|
|
|
} else {
|
|
|
|
return Math.max(1, 34 - matchLength);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Searchers
|
|
|
|
//
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
let CHUNK_SIZE = undefined;
|
|
|
|
let DEFAULTS = undefined;
|
|
|
|
let SEPARATORS_REGEXP = undefined;
|
|
|
|
let EOS_SEPARATORS_REGEXP = undefined;
|
|
|
|
let INFO_PARANTHESES_REGEXP = undefined;
|
|
|
|
let EMPTY_PARANTHESES_REGEXP = undefined;
|
|
|
|
let EVENT_REGEXP = undefined;
|
|
|
|
let DOT_REGEXP = undefined;
|
|
|
|
let WHITESPACE_REGEXP = undefined;
|
|
|
|
let EMPTY_STRING = undefined;
|
|
|
|
let ELLIPSIS = undefined;
|
|
|
|
let STRING = undefined;
|
|
|
|
const Cls = (app.Searcher = class Searcher {
|
|
|
|
static initClass() {
|
|
|
|
$.extend(this.prototype, Events);
|
|
|
|
|
|
|
|
CHUNK_SIZE = 20000;
|
|
|
|
|
|
|
|
DEFAULTS = {
|
|
|
|
max_results: app.config.max_results,
|
|
|
|
fuzzy_min_length: 3
|
|
|
|
};
|
|
|
|
|
|
|
|
SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g;
|
|
|
|
EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/;
|
|
|
|
INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/;
|
|
|
|
EMPTY_PARANTHESES_REGEXP = /\(\)/;
|
|
|
|
EVENT_REGEXP = /\ event$/;
|
|
|
|
DOT_REGEXP = /\.+/g;
|
|
|
|
WHITESPACE_REGEXP = /\s/g;
|
|
|
|
|
|
|
|
EMPTY_STRING = '';
|
|
|
|
ELLIPSIS = '...';
|
|
|
|
STRING = 'string';
|
|
|
|
}
|
|
|
|
|
|
|
|
static normalizeString(string) {
|
|
|
|
return string
|
|
|
|
.toLowerCase()
|
|
|
|
.replace(ELLIPSIS, EMPTY_STRING)
|
|
|
|
.replace(EVENT_REGEXP, EMPTY_STRING)
|
|
|
|
.replace(INFO_PARANTHESES_REGEXP, EMPTY_STRING)
|
|
|
|
.replace(SEPARATORS_REGEXP, SEPARATOR)
|
|
|
|
.replace(DOT_REGEXP, SEPARATOR)
|
|
|
|
.replace(EMPTY_PARANTHESES_REGEXP, EMPTY_STRING)
|
|
|
|
.replace(WHITESPACE_REGEXP, EMPTY_STRING);
|
|
|
|
}
|
|
|
|
|
|
|
|
static normalizeQuery(string) {
|
|
|
|
string = this.normalizeString(string);
|
|
|
|
return string.replace(EOS_SEPARATORS_REGEXP, '$1.');
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(options) {
|
|
|
|
this.match = this.match.bind(this);
|
|
|
|
this.matchChunks = this.matchChunks.bind(this);
|
|
|
|
if (options == null) { options = {}; }
|
|
|
|
this.options = $.extend({}, DEFAULTS, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
find(data, attr, q) {
|
|
|
|
this.kill();
|
|
|
|
|
|
|
|
this.data = data;
|
|
|
|
this.attr = attr;
|
|
|
|
this.query = q;
|
|
|
|
this.setup();
|
|
|
|
|
|
|
|
if (this.isValid()) { this.match(); } else { this.end(); }
|
|
|
|
}
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
query = (this.query = this.constructor.normalizeQuery(this.query));
|
|
|
|
queryLength = query.length;
|
|
|
|
this.dataLength = this.data.length;
|
|
|
|
this.matchers = [exactMatch];
|
|
|
|
this.totalResults = 0;
|
|
|
|
this.setupFuzzy();
|
|
|
|
}
|
|
|
|
|
|
|
|
setupFuzzy() {
|
|
|
|
if (queryLength >= this.options.fuzzy_min_length) {
|
|
|
|
fuzzyRegexp = this.queryToFuzzyRegexp(query);
|
|
|
|
this.matchers.push(fuzzyMatch);
|
|
|
|
} else {
|
|
|
|
fuzzyRegexp = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isValid() {
|
|
|
|
return (queryLength > 0) && (query !== SEPARATOR);
|
|
|
|
}
|
|
|
|
|
|
|
|
end() {
|
|
|
|
if (!this.totalResults) { this.triggerResults([]); }
|
|
|
|
this.trigger('end');
|
|
|
|
this.free();
|
|
|
|
}
|
|
|
|
|
|
|
|
kill() {
|
|
|
|
if (this.timeout) {
|
|
|
|
clearTimeout(this.timeout);
|
|
|
|
this.free();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
free() {
|
|
|
|
this.data = (this.attr = (this.dataLength = (this.matchers = (this.matcher = (this.query =
|
|
|
|
(this.totalResults = (this.scoreMap = (this.cursor = (this.timeout = null)))))))));
|
|
|
|
}
|
|
|
|
|
|
|
|
match() {
|
|
|
|
if (!this.foundEnough() && (this.matcher = this.matchers.shift())) {
|
|
|
|
this.setupMatcher();
|
|
|
|
this.matchChunks();
|
|
|
|
} else {
|
|
|
|
this.end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setupMatcher() {
|
|
|
|
this.cursor = 0;
|
|
|
|
this.scoreMap = new Array(101);
|
|
|
|
}
|
|
|
|
|
|
|
|
matchChunks() {
|
|
|
|
this.matchChunk();
|
|
|
|
|
|
|
|
if ((this.cursor === this.dataLength) || this.scoredEnough()) {
|
|
|
|
this.delay(this.match);
|
|
|
|
this.sendResults();
|
|
|
|
} else {
|
|
|
|
this.delay(this.matchChunks);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
matchChunk() {
|
|
|
|
({
|
|
|
|
matcher
|
|
|
|
} = this);
|
|
|
|
for (let j = 0, end = this.chunkSize(), asc = 0 <= end; asc ? j < end : j > end; asc ? j++ : j--) {
|
|
|
|
value = this.data[this.cursor][this.attr];
|
|
|
|
if (value.split) { // string
|
|
|
|
valueLength = value.length;
|
|
|
|
if (score = matcher()) { this.addResult(this.data[this.cursor], score); }
|
|
|
|
} else { // array
|
|
|
|
score = 0;
|
|
|
|
for (value of Array.from(this.data[this.cursor][this.attr])) {
|
|
|
|
valueLength = value.length;
|
|
|
|
score = Math.max(score, matcher() || 0);
|
|
|
|
}
|
|
|
|
if (score > 0) { this.addResult(this.data[this.cursor], score); }
|
|
|
|
}
|
|
|
|
this.cursor++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
chunkSize() {
|
|
|
|
if ((this.cursor + CHUNK_SIZE) > this.dataLength) {
|
|
|
|
return this.dataLength % CHUNK_SIZE;
|
|
|
|
} else {
|
|
|
|
return CHUNK_SIZE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scoredEnough() {
|
|
|
|
return (this.scoreMap[100] != null ? this.scoreMap[100].length : undefined) >= this.options.max_results;
|
|
|
|
}
|
|
|
|
|
|
|
|
foundEnough() {
|
|
|
|
return this.totalResults >= this.options.max_results;
|
|
|
|
}
|
|
|
|
|
|
|
|
addResult(object, score) {
|
|
|
|
let name;
|
|
|
|
(this.scoreMap[name = Math.round(score)] || (this.scoreMap[name] = [])).push(object);
|
|
|
|
this.totalResults++;
|
|
|
|
}
|
|
|
|
|
|
|
|
getResults() {
|
|
|
|
const results = [];
|
|
|
|
for (let j = this.scoreMap.length - 1; j >= 0; j--) {
|
|
|
|
var objects = this.scoreMap[j];
|
|
|
|
if (objects) {
|
|
|
|
results.push.apply(results, objects);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results.slice(0, this.options.max_results);
|
|
|
|
}
|
|
|
|
|
|
|
|
sendResults() {
|
|
|
|
const results = this.getResults();
|
|
|
|
if (results.length) { this.triggerResults(results); }
|
|
|
|
}
|
|
|
|
|
|
|
|
triggerResults(results) {
|
|
|
|
this.trigger('results', results);
|
|
|
|
}
|
|
|
|
|
|
|
|
delay(fn) {
|
|
|
|
return this.timeout = setTimeout(fn, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
queryToFuzzyRegexp(string) {
|
|
|
|
const chars = string.split('');
|
|
|
|
for (i = 0; i < chars.length; i++) { var char = chars[i]; chars[i] = $.escapeRegexp(char); }
|
|
|
|
return new RegExp(chars.join('.*?'));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
Cls.initClass();
|
|
|
|
return Cls; // abc -> /a.*?b.*?c.*?/
|
|
|
|
})();
|
|
|
|
|
|
|
|
app.SynchronousSearcher = class SynchronousSearcher extends app.Searcher {
|
|
|
|
constructor(...args) {
|
|
|
|
this.match = this.match.bind(this);
|
|
|
|
super(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
match() {
|
|
|
|
if (this.matcher) {
|
|
|
|
if (!this.allResults) { this.allResults = []; }
|
|
|
|
this.allResults.push.apply(this.allResults, this.getResults());
|
|
|
|
}
|
|
|
|
return super.match(...arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
free() {
|
|
|
|
this.allResults = null;
|
|
|
|
return super.free(...arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
end() {
|
|
|
|
this.sendResults(true);
|
|
|
|
return super.end(...arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
sendResults(end) {
|
|
|
|
if (end && (this.allResults != null ? this.allResults.length : undefined)) {
|
|
|
|
return this.triggerResults(this.allResults);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
delay(fn) {
|
|
|
|
return fn();
|
|
|
|
}
|
|
|
|
};
|