gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: URL polyfill


From: gnunet
Subject: [taler-wallet-core] branch master updated: URL polyfill
Date: Thu, 10 Nov 2022 14:01:43 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new dec3a3035 URL polyfill
dec3a3035 is described below

commit dec3a30352da00435923dbf96c8feb4df56df6c7
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Nov 10 14:01:40 2022 +0100

    URL polyfill
---
 packages/taler-util/src/punycode.ts   |  468 ++++++++
 packages/taler-util/src/url.ts        |    8 +-
 packages/taler-util/src/whatwg-url.ts | 2107 +++++++++++++++++++++++++++++++++
 3 files changed, 2581 insertions(+), 2 deletions(-)

diff --git a/packages/taler-util/src/punycode.ts 
b/packages/taler-util/src/punycode.ts
new file mode 100644
index 000000000..353e3bf25
--- /dev/null
+++ b/packages/taler-util/src/punycode.ts
@@ -0,0 +1,468 @@
+/*
+Copyright Mathias Bynens <https://mathiasbynens.be/>
+Copyright (c) 2022 Taler Systems S.A.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+/** Highest positive signed 32-bit float value */
+const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1
+
+/** Bootstring parameters */
+const base = 36;
+const tMin = 1;
+const tMax = 26;
+const skew = 38;
+const damp = 700;
+const initialBias = 72;
+const initialN = 128; // 0x80
+const delimiter = "-"; // '\x2D'
+
+/** Regular expressions */
+const regexPunycode = /^xn--/;
+const regexNonASCII = /[^\0-\x7E]/; // non-ASCII chars
+const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators
+
+/** Error messages */
+const errors = {
+  overflow: "Overflow: input needs wider integers to process",
+  "not-basic": "Illegal input >= 0x80 (not a basic code point)",
+  "invalid-input": "Invalid input",
+} as { [x: string]: string };
+
+/** Convenience shortcuts */
+const baseMinusTMin = base - tMin;
+const floor = Math.floor;
+const stringFromCharCode = String.fromCharCode;
+
+/*--------------------------------------------------------------------------*/
+
+/**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+function error(type: string) {
+  throw new RangeError(errors[type]);
+}
+
+/**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+function map(array: any[], fn: (arg0: any) => any) {
+  const result = [];
+  let length = array.length;
+  while (length--) {
+    result[length] = fn(array[length]);
+  }
+  return result;
+}
+
+/**
+ * A simple `Array#map`-like wrapper to work with domain name strings or email
+ * addresses.
+ * @private
+ * @param {String} domain The domain name or email address.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+function mapDomain(
+  string: string,
+  fn: { (string: any): any; (string: any): any; (arg0: any): any },
+) {
+  const parts = string.split("@");
+  let result = "";
+  if (parts.length > 1) {
+    // In email addresses, only the domain name should be punycoded. Leave
+    // the local part (i.e. everything up to `@`) intact.
+    result = parts[0] + "@";
+    string = parts[1];
+  }
+  // Avoid `split(regex)` for IE8 compatibility. See #17.
+  string = string.replace(regexSeparators, "\x2E");
+  const labels = string.split(".");
+  const encoded = map(labels, fn).join(".");
+  return result + encoded;
+}
+
+/**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+function ucs2decode(string: string) {
+  const output = [];
+  let counter = 0;
+  const length = string.length;
+  while (counter < length) {
+    const value = string.charCodeAt(counter++);
+    if (value >= 0xd800 && value <= 0xdbff && counter < length) {
+      // It's a high surrogate, and there is a next character.
+      const extra = string.charCodeAt(counter++);
+      if ((extra & 0xfc00) == 0xdc00) {
+        // Low surrogate.
+        output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
+      } else {
+        // It's an unmatched surrogate; only append this code unit, in case the
+        // next code unit is the high surrogate of a surrogate pair.
+        output.push(value);
+        counter--;
+      }
+    } else {
+      output.push(value);
+    }
+  }
+  return output;
+}
+
+/**
+ * Creates a string based on an array of numeric code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of numeric code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+const ucs2encode = (array: any): string => String.fromCodePoint(...array);
+
+/**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic numeric code point value.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+const basicToDigit = function (codePoint: number) {
+  if (codePoint - 0x30 < 0x0a) {
+    return codePoint - 0x16;
+  }
+  if (codePoint - 0x41 < 0x1a) {
+    return codePoint - 0x41;
+  }
+  if (codePoint - 0x61 < 0x1a) {
+    return codePoint - 0x61;
+  }
+  return base;
+};
+
+/**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+const digitToBasic = function (digit: number, flag: number) {
+  //  0..25 map to ASCII a..z or A..Z
+  // 26..35 map to ASCII 0..9
+  return digit + 22 + 75 * Number(digit < 26) - (Number(flag != 0) << 5);
+};
+
+/**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * https://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+const adapt = function (delta: number, numPoints: number, firstTime: boolean) {
+  let k = 0;
+  delta = firstTime ? floor(delta / damp) : delta >> 1;
+  delta += floor(delta / numPoints);
+  for (
+    ;
+    /* no initialization */ delta > (baseMinusTMin * tMax) >> 1;
+    k += base
+  ) {
+    delta = floor(delta / baseMinusTMin);
+  }
+  return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
+};
+
+/**
+ * Converts a Punycode string of ASCII-only symbols to a string of Unicode
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII-only symbols.
+ * @returns {String} The resulting string of Unicode symbols.
+ */
+const decode = function (input: string) {
+  // Don't use UCS-2.
+  const output = [];
+  const inputLength = input.length;
+  let i = 0;
+  let n = initialN;
+  let bias = initialBias;
+
+  // Handle the basic code points: let `basic` be the number of input code
+  // points before the last delimiter, or `0` if there is none, then copy
+  // the first basic code points to the output.
+
+  let basic = input.lastIndexOf(delimiter);
+  if (basic < 0) {
+    basic = 0;
+  }
+
+  for (let j = 0; j < basic; ++j) {
+    // if it's not a basic code point
+    if (input.charCodeAt(j) >= 0x80) {
+      error("not-basic");
+    }
+    output.push(input.charCodeAt(j));
+  }
+
+  // Main decoding loop: start just after the last delimiter if any basic code
+  // points were copied; start at the beginning otherwise.
+
+  for (
+    let index = basic > 0 ? basic + 1 : 0;
+    index < inputLength /* no final expression */;
+
+  ) {
+    // `index` is the index of the next character to be consumed.
+    // Decode a generalized variable-length integer into `delta`,
+    // which gets added to `i`. The overflow checking is easier
+    // if we increase `i` as we go, then subtract off its starting
+    // value at the end to obtain `delta`.
+    let oldi = i;
+    for (let w = 1, k = base /* no condition */; ; k += base) {
+      if (index >= inputLength) {
+        error("invalid-input");
+      }
+
+      const digit = basicToDigit(input.charCodeAt(index++));
+
+      if (digit >= base || digit > floor((maxInt - i) / w)) {
+        error("overflow");
+      }
+
+      i += digit * w;
+      const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
+
+      if (digit < t) {
+        break;
+      }
+
+      const baseMinusT = base - t;
+      if (w > floor(maxInt / baseMinusT)) {
+        error("overflow");
+      }
+
+      w *= baseMinusT;
+    }
+
+    const out = output.length + 1;
+    bias = adapt(i - oldi, out, oldi == 0);
+
+    // `i` was supposed to wrap around from `out` to `0`,
+    // incrementing `n` each time, so we'll fix that now:
+    if (floor(i / out) > maxInt - n) {
+      error("overflow");
+    }
+
+    n += floor(i / out);
+    i %= out;
+
+    // Insert `n` at position `i` of the output.
+    output.splice(i++, 0, n);
+  }
+
+  return String.fromCodePoint(...output);
+};
+
+/**
+ * Converts a string of Unicode symbols (e.g. a domain name label) to a
+ * Punycode string of ASCII-only symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+const encode = function (inputArg: string) {
+  const output = [];
+
+  // Convert the input in UCS-2 to an array of Unicode code points.
+  let input = ucs2decode(inputArg);
+
+  // Cache the length.
+  let inputLength = input.length;
+
+  // Initialize the state.
+  let n = initialN;
+  let delta = 0;
+  let bias = initialBias;
+
+  // Handle the basic code points.
+  for (const currentValue of input) {
+    if (currentValue < 0x80) {
+      output.push(stringFromCharCode(currentValue));
+    }
+  }
+
+  let basicLength = output.length;
+  let handledCPCount = basicLength;
+
+  // `handledCPCount` is the number of code points that have been handled;
+  // `basicLength` is the number of basic code points.
+
+  // Finish the basic string with a delimiter unless it's empty.
+  if (basicLength) {
+    output.push(delimiter);
+  }
+
+  // Main encoding loop:
+  while (handledCPCount < inputLength) {
+    // All non-basic code points < n have been handled already. Find the next
+    // larger one:
+    let m = maxInt;
+    for (const currentValue of input) {
+      if (currentValue >= n && currentValue < m) {
+        m = currentValue;
+      }
+    }
+
+    // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+    // but guard against overflow.
+    const handledCPCountPlusOne = handledCPCount + 1;
+    if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+      error("overflow");
+    }
+
+    delta += (m - n) * handledCPCountPlusOne;
+    n = m;
+
+    for (const currentValue of input) {
+      if (currentValue < n && ++delta > maxInt) {
+        error("overflow");
+      }
+      if (currentValue == n) {
+        // Represent delta as a generalized variable-length integer.
+        let q = delta;
+        for (let k = base /* no condition */; ; k += base) {
+          const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
+          if (q < t) {
+            break;
+          }
+          const qMinusT = q - t;
+          const baseMinusT = base - t;
+          output.push(
+            stringFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0)),
+          );
+          q = floor(qMinusT / baseMinusT);
+        }
+
+        output.push(stringFromCharCode(digitToBasic(q, 0)));
+        bias = adapt(
+          delta,
+          handledCPCountPlusOne,
+          handledCPCount == basicLength,
+        );
+        delta = 0;
+        ++handledCPCount;
+      }
+    }
+
+    ++delta;
+    ++n;
+  }
+  return output.join("");
+};
+
+/**
+ * Converts a Punycode string representing a domain name or an email address
+ * to Unicode. Only the Punycoded parts of the input will be converted, i.e.
+ * it doesn't matter if you call it on a string that has already been
+ * converted to Unicode.
+ * @memberOf punycode
+ * @param {String} input The Punycoded domain name or email address to
+ * convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+const toUnicode = function (input: string) {
+  return mapDomain(input, function (string) {
+    return regexPunycode.test(string)
+      ? decode(string.slice(4).toLowerCase())
+      : string;
+  });
+};
+
+/**
+ * Converts a Unicode string representing a domain name or an email address to
+ * Punycode. Only the non-ASCII parts of the domain name will be converted,
+ * i.e. it doesn't matter if you call it with a domain that's already in
+ * ASCII.
+ * @memberOf punycode
+ * @param {String} input The domain name or email address to convert, as a
+ * Unicode string.
+ * @returns {String} The Punycode representation of the given domain name or
+ * email address.
+ */
+const toASCII = function (input: string) {
+  return mapDomain(input, function (string) {
+    return regexNonASCII.test(string) ? "xn--" + encode(string) : string;
+  });
+};
+
+/*--------------------------------------------------------------------------*/
+
+/** Define the public API */
+export const punycode = {
+  /**
+   * A string representing the current Punycode.js version number.
+   * @memberOf punycode
+   * @type String
+   */
+  version: "2.1.0",
+  /**
+   * An object of methods to convert from JavaScript's internal character
+   * representation (UCS-2) to Unicode code points, and back.
+   * @see <https://mathiasbynens.be/notes/javascript-encoding>
+   * @memberOf punycode
+   * @type Object
+   */
+  ucs2: {
+    decode: ucs2decode,
+    encode: ucs2encode,
+  },
+  decode: decode,
+  encode: encode,
+  toASCII: toASCII,
+  toUnicode: toUnicode,
+};
\ No newline at end of file
diff --git a/packages/taler-util/src/url.ts b/packages/taler-util/src/url.ts
index 7c5298ea0..245c4f8f7 100644
--- a/packages/taler-util/src/url.ts
+++ b/packages/taler-util/src/url.ts
@@ -14,6 +14,8 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { URLImpl, URLSearchParamsImpl } from "./whatwg-url.js";
+
 interface URL {
   hash: string;
   host: string;
@@ -83,7 +85,8 @@ export interface URLCtor {
 // @ts-ignore
 const _URL = globalThis.URL;
 if (!_URL) {
-  throw Error("FATAL: URL not available");
+  // @ts-ignore
+  globalThis.URL = URLImpl;
 }
 
 export const URL: URLCtor = _URL;
@@ -92,7 +95,8 @@ export const URL: URLCtor = _URL;
 const _URLSearchParams = globalThis.URLSearchParams;
 
 if (!_URLSearchParams) {
-  throw Error("FATAL: URLSearchParams not available");
+  // @ts-ignore
+  globalThis.URL = URLSearchParamsImpl;
 }
 
 export const URLSearchParams: URLSearchParamsCtor = _URLSearchParams;
diff --git a/packages/taler-util/src/whatwg-url.ts 
b/packages/taler-util/src/whatwg-url.ts
new file mode 100644
index 000000000..a0fe55d8f
--- /dev/null
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -0,0 +1,2107 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) Sebastian Mayr
+Copyright (c) 2022 Taler Systems S.A.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// Vendored with modifications (TypeScript etc.) from 
https://github.com/jsdom/whatwg-url
+
+const utf8Encoder = new TextEncoder();
+const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+
+function utf8Encode(string: string | undefined) {
+  return utf8Encoder.encode(string);
+}
+
+function utf8DecodeWithoutBOM(
+  bytes: DataView | ArrayBuffer | null | undefined,
+) {
+  return utf8Decoder.decode(bytes);
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-parser
+function parseUrlencoded(input: Uint8Array) {
+  const sequences = strictlySplitByteSequence(input, p("&"));
+  const output = [];
+  for (const bytes of sequences) {
+    if (bytes.length === 0) {
+      continue;
+    }
+
+    let name, value;
+    const indexOfEqual = bytes.indexOf(p("=")!);
+
+    if (indexOfEqual >= 0) {
+      name = bytes.slice(0, indexOfEqual);
+      value = bytes.slice(indexOfEqual + 1);
+    } else {
+      name = bytes;
+      value = new Uint8Array(0);
+    }
+
+    name = replaceByteInByteSequence(name, 0x2b, 0x20);
+    value = replaceByteInByteSequence(value, 0x2b, 0x20);
+
+    const nameString = utf8DecodeWithoutBOM(percentDecodeBytes(name));
+    const valueString = utf8DecodeWithoutBOM(percentDecodeBytes(value));
+
+    output.push([nameString, valueString]);
+  }
+  return output;
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-string-parser
+function parseUrlencodedString(input: string | undefined) {
+  return parseUrlencoded(utf8Encode(input));
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-serializer
+function serializeUrlencoded(tuples: any[], encodingOverride = undefined) {
+  let encoding = "utf-8";
+  if (encodingOverride !== undefined) {
+    // TODO "get the output encoding", i.e. handle encoding labels vs. names.
+    encoding = encodingOverride;
+  }
+
+  let output = "";
+  for (const [i, tuple] of tuples.entries()) {
+    // TODO: handle encoding override
+
+    const name = utf8PercentEncodeString(
+      tuple[0],
+      isURLEncodedPercentEncode,
+      true,
+    );
+
+    let value = tuple[1];
+    if (tuple.length > 2 && tuple[2] !== undefined) {
+      if (tuple[2] === "hidden" && name === "_charset_") {
+        value = encoding;
+      } else if (tuple[2] === "file") {
+        // value is a File object
+        value = value.name;
+      }
+    }
+
+    value = utf8PercentEncodeString(value, isURLEncodedPercentEncode, true);
+
+    if (i !== 0) {
+      output += "&";
+    }
+    output += `${name}=${value}`;
+  }
+  return output;
+}
+
+function strictlySplitByteSequence(buf: Uint8Array, cp: any) {
+  const list = [];
+  let last = 0;
+  let i = buf.indexOf(cp);
+  while (i >= 0) {
+    list.push(buf.slice(last, i));
+    last = i + 1;
+    i = buf.indexOf(cp, last);
+  }
+  if (last !== buf.length) {
+    list.push(buf.slice(last));
+  }
+  return list;
+}
+
+function replaceByteInByteSequence(buf: Uint8Array, from: number, to: number) {
+  let i = buf.indexOf(from);
+  while (i >= 0) {
+    buf[i] = to;
+    i = buf.indexOf(from, i + 1);
+  }
+  return buf;
+}
+
+function p(char: string) {
+  return char.codePointAt(0);
+}
+
+// https://url.spec.whatwg.org/#percent-encode
+function percentEncode(c: number) {
+  let hex = c.toString(16).toUpperCase();
+  if (hex.length === 1) {
+    hex = `0${hex}`;
+  }
+
+  return `%${hex}`;
+}
+
+// https://url.spec.whatwg.org/#percent-decode
+function percentDecodeBytes(input: Uint8Array) {
+  const output = new Uint8Array(input.byteLength);
+  let outputIndex = 0;
+  for (let i = 0; i < input.byteLength; ++i) {
+    const byte = input[i];
+    if (byte !== 0x25) {
+      output[outputIndex++] = byte;
+    } else if (
+      byte === 0x25 &&
+      (!isASCIIHex(input[i + 1]) || !isASCIIHex(input[i + 2]))
+    ) {
+      output[outputIndex++] = byte;
+    } else {
+      const bytePoint = parseInt(
+        String.fromCodePoint(input[i + 1], input[i + 2]),
+        16,
+      );
+      output[outputIndex++] = bytePoint;
+      i += 2;
+    }
+  }
+
+  return output.slice(0, outputIndex);
+}
+
+// https://url.spec.whatwg.org/#string-percent-decode
+function percentDecodeString(input: string) {
+  const bytes = utf8Encode(input);
+  return percentDecodeBytes(bytes);
+}
+
+// https://url.spec.whatwg.org/#c0-control-percent-encode-set
+function isC0ControlPercentEncode(c: number) {
+  return c <= 0x1f || c > 0x7e;
+}
+
+// https://url.spec.whatwg.org/#fragment-percent-encode-set
+const extraFragmentPercentEncodeSet = new Set([
+  p(" "),
+  p('"'),
+  p("<"),
+  p(">"),
+  p("`"),
+]);
+
+function isFragmentPercentEncode(c: number) {
+  return isC0ControlPercentEncode(c) || extraFragmentPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#query-percent-encode-set
+const extraQueryPercentEncodeSet = new Set([
+  p(" "),
+  p('"'),
+  p("#"),
+  p("<"),
+  p(">"),
+]);
+
+function isQueryPercentEncode(c: number) {
+  return isC0ControlPercentEncode(c) || extraQueryPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#special-query-percent-encode-set
+function isSpecialQueryPercentEncode(c: number) {
+  return isQueryPercentEncode(c) || c === p("'");
+}
+
+// https://url.spec.whatwg.org/#path-percent-encode-set
+const extraPathPercentEncodeSet = new Set([p("?"), p("`"), p("{"), p("}")]);
+function isPathPercentEncode(c: number) {
+  return isQueryPercentEncode(c) || extraPathPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#userinfo-percent-encode-set
+const extraUserinfoPercentEncodeSet = new Set([
+  p("/"),
+  p(":"),
+  p(";"),
+  p("="),
+  p("@"),
+  p("["),
+  p("\\"),
+  p("]"),
+  p("^"),
+  p("|"),
+]);
+function isUserinfoPercentEncode(c: number) {
+  return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#component-percent-encode-set
+const extraComponentPercentEncodeSet = new Set([
+  p("$"),
+  p("%"),
+  p("&"),
+  p("+"),
+  p(","),
+]);
+function isComponentPercentEncode(c: number) {
+  return isUserinfoPercentEncode(c) || extraComponentPercentEncodeSet.has(c);
+}
+
+// 
https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
+const extraURLEncodedPercentEncodeSet = new Set([
+  p("!"),
+  p("'"),
+  p("("),
+  p(")"),
+  p("~"),
+]);
+
+function isURLEncodedPercentEncode(c: number) {
+  return isComponentPercentEncode(c) || extraURLEncodedPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#code-point-percent-encode-after-encoding
+// https://url.spec.whatwg.org/#utf-8-percent-encode
+// Assuming encoding is always utf-8 allows us to trim one of the logic 
branches. TODO: support encoding.
+// The "-Internal" variant here has code points as JS strings. The external 
version used by other files has code points
+// as JS numbers, like the rest of the codebase.
+function utf8PercentEncodeCodePointInternal(
+  codePoint: string,
+  percentEncodePredicate: (arg0: number) => any,
+) {
+  const bytes = utf8Encode(codePoint);
+  let output = "";
+  for (const byte of bytes) {
+    // Our percentEncodePredicate operates on bytes, not code points, so this 
is slightly different from the spec.
+    if (!percentEncodePredicate(byte)) {
+      output += String.fromCharCode(byte);
+    } else {
+      output += percentEncode(byte);
+    }
+  }
+
+  return output;
+}
+
+function utf8PercentEncodeCodePoint(
+  codePoint: number,
+  percentEncodePredicate: (arg0: number) => any,
+) {
+  return utf8PercentEncodeCodePointInternal(
+    String.fromCodePoint(codePoint),
+    percentEncodePredicate,
+  );
+}
+
+// https://url.spec.whatwg.org/#string-percent-encode-after-encoding
+// https://url.spec.whatwg.org/#string-utf-8-percent-encode
+function utf8PercentEncodeString(
+  input: string,
+  percentEncodePredicate: {
+    (c: number): boolean;
+    (c: number): boolean;
+    (arg0: number): any;
+  },
+  spaceAsPlus = false,
+) {
+  let output = "";
+  for (const codePoint of input) {
+    if (spaceAsPlus && codePoint === " ") {
+      output += "+";
+    } else {
+      output += utf8PercentEncodeCodePointInternal(
+        codePoint,
+        percentEncodePredicate,
+      );
+    }
+  }
+  return output;
+}
+
+// Note that we take code points as JS numbers, not JS strings.
+
+function isASCIIDigit(c: number) {
+  return c >= 0x30 && c <= 0x39;
+}
+
+function isASCIIAlpha(c: number) {
+  return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
+}
+
+function isASCIIAlphanumeric(c: number) {
+  return isASCIIAlpha(c) || isASCIIDigit(c);
+}
+
+function isASCIIHex(c: number) {
+  return (
+    isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66)
+  );
+}
+
+export class URLSearchParamsImpl {
+  _list: any[];
+  _url: any;
+  constructor(constructorArgs: any[], { doNotStripQMark = false }: any) {
+    let init = constructorArgs[0];
+    this._list = [];
+    this._url = null;
+
+    if (!doNotStripQMark && typeof init === "string" && init[0] === "?") {
+      init = init.slice(1);
+    }
+
+    if (Array.isArray(init)) {
+      for (const pair of init) {
+        if (pair.length !== 2) {
+          throw new TypeError(
+            "Failed to construct 'URLSearchParams': parameter 1 sequence's 
element does not " +
+              "contain exactly two elements.",
+          );
+        }
+        this._list.push([pair[0], pair[1]]);
+      }
+    } else if (
+      typeof init === "object" &&
+      Object.getPrototypeOf(init) === null
+    ) {
+      for (const name of Object.keys(init)) {
+        const value = init[name];
+        this._list.push([name, value]);
+      }
+    } else {
+      this._list = parseUrlencodedString(init);
+    }
+  }
+
+  _updateSteps() {
+    if (this._url !== null) {
+      let query: string | null = serializeUrlencoded(this._list);
+      if (query === "") {
+        query = null;
+      }
+      this._url._url.query = query;
+    }
+  }
+
+  append(name: string, value: string) {
+    this._list.push([name, value]);
+    this._updateSteps();
+  }
+
+  delete(name: string) {
+    let i = 0;
+    while (i < this._list.length) {
+      if (this._list[i][0] === name) {
+        this._list.splice(i, 1);
+      } else {
+        i++;
+      }
+    }
+    this._updateSteps();
+  }
+
+  get(name: string) {
+    for (const tuple of this._list) {
+      if (tuple[0] === name) {
+        return tuple[1];
+      }
+    }
+    return null;
+  }
+
+  getAll(name: string) {
+    const output = [];
+    for (const tuple of this._list) {
+      if (tuple[0] === name) {
+        output.push(tuple[1]);
+      }
+    }
+    return output;
+  }
+
+  has(name: string) {
+    for (const tuple of this._list) {
+      if (tuple[0] === name) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  set(name: string, value: string) {
+    let found = false;
+    let i = 0;
+    while (i < this._list.length) {
+      if (this._list[i][0] === name) {
+        if (found) {
+          this._list.splice(i, 1);
+        } else {
+          found = true;
+          this._list[i][1] = value;
+          i++;
+        }
+      } else {
+        i++;
+      }
+    }
+    if (!found) {
+      this._list.push([name, value]);
+    }
+    this._updateSteps();
+  }
+
+  sort() {
+    this._list.sort((a, b) => {
+      if (a[0] < b[0]) {
+        return -1;
+      }
+      if (a[0] > b[0]) {
+        return 1;
+      }
+      return 0;
+    });
+
+    this._updateSteps();
+  }
+
+  [Symbol.iterator]() {
+    return this._list[Symbol.iterator]();
+  }
+
+  toString() {
+    return serializeUrlencoded(this._list);
+  }
+}
+
+const specialSchemes = {
+  ftp: 21,
+  file: null,
+  http: 80,
+  https: 443,
+  ws: 80,
+  wss: 443,
+} as { [x: string]: number | null };
+
+const failure = Symbol("failure");
+
+function countSymbols(str: any) {
+  return [...str].length;
+}
+
+function at(input: any, idx: any) {
+  const c = input[idx];
+  return isNaN(c) ? undefined : String.fromCodePoint(c);
+}
+
+function isSingleDot(buffer: string) {
+  return buffer === "." || buffer.toLowerCase() === "%2e";
+}
+
+function isDoubleDot(buffer: string) {
+  buffer = buffer.toLowerCase();
+  return (
+    buffer === ".." ||
+    buffer === "%2e." ||
+    buffer === ".%2e" ||
+    buffer === "%2e%2e"
+  );
+}
+
+function isWindowsDriveLetterCodePoints(cp1: number, cp2: number) {
+  return isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
+}
+
+function isWindowsDriveLetterString(string: string) {
+  return (
+    string.length === 2 &&
+    isASCIIAlpha(string.codePointAt(0)!) &&
+    (string[1] === ":" || string[1] === "|")
+  );
+}
+
+function isNormalizedWindowsDriveLetterString(string: string) {
+  return (
+    string.length === 2 &&
+    isASCIIAlpha(string.codePointAt(0)!) &&
+    string[1] === ":"
+  );
+}
+
+function containsForbiddenHostCodePoint(string: string) {
+  return (
+    string.search(
+      /\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|<|>|\?|@|\[|\\|\]|\^|\|/u,
+    ) !== -1
+  );
+}
+
+function containsForbiddenDomainCodePoint(string: string) {
+  return (
+    containsForbiddenHostCodePoint(string) ||
+    string.search(/[\u0000-\u001F]|%|\u007F/u) !== -1
+  );
+}
+
+function isSpecialScheme(scheme: string) {
+  return specialSchemes[scheme] !== undefined;
+}
+
+function isSpecial(url: any) {
+  return isSpecialScheme(url.scheme);
+}
+
+function isNotSpecial(url: UrlObj) {
+  return !isSpecialScheme(url.scheme);
+}
+
+function defaultPort(scheme: string) {
+  return specialSchemes[scheme];
+}
+
+function parseIPv4Number(input: string) {
+  if (input === "") {
+    return failure;
+  }
+
+  let R = 10;
+
+  if (
+    input.length >= 2 &&
+    input.charAt(0) === "0" &&
+    input.charAt(1).toLowerCase() === "x"
+  ) {
+    input = input.substring(2);
+    R = 16;
+  } else if (input.length >= 2 && input.charAt(0) === "0") {
+    input = input.substring(1);
+    R = 8;
+  }
+
+  if (input === "") {
+    return 0;
+  }
+
+  let regex = /[^0-7]/u;
+  if (R === 10) {
+    regex = /[^0-9]/u;
+  }
+  if (R === 16) {
+    regex = /[^0-9A-Fa-f]/u;
+  }
+
+  if (regex.test(input)) {
+    return failure;
+  }
+
+  return parseInt(input, R);
+}
+
+function parseIPv4(input: string) {
+  const parts = input.split(".");
+  if (parts[parts.length - 1] === "") {
+    if (parts.length > 1) {
+      parts.pop();
+    }
+  }
+
+  if (parts.length > 4) {
+    return failure;
+  }
+
+  const numbers = [];
+  for (const part of parts) {
+    const n = parseIPv4Number(part);
+    if (n === failure) {
+      return failure;
+    }
+
+    numbers.push(n);
+  }
+
+  for (let i = 0; i < numbers.length - 1; ++i) {
+    if (numbers[i] > 255) {
+      return failure;
+    }
+  }
+  if (numbers[numbers.length - 1] >= 256 ** (5 - numbers.length)) {
+    return failure;
+  }
+
+  let ipv4 = numbers.pop();
+  let counter = 0;
+
+  for (const n of numbers) {
+    ipv4! += n * 256 ** (3 - counter);
+    ++counter;
+  }
+
+  return ipv4;
+}
+
+function serializeIPv4(address: number) {
+  let output = "";
+  let n = address;
+
+  for (let i = 1; i <= 4; ++i) {
+    output = String(n % 256) + output;
+    if (i !== 4) {
+      output = `.${output}`;
+    }
+    n = Math.floor(n / 256);
+  }
+
+  return output;
+}
+
+function parseIPv6(inputArg: string) {
+  const address = [0, 0, 0, 0, 0, 0, 0, 0];
+  let pieceIndex = 0;
+  let compress = null;
+  let pointer = 0;
+
+  const input = Array.from(inputArg, (c) => c.codePointAt(0));
+
+  if (input[pointer] === p(":")) {
+    if (input[pointer + 1] !== p(":")) {
+      return failure;
+    }
+
+    pointer += 2;
+    ++pieceIndex;
+    compress = pieceIndex;
+  }
+
+  while (pointer < input.length) {
+    if (pieceIndex === 8) {
+      return failure;
+    }
+
+    if (input[pointer] === p(":")) {
+      if (compress !== null) {
+        return failure;
+      }
+      ++pointer;
+      ++pieceIndex;
+      compress = pieceIndex;
+      continue;
+    }
+
+    let value = 0;
+    let length = 0;
+
+    while (length < 4 && isASCIIHex(input[pointer]!)) {
+      value = value * 0x10 + parseInt(at(input, pointer)!, 16);
+      ++pointer;
+      ++length;
+    }
+
+    if (input[pointer] === p(".")) {
+      if (length === 0) {
+        return failure;
+      }
+
+      pointer -= length;
+
+      if (pieceIndex > 6) {
+        return failure;
+      }
+
+      let numbersSeen = 0;
+
+      while (input[pointer] !== undefined) {
+        let ipv4Piece = null;
+
+        if (numbersSeen > 0) {
+          if (input[pointer] === p(".") && numbersSeen < 4) {
+            ++pointer;
+          } else {
+            return failure;
+          }
+        }
+
+        if (!isASCIIDigit(input[pointer]!)) {
+          return failure;
+        }
+
+        while (isASCIIDigit(input[pointer]!)) {
+          const number = parseInt(at(input, pointer)!);
+          if (ipv4Piece === null) {
+            ipv4Piece = number;
+          } else if (ipv4Piece === 0) {
+            return failure;
+          } else {
+            ipv4Piece = ipv4Piece * 10 + number;
+          }
+          if (ipv4Piece > 255) {
+            return failure;
+          }
+          ++pointer;
+        }
+
+        address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece!;
+
+        ++numbersSeen;
+
+        if (numbersSeen === 2 || numbersSeen === 4) {
+          ++pieceIndex;
+        }
+      }
+
+      if (numbersSeen !== 4) {
+        return failure;
+      }
+
+      break;
+    } else if (input[pointer] === p(":")) {
+      ++pointer;
+      if (input[pointer] === undefined) {
+        return failure;
+      }
+    } else if (input[pointer] !== undefined) {
+      return failure;
+    }
+
+    address[pieceIndex] = value;
+    ++pieceIndex;
+  }
+
+  if (compress !== null) {
+    let swaps = pieceIndex - compress;
+    pieceIndex = 7;
+    while (pieceIndex !== 0 && swaps > 0) {
+      const temp = address[compress + swaps - 1];
+      address[compress + swaps - 1] = address[pieceIndex];
+      address[pieceIndex] = temp;
+      --pieceIndex;
+      --swaps;
+    }
+  } else if (compress === null && pieceIndex !== 8) {
+    return failure;
+  }
+
+  return address;
+}
+
+function serializeIPv6(address: any[]) {
+  let output = "";
+  const compress = findLongestZeroSequence(address);
+  let ignore0 = false;
+
+  for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) {
+    if (ignore0 && address[pieceIndex] === 0) {
+      continue;
+    } else if (ignore0) {
+      ignore0 = false;
+    }
+
+    if (compress === pieceIndex) {
+      const separator = pieceIndex === 0 ? "::" : ":";
+      output += separator;
+      ignore0 = true;
+      continue;
+    }
+
+    output += address[pieceIndex].toString(16);
+
+    if (pieceIndex !== 7) {
+      output += ":";
+    }
+  }
+
+  return output;
+}
+
+function parseHost(input: string, isNotSpecialArg = false) {
+  if (input[0] === "[") {
+    if (input[input.length - 1] !== "]") {
+      return failure;
+    }
+
+    return parseIPv6(input.substring(1, input.length - 1));
+  }
+
+  if (isNotSpecialArg) {
+    return parseOpaqueHost(input);
+  }
+
+  const domain = utf8DecodeWithoutBOM(percentDecodeString(input));
+  const asciiDomain = domainToASCII(domain);
+  if (asciiDomain === failure) {
+    return failure;
+  }
+
+  if (containsForbiddenDomainCodePoint(asciiDomain)) {
+    return failure;
+  }
+
+  if (endsInANumber(asciiDomain)) {
+    return parseIPv4(asciiDomain);
+  }
+
+  return asciiDomain;
+}
+
+function endsInANumber(input: string) {
+  const parts = input.split(".");
+  if (parts[parts.length - 1] === "") {
+    if (parts.length === 1) {
+      return false;
+    }
+    parts.pop();
+  }
+
+  const last = parts[parts.length - 1];
+  if (parseIPv4Number(last) !== failure) {
+    return true;
+  }
+
+  if (/^[0-9]+$/u.test(last)) {
+    return true;
+  }
+
+  return false;
+}
+
+function parseOpaqueHost(input: string) {
+  if (containsForbiddenHostCodePoint(input)) {
+    return failure;
+  }
+
+  return utf8PercentEncodeString(input, isC0ControlPercentEncode);
+}
+
+function findLongestZeroSequence(arr: number[]) {
+  let maxIdx = null;
+  let maxLen = 1; // only find elements > 1
+  let currStart = null;
+  let currLen = 0;
+
+  for (let i = 0; i < arr.length; ++i) {
+    if (arr[i] !== 0) {
+      if (currLen > maxLen) {
+        maxIdx = currStart;
+        maxLen = currLen;
+      }
+
+      currStart = null;
+      currLen = 0;
+    } else {
+      if (currStart === null) {
+        currStart = i;
+      }
+      ++currLen;
+    }
+  }
+
+  // if trailing zeros
+  if (currLen > maxLen) {
+    return currStart;
+  }
+
+  return maxIdx;
+}
+
+function serializeHost(host: number | number[] | string) {
+  if (typeof host === "number") {
+    return serializeIPv4(host);
+  }
+
+  // IPv6 serializer
+  if (host instanceof Array) {
+    return `[${serializeIPv6(host)}]`;
+  }
+
+  return host;
+}
+
+import { punycode } from "./punycode.js";
+
+function domainToASCII(domain: string, beStrict = false) {
+  // const result = tr46.toASCII(domain, {
+  //   checkBidi: true,
+  //   checkHyphens: false,
+  //   checkJoiners: true,
+  //   useSTD3ASCIIRules: beStrict,
+  //   verifyDNSLength: beStrict,
+  // });
+  let result;
+  try {
+    result = punycode.toASCII(domain);
+  } catch (e) {
+    return failure;
+  }
+  if (result === null || result === "") {
+    return failure;
+  }
+  return result;
+}
+
+function trimControlChars(url: string) {
+  return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/gu, "");
+}
+
+function trimTabAndNewline(url: string) {
+  return url.replace(/\u0009|\u000A|\u000D/gu, "");
+}
+
+function shortenPath(url: UrlObj) {
+  const { path } = url;
+  if (path.length === 0) {
+    return;
+  }
+  if (
+    url.scheme === "file" &&
+    path.length === 1 &&
+    isNormalizedWindowsDriveLetter(path[0])
+  ) {
+    return;
+  }
+
+  path.pop();
+}
+
+function includesCredentials(url: UrlObj) {
+  return url.username !== "" || url.password !== "";
+}
+
+function cannotHaveAUsernamePasswordPort(url: UrlObj) {
+  return url.host === null || url.host === "" || url.scheme === "file";
+}
+
+function hasAnOpaquePath(url: UrlObj) {
+  return typeof url.path === "string";
+}
+
+function isNormalizedWindowsDriveLetter(string: string) {
+  return /^[A-Za-z]:$/u.test(string);
+}
+
+export interface UrlObj {
+  scheme: string;
+  username: string;
+  password: string;
+  host: string | number[] | number | null | undefined;
+  port: number | null;
+  path: string[];
+  query: any;
+  fragment: any;
+}
+
+class URLStateMachine {
+  pointer: number;
+  input: number[];
+  base: any;
+  encodingOverride: string;
+  url: UrlObj;
+  state: string;
+  stateOverride: string;
+  failure: boolean;
+  parseError: boolean;
+  buffer: string;
+  atFlag: boolean;
+  arrFlag: boolean;
+  passwordTokenSeenFlag: boolean;
+
+  constructor(
+    input: string,
+    base: any,
+    encodingOverride: string,
+    url: UrlObj,
+    stateOverride: string,
+  ) {
+    this.pointer = 0;
+    this.base = base || null;
+    this.encodingOverride = encodingOverride || "utf-8";
+    this.url = url;
+    this.failure = false;
+    this.parseError = false;
+
+    if (!this.url) {
+      this.url = {
+        scheme: "",
+        username: "",
+        password: "",
+        host: null,
+        port: null,
+        path: [],
+        query: null,
+        fragment: null,
+      };
+
+      const res = trimControlChars(input);
+      if (res !== input) {
+        this.parseError = true;
+      }
+      input = res;
+    }
+
+    const res = trimTabAndNewline(input);
+    if (res !== input) {
+      this.parseError = true;
+    }
+    input = res;
+
+    this.state = stateOverride || "scheme start";
+
+    this.buffer = "";
+    this.atFlag = false;
+    this.arrFlag = false;
+    this.passwordTokenSeenFlag = false;
+
+    this.input = Array.from(input, (c) => c.codePointAt(0)!);
+
+    for (; this.pointer <= this.input.length; ++this.pointer) {
+      const c = this.input[this.pointer];
+      const cStr = isNaN(c) ? undefined : String.fromCodePoint(c);
+
+      // exec state machine
+      const ret = this.table[`parse ${this.state}`].call(this, c, cStr!);
+      if (!ret) {
+        break; // terminate algorithm
+      } else if (ret === failure) {
+        this.failure = true;
+        break;
+      }
+    }
+  }
+
+  table = {
+    "parse scheme start": this.parseSchemeStart,
+    "parse scheme": this.parseScheme,
+    "parse no scheme": this.parseNoScheme,
+    "parse special relative or authority": 
this.parseSpecialRelativeOrAuthority,
+    "parse path or authority": this.parsePathOrAuthority,
+    "parse relative": this.parseRelative,
+    "parse relative slash": this.parseRelativeSlash,
+    "parse special authority slashes": this.parseSpecialAuthoritySlashes,
+    "parse special authority ignore slashes":
+      this.parseSpecialAuthorityIgnoreSlashes,
+    "parse authority": this.parseAuthority,
+    "parse host": this.parseHostName,
+    "parse hostname": this.parseHostName /* intentional duplication */,
+    "parse port": this.parsePort,
+    "parse file": this.parseFile,
+    "parse file slash": this.parseFileSlash,
+    "parse file host": this.parseFileHost,
+    "parse path start": this.parsePathStart,
+    "parse path": this.parsePath,
+    "parse opaque path": this.parseOpaquePath,
+    "parse query": this.parseQuery,
+    "parse fragment": this.parseFragment,
+  } as { [x: string]: (c: number, cStr: string) => any };
+
+  parseSchemeStart(c: number, cStr: string) {
+    if (isASCIIAlpha(c)) {
+      this.buffer += cStr.toLowerCase();
+      this.state = "scheme";
+    } else if (!this.stateOverride) {
+      this.state = "no scheme";
+      --this.pointer;
+    } else {
+      this.parseError = true;
+      return failure;
+    }
+
+    return true;
+  }
+
+  parseScheme(c: number, cStr: string) {
+    if (
+      isASCIIAlphanumeric(c) ||
+      c === p("+") ||
+      c === p("-") ||
+      c === p(".")
+    ) {
+      this.buffer += cStr.toLowerCase();
+    } else if (c === p(":")) {
+      if (this.stateOverride) {
+        if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) {
+          return false;
+        }
+
+        if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) {
+          return false;
+        }
+
+        if (
+          (includesCredentials(this.url) || this.url.port !== null) &&
+          this.buffer === "file"
+        ) {
+          return false;
+        }
+
+        if (this.url.scheme === "file" && this.url.host === "") {
+          return false;
+        }
+      }
+      this.url.scheme = this.buffer;
+      if (this.stateOverride) {
+        if (this.url.port === defaultPort(this.url.scheme)) {
+          this.url.port = null;
+        }
+        return false;
+      }
+      this.buffer = "";
+      if (this.url.scheme === "file") {
+        if (
+          this.input[this.pointer + 1] !== p("/") ||
+          this.input[this.pointer + 2] !== p("/")
+        ) {
+          this.parseError = true;
+        }
+        this.state = "file";
+      } else if (
+        isSpecial(this.url) &&
+        this.base !== null &&
+        this.base.scheme === this.url.scheme
+      ) {
+        this.state = "special relative or authority";
+      } else if (isSpecial(this.url)) {
+        this.state = "special authority slashes";
+      } else if (this.input[this.pointer + 1] === p("/")) {
+        this.state = "path or authority";
+        ++this.pointer;
+      } else {
+        this.url.path = [""];
+        this.state = "opaque path";
+      }
+    } else if (!this.stateOverride) {
+      this.buffer = "";
+      this.state = "no scheme";
+      this.pointer = -1;
+    } else {
+      this.parseError = true;
+      return failure;
+    }
+
+    return true;
+  }
+
+  parseNoScheme(c: number) {
+    if (this.base === null || (hasAnOpaquePath(this.base) && c !== p("#"))) {
+      return failure;
+    } else if (hasAnOpaquePath(this.base) && c === p("#")) {
+      this.url.scheme = this.base.scheme;
+      this.url.path = this.base.path;
+      this.url.query = this.base.query;
+      this.url.fragment = "";
+      this.state = "fragment";
+    } else if (this.base.scheme === "file") {
+      this.state = "file";
+      --this.pointer;
+    } else {
+      this.state = "relative";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseSpecialRelativeOrAuthority(c: number) {
+    if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
+      this.state = "special authority ignore slashes";
+      ++this.pointer;
+    } else {
+      this.parseError = true;
+      this.state = "relative";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parsePathOrAuthority(c: number) {
+    if (c === p("/")) {
+      this.state = "authority";
+    } else {
+      this.state = "path";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseRelative(c: number) {
+    this.url.scheme = this.base.scheme;
+    if (c === p("/")) {
+      this.state = "relative slash";
+    } else if (isSpecial(this.url) && c === p("\\")) {
+      this.parseError = true;
+      this.state = "relative slash";
+    } else {
+      this.url.username = this.base.username;
+      this.url.password = this.base.password;
+      this.url.host = this.base.host;
+      this.url.port = this.base.port;
+      this.url.path = this.base.path.slice();
+      this.url.query = this.base.query;
+      if (c === p("?")) {
+        this.url.query = "";
+        this.state = "query";
+      } else if (c === p("#")) {
+        this.url.fragment = "";
+        this.state = "fragment";
+      } else if (!isNaN(c)) {
+        this.url.query = null;
+        this.url.path.pop();
+        this.state = "path";
+        --this.pointer;
+      }
+    }
+
+    return true;
+  }
+
+  parseRelativeSlash(c: number) {
+    if (isSpecial(this.url) && (c === p("/") || c === p("\\"))) {
+      if (c === p("\\")) {
+        this.parseError = true;
+      }
+      this.state = "special authority ignore slashes";
+    } else if (c === p("/")) {
+      this.state = "authority";
+    } else {
+      this.url.username = this.base.username;
+      this.url.password = this.base.password;
+      this.url.host = this.base.host;
+      this.url.port = this.base.port;
+      this.state = "path";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseSpecialAuthoritySlashes(c: number) {
+    if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
+      this.state = "special authority ignore slashes";
+      ++this.pointer;
+    } else {
+      this.parseError = true;
+      this.state = "special authority ignore slashes";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseSpecialAuthorityIgnoreSlashes(c: number) {
+    if (c !== p("/") && c !== p("\\")) {
+      this.state = "authority";
+      --this.pointer;
+    } else {
+      this.parseError = true;
+    }
+
+    return true;
+  }
+
+  parseAuthority(c: number, cStr: string) {
+    if (c === p("@")) {
+      this.parseError = true;
+      if (this.atFlag) {
+        this.buffer = `%40${this.buffer}`;
+      }
+      this.atFlag = true;
+
+      // careful, this is based on buffer and has its own pointer 
(this.pointer != pointer) and inner chars
+      const len = countSymbols(this.buffer);
+      for (let pointer = 0; pointer < len; ++pointer) {
+        const codePoint = this.buffer.codePointAt(pointer);
+
+        if (codePoint === p(":") && !this.passwordTokenSeenFlag) {
+          this.passwordTokenSeenFlag = true;
+          continue;
+        }
+        const encodedCodePoints = utf8PercentEncodeCodePoint(
+          codePoint!,
+          isUserinfoPercentEncode,
+        );
+        if (this.passwordTokenSeenFlag) {
+          this.url.password += encodedCodePoints;
+        } else {
+          this.url.username += encodedCodePoints;
+        }
+      }
+      this.buffer = "";
+    } else if (
+      isNaN(c) ||
+      c === p("/") ||
+      c === p("?") ||
+      c === p("#") ||
+      (isSpecial(this.url) && c === p("\\"))
+    ) {
+      if (this.atFlag && this.buffer === "") {
+        this.parseError = true;
+        return failure;
+      }
+      this.pointer -= countSymbols(this.buffer) + 1;
+      this.buffer = "";
+      this.state = "host";
+    } else {
+      this.buffer += cStr;
+    }
+
+    return true;
+  }
+
+  parseHostName(c: number, cStr: string) {
+    if (this.stateOverride && this.url.scheme === "file") {
+      --this.pointer;
+      this.state = "file host";
+    } else if (c === p(":") && !this.arrFlag) {
+      if (this.buffer === "") {
+        this.parseError = true;
+        return failure;
+      }
+
+      if (this.stateOverride === "hostname") {
+        return false;
+      }
+
+      const host = parseHost(this.buffer, isNotSpecial(this.url));
+      if (host === failure) {
+        return failure;
+      }
+
+      this.url.host = host;
+      this.buffer = "";
+      this.state = "port";
+    } else if (
+      isNaN(c) ||
+      c === p("/") ||
+      c === p("?") ||
+      c === p("#") ||
+      (isSpecial(this.url) && c === p("\\"))
+    ) {
+      --this.pointer;
+      if (isSpecial(this.url) && this.buffer === "") {
+        this.parseError = true;
+        return failure;
+      } else if (
+        this.stateOverride &&
+        this.buffer === "" &&
+        (includesCredentials(this.url) || this.url.port !== null)
+      ) {
+        this.parseError = true;
+        return false;
+      }
+
+      const host = parseHost(this.buffer, isNotSpecial(this.url));
+      if (host === failure) {
+        return failure;
+      }
+
+      this.url.host = host;
+      this.buffer = "";
+      this.state = "path start";
+      if (this.stateOverride) {
+        return false;
+      }
+    } else {
+      if (c === p("[")) {
+        this.arrFlag = true;
+      } else if (c === p("]")) {
+        this.arrFlag = false;
+      }
+      this.buffer += cStr;
+    }
+
+    return true;
+  }
+
+  parsePort(c: number, cStr: any) {
+    if (isASCIIDigit(c)) {
+      this.buffer += cStr;
+    } else if (
+      isNaN(c) ||
+      c === p("/") ||
+      c === p("?") ||
+      c === p("#") ||
+      (isSpecial(this.url) && c === p("\\")) ||
+      this.stateOverride
+    ) {
+      if (this.buffer !== "") {
+        const port = parseInt(this.buffer);
+        if (port > 2 ** 16 - 1) {
+          this.parseError = true;
+          return failure;
+        }
+        this.url.port = port === defaultPort(this.url.scheme) ? null : port;
+        this.buffer = "";
+      }
+      if (this.stateOverride) {
+        return false;
+      }
+      this.state = "path start";
+      --this.pointer;
+    } else {
+      this.parseError = true;
+      return failure;
+    }
+
+    return true;
+  }
+
+  parseFile(c: number) {
+    this.url.scheme = "file";
+    this.url.host = "";
+
+    if (c === p("/") || c === p("\\")) {
+      if (c === p("\\")) {
+        this.parseError = true;
+      }
+      this.state = "file slash";
+    } else if (this.base !== null && this.base.scheme === "file") {
+      this.url.host = this.base.host;
+      this.url.path = this.base.path.slice();
+      this.url.query = this.base.query;
+      if (c === p("?")) {
+        this.url.query = "";
+        this.state = "query";
+      } else if (c === p("#")) {
+        this.url.fragment = "";
+        this.state = "fragment";
+      } else if (!isNaN(c)) {
+        this.url.query = null;
+        if (!startsWithWindowsDriveLetter(this.input, this.pointer)) {
+          shortenPath(this.url);
+        } else {
+          this.parseError = true;
+          this.url.path = [];
+        }
+
+        this.state = "path";
+        --this.pointer;
+      }
+    } else {
+      this.state = "path";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseFileSlash(c: number) {
+    if (c === p("/") || c === p("\\")) {
+      if (c === p("\\")) {
+        this.parseError = true;
+      }
+      this.state = "file host";
+    } else {
+      if (this.base !== null && this.base.scheme === "file") {
+        if (
+          !startsWithWindowsDriveLetter(this.input, this.pointer) &&
+          isNormalizedWindowsDriveLetterString(this.base.path[0])
+        ) {
+          this.url.path.push(this.base.path[0]);
+        }
+        this.url.host = this.base.host;
+      }
+      this.state = "path";
+      --this.pointer;
+    }
+
+    return true;
+  }
+
+  parseFileHost(c: number, cStr: string) {
+    if (
+      isNaN(c) ||
+      c === p("/") ||
+      c === p("\\") ||
+      c === p("?") ||
+      c === p("#")
+    ) {
+      --this.pointer;
+      if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) {
+        this.parseError = true;
+        this.state = "path";
+      } else if (this.buffer === "") {
+        this.url.host = "";
+        if (this.stateOverride) {
+          return false;
+        }
+        this.state = "path start";
+      } else {
+        let host = parseHost(this.buffer, isNotSpecial(this.url));
+        if (host === failure) {
+          return failure;
+        }
+        if (host === "localhost") {
+          host = "";
+        }
+        this.url.host = host as any;
+
+        if (this.stateOverride) {
+          return false;
+        }
+
+        this.buffer = "";
+        this.state = "path start";
+      }
+    } else {
+      this.buffer += cStr;
+    }
+
+    return true;
+  }
+
+  parsePathStart(c: number) {
+    if (isSpecial(this.url)) {
+      if (c === p("\\")) {
+        this.parseError = true;
+      }
+      this.state = "path";
+
+      if (c !== p("/") && c !== p("\\")) {
+        --this.pointer;
+      }
+    } else if (!this.stateOverride && c === p("?")) {
+      this.url.query = "";
+      this.state = "query";
+    } else if (!this.stateOverride && c === p("#")) {
+      this.url.fragment = "";
+      this.state = "fragment";
+    } else if (c !== undefined) {
+      this.state = "path";
+      if (c !== p("/")) {
+        --this.pointer;
+      }
+    } else if (this.stateOverride && this.url.host === null) {
+      this.url.path.push("");
+    }
+
+    return true;
+  }
+
+  parsePath(c: number) {
+    if (
+      isNaN(c) ||
+      c === p("/") ||
+      (isSpecial(this.url) && c === p("\\")) ||
+      (!this.stateOverride && (c === p("?") || c === p("#")))
+    ) {
+      if (isSpecial(this.url) && c === p("\\")) {
+        this.parseError = true;
+      }
+
+      if (isDoubleDot(this.buffer)) {
+        shortenPath(this.url);
+        if (c !== p("/") && !(isSpecial(this.url) && c === p("\\"))) {
+          this.url.path.push("");
+        }
+      } else if (
+        isSingleDot(this.buffer) &&
+        c !== p("/") &&
+        !(isSpecial(this.url) && c === p("\\"))
+      ) {
+        this.url.path.push("");
+      } else if (!isSingleDot(this.buffer)) {
+        if (
+          this.url.scheme === "file" &&
+          this.url.path.length === 0 &&
+          isWindowsDriveLetterString(this.buffer)
+        ) {
+          this.buffer = `${this.buffer[0]}:`;
+        }
+        this.url.path.push(this.buffer);
+      }
+      this.buffer = "";
+      if (c === p("?")) {
+        this.url.query = "";
+        this.state = "query";
+      }
+      if (c === p("#")) {
+        this.url.fragment = "";
+        this.state = "fragment";
+      }
+    } else {
+      // TODO: If c is not a URL code point and not "%", parse error.
+
+      if (
+        c === p("%") &&
+        (!isASCIIHex(this.input[this.pointer + 1]) ||
+          !isASCIIHex(this.input[this.pointer + 2]))
+      ) {
+        this.parseError = true;
+      }
+
+      this.buffer += utf8PercentEncodeCodePoint(c, isPathPercentEncode);
+    }
+
+    return true;
+  }
+
+  parseOpaquePath(c: number) {
+    if (c === p("?")) {
+      this.url.query = "";
+      this.state = "query";
+    } else if (c === p("#")) {
+      this.url.fragment = "";
+      this.state = "fragment";
+    } else {
+      // TODO: Add: not a URL code point
+      if (!isNaN(c) && c !== p("%")) {
+        this.parseError = true;
+      }
+
+      if (
+        c === p("%") &&
+        (!isASCIIHex(this.input[this.pointer + 1]) ||
+          !isASCIIHex(this.input[this.pointer + 2]))
+      ) {
+        this.parseError = true;
+      }
+
+      if (!isNaN(c)) {
+        // @ts-ignore
+        this.url.path += utf8PercentEncodeCodePoint(
+          c,
+          isC0ControlPercentEncode,
+        );
+      }
+    }
+
+    return true;
+  }
+
+  parseQuery(c: number, cStr: string) {
+    if (
+      !isSpecial(this.url) ||
+      this.url.scheme === "ws" ||
+      this.url.scheme === "wss"
+    ) {
+      this.encodingOverride = "utf-8";
+    }
+
+    if ((!this.stateOverride && c === p("#")) || isNaN(c)) {
+      const queryPercentEncodePredicate = isSpecial(this.url)
+        ? isSpecialQueryPercentEncode
+        : isQueryPercentEncode;
+      this.url.query += utf8PercentEncodeString(
+        this.buffer,
+        queryPercentEncodePredicate,
+      );
+
+      this.buffer = "";
+
+      if (c === p("#")) {
+        this.url.fragment = "";
+        this.state = "fragment";
+      }
+    } else if (!isNaN(c)) {
+      // TODO: If c is not a URL code point and not "%", parse error.
+
+      if (
+        c === p("%") &&
+        (!isASCIIHex(this.input[this.pointer + 1]) ||
+          !isASCIIHex(this.input[this.pointer + 2]))
+      ) {
+        this.parseError = true;
+      }
+
+      this.buffer += cStr;
+    }
+
+    return true;
+  }
+
+  parseFragment(c: number) {
+    if (!isNaN(c)) {
+      // TODO: If c is not a URL code point and not "%", parse error.
+      if (
+        c === p("%") &&
+        (!isASCIIHex(this.input[this.pointer + 1]) ||
+          !isASCIIHex(this.input[this.pointer + 2]))
+      ) {
+        this.parseError = true;
+      }
+
+      this.url.fragment += utf8PercentEncodeCodePoint(
+        c,
+        isFragmentPercentEncode,
+      );
+    }
+
+    return true;
+  }
+}
+
+const fileOtherwiseCodePoints = new Set([p("/"), p("\\"), p("?"), p("#")]);
+
+function startsWithWindowsDriveLetter(input: number[], pointer: number) {
+  const length = input.length - pointer;
+  return (
+    length >= 2 &&
+    isWindowsDriveLetterCodePoints(input[pointer], input[pointer + 1]) &&
+    (length === 2 || fileOtherwiseCodePoints.has(input[pointer + 2]))
+  );
+}
+
+function serializeURL(url: any, excludeFragment?: boolean) {
+  let output = `${url.scheme}:`;
+  if (url.host !== null) {
+    output += "//";
+
+    if (url.username !== "" || url.password !== "") {
+      output += url.username;
+      if (url.password !== "") {
+        output += `:${url.password}`;
+      }
+      output += "@";
+    }
+
+    output += serializeHost(url.host);
+
+    if (url.port !== null) {
+      output += `:${url.port}`;
+    }
+  }
+
+  if (
+    url.host === null &&
+    !hasAnOpaquePath(url) &&
+    url.path.length > 1 &&
+    url.path[0] === ""
+  ) {
+    output += "/.";
+  }
+  output += serializePath(url);
+
+  if (url.query !== null) {
+    output += `?${url.query}`;
+  }
+
+  if (!excludeFragment && url.fragment !== null) {
+    output += `#${url.fragment}`;
+  }
+
+  return output;
+}
+
+function serializeOrigin(tuple: {
+  scheme: string;
+  port: number;
+  host: number | number[] | string;
+}) {
+  let result = `${tuple.scheme}://`;
+  result += serializeHost(tuple.host);
+
+  if (tuple.port !== null) {
+    result += `:${tuple.port}`;
+  }
+
+  return result;
+}
+
+function serializePath(url: UrlObj): string {
+  if (typeof url.path === "string") {
+    return url.path;
+  }
+
+  let output = "";
+  for (const segment of url.path) {
+    output += `/${segment}`;
+  }
+  return output;
+}
+
+function serializeURLOrigin(url: any): any {
+  // https://url.spec.whatwg.org/#concept-url-origin
+  switch (url.scheme) {
+    case "blob":
+      try {
+        return serializeURLOrigin(parseURL(serializePath(url)));
+      } catch (e) {
+        // serializing an opaque origin returns "null"
+        return "null";
+      }
+    case "ftp":
+    case "http":
+    case "https":
+    case "ws":
+    case "wss":
+      return serializeOrigin({
+        scheme: url.scheme,
+        host: url.host,
+        port: url.port,
+      });
+    case "file":
+      // The spec says:
+      // > Unfortunate as it is, this is left as an exercise to the reader. 
When in doubt, return a new opaque origin.
+      // Browsers tested so far:
+      // - Chrome says "file://", but treats file: URLs as cross-origin for 
most (all?) purposes; see e.g.
+      //   https://bugs.chromium.org/p/chromium/issues/detail?id=37586
+      // - Firefox says "null", but treats file: URLs as same-origin sometimes 
based on directory stuff; see
+      //   
https://developer.mozilla.org/en-US/docs/Archive/Misc_top_level/Same-origin_policy_for_file:_URIs
+      return "null";
+    default:
+      // serializing an opaque origin returns "null"
+      return "null";
+  }
+}
+
+export function basicURLParse(input: string, options?: any) {
+  if (options === undefined) {
+    options = {};
+  }
+
+  const usm = new URLStateMachine(
+    input,
+    options.baseURL,
+    options.encodingOverride,
+    options.url,
+    options.stateOverride,
+  );
+
+  if (usm.failure) {
+    return null;
+  }
+
+  return usm.url;
+}
+
+function setTheUsername(url: UrlObj, username: string) {
+  url.username = utf8PercentEncodeString(username, isUserinfoPercentEncode);
+}
+
+function setThePassword(url: UrlObj, password: string) {
+  url.password = utf8PercentEncodeString(password, isUserinfoPercentEncode);
+}
+
+function serializeInteger(integer: number) {
+  return String(integer);
+}
+
+function parseURL(
+  input: any,
+  options?: { baseURL?: any; encodingOverride?: any },
+) {
+  if (options === undefined) {
+    options = {};
+  }
+
+  // We don't handle blobs, so this just delegates:
+  return basicURLParse(input, {
+    baseURL: options.baseURL,
+    encodingOverride: options.encodingOverride,
+  });
+}
+
+export class URLImpl {
+  constructor(url: string, base?: string) {
+    let parsedBase = null;
+    if (base !== undefined) {
+      parsedBase = basicURLParse(base);
+      if (parsedBase === null) {
+        throw new TypeError(`Invalid base URL: ${base}`);
+      }
+    }
+
+    const parsedURL = basicURLParse(url, { baseURL: parsedBase });
+    if (parsedURL === null) {
+      throw new TypeError(`Invalid URL: ${url}`);
+    }
+
+    const query = parsedURL.query !== null ? parsedURL.query : "";
+
+    this._url = parsedURL;
+
+    // We cannot invoke the "new URLSearchParams object" algorithm without 
going through the constructor, which strips
+    // question mark by default. Therefore the doNotStripQMark hack is used.
+    this._query = new URLSearchParamsImpl([query], {
+      doNotStripQMark: true,
+    });
+    this._query._url = this;
+  }
+
+  get href() {
+    return serializeURL(this._url);
+  }
+
+  set href(v) {
+    const parsedURL = basicURLParse(v);
+    if (parsedURL === null) {
+      throw new TypeError(`Invalid URL: ${v}`);
+    }
+
+    this._url = parsedURL;
+
+    this._query._list.splice(0);
+    const { query } = parsedURL;
+    if (query !== null) {
+      this._query._list = parseUrlencodedString(query);
+    }
+  }
+
+  get origin() {
+    return serializeURLOrigin(this._url);
+  }
+
+  get protocol() {
+    return `${this._url.scheme}:`;
+  }
+
+  set protocol(v) {
+    basicURLParse(`${v}:`, {
+      url: this._url,
+      stateOverride: "scheme start",
+    });
+  }
+
+  get username() {
+    return this._url.username;
+  }
+
+  set username(v) {
+    if (cannotHaveAUsernamePasswordPort(this._url)) {
+      return;
+    }
+
+    setTheUsername(this._url, v);
+  }
+
+  get password() {
+    return this._url.password;
+  }
+
+  set password(v) {
+    if (cannotHaveAUsernamePasswordPort(this._url)) {
+      return;
+    }
+
+    setThePassword(this._url, v);
+  }
+
+  get host() {
+    const url = this._url;
+
+    if (url.host === null) {
+      return "";
+    }
+
+    if (url.port === null) {
+      return serializeHost(url.host);
+    }
+
+    return `${serializeHost(url.host)}:${serializeInteger(url.port)}`;
+  }
+
+  set host(v) {
+    if (hasAnOpaquePath(this._url)) {
+      return;
+    }
+
+    basicURLParse(v, { url: this._url, stateOverride: "host" });
+  }
+
+  get hostname() {
+    if (this._url.host === null) {
+      return "";
+    }
+
+    return serializeHost(this._url.host);
+  }
+
+  set hostname(v) {
+    if (hasAnOpaquePath(this._url)) {
+      return;
+    }
+
+    basicURLParse(v, { url: this._url, stateOverride: "hostname" });
+  }
+
+  get port() {
+    if (this._url.port === null) {
+      return "";
+    }
+
+    return serializeInteger(this._url.port);
+  }
+
+  set port(v) {
+    if (cannotHaveAUsernamePasswordPort(this._url)) {
+      return;
+    }
+
+    if (v === "") {
+      this._url.port = null;
+    } else {
+      basicURLParse(v, { url: this._url, stateOverride: "port" });
+    }
+  }
+
+  get pathname() {
+    return serializePath(this._url);
+  }
+
+  set pathname(v: string) {
+    if (hasAnOpaquePath(this._url)) {
+      return;
+    }
+
+    this._url.path = [];
+    basicURLParse(v, { url: this._url, stateOverride: "path start" });
+  }
+
+  get search() {
+    if (this._url.query === null || this._url.query === "") {
+      return "";
+    }
+
+    return `?${this._url.query}`;
+  }
+
+  set search(v) {
+    const url = this._url;
+
+    if (v === "") {
+      url.query = null;
+      this._query._list = [];
+      return;
+    }
+
+    const input = v[0] === "?" ? v.substring(1) : v;
+    url.query = "";
+    basicURLParse(input, { url, stateOverride: "query" });
+    this._query._list = parseUrlencodedString(input);
+  }
+
+  get searchParams() {
+    return this._query;
+  }
+
+  get hash() {
+    if (this._url.fragment === null || this._url.fragment === "") {
+      return "";
+    }
+
+    return `#${this._url.fragment}`;
+  }
+
+  set hash(v) {
+    if (v === "") {
+      this._url.fragment = null;
+      return;
+    }
+
+    const input = v[0] === "#" ? v.substring(1) : v;
+    this._url.fragment = "";
+    basicURLParse(input, { url: this._url, stateOverride: "fragment" });
+  }
+
+  toJSON() {
+    return this.href;
+  }
+
+  // FIXME: type!
+  _url: any;
+  _query: any;
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]