Просмотр исходного кода

feat: add @exclude-match rule

transform meta fields to camel case, e.g. `run-at` to `runAt`
Gerald 8 лет назад
Родитель
Сommit
1e134b400d

+ 2 - 1
src/background/utils/script.js

@@ -17,6 +17,7 @@ export function parseMeta(code) {
     include: [],
     exclude: [],
     match: [],
+    excludeMatch: [],
     require: [],
     resource: [],
     grant: [],
@@ -31,7 +32,7 @@ export function parseMeta(code) {
       flag = 0;
     }
     if (flag === 1 && group1[0] === '@') {
-      const key = group1.slice(1);
+      const key = group1.slice(1).replace(/[-_](\w)/g, (m, g) => g.toUpperCase());
       const val = group2.trim();
       const data = meta[key];
       // multiple values allowed

+ 74 - 41
src/background/utils/tester.js

@@ -1,31 +1,64 @@
+import cache from './cache';
 import { getOption, hookOptions } from './options';
 
+/**
+ * Test glob rules like `@include` and `@exclude`.
+ */
+export function testGlob(url, rules) {
+  const lifetime = 60 * 1000;
+  return rules.some(rule => {
+    const key = `re:${rule}`;
+    let re = cache.get(key);
+    if (re) {
+      cache.hit(key, lifetime);
+    } else {
+      re = autoReg(rule);
+      cache.put(key, re, lifetime);
+    }
+    return re.test(url);
+  });
+}
+
+/**
+ * Test match rules like `@match` and `@exclude_match`.
+ */
+export function testMatch(url, rules) {
+  const lifetime = 10 * 1000;
+  const key = `match:${url}`;
+  let matcher = cache.get(key);
+  if (matcher) {
+    cache.hit(key, lifetime);
+  } else {
+    matcher = matchTester(url);
+    cache.put(key, matcher, lifetime);
+  }
+  return rules.some(matcher);
+}
+
 export function testScript(url, script) {
   const { custom, meta } = script;
-  let inc = [];
-  let exc = [];
-  let mat = [];
-  if (custom._match !== false && meta.match) mat = mat.concat(meta.match);
-  if (custom.match) mat = mat.concat(custom.match);
-  if (custom._include !== false && meta.include) inc = inc.concat(meta.include);
-  if (custom.include) inc = inc.concat(custom.include);
-  if (custom._exclude !== false && meta.exclude) exc = exc.concat(meta.exclude);
-  if (custom.exclude) exc = exc.concat(custom.exclude);
+  const mat = mergeLists(custom._match !== false && meta.match, custom.match);
+  const inc = mergeLists(custom._include !== false && meta.include, custom.include);
+  const exc = mergeLists(custom._exclude !== false && meta.exclude, custom.exclude);
+  const excMat = mergeLists(
+    custom._excludeMatch !== false && meta.excludeMatch,
+    custom.excludeMatch,
+  );
+  // match all if no @match or @include rule
   let ok = !mat.length && !inc.length;
   // @match
-  ok = ok || testMatches(url, mat);
+  ok = ok || testMatch(url, mat);
   // @include
-  ok = ok || testRules(url, inc);
+  ok = ok || testGlob(url, inc);
+  // @exclude-match
+  ok = ok && !testMatch(url, excMat);
   // @exclude
-  ok = ok && !testRules(url, exc);
+  ok = ok && !testGlob(url, exc);
   return ok;
 }
 
-function testRules(url, rules) {
-  return rules.some(rule => autoReg(rule).test(url));
-}
-function testMatches(url, rules) {
-  return rules.length && rules.some(matchTester(url));
+function mergeLists(...args) {
+  return args.reduce((res, item) => (item ? res.concat(item) : res), []);
 }
 
 function str2RE(str) {
@@ -40,31 +73,31 @@ function autoReg(str) {
   return str2RE(str);              // String with wildcards
 }
 
-function matchTester(url) {
-  function matchScheme(rule, data) {
-    // exact match
-    if (rule === data) return 1;
-    // * = http | https
-    if (rule === '*' && /^https?$/i.test(data)) return 1;
-    return 0;
-  }
-  function matchHost(rule, data) {
-    // * matches all
-    if (rule === '*') return 1;
-    // exact match
-    if (rule === data) return 1;
-    // *.example.com
-    if (/^\*\.[^*]*$/.test(rule)) {
-      // matches the specified domain
-      if (rule.slice(2) === data) return 1;
-      // matches subdomains
-      if (str2RE(rule).test(data)) return 1;
-    }
-    return 0;
-  }
-  function matchPath(rule, data) {
-    return str2RE(rule).test(data);
+function matchScheme(rule, data) {
+  // exact match
+  if (rule === data) return 1;
+  // * = http | https
+  if (rule === '*' && /^https?$/i.test(data)) return 1;
+  return 0;
+}
+function matchHost(rule, data) {
+  // * matches all
+  if (rule === '*') return 1;
+  // exact match
+  if (rule === data) return 1;
+  // *.example.com
+  if (/^\*\.[^*]*$/.test(rule)) {
+    // matches the specified domain
+    if (rule.slice(2) === data) return 1;
+    // matches subdomains
+    if (str2RE(rule).test(data)) return 1;
   }
+  return 0;
+}
+function matchPath(rule, data) {
+  return str2RE(rule).test(data);
+}
+function matchTester(url) {
   const RE = /(.*?):\/\/([^/]*)\/(.*)/;
   const urlParts = url.match(RE);
   return str => {

+ 4 - 1
src/common/cache.js

@@ -5,7 +5,7 @@ const defaults = {
 export default function initCache(options) {
   const cache = {};
   const { lifetime: defaultLifetime } = options || defaults;
-  return { get, put, del, has, hit };
+  return { get, put, del, has, hit, destroy };
   function get(key, def) {
     const item = cache[key];
     return item ? item.value : def;
@@ -32,4 +32,7 @@ export default function initCache(options) {
   function hit(key, lifetime) {
     put(key, get(key), lifetime);
   }
+  function destroy() {
+    Object.keys(cache).forEach(del);
+  }
 }

+ 3 - 3
src/options/views/edit.vue

@@ -14,7 +14,7 @@
                 </td>
                 <td title="@run-at" v-text="i18n('labelRunAt')"></td>
                 <td>
-                  <select v-model="custom['run-at']">
+                  <select v-model="custom.runAt">
                     <option value="" v-text="i18n('labelRunAtDefault')"></option>
                     <option value=start>document-start</option>
                     <option value=idle>document-idle</option>
@@ -259,7 +259,7 @@ export default {
         include: fromList(custom.include),
         match: fromList(custom.match),
         exclude: fromList(custom.exclude),
-        'run-at': custom['run-at'] || '',
+        runAt: custom.runAt || custom['run-at'] || '',
       });
       this.$nextTick(() => {
         this.canSave = false;
@@ -274,7 +274,7 @@ export default {
       const { custom } = this;
       const value = [
         'name',
-        'run-at',
+        'runAt',
         'homepageURL',
         'updateURL',
         'downloadURL',

+ 109 - 0
test/background/tester.js

@@ -1,5 +1,8 @@
 import test from 'tape';
 import { testScript } from 'src/background/utils/tester';
+import cache from 'src/background/utils/cache';
+
+test.onFinish(cache.destroy);
 
 test('scheme', t => {
   t.test('should match all', q => {
@@ -103,3 +106,109 @@ test('path', t => {
 
   t.end();
 });
+
+test('include', t => {
+  t.test('should include any', q => {
+    const script = {
+      custom: {},
+      meta: {
+        include: [
+          '*',
+        ],
+      },
+    };
+    q.ok(testScript('https://www.google.com/', script), 'should match `http | https`');
+    q.ok(testScript('file:///Users/Gerald/file', script), 'should match `file`');
+    q.end();
+  });
+
+  t.test('should include by regexp', q => {
+    const script = {
+      custom: {},
+      meta: {
+        include: [
+          'https://www.google.com/*',
+          'https://twitter.com/*',
+        ],
+      },
+    };
+    q.ok(testScript('https://www.google.com/', script), 'should match `/`');
+    q.ok(testScript('https://www.google.com/hello/world', script), 'include by prefix');
+    q.notOk(testScript('https://www.hello.com/', script), 'not include by prefix');
+    q.end();
+  });
+});
+
+test('exclude', t => {
+  t.test('should exclude any', q => {
+    const script = {
+      custom: {},
+      meta: {
+        match: [
+          '*://*/*',
+        ],
+        exclude: [
+          '*',
+        ],
+      },
+    };
+    q.notOk(testScript('https://www.google.com/', script), 'should exclude `http | https`');
+    q.end();
+  });
+
+  t.test('should include by regexp', q => {
+    const script = {
+      custom: {},
+      meta: {
+        match: [
+          '*://*/*',
+        ],
+        excludeMatch: [
+          'https://www.google.com/*',
+          'https://twitter.com/*',
+        ],
+      },
+    };
+    q.notOk(testScript('https://www.google.com/', script), 'should exclude `/`');
+    q.notOk(testScript('https://www.google.com/hello/world', script), 'exclude by prefix');
+    q.ok(testScript('https://www.hello.com/', script), 'not exclude by prefix');
+    q.end();
+  });
+});
+
+test('exclude-match', t => {
+  t.test('should exclude any', q => {
+    const script = {
+      custom: {},
+      meta: {
+        match: [
+          '*://*/*',
+        ],
+        excludeMatch: [
+          '*://*/*',
+        ],
+      },
+    };
+    q.notOk(testScript('https://www.google.com/', script), 'should exclude `http | https`');
+    q.end();
+  });
+
+  t.test('should include by regexp', q => {
+    const script = {
+      custom: {},
+      meta: {
+        match: [
+          '*://*/*',
+        ],
+        excludeMatch: [
+          'https://www.google.com/*',
+          'https://twitter.com/*',
+        ],
+      },
+    };
+    q.notOk(testScript('https://www.google.com/', script), 'should exclude `/`');
+    q.notOk(testScript('https://www.google.com/hello/world', script), 'exclude by prefix');
+    q.ok(testScript('https://www.hello.com/', script), 'not exclude by prefix');
+    q.end();
+  });
+});