trousse 2.6.0 → 2.7.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/coercions.d.ts +1 -1
- package/dist/coercions.js +3 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/is.js +1 -1
- package/dist/locales.js +6 -2
- package/dist/matching.js +36 -14
- package/dist/objects.js +14 -2
- package/dist/timers.js +2 -1
- package/dist/types.d.ts +6 -1
- package/dist/urls.d.ts +14 -6
- package/dist/urls.js +136 -60
- package/package.json +2 -2
package/dist/coercions.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export declare const coerceString: (value: unknown) => string | undefined;
|
|
|
4
4
|
export declare const coerceNumber: (value: unknown) => number | undefined;
|
|
5
5
|
export declare const coerceBoolean: (value: unknown) => boolean | undefined;
|
|
6
6
|
export declare const coerceDate: (value: unknown) => Date | undefined;
|
|
7
|
-
export declare const coerceSingular: <T>(value: T | Array<T>) => T;
|
|
7
|
+
export declare const coerceSingular: <T>(value: T | Array<T>) => T | undefined;
|
|
8
8
|
export declare const coerceArray: <T>(value: Nullish<T | Array<T>>) => Array<T>;
|
|
9
9
|
//#endregion
|
package/dist/coercions.js
CHANGED
|
@@ -6,7 +6,7 @@ const coerceString = (value) => {
|
|
|
6
6
|
};
|
|
7
7
|
const coerceNumber = (value) => {
|
|
8
8
|
if (isNumber(value)) return Number.isNaN(value) ? void 0 : value;
|
|
9
|
-
if (
|
|
9
|
+
if (isString(value) && value.trim()) {
|
|
10
10
|
const numeric = +value;
|
|
11
11
|
return Number.isNaN(numeric) ? void 0 : numeric;
|
|
12
12
|
}
|
|
@@ -20,8 +20,10 @@ const coerceBoolean = (value) => {
|
|
|
20
20
|
if (falseRegex.test(value)) return false;
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
const nonYearDigitsRegex = /^\s*(\d{1,3}|\d{5,})\s*$/;
|
|
23
24
|
const coerceDate = (value) => {
|
|
24
25
|
if (isValidDate(value)) return value;
|
|
26
|
+
if (isString(value) && nonYearDigitsRegex.test(value)) return;
|
|
25
27
|
if (isNumber(value) || isNonEmptyString(value)) {
|
|
26
28
|
const date = new Date(value);
|
|
27
29
|
return Number.isNaN(date.getTime()) ? void 0 : date;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { omitEmpty } from "./arrays.js";
|
|
2
|
-
import { AnyOf, DeepOmit, IsPlainObject, MaybePromise, NormalizeOptions, Nullish, PartialNullish, Pattern } from "./types.js";
|
|
2
|
+
import { AnyOf, DeepOmit, IsPlainObject, MaybePromise, NormalizeOptions, Nullish, PartialNullish, Pattern, RegistrableDomainOptions } from "./types.js";
|
|
3
3
|
import { coerceArray, coerceBoolean, coerceDate, coerceNumber, coerceSingular, coerceString } from "./coercions.js";
|
|
4
4
|
import { archiveExtensions, audioExtensions, codeExtensions, documentExtensions, flashExtensions, fontExtensions, imageExtensions, installerExtensions, subtitleExtensions, videoExtensions } from "./files.js";
|
|
5
5
|
import { isBoolean, isFunction, isNonEmptyString, isNullish, isNumber, isObject, isPlainObject, isPresent, isString, isValidDate } from "./is.js";
|
|
@@ -8,5 +8,5 @@ import { escapeHtml, t, tx } from "./locales.js";
|
|
|
8
8
|
import { anyWordMatchesAnyOf, endsWithAnyOf, escapeRegex, getAnyOf, includesAnyOf, isAnyOf, startsWithAnyOf } from "./matching.js";
|
|
9
9
|
import { omit, pick, toMap, trimObject } from "./objects.js";
|
|
10
10
|
import { sleep } from "./timers.js";
|
|
11
|
-
import { addMissingProtocol, decodeSegment, fixMalformedProtocol, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveUrl, stripWww, upgradeProtocol } from "./urls.js";
|
|
12
|
-
export { type AnyOf, type DeepOmit, type IsPlainObject, type MaybePromise, type NormalizeOptions, type Nullish, type PartialNullish, type Pattern, addMissingProtocol, anyWordMatchesAnyOf, archiveExtensions, audioExtensions, codeExtensions, coerceArray, coerceBoolean, coerceDate, coerceNumber, coerceSingular, coerceString, decodeSegment, documentExtensions, endsWithAnyOf, escapeHtml, escapeRegex, fixMalformedProtocol, flashExtensions, fontExtensions, getAnyOf, getPathSegments, getRegistrableDomain, getSubdomain, imageExtensions, includesAnyOf, installerExtensions, isAnyOf, isBoolean, isFunction, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isJsonLike, isNonEmptyString, isNullish, isNumber, isObject, isParseableJson, isPlainObject, isPresent, isString, isSubdomainOf, isValidDate, normalizeUrl, omit, omitEmpty, parseUrl, pick, resolveFeedProtocol, resolveUrl, sleep, startsWithAnyOf, stripWww, subtitleExtensions, t, toMap, trimObject, tx, upgradeProtocol, videoExtensions };
|
|
11
|
+
import { addMissingProtocol, addMissingScheme, decodeSegment, fixMalformedProtocol, fixMalformedScheme, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveFeedScheme, resolveUrl, stripWww, upgradeProtocol, upgradeScheme } from "./urls.js";
|
|
12
|
+
export { type AnyOf, type DeepOmit, type IsPlainObject, type MaybePromise, type NormalizeOptions, type Nullish, type PartialNullish, type Pattern, type RegistrableDomainOptions, addMissingProtocol, addMissingScheme, anyWordMatchesAnyOf, archiveExtensions, audioExtensions, codeExtensions, coerceArray, coerceBoolean, coerceDate, coerceNumber, coerceSingular, coerceString, decodeSegment, documentExtensions, endsWithAnyOf, escapeHtml, escapeRegex, fixMalformedProtocol, fixMalformedScheme, flashExtensions, fontExtensions, getAnyOf, getPathSegments, getRegistrableDomain, getSubdomain, imageExtensions, includesAnyOf, installerExtensions, isAnyOf, isBoolean, isFunction, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isJsonLike, isNonEmptyString, isNullish, isNumber, isObject, isParseableJson, isPlainObject, isPresent, isString, isSubdomainOf, isValidDate, normalizeUrl, omit, omitEmpty, parseUrl, pick, resolveFeedProtocol, resolveFeedScheme, resolveUrl, sleep, startsWithAnyOf, stripWww, subtitleExtensions, t, toMap, trimObject, tx, upgradeProtocol, upgradeScheme, videoExtensions };
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,5 @@ import { escapeHtml, t, tx } from "./locales.js";
|
|
|
7
7
|
import { anyWordMatchesAnyOf, endsWithAnyOf, escapeRegex, getAnyOf, includesAnyOf, isAnyOf, startsWithAnyOf } from "./matching.js";
|
|
8
8
|
import { omit, pick, toMap, trimObject } from "./objects.js";
|
|
9
9
|
import { sleep } from "./timers.js";
|
|
10
|
-
import { addMissingProtocol, decodeSegment, fixMalformedProtocol, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveUrl, stripWww, upgradeProtocol } from "./urls.js";
|
|
11
|
-
export { addMissingProtocol, anyWordMatchesAnyOf, archiveExtensions, audioExtensions, codeExtensions, coerceArray, coerceBoolean, coerceDate, coerceNumber, coerceSingular, coerceString, decodeSegment, documentExtensions, endsWithAnyOf, escapeHtml, escapeRegex, fixMalformedProtocol, flashExtensions, fontExtensions, getAnyOf, getPathSegments, getRegistrableDomain, getSubdomain, imageExtensions, includesAnyOf, installerExtensions, isAnyOf, isBoolean, isFunction, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isJsonLike, isNonEmptyString, isNullish, isNumber, isObject, isParseableJson, isPlainObject, isPresent, isString, isSubdomainOf, isValidDate, normalizeUrl, omit, omitEmpty, parseUrl, pick, resolveFeedProtocol, resolveUrl, sleep, startsWithAnyOf, stripWww, subtitleExtensions, t, toMap, trimObject, tx, upgradeProtocol, videoExtensions };
|
|
10
|
+
import { addMissingProtocol, addMissingScheme, decodeSegment, fixMalformedProtocol, fixMalformedScheme, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveFeedScheme, resolveUrl, stripWww, upgradeProtocol, upgradeScheme } from "./urls.js";
|
|
11
|
+
export { addMissingProtocol, addMissingScheme, anyWordMatchesAnyOf, archiveExtensions, audioExtensions, codeExtensions, coerceArray, coerceBoolean, coerceDate, coerceNumber, coerceSingular, coerceString, decodeSegment, documentExtensions, endsWithAnyOf, escapeHtml, escapeRegex, fixMalformedProtocol, fixMalformedScheme, flashExtensions, fontExtensions, getAnyOf, getPathSegments, getRegistrableDomain, getSubdomain, imageExtensions, includesAnyOf, installerExtensions, isAnyOf, isBoolean, isFunction, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isJsonLike, isNonEmptyString, isNullish, isNumber, isObject, isParseableJson, isPlainObject, isPresent, isString, isSubdomainOf, isValidDate, normalizeUrl, omit, omitEmpty, parseUrl, pick, resolveFeedProtocol, resolveFeedScheme, resolveUrl, sleep, startsWithAnyOf, stripWww, subtitleExtensions, t, toMap, trimObject, tx, upgradeProtocol, upgradeScheme, videoExtensions };
|
package/dist/is.js
CHANGED
|
@@ -21,7 +21,7 @@ const isObject = (value) => {
|
|
|
21
21
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
22
|
};
|
|
23
23
|
const isPlainObject = (value) => {
|
|
24
|
-
return isObject(value) && value
|
|
24
|
+
return isObject(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
25
25
|
};
|
|
26
26
|
const whitespaceOnlyRegex = /^\p{White_Space}*$/u;
|
|
27
27
|
const isNonEmptyString = (value) => {
|
package/dist/locales.js
CHANGED
|
@@ -13,10 +13,14 @@ const escapeHtml = (value) => {
|
|
|
13
13
|
});
|
|
14
14
|
};
|
|
15
15
|
const t = (template, params) => {
|
|
16
|
-
return template.replace(placeholderRegex, (
|
|
16
|
+
return template.replace(placeholderRegex, (placeholder, key) => {
|
|
17
|
+
return Object.hasOwn(params, key) ? String(params[key]) : placeholder;
|
|
18
|
+
});
|
|
17
19
|
};
|
|
18
20
|
const tx = (template, params) => {
|
|
19
|
-
return template.replace(placeholderRegex, (
|
|
21
|
+
return template.replace(placeholderRegex, (placeholder, key) => {
|
|
22
|
+
return Object.hasOwn(params, key) ? escapeHtml(String(params[key])) : placeholder;
|
|
23
|
+
});
|
|
20
24
|
};
|
|
21
25
|
//#endregion
|
|
22
26
|
export { escapeHtml, t, tx };
|
package/dist/matching.js
CHANGED
|
@@ -1,54 +1,76 @@
|
|
|
1
1
|
import { isNullish } from "./is.js";
|
|
2
2
|
//#region src/matching.ts
|
|
3
3
|
const whitespaceRegex = /\s+/;
|
|
4
|
-
const
|
|
4
|
+
const regexSyntaxCharsRegex = /[.*+?^${}()|[\]\\/]/;
|
|
5
|
+
const regexEscapableCharsRegex = /[.*+?^${}()|[\]\\/&!#%,:;<=>@`~-]/g;
|
|
6
|
+
const testRegex = (regex, value) => {
|
|
7
|
+
regex.lastIndex = 0;
|
|
8
|
+
return regex.test(value);
|
|
9
|
+
};
|
|
10
|
+
const testRegexAnyCase = (regex, lowerValue, value) => {
|
|
11
|
+
return testRegex(regex, lowerValue) || lowerValue !== value && testRegex(regex, value);
|
|
12
|
+
};
|
|
5
13
|
const escapeRegex = (value) => {
|
|
6
|
-
return value.replace(
|
|
14
|
+
return value.replace(regexEscapableCharsRegex, (char) => {
|
|
15
|
+
if (regexSyntaxCharsRegex.test(char)) return `\\${char}`;
|
|
16
|
+
return `\\x${char.charCodeAt(0).toString(16)}`;
|
|
17
|
+
});
|
|
7
18
|
};
|
|
8
19
|
const isAnyOf = (value, patterns, parser) => {
|
|
9
20
|
if (isNullish(value)) return false;
|
|
10
21
|
const parsedValue = parser ? parser(value) : value.toLowerCase().trim();
|
|
22
|
+
const originalValue = parser ? parsedValue : value.trim();
|
|
11
23
|
return (typeof patterns === "string" || patterns instanceof RegExp ? [patterns] : patterns).some((pattern) => {
|
|
12
|
-
if (pattern instanceof RegExp) return pattern
|
|
13
|
-
return parsedValue === pattern.toLowerCase().trim();
|
|
24
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, parsedValue, originalValue);
|
|
25
|
+
return parsedValue === (parser ? pattern : pattern.toLowerCase().trim());
|
|
14
26
|
});
|
|
15
27
|
};
|
|
16
28
|
const getAnyOf = (value, patterns, parser) => {
|
|
17
29
|
if (isNullish(value)) return;
|
|
18
30
|
const parsedValue = parser ? parser(value) : value.toLowerCase().trim();
|
|
19
|
-
return patterns.find((pattern) =>
|
|
31
|
+
return patterns.find((pattern) => {
|
|
32
|
+
return parsedValue === (parser ? pattern : pattern.toLowerCase().trim());
|
|
33
|
+
});
|
|
20
34
|
};
|
|
21
35
|
const includesAnyOf = (value, patterns, parser) => {
|
|
22
36
|
if (isNullish(value)) return false;
|
|
23
37
|
const parsedValue = parser ? parser(value) : value.toLowerCase();
|
|
38
|
+
const originalValue = parser ? parsedValue : value;
|
|
24
39
|
return patterns.some((pattern) => {
|
|
25
|
-
if (pattern instanceof RegExp) return pattern
|
|
26
|
-
return pattern && parsedValue.includes(pattern.toLowerCase());
|
|
40
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, parsedValue, originalValue);
|
|
41
|
+
return pattern && parsedValue.includes(parser ? pattern : pattern.toLowerCase());
|
|
27
42
|
});
|
|
28
43
|
};
|
|
29
44
|
const startsWithAnyOf = (value, patterns) => {
|
|
30
45
|
const lowerValue = value.toLowerCase();
|
|
31
46
|
return patterns.some((pattern) => {
|
|
32
|
-
if (pattern instanceof RegExp) return pattern
|
|
47
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, lowerValue, value);
|
|
33
48
|
return pattern && lowerValue.startsWith(pattern.toLowerCase());
|
|
34
49
|
});
|
|
35
50
|
};
|
|
36
51
|
const endsWithAnyOf = (value, patterns) => {
|
|
37
52
|
const lowerValue = value.toLowerCase();
|
|
38
53
|
return patterns.some((pattern) => {
|
|
39
|
-
if (pattern instanceof RegExp) return pattern
|
|
54
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, lowerValue, value);
|
|
40
55
|
return pattern && lowerValue.endsWith(pattern.toLowerCase());
|
|
41
56
|
});
|
|
42
57
|
};
|
|
43
58
|
const anyWordMatchesAnyOf = (value, patterns) => {
|
|
44
59
|
const stringPatterns = [];
|
|
45
60
|
const regexPatterns = [];
|
|
46
|
-
for (const pattern of patterns)
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
if (pattern instanceof RegExp) {
|
|
63
|
+
regexPatterns.push(pattern);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const stringPattern = pattern.toLowerCase().trim();
|
|
67
|
+
if (stringPattern) stringPatterns.push(stringPattern);
|
|
68
|
+
}
|
|
69
|
+
const words = value.split(whitespaceRegex);
|
|
49
70
|
for (const word of words) {
|
|
50
|
-
|
|
51
|
-
for (const
|
|
71
|
+
const lowerWord = word.toLowerCase();
|
|
72
|
+
for (const stringPattern of stringPatterns) if (lowerWord === stringPattern) return true;
|
|
73
|
+
for (const regexPattern of regexPatterns) if (testRegexAnyCase(regexPattern, lowerWord, word)) return true;
|
|
52
74
|
}
|
|
53
75
|
return false;
|
|
54
76
|
};
|
package/dist/objects.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { isPresent } from "./is.js";
|
|
2
2
|
//#region src/objects.ts
|
|
3
|
+
const setKey = (object, key, value) => {
|
|
4
|
+
if (key === "__proto__") {
|
|
5
|
+
Object.defineProperty(object, key, {
|
|
6
|
+
value,
|
|
7
|
+
enumerable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
configurable: true
|
|
10
|
+
});
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
object[key] = value;
|
|
14
|
+
};
|
|
3
15
|
const pick = (object, keys) => {
|
|
4
16
|
const result = {};
|
|
5
|
-
for (const key of keys) if (key in object) result
|
|
17
|
+
for (const key of keys) if (key in object) setKey(result, key, object[key]);
|
|
6
18
|
return result;
|
|
7
19
|
};
|
|
8
20
|
const omit = (object, keys) => {
|
|
@@ -23,7 +35,7 @@ const trimObject = (object, keep = isPresent) => {
|
|
|
23
35
|
const result = {};
|
|
24
36
|
for (const key in object) {
|
|
25
37
|
const value = object[key];
|
|
26
|
-
if (keep(value)) result
|
|
38
|
+
if (keep(value)) setKey(result, key, value);
|
|
27
39
|
}
|
|
28
40
|
return result;
|
|
29
41
|
};
|
package/dist/timers.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,12 @@ export type Nullish<T> = T | null | undefined;
|
|
|
6
6
|
export type PartialNullish<T> = { [Key in keyof T]?: Nullish<T[Key]>; };
|
|
7
7
|
export type MaybePromise<T> = T | Promise<T>;
|
|
8
8
|
export type Pattern = string | RegExp;
|
|
9
|
+
export type RegistrableDomainOptions = {
|
|
10
|
+
suffixes?: ReadonlyArray<string>;
|
|
11
|
+
};
|
|
9
12
|
export type NormalizeOptions = {
|
|
13
|
+
stripScheme?: boolean;
|
|
14
|
+
/** @deprecated Use `stripScheme` instead, which wins when both are set. */
|
|
10
15
|
stripProtocol?: boolean;
|
|
11
16
|
stripAuthentication?: boolean;
|
|
12
17
|
stripWww?: boolean;
|
|
@@ -15,7 +20,7 @@ export type NormalizeOptions = {
|
|
|
15
20
|
collapseSlashes?: boolean;
|
|
16
21
|
stripHash?: boolean;
|
|
17
22
|
sortQueryParams?: boolean;
|
|
18
|
-
stripQueryParams?:
|
|
23
|
+
stripQueryParams?: ReadonlyArray<string>;
|
|
19
24
|
stripQuery?: boolean;
|
|
20
25
|
stripEmptyQuery?: boolean;
|
|
21
26
|
lowercaseQuery?: boolean;
|
package/dist/urls.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NormalizeOptions } from "./types.js";
|
|
1
|
+
import { NormalizeOptions, RegistrableDomainOptions } from "./types.js";
|
|
2
2
|
//#region src/urls.d.ts
|
|
3
3
|
export declare const parseUrl: (value: string | URL, base?: string | URL) => URL | undefined;
|
|
4
4
|
export declare const getPathSegments: (value: string | URL) => Array<string>;
|
|
@@ -8,13 +8,21 @@ export declare const isHttpUrl: (url: string | URL) => boolean;
|
|
|
8
8
|
export declare const isHostOrSubdomainOf: (url: string | URL, domains: string | ReadonlyArray<string>) => boolean;
|
|
9
9
|
export declare const getSubdomain: (url: string | URL, domains: string | ReadonlyArray<string>) => string | undefined;
|
|
10
10
|
export declare const decodeSegment: (segment: string | undefined) => string | undefined;
|
|
11
|
-
export declare const
|
|
12
|
-
export declare const
|
|
13
|
-
export declare const
|
|
14
|
-
export declare const
|
|
11
|
+
export declare const fixMalformedScheme: (url: string) => string;
|
|
12
|
+
export declare const resolveFeedScheme: (url: string, scheme?: 'http' | 'https') => string;
|
|
13
|
+
export declare const addMissingScheme: (url: string, scheme?: 'http' | 'https') => string;
|
|
14
|
+
export declare const upgradeScheme: (url: string, scheme?: 'http' | 'https') => string;
|
|
15
15
|
export declare const resolveUrl: (url: string, base?: string) => string | undefined;
|
|
16
16
|
export declare const normalizeUrl: (url: string, options: NormalizeOptions) => string;
|
|
17
17
|
export declare const stripWww: (hostname: string) => string;
|
|
18
18
|
export declare const isIpAddress: (hostname: string) => boolean;
|
|
19
|
-
export declare const getRegistrableDomain: (url: string | URL) => string | undefined;
|
|
19
|
+
export declare const getRegistrableDomain: (url: string | URL, options?: RegistrableDomainOptions) => string | undefined;
|
|
20
|
+
/** @deprecated Use `fixMalformedScheme` instead. */
|
|
21
|
+
export declare const fixMalformedProtocol: typeof fixMalformedScheme;
|
|
22
|
+
/** @deprecated Use `resolveFeedScheme` instead. */
|
|
23
|
+
export declare const resolveFeedProtocol: typeof resolveFeedScheme;
|
|
24
|
+
/** @deprecated Use `addMissingScheme` instead. */
|
|
25
|
+
export declare const addMissingProtocol: typeof addMissingScheme;
|
|
26
|
+
/** @deprecated Use `upgradeScheme` instead. */
|
|
27
|
+
export declare const upgradeProtocol: typeof upgradeScheme;
|
|
20
28
|
//#endregion
|
package/dist/urls.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { endsWithAnyOf, isAnyOf } from "./matching.js";
|
|
2
2
|
import { decodeHTMLStrict } from "entities";
|
|
3
3
|
//#region src/urls.ts
|
|
4
|
-
const
|
|
4
|
+
const httpSchemes = ["http:", "https:"];
|
|
5
5
|
const parseUrl = (value, base) => {
|
|
6
6
|
if (value instanceof URL && base === void 0) return value;
|
|
7
|
+
if (URL.parse) return URL.parse(value, base) ?? void 0;
|
|
7
8
|
try {
|
|
8
9
|
return new URL(value, base);
|
|
9
10
|
} catch {}
|
|
@@ -11,30 +12,42 @@ const parseUrl = (value, base) => {
|
|
|
11
12
|
const getPathSegments = (value) => {
|
|
12
13
|
return parseUrl(value)?.pathname.split("/").filter(Boolean) ?? [];
|
|
13
14
|
};
|
|
14
|
-
const
|
|
15
|
+
const getHostname = (url) => {
|
|
15
16
|
const hostname = parseUrl(url)?.hostname;
|
|
17
|
+
if (!hostname) return;
|
|
18
|
+
return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
|
|
19
|
+
};
|
|
20
|
+
const nonAsciiRegex = /[\u0080-\uffff]/;
|
|
21
|
+
const normalizeDomain = (domain) => {
|
|
22
|
+
const trimmed = domain.trim();
|
|
23
|
+
const bare = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
24
|
+
if (!nonAsciiRegex.test(bare)) return bare;
|
|
25
|
+
return parseUrl(`http://${bare}`)?.hostname ?? bare;
|
|
26
|
+
};
|
|
27
|
+
const isHostOf = (url, hosts) => {
|
|
28
|
+
const hostname = getHostname(url);
|
|
16
29
|
if (!hostname) return false;
|
|
17
|
-
return isAnyOf(hostname, hosts);
|
|
30
|
+
return isAnyOf(hostname, (typeof hosts === "string" ? [hosts] : hosts).map(normalizeDomain));
|
|
18
31
|
};
|
|
19
32
|
const isSubdomainOf = (url, domains) => {
|
|
20
|
-
const hostname =
|
|
33
|
+
const hostname = getHostname(url);
|
|
21
34
|
if (!hostname) return false;
|
|
22
|
-
return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${domain}`));
|
|
35
|
+
return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${normalizeDomain(domain)}`));
|
|
23
36
|
};
|
|
24
37
|
const isHttpUrl = (url) => {
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
return
|
|
38
|
+
const scheme = parseUrl(url)?.protocol;
|
|
39
|
+
if (!scheme) return false;
|
|
40
|
+
return httpSchemes.includes(scheme);
|
|
28
41
|
};
|
|
29
42
|
const isHostOrSubdomainOf = (url, domains) => {
|
|
30
43
|
return isHostOf(url, domains) || isSubdomainOf(url, domains);
|
|
31
44
|
};
|
|
32
45
|
const getSubdomain = (url, domains) => {
|
|
33
|
-
const hostname =
|
|
46
|
+
const hostname = getHostname(url)?.toLowerCase();
|
|
34
47
|
if (!hostname) return;
|
|
35
48
|
const list = typeof domains === "string" ? [domains] : domains;
|
|
36
49
|
for (const domain of list) {
|
|
37
|
-
const suffix = `.${domain.toLowerCase()}`;
|
|
50
|
+
const suffix = `.${normalizeDomain(domain).toLowerCase()}`;
|
|
38
51
|
if (hostname.endsWith(suffix) && hostname.length > suffix.length) return hostname.slice(0, -suffix.length);
|
|
39
52
|
}
|
|
40
53
|
};
|
|
@@ -57,41 +70,55 @@ const ipv4Regex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
|
57
70
|
const ipv6Regex = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i;
|
|
58
71
|
const wwwPrefixRegex = /^www\./;
|
|
59
72
|
const ipv6BracketsRegex = /^\[|\]$/g;
|
|
60
|
-
const
|
|
73
|
+
const unreservedCharsRegex = /[a-zA-Z0-9._~-]/;
|
|
61
74
|
const httpsLetterRegex = /s/i;
|
|
62
|
-
const
|
|
75
|
+
const schemePrefixRegex = /^https?:\/\//;
|
|
63
76
|
const percentEscapeOrLettersRegex = /%[0-9A-Fa-f]{2}|[A-Z]+/g;
|
|
64
77
|
const plusRegex = /\+/g;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
78
|
+
const httpSchemeRegex = /^http:\/\//i;
|
|
79
|
+
const httpsSchemeRegex = /^https:\/\//i;
|
|
67
80
|
const leadingWhitespaceChars = [
|
|
68
81
|
" ",
|
|
69
82
|
" ",
|
|
70
83
|
"\n"
|
|
71
84
|
];
|
|
85
|
+
const relativeLeadChars = [
|
|
86
|
+
"/",
|
|
87
|
+
".",
|
|
88
|
+
"?",
|
|
89
|
+
"#"
|
|
90
|
+
];
|
|
91
|
+
const localhostRegex = /^localhost(?:[:/?#]|$)/i;
|
|
92
|
+
const schemeColonRegex = /^[^/]*:/;
|
|
93
|
+
const missingColonSchemeRegex = /^https?[\s=.\\]*\/\//i;
|
|
72
94
|
const validUrlRegex = /^https?:\/\/(?:www\.|[a-vx-z0-9])/i;
|
|
73
95
|
const schemeSeparator = String.raw`\.*[:\s=\\/][:\s=.\\/]*`;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
96
|
+
const leadingSchemeSeparator = String.raw`\.*(?::[:\s=.\\/]*|[\s=\\/][:\s=.\\/]+)`;
|
|
97
|
+
const doubledSchemeRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?[htps]{2,7}${leadingSchemeSeparator}([htps]{2,7})(?!:\d)(${schemeSeparator})[.,:/]*(www[./]+)?`, "i");
|
|
98
|
+
const singleMalformedRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?(h[htps():]{1,10}|t{1,2}ps?)(${leadingSchemeSeparator})[.,:/]*(www[./]+)?`, "i");
|
|
99
|
+
const isSchemeToken = (token, separator) => {
|
|
100
|
+
const lowered = token.toLowerCase();
|
|
101
|
+
return `${token}${separator}`.includes(":") || lowered.includes("t") && lowered.includes("p");
|
|
102
|
+
};
|
|
103
|
+
const fixMalformedScheme = (url) => {
|
|
104
|
+
if (validUrlRegex.test(url) && !doubledSchemeRegex.test(url)) return url;
|
|
105
|
+
const doubledMatch = doubledSchemeRegex.exec(url);
|
|
106
|
+
if (doubledMatch && isSchemeToken(doubledMatch[1], doubledMatch[2])) {
|
|
80
107
|
const inner = doubledMatch[1];
|
|
81
|
-
const www = doubledMatch[
|
|
108
|
+
const www = doubledMatch[3];
|
|
82
109
|
const rest = url.slice(doubledMatch[0].length);
|
|
83
|
-
return
|
|
110
|
+
return `${httpsLetterRegex.test(inner) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
|
|
84
111
|
}
|
|
85
112
|
const singleMatch = singleMalformedRegex.exec(url);
|
|
86
|
-
if (singleMatch) {
|
|
113
|
+
if (singleMatch && isSchemeToken(singleMatch[1], singleMatch[2])) {
|
|
87
114
|
const fullMatch = singleMatch[0];
|
|
88
|
-
const www = singleMatch[
|
|
115
|
+
const www = singleMatch[3];
|
|
89
116
|
const rest = url.slice(fullMatch.length);
|
|
90
|
-
return
|
|
117
|
+
return `${httpsLetterRegex.test(fullMatch) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
|
|
91
118
|
}
|
|
92
119
|
return url;
|
|
93
120
|
};
|
|
94
|
-
const
|
|
121
|
+
const feedSchemes = [
|
|
95
122
|
"feed:",
|
|
96
123
|
"rss:",
|
|
97
124
|
"podcast:",
|
|
@@ -104,57 +131,64 @@ const feedProtocols = [
|
|
|
104
131
|
"itms-podcast:",
|
|
105
132
|
"itms-podcasts:"
|
|
106
133
|
];
|
|
107
|
-
const
|
|
134
|
+
const resolveFeedScheme = (url, scheme = "https") => {
|
|
108
135
|
const firstCharCode = url.charCodeAt(0) | 32;
|
|
109
136
|
if (firstCharCode !== 102 && firstCharCode !== 114 && firstCharCode !== 112 && firstCharCode !== 105) return url;
|
|
110
137
|
const urlLower = url.toLowerCase();
|
|
111
|
-
for (const
|
|
112
|
-
if (!urlLower.startsWith(
|
|
113
|
-
if (urlLower.startsWith(`${
|
|
114
|
-
if (urlLower.startsWith(`${
|
|
138
|
+
for (const prefix of feedSchemes) {
|
|
139
|
+
if (!urlLower.startsWith(prefix)) continue;
|
|
140
|
+
if (urlLower.startsWith(`${prefix}http://`) || urlLower.startsWith(`${prefix}https://`)) return url.slice(prefix.length);
|
|
141
|
+
if (urlLower.startsWith(`${prefix}//`)) return `${scheme}:${url.slice(prefix.length)}`;
|
|
115
142
|
}
|
|
116
143
|
return url;
|
|
117
144
|
};
|
|
118
|
-
const
|
|
145
|
+
const addMissingScheme = (url, scheme = "https") => {
|
|
146
|
+
if (url.startsWith("[")) return parseUrl(`${scheme}://${url}`) ? `${scheme}://${url}` : url;
|
|
119
147
|
const colonIndex = url.indexOf(":");
|
|
120
148
|
if (colonIndex > 0) {
|
|
121
149
|
const beforeColon = url.slice(0, colonIndex);
|
|
122
|
-
if (!beforeColon.includes(".") && !beforeColon.includes("/") &&
|
|
150
|
+
if (!beforeColon.includes(".") && !beforeColon.includes("/") && !localhostRegex.test(url)) return url;
|
|
123
151
|
}
|
|
124
152
|
if (url.startsWith("//") && !url.startsWith("///")) {
|
|
125
|
-
const parsed = parseUrl(`${
|
|
153
|
+
const parsed = parseUrl(`${scheme}:${url}`);
|
|
126
154
|
if (!parsed) return url;
|
|
127
155
|
const hostname = parsed.hostname;
|
|
128
156
|
if (hostname.includes(".") || hostname === "localhost" || isIpAddress(hostname)) return parsed.href;
|
|
129
157
|
return url;
|
|
130
158
|
}
|
|
131
|
-
if (
|
|
159
|
+
if (relativeLeadChars.includes(url.charAt(0))) return url;
|
|
132
160
|
const slashIndex = url.indexOf("/");
|
|
133
161
|
const dotIndex = url.indexOf(".");
|
|
134
162
|
if (dotIndex === -1 || slashIndex !== -1 && dotIndex > slashIndex) {
|
|
135
|
-
if (!
|
|
163
|
+
if (!localhostRegex.test(url)) return url;
|
|
136
164
|
}
|
|
137
165
|
const firstChar = url.charAt(0);
|
|
138
166
|
if (leadingWhitespaceChars.includes(firstChar)) return url;
|
|
139
|
-
return `${
|
|
167
|
+
return `${scheme}://${url}`;
|
|
140
168
|
};
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
const upgradeScheme = (url, scheme = "https") => {
|
|
170
|
+
const sourceRegex = scheme === "https" ? httpSchemeRegex : httpsSchemeRegex;
|
|
171
|
+
if (!sourceRegex.test(url)) return url;
|
|
172
|
+
const parsed = parseUrl(url);
|
|
173
|
+
if (!parsed) return url.replace(sourceRegex, `${scheme}://`);
|
|
174
|
+
parsed.protocol = scheme;
|
|
175
|
+
return parsed.href;
|
|
144
176
|
};
|
|
145
177
|
const resolveUrl = (url, base) => {
|
|
146
|
-
|
|
178
|
+
const trimmedUrl = url.trim();
|
|
179
|
+
if (!trimmedUrl) return;
|
|
180
|
+
if (trimmedUrl.startsWith("#") && !base) return;
|
|
147
181
|
let resolvedUrl;
|
|
148
|
-
resolvedUrl =
|
|
149
|
-
resolvedUrl =
|
|
150
|
-
resolvedUrl =
|
|
182
|
+
resolvedUrl = trimmedUrl.includes("&") ? decodeHTMLStrict(trimmedUrl) : trimmedUrl;
|
|
183
|
+
resolvedUrl = resolveFeedScheme(resolvedUrl);
|
|
184
|
+
if (!base || schemeColonRegex.test(resolvedUrl) || missingColonSchemeRegex.test(resolvedUrl)) resolvedUrl = fixMalformedScheme(resolvedUrl);
|
|
151
185
|
if (base) {
|
|
152
|
-
const resolved = parseUrl(resolvedUrl, base);
|
|
186
|
+
const resolved = parseUrl(resolvedUrl, base) ?? parseUrl(resolvedUrl);
|
|
153
187
|
if (!resolved) return;
|
|
154
188
|
if (isHttpUrl(resolved)) return resolved.href;
|
|
155
189
|
resolvedUrl = resolved.href;
|
|
156
190
|
}
|
|
157
|
-
resolvedUrl =
|
|
191
|
+
resolvedUrl = addMissingScheme(resolvedUrl);
|
|
158
192
|
const parsed = parseUrl(resolvedUrl);
|
|
159
193
|
if (!parsed || !isHttpUrl(parsed)) return;
|
|
160
194
|
return parsed.href;
|
|
@@ -164,7 +198,7 @@ const decodeAndNormalizeEncoding = (value) => {
|
|
|
164
198
|
return value.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex) => {
|
|
165
199
|
const charCode = Number.parseInt(hex, 16);
|
|
166
200
|
const char = String.fromCharCode(charCode);
|
|
167
|
-
if (
|
|
201
|
+
if (unreservedCharsRegex.test(char)) return char;
|
|
168
202
|
return `%${hex.toUpperCase()}`;
|
|
169
203
|
});
|
|
170
204
|
};
|
|
@@ -190,11 +224,7 @@ const lowercaseQueryPair = (pair) => {
|
|
|
190
224
|
};
|
|
191
225
|
const normalizeUrl = (url, options) => {
|
|
192
226
|
try {
|
|
193
|
-
const parsed = new URL(url);
|
|
194
|
-
if (options.normalizeUnicode) {
|
|
195
|
-
parsed.hostname = parsed.hostname.normalize("NFC");
|
|
196
|
-
parsed.pathname = parsed.pathname.normalize("NFC");
|
|
197
|
-
}
|
|
227
|
+
const parsed = new URL(options.normalizeUnicode ? url.normalize("NFC") : url);
|
|
198
228
|
if (options.stripAuthentication) {
|
|
199
229
|
parsed.username = "";
|
|
200
230
|
parsed.password = "";
|
|
@@ -205,30 +235,32 @@ const normalizeUrl = (url, options) => {
|
|
|
205
235
|
if (options.normalizeEncoding) pathname = decodeAndNormalizeEncoding(pathname);
|
|
206
236
|
if (options.collapseSlashes) pathname = pathname.replace(/\/+/g, "/");
|
|
207
237
|
if (options.stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
208
|
-
if (options.stripRootSlash && pathname === "/") pathname = "";
|
|
209
238
|
parsed.pathname = pathname;
|
|
239
|
+
if (options.normalizeEncoding && parsed.search.includes("%")) parsed.search = decodeAndNormalizeEncoding(parsed.search);
|
|
240
|
+
if (options.normalizeEncoding && parsed.hash.includes("%")) parsed.hash = decodeAndNormalizeEncoding(parsed.hash);
|
|
210
241
|
if (options.stripQuery) parsed.search = "";
|
|
211
242
|
if (parsed.search && (options.stripQueryParams || options.lowercaseQuery)) {
|
|
212
243
|
let pairs = parsed.search.slice(1).split("&");
|
|
213
244
|
if (options.stripQueryParams) {
|
|
214
245
|
const strippedSet = getStrippedParamsSet(options.stripQueryParams);
|
|
215
|
-
pairs = pairs.filter((pair) => !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
|
|
246
|
+
pairs = pairs.filter((pair) => pair && !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
|
|
216
247
|
}
|
|
217
248
|
if (options.lowercaseQuery) pairs = pairs.map(lowercaseQueryPair);
|
|
218
249
|
parsed.search = pairs.join("&");
|
|
219
250
|
}
|
|
220
251
|
if (options.sortQueryParams && parsed.search.includes("&")) parsed.search = parsed.search.slice(1).split("&").filter(Boolean).sort(compareQueryPairs).join("&");
|
|
221
|
-
if (options.stripEmptyQuery && parsed.
|
|
252
|
+
if (options.stripEmptyQuery && parsed.search === "") parsed.search = "";
|
|
222
253
|
let result = parsed.href;
|
|
223
254
|
if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
|
|
224
|
-
if (options.stripProtocol) result = result.replace(
|
|
255
|
+
if (options.stripScheme ?? options.stripProtocol) result = result.replace(schemePrefixRegex, "");
|
|
225
256
|
return result;
|
|
226
257
|
} catch {
|
|
227
258
|
return url;
|
|
228
259
|
}
|
|
229
260
|
};
|
|
230
261
|
const stripWww = (hostname) => {
|
|
231
|
-
|
|
262
|
+
const stripped = hostname.replace(wwwPrefixRegex, "");
|
|
263
|
+
return stripped.includes(".") ? stripped : hostname;
|
|
232
264
|
};
|
|
233
265
|
const isIpAddress = (hostname) => {
|
|
234
266
|
return ipv4Regex.test(hostname) || ipv6Regex.test(hostname.replace(ipv6BracketsRegex, ""));
|
|
@@ -247,15 +279,59 @@ const genericSecondLevels = [
|
|
|
247
279
|
"or",
|
|
248
280
|
"org"
|
|
249
281
|
];
|
|
250
|
-
const
|
|
251
|
-
|
|
282
|
+
const suffixRulesCache = /* @__PURE__ */ new WeakMap();
|
|
283
|
+
const getSuffixRules = (suffixes) => {
|
|
284
|
+
let cached = suffixRulesCache.get(suffixes);
|
|
285
|
+
if (cached) return cached;
|
|
286
|
+
cached = {
|
|
287
|
+
names: /* @__PURE__ */ new Set(),
|
|
288
|
+
wildcards: /* @__PURE__ */ new Set(),
|
|
289
|
+
exceptions: /* @__PURE__ */ new Set()
|
|
290
|
+
};
|
|
291
|
+
for (const entry of suffixes) {
|
|
292
|
+
const rule = entry.toLowerCase();
|
|
293
|
+
if (rule.startsWith("!")) {
|
|
294
|
+
cached.exceptions.add(normalizeDomain(rule.slice(1)));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (rule.startsWith("*.")) {
|
|
298
|
+
cached.wildcards.add(normalizeDomain(rule.slice(2)));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
cached.names.add(normalizeDomain(rule));
|
|
302
|
+
}
|
|
303
|
+
suffixRulesCache.set(suffixes, cached);
|
|
304
|
+
return cached;
|
|
305
|
+
};
|
|
306
|
+
const getPublicSuffixLength = (labels, rules) => {
|
|
307
|
+
for (let index = 0; index < labels.length; index++) if (rules.exceptions.has(labels.slice(index).join("."))) return labels.length - index - 1;
|
|
308
|
+
for (let index = 0; index < labels.length; index++) {
|
|
309
|
+
if (rules.names.has(labels.slice(index).join("."))) return labels.length - index;
|
|
310
|
+
if (index + 1 < labels.length && rules.wildcards.has(labels.slice(index + 1).join("."))) return labels.length - index;
|
|
311
|
+
}
|
|
312
|
+
return 1;
|
|
313
|
+
};
|
|
314
|
+
const getRegistrableDomain = (url, options) => {
|
|
315
|
+
const hostname = getHostname(url)?.toLowerCase();
|
|
252
316
|
if (!hostname) return;
|
|
253
317
|
if (isIpAddress(hostname)) return hostname;
|
|
254
318
|
const parts = hostname.split(".");
|
|
319
|
+
if (options?.suffixes) {
|
|
320
|
+
const suffixLength = getPublicSuffixLength(parts, getSuffixRules(options.suffixes));
|
|
321
|
+
return parts.slice(-Math.min(suffixLength + 1, parts.length)).join(".");
|
|
322
|
+
}
|
|
255
323
|
if (parts.length <= 2) return hostname;
|
|
256
324
|
const [secondLevel, topLevel] = parts.slice(-2);
|
|
257
325
|
const labels = topLevel.length === 2 && genericSecondLevels.includes(secondLevel) ? 3 : 2;
|
|
258
326
|
return parts.slice(-labels).join(".");
|
|
259
327
|
};
|
|
328
|
+
/** @deprecated Use `fixMalformedScheme` instead. */
|
|
329
|
+
const fixMalformedProtocol = fixMalformedScheme;
|
|
330
|
+
/** @deprecated Use `resolveFeedScheme` instead. */
|
|
331
|
+
const resolveFeedProtocol = resolveFeedScheme;
|
|
332
|
+
/** @deprecated Use `addMissingScheme` instead. */
|
|
333
|
+
const addMissingProtocol = addMissingScheme;
|
|
334
|
+
/** @deprecated Use `upgradeScheme` instead. */
|
|
335
|
+
const upgradeProtocol = upgradeScheme;
|
|
260
336
|
//#endregion
|
|
261
|
-
export { addMissingProtocol, decodeSegment, fixMalformedProtocol, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveUrl, stripWww, upgradeProtocol };
|
|
337
|
+
export { addMissingProtocol, addMissingScheme, decodeSegment, fixMalformedProtocol, fixMalformedScheme, getPathSegments, getRegistrableDomain, getSubdomain, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, resolveFeedProtocol, resolveFeedScheme, resolveUrl, stripWww, upgradeProtocol, upgradeScheme };
|
package/package.json
CHANGED