@coroboros/uri 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1665 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_url = require("node:url");
3
+ //#region src/config/index.ts
4
+ /**
5
+ * config module
6
+ */
7
+ const maxLengthURL = 2048;
8
+ const maxPortInteger = 65535;
9
+ //#endregion
10
+ //#region src/helpers/object.ts
11
+ /**
12
+ * Internal type guards.
13
+ *
14
+ * - exists(thing) -> boolean
15
+ * - is(Type, thing) -> boolean
16
+ */
17
+ /**
18
+ * Whether the specified value is not null, undefined or NaN.
19
+ */
20
+ const exists = function exists(thing) {
21
+ return !(thing === void 0 || thing === null || Number.isNaN(thing));
22
+ };
23
+ function is(Type, thing) {
24
+ return exists(Type) && exists(thing) && (thing.constructor === Type || thing instanceof Type);
25
+ }
26
+ //#endregion
27
+ //#region src/checkers/chars.ts
28
+ /**
29
+ * chars checkers
30
+ *
31
+ * - isSchemeChar(char, { start } = {}) -> Boolean
32
+ * - isUserinfoChar(char, encode) -> Boolean
33
+ * - isSitemapUserinfoChar(char, encode) -> Boolean
34
+ * - isDomainChar(char, { start, end } = {}) -> Boolean
35
+ * - isPathChar(char, encode) -> Boolean
36
+ * - isSitemapPathChar(char, encode) -> Boolean
37
+ * - isQueryOrFragmentChar(char, encode) -> Boolean
38
+ * - isSitemapQueryOrFragmentChar(char, encode) -> Boolean
39
+ * - isPercentEncodingChar(char) -> Boolean
40
+ */
41
+ /**
42
+ * @func isSchemeChar
43
+ *
44
+ * Check scheme legal ascii codes according to
45
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.1.
46
+ *
47
+ * Scheme must start with a letter and be followed by any combination of
48
+ * letters, digits, plus ("+"), period ("."), or hyphen ("-").
49
+ *
50
+ * Letters must be in lowercase.
51
+ *
52
+ * 43 +
53
+ * 45 -
54
+ * 46 .
55
+ * 48 to 57 0-9
56
+ * 97 to 122 a-z
57
+ */
58
+ const isSchemeChar = function isSchemeChar(char, { start } = {}) {
59
+ if (!is(String, char)) return false;
60
+ const code = char.charCodeAt(0);
61
+ if (start) return code >= 97 && code <= 122;
62
+ return code >= 48 && code <= 57 || code >= 97 && code <= 122 || code === 43 || code === 45 || code === 46;
63
+ };
64
+ /**
65
+ * @func isUserinfoChar
66
+ *
67
+ * Check userinfo legal ascii codes according to
68
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.2.1.
69
+ *
70
+ * 33 !
71
+ * 36 $
72
+ * 37 % (not allowed when encoding)
73
+ * 38 to 46 &'()*+,-.
74
+ * 48 to 57 0-9
75
+ * 58 :
76
+ * 59 ;
77
+ * 61 =
78
+ * 65 to 90 A-Z
79
+ * 95 _
80
+ * 97 to 122 a-z
81
+ * 126 ~
82
+ */
83
+ const isUserinfoChar = function isUserinfoChar(char, encode) {
84
+ if (!is(String, char)) return false;
85
+ const encoding = encode === true;
86
+ const code = char.charCodeAt(0);
87
+ if (code === 37) return !encoding;
88
+ return code >= 38 && code <= 46 || code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 33 || code === 36 || code === 58 || code === 59 || code === 61 || code === 95 || code === 126;
89
+ };
90
+ /**
91
+ * @func isSitemapUserinfoChar
92
+ *
93
+ * Check sitemap userinfo legal ascii codes according to
94
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.2.1;
95
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
96
+ *
97
+ * Removed AZ ' and *
98
+ *
99
+ * 33 !
100
+ * 36 $
101
+ * 37 % (not allowed when encoding)
102
+ * 38 & (allowed but must be a proper escape code)
103
+ * 40 to 41 ()
104
+ * 43 to 46 +,-.
105
+ * 48 to 57 0-9
106
+ * 58 :
107
+ * 59 ;
108
+ * 61 =
109
+ * 95 _
110
+ * 97 to 122 a-z
111
+ * 126 ~
112
+ */
113
+ const isSitemapUserinfoChar = function isSitemapUserinfoChar(char, encode) {
114
+ if (!is(String, char)) return false;
115
+ const encoding = encode === true;
116
+ const code = char.charCodeAt(0);
117
+ if (code === 37) return !encoding;
118
+ return code >= 40 && code <= 41 || code >= 43 && code <= 46 || code >= 48 && code <= 57 || code >= 97 && code <= 122 || code === 33 || code === 36 || code === 38 || code === 58 || code === 59 || code === 61 || code === 95 || code === 126;
119
+ };
120
+ /**
121
+ * @func isDomainChar
122
+ *
123
+ * Check domain legal codes according to
124
+ * - RFC-1034 https://www.ietf.org/rfc/rfc1034.txt.
125
+ *
126
+ * 45 -
127
+ * 48 to 57 0-9
128
+ * 97 to 122 a-z
129
+ */
130
+ const isDomainChar = function isDomainChar(char, { start, end } = {}) {
131
+ if (!is(String, char)) return false;
132
+ const code = char.charCodeAt(0);
133
+ if ((start === true || end === true) && code === 45) return false;
134
+ return code >= 48 && code <= 57 || code >= 97 && code <= 122 || code === 45;
135
+ };
136
+ /**
137
+ * @func isPathChar
138
+ *
139
+ * Check path legal ascii codes according to
140
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.3.
141
+ *
142
+ * 33 !
143
+ * 36 to 59 $%&'()*+,-./0-9:;
144
+ * 61 =
145
+ * 64 to 90 @A-Z
146
+ * 95 _
147
+ * 97 to 122 a-z
148
+ * 126 ~
149
+ */
150
+ const isPathChar = function isPathChar(char, encode) {
151
+ if (!is(String, char)) return false;
152
+ const encoding = encode === true;
153
+ const code = char.charCodeAt(0);
154
+ if (code === 37) return !encoding;
155
+ return code >= 36 && code <= 59 || code >= 64 && code <= 90 || code >= 97 && code <= 122 || code === 33 || code === 61 || code === 95 || code === 126;
156
+ };
157
+ /**
158
+ * @func isSitemapPathChar
159
+ *
160
+ * Check path legal ascii codes according to
161
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.3.
162
+ *
163
+ * Removed AZ ' and *
164
+ *
165
+ * 33 !
166
+ * 36 to 38 $%& (& allowed but must be a proper escape code)
167
+ * 40 to 41 ()
168
+ * 43 to 59 +,-./0-9:;
169
+ * 61 =
170
+ * 64 @
171
+ * 95 _
172
+ * 97 to 122 a-z
173
+ * 126 ~
174
+ */
175
+ const isSitemapPathChar = function isSitemapPathChar(char, encode) {
176
+ if (!is(String, char)) return false;
177
+ const encoding = encode === true;
178
+ const code = char.charCodeAt(0);
179
+ if (code === 37) return !encoding;
180
+ return code >= 36 && code <= 38 || code >= 40 && code <= 41 || code >= 43 && code <= 59 || code >= 97 && code <= 122 || code === 33 || code === 61 || code === 64 || code === 95 || code === 126;
181
+ };
182
+ /**
183
+ * @func isQueryOrFragmentChar
184
+ *
185
+ * Check query/fragment legal ascii codes according to
186
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.4;
187
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.5.
188
+ *
189
+ * path char
190
+ * 63 ?
191
+ */
192
+ const isQueryOrFragmentChar = function isQueryOrFragmentChar(char, encode) {
193
+ if (isPathChar(char, encode)) return true;
194
+ return is(String, char) && char.charCodeAt(0) === 63;
195
+ };
196
+ /**
197
+ * @func isSitemapQueryOrFragmentChar
198
+ *
199
+ * Check query/fragment legal ascii codes according to
200
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.4;
201
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.5.
202
+ *
203
+ * sitemap path char
204
+ * 63 ?
205
+ */
206
+ const isSitemapQueryOrFragmentChar = function isSitemapQueryOrFragmentChar(char, encode) {
207
+ if (isSitemapPathChar(char, encode)) return true;
208
+ return is(String, char) && char.charCodeAt(0) === 63;
209
+ };
210
+ /**
211
+ * @func isPercentEncodingChar
212
+ *
213
+ * Check percent encoding legal ascii codes according to RFC-3986 https://tools.ietf.org/html/rfc3986#section-2.1.
214
+ *
215
+ * HEXDIG is case-insensitive: %3a and %3A are equivalent
216
+ * (RFC-3986 https://tools.ietf.org/html/rfc3986#section-6.2.2.1).
217
+ *
218
+ * 48 to 57 0-9
219
+ * 65 to 70 A-F
220
+ * 97 to 102 a-f
221
+ */
222
+ const isPercentEncodingChar = function isPercentEncodingChar(char) {
223
+ if (!is(String, char)) return false;
224
+ const code = char.charCodeAt(0);
225
+ return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
226
+ };
227
+ //#endregion
228
+ //#region src/ip/index.ts
229
+ /**
230
+ * IP validator
231
+ *
232
+ * - isIP(ip) -> Boolean
233
+ * - isIPv4(ip) -> Boolean
234
+ * - isIPv6(ip) -> Boolean
235
+ */
236
+ const v4 = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}";
237
+ const v6seg = "[a-fA-F\\d]{1,4}";
238
+ const v6 = `
239
+ (
240
+ (?:${v6seg}:){7}(?:${v6seg}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
241
+ (?:${v6seg}:){6}(?:${v4}|:${v6seg}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
242
+ (?:${v6seg}:){5}(?::${v4}|(:${v6seg}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
243
+ (?:${v6seg}:){4}(?:(:${v6seg}){0,1}:${v4}|(:${v6seg}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
244
+ (?:${v6seg}:){3}(?:(:${v6seg}){0,2}:${v4}|(:${v6seg}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
245
+ (?:${v6seg}:){2}(?:(:${v6seg}){0,3}:${v4}|(:${v6seg}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
246
+ (?:${v6seg}:){1}(?:(:${v6seg}){0,4}:${v4}|(:${v6seg}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
247
+ (?::((?::${v6seg}){0,5}:${v4}|(?::${v6seg}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
248
+ )(%[0-9a-zA-Z]{1,})? // %eth0 %1
249
+ `.replace(/\s*\/\/.*$/gm, "").replace(/\n/g, "").trim();
250
+ const ipv4Regexp = new RegExp(`^${v4}$`);
251
+ const ipv6Regexp$1 = new RegExp(`^${v6}$`);
252
+ const ipRegexp = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
253
+ /**
254
+ * @func isIP
255
+ *
256
+ * Test a string is a valid IP.
257
+ */
258
+ const isIP = function isIP(ip) {
259
+ if (!is(String, ip)) return false;
260
+ return ipRegexp.test(ip);
261
+ };
262
+ /**
263
+ * @func isIPv4
264
+ *
265
+ * Test a string is a valid IPv4.
266
+ */
267
+ const isIPv4 = function isIPv4(ip) {
268
+ if (!is(String, ip)) return false;
269
+ return ipv4Regexp.test(ip);
270
+ };
271
+ /**
272
+ * @func isIPv6
273
+ *
274
+ * Test a string is a valid IPv6.
275
+ */
276
+ const isIPv6 = function isIPv6(ip) {
277
+ if (!is(String, ip)) return false;
278
+ return ipv6Regexp$1.test(ip);
279
+ };
280
+ //#endregion
281
+ //#region src/punycode/index.ts
282
+ /**
283
+ * punycode and punydecode
284
+ *
285
+ * - punycode(domain) -> String
286
+ * - punydecode(domain) -> String
287
+ */
288
+ /**
289
+ * @func punycode
290
+ *
291
+ * Returns the Punycode ASCII serialization of the domain.
292
+ * If domain is an invalid domain, the empty string is returned.
293
+ *
294
+ * Note:
295
+ * - native function url.domainToASCII does not support IPv6 only IPv4;
296
+ * - native function url.domainToASCII throws if no domain is provided or returns
297
+ * `null`, `undefined`, `nan` for `null`, `undefined` or `NaN` values which is
298
+ * not what to be expected.
299
+ */
300
+ const punycode = function punycode(domain) {
301
+ if (isIPv6(domain)) return domain;
302
+ return is(String, domain) ? (0, node_url.domainToASCII)(domain) : "";
303
+ };
304
+ /**
305
+ * @func punydecode
306
+ *
307
+ * Returns the Unicode serialization of the domain.
308
+ * If domain is an invalid domain, the empty string is returned.
309
+ *
310
+ * Note:
311
+ * - native function url.domainToUnicode does not support IPv6 only IPv4;
312
+ * - native function url.domainToUnicode throws if no domain is provided or returns
313
+ * `null`, `undefined`, `nan` for `null`, `undefined` or `NaN` values which is
314
+ * not what to be expected.
315
+ */
316
+ const punydecode = function punydecode(domain) {
317
+ if (isIPv6(domain)) return domain;
318
+ return is(String, domain) ? (0, node_url.domainToUnicode)(domain) : "";
319
+ };
320
+ //#endregion
321
+ //#region src/domain/index.ts
322
+ /**
323
+ * domain
324
+ *
325
+ * - isDomainLabel(label) -> Boolean
326
+ * - isDomain(name) -> Boolean
327
+ */
328
+ /**
329
+ * @func isDomainLabel
330
+ *
331
+ * Test a label is a valid domain label according to RFC-1034.
332
+ *
333
+ * "Note that while upper and lower case letters are allowed in domain names,
334
+ * no significance is attached to the case. That is, two names with the same
335
+ * spelling but different case are to be treated as if identical."
336
+ *
337
+ * By convention uppercased domain label will be considered invalid.
338
+ *
339
+ * Rules:
340
+ * 1. "Labels must be 63 characters or less.";
341
+ * 2. can be minimum one character;
342
+ * 3. must only use lowercase letters, digits or hyphens;
343
+ * 4. must not start or end with a hyphen;
344
+ * 5. must not have consecutive hyphens;
345
+ * 6. can start or end with a digit.
346
+ *
347
+ * Based on:
348
+ * - RFC-1034 https://www.ietf.org/rfc/rfc1034.txt.
349
+ */
350
+ const isDomainLabel = function isDomainLabel(label) {
351
+ if (!is(String, label)) return false;
352
+ const len = label.length;
353
+ if (len < 1 || len > 63) return false;
354
+ for (let i = 0; i < len; i += 1) {
355
+ if (!isDomainChar(label.charAt(i), {
356
+ start: i === 0,
357
+ end: i === len - 1
358
+ })) return false;
359
+ if (label.charAt(i) === "-" && i + 1 < len && label.charAt(i + 1) === "-") return false;
360
+ }
361
+ return true;
362
+ };
363
+ /**
364
+ * @func isDomain
365
+ *
366
+ * Test a name is a valid domain according to RFC-1034.
367
+ *
368
+ * Supports Fully-Qualified Domain Name (FQDN) and Internationalized Domain Name (IDN).
369
+ *
370
+ * Rules:
371
+ * 1. labels rules apply;
372
+ * 2. "[...] the total number of octets that represent a domain name
373
+ * (i.e., the sum of all label octets and label lengths) is limited to 255.";
374
+ * 3. labels are separated by dots (".");
375
+ * 4. must have at least one extension label;
376
+ * 5. must have labels different from each other;
377
+ * 6. last label can be empty (root label ".");
378
+ * 7. labels can start with `xn--` for IDNs if the ASCII serialization is a valid Punycode;
379
+ * 8. check also Punycodes as ॐ gives xn--'-6xd where ' is not valid.
380
+ *
381
+ * Based on:
382
+ * - RFC-1034 https://www.ietf.org/rfc/rfc1034.txt.
383
+ */
384
+ const isDomain = function isDomain(name) {
385
+ if (!is(String, name)) return false;
386
+ const domain = punycode(name);
387
+ if (domain === "localhost") return true;
388
+ const len = domain.length;
389
+ if (len <= 0 || len > 255) return false;
390
+ const labels = domain.split(".");
391
+ const labelsLen = labels.length;
392
+ if (labelsLen <= 1) return false;
393
+ const occurences = {};
394
+ for (const [i, current] of labels.entries()) if (!(i === labelsLen - 1 && current === "")) {
395
+ if (!isDomainLabel(current.startsWith("xn--") ? current.slice(4) : current)) return false;
396
+ occurences[current] = (occurences[current] || 0) + 1;
397
+ if (occurences[current] > 1) return false;
398
+ }
399
+ return true;
400
+ };
401
+ //#endregion
402
+ //#region src/helpers/cast.ts
403
+ /**
404
+ * Type casting helpers.
405
+ *
406
+ * Cast a value to a specific primitive type. If the value is
407
+ * not of this type or can not be infer from this type, undefined is returned.
408
+ *
409
+ * undefined is an interesting value. When stringifying an object, an undefined property
410
+ * disappears. Useful to respect data type schemas and where null values are not allowed.
411
+ *
412
+ * - num(thing, { le, ge } = {}) -> Number or undefined
413
+ * - int(thing, { le, ge } = {}) -> Integer Number or undefined
414
+ */
415
+ /**
416
+ * @func number
417
+ *
418
+ * cast to primitive number if possible or returns undefined
419
+ * because Number(null) returns 0 and Number(undefined|NaN) returns NaN
420
+ * beware to call Number.isFinite only on number values
421
+ * NOTE: only finite values
422
+ */
423
+ const number = function number(thing) {
424
+ let castNum;
425
+ if (exists(thing)) {
426
+ const value = thing.valueOf();
427
+ if (is(Number, value)) {
428
+ if (Number.isFinite(value)) castNum = value;
429
+ } else if (is(String, value) || is(Boolean, value)) {
430
+ const cast = Number(value);
431
+ if (Number.isFinite(cast)) castNum = cast;
432
+ }
433
+ }
434
+ return castNum;
435
+ };
436
+ /**
437
+ * @func integer
438
+ *
439
+ * cast to primitive integer number if possible or returns undefined
440
+ * NOTE: based on "number" function, in base 10 only
441
+ */
442
+ const integer = function integer(thing) {
443
+ const castNum = number(thing);
444
+ let castInt;
445
+ if (castNum !== void 0) {
446
+ const int = parseInt(String(castNum), 10);
447
+ /* v8 ignore next -- unreachable: parseInt of a finite number's String is never NaN */
448
+ if (!Number.isNaN(int)) castInt = int;
449
+ }
450
+ return castInt;
451
+ };
452
+ /**
453
+ * @func int
454
+ *
455
+ * cast to primitive integer number, with 'less or equal than'
456
+ * or 'greater or equal than' options, or returns undefined
457
+ * NOTE: based on "integer" function, in base 10 only
458
+ */
459
+ const int = function int(thing, { ge, le } = {}) {
460
+ let castInt = integer(thing);
461
+ if (castInt !== void 0) {
462
+ const lessThan = integer(le);
463
+ const greaterThan = integer(ge);
464
+ if (lessThan !== void 0 && greaterThan !== void 0) {
465
+ if (castInt < greaterThan || castInt > lessThan) castInt = void 0;
466
+ } else if (lessThan !== void 0 && castInt > lessThan) castInt = void 0;
467
+ else if (greaterThan !== void 0 && castInt < greaterThan) castInt = void 0;
468
+ }
469
+ return castInt;
470
+ };
471
+ /**
472
+ * @func isPort
473
+ *
474
+ * RFC-3986 §3.2.3: port = *DIGIT. True if the value is absent
475
+ * (null/undefined) or a possibly empty string of ASCII digits.
476
+ * The numeric range is validated separately by `int`.
477
+ *
478
+ * Rejects JS Number coercion artefacts (hex `0x1F`, scientific `1e3`,
479
+ * whitespace) that `Number()` would otherwise accept.
480
+ */
481
+ const isPort = function isPort(thing) {
482
+ return thing === null || thing === void 0 || /^[0-9]*$/.test(String(thing));
483
+ };
484
+ //#endregion
485
+ //#region src/helpers/error.ts
486
+ /**
487
+ * @func fail
488
+ *
489
+ * Throw a URIError carrying a stable `code` string. The thrown value is
490
+ * always `instanceof URIError`.
491
+ */
492
+ const fail = function fail(code, message) {
493
+ const error = new URIError(message);
494
+ error.code = code;
495
+ throw error;
496
+ };
497
+ //#endregion
498
+ //#region src/parser/index.ts
499
+ /**
500
+ * parser
501
+ *
502
+ * - hostToURI(host) -> String
503
+ * - recomposeURI({ scheme, userinfo, host port, path, query, fragment } = {}) -> String
504
+ * - parseURI(uri) -> Object
505
+ */
506
+ const uriRegexp = /^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/;
507
+ const ipv6Regexp = /^(?:\[([^\]]+)\]:?)([0-9]+)?$/;
508
+ /**
509
+ * @func hostToURI
510
+ *
511
+ * Format host with special [] for IPv6. The empty string is returned if host
512
+ * is not a string.
513
+ */
514
+ const hostToURI = function hostToURI(host) {
515
+ if (!is(String, host)) return "";
516
+ return isIPv6(host) ? `[${host}]` : host;
517
+ };
518
+ /**
519
+ * @func recomposeURI
520
+ *
521
+ * Recompose an URI from its components with basic URI checking.
522
+ *
523
+ * The empty string is returned if unable to recompose the URI.
524
+ *
525
+ * Rules:
526
+ * 1. scheme is required and must be at least 1 character;
527
+ * 2. path is required and can be empty;
528
+ * 3. if host is present path must be empty or start with /;
529
+ * 4. if host is not present path must not start with //;
530
+ * 5. host, if any, must be at least 3 characters;
531
+ * 6. userinfo will be ignored if empty;
532
+ * 7. port will be ignored if empty or not an integer;
533
+ * 8. query is emitted when defined (a string, including ''); a null
534
+ * or undefined query is omitted (RFC-3986 §5.3);
535
+ * 9. fragment is emitted when defined (a string, including ''); a null
536
+ * or undefined fragment is omitted (RFC-3986 §5.3).
537
+ *
538
+ * Support:
539
+ * - IPv4 and IPv6.
540
+ *
541
+ * Note:
542
+ * / is added to any URI with a host and an empty path.
543
+ *
544
+ * Based on:
545
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986.
546
+ */
547
+ const recomposeURI = function recomposeURI(components) {
548
+ const cpts = components || {};
549
+ const defaultReturnValue = "";
550
+ const { scheme, userinfo, host, port, path, query, fragment } = cpts;
551
+ if (!(is(String, scheme) && scheme.length > 0) || !is(String, path)) return defaultReturnValue;
552
+ let uri = scheme;
553
+ if (is(String, host)) {
554
+ if (!(path === "" || path.startsWith("/"))) return defaultReturnValue;
555
+ if (host.length <= 2) return defaultReturnValue;
556
+ uri += "://";
557
+ if (is(String, userinfo) && userinfo.length > 0) uri += `${userinfo}@`;
558
+ uri += hostToURI(host);
559
+ if (exists(port) && isPort(port) && int(port, {
560
+ ge: 0,
561
+ le: 65535
562
+ }) !== void 0) uri += `:${port}`;
563
+ } else {
564
+ if (path.startsWith("//")) return defaultReturnValue;
565
+ uri += ":";
566
+ }
567
+ if (path === "" && is(String, host)) uri += "/";
568
+ else uri += path;
569
+ if (is(String, query)) uri += `?${query}`;
570
+ if (is(String, fragment)) uri += `#${fragment}`;
571
+ return uri;
572
+ };
573
+ /**
574
+ * @func parseURI
575
+ *
576
+ * Parse a string to get URI components.
577
+ *
578
+ * Support:
579
+ * - IPv4 and IPv6 hosts;
580
+ * - Internationalized Domain Name (IDN).
581
+ *
582
+ * Note:
583
+ * - RegExp from RFC-3986 https://tools.ietf.org/html/rfc3986#appendix-B;
584
+ * - scheme and host strings will always be put in lowercase once parsed,
585
+ * as specified in RFC-3986;
586
+ * - authority and its components will be put at null values if authority
587
+ * parsed is missing or empty.
588
+ *
589
+ * Based on:
590
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986.
591
+ */
592
+ const parseURI = function parseURI(uri) {
593
+ const parsed = {
594
+ scheme: null,
595
+ authority: null,
596
+ authorityPunydecoded: null,
597
+ userinfo: null,
598
+ host: null,
599
+ hostPunydecoded: null,
600
+ port: null,
601
+ path: null,
602
+ pathqf: null,
603
+ query: null,
604
+ fragment: null,
605
+ href: null
606
+ };
607
+ if (!(is(String, uri) && uri.length > 0)) return parsed;
608
+ /* v8 ignore next -- unreachable []: the all-optional Appendix-B regexp always matches a non-empty string */
609
+ const [, scheme, authorityParsed, path, queryParsed, fragmentParsed] = uri.match(uriRegexp) ?? [];
610
+ if (!(is(String, scheme) && scheme.length > 0)) return parsed;
611
+ let authority = null;
612
+ let authorityPunydecoded = null;
613
+ let userinfo = null;
614
+ let host = null;
615
+ let hostPunydecoded = null;
616
+ let port = null;
617
+ if (is(String, authorityParsed)) {
618
+ let hostAndPort = null;
619
+ const userinfoEnd = authorityParsed.lastIndexOf("@");
620
+ if (userinfoEnd === -1) hostAndPort = authorityParsed;
621
+ else {
622
+ userinfo = authorityParsed.slice(0, userinfoEnd);
623
+ hostAndPort = authorityParsed.slice(userinfoEnd + 1);
624
+ }
625
+ /* v8 ignore next -- unreachable false branch: hostAndPort is always an assigned string after the authority split */
626
+ if (is(String, hostAndPort)) {
627
+ const ipv6Match = hostAndPort.match(ipv6Regexp);
628
+ let hostParsed = null;
629
+ let portToCast = null;
630
+ if (Array.isArray(ipv6Match)) [, hostParsed = null, portToCast = null] = ipv6Match;
631
+ else {
632
+ const portStart = hostAndPort.lastIndexOf(":");
633
+ if (portStart === -1) hostParsed = hostAndPort;
634
+ else {
635
+ hostParsed = hostAndPort.slice(0, portStart);
636
+ portToCast = hostAndPort.slice(portStart + 1);
637
+ }
638
+ }
639
+ /* v8 ignore start -- unreachable null branch: the ipv6 regexp's required capture means hostParsed is always a string here */
640
+ const hostLowerCase = is(String, hostParsed) ? hostParsed.toLowerCase() : null;
641
+ const toASCII = punycode(hostLowerCase ?? "");
642
+ const toUnicode = punydecode(hostLowerCase ?? "");
643
+ /* v8 ignore stop */
644
+ if (hostLowerCase !== toASCII) {
645
+ host = toASCII;
646
+ hostPunydecoded = hostLowerCase;
647
+ } else if (hostLowerCase !== toUnicode) {
648
+ host = hostLowerCase;
649
+ hostPunydecoded = toUnicode;
650
+ } else {
651
+ host = hostLowerCase;
652
+ hostPunydecoded = hostLowerCase;
653
+ }
654
+ if (host === "" || hostPunydecoded === "") host = null;
655
+ port = is(String, portToCast) && portToCast.length > 0 && !isPort(portToCast) ? portToCast : int(portToCast) || portToCast;
656
+ /* v8 ignore next -- unreachable false branch: hostPunydecoded is always an assigned string in this block */
657
+ if (exists(hostPunydecoded)) {
658
+ authorityPunydecoded = "";
659
+ if (exists(userinfo)) authorityPunydecoded += `${userinfo}@`;
660
+ authorityPunydecoded += hostToURI(hostPunydecoded);
661
+ if (exists(port)) authorityPunydecoded += `:${port}`;
662
+ }
663
+ if (exists(host)) {
664
+ authority = "";
665
+ if (exists(userinfo)) authority += `${userinfo}@`;
666
+ authority += hostToURI(host);
667
+ if (exists(port)) authority += `:${port}`;
668
+ } else {
669
+ userinfo = null;
670
+ port = null;
671
+ }
672
+ }
673
+ }
674
+ const query = is(String, queryParsed) ? queryParsed : null;
675
+ const fragment = is(String, fragmentParsed) ? fragmentParsed : null;
676
+ /* v8 ignore next -- unreachable null branch: the Appendix-B regexp always captures a string path */
677
+ parsed.pathqf = is(String, path) ? path.valueOf() : null;
678
+ if (is(String, parsed.pathqf)) {
679
+ if (is(String, query)) parsed.pathqf += `?${query}`;
680
+ if (is(String, fragment)) parsed.pathqf += `#${fragment}`;
681
+ }
682
+ parsed.scheme = scheme.toLowerCase();
683
+ parsed.authority = authority;
684
+ parsed.authorityPunydecoded = authorityPunydecoded;
685
+ parsed.userinfo = userinfo;
686
+ parsed.host = host;
687
+ parsed.hostPunydecoded = hostPunydecoded;
688
+ parsed.port = port;
689
+ /* v8 ignore next -- unreachable: the Appendix-B regexp always captures a string path */
690
+ parsed.path = path ?? null;
691
+ parsed.query = query;
692
+ parsed.fragment = fragment;
693
+ const recomposedURI = recomposeURI({
694
+ scheme: parsed.scheme,
695
+ userinfo: parsed.userinfo,
696
+ host: parsed.host,
697
+ port: parsed.port,
698
+ path: parsed.path,
699
+ query: parsed.query,
700
+ fragment: parsed.fragment
701
+ });
702
+ parsed.href = recomposedURI !== "" ? recomposedURI : null;
703
+ return parsed;
704
+ };
705
+ //#endregion
706
+ //#region src/sitemap/index.ts
707
+ /**
708
+ * sitemap
709
+ *
710
+ * Percent encodings, entities and escape codes.
711
+ *
712
+ * - specialChars
713
+ * - specialCharsKeys
714
+ * - pencodings
715
+ * - pencodingsKeys
716
+ * - entities
717
+ * - entitiesKeys
718
+ * - escapeCodes
719
+ * - escapeCodesKeys
720
+ * - escapeCodesKeysLen
721
+ */
722
+ const specialChars = { "*": "%2A" };
723
+ const specialCharsKeys = Object.keys(specialChars);
724
+ const pencodings = {};
725
+ specialCharsKeys.forEach((char) => {
726
+ pencodings[specialChars[char]] = char;
727
+ });
728
+ const pencodingsKeys = Object.keys(pencodings);
729
+ const entities = {
730
+ "&": "&amp;",
731
+ "'": "&apos;",
732
+ "\"": "&quot;",
733
+ ">": "&gt;",
734
+ "<": "&lt;"
735
+ };
736
+ const entitiesKeys = Object.keys(entities);
737
+ const escapeCodes = {};
738
+ entitiesKeys.forEach((entity) => {
739
+ escapeCodes[entities[entity]] = entity;
740
+ });
741
+ const escapeCodesKeys = Object.keys(escapeCodes);
742
+ const escapeCodesKeysLen = escapeCodesKeys.length;
743
+ //#endregion
744
+ //#region src/checkers/index.ts
745
+ /**
746
+ * checkers
747
+ *
748
+ * - checkPercentEncoding(string, index, stringLen) -> Number throws URIError
749
+ * - checkSitemapEncoding(string, index, stringLen) -> Number throws URIError
750
+ * - checkComponent({ type, string, sitemap } = {}) -> Boolean throws URIError
751
+ * - checkSchemeChars(scheme, len) -> Boolean throws URIError
752
+ * - checkLowercase(uri) -> Boolean throws URIError
753
+ * - checkURISyntax(uri) -> Object throws URIError
754
+ * - checkURI(uri, { sitemap } = {}) -> Object throws URIError
755
+ * - checkHttpURL(uri, { https, web, sitemap } = {}) -> Object throws URIError
756
+ * - checkHttpsURL(uri) -> Object throws URIError
757
+ * - checkHttpSitemapURL(uri) -> Object throws URIError
758
+ * - checkHttpsSitemapURL(uri) -> Object throws URIError
759
+ * - checkWebURL(uri) -> Object throws URIError
760
+ * - checkSitemapURL(uri) -> Object throws URIError
761
+ */
762
+ const ipv6ZoneIdRegexp = /^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$/;
763
+ /**
764
+ * @func checkPercentEncoding
765
+ *
766
+ * Check a % char found from a string at a specific index has a valid
767
+ * percent encoding following this char.
768
+ */
769
+ const checkPercentEncoding = function checkPercentEncoding(string, index, stringLen) {
770
+ if (!is(String, string)) fail("URI_INVALID_PERCENT_ENCODING", "a string is required when checking for percent encoding");
771
+ const len = is(Number, stringLen) && stringLen >= 0 ? stringLen : string.length;
772
+ const i = is(Number, index) && index < len ? index : 0;
773
+ let offset = 0;
774
+ if (len > 0 && string.charAt(i) === "%") if (i + 2 < len) if (!isPercentEncodingChar(string.charAt(i + 1))) fail("URI_INVALID_PERCENT_ENCODING", `invalid percent encoding char '${string.charAt(i + 1)}'`);
775
+ else if (!isPercentEncodingChar(string.charAt(i + 2))) fail("URI_INVALID_PERCENT_ENCODING", `invalid percent encoding char '${string.charAt(i + 2)}'`);
776
+ else offset = 2;
777
+ else fail("URI_INVALID_PERCENT_ENCODING", "incomplete percent encoding found");
778
+ return offset;
779
+ };
780
+ /**
781
+ * @func checkSitemapEncoding
782
+ *
783
+ * Check an entity in an URL at a specific index has a valid
784
+ * sitemap escape encoding following this char.
785
+ */
786
+ const checkSitemapEncoding = function checkSitemapEncoding(string, index, stringLen) {
787
+ if (!is(String, string)) fail("URI_INVALID_SITEMAP_ENCODING", "a string is required when checking for sitemap encoding");
788
+ const len = is(Number, stringLen) && stringLen >= 0 ? stringLen : string.length;
789
+ const i = is(Number, index) && index < len ? index : 0;
790
+ let offset = 0;
791
+ if (len > 0 && string.charAt(i) === "&") {
792
+ let escapeOffset;
793
+ for (let j = 0; j < escapeCodesKeysLen; j += 1) {
794
+ const code = escapeCodesKeys[j];
795
+ /* v8 ignore next 3 -- unreachable: j is bounded by escapeCodesKeys.length so the index is always defined */
796
+ if (code === void 0) break;
797
+ const codeLen = code.length;
798
+ if (i + codeLen <= len && code === string.substring(i, i + codeLen)) {
799
+ escapeOffset = codeLen - 1;
800
+ break;
801
+ }
802
+ }
803
+ if (!exists(escapeOffset)) fail("URI_INVALID_SITEMAP_ENCODING", `entity '${string.charAt(i)}' is not properly escaped`);
804
+ else offset = escapeOffset;
805
+ }
806
+ return offset;
807
+ };
808
+ /**
809
+ * @func checkComponent
810
+ *
811
+ * Check a string has valid characters regarding userinfo, path, query,
812
+ * or fragment URI component type.
813
+ *
814
+ * NOTE:
815
+ * - check only if string is present as these components are not required;
816
+ * - path is required but is at least empty,
817
+ * regexp assures that and checkURISyntax verifies that too.
818
+ */
819
+ const checkComponent = function checkComponent({ type, string, sitemap } = {}) {
820
+ if (![
821
+ "userinfo",
822
+ "path",
823
+ "query",
824
+ "fragment"
825
+ ].includes(type)) fail("URI_INVALID_CHECKING_COMPONENT", `unable to check pathqf, got '${type}' component to check`);
826
+ if (type === "path" && (!exists(string) || string === "") || !exists(string)) return true;
827
+ const len = string.length;
828
+ const checkSitemap = sitemap === true;
829
+ let checkCharFunc;
830
+ switch (type) {
831
+ case "userinfo":
832
+ checkCharFunc = checkSitemap ? isSitemapUserinfoChar : isUserinfoChar;
833
+ break;
834
+ case "path":
835
+ checkCharFunc = checkSitemap ? isSitemapPathChar : isPathChar;
836
+ break;
837
+ case "query":
838
+ case "fragment":
839
+ checkCharFunc = checkSitemap ? isSitemapQueryOrFragmentChar : isQueryOrFragmentChar;
840
+ break;
841
+ /* v8 ignore next -- unreachable: type is validated to one of the four cases before the switch */
842
+ default:
843
+ }
844
+ for (let i = 0; i < len; i += 1) {
845
+ if (!checkCharFunc(string.charAt(i))) fail(`URI_INVALID_${type.toUpperCase()}_CHAR`, `invalid ${type} char '${string.charAt(i)}'`);
846
+ i += checkPercentEncoding(string, i, len);
847
+ if (checkSitemap) i += checkSitemapEncoding(string, i, len);
848
+ }
849
+ return true;
850
+ };
851
+ /**
852
+ * @func checkSchemeChars
853
+ *
854
+ * Check scheme characters are valid.
855
+ *
856
+ * Based on:
857
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-3.1.
858
+ */
859
+ const checkSchemeChars = function checkSchemeChars(scheme, len) {
860
+ if (!is(String, scheme)) fail("URI_INVALID_SCHEME", "scheme must be a string");
861
+ const schemeLen = is(Number, len) && len > 0 ? len : scheme.length;
862
+ if (schemeLen <= 0) fail("URI_INVALID_SCHEME", "scheme cannot be empty");
863
+ for (let i = 0; i < schemeLen; i += 1) if (!isSchemeChar(scheme.charAt(i), { start: i === 0 })) fail("URI_INVALID_SCHEME_CHAR", `invalid scheme char '${scheme.charAt(i)}'`);
864
+ return true;
865
+ };
866
+ /**
867
+ * @func checkLowercase
868
+ *
869
+ * Check a string has not any uppercase characters.
870
+ */
871
+ const checkLowercase = function checkLowercase(uri) {
872
+ if (!is(String, uri)) fail("URI_INVALID_TYPE", "uri must be a string");
873
+ if (uri.toLowerCase() !== uri) fail("URI_INVALID_CHAR", "uri cannot contain any uppercase characters");
874
+ return true;
875
+ };
876
+ /**
877
+ * @func checkURISyntax
878
+ *
879
+ * Check an URI syntax is valid according to RFC-3986.
880
+ *
881
+ * Beware this function does not fully check if an URI is valid.
882
+ * Rules:
883
+ * 1. scheme is required and cannot be empty;
884
+ * 2. path is required and can be empty;
885
+ * 3. if authority is present path must be empty or start with /;
886
+ * 4. if authority is not present path must not start with //;
887
+ * 5. check for inconsistent authority (original vs parsed)
888
+ * which would mean host parsed was actually wrong.
889
+ */
890
+ const checkURISyntax = function checkURISyntax(uri) {
891
+ if (!is(String, uri)) fail("URI_INVALID_TYPE", "uri must be a string");
892
+ const { scheme, authority, authorityPunydecoded, userinfo, host, hostPunydecoded, port, path, pathqf, query, fragment, href } = parseURI(uri);
893
+ const schemeLen = is(String, scheme) ? scheme.length : 0;
894
+ if (!is(String, scheme)) fail("URI_MISSING_SCHEME", "uri scheme is required");
895
+ else if (schemeLen <= 0) fail("URI_EMPTY_SCHEME", "uri scheme must not be empty");
896
+ /* v8 ignore stop */
897
+ /* v8 ignore next 3 -- unreachable: the Appendix-B regexp always captures a string path */
898
+ if (!is(String, path)) fail("URI_MISSING_PATH", "uri path is required");
899
+ if (is(String, authority) && authority.length > 0) {
900
+ /* v8 ignore next 3 -- unreachable: when authority is present the Appendix-B regexp makes path empty or '/'-prefixed */
901
+ if (!(path === "" || path.startsWith("/"))) fail("URI_INVALID_PATH", "path must be empty or start with '/' when authority is present");
902
+ } else if (path.startsWith("//")) fail("URI_INVALID_PATH", "path must not start with '//' when authority is not present");
903
+ if (!exists(authority) && exists(authorityPunydecoded)) fail("URI_INVALID_HOST", `host must be a valid ip or domain name, got '${hostPunydecoded}'`);
904
+ if (is(String, host) && host.includes(":")) {
905
+ const zoneAt = host.indexOf("%");
906
+ if (zoneAt !== -1) {
907
+ const zoneId = host.slice(zoneAt + 3);
908
+ if (host.slice(zoneAt, zoneAt + 3) !== "%25") fail("URI_INVALID_HOST", `IPv6 zone identifier must use the '%25' delimiter, got '${host}'`);
909
+ if (zoneId === "" || !ipv6ZoneIdRegexp.test(zoneId)) fail("URI_INVALID_HOST", `IPv6 zone identifier must be a non-empty RFC 6874 ZoneID, got '${host}'`);
910
+ }
911
+ }
912
+ return {
913
+ scheme,
914
+ authority,
915
+ authorityPunydecoded,
916
+ userinfo,
917
+ host,
918
+ hostPunydecoded,
919
+ port,
920
+ path,
921
+ pathqf,
922
+ query,
923
+ fragment,
924
+ href,
925
+ schemeLen,
926
+ valid: true
927
+ };
928
+ };
929
+ /**
930
+ * @func checkURI
931
+ *
932
+ * Check an URI is valid according to RFC-3986.
933
+ *
934
+ * Rules:
935
+ * 1. scheme is required and cannot be empty;
936
+ * 2. path is required and can be empty;
937
+ * 3. if authority is present path must be empty or start with /;
938
+ * 4. if authority is not present path must not start with //;
939
+ * 5. scheme can only have specific characters:
940
+ * https://tools.ietf.org/html/rfc3986#section-3.1;
941
+ * 6. if authority is present:
942
+ * 1. host must be a valid IP or domain name;
943
+ * 2. userinfo, if any, can only have specific characters:
944
+ * https://tools.ietf.org/html/rfc3986#section-3.2.1;
945
+ * 3. port, if any, must be an integer in a specific range.
946
+ * 7. path, query and fragment can only have specific characters:
947
+ * https://tools.ietf.org/html/rfc3986#section-3.3.
948
+ */
949
+ const checkURI = function checkURI(uri, { sitemap } = {}) {
950
+ const { scheme, authority, authorityPunydecoded, userinfo, host, hostPunydecoded, port, path, pathqf, query, fragment, href, schemeLen } = checkURISyntax(uri);
951
+ checkSchemeChars(scheme, schemeLen);
952
+ if (exists(authority)) {
953
+ checkComponent({
954
+ sitemap,
955
+ type: "userinfo",
956
+ string: userinfo
957
+ });
958
+ if (!isIP(host) && !isDomain(host)) fail("URI_INVALID_HOST", `host must be a valid ip or domain name, got '${host}'`);
959
+ if (exists(port) && (!isPort(port) || int(port, {
960
+ ge: 0,
961
+ le: 65535
962
+ }) === void 0)) fail("URI_INVALID_PORT", `port must be an integer between 0-${maxPortInteger}, got '${port}'`);
963
+ }
964
+ checkComponent({
965
+ sitemap,
966
+ type: "path",
967
+ string: path
968
+ });
969
+ checkComponent({
970
+ sitemap,
971
+ type: "query",
972
+ string: query
973
+ });
974
+ checkComponent({
975
+ sitemap,
976
+ type: "fragment",
977
+ string: fragment
978
+ });
979
+ return {
980
+ scheme,
981
+ authority,
982
+ authorityPunydecoded,
983
+ userinfo,
984
+ host,
985
+ hostPunydecoded,
986
+ port,
987
+ path,
988
+ pathqf,
989
+ query,
990
+ fragment,
991
+ href,
992
+ valid: true
993
+ };
994
+ };
995
+ /**
996
+ * @func checkHttpURL
997
+ *
998
+ * Check an URI is a valid HTTP URL (sitemap URLs supported to create aliases).
999
+ *
1000
+ * This function uses checkURI to check URI provided is valid.
1001
+ *
1002
+ * Rules:
1003
+ * 1. scheme must be http or HTTP;
1004
+ * 2. authority is required;
1005
+ * 3. URL must be less than max length.
1006
+ *
1007
+ * Based on:
1008
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1009
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1010
+ */
1011
+ const checkHttpURL = function checkHttpURL(uri, { https, web, sitemap } = {}) {
1012
+ if (sitemap === true) checkLowercase(uri);
1013
+ const schemesToCheck = [];
1014
+ if (https === true) schemesToCheck.push("https");
1015
+ else if (web === true) schemesToCheck.push("http", "https");
1016
+ else schemesToCheck.push("http");
1017
+ const { scheme, authority, authorityPunydecoded, userinfo, host, hostPunydecoded, port, path, pathqf, query, fragment, href } = checkURI(uri, { sitemap });
1018
+ if (!schemesToCheck.includes(scheme)) fail("URI_INVALID_SCHEME", `scheme must be ${schemesToCheck.join(" or ")}, got '${scheme}'`);
1019
+ if (!is(String, authority)) fail("URI_MISSING_AUTHORITY", "authority is required");
1020
+ if (is(String, href) && href.length >= 2048) fail("URI_MAX_LENGTH_URL", `max URL length of ${maxLengthURL} reached: ${href.length}`);
1021
+ return {
1022
+ scheme,
1023
+ authority,
1024
+ authorityPunydecoded,
1025
+ userinfo,
1026
+ host,
1027
+ hostPunydecoded,
1028
+ port,
1029
+ path,
1030
+ pathqf,
1031
+ query,
1032
+ fragment,
1033
+ href,
1034
+ valid: true
1035
+ };
1036
+ };
1037
+ /**
1038
+ * @func checkHttpsURL
1039
+ *
1040
+ * Check an URI is a valid HTTPS URL.
1041
+ *
1042
+ * Same behavior than checkHttpURL except scheme must be https or HTTPS.
1043
+ */
1044
+ const checkHttpsURL = function checkHttpsURL(uri) {
1045
+ return checkHttpURL(uri, { https: true });
1046
+ };
1047
+ /**
1048
+ * @func checkHttpSitemapURL
1049
+ *
1050
+ * Check an URI is a valid HTTP URL to be used in an XML sitemap file.
1051
+ *
1052
+ * This function uses checkHttpURL to check URI provided is a valid HTTP URL.
1053
+ *
1054
+ * Rules:
1055
+ * 1. scheme must be http;
1056
+ * 2. authority is required;
1057
+ * 3. specific characters must be escaped;
1058
+ * 4. can only contain lowercase characters;
1059
+ * 5. URL must be less than max length.
1060
+ *
1061
+ * Based on:
1062
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1063
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1064
+ */
1065
+ const checkHttpSitemapURL = function checkHttpSitemapURL(uri) {
1066
+ return checkHttpURL(uri, { sitemap: true });
1067
+ };
1068
+ /**
1069
+ * @func checkHttpsSitemapURL
1070
+ *
1071
+ * Check an URI is a valid HTTPS URL to be used in an XML sitemap file.
1072
+ * Same behavior than checkHttpSitemapURL except scheme must be https.
1073
+ */
1074
+ const checkHttpsSitemapURL = function checkHttpsSitemapURL(uri) {
1075
+ return checkHttpURL(uri, {
1076
+ https: true,
1077
+ sitemap: true
1078
+ });
1079
+ };
1080
+ /**
1081
+ * @func checkWebURL
1082
+ *
1083
+ * Check an URI is a valid HTTP or HTTPS URL.
1084
+ *
1085
+ * Same behavior than checkHttpURL except scheme can be http/HTTP or https/HTTPS.
1086
+ */
1087
+ const checkWebURL = function checkWebURL(uri) {
1088
+ return checkHttpURL(uri, { web: true });
1089
+ };
1090
+ /**
1091
+ * @func checkSitemapURL
1092
+ *
1093
+ * Check an URI is a valid HTTP or HTTPS URL to be used in an XML sitemap file.
1094
+ *
1095
+ * Same behavior than checkHttpSitemapURL except scheme can be http or https.
1096
+ */
1097
+ const checkSitemapURL = function checkSitemapURL(uri) {
1098
+ return checkHttpURL(uri, {
1099
+ web: true,
1100
+ sitemap: true
1101
+ });
1102
+ };
1103
+ //#endregion
1104
+ //#region src/decoders/index.ts
1105
+ /**
1106
+ * decoders
1107
+ *
1108
+ * - decodeURIComponentString(component, { sitemap, lowercase } = {}) -> String
1109
+ * - decodeURIString(uri, { sitemap, lowercase } = {}) -> String throws URIError
1110
+ * - decodeWebURL(uri, { lowercase } = {}) -> String
1111
+ * - decodeSitemapURL(uri, { lowercase } = {}) -> String
1112
+ */
1113
+ const sitemapDecodeRegexp = new RegExp(escapeCodesKeys.concat(pencodingsKeys).join("|"), "g");
1114
+ /**
1115
+ * @func decodeURIComponentString
1116
+ *
1117
+ * Decode an URI component string with Sitemap's escape codes support.
1118
+ *
1119
+ * Native function decodeURIComponent could throw and to be consistent with
1120
+ * encodeURIComponentString the empty string is returned if unable to decode.
1121
+ *
1122
+ * Based on:
1123
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1124
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1125
+ */
1126
+ const decodeURIComponentString = function decodeURIComponentString(component, { sitemap, lowercase } = {}) {
1127
+ if (!is(String, component)) return "";
1128
+ const componentToDecode = lowercase === true ? component.toLowerCase() : component;
1129
+ if (sitemap === true) {
1130
+ const uriToDecode = componentToDecode.replace(
1131
+ sitemapDecodeRegexp,
1132
+ /* v8 ignore next -- unreachable '': the regexp is built from these keys so every match resolves */
1133
+ (match) => escapeCodes[match] || pencodings[match] || ""
1134
+ );
1135
+ try {
1136
+ return decodeURIComponent(uriToDecode);
1137
+ } catch {
1138
+ return "";
1139
+ }
1140
+ }
1141
+ try {
1142
+ return decodeURIComponent(componentToDecode);
1143
+ } catch {
1144
+ return "";
1145
+ }
1146
+ };
1147
+ /**
1148
+ * @func decodeURIString
1149
+ *
1150
+ * Decode an URI string according to RFC-3986 with basic checking.
1151
+ *
1152
+ * Checked:
1153
+ * - scheme is required;
1154
+ * - path is required, can be empty;
1155
+ * - port, if any, must be an integer in a specific range;
1156
+ * - host must be a valid ip or domain name;
1157
+ * - maximum size once encoded for URLs.
1158
+ *
1159
+ * Support:
1160
+ * - IDNs: returns URI with its Punydecoded host (Unicode serialization of the domain), if any;
1161
+ * - lower and upper case.
1162
+ *
1163
+ * Note:
1164
+ * - if one of userinfo, path, query or fragment component cannot be decoded, it will be ignored;
1165
+ * - native function decodeURI does not support IDNs and cannot properly work
1166
+ * with encodeURI since the function is based on an outdated standard;
1167
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase;
1168
+ * - to only use with encodeURIString.
1169
+ *
1170
+ * Based on:
1171
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1172
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1173
+ */
1174
+ const decodeURIString = function decodeURIString(uri, { web, sitemap, lowercase } = {}) {
1175
+ const uriToDecode = is(String, uri) && lowercase === true ? uri.toLowerCase() : uri;
1176
+ const webURL = web === true || sitemap === true;
1177
+ const { scheme, authority, userinfo, host, hostPunydecoded, port, path, query, fragment, schemeLen } = checkURISyntax(uriToDecode);
1178
+ if (webURL) {
1179
+ if (scheme !== "http" && scheme !== "https") fail("URI_INVALID_SCHEME", `scheme must be http or https, got '${scheme}'`);
1180
+ } else checkSchemeChars(scheme, schemeLen);
1181
+ if (webURL && !is(String, authority)) fail("URI_MISSING_AUTHORITY", "authority is required");
1182
+ if (exists(host) && !isIP(host) && !isDomain(host)) fail("URI_INVALID_HOST", `host must be a valid ip or domain name, got '${host}'`);
1183
+ if (exists(port) && (!isPort(port) || int(port, {
1184
+ ge: 0,
1185
+ le: 65535
1186
+ }) === void 0)) fail("URI_INVALID_PORT", `port must be an integer between 0-${maxPortInteger}, got '${port}'`);
1187
+ const userinfoDecoded = decodeURIComponentString(userinfo ?? "", {
1188
+ sitemap,
1189
+ lowercase: false
1190
+ });
1191
+ /* v8 ignore next -- unreachable '': checkURISyntax always yields a string path */
1192
+ const pathDecoded = decodeURIComponentString(path ?? "", {
1193
+ sitemap,
1194
+ lowercase: false
1195
+ });
1196
+ const decodeComponent = (value) => {
1197
+ if (!is(String, value) || value === "") return value;
1198
+ return decodeURIComponentString(value, {
1199
+ sitemap,
1200
+ lowercase: false
1201
+ }) || null;
1202
+ };
1203
+ const uridecoded = recomposeURI({
1204
+ scheme,
1205
+ port,
1206
+ host: hostPunydecoded,
1207
+ userinfo: userinfoDecoded,
1208
+ path: pathDecoded,
1209
+ query: decodeComponent(query),
1210
+ fragment: decodeComponent(fragment)
1211
+ });
1212
+ if (webURL && uridecoded.length >= 2048) fail("URI_MAX_LENGTH_URL", `max URL length of ${maxLengthURL} reached: ${uridecoded.length}`);
1213
+ return uridecoded;
1214
+ };
1215
+ /**
1216
+ * @func decodeWebURL
1217
+ *
1218
+ * Decode an URI string with basic checking based on RFC-3986 standard applied
1219
+ * to HTTP and HTTPS URLs.
1220
+ *
1221
+ * Uses a fixed decodeURI function to be RFC-3986 compliant.
1222
+ *
1223
+ * Checked:
1224
+ * - scheme must be http/HTTP or https/HTTPS;
1225
+ * - path is required, can be empty;
1226
+ * - authority is required;
1227
+ * - port, if any, must be an integer in a specific range;
1228
+ * - parseURI prechecked host, will be null if invalid and so does authority.
1229
+ *
1230
+ * Support:
1231
+ * - IDNs: returns URI with its Punydecoded host
1232
+ * (Unicode serialization of the domain), if any;
1233
+ * - lower and upper case.
1234
+ *
1235
+ * Note:
1236
+ * - native function decodeURI does not support IDNs and cannot properly work
1237
+ * with encodeURI since the function is based on an outdated standard;
1238
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase;
1239
+ * - to use only with encodeWebURL.
1240
+ */
1241
+ const decodeWebURL = function decodeWebURL(uri, { lowercase } = {}) {
1242
+ return decodeURIString(uri, {
1243
+ lowercase,
1244
+ web: true
1245
+ });
1246
+ };
1247
+ /**
1248
+ * @func decodeSitemapURL
1249
+ *
1250
+ * Decode an URI string with basic checking based on RFC-3986 standard applied
1251
+ * to HTTP and HTTPS URLs and sitemap requirements regarding escape codes to decode.
1252
+ *
1253
+ * Checked:
1254
+ * - scheme must be http/HTTP or https/HTTPS;
1255
+ * - path is required, can be empty;
1256
+ * - authority is required;
1257
+ * - port, if any, must be an integer in a specific range;
1258
+ * - parseURI prechecked host, will be null if invalid and so does authority.
1259
+ *
1260
+ * Support:
1261
+ * - Sitemap's escape codes;
1262
+ * - IDNs: returns URI with its Punydecoded host
1263
+ * (Unicode serialization of the domain), if any;
1264
+ * - lower and upper case.
1265
+ *
1266
+ * Note:
1267
+ * - native function decodeURI does not support IDNs and cannot properly work
1268
+ * with encodeURI since the function is based on an outdated standard;
1269
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase;
1270
+ * - to use only with encodeSitemapURL.
1271
+ *
1272
+ * Based on:
1273
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1274
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1275
+ */
1276
+ const decodeSitemapURL = function decodeSitemapURL(uri, { lowercase } = {}) {
1277
+ return decodeURIString(uri, {
1278
+ lowercase,
1279
+ sitemap: true
1280
+ });
1281
+ };
1282
+ //#endregion
1283
+ //#region src/encoders/index.ts
1284
+ /**
1285
+ * encoders
1286
+ *
1287
+ * - encodeURIComponentString(uri, { sitemap, lowercase } = {}) -> String
1288
+ * - encodeURIString(uri, { web, sitemap, lowercase } = {}) -> String throws URIError
1289
+ * - encodeWebURL(uri, { lowercase } = {}) -> String
1290
+ * - encodeSitemapURL(uri, { lowercase } = {}) -> String
1291
+ */
1292
+ /**
1293
+ * @func encodeURIComponentString
1294
+ *
1295
+ * Encode an URI component according to RFC-3986 with Sitemap entities support.
1296
+ *
1297
+ * Support:
1298
+ * - Sitemap's special characters;
1299
+ * - lower and upper case.
1300
+ *
1301
+ * Note:
1302
+ * - only userinfo, path, query and fragment components can be encoded;
1303
+ * - scheme and authority host+port can never have percent encoded characters;
1304
+ * - the empty string is returned if unable to encode;
1305
+ * - sitemap characters must be in lowercase and escaped for XML sitemap.
1306
+ *
1307
+ * Based on:
1308
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1309
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1310
+ */
1311
+ const encodeURIComponentString = function encodeURIComponentString(component, { type, sitemap, lowercase } = {}) {
1312
+ if (!is(String, component)) return "";
1313
+ const componentToEncode = lowercase === true || sitemap === true ? component.toLowerCase() : component;
1314
+ const componentToEncodeLen = componentToEncode.length;
1315
+ let uricomponent = "";
1316
+ for (let i = 0; i < componentToEncodeLen; i += 1) {
1317
+ const char = componentToEncode.charAt(i);
1318
+ let encoded = false;
1319
+ if (sitemap === true) {
1320
+ const entity = entities[char];
1321
+ const special = specialChars[char];
1322
+ if (exists(entity)) {
1323
+ uricomponent += entity;
1324
+ encoded = true;
1325
+ } else if (exists(special)) {
1326
+ uricomponent += special;
1327
+ encoded = true;
1328
+ }
1329
+ }
1330
+ if (!encoded) {
1331
+ let isChar;
1332
+ switch (type) {
1333
+ case "userinfo":
1334
+ isChar = sitemap === true && isSitemapUserinfoChar(char, true) || isUserinfoChar(char, true);
1335
+ break;
1336
+ case "path":
1337
+ isChar = sitemap === true && isSitemapPathChar(char, true) || isPathChar(char, true);
1338
+ break;
1339
+ case "query":
1340
+ case "fragment":
1341
+ isChar = sitemap === true && isSitemapQueryOrFragmentChar(char, true) || isQueryOrFragmentChar(char, true);
1342
+ break;
1343
+ default: isChar = false;
1344
+ }
1345
+ uricomponent += !isChar ? encodeURIComponent(char) : char;
1346
+ }
1347
+ }
1348
+ return uricomponent;
1349
+ };
1350
+ /**
1351
+ * @func encodeURIString
1352
+ *
1353
+ * Encode an URI string according to RFC-3986 with basic checking.
1354
+ *
1355
+ * Checked:
1356
+ * - scheme is required;
1357
+ * - path is required, can be empty;
1358
+ * - port, if any, must be an integer in a specific range;
1359
+ * - host must be a valid ip or domain name;
1360
+ * - maximum size once encoded for URLs.
1361
+ *
1362
+ * Support:
1363
+ * - IDNs: returns URI with its Punycode host, if any;
1364
+ * - lower and upper case.
1365
+ *
1366
+ * Note:
1367
+ * - only userinfo, path, query and fragment can be percent encoded;
1368
+ * - native function encodeURI encodes string according to RFC-2396 which is outdated;
1369
+ * - native function encodeURI also encodes scheme and host that cannot have
1370
+ * percend-encoded characters;
1371
+ * - characters that should not be percent-encoded in RFC-3986 are [] to represent IPv6 host;
1372
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase.
1373
+ *
1374
+ * Based on:
1375
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1376
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1377
+ */
1378
+ const encodeURIString = function encodeURIString(uri, { web, sitemap, lowercase } = {}) {
1379
+ const uriToEncode = is(String, uri) && lowercase === true ? uri.toLowerCase() : uri;
1380
+ const webURL = web === true || sitemap === true;
1381
+ const { scheme, authority, userinfo, host, port, path, query, fragment, schemeLen } = checkURISyntax(uriToEncode);
1382
+ if (webURL) {
1383
+ if (scheme !== "http" && scheme !== "https") fail("URI_INVALID_SCHEME", `scheme must be http or https, got '${scheme}'`);
1384
+ } else checkSchemeChars(scheme, schemeLen);
1385
+ if (webURL && !is(String, authority)) fail("URI_MISSING_AUTHORITY", "authority is required");
1386
+ if (exists(host) && !isIP(host) && !isDomain(host)) fail("URI_INVALID_HOST", `host must be a valid ip or domain name, got '${host}'`);
1387
+ if (exists(port) && (!isPort(port) || int(port, {
1388
+ ge: 0,
1389
+ le: 65535
1390
+ }) === void 0)) fail("URI_INVALID_PORT", `port must be an integer between 0-${maxPortInteger}, got '${port}'`);
1391
+ const uriencoded = recomposeURI({
1392
+ scheme,
1393
+ host,
1394
+ port,
1395
+ userinfo: encodeURIComponentString(userinfo ?? "", {
1396
+ sitemap,
1397
+ type: "userinfo",
1398
+ lowercase: false
1399
+ }),
1400
+ path: encodeURIComponentString(path ?? "", {
1401
+ sitemap,
1402
+ type: "path",
1403
+ lowercase: false
1404
+ }),
1405
+ query: is(String, query) ? encodeURIComponentString(query, {
1406
+ sitemap,
1407
+ type: "query",
1408
+ lowercase: false
1409
+ }) : query,
1410
+ fragment: is(String, fragment) ? encodeURIComponentString(fragment, {
1411
+ sitemap,
1412
+ type: "fragment",
1413
+ lowercase: false
1414
+ }) : fragment
1415
+ });
1416
+ if (webURL && uriencoded.length >= 2048) fail("URI_MAX_LENGTH_URL", `max URL length of ${maxLengthURL} reached: ${uriencoded.length}`);
1417
+ return uriencoded;
1418
+ };
1419
+ /**
1420
+ * @func encodeWebURL
1421
+ *
1422
+ * Encode an URI string with basic checking based on RFC-3986 standard applied
1423
+ * to HTTP and HTTPS URLs.
1424
+ *
1425
+ * Uses a fixed encodeURI function to be RFC-3986 compliant.
1426
+ *
1427
+ * Checked:
1428
+ * - scheme must be http/HTTP or https/HTTPS;
1429
+ * - path is required, can be empty;
1430
+ * - authority is required;
1431
+ * - port, if any, must be an integer in a specific range;
1432
+ * - host must be a valid IP or domain name;
1433
+ * - maximum size once encoded.
1434
+ *
1435
+ * Support:
1436
+ * - IDNs: returns URL with its Punycode host, if any;
1437
+ * - lower and upper case.
1438
+ *
1439
+ * Note:
1440
+ * - only userinfo, path, query and fragment can be percent encoded;
1441
+ * - native function encodeURI encodes string according to RFC-2396 which is outdated;
1442
+ * - native function encodes also scheme and host that cannot have percend encoded characters;
1443
+ * - characters that should not be percent-encoded in RFC-3986 are [] to represent IPv6 host;
1444
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase.
1445
+ *
1446
+ * Based on:
1447
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986.
1448
+ */
1449
+ const encodeWebURL = function encodeWebURL(uri, { lowercase } = {}) {
1450
+ return encodeURIString(uri, {
1451
+ lowercase,
1452
+ web: true
1453
+ });
1454
+ };
1455
+ /**
1456
+ * @func encodeSitemapURL
1457
+ *
1458
+ * Encode an URI string with basic checking based on RFC-3986 standard applied
1459
+ * to HTTP and HTTPS URLs and sitemap requirements regarding special entities to escape.
1460
+ *
1461
+ * Checked:
1462
+ * - scheme must be http/HTTP or https/HTTPS;
1463
+ * - path is required, can be empty;
1464
+ * - authority is required;
1465
+ * - port, if any, must be an integer in a specific range;
1466
+ * - host must be a valid IP or domain name;
1467
+ * - maximum size once encoded.
1468
+ *
1469
+ * Support:
1470
+ * - Sitemap's special characters;
1471
+ * - IDNs: returns URI with its Punycode host, if any;
1472
+ * - lower case only.
1473
+ *
1474
+ * Note:
1475
+ * - only userinfo, path, query and fragment can be percent encoded;
1476
+ * - native function encodeURI encodes string according to RFC-2396 which is outdated;
1477
+ * - native function encodes also scheme and host that cannot have percend encoded characters;
1478
+ * - characters that should not be percent-encoded in RFC-3986 are [] to represent IPv6 host;
1479
+ * - to stay fully RFC-3986 compliant, scheme and host are put in lowercase.
1480
+ *
1481
+ * Based on:
1482
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986;
1483
+ * - https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190.
1484
+ */
1485
+ const encodeSitemapURL = function encodeSitemapURL(uri) {
1486
+ return encodeURIString(uri, {
1487
+ lowercase: true,
1488
+ sitemap: true
1489
+ });
1490
+ };
1491
+ //#endregion
1492
+ //#region src/resolver/index.ts
1493
+ /**
1494
+ * reference resolution
1495
+ *
1496
+ * - removeDotSegments(path) -> String
1497
+ * - resolveURI(base, reference) -> String
1498
+ *
1499
+ * Based on:
1500
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-5.
1501
+ */
1502
+ const referenceRegexp = /^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/;
1503
+ /**
1504
+ * @func parseReference
1505
+ *
1506
+ * Split a URI-reference into its five RFC-3986 components. A component is
1507
+ * null when the delimiter is absent and '' when present but empty, so the
1508
+ * defined/undefined distinction §5.2.2 relies on is preserved.
1509
+ */
1510
+ const parseReference = function parseReference(reference) {
1511
+ /* v8 ignore next -- unreachable []: the Appendix-B regexp is all-optional and matches any string */
1512
+ const [, scheme, authority, path, query, fragment] = reference.match(referenceRegexp) ?? [];
1513
+ return {
1514
+ scheme: scheme ?? null,
1515
+ authority: authority ?? null,
1516
+ /* v8 ignore next -- unreachable '': the path group [^?#]* always captures a string */
1517
+ path: path ?? "",
1518
+ query: query ?? null,
1519
+ fragment: fragment ?? null
1520
+ };
1521
+ };
1522
+ /**
1523
+ * @func removeDotSegments
1524
+ *
1525
+ * Remove the special "." and ".." complete path segments from a path,
1526
+ * implementing the RFC-3986 §5.2.4 ordered loop verbatim.
1527
+ *
1528
+ * Based on:
1529
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-5.2.4.
1530
+ */
1531
+ const removeDotSegments = function removeDotSegments(path) {
1532
+ if (!is(String, path)) return "";
1533
+ let input = path;
1534
+ let output = "";
1535
+ while (input.length > 0) if (input.startsWith("../")) input = input.slice(3);
1536
+ else if (input.startsWith("./")) input = input.slice(2);
1537
+ else if (input.startsWith("/./")) input = `/${input.slice(3)}`;
1538
+ else if (input === "/.") input = "/";
1539
+ else if (input.startsWith("/../")) {
1540
+ input = `/${input.slice(4)}`;
1541
+ output = output.slice(0, Math.max(0, output.lastIndexOf("/")));
1542
+ } else if (input === "/..") {
1543
+ input = "/";
1544
+ output = output.slice(0, Math.max(0, output.lastIndexOf("/")));
1545
+ } else if (input === "." || input === "..") input = "";
1546
+ else {
1547
+ const start = input.startsWith("/") ? 1 : 0;
1548
+ const next = input.indexOf("/", start);
1549
+ if (next === -1) {
1550
+ output += input;
1551
+ input = "";
1552
+ } else {
1553
+ output += input.slice(0, next);
1554
+ input = input.slice(next);
1555
+ }
1556
+ }
1557
+ return output;
1558
+ };
1559
+ /**
1560
+ * @func merge
1561
+ *
1562
+ * Merge a relative reference's path with the base path, per RFC-3986 §5.2.3.
1563
+ */
1564
+ const merge = function merge(base, refPath) {
1565
+ if (is(String, base.authority) && base.path === "") return `/${refPath}`;
1566
+ const lastSlash = base.path.lastIndexOf("/");
1567
+ return lastSlash === -1 ? refPath : base.path.slice(0, lastSlash + 1) + refPath;
1568
+ };
1569
+ /**
1570
+ * @func recompose
1571
+ *
1572
+ * Recompose a resolved target from its components, per RFC-3986 §5.3.
1573
+ * A component is emitted whenever it is defined (non-null), including ''.
1574
+ */
1575
+ const recompose = function recompose(target) {
1576
+ let result = "";
1577
+ /* v8 ignore next -- unreachable false branch: a resolved target always has a scheme (the base is absolute) */
1578
+ if (is(String, target.scheme)) result += `${target.scheme}:`;
1579
+ if (is(String, target.authority)) result += `//${target.authority}`;
1580
+ result += target.path;
1581
+ if (is(String, target.query)) result += `?${target.query}`;
1582
+ if (is(String, target.fragment)) result += `#${target.fragment}`;
1583
+ return result;
1584
+ };
1585
+ /**
1586
+ * @func resolveURI
1587
+ *
1588
+ * Resolve a URI reference against a base URI, implementing the RFC-3986
1589
+ * §5.2.2 strict transform (with §5.2.3 merge and §5.2.4 remove_dot_segments)
1590
+ * and recomposing per §5.3.
1591
+ *
1592
+ * The base must be an absolute URI (a scheme is required, RFC-3986 §5.2.1);
1593
+ * a fragment on the base is ignored (RFC-3986 §5.1: the base is used
1594
+ * stripped of any fragment); the empty string is returned if base or
1595
+ * reference is invalid.
1596
+ *
1597
+ * Based on:
1598
+ * - RFC-3986 https://tools.ietf.org/html/rfc3986#section-5.2.
1599
+ */
1600
+ const resolveURI = function resolveURI(base, reference) {
1601
+ if (!(is(String, base) && is(String, reference))) return "";
1602
+ const baseRef = parseReference(base);
1603
+ if (!is(String, baseRef.scheme)) return "";
1604
+ const r = parseReference(reference);
1605
+ const t = {
1606
+ scheme: null,
1607
+ authority: null,
1608
+ path: "",
1609
+ query: null,
1610
+ fragment: null
1611
+ };
1612
+ if (is(String, r.scheme)) {
1613
+ t.scheme = r.scheme;
1614
+ t.authority = r.authority;
1615
+ t.path = removeDotSegments(r.path);
1616
+ t.query = r.query;
1617
+ } else {
1618
+ if (is(String, r.authority)) {
1619
+ t.authority = r.authority;
1620
+ t.path = removeDotSegments(r.path);
1621
+ t.query = r.query;
1622
+ } else {
1623
+ if (r.path === "") {
1624
+ t.path = baseRef.path;
1625
+ t.query = is(String, r.query) ? r.query : baseRef.query;
1626
+ } else {
1627
+ t.path = r.path.startsWith("/") ? removeDotSegments(r.path) : removeDotSegments(merge(baseRef, r.path));
1628
+ t.query = r.query;
1629
+ }
1630
+ t.authority = baseRef.authority;
1631
+ }
1632
+ t.scheme = baseRef.scheme;
1633
+ }
1634
+ t.fragment = r.fragment;
1635
+ return recompose(t);
1636
+ };
1637
+ //#endregion
1638
+ exports.checkHttpSitemapURL = checkHttpSitemapURL;
1639
+ exports.checkHttpURL = checkHttpURL;
1640
+ exports.checkHttpsSitemapURL = checkHttpsSitemapURL;
1641
+ exports.checkHttpsURL = checkHttpsURL;
1642
+ exports.checkSitemapURL = checkSitemapURL;
1643
+ exports.checkURI = checkURI;
1644
+ exports.checkWebURL = checkWebURL;
1645
+ exports.decodeSitemapURL = decodeSitemapURL;
1646
+ exports.decodeURIComponentString = decodeURIComponentString;
1647
+ exports.decodeURIString = decodeURIString;
1648
+ exports.decodeWebURL = decodeWebURL;
1649
+ exports.encodeSitemapURL = encodeSitemapURL;
1650
+ exports.encodeURIComponentString = encodeURIComponentString;
1651
+ exports.encodeURIString = encodeURIString;
1652
+ exports.encodeWebURL = encodeWebURL;
1653
+ exports.isDomain = isDomain;
1654
+ exports.isDomainLabel = isDomainLabel;
1655
+ exports.isIP = isIP;
1656
+ exports.isIPv4 = isIPv4;
1657
+ exports.isIPv6 = isIPv6;
1658
+ exports.parseURI = parseURI;
1659
+ exports.punycode = punycode;
1660
+ exports.punydecode = punydecode;
1661
+ exports.recomposeURI = recomposeURI;
1662
+ exports.removeDotSegments = removeDotSegments;
1663
+ exports.resolveURI = resolveURI;
1664
+
1665
+ //# sourceMappingURL=index.cjs.map