CVE-2021-3803: Inefficient Regular Expression Complexity (CWE-1333)
value: nth-check is vulnerable to Inefficient Regular Expression Complexity
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
CVSS v3 base score: 7.5
Published: 2021-09-17T07:15Z
Repository: https://github.com/fb55/nth-check
Language: JSON
Files changed: package-lock.json, src/parse.ts
Commit date: 2021-09-14 22:42:54 +0100
Commit message: fix(parse): Replace regex with hand-rolled parser (#9)


Diff: diff --git a/package-lock.json b/package-lock.json
index 925b385..188f756 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3,10 +3,11 @@
     "version": "2.0.0",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
+            "name": "nth-check",
             "version": "2.0.0",
             "license": "BSD-2-Clause",
             "dependencies": {
                 "boolbase": "^1.0.0"
             },
@@ -724,11 +725,10 @@
                 "istanbul-reports": "^3.0.2",
                 "jest-haste-map": "^26.6.2",
                 "jest-resolve": "^26.6.2",
                 "jest-util": "^26.6.2",
                 "jest-worker": "^26.6.2",
-                "node-notifier": "^8.0.0",
                 "slash": "^3.0.0",
                 "source-map": "^0.6.0",
                 "string-length": "^4.0.1",
                 "terminal-link": "^2.0.0",
                 "v8-to-istanbul": "^7.0.0"
@@ -2226,12 +2226,11 @@
             "dev": true,
             "dependencies": {
                 "esprima": "^4.0.1",
                 "estraverse": "^4.2.0",
                 "esutils": "^2.0.2",
-                "optionator": "^0.8.1",
-                "source-map": "~0.6.1"
+                "optionator": "^0.8.1"
             },
             "bin": {
                 "escodegen": "bin/escodegen.js",
                 "esgenerate": "bin/esgenerate.js"
             },
@@ -3050,13 +3049,13 @@
             "funding": {
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
         "node_modules/glob-parent": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
             "dev": true,
             "dependencies": {
                 "is-glob": "^4.0.1"
             },
             "engines": {
@@ -3226,13 +3225,13 @@
             "engines": {
                 "node": ">=0.10.0"
             }
         },
         "node_modules/hosted-git-info": {
-            "version": "2.8.8",
-            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+            "version": "2.8.9",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
             "dev": true
         },
         "node_modules/html-encoding-sniffer": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -3856,11 +3855,10 @@
                 "@jest/types": "^26.6.2",
                 "@types/graceful-fs": "^4.1.2",
                 "@types/node": "*",
                 "anymatch": "^3.0.3",
                 "fb-watchman": "^2.0.0",
-                "fsevents": "^2.1.2",
                 "graceful-fs": "^4.2.4",
                 "jest-regex-util": "^26.0.0",
                 "jest-serializer": "^26.6.2",
                 "jest-util": "^26.6.2",
                 "jest-worker": "^26.6.2",
@@ -4413,13 +4411,13 @@
             "engines": {
                 "node": ">=8"
             }
         },
         "node_modules/lodash": {
-            "version": "4.17.20",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "node_modules/lodash.memoize": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -4998,13 +4996,13 @@
             "engines": {
                 "node": ">=8"
             }
         },
         "node_modules/path-parse": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
         "node_modules/path-type": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -7116,13 +7114,13 @@
             "bin": {
                 "mkdirp": "bin/cmd.js"
             }
         },
         "node_modules/ws": {
-            "version": "7.4.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
-            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
+            "version": "7.5.5",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
             "dev": true,
             "engines": {
                 "node": ">=8.3.0"
             },
             "peerDependencies": {
@@ -9638,13 +9636,13 @@
                 "once": "^1.3.0",
                 "path-is-absolute": "^1.0.0"
             }
         },
         "glob-parent": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
             "dev": true,
             "requires": {
                 "is-glob": "^4.0.1"
             }
         },
@@ -9774,13 +9772,13 @@
                     }
                 }
             }
         },
         "hosted-git-info": {
-            "version": "2.8.8",
-            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+            "version": "2.8.9",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
             "dev": true
         },
         "html-encoding-sniffer": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -10698,13 +10696,13 @@
             "requires": {
                 "p-locate": "^4.1.0"
             }
         },
         "lodash": {
-            "version": "4.17.20",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "lodash.memoize": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -11153,13 +11151,13 @@
             "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
             "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
             "dev": true
         },
         "path-parse": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
         "path-type": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -12826,13 +12824,13 @@
                 "signal-exit": "^3.0.2",
                 "typedarray-to-buffer": "^3.1.5"
             }
         },
         "ws": {
-            "version": "7.4.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
-            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
+            "version": "7.5.5",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
             "dev": true,
             "requires": {}
         },
         "xml-name-validator": {
             "version": "3.0.0",
diff --git a/src/parse.ts b/src/parse.ts
index 97171a3..5563a03 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -1,9 +1,11 @@
 // Following http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
 
-// [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
-const RE_NTH_ELEMENT = /^([+-]?\d*n)?\s*(?:([+-]?)\s*(\d+))?$/;
+// Whitespace as per https://www.w3.org/TR/selectors-3/#lex is " \t\r\n\f"
+const whitespace = new Set([9, 10, 12, 13, 32]);
+const ZERO = "0".charCodeAt(0);
+const NINE = "9".charCodeAt(0);
 
 /**
  * Parses an expression.
  *
  * @throws An `Error` if parsing fails.
@@ -17,26 +19,74 @@ export function parse(formula: string): [a: number, b: number] {
         return [2, 0];
     } else if (formula === "odd") {
         return [2, 1];
     }
 
-    const parsed = formula.match(RE_NTH_ELEMENT);
+    // Parse [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
 
-    if (!parsed) {
+    let idx = 0;
+
+    let a = 0;
+    let sign = readSign();
+    let number = readNumber();
+
+    if (idx < formula.length && formula.charAt(idx) === "n") {
+        idx++;
+        a = sign * (number ?? 1);
+
+        skipWhitespace();
+
+        if (idx < formula.length) {
+            sign = readSign();
+            skipWhitespace();
+            number = readNumber();
+        } else {
+            sign = number = 0;
+        }
+    }
+
+    // Throw if there is anything else
+    if (number === null || idx < formula.length) {
         throw new Error(`n-th rule couldn't be parsed ('${formula}')`);
     }
 
-    let a;
+    return [a, sign * number];
 
-    if (parsed[1]) {
-        a = parseInt(parsed[1], 10);
-        if (isNaN(a)) {
-            a = parsed[1].startsWith("-") ? -1 : 1;
+    function readSign() {
+        if (formula.charAt(idx) === "-") {
+            idx++;
+            return -1;
         }
-    } else a = 0;
 
-    const b =
-        (parsed[2] === "-" ? -1 : 1) *
-        (parsed[3] ? parseInt(parsed[3], 10) : 0);
+        if (formula.charAt(idx) === "+") {
+            idx++;
+        }
 
-    return [a, b];
+        return 1;
+    }
+
+    function readNumber() {
+        const start = idx;
+        let value = 0;
+
+        while (
+            idx < formula.length &&
+            formula.charCodeAt(idx) >= ZERO &&
+            formula.charCodeAt(idx) <= NINE
+        ) {
+            value = value * 10 + (formula.charCodeAt(idx) - ZERO);
+            idx++;
+        }
+
+        // Return `null` if we didn't read anything.
+        return idx === start ? null : value;
+    }
+
+    function skipWhitespace() {
+        while (
+            idx < formula.length &&
+            whitespace.has(formula.charCodeAt(idx))
+        ) {
+            idx++;
+        }
+    }
 }

Vulnerable code:                 "node-notifier": "^8.0.0",
                "optionator": "^0.8.1",
                "source-map": "~0.6.1"
            "version": "5.1.1",
            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
            "version": "2.8.8",
            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
                "fsevents": "^2.1.2",
            "version": "4.17.20",
            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
            "version": "1.0.6",
            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
            "version": "7.4.0",
            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
            "version": "5.1.1",
            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
            "version": "2.8.8",
            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
            "version": "4.17.20",
            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
            "version": "1.0.6",
            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
            "version": "7.4.0",
            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
// [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
const RE_NTH_ELEMENT = /^([+-]?\d*n)?\s*(?:([+-]?)\s*(\d+))?$/;
    const parsed = formula.match(RE_NTH_ELEMENT);
    if (!parsed) {
    let a;
    if (parsed[1]) {
        a = parseInt(parsed[1], 10);
        if (isNaN(a)) {
            a = parsed[1].startsWith("-") ? -1 : 1;
    } else a = 0;
    const b =
        (parsed[2] === "-" ? -1 : 1) *
        (parsed[3] ? parseInt(parsed[3], 10) : 0);
    return [a, b];
Fixed code:             "name": "nth-check",
                "optionator": "^0.8.1"
            "version": "5.1.2",
            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
            "version": "2.8.9",
            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
            "version": "4.17.21",
            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
            "version": "1.0.7",
            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
            "version": "7.5.5",
            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
            "version": "5.1.2",
            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
            "version": "2.8.9",
            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
            "version": "4.17.21",
            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
            "version": "1.0.7",
            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
            "version": "7.5.5",
            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
// Whitespace as per https://www.w3.org/TR/selectors-3/#lex is " \t\r\n\f"
const whitespace = new Set([9, 10, 12, 13, 32]);
const ZERO = "0".charCodeAt(0);
const NINE = "9".charCodeAt(0);
    // Parse [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
    let idx = 0;

    let a = 0;
    let sign = readSign();
    let number = readNumber();

    if (idx < formula.length && formula.charAt(idx) === "n") {
        idx++;
        a = sign * (number ?? 1);

        skipWhitespace();

        if (idx < formula.length) {
            sign = readSign();
            skipWhitespace();
            number = readNumber();
        } else {
            sign = number = 0;
        }
    }

    // Throw if there is anything else
    if (number === null || idx < formula.length) {
    return [a, sign * number];
    function readSign() {
        if (formula.charAt(idx) === "-") {
            idx++;
            return -1;
        if (formula.charAt(idx) === "+") {
            idx++;
        }
        return 1;
    }

    function readNumber() {
        const start = idx;
        let value = 0;

        while (
            idx < formula.length &&
            formula.charCodeAt(idx) >= ZERO &&
            formula.charCodeAt(idx) <= NINE
        ) {
            value = value * 10 + (formula.charCodeAt(idx) - ZERO);
            idx++;
        }

        // Return `null` if we didn't read anything.
        return idx === start ? null : value;
    }

    function skipWhitespace() {
        while (
            idx < formula.length &&
            whitespace.has(formula.charCodeAt(idx))
        ) {
            idx++;
        }
    }