"use strict"; // MARK: - Parsing Regular Expressions Object.defineProperty(exports, "__esModule", { value: true }); exports.URL = exports.QueryHandling = void 0; const optional_1 = require("../types/optional"); const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i; const queryParamRegex = /([^=?&]+)=?([^&]*)/g; const componentOrder = ["hash", "query", "pathname", "host"]; /** * Defines how query parameters should be parsed and encoded. */ var QueryHandling; (function (QueryHandling) { /** * Handle according to `application/x-www-form-urlencoded` rules (HTML forms). * * This is the **default decoding mode** for backward compatibility. * * **Example:** * ```typescript * // Input: "?search=hello+world&category=news+articles" * // Parsed: { search: "hello world", category: "news articles" } * // Output: "?search=hello+world&category=news+articles" * ``` * * @see {@link https://url.spec.whatwg.org/#concept-urlencoded-parser WHATWG URL Standard} */ QueryHandling["FORM_ENCODED"] = "form-encoded"; /** * Handle according to RFC 3986 URI specification rules. * * This is the **default encoding mode** for backward compatibility. * * **Example:** * ```typescript * // Input: "?search=hello+world&math=2+2%3D4" * // Parsed: { search: "hello+world", math: "2+2=4" } * // Output: "?search=hello+world&math=2+2%3D4" * ``` * * @see {@link https://tools.ietf.org/html/rfc3986#section-3.4 RFC 3986 Section 3.4} */ QueryHandling["RFC3986"] = "rfc3986"; })(QueryHandling = exports.QueryHandling || (exports.QueryHandling = {})); class URL { constructor(url, options) { var _a; this.query = {}; this.queryHandling = options === null || options === void 0 ? void 0 : options.queryHandling; if ((0, optional_1.isNothing)(url)) { return; } // Split the protocol from the rest of the urls let remainder = url; const match = protocolRegex.exec(url); if ((0, optional_1.isSome)(match)) { // Pull out the protocol let protocol = match[1]; if (protocol !== null && protocol !== undefined) { protocol = protocol.split(":")[0]; } this.protocol = protocol !== null && protocol !== void 0 ? protocol : undefined; // Save the remainder remainder = (_a = match[3]) !== null && _a !== void 0 ? _a : undefined; } // Then match each component in a specific order let parse = { remainder: remainder, result: undefined }; for (const component of componentOrder) { if (parse === undefined || parse.remainder === undefined) { break; } switch (component) { case "hash": { parse = splitUrlComponent(parse.remainder, "#", "suffix"); this.hash = parse === null || parse === void 0 ? void 0 : parse.result; break; } case "query": { parse = splitUrlComponent(parse.remainder, "?", "suffix"); if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) { this.query = URL.queryFromString(parse.result, this.queryHandling); } break; } case "pathname": { parse = splitUrlComponent(parse.remainder, "/", "suffix"); if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) { // Replace the initial /, since paths require it this.pathname = "/" + parse.result; } break; } case "host": { const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix"); const userInfo = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.result; const hostPort = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.remainder; if (userInfo !== undefined) { const userInfoSplit = userInfo.split(":"); this.username = decodeURIComponent(userInfoSplit[0]); this.password = decodeURIComponent(userInfoSplit[1]); } if (hostPort !== undefined) { const hostPortSplit = hostPort.split(":"); this.host = hostPortSplit[0]; this.port = hostPortSplit[1]; } break; } default: { throw new Error("Unhandled case!"); } } } } get(component) { switch (component) { // Exhaustive match to make sure TS property minifiers and other // transformer plugins do not break this code. case "protocol": return this.protocol; case "username": return this.username; case "password": return this.password; case "port": return this.port; case "pathname": return this.pathname; case "query": return this.query; case "hash": return this.hash; default: // The fallback for component which is not a property of URL object. return this[component]; } } set(component, value) { if (value === undefined) { return this; } if (component === "query") { if (typeof value === "string") { value = URL.queryFromString(value, this.queryHandling); } } switch (component) { // Exhaustive match to make sure TS property minifiers and other // transformer plugins do not break this code. case "protocol": this.protocol = value; break; case "username": this.username = value; break; case "password": this.password = value; break; case "port": this.port = value; break; case "pathname": this.pathname = value; break; case "query": this.query = value; break; case "hash": this.hash = value; break; default: // The fallback for component which is not a property of URL object. this[component] = value; break; } return this; } append(component, value) { let existingValue = this.get(component); let newValue; if (component === "query") { if (existingValue === undefined) { existingValue = {}; } if (typeof value === "string") { value = URL.queryFromString(value, this.queryHandling); } if (typeof existingValue === "string") { newValue = { existingValue, ...value }; } else { newValue = { ...existingValue, ...value }; } } else { if (existingValue === undefined) { existingValue = ""; } let existingValueString = existingValue; if (existingValueString === undefined) { existingValueString = ""; } let newValueString = existingValueString; if (component === "pathname") { const pathLength = existingValueString.length; if (pathLength === 0 || existingValueString[pathLength - 1] !== "/") { newValueString += "/"; } } // The component is not "query" so we treat value as string. // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-plus-operands newValueString += value; newValue = newValueString; } return this.set(component, newValue); } param(key, value) { if (key === null) { return this; } if (this.query === undefined) { this.query = {}; } if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.query[key]; } else { this.query[key] = value; } return this; } removeParam(key) { if (key === undefined || this.query === undefined) { return this; } if (key in this.query) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.query[key]; } return this; } path(value) { return this.append("pathname", value); } pathExtension() { var _a, _b; // Extract path extension if one exists if (this.pathname === undefined) { return undefined; } const lastFilenameComponents = (_b = (_a = this.pathname .split("/") .filter((item) => item.length > 0) // Remove any double or trailing slashes .pop()) === null || _a === void 0 ? void 0 : _a.split(".")) !== null && _b !== void 0 ? _b : []; if (lastFilenameComponents.filter(function (part) { return part !== ""; }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"]) ) { return undefined; } return lastFilenameComponents.pop(); } /** * Returns the path components of the URL * @returns An array of non-empty path components from `urls`. */ pathComponents() { if (this.pathname === undefined) { return []; } return this.pathname.split("/").filter((component) => component.length > 0); } /** * Same as toString * * @returns A string representation of the URL */ build() { return this.toString(); } /** * Converts the URL to a string * * @returns A string representation of the URL */ toString() { let url = ""; if (this.protocol !== undefined) { url += this.protocol + "://"; } if (this.username !== undefined) { url += encodeURIComponent(this.username); if (this.password !== undefined) { url += ":" + encodeURIComponent(this.password); } url += "@"; } if (this.host !== undefined) { url += this.host; if (this.port !== undefined) { url += ":" + this.port; } } if (this.pathname !== undefined) { url += this.pathname; } if (this.query !== undefined && Object.keys(this.query).length !== 0) { url += "?" + URL.toQueryString(this.query, this.queryHandling); } if (this.hash !== undefined) { url += "#" + this.hash; } return url; } // ---------------- // Static API // ---------------- /** * Converts a string into a query dictionary * @param query - The string to parse * @returns The query dictionary containing the key-value pairs in the query string */ static queryFromString(query, queryHandling = QueryHandling.FORM_ENCODED) { const result = {}; let parseResult = queryParamRegex.exec(query); while (parseResult !== null && parseResult.length >= 3) { let key = parseResult[1]; let value = parseResult[2]; // We support the legacy query format for "application/x-www-form-urlencoded" which can represent spaces as "+" symbols. // https://url.spec.whatwg.org/#concept-urlencoded-parser // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url // // For RFC3986 mode, plus signs remain as literal plus signs if (queryHandling === QueryHandling.FORM_ENCODED) { key = key.replace(/\+/g, " "); value = value.replace(/\+/g, " "); } const decodedKey = decodeURIComponent(key); const decodedValue = decodeURIComponent(value); result[decodedKey] = decodedValue; parseResult = queryParamRegex.exec(query); } return result; } /** * Converts a query dictionary into a query string * * @param query - The query dictionary * @returns The string representation of the query dictionary */ static toQueryString(query, queryHandling = QueryHandling.RFC3986) { let queryString = ""; let first = true; for (const key of Object.keys(query)) { if (!first) { queryString += "&"; } first = false; queryString += URL.encodeQueryComponent(key, queryHandling); const value = query[key]; if (value !== null && value.length > 0) { queryString += "=" + URL.encodeQueryComponent(value, queryHandling); } } return queryString; } /** * Encode a query parameter key or value according to the specified mode. * @param component - The key or value to encode * @param queryHandling - The encoding mode * @returns The encoded component */ static encodeQueryComponent(component, queryHandling) { if (queryHandling === QueryHandling.FORM_ENCODED) { // For form-encoded: encode with encodeURIComponent, then convert %20 back to + return encodeURIComponent(component).replace(/%20/g, "+"); } else { // For RFC 3986: standard percent-encoding (spaces become %20) return encodeURIComponent(component); } } static from(url) { return new URL(url); } /** * Convenience method to instantiate a URL from numerous (optional) components * @param protocol - The protocol type * @param host - The host name * @param path - The path * @param query - The query * @param hash - The hash * @param options - Configuration options for URL construction * @returns The new URL object representing the URL */ static fromComponents(protocol, host, path, query, hash, options) { const url = new URL(undefined, options); url.protocol = protocol; url.host = host; url.pathname = path; url.query = query !== null && query !== void 0 ? query : {}; url.hash = hash; return url; } } exports.URL = URL; // MARK: - Helpers function splitUrlComponent(input, marker, style) { const index = input.indexOf(marker); let result; let remainder = input; if (index !== -1) { const prefix = input.slice(0, index); const suffix = input.slice(index + marker.length, input.length); if (style === "prefix") { result = prefix; remainder = suffix; } else { result = suffix; remainder = prefix; } } return { result: result, remainder: remainder, }; } //# sourceMappingURL=urls.js.map