this repo has no description
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