this repo has no description
at main 430 lines 16 kB view raw
1"use strict"; 2// MARK: - Parsing Regular Expressions 3Object.defineProperty(exports, "__esModule", { value: true }); 4exports.URL = exports.QueryHandling = void 0; 5const optional_1 = require("../types/optional"); 6const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i; 7const queryParamRegex = /([^=?&]+)=?([^&]*)/g; 8const componentOrder = ["hash", "query", "pathname", "host"]; 9/** 10 * Defines how query parameters should be parsed and encoded. 11 */ 12var QueryHandling; 13(function (QueryHandling) { 14 /** 15 * Handle according to `application/x-www-form-urlencoded` rules (HTML forms). 16 * 17 * This is the **default decoding mode** for backward compatibility. 18 * 19 * **Example:** 20 * ```typescript 21 * // Input: "?search=hello+world&category=news+articles" 22 * // Parsed: { search: "hello world", category: "news articles" } 23 * // Output: "?search=hello+world&category=news+articles" 24 * ``` 25 * 26 * @see {@link https://url.spec.whatwg.org/#concept-urlencoded-parser WHATWG URL Standard} 27 */ 28 QueryHandling["FORM_ENCODED"] = "form-encoded"; 29 /** 30 * Handle according to RFC 3986 URI specification rules. 31 * 32 * This is the **default encoding mode** for backward compatibility. 33 * 34 * **Example:** 35 * ```typescript 36 * // Input: "?search=hello+world&math=2+2%3D4" 37 * // Parsed: { search: "hello+world", math: "2+2=4" } 38 * // Output: "?search=hello+world&math=2+2%3D4" 39 * ``` 40 * 41 * @see {@link https://tools.ietf.org/html/rfc3986#section-3.4 RFC 3986 Section 3.4} 42 */ 43 QueryHandling["RFC3986"] = "rfc3986"; 44})(QueryHandling = exports.QueryHandling || (exports.QueryHandling = {})); 45class URL { 46 constructor(url, options) { 47 var _a; 48 this.query = {}; 49 this.queryHandling = options === null || options === void 0 ? void 0 : options.queryHandling; 50 if ((0, optional_1.isNothing)(url)) { 51 return; 52 } 53 // Split the protocol from the rest of the urls 54 let remainder = url; 55 const match = protocolRegex.exec(url); 56 if ((0, optional_1.isSome)(match)) { 57 // Pull out the protocol 58 let protocol = match[1]; 59 if (protocol !== null && protocol !== undefined) { 60 protocol = protocol.split(":")[0]; 61 } 62 this.protocol = protocol !== null && protocol !== void 0 ? protocol : undefined; 63 // Save the remainder 64 remainder = (_a = match[3]) !== null && _a !== void 0 ? _a : undefined; 65 } 66 // Then match each component in a specific order 67 let parse = { remainder: remainder, result: undefined }; 68 for (const component of componentOrder) { 69 if (parse === undefined || parse.remainder === undefined) { 70 break; 71 } 72 switch (component) { 73 case "hash": { 74 parse = splitUrlComponent(parse.remainder, "#", "suffix"); 75 this.hash = parse === null || parse === void 0 ? void 0 : parse.result; 76 break; 77 } 78 case "query": { 79 parse = splitUrlComponent(parse.remainder, "?", "suffix"); 80 if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) { 81 this.query = URL.queryFromString(parse.result, this.queryHandling); 82 } 83 break; 84 } 85 case "pathname": { 86 parse = splitUrlComponent(parse.remainder, "/", "suffix"); 87 if ((parse === null || parse === void 0 ? void 0 : parse.result) !== undefined) { 88 // Replace the initial /, since paths require it 89 this.pathname = "/" + parse.result; 90 } 91 break; 92 } 93 case "host": { 94 const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix"); 95 const userInfo = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.result; 96 const hostPort = authorityParse === null || authorityParse === void 0 ? void 0 : authorityParse.remainder; 97 if (userInfo !== undefined) { 98 const userInfoSplit = userInfo.split(":"); 99 this.username = decodeURIComponent(userInfoSplit[0]); 100 this.password = decodeURIComponent(userInfoSplit[1]); 101 } 102 if (hostPort !== undefined) { 103 const hostPortSplit = hostPort.split(":"); 104 this.host = hostPortSplit[0]; 105 this.port = hostPortSplit[1]; 106 } 107 break; 108 } 109 default: { 110 throw new Error("Unhandled case!"); 111 } 112 } 113 } 114 } 115 get(component) { 116 switch (component) { 117 // Exhaustive match to make sure TS property minifiers and other 118 // transformer plugins do not break this code. 119 case "protocol": 120 return this.protocol; 121 case "username": 122 return this.username; 123 case "password": 124 return this.password; 125 case "port": 126 return this.port; 127 case "pathname": 128 return this.pathname; 129 case "query": 130 return this.query; 131 case "hash": 132 return this.hash; 133 default: 134 // The fallback for component which is not a property of URL object. 135 return this[component]; 136 } 137 } 138 set(component, value) { 139 if (value === undefined) { 140 return this; 141 } 142 if (component === "query") { 143 if (typeof value === "string") { 144 value = URL.queryFromString(value, this.queryHandling); 145 } 146 } 147 switch (component) { 148 // Exhaustive match to make sure TS property minifiers and other 149 // transformer plugins do not break this code. 150 case "protocol": 151 this.protocol = value; 152 break; 153 case "username": 154 this.username = value; 155 break; 156 case "password": 157 this.password = value; 158 break; 159 case "port": 160 this.port = value; 161 break; 162 case "pathname": 163 this.pathname = value; 164 break; 165 case "query": 166 this.query = value; 167 break; 168 case "hash": 169 this.hash = value; 170 break; 171 default: 172 // The fallback for component which is not a property of URL object. 173 this[component] = value; 174 break; 175 } 176 return this; 177 } 178 append(component, value) { 179 let existingValue = this.get(component); 180 let newValue; 181 if (component === "query") { 182 if (existingValue === undefined) { 183 existingValue = {}; 184 } 185 if (typeof value === "string") { 186 value = URL.queryFromString(value, this.queryHandling); 187 } 188 if (typeof existingValue === "string") { 189 newValue = { existingValue, ...value }; 190 } 191 else { 192 newValue = { ...existingValue, ...value }; 193 } 194 } 195 else { 196 if (existingValue === undefined) { 197 existingValue = ""; 198 } 199 let existingValueString = existingValue; 200 if (existingValueString === undefined) { 201 existingValueString = ""; 202 } 203 let newValueString = existingValueString; 204 if (component === "pathname") { 205 const pathLength = existingValueString.length; 206 if (pathLength === 0 || existingValueString[pathLength - 1] !== "/") { 207 newValueString += "/"; 208 } 209 } 210 // The component is not "query" so we treat value as string. 211 // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-plus-operands 212 newValueString += value; 213 newValue = newValueString; 214 } 215 return this.set(component, newValue); 216 } 217 param(key, value) { 218 if (key === null) { 219 return this; 220 } 221 if (this.query === undefined) { 222 this.query = {}; 223 } 224 if (value === undefined) { 225 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 226 delete this.query[key]; 227 } 228 else { 229 this.query[key] = value; 230 } 231 return this; 232 } 233 removeParam(key) { 234 if (key === undefined || this.query === undefined) { 235 return this; 236 } 237 if (key in this.query) { 238 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 239 delete this.query[key]; 240 } 241 return this; 242 } 243 path(value) { 244 return this.append("pathname", value); 245 } 246 pathExtension() { 247 var _a, _b; 248 // Extract path extension if one exists 249 if (this.pathname === undefined) { 250 return undefined; 251 } 252 const lastFilenameComponents = (_b = (_a = this.pathname 253 .split("/") 254 .filter((item) => item.length > 0) // Remove any double or trailing slashes 255 .pop()) === null || _a === void 0 ? void 0 : _a.split(".")) !== null && _b !== void 0 ? _b : []; 256 if (lastFilenameComponents.filter(function (part) { 257 return part !== ""; 258 }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"]) 259 ) { 260 return undefined; 261 } 262 return lastFilenameComponents.pop(); 263 } 264 /** 265 * Returns the path components of the URL 266 * @returns An array of non-empty path components from `urls`. 267 */ 268 pathComponents() { 269 if (this.pathname === undefined) { 270 return []; 271 } 272 return this.pathname.split("/").filter((component) => component.length > 0); 273 } 274 /** 275 * Same as toString 276 * 277 * @returns A string representation of the URL 278 */ 279 build() { 280 return this.toString(); 281 } 282 /** 283 * Converts the URL to a string 284 * 285 * @returns A string representation of the URL 286 */ 287 toString() { 288 let url = ""; 289 if (this.protocol !== undefined) { 290 url += this.protocol + "://"; 291 } 292 if (this.username !== undefined) { 293 url += encodeURIComponent(this.username); 294 if (this.password !== undefined) { 295 url += ":" + encodeURIComponent(this.password); 296 } 297 url += "@"; 298 } 299 if (this.host !== undefined) { 300 url += this.host; 301 if (this.port !== undefined) { 302 url += ":" + this.port; 303 } 304 } 305 if (this.pathname !== undefined) { 306 url += this.pathname; 307 } 308 if (this.query !== undefined && Object.keys(this.query).length !== 0) { 309 url += "?" + URL.toQueryString(this.query, this.queryHandling); 310 } 311 if (this.hash !== undefined) { 312 url += "#" + this.hash; 313 } 314 return url; 315 } 316 // ---------------- 317 // Static API 318 // ---------------- 319 /** 320 * Converts a string into a query dictionary 321 * @param query - The string to parse 322 * @returns The query dictionary containing the key-value pairs in the query string 323 */ 324 static queryFromString(query, queryHandling = QueryHandling.FORM_ENCODED) { 325 const result = {}; 326 let parseResult = queryParamRegex.exec(query); 327 while (parseResult !== null && parseResult.length >= 3) { 328 let key = parseResult[1]; 329 let value = parseResult[2]; 330 // We support the legacy query format for "application/x-www-form-urlencoded" which can represent spaces as "+" symbols. 331 // https://url.spec.whatwg.org/#concept-urlencoded-parser 332 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url 333 // 334 // For RFC3986 mode, plus signs remain as literal plus signs 335 if (queryHandling === QueryHandling.FORM_ENCODED) { 336 key = key.replace(/\+/g, " "); 337 value = value.replace(/\+/g, " "); 338 } 339 const decodedKey = decodeURIComponent(key); 340 const decodedValue = decodeURIComponent(value); 341 result[decodedKey] = decodedValue; 342 parseResult = queryParamRegex.exec(query); 343 } 344 return result; 345 } 346 /** 347 * Converts a query dictionary into a query string 348 * 349 * @param query - The query dictionary 350 * @returns The string representation of the query dictionary 351 */ 352 static toQueryString(query, queryHandling = QueryHandling.RFC3986) { 353 let queryString = ""; 354 let first = true; 355 for (const key of Object.keys(query)) { 356 if (!first) { 357 queryString += "&"; 358 } 359 first = false; 360 queryString += URL.encodeQueryComponent(key, queryHandling); 361 const value = query[key]; 362 if (value !== null && value.length > 0) { 363 queryString += "=" + URL.encodeQueryComponent(value, queryHandling); 364 } 365 } 366 return queryString; 367 } 368 /** 369 * Encode a query parameter key or value according to the specified mode. 370 * @param component - The key or value to encode 371 * @param queryHandling - The encoding mode 372 * @returns The encoded component 373 */ 374 static encodeQueryComponent(component, queryHandling) { 375 if (queryHandling === QueryHandling.FORM_ENCODED) { 376 // For form-encoded: encode with encodeURIComponent, then convert %20 back to + 377 return encodeURIComponent(component).replace(/%20/g, "+"); 378 } 379 else { 380 // For RFC 3986: standard percent-encoding (spaces become %20) 381 return encodeURIComponent(component); 382 } 383 } 384 static from(url) { 385 return new URL(url); 386 } 387 /** 388 * Convenience method to instantiate a URL from numerous (optional) components 389 * @param protocol - The protocol type 390 * @param host - The host name 391 * @param path - The path 392 * @param query - The query 393 * @param hash - The hash 394 * @param options - Configuration options for URL construction 395 * @returns The new URL object representing the URL 396 */ 397 static fromComponents(protocol, host, path, query, hash, options) { 398 const url = new URL(undefined, options); 399 url.protocol = protocol; 400 url.host = host; 401 url.pathname = path; 402 url.query = query !== null && query !== void 0 ? query : {}; 403 url.hash = hash; 404 return url; 405 } 406} 407exports.URL = URL; 408// MARK: - Helpers 409function splitUrlComponent(input, marker, style) { 410 const index = input.indexOf(marker); 411 let result; 412 let remainder = input; 413 if (index !== -1) { 414 const prefix = input.slice(0, index); 415 const suffix = input.slice(index + marker.length, input.length); 416 if (style === "prefix") { 417 result = prefix; 418 remainder = suffix; 419 } 420 else { 421 result = suffix; 422 remainder = prefix; 423 } 424 } 425 return { 426 result: result, 427 remainder: remainder, 428 }; 429} 430//# sourceMappingURL=urls.js.map