diff options
author | Nayeem Rahman <nayeemrmn99@gmail.com> | 2021-03-02 01:30:24 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-02 02:30:24 +0100 |
commit | badc88b78a623d66ae86157d44a438c14dd7616a (patch) | |
tree | e06e4c6985c7721441e5857f72ec527f4100a816 /op_crates/web/11_url.js | |
parent | 62f33e3b146da2f4264d6fa71409516de892bdf3 (diff) |
refactor(op_crates/web): Move URL parsing to Rust (#9276)
Diffstat (limited to 'op_crates/web/11_url.js')
-rw-r--r-- | op_crates/web/11_url.js | 754 |
1 files changed, 126 insertions, 628 deletions
diff --git a/op_crates/web/11_url.js b/op_crates/web/11_url.js index eac679549..d8f5bd5f7 100644 --- a/op_crates/web/11_url.js +++ b/op_crates/web/11_url.js @@ -4,11 +4,7 @@ ((window) => { const core = window.Deno.core; - function requiredArguments( - name, - length, - required, - ) { + function requiredArguments(name, length, required) { if (length < required) { const errMsg = `${name} requires at least ${required} argument${ required === 1 ? "" : "s" @@ -17,39 +13,7 @@ } } - function isIterable( - o, - ) { - // checks for null and undefined - if (o == null) { - return false; - } - return ( - typeof (o)[Symbol.iterator] === "function" - ); - } - - /** https://url.spec.whatwg.org/#idna */ - function domainToAscii( - domain, - { beStrict = false } = {}, - ) { - return core.jsonOpSync("op_domain_to_ascii", { domain, beStrict }); - } - - function decodeSearchParam(p) { - const s = p.replaceAll("+", " "); - const decoder = new TextDecoder(); - - return s.replace(/(%[0-9a-f]{2})+/gi, (matched) => { - const buf = new Uint8Array(Math.ceil(matched.length / 3)); - for (let i = 0, offset = 0; i < matched.length; i += 3, offset += 1) { - buf[offset] = parseInt(matched.slice(i + 1, i + 3), 16); - } - return decoder.decode(buf); - }); - } - + const paramLists = new WeakMap(); const urls = new WeakMap(); class URLSearchParams { @@ -57,83 +21,56 @@ constructor(init = "") { if (typeof init === "string") { - this.#handleStringInitialization(init); - return; - } - - if (Array.isArray(init) || isIterable(init)) { - this.#handleArrayInitialization(init); - return; - } - - if (Object(init) !== init) { - return; - } + // Overload: USVString + // If init is a string and starts with U+003F (?), + // remove the first code point from init. + if (init[0] == "?") { + init = init.slice(1); + } - if (init instanceof URLSearchParams) { + this.#params = core.jsonOpSync("op_parse_url_search_params", init); + } else if ( + Array.isArray(init) || + typeof init?.[Symbol.iterator] == "function" + ) { + // Overload: sequence<sequence<USVString>> + for (const pair of init) { + // If pair does not contain exactly two items, then throw a TypeError. + if (pair.length !== 2) { + throw new TypeError( + "URLSearchParams.constructor sequence argument must only contain pair elements", + ); + } + this.#params.push([String(pair[0]), String(pair[1])]); + } + } else if (Object(init) !== init) { + // pass + } else if (init instanceof URLSearchParams) { this.#params = [...init.#params]; - return; - } - - // Overload: record<USVString, USVString> - for (const key of Object.keys(init)) { - this.#append(key, init[key]); + } else { + // Overload: record<USVString, USVString> + for (const key of Object.keys(init)) { + this.#params.push([key, String(init[key])]); + } } + paramLists.set(this, this.#params); urls.set(this, null); } - #handleStringInitialization = (init) => { - // Overload: USVString - // If init is a string and starts with U+003F (?), - // remove the first code point from init. - if (init.charCodeAt(0) === 0x003f) { - init = init.slice(1); - } - - for (const pair of init.split("&")) { - // Empty params are ignored - if (pair.length === 0) { - continue; - } - const position = pair.indexOf("="); - const name = pair.slice(0, position === -1 ? pair.length : position); - const value = pair.slice(name.length + 1); - this.#append(decodeSearchParam(name), decodeSearchParam(value)); - } - }; - - #handleArrayInitialization = ( - init, - ) => { - // Overload: sequence<sequence<USVString>> - for (const tuple of init) { - // If pair does not contain exactly two items, then throw a TypeError. - if (tuple.length !== 2) { - throw new TypeError( - "URLSearchParams.constructor tuple array argument must only contain pair elements", - ); - } - this.#append(tuple[0], tuple[1]); - } - }; - - #updateSteps = () => { + #updateUrlSearch = () => { const url = urls.get(this); if (url == null) { return; } - parts.get(url).query = this.toString(); - }; - - #append = (name, value) => { - this.#params.push([String(name), String(value)]); + const parseArgs = { href: url.href, setSearch: this.toString() }; + parts.set(url, core.jsonOpSync("op_parse_url", parseArgs)); }; append(name, value) { requiredArguments("URLSearchParams.append", arguments.length, 2); - this.#append(name, value); - this.#updateSteps(); + this.#params.push([String(name), String(value)]); + this.#updateUrlSearch(); } delete(name) { @@ -147,7 +84,7 @@ i++; } } - this.#updateSteps(); + this.#updateUrlSearch(); } getAll(name) { @@ -208,21 +145,18 @@ // Otherwise, append a new name-value pair whose name is name // and value is value, to list. if (!found) { - this.#append(name, value); + this.#params.push([String(name), String(value)]); } - this.#updateSteps(); + this.#updateUrlSearch(); } sort() { this.#params.sort((a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1)); - this.#updateSteps(); + this.#updateUrlSearch(); } - forEach( - callbackfn, - thisArg, - ) { + forEach(callbackfn, thisArg) { requiredArguments("URLSearchParams.forEach", arguments.length, 1); if (typeof thisArg !== "undefined") { @@ -255,272 +189,26 @@ } toString() { - return this.#params - .map( - (tuple) => - `${encodeSearchParam(tuple[0])}=${encodeSearchParam(tuple[1])}`, - ) - .join("&"); - } - } - - const searchParamsMethods = [ - "append", - "delete", - "set", - ]; - - const specialSchemes = ["ftp", "file", "http", "https", "ws", "wss"]; - - // https://url.spec.whatwg.org/#special-scheme - const schemePorts = { - ftp: "21", - file: "", - http: "80", - https: "443", - ws: "80", - wss: "443", - }; - const MAX_PORT = 2 ** 16 - 1; - - // Remove the part of the string that matches the pattern and return the - // remainder (RHS) as well as the first captured group of the matched substring - // (LHS). e.g. - // takePattern("https://deno.land:80", /^([a-z]+):[/]{2}/) - // = ["http", "deno.land:80"] - // takePattern("deno.land:80", /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/) - // = ["deno.land", "80"] - function takePattern(string, pattern) { - let capture = ""; - const rest = string.replace(pattern, (_, capture_) => { - capture = capture_; - return ""; - }); - return [capture, rest]; - } - - function parse(url, baseParts = null) { - const parts = {}; - let restUrl; - let usedNonBase = false; - [parts.protocol, restUrl] = takePattern( - url.trim(), - /^([A-Za-z][+-.0-9A-Za-z]*):/, - ); - parts.protocol = parts.protocol.toLowerCase(); - if (parts.protocol == "") { - if (baseParts == null) { - return null; - } - parts.protocol = baseParts.protocol; - } else if ( - parts.protocol != baseParts?.protocol || - !specialSchemes.includes(parts.protocol) - ) { - usedNonBase = true; - } - const isSpecial = specialSchemes.includes(parts.protocol); - if (parts.protocol == "file") { - parts.slashes = "//"; - parts.username = ""; - parts.password = ""; - if (usedNonBase || restUrl.match(/^[/\\]{2}/)) { - [parts.hostname, restUrl] = takePattern( - restUrl, - /^[/\\]{2}([^/\\?#]*)/, - ); - usedNonBase = true; - } else { - parts.hostname = baseParts.hostname; - } - parts.port = ""; - } else { - if (usedNonBase || restUrl.match(/^[/\\]{2}/)) { - let restAuthority; - if (isSpecial) { - parts.slashes = "//"; - [restAuthority, restUrl] = takePattern( - restUrl, - /^[/\\]*([^/\\?#]*)/, - ); - } else { - parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : ""; - [restAuthority, restUrl] = takePattern( - restUrl, - /^[/\\]{2}([^/\\?#]*)/, - ); - } - let restAuthentication; - [restAuthentication, restAuthority] = takePattern( - restAuthority, - /^(.*)@/, - ); - [parts.username, restAuthentication] = takePattern( - restAuthentication, - /^([^:]*)/, - ); - parts.username = encodeUserinfo(parts.username); - [parts.password] = takePattern(restAuthentication, /^:(.*)/); - parts.password = encodeUserinfo(parts.password); - [parts.hostname, restAuthority] = takePattern( - restAuthority, - /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/, - ); - [parts.port] = takePattern(restAuthority, /^:(.*)/); - if (!isValidPort(parts.port)) { - return null; - } - if (parts.hostname == "" && isSpecial) { - return null; - } - usedNonBase = true; - } else { - parts.slashes = baseParts.slashes; - parts.username = baseParts.username; - parts.password = baseParts.password; - parts.hostname = baseParts.hostname; - parts.port = baseParts.port; - } - } - try { - parts.hostname = encodeHostname(parts.hostname, isSpecial); - } catch { - return null; + return core.jsonOpSync("op_stringify_url_search_params", this.#params); } - [parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/); - parts.path = encodePathname(parts.path); - if (usedNonBase) { - parts.path = normalizePath(parts.path, parts.protocol == "file"); - } else { - if (parts.path != "") { - usedNonBase = true; - } - parts.path = resolvePathFromBase( - parts.path, - baseParts.path || "/", - baseParts.protocol == "file", - ); - } - // Drop the hostname if a drive letter is parsed. - if (parts.protocol == "file" && parts.path.match(/^\/+[A-Za-z]:(\/|$)/)) { - parts.hostname = ""; - } - if (usedNonBase || restUrl.startsWith("?")) { - [parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/); - parts.query = encodeSearch(parts.query, isSpecial); - usedNonBase = true; - } else { - parts.query = baseParts.query; - } - [parts.hash] = takePattern(restUrl, /^(#.*)/); - parts.hash = encodeHash(parts.hash); - return parts; } - // Resolves `.`s and `..`s where possible. - // Preserves repeating and trailing `/`s by design. - // Assumes drive letter file paths will have a leading slash. - function normalizePath(path, isFilePath) { - const isAbsolute = path.startsWith("/"); - path = path.replace(/^\//, ""); - const pathSegments = path.split("/"); + const parts = new WeakMap(); - let driveLetter = null; - if (isFilePath && pathSegments[0].match(/^[A-Za-z]:$/)) { - driveLetter = pathSegments.shift(); - } + class URL { + #searchParams = null; - if (isFilePath && isAbsolute) { - while (pathSegments.length > 1 && pathSegments[0] == "") { - pathSegments.shift(); - } - } + constructor(url, base) { + new.target; - let ensureTrailingSlash = false; - const newPathSegments = []; - for (let i = 0; i < pathSegments.length; i++) { - const previous = newPathSegments[newPathSegments.length - 1]; - if ( - pathSegments[i] == ".." && - previous != ".." && - (previous != undefined || isAbsolute) - ) { - newPathSegments.pop(); - ensureTrailingSlash = true; - } else if (pathSegments[i] == ".") { - ensureTrailingSlash = true; + if (url instanceof URL && base === undefined) { + parts.set(this, parts.get(url)); } else { - newPathSegments.push(pathSegments[i]); - ensureTrailingSlash = false; + base = base !== undefined ? String(base) : base; + const parseArgs = { href: String(url), baseHref: base }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); } } - if (driveLetter != null) { - newPathSegments.unshift(driveLetter); - } - if (newPathSegments.length == 0 && !isAbsolute) { - newPathSegments.push("."); - ensureTrailingSlash = false; - } - - let newPath = newPathSegments.join("/"); - if (isAbsolute) { - newPath = `/${newPath}`; - } - if (ensureTrailingSlash) { - newPath = newPath.replace(/\/*$/, "/"); - } - return newPath; - } - - // Standard URL basing logic, applied to paths. - function resolvePathFromBase(path, basePath, isFilePath) { - let basePrefix; - let suffix; - const baseDriveLetter = basePath.match(/^\/+[A-Za-z]:(?=\/|$)/)?.[0]; - if (isFilePath && path.match(/^\/+[A-Za-z]:(\/|$)/)) { - basePrefix = ""; - suffix = path; - } else if (path.startsWith("/")) { - if (isFilePath && baseDriveLetter) { - basePrefix = baseDriveLetter; - suffix = path; - } else { - basePrefix = ""; - suffix = path; - } - } else if (path != "") { - basePath = normalizePath(basePath, isFilePath); - path = normalizePath(path, isFilePath); - // Remove everything after the last `/` in `basePath`. - if (baseDriveLetter && isFilePath) { - basePrefix = `${baseDriveLetter}${ - basePath.slice(baseDriveLetter.length).replace(/[^\/]*$/, "") - }`; - } else { - basePrefix = basePath.replace(/[^\/]*$/, ""); - } - basePrefix = basePrefix.replace(/\/*$/, "/"); - // If `normalizedPath` ends with `.` or `..`, add a trailing slash. - suffix = path.replace(/(?<=(^|\/)(\.|\.\.))$/, "/"); - } else { - basePrefix = basePath; - suffix = ""; - } - return normalizePath(basePrefix + suffix, isFilePath); - } - - function isValidPort(value) { - // https://url.spec.whatwg.org/#port-state - if (value === "") return true; - - const port = Number(value); - return Number.isInteger(port) && port >= 0 && port <= MAX_PORT; - } - - const parts = new WeakMap(); - - class URL { - #searchParams = null; [Symbol.for("Deno.customInspect")](inspect) { const object = { @@ -540,18 +228,14 @@ } #updateSearchParams = () => { - const searchParams = new URLSearchParams(this.search); - - for (const methodName of searchParamsMethods) { - const method = searchParams[methodName]; - searchParams[methodName] = (...args) => { - method.apply(searchParams, args); - this.search = searchParams.toString(); - }; + if (this.#searchParams != null) { + const params = paramLists.get(this.#searchParams); + const newParams = core.jsonOpSync( + "op_parse_url_search_params", + this.search.slice(1), + ); + params.splice(0, params.length, ...newParams); } - this.#searchParams = searchParams; - - urls.set(searchParams, this); }; get hash() { @@ -559,27 +243,25 @@ } set hash(value) { - value = unescape(String(value)); - if (!value) { - parts.get(this).hash = ""; - } else { - if (value.charAt(0) !== "#") { - value = `#${value}`; - } - // hashes can contain % and # unescaped - parts.get(this).hash = encodeHash(value); + try { + const parseArgs = { href: this.href, setHash: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ } } get host() { - return `${this.hostname}${this.port ? `:${this.port}` : ""}`; + return parts.get(this).host; } set host(value) { - value = String(value); - const url = new URL(`http://${value}`); - parts.get(this).hostname = url.hostname; - parts.get(this).port = url.port; + try { + const parseArgs = { href: this.href, setHost: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ + } } get hostname() { @@ -587,42 +269,30 @@ } set hostname(value) { - value = String(value); try { - const isSpecial = specialSchemes.includes(parts.get(this).protocol); - parts.get(this).hostname = encodeHostname(value, isSpecial); + const parseArgs = { href: this.href, setHostname: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); } catch { - // pass + /* pass */ } } get href() { - const authentication = this.username || this.password - ? `${this.username}${this.password ? ":" + this.password : ""}@` - : ""; - const host = this.host; - const slashes = host ? "//" : parts.get(this).slashes; - let pathname = this.pathname; - if (pathname.charAt(0) != "/" && pathname != "" && host != "") { - pathname = `/${pathname}`; - } - return `${this.protocol}${slashes}${authentication}${host}${pathname}${this.search}${this.hash}`; + return parts.get(this).href; } set href(value) { - value = String(value); - if (value !== this.href) { - const url = new URL(value); - parts.set(this, { ...parts.get(url) }); - this.#updateSearchParams(); + try { + const parseArgs = { href: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + throw new TypeError("Invalid URL"); } + this.#updateSearchParams(); } get origin() { - if (this.host) { - return `${this.protocol}//${this.host}`; - } - return "null"; + return parts.get(this).origin; } get password() { @@ -630,64 +300,65 @@ } set password(value) { - value = String(value); - parts.get(this).password = encodeUserinfo(value); + try { + const parseArgs = { href: this.href, setPassword: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ + } } get pathname() { - let path = parts.get(this).path; - if (specialSchemes.includes(parts.get(this).protocol)) { - if (path.charAt(0) != "/") { - path = `/${path}`; - } - } - return path; + return parts.get(this).pathname; } set pathname(value) { - parts.get(this).path = encodePathname(String(value)); + try { + const parseArgs = { href: this.href, setPathname: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ + } } get port() { - const port = parts.get(this).port; - if (schemePorts[parts.get(this).protocol] === port) { - return ""; - } - - return port; + return parts.get(this).port; } set port(value) { - if (!isValidPort(value)) { - return; + try { + const parseArgs = { href: this.href, setPort: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ } - parts.get(this).port = value.toString(); } get protocol() { - return `${parts.get(this).protocol}:`; + return parts.get(this).protocol; } set protocol(value) { - value = String(value); - if (value) { - if (value.charAt(value.length - 1) === ":") { - value = value.slice(0, -1); - } - parts.get(this).protocol = encodeURIComponent(value); + try { + const parseArgs = { href: this.href, setProtocol: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ } } get search() { - return parts.get(this).query; + return parts.get(this).search; } set search(value) { - value = String(value); - const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`; - const isSpecial = specialSchemes.includes(parts.get(this).protocol); - parts.get(this).query = encodeSearch(query, isSpecial); - this.#updateSearchParams(); + try { + const parseArgs = { href: this.href, setSearch: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + this.#updateSearchParams(); + } catch { + /* pass */ + } } get username() { @@ -695,33 +366,20 @@ } set username(value) { - value = String(value); - parts.get(this).username = encodeUserinfo(value); + try { + const parseArgs = { href: this.href, setUsername: String(value) }; + parts.set(this, core.jsonOpSync("op_parse_url", parseArgs)); + } catch { + /* pass */ + } } get searchParams() { - return this.#searchParams; - } - - constructor(url, base) { - let baseParts = null; - new.target; - if (base) { - baseParts = base instanceof URL ? parts.get(base) : parse(base); - if (baseParts == null) { - throw new TypeError("Invalid base URL."); - } - } - - const urlParts = url instanceof URL - ? parts.get(url) - : parse(url, baseParts); - if (urlParts == null) { - throw new TypeError("Invalid URL."); + if (this.#searchParams == null) { + this.#searchParams = new URLSearchParams(this.search); + urls.set(this.#searchParams, this); } - parts.set(this, urlParts); - - this.#updateSearchParams(); + return this.#searchParams; } toString() { @@ -741,166 +399,6 @@ } } - function parseIpv4Number(s) { - if (s.match(/^(0[Xx])[0-9A-Za-z]+$/)) { - return Number(s); - } - if (s.match(/^[0-9]+$/)) { - return Number(s.startsWith("0") ? `0o${s}` : s); - } - return NaN; - } - - function parseIpv4(s) { - const parts = s.split("."); - if (parts[parts.length - 1] == "" && parts.length > 1) { - parts.pop(); - } - if (parts.includes("") || parts.length > 4) { - return s; - } - const numbers = parts.map(parseIpv4Number); - if (numbers.includes(NaN)) { - return s; - } - const last = numbers.pop(); - if (last >= 256 ** (4 - numbers.length) || numbers.find((n) => n >= 256)) { - throw new TypeError("Invalid hostname."); - } - const ipv4 = numbers.reduce((sum, n, i) => sum + n * 256 ** (3 - i), last); - const ipv4Hex = ipv4.toString(16).padStart(8, "0"); - const ipv4HexParts = ipv4Hex.match(/(..)(..)(..)(..)$/).slice(1); - return ipv4HexParts.map((s) => String(Number(`0x${s}`))).join("."); - } - - function charInC0ControlSet(c) { - return (c >= "\u0000" && c <= "\u001F") || c > "\u007E"; - } - - function charInSearchSet(c, isSpecial) { - // deno-fmt-ignore - return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u003C", "\u003E"].includes(c) || isSpecial && c == "\u0027" || c > "\u007E"; - } - - function charInFragmentSet(c) { - // deno-fmt-ignore - return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c); - } - - function charInPathSet(c) { - // deno-fmt-ignore - return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c); - } - - function charInUserinfoSet(c) { - // "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox. - // deno-fmt-ignore - return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c); - } - - function charIsForbiddenInHost(c) { - // deno-fmt-ignore - return ["\u0000", "\u0009", "\u000A", "\u000D", "\u0020", "\u0023", "\u0025", "\u002F", "\u003A", "\u003C", "\u003E", "\u003F", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E"].includes(c); - } - - function charInFormUrlencodedSet(c) { - // deno-fmt-ignore - return charInUserinfoSet(c) || ["\u0021", "\u0024", "\u0025", "\u0026", "\u0027", "\u0028", "\u0029", "\u002B", "\u002C", "\u007E"].includes(c); - } - - const encoder = new TextEncoder(); - - function encodeChar(c) { - return [...encoder.encode(c)] - .map((n) => `%${n.toString(16).padStart(2, "0")}`) - .join("") - .toUpperCase(); - } - - function encodeUserinfo(s) { - return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join( - "", - ); - } - - function encodeHostname(s, isSpecial = true) { - // IPv6 parsing. - if (s.startsWith("[") && s.endsWith("]")) { - if (!s.match(/^\[[0-9A-Fa-f.:]{2,}\]$/)) { - throw new TypeError("Invalid hostname."); - } - // IPv6 address compress - return s.toLowerCase().replace(/\b:?(?:0+:?){2,}/, "::"); - } - - let result = s; - - if (!isSpecial) { - // Check against forbidden host code points except for "%". - for (const c of result) { - if (charIsForbiddenInHost(c) && c != "\u0025") { - throw new TypeError("Invalid hostname."); - } - } - - // Percent-encode C0 control set. - result = [...result] - .map((c) => (charInC0ControlSet(c) ? encodeChar(c) : c)) - .join(""); - - return result; - } - - // Percent-decode. - if (result.match(/%(?![0-9A-Fa-f]{2})/) != null) { - throw new TypeError("Invalid hostname."); - } - result = result.replace( - /%(.{2})/g, - (_, hex) => String.fromCodePoint(Number(`0x${hex}`)), - ); - - // IDNA domain to ASCII. - result = domainToAscii(result); - - // Check against forbidden host code points. - for (const c of result) { - if (charIsForbiddenInHost(c)) { - throw new TypeError("Invalid hostname."); - } - } - - // IPv4 parsing. - if (isSpecial) { - result = parseIpv4(result); - } - - return result; - } - - function encodePathname(s) { - return [...s.replace(/\\/g, "/")].map(( - c, - ) => (charInPathSet(c) ? encodeChar(c) : c)).join(""); - } - - function encodeSearch(s, isSpecial) { - return [...s].map(( - c, - ) => (charInSearchSet(c, isSpecial) ? encodeChar(c) : c)).join(""); - } - - function encodeHash(s) { - return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join( - "", - ); - } - - function encodeSearchParam(s) { - return [...s].map((c) => (charInFormUrlencodedSet(c) ? encodeChar(c) : c)) - .join("").replace(/%20/g, "+"); - } - window.__bootstrap.url = { URL, URLSearchParams, |