Переглянути джерело

fix: search in code by default + mixed case

* fix regexps with uppercase tokens like \D+
* use the old 100ms delay for more responsive typing
* use toLocaleLowerCase in name/desc instead of toLowerCase
+ add desc: prefix to search in name/desc and localized name/desc
+ add u flag to all regexps
- don't add i flag to /regexp/
* case-sensitive searching when input has upper case letters
* parse tokens in one loop using a combined regexp
tophf 1 рік тому
батько
коміт
cdcedc55d0

+ 2 - 2
src/background/utils/tester.js

@@ -1,5 +1,5 @@
 /* eslint-disable max-classes-per-file */
 /* eslint-disable max-classes-per-file */
-import { getScriptPrettyUrl } from '@/common';
+import { escapeStringForRegExp, getScriptPrettyUrl } from '@/common';
 import { BLACKLIST, BLACKLIST_ERRORS } from '@/common/consts';
 import { BLACKLIST, BLACKLIST_ERRORS } from '@/common/consts';
 import initCache from '@/common/cache';
 import initCache from '@/common/cache';
 import { getPublicSuffix } from '@/common/tld';
 import { getPublicSuffix } from '@/common/tld';
@@ -232,7 +232,7 @@ function testRules(url, script, ...list) {
 }
 }
 
 
 function str2RE(str) {
 function str2RE(str) {
-  return str.replace(/[.?+[\]{}()|^$]/g, '\\$&').replace(/\*/g, '.*?');
+  return escapeStringForRegExp(str).replace(/\*/g, '.*?');
 }
 }
 
 
 function autoReg(str) {
 function autoReg(str) {

+ 4 - 0
src/common/util.js

@@ -342,3 +342,7 @@ export function dumpScriptValue(value, jsonDump = JSON.stringify) {
 export function normalizeTag(tag) {
 export function normalizeTag(tag) {
   return tag.replace(/[^\w.-]/g, '');
   return tag.replace(/[^\w.-]/g, '');
 }
 }
+
+export function escapeStringForRegExp(str) {
+  return str.replace(/[.?+[\]{}()|^$]/g, '\\$&');
+}

+ 5 - 5
src/options/index.js

@@ -31,26 +31,26 @@ render(App);
 function initScript(script, sizes) {
 function initScript(script, sizes) {
   const meta = script.meta || {};
   const meta = script.meta || {};
   const localeName = getLocaleString(meta, 'name');
   const localeName = getLocaleString(meta, 'name');
-  const search = [
+  const desc = [
     meta.name,
     meta.name,
     localeName,
     localeName,
     meta.description,
     meta.description,
     getLocaleString(meta, 'description'),
     getLocaleString(meta, 'description'),
     script.custom.name,
     script.custom.name,
     script.custom.description,
     script.custom.description,
-  ]::trueJoin('\n').toLowerCase();
+  ]::trueJoin('\n');
   const name = script.custom.name || localeName;
   const name = script.custom.name || localeName;
-  const lowerName = name.toLowerCase();
   let total = 0;
   let total = 0;
   let str = '';
   let str = '';
   sizes.forEach((val, i) => {
   sizes.forEach((val, i) => {
     total += val;
     total += val;
     if (val) str += `${SIZE_TITLES[i]}: ${formatByteLength(val)}\n`;
     if (val) str += `${SIZE_TITLES[i]}: ${formatByteLength(val)}\n`;
   });
   });
+  /** @namespace VMScriptItemCache */
   script.$cache = {
   script.$cache = {
-    search,
+    desc,
     name,
     name,
-    lowerName,
+    lowerName: name.toLocaleLowerCase(),
     tags: script.custom.tags || '',
     tags: script.custom.tags || '',
     size: formatByteLength(total, true).replace(' ', ''),
     size: formatByteLength(total, true).replace(' ', ''),
     sizes: str.slice(0, -1).replace(/\x20/g, '\xA0').replace(/[^B]$/gm, '$&B'),
     sizes: str.slice(0, -1).replace(/\x20/g, '\xA0').replace(/[^B]$/gm, '$&B'),

+ 68 - 87
src/options/utils/search.js

@@ -1,100 +1,78 @@
-import { normalizeTag } from '@/common';
+import { escapeStringForRegExp, normalizeTag } from '@/common';
 
 
-export function parseSearch(search) {
-  /**
-   * @type Array<{
-   *   prefix: string;
-   *   raw: string;
-   *   negative: boolean;
-   * }>
-   */
-  const tokens = [];
-  search = search.toLowerCase();
-  let offset = 0;
-  while (search[offset] === ' ') offset += 1;
-  while (offset < search.length) {
-    const negative = search[offset] === '!';
-    if (negative) offset += 1;
-    const prefix =
-      search.slice(offset).match(/^(#|re:|(?:name|code)(?:\+re)?:)/)?.[1] || '';
-    if (prefix) offset += prefix.length;
-    const startOffset = offset;
-    const quote =
-      (!prefix || prefix.endsWith(':')) &&
-      search.slice(offset).match(/^['"]/)?.[0];
-    if (quote) offset += 1;
-    let pattern = '';
-    const endChar = quote || ' ';
-    while (offset < search.length) {
-      const ch = search[offset];
-      if (quote && ch === quote && search[offset + 1] === quote) {
-        // escape quotes by double it
-        pattern += quote;
-        offset += 2;
-      } else if (ch !== endChar) {
-        pattern += ch;
-        offset += 1;
-      } else {
-        break;
-      }
-    }
-    if (quote) {
-      if (offset < search.length) offset += 1;
-      else throw new Error('Unmatched quotes');
-    }
-    tokens.push({
-      prefix,
-      raw: search.slice(startOffset, offset),
-      parsed: pattern,
-      negative,
-    });
-    while (search[offset] === ' ') offset += 1;
-  }
-  return tokens;
-}
+const reToken = re`/\s*
+  (!)?
+  (
+    \# |
+    (name|code|desc)(\+re)?: |
+    (re:) |
+  )
+  (
+    '((?:[^']+|'')*) ('|$) |
+    "((?:[^"]+|"")*) ("|$) |
+    \/(\S+?)\/([a-z]*) |
+    \S+
+  )
+  (?:\s+|$)
+/yx`;
+const reTwoSingleQuotes = /''/g;
+const reTwoDoubleQuotes = /""/g;
 
 
 export function createSearchRules(search) {
 export function createSearchRules(search) {
-  const tokens = parseSearch(search);
-  /**
-   * @type Array<{
-   *   scope: string;
-   *   pattern: string | RegExp;
-   *   negative: boolean;
-   * }>
-   */
+  /** @type {VMSearchRule[]} */
   const rules = [];
   const rules = [];
+  const tokens = [];
   const includeTags = [];
   const includeTags = [];
   const excludeTags = [];
   const excludeTags = [];
-  for (const token of tokens) {
-    if (token.prefix === '#') {
-      (token.negative ? excludeTags : includeTags).push(token.parsed);
+  reToken.lastIndex = 0;
+  for (let m; (m = reToken.exec(search)); ) {
+    let [,
+      negative,
+      prefix, scope = '', re1, re2,
+      raw,
+      q1, q1end,
+      quoted = q1, quoteEnd = q1end,
+      reStr, flags = '',
+    ] = m;
+    let str;
+    if (quoted) {
+      if (!quoteEnd) throw new Error('Unmatched quotes');
+      str = quoted.replace(q1 ? reTwoSingleQuotes : reTwoDoubleQuotes, quoted[0]);
+    } else {
+      str = raw;
+    }
+    negative = !!negative;
+    tokens.push({
+      negative,
+      prefix,
+      raw,
+      parsed: str,
+    });
+    if (prefix === '#') {
+      str = normalizeTag(str).replace(/\./g, '\\.');
+      if (str) (negative ? excludeTags : includeTags).push(str);
     } else {
     } else {
-      // Strip ':'
-      let scope = token.prefix.slice(0, -1);
-      let pattern = token.parsed;
-      if (/(?:^|\+)re$/.test(scope)) {
-        scope = scope.slice(0, -3);
-        pattern = new RegExp(pattern, 'i');
+      if (re1 || re2) {
+        flags = 'i';
+      } else if (reStr) {
+        str = reStr;
       } else {
       } else {
-        const reMatches = pattern.match(/^\/(.*?)\/(\w*)$/);
-        if (reMatches) pattern = new RegExp(reMatches[1], reMatches[2] || 'i');
+        if (str === str.toLocaleLowerCase()) flags = 'i';
+        str = escapeStringForRegExp(str);
       }
       }
+      /** @namespace VMSearchRule */
       rules.push({
       rules.push({
+        negative,
         scope,
         scope,
-        pattern,
-        negative: token.negative,
+        re: new RegExp(str, flags.includes('u') ? flags : flags + 'u'),
       });
       });
     }
     }
   }
   }
   [includeTags, excludeTags].forEach((tags, negative) => {
   [includeTags, excludeTags].forEach((tags, negative) => {
-    const sanitizedTags = tags
-      .map((tag) => normalizeTag(tag).replace(/\./g, '\\.'))
-      .filter(Boolean)
-      .join('|');
-    if (sanitizedTags) {
+    if (tags.length) {
       rules.unshift({
       rules.unshift({
         scope: 'tags',
         scope: 'tags',
-        pattern: new RegExp(`(?:^|\\s)(${sanitizedTags})(\\s|$)`),
+        re: new RegExp(`(?:^|\\s)(${tags.join('|').toLowerCase()})(\\s|$)`, 'u'),
         negative: !!negative,
         negative: !!negative,
       });
       });
     }
     }
@@ -105,11 +83,14 @@ export function createSearchRules(search) {
   };
   };
 }
 }
 
 
-export function testSearchRule(rule, data) {
-  const { pattern, negative } = rule;
-  const result =
-    typeof pattern.test === 'function'
-      ? pattern.test(data)
-      : data.includes(pattern);
-  return negative ^ result;
+/**
+ * @this {VMScriptItemCache}
+ * @param {VMSearchRule} rule
+ * @return {number}
+ */
+export function testSearchRule({ re, negative, scope }) {
+  return negative ^ (
+    re.test(this[scope || 'desc'])
+    || !scope && re.test(this.code)
+  );
 }
 }

+ 4 - 13
src/options/views/tab-installed.vue

@@ -327,7 +327,7 @@ const batchActions = computed(() => [
   },
   },
 ]);
 ]);
 
 
-const debouncedSearch = debounce(scheduleSearch, 200);
+const debouncedSearch = debounce(scheduleSearch, 100);
 const debouncedRender = debounce(renderScripts);
 const debouncedRender = debounce(renderScripts);
 
 
 function resetList() {
 function resetList() {
@@ -454,18 +454,9 @@ async function renderScripts() {
   }
   }
 }
 }
 function performSearch() {
 function performSearch() {
-  let count = 0;
-  store.scripts.forEach(({ $cache }) => {
-    const dataMap = {
-      name: $cache.lowerName,
-      code: $cache.code,
-      tags: $cache.tags,
-      '': $cache.search,
-    };
-    $cache.show = state.search.rules.every(rule => testSearchRule(rule, dataMap[rule.scope]));
-    count += $cache.show;
-  });
-  return count;
+  return store.scripts.reduce((num, { $cache }) => num + (
+    $cache.show = state.search.rules.every(testSearchRule, $cache)
+  ), 0);
 }
 }
 function scheduleSearch() {
 function scheduleSearch() {
   try {
   try {

+ 37 - 26
test/options/__snapshots__/search.test.js.snap

@@ -12,17 +12,22 @@ exports[`createSearchRules 2`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": true,
       "negative": true,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(c\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(c\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(a\\|b\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(a\\|b\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "hello",
+      "re": /hello/iu,
+      "scope": "",
+    },
+    {
+      "negative": false,
+      "re": /CaseSensitive/u,
       "scope": "",
       "scope": "",
     },
     },
   ],
   ],
@@ -51,6 +56,12 @@ exports[`createSearchRules 2`] = `
       "prefix": "",
       "prefix": "",
       "raw": "hello",
       "raw": "hello",
     },
     },
+    {
+      "negative": false,
+      "parsed": "CaseSensitive",
+      "prefix": "",
+      "raw": "CaseSensitive",
+    },
   ],
   ],
 }
 }
 `;
 `;
@@ -60,17 +71,17 @@ exports[`createSearchRules 3`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(a-b\\|b\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(a-b\\|b\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "hello",
+      "re": /hello/iu,
       "scope": "name",
       "scope": "name",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "world",
+      "re": /world/iu,
       "scope": "",
       "scope": "",
     },
     },
   ],
   ],
@@ -108,12 +119,12 @@ exports[`createSearchRules 4`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "hello world",
+      "re": /hello world/iu,
       "scope": "name",
       "scope": "name",
     },
     },
   ],
   ],
@@ -145,12 +156,12 @@ exports[`createSearchRules 5`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /hello world/i,
+      "re": /hello world/iu,
       "scope": "name",
       "scope": "name",
     },
     },
   ],
   ],
@@ -182,12 +193,12 @@ exports[`createSearchRules 6`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/,
+      "re": /\\(\\?:\\^\\|\\\\s\\)\\(a\\\\\\.b\\|b\\)\\(\\\\s\\|\\$\\)/u,
       "scope": "tags",
       "scope": "tags",
     },
     },
     {
     {
       "negative": true,
       "negative": true,
-      "pattern": /hello world/i,
+      "re": /hello world/iu,
       "scope": "name",
       "scope": "name",
     },
     },
   ],
   ],
@@ -219,12 +230,12 @@ exports[`createSearchRules 7`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "#a.b",
+      "re": /#a\\\\\\.b/iu,
       "scope": "",
       "scope": "",
     },
     },
     {
     {
       "negative": true,
       "negative": true,
-      "pattern": "#b",
+      "re": /#b/iu,
       "scope": "",
       "scope": "",
     },
     },
   ],
   ],
@@ -250,37 +261,37 @@ exports[`createSearchRules 8`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /regexp/i,
+      "re": /\\\\d\\+\\\\D\\+/u,
       "scope": "",
       "scope": "",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /regexp/u,
+      "re": /\\\\d\\+\\\\D\\+/u,
       "scope": "code",
       "scope": "code",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "/not",
+      "re": /\\\\/not/iu,
       "scope": "",
       "scope": "",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "regexp/",
+      "re": /regexp\\\\//iu,
       "scope": "",
       "scope": "",
     },
     },
   ],
   ],
   "tokens": [
   "tokens": [
     {
     {
       "negative": false,
       "negative": false,
-      "parsed": "/regexp/",
+      "parsed": "/\\d+\\D+/",
       "prefix": "",
       "prefix": "",
-      "raw": "/regexp/",
+      "raw": "/\\d+\\D+/",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "parsed": "/regexp/u",
+      "parsed": "/\\d+\\D+/u",
       "prefix": "code:",
       "prefix": "code:",
-      "raw": "/regexp/u",
+      "raw": "/\\d+\\D+/u",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
@@ -303,22 +314,22 @@ exports[`createSearchRules 9`] = `
   "rules": [
   "rules": [
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": "foobar",
+      "re": /foobar/iu,
       "scope": "",
       "scope": "",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /foobar/i,
+      "re": /foobar/iu,
       "scope": "",
       "scope": "",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /foobar/i,
+      "re": /foobar/iu,
       "scope": "name",
       "scope": "name",
     },
     },
     {
     {
       "negative": false,
       "negative": false,
-      "pattern": /foobar/i,
+      "re": /foobar/iu,
       "scope": "code",
       "scope": "code",
     },
     },
   ],
   ],

+ 2 - 2
test/options/search.test.js

@@ -2,12 +2,12 @@ import { createSearchRules } from '@/options/utils/search';
 
 
 test('createSearchRules', () => {
 test('createSearchRules', () => {
   expect(createSearchRules('')).toMatchSnapshot();
   expect(createSearchRules('')).toMatchSnapshot();
-  expect(createSearchRules('#a #b !#c hello')).toMatchSnapshot();
+  expect(createSearchRules('#a #b !#c hello CaseSensitive')).toMatchSnapshot();
   expect(createSearchRules('#a-b #b name:hello world')).toMatchSnapshot();
   expect(createSearchRules('#a-b #b name:hello world')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b name:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b name:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b name+re:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b name+re:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b !name+re:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('#a.b #b !name+re:"hello world"')).toMatchSnapshot();
   expect(createSearchRules('"#a.b" !"#b"')).toMatchSnapshot();
   expect(createSearchRules('"#a.b" !"#b"')).toMatchSnapshot();
-  expect(createSearchRules('/regexp/ code:/regexp/u /not regexp/')).toMatchSnapshot();
+  expect(createSearchRules(String.raw`/\d+\D+/ code:/\d+\D+/u /not regexp/`)).toMatchSnapshot();
   expect(createSearchRules('foobar re:foobar name+re:foobar code+re:foobar')).toMatchSnapshot();
   expect(createSearchRules('foobar re:foobar name+re:foobar code+re:foobar')).toMatchSnapshot();
 });
 });