Files
apps.apple.com/node_modules/@jet/environment/util/urls.js
2025-11-04 05:03:50 +08:00

430 lines
16 KiB
JavaScript

"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