Browse Source

Implement suffix (reversed) trie

SukkaW 2 years ago
parent
commit
9dd9e4aa05
4 changed files with 760 additions and 10 deletions
  1. 300 0
      Build/lib/trie.js
  2. 185 0
      Build/lib/trie.test.js
  3. 3 0
      package.json
  4. 272 10
      pnpm-lock.yaml

+ 300 - 0
Build/lib/trie.js

@@ -0,0 +1,300 @@
+/**
+ * Suffix Trie based on Mnemonist Trie
+ */
+
+const SENTINEL = String.fromCharCode(0);
+
+class Trie {
+  size = 0;
+  root = {};
+
+  /**
+   * Method used to add the given prefix to the trie.
+   *
+   * @param  {string} suffix - Prefix to follow.
+   * @return {Trie}
+   */
+  add(suffix) {
+    let node = this.root;
+    let token;
+
+    for (let i = suffix.length - 1; i >= 0; i--) {
+      token = suffix[i];
+
+      node = node[token] || (node[token] = {});
+    }
+
+    // Do we need to increase size?
+    if (!(SENTINEL in node)) this.size++;
+    node[SENTINEL] = true;
+
+    return this;
+  }
+
+  /**
+   * Method used to retrieve every item in the trie with the given prefix.
+   *
+   * @param  {string} suffix - Prefix to query.
+   * @param  {boolean} [includeEqualWithSuffix]
+   * @return {string[]}
+   */
+  find(suffix, includeEqualWithSuffix = true) {
+    let node = this.root;
+    const matches = [];
+    let token;
+    let i;
+    let l;
+
+    for (let i = suffix.length - 1; i >= 0; i--) {
+      token = suffix[i];
+
+      node = node[token];
+
+      if (node == null) return matches;
+    }
+
+    // Performing DFS from prefix
+    const nodeStack = [node];
+
+    const suffixStack = [suffix];
+    let k;
+
+    let $suffix = suffix;
+
+    while (nodeStack.length) {
+      $suffix = suffixStack.pop();
+      node = nodeStack.pop();
+
+      for (k in node) {
+        if (k === SENTINEL) {
+          if (includeEqualWithSuffix) {
+            matches.push($suffix);
+          } else if ($suffix !== suffix) {
+            matches.push($suffix);
+          }
+
+
+          continue;
+        }
+
+        nodeStack.push(node[k]);
+        suffixStack.push(k + $suffix);
+      }
+    }
+
+    return matches;
+  }
+
+  toJSON() {
+    return this.root;
+  }
+
+  /**
+   * Method used to clear the trie.
+   *
+   * @return {void}
+   */
+  // clear() {
+  //   // Properties
+  //   this.root = {};
+  //   this.size = 0;
+  // }
+
+  /**
+   * Method used to update the value of the given prefix in the trie.
+   *
+   * @param  {string|array} prefix - Prefix to follow.
+   * @param  {(oldValue: any | undefined) => any} updateFunction - Update value visitor callback.
+   * @return {Trie}
+   */
+  // update(prefix, updateFunction) {
+  //   let node = this.root;
+  //   let token;
+
+  //   for (let i = 0, l = prefix.length; i < l; i++) {
+  //     token = prefix[i];
+
+  //     node = node[token] || (node[token] = {});
+  //   }
+
+  //   // Do we need to increase size?
+  //   if (!(SENTINEL in node))
+  //     this.size++;
+
+  //   node[SENTINEL] = updateFunction(node[SENTINEL]);
+
+  //   return this;
+  // }
+
+  /**
+   * Method used to delete a prefix from the trie.
+   *
+   * @param  {string|array} suffix - Prefix to delete.
+   * @return {boolean}
+   */
+  delete(suffix) {
+    let node = this.root;
+    let toPrune = null;
+    let tokenToPrune = null;
+    let parent;
+    let token;
+
+    for (let i = suffix.length - 1; i >= 0; i--) {
+      token = suffix[i];
+      parent = node;
+      node = node[token];
+
+      // Prefix does not exist
+      if (typeof node === 'undefined')
+        return false;
+
+      // Keeping track of a potential branch to prune
+      if (toPrune !== null) {
+        if (Object.keys(node).length > 1) {
+          toPrune = null;
+          tokenToPrune = null;
+        }
+      }
+      else {
+        if (Object.keys(node).length < 2) {
+          toPrune = parent;
+          tokenToPrune = token;
+        }
+      }
+    }
+
+    if (!(SENTINEL in node)) return false;
+
+    this.size--;
+
+    if (toPrune) {
+      delete toPrune[tokenToPrune];
+    } else {
+      delete node[SENTINEL];
+    }
+
+    return true;
+  }
+
+  /**
+   * Method used to assert whether the given prefix exists in the Trie.
+   *
+   * @param  {string} suffix - Prefix to check.
+   * @return {boolean}
+   */
+  has(suffix) {
+    let node = this.root;
+    let token;
+
+    for (let i = suffix.length - 1; i >= 0; i--) {
+      token = suffix[i];
+      node = node[token];
+
+      if (typeof node === 'undefined')
+        return false;
+    }
+
+    return SENTINEL in node;
+  }
+
+  /**
+   * Method returning an iterator over the trie's prefixes.
+   *
+   * @param  {string|array} [prefix] - Optional starting prefix.
+   * @return {Iterator}
+   */
+  // prefixes(prefix) {
+  //   let node = this.root;
+  //   const nodeStack = [];
+  //   const prefixStack = [];
+  //   let token;
+  //   let i;
+  //   let l;
+
+  //   const isString = this.mode === 'string';
+
+  //   // Resolving initial prefix
+  //   if (prefix) {
+  //     for (i = 0, l = prefix.length; i < l; i++) {
+  //       token = prefix[i];
+  //       node = node[token];
+
+  //       // If the prefix does not exist, we return an empty iterator
+  //       if (typeof node === 'undefined')
+  //         return Iterator.empty();
+  //     }
+  //   }
+  //   else {
+  //     prefix = isString ? '' : [];
+  //   }
+
+  //   nodeStack.push(node);
+  //   prefixStack.push(prefix);
+
+  //   return new Iterator(() => {
+  //     let currentNode;
+  //     let currentPrefix;
+  //     let hasValue = false;
+  //     let k;
+
+  //     while (nodeStack.length) {
+  //       currentNode = nodeStack.pop();
+  //       currentPrefix = prefixStack.pop();
+
+  //       for (k in currentNode) {
+  //         if (k === SENTINEL) {
+  //           hasValue = true;
+  //           continue;
+  //         }
+
+  //         nodeStack.push(currentNode[k]);
+  //         prefixStack.push(isString ? currentPrefix + k : currentPrefix.concat(k));
+  //       }
+
+  //       if (hasValue)
+  //         return { done: false, value: currentPrefix };
+  //     }
+
+  //     return { done: true };
+  //   });
+  // }
+
+  /**
+   * Convenience known methods.
+   */
+  // inspect() {
+  //   const proxy = new Set();
+
+  //   const iterator = this.prefixes();
+  //   let step;
+
+  //   while ((step = iterator.next(), !step.done))
+  //     proxy.add(step.value);
+
+  //   // Trick so that node displays the name of the constructor
+  //   Object.defineProperty(proxy, 'constructor', {
+  //     value: Trie,
+  //     enumerable: false
+  //   });
+
+  //   return proxy;
+  // }
+  /**
+   * Static .from function taking an arbitrary iterable & converting it into
+   * a trie.
+   *
+   * @param  {string[]} iterable   - Target iterable.
+   * @return {Trie}
+   */
+  static from = iterable => {
+    const trie = new Trie();
+    iterable.forEach(i => trie.add(i));
+    return trie;
+  };
+}
+
+/**
+ * Exporting.
+ */
+module.exports.SENTINEL = SENTINEL;
+module.exports = Trie;

+ 185 - 0
Build/lib/trie.test.js

@@ -0,0 +1,185 @@
+require('chai').should();
+
+const Trie = require('./trie');
+const assert = require('assert');
+
+var SENTINEL = Trie.SENTINEL;
+
+describe('Trie', () => {
+  it('should be possible to add items to a Trie.', () => {
+    const trie = new Trie();
+
+    trie.add('sukka');
+    trie.add('ukka');
+    trie.add('akku');
+
+    trie.size.should.eq(3);
+    trie.has('sukka').should.eq(true);
+    trie.has('ukka').should.eq(true);
+    trie.has('akku').should.eq(true);
+    trie.has('noc').should.eq(false);
+    trie.has('suk').should.eq(false);
+    trie.has('sukkaw').should.eq(false);
+  });
+
+  it('adding the same item several times should not increase size.', () => {
+    const trie = new Trie();
+
+    trie.add('rat');
+    trie.add('erat');
+    trie.add('rat');
+
+    assert.strictEqual(trie.size, 2);
+    assert.strictEqual(trie.has('rat'), true);
+  });
+
+  it('should be possible to set the null sequence.', () => {
+    let trie = new Trie();
+
+    trie.add('');
+    trie.size.should.eq(1);
+    trie.has('').should.eq(true);
+
+    trie = new Trie(Array);
+
+    trie.add([]);
+    trie.size.should.eq(1);
+    trie.has([]).should.eq(true);
+  });
+
+  it('should be possible to delete items.', () => {
+    const trie = new Trie();
+
+    trie.add('rat');
+    trie.add('rate');
+    trie.add('tar');
+
+    assert.strictEqual(trie.delete(''), false);
+    trie.delete('').should.eq(false);
+    trie.delete('hello').should.eq(false);
+    
+    trie.delete('rat').should.eq(true);
+    trie.has('rat').should.eq(false);
+    trie.has('rate').should.eq(true);
+
+    trie.size.should.eq(2);
+
+    assert.strictEqual(trie.delete('rate'), true);
+
+    assert.strictEqual(trie.size, 1);
+
+    assert.strictEqual(trie.delete('tar'), true);
+
+    assert.strictEqual(trie.size, 0);
+  });
+
+  it('should be possible to check the existence of a sequence in the Trie.', () => {
+    const trie = new Trie();
+
+    trie.add('romanesque');
+
+    assert.strictEqual(trie.has('romanesque'), true);
+    assert.strictEqual(trie.has('roman'), false);
+    assert.strictEqual(trie.has(''), false);
+  });
+
+  it('should be possible to retrieve items matching the given prefix.', () => {
+    const trie = new Trie();
+
+    trie.add('roman');
+    trie.add('esqueroman');
+    trie.add('sesqueroman');
+    trie.add('greek');
+
+    assert.deepStrictEqual(trie.find('roman'), ['roman', 'esqueroman', 'sesqueroman']);
+    assert.deepStrictEqual(trie.find('man'), ['roman', 'esqueroman', 'sesqueroman']);
+    assert.deepStrictEqual(trie.find('esqueroman'), ['esqueroman', 'sesqueroman']);
+    assert.deepStrictEqual(trie.find('eek'), ['greek']);
+    assert.deepStrictEqual(trie.find('hello'), []);
+    assert.deepStrictEqual(trie.find(''), ['greek', 'roman', 'esqueroman', 'sesqueroman']);
+  });
+
+  // it('should work with custom tokens.', () => {
+  //   const trie = new Trie(Array);
+
+  //   trie.add(['the', 'cat', 'eats', 'the', 'mouse']);
+  //   trie.add(['the', 'mouse', 'eats', 'cheese']);
+  //   trie.add(['hello', 'world']);
+
+  //   assert.strictEqual(trie.size, 3);
+
+  //   assert.strictEqual(trie.has(['the', 'mouse', 'eats', 'cheese']), true);
+  //   assert.strictEqual(trie.has(['the', 'mouse', 'eats']), false);
+
+  //   assert.strictEqual(trie.delete(['hello']), false);
+  //   assert.strictEqual(trie.delete(['hello', 'world']), true);
+
+  //   assert.strictEqual(trie.size, 2);
+  // });
+
+  // it('should be possible to iterate over the trie\'s prefixes.', () => {
+  //   const trie = new Trie();
+
+  //   trie.add('rat');
+  //   trie.add('rate');
+
+  //   let prefixes = take(trie.prefixes());
+
+  //   assert.deepStrictEqual(prefixes, ['rat', 'rate']);
+
+  //   trie.add('rater');
+  //   trie.add('rates');
+
+  //   prefixes = take(trie.keys('rate'));
+
+  //   assert.deepStrictEqual(prefixes, ['rate', 'rates', 'rater']);
+  // });
+
+  // it('should be possible to iterate over the trie\'s prefixes using for...of.', () => {
+  //   const trie = new Trie();
+
+  //   trie.add('rat');
+  //   trie.add('rate');
+
+  //   const tests = [
+  //     'rat',
+  //     'rate'
+  //   ];
+
+  //   let i = 0;
+
+  //   for (const prefix of trie)
+  //     assert.deepStrictEqual(prefix, tests[i++]);
+  // });
+
+  it('should be possible to create a trie from an arbitrary iterable.', () => {
+    const words = ['roman', 'esqueroman'];
+
+    const trie = Trie.from(words);
+
+    assert.strictEqual(trie.size, 2);
+    assert.deepStrictEqual(trie.has('roman'), true);
+  });
+});
+
+describe('surge domainset dedupe', () => {
+  it('should not remove same entry', () => {
+    const trie = Trie.from(['.skk.moe', 'noc.one']);
+
+    trie.find('.skk.moe').should.eql(['.skk.moe']);
+    trie.find('noc.one').should.eql(['noc.one']);
+  });
+
+  it('should remove subdomain', () => {
+    const trie = Trie.from(['www.noc.one', 'www.sukkaw.com', 'blog.skk.moe', 'image.cdn.skk.moe', 'cdn.sukkaw.net']);
+    // trie.find('noc.one').should.eql(['www.noc.one']);
+    trie.find('.skk.moe').should.eql(['image.cdn.skk.moe', 'blog.skk.moe']);
+    // trie.find('sukkaw.net').should.eql(['cdn.sukkaw.net']);
+    trie.find('.sukkaw.com').should.eql(['www.sukkaw.com']);
+  });
+
+  it('should not remove non-subdomain', () => {
+    const trie = Trie.from(['skk.moe', 'sukkaskk.moe']);
+    trie.find('.skk.moe').should.eql([]);
+  });
+})

+ 3 - 0
package.json

@@ -125,7 +125,10 @@
     "undici": "5.22.1"
   },
   "devDependencies": {
+    "@types/mocha": "^10.0.1",
+    "chai": "^4.3.7",
     "eslint-plugin-import": "npm:eslint-plugin-i@2.27.5-4",
+    "mocha": "^10.2.0",
     "wireit": "^0.9.5"
   },
   "engines": {

+ 272 - 10
pnpm-lock.yaml

@@ -43,9 +43,18 @@ dependencies:
     version: 5.22.1
 
 devDependencies:
+  '@types/mocha':
+    specifier: ^10.0.1
+    version: 10.0.1
+  chai:
+    specifier: ^4.3.7
+    version: 4.3.7
   eslint-plugin-import:
     specifier: npm:eslint-plugin-i@2.27.5-4
     version: /eslint-plugin-i@2.27.5-4(eslint@8.44.0)
+  mocha:
+    specifier: ^10.2.0
+    version: 10.2.0
   wireit:
     specifier: ^0.9.5
     version: 0.9.5
@@ -104,7 +113,7 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       espree: 9.6.0
       globals: 13.20.0
       ignore: 5.2.4
@@ -126,7 +135,7 @@ packages:
     engines: {node: '>=10.10.0'}
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -220,13 +229,17 @@ packages:
     resolution: {integrity: sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==}
     dev: false
 
+  /@types/mocha@10.0.1:
+    resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==}
+    dev: true
+
   /@vercel/fetch-retry@5.1.3(node-fetch@2.6.11):
     resolution: {integrity: sha512-UIbFc4VsEZHOr6dWuE+kxY4NxnOLXFMCWm0fSKRRHUEtrIzaJLzHpWk2QskCXTSzFgFvhkLAvSrBK2XZg7NSzg==}
     peerDependencies:
       node-fetch: ^2.6.7
     dependencies:
       async-retry: 1.3.3
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       node-fetch: 2.6.11
     transitivePeerDependencies:
       - supports-color
@@ -264,6 +277,11 @@ packages:
       uri-js: 4.4.1
     dev: false
 
+  /ansi-colors@4.1.1:
+    resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
+    engines: {node: '>=6'}
+    dev: true
+
   /ansi-regex@5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
@@ -286,6 +304,10 @@ packages:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
     dev: true
 
+  /assertion-error@1.1.0:
+    resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+    dev: true
+
   /astral-regex@2.0.0:
     resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
     engines: {node: '>=8'}
@@ -317,6 +339,12 @@ packages:
       concat-map: 0.0.1
     dev: true
 
+  /brace-expansion@2.0.1:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+    dependencies:
+      balanced-match: 1.0.2
+    dev: true
+
   /braces@3.0.2:
     resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
     engines: {node: '>=8'}
@@ -324,6 +352,10 @@ packages:
       fill-range: 7.0.1
     dev: true
 
+  /browser-stdout@1.3.1:
+    resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
+    dev: true
+
   /busboy@1.6.0:
     resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
     engines: {node: '>=10.16.0'}
@@ -336,6 +368,24 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
+  /camelcase@6.3.0:
+    resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+    engines: {node: '>=10'}
+    dev: true
+
+  /chai@4.3.7:
+    resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==}
+    engines: {node: '>=4'}
+    dependencies:
+      assertion-error: 1.1.0
+      check-error: 1.0.2
+      deep-eql: 4.1.3
+      get-func-name: 2.0.0
+      loupe: 2.3.6
+      pathval: 1.1.1
+      type-detect: 4.0.8
+    dev: true
+
   /chalk@4.1.2:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
@@ -344,6 +394,10 @@ packages:
       supports-color: 7.2.0
     dev: true
 
+  /check-error@1.0.2:
+    resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
+    dev: true
+
   /chokidar@3.5.3:
     resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
     engines: {node: '>= 8.10.0'}
@@ -386,6 +440,14 @@ packages:
       string-natural-compare: 3.0.1
     dev: false
 
+  /cliui@7.0.4:
+    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 7.0.0
+    dev: true
+
   /color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
@@ -416,10 +478,10 @@ packages:
       supports-color:
         optional: true
     dependencies:
-      ms: 2.1.2
+      ms: 2.1.3
     dev: true
 
-  /debug@4.3.4:
+  /debug@4.3.4(supports-color@8.1.1):
     resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
     engines: {node: '>=6.0'}
     peerDependencies:
@@ -429,11 +491,29 @@ packages:
         optional: true
     dependencies:
       ms: 2.1.2
+      supports-color: 8.1.1
+
+  /decamelize@4.0.0:
+    resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
+    engines: {node: '>=10'}
+    dev: true
+
+  /deep-eql@4.1.3:
+    resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
+    engines: {node: '>=6'}
+    dependencies:
+      type-detect: 4.0.8
+    dev: true
 
   /deep-is@0.1.4:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     dev: true
 
+  /diff@5.0.0:
+    resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
+    engines: {node: '>=0.3.1'}
+    dev: true
+
   /doctrine@2.1.0:
     resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
     engines: {node: '>=0.10.0'}
@@ -450,7 +530,11 @@ packages:
 
   /emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-    dev: false
+
+  /escalade@3.1.1:
+    resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
+    engines: {node: '>=6'}
+    dev: true
 
   /escape-string-regexp@4.0.0:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
@@ -546,7 +630,7 @@ packages:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4
+      debug: 4.3.4(supports-color@8.1.1)
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.0
@@ -674,6 +758,11 @@ packages:
       rimraf: 3.0.2
     dev: true
 
+  /flat@5.0.2:
+    resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
+    hasBin: true
+    dev: true
+
   /flatted@3.2.7:
     resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
     dev: true
@@ -710,6 +799,15 @@ packages:
     resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
     dev: true
 
+  /get-caller-file@2.0.5:
+    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+    engines: {node: 6.* || 8.* || >= 10.*}
+    dev: true
+
+  /get-func-name@2.0.0:
+    resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
+    dev: true
+
   /get-tsconfig@4.6.2:
     resolution: {integrity: sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==}
     dependencies:
@@ -730,6 +828,17 @@ packages:
       is-glob: 4.0.3
     dev: true
 
+  /glob@7.2.0:
+    resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+    dev: true
+
   /glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
     dependencies:
@@ -758,7 +867,6 @@ packages:
   /has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
-    dev: true
 
   /has@1.0.3:
     resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
@@ -779,6 +887,11 @@ packages:
     resolution: {integrity: sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==}
     dev: false
 
+  /he@1.2.0:
+    resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+    hasBin: true
+    dev: true
+
   /ignore@5.2.4:
     resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
     engines: {node: '>= 4'}
@@ -839,7 +952,6 @@ packages:
   /is-fullwidth-code-point@3.0.0:
     resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
     engines: {node: '>=8'}
-    dev: false
 
   /is-glob@4.0.3:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
@@ -858,6 +970,16 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /is-plain-obj@2.1.0:
+    resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+    engines: {node: '>=8'}
+    dev: true
+
+  /is-unicode-supported@0.1.0:
+    resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+    engines: {node: '>=10'}
+    dev: true
+
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
@@ -916,6 +1038,20 @@ packages:
     resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
     dev: false
 
+  /log-symbols@4.1.0:
+    resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+    engines: {node: '>=10'}
+    dependencies:
+      chalk: 4.1.2
+      is-unicode-supported: 0.1.0
+    dev: true
+
+  /loupe@2.3.6:
+    resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==}
+    dependencies:
+      get-func-name: 2.0.0
+    dev: true
+
   /lru-cache@6.0.0:
     resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
     engines: {node: '>=10'}
@@ -942,6 +1078,13 @@ packages:
       brace-expansion: 1.1.11
     dev: true
 
+  /minimatch@5.0.1:
+    resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==}
+    engines: {node: '>=10'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: true
+
   /minipass@3.3.6:
     resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
     engines: {node: '>=8'}
@@ -968,9 +1111,47 @@ packages:
     hasBin: true
     dev: false
 
+  /mocha@10.2.0:
+    resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==}
+    engines: {node: '>= 14.0.0'}
+    hasBin: true
+    dependencies:
+      ansi-colors: 4.1.1
+      browser-stdout: 1.3.1
+      chokidar: 3.5.3
+      debug: 4.3.4(supports-color@8.1.1)
+      diff: 5.0.0
+      escape-string-regexp: 4.0.0
+      find-up: 5.0.0
+      glob: 7.2.0
+      he: 1.2.0
+      js-yaml: 4.1.0
+      log-symbols: 4.1.0
+      minimatch: 5.0.1
+      ms: 2.1.3
+      nanoid: 3.3.3
+      serialize-javascript: 6.0.0
+      strip-json-comments: 3.1.1
+      supports-color: 8.1.1
+      workerpool: 6.2.1
+      yargs: 16.2.0
+      yargs-parser: 20.2.4
+      yargs-unparser: 2.0.0
+    dev: true
+
   /ms@2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
 
+  /ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+    dev: true
+
+  /nanoid@3.3.3:
+    resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+    dev: true
+
   /natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
     dev: true
@@ -1075,6 +1256,10 @@ packages:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
     dev: true
 
+  /pathval@1.1.1:
+    resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+    dev: true
+
   /picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
     dev: false
@@ -1115,6 +1300,12 @@ packages:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
     dev: true
 
+  /randombytes@2.1.0:
+    resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+    dependencies:
+      safe-buffer: 5.2.1
+    dev: true
+
   /readdirp@3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -1122,6 +1313,11 @@ packages:
       picomatch: 2.3.1
     dev: true
 
+  /require-directory@2.1.1:
+    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
   /require-from-string@2.0.2:
     resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
     engines: {node: '>=0.10.0'}
@@ -1173,6 +1369,10 @@ packages:
       queue-microtask: 1.2.3
     dev: true
 
+  /safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+    dev: true
+
   /semver@7.5.3:
     resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==}
     engines: {node: '>=10'}
@@ -1181,6 +1381,12 @@ packages:
       lru-cache: 6.0.0
     dev: true
 
+  /serialize-javascript@6.0.0:
+    resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
+    dependencies:
+      randombytes: 2.1.0
+    dev: true
+
   /shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -1222,7 +1428,6 @@ packages:
       emoji-regex: 8.0.0
       is-fullwidth-code-point: 3.0.0
       strip-ansi: 6.0.1
-    dev: false
 
   /strip-ansi@6.0.1:
     resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
@@ -1242,6 +1447,12 @@ packages:
       has-flag: 4.0.0
     dev: true
 
+  /supports-color@8.1.1:
+    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+    engines: {node: '>=10'}
+    dependencies:
+      has-flag: 4.0.0
+
   /supports-preserve-symlinks-flag@1.0.0:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
@@ -1313,6 +1524,11 @@ packages:
       prelude-ls: 1.2.1
     dev: true
 
+  /type-detect@4.0.8:
+    resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+    engines: {node: '>=4'}
+    dev: true
+
   /type-fest@0.20.2:
     resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
     engines: {node: '>=10'}
@@ -1366,13 +1582,59 @@ packages:
       proper-lockfile: 4.1.2
     dev: true
 
+  /workerpool@6.2.1:
+    resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==}
+    dev: true
+
+  /wrap-ansi@7.0.0:
+    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+    engines: {node: '>=10'}
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+    dev: true
+
   /wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
     dev: true
 
+  /y18n@5.0.8:
+    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+    engines: {node: '>=10'}
+    dev: true
+
   /yallist@4.0.0:
     resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
 
+  /yargs-parser@20.2.4:
+    resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
+    engines: {node: '>=10'}
+    dev: true
+
+  /yargs-unparser@2.0.0:
+    resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
+    engines: {node: '>=10'}
+    dependencies:
+      camelcase: 6.3.0
+      decamelize: 4.0.0
+      flat: 5.0.2
+      is-plain-obj: 2.1.0
+    dev: true
+
+  /yargs@16.2.0:
+    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+    engines: {node: '>=10'}
+    dependencies:
+      cliui: 7.0.4
+      escalade: 3.1.1
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      string-width: 4.2.3
+      y18n: 5.0.8
+      yargs-parser: 20.2.4
+    dev: true
+
   /yocto-queue@0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}