searcher.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. //
  2. // Match functions
  3. //
  4. let fuzzyRegexp,
  5. i,
  6. index,
  7. lastIndex,
  8. match,
  9. matcher,
  10. matchIndex,
  11. matchLength,
  12. queryLength,
  13. score,
  14. separators,
  15. value,
  16. valueLength;
  17. const SEPARATOR = ".";
  18. let query =
  19. (queryLength =
  20. value =
  21. valueLength =
  22. matcher = // current match function
  23. fuzzyRegexp = // query fuzzy regexp
  24. index = // position of the query in the string being matched
  25. lastIndex = // last position of the query in the string being matched
  26. match = // regexp match data
  27. matchIndex =
  28. matchLength =
  29. score = // score for the current match
  30. separators = // counter
  31. i =
  32. null); // cursor
  33. function exactMatch() {
  34. index = value.indexOf(query);
  35. if (!(index >= 0)) {
  36. return;
  37. }
  38. lastIndex = value.lastIndexOf(query);
  39. if (index !== lastIndex) {
  40. return Math.max(
  41. scoreExactMatch(),
  42. ((index = lastIndex) && scoreExactMatch()) || 0,
  43. );
  44. } else {
  45. return scoreExactMatch();
  46. }
  47. }
  48. function scoreExactMatch() {
  49. // Remove one point for each unmatched character.
  50. score = 100 - (valueLength - queryLength);
  51. if (index > 0) {
  52. // If the character preceding the query is a dot, assign the same score
  53. // as if the query was found at the beginning of the string, minus one.
  54. if (value.charAt(index - 1) === SEPARATOR) {
  55. score += index - 1;
  56. // Don't match a single-character query unless it's found at the beginning
  57. // of the string or is preceded by a dot.
  58. } else if (queryLength === 1) {
  59. return;
  60. // (1) Remove one point for each unmatched character up to the nearest
  61. // preceding dot or the beginning of the string.
  62. // (2) Remove one point for each unmatched character following the query.
  63. } else {
  64. i = index - 2;
  65. while (i >= 0 && value.charAt(i) !== SEPARATOR) {
  66. i--;
  67. }
  68. score -=
  69. index -
  70. i + // (1)
  71. (valueLength - queryLength - index); // (2)
  72. }
  73. // Remove one point for each dot preceding the query, except for the one
  74. // immediately before the query.
  75. separators = 0;
  76. i = index - 2;
  77. while (i >= 0) {
  78. if (value.charAt(i) === SEPARATOR) {
  79. separators++;
  80. }
  81. i--;
  82. }
  83. score -= separators;
  84. }
  85. // Remove five points for each dot following the query.
  86. separators = 0;
  87. i = valueLength - queryLength - index - 1;
  88. while (i >= 0) {
  89. if (value.charAt(index + queryLength + i) === SEPARATOR) {
  90. separators++;
  91. }
  92. i--;
  93. }
  94. score -= separators * 5;
  95. return Math.max(1, score);
  96. }
  97. function fuzzyMatch() {
  98. if (valueLength <= queryLength || value.indexOf(query) >= 0) {
  99. return;
  100. }
  101. if (!(match = fuzzyRegexp.exec(value))) {
  102. return;
  103. }
  104. matchIndex = match.index;
  105. matchLength = match[0].length;
  106. score = scoreFuzzyMatch();
  107. if (
  108. (match = fuzzyRegexp.exec(
  109. value.slice((i = value.lastIndexOf(SEPARATOR) + 1)),
  110. ))
  111. ) {
  112. matchIndex = i + match.index;
  113. matchLength = match[0].length;
  114. return Math.max(score, scoreFuzzyMatch());
  115. } else {
  116. return score;
  117. }
  118. }
  119. function scoreFuzzyMatch() {
  120. // When the match is at the beginning of the string or preceded by a dot.
  121. if (matchIndex === 0 || value.charAt(matchIndex - 1) === SEPARATOR) {
  122. return Math.max(66, 100 - matchLength);
  123. // When the match is at the end of the string.
  124. } else if (matchIndex + matchLength === valueLength) {
  125. return Math.max(33, 67 - matchLength);
  126. // When the match is in the middle of the string.
  127. } else {
  128. return Math.max(1, 34 - matchLength);
  129. }
  130. }
  131. //
  132. // Searchers
  133. //
  134. app.Searcher = class Searcher extends Events {
  135. static CHUNK_SIZE = 20000;
  136. static DEFAULTS = {
  137. max_results: app.config.max_results,
  138. fuzzy_min_length: 3,
  139. };
  140. static SEPARATORS_REGEXP =
  141. /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g;
  142. static EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/;
  143. static INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/;
  144. static EMPTY_PARANTHESES_REGEXP = /\(\)/;
  145. static EVENT_REGEXP = /\ event$/;
  146. static DOT_REGEXP = /\.+/g;
  147. static WHITESPACE_REGEXP = /\s/g;
  148. static EMPTY_STRING = "";
  149. static ELLIPSIS = "...";
  150. static STRING = "string";
  151. static normalizeString(string) {
  152. return string
  153. .toLowerCase()
  154. .replace(Searcher.ELLIPSIS, Searcher.EMPTY_STRING)
  155. .replace(Searcher.EVENT_REGEXP, Searcher.EMPTY_STRING)
  156. .replace(Searcher.INFO_PARANTHESES_REGEXP, Searcher.EMPTY_STRING)
  157. .replace(Searcher.SEPARATORS_REGEXP, SEPARATOR)
  158. .replace(Searcher.DOT_REGEXP, SEPARATOR)
  159. .replace(Searcher.EMPTY_PARANTHESES_REGEXP, Searcher.EMPTY_STRING)
  160. .replace(Searcher.WHITESPACE_REGEXP, Searcher.EMPTY_STRING);
  161. }
  162. static normalizeQuery(string) {
  163. string = this.normalizeString(string);
  164. return string.replace(Searcher.EOS_SEPARATORS_REGEXP, "$1.");
  165. }
  166. constructor(options) {
  167. super();
  168. this.options = $.extend({}, Searcher.DEFAULTS, options || {});
  169. }
  170. find(data, attr, q) {
  171. this.kill();
  172. this.data = data;
  173. this.attr = attr;
  174. this.query = q;
  175. this.setup();
  176. if (this.isValid()) {
  177. this.match();
  178. } else {
  179. this.end();
  180. }
  181. }
  182. setup() {
  183. query = this.query = this.constructor.normalizeQuery(this.query);
  184. queryLength = query.length;
  185. this.dataLength = this.data.length;
  186. this.matchers = [exactMatch];
  187. this.totalResults = 0;
  188. this.setupFuzzy();
  189. }
  190. setupFuzzy() {
  191. if (queryLength >= this.options.fuzzy_min_length) {
  192. fuzzyRegexp = this.queryToFuzzyRegexp(query);
  193. this.matchers.push(fuzzyMatch);
  194. } else {
  195. fuzzyRegexp = null;
  196. }
  197. }
  198. isValid() {
  199. return queryLength > 0 && query !== SEPARATOR;
  200. }
  201. end() {
  202. if (!this.totalResults) {
  203. this.triggerResults([]);
  204. }
  205. this.trigger("end");
  206. this.free();
  207. }
  208. kill() {
  209. if (this.timeout) {
  210. clearTimeout(this.timeout);
  211. this.free();
  212. }
  213. }
  214. free() {
  215. this.data = null;
  216. this.attr = null;
  217. this.dataLength = null;
  218. this.matchers = null;
  219. this.matcher = null;
  220. this.query = null;
  221. this.totalResults = null;
  222. this.scoreMap = null;
  223. this.cursor = null;
  224. this.timeout = null;
  225. }
  226. match() {
  227. if (!this.foundEnough() && (this.matcher = this.matchers.shift())) {
  228. this.setupMatcher();
  229. this.matchChunks();
  230. } else {
  231. this.end();
  232. }
  233. }
  234. setupMatcher() {
  235. this.cursor = 0;
  236. this.scoreMap = new Array(101);
  237. }
  238. matchChunks() {
  239. this.matchChunk();
  240. if (this.cursor === this.dataLength || this.scoredEnough()) {
  241. this.delay(() => this.match());
  242. this.sendResults();
  243. } else {
  244. this.delay(() => this.matchChunks());
  245. }
  246. }
  247. matchChunk() {
  248. ({ matcher } = this);
  249. for (let j = 0, end = this.chunkSize(); j < end; j++) {
  250. value = this.data[this.cursor][this.attr];
  251. if (value.split) {
  252. // string
  253. valueLength = value.length;
  254. if ((score = matcher())) {
  255. this.addResult(this.data[this.cursor], score);
  256. }
  257. } else {
  258. // array
  259. score = 0;
  260. for (value of Array.from(this.data[this.cursor][this.attr])) {
  261. valueLength = value.length;
  262. score = Math.max(score, matcher() || 0);
  263. }
  264. if (score > 0) {
  265. this.addResult(this.data[this.cursor], score);
  266. }
  267. }
  268. this.cursor++;
  269. }
  270. }
  271. chunkSize() {
  272. if (this.cursor + Searcher.CHUNK_SIZE > this.dataLength) {
  273. return this.dataLength % Searcher.CHUNK_SIZE;
  274. } else {
  275. return Searcher.CHUNK_SIZE;
  276. }
  277. }
  278. scoredEnough() {
  279. return (
  280. (this.scoreMap[100] != null ? this.scoreMap[100].length : undefined) >=
  281. this.options.max_results
  282. );
  283. }
  284. foundEnough() {
  285. return this.totalResults >= this.options.max_results;
  286. }
  287. addResult(object, score) {
  288. let name;
  289. (
  290. this.scoreMap[(name = Math.round(score))] || (this.scoreMap[name] = [])
  291. ).push(object);
  292. this.totalResults++;
  293. }
  294. getResults() {
  295. const results = [];
  296. for (let j = this.scoreMap.length - 1; j >= 0; j--) {
  297. var objects = this.scoreMap[j];
  298. if (objects) {
  299. results.push.apply(results, objects);
  300. }
  301. }
  302. return results.slice(0, this.options.max_results);
  303. }
  304. sendResults() {
  305. const results = this.getResults();
  306. if (results.length) {
  307. this.triggerResults(results);
  308. }
  309. }
  310. triggerResults(results) {
  311. this.trigger("results", results);
  312. }
  313. delay(fn) {
  314. return (this.timeout = setTimeout(fn, 1));
  315. }
  316. queryToFuzzyRegexp(string) {
  317. const chars = string.split("");
  318. for (i = 0; i < chars.length; i++) {
  319. var char = chars[i];
  320. chars[i] = $.escapeRegexp(char);
  321. }
  322. return new RegExp(chars.join(".*?")); // abc -> /a.*?b.*?c.*?/
  323. }
  324. };
  325. app.SynchronousSearcher = class SynchronousSearcher extends app.Searcher {
  326. match() {
  327. if (this.matcher) {
  328. if (!this.allResults) {
  329. this.allResults = [];
  330. }
  331. this.allResults.push.apply(this.allResults, this.getResults());
  332. }
  333. return super.match(...arguments);
  334. }
  335. free() {
  336. this.allResults = null;
  337. return super.free(...arguments);
  338. }
  339. end() {
  340. this.sendResults(true);
  341. return super.end(...arguments);
  342. }
  343. sendResults(end) {
  344. if (end && (this.allResults != null ? this.allResults.length : undefined)) {
  345. return this.triggerResults(this.allResults);
  346. }
  347. }
  348. delay(fn) {
  349. return fn();
  350. }
  351. };