searcher.js 11 KB

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