trousse 2.6.1 → 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/matching.js +13 -7
- package/dist/timers.js +2 -1
- package/dist/types.d.ts +5 -0
- package/dist/urls.d.ts +14 -6
- package/dist/urls.js +96 -41
- 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/matching.js
CHANGED
|
@@ -7,6 +7,9 @@ const testRegex = (regex, value) => {
|
|
|
7
7
|
regex.lastIndex = 0;
|
|
8
8
|
return regex.test(value);
|
|
9
9
|
};
|
|
10
|
+
const testRegexAnyCase = (regex, lowerValue, value) => {
|
|
11
|
+
return testRegex(regex, lowerValue) || lowerValue !== value && testRegex(regex, value);
|
|
12
|
+
};
|
|
10
13
|
const escapeRegex = (value) => {
|
|
11
14
|
return value.replace(regexEscapableCharsRegex, (char) => {
|
|
12
15
|
if (regexSyntaxCharsRegex.test(char)) return `\\${char}`;
|
|
@@ -16,8 +19,9 @@ const escapeRegex = (value) => {
|
|
|
16
19
|
const isAnyOf = (value, patterns, parser) => {
|
|
17
20
|
if (isNullish(value)) return false;
|
|
18
21
|
const parsedValue = parser ? parser(value) : value.toLowerCase().trim();
|
|
22
|
+
const originalValue = parser ? parsedValue : value.trim();
|
|
19
23
|
return (typeof patterns === "string" || patterns instanceof RegExp ? [patterns] : patterns).some((pattern) => {
|
|
20
|
-
if (pattern instanceof RegExp) return
|
|
24
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, parsedValue, originalValue);
|
|
21
25
|
return parsedValue === (parser ? pattern : pattern.toLowerCase().trim());
|
|
22
26
|
});
|
|
23
27
|
};
|
|
@@ -31,22 +35,23 @@ const getAnyOf = (value, patterns, parser) => {
|
|
|
31
35
|
const includesAnyOf = (value, patterns, parser) => {
|
|
32
36
|
if (isNullish(value)) return false;
|
|
33
37
|
const parsedValue = parser ? parser(value) : value.toLowerCase();
|
|
38
|
+
const originalValue = parser ? parsedValue : value;
|
|
34
39
|
return patterns.some((pattern) => {
|
|
35
|
-
if (pattern instanceof RegExp) return
|
|
40
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, parsedValue, originalValue);
|
|
36
41
|
return pattern && parsedValue.includes(parser ? pattern : pattern.toLowerCase());
|
|
37
42
|
});
|
|
38
43
|
};
|
|
39
44
|
const startsWithAnyOf = (value, patterns) => {
|
|
40
45
|
const lowerValue = value.toLowerCase();
|
|
41
46
|
return patterns.some((pattern) => {
|
|
42
|
-
if (pattern instanceof RegExp) return
|
|
47
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, lowerValue, value);
|
|
43
48
|
return pattern && lowerValue.startsWith(pattern.toLowerCase());
|
|
44
49
|
});
|
|
45
50
|
};
|
|
46
51
|
const endsWithAnyOf = (value, patterns) => {
|
|
47
52
|
const lowerValue = value.toLowerCase();
|
|
48
53
|
return patterns.some((pattern) => {
|
|
49
|
-
if (pattern instanceof RegExp) return
|
|
54
|
+
if (pattern instanceof RegExp) return testRegexAnyCase(pattern, lowerValue, value);
|
|
50
55
|
return pattern && lowerValue.endsWith(pattern.toLowerCase());
|
|
51
56
|
});
|
|
52
57
|
};
|
|
@@ -61,10 +66,11 @@ const anyWordMatchesAnyOf = (value, patterns) => {
|
|
|
61
66
|
const stringPattern = pattern.toLowerCase().trim();
|
|
62
67
|
if (stringPattern) stringPatterns.push(stringPattern);
|
|
63
68
|
}
|
|
64
|
-
const words = value.
|
|
69
|
+
const words = value.split(whitespaceRegex);
|
|
65
70
|
for (const word of words) {
|
|
66
|
-
|
|
67
|
-
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;
|
|
68
74
|
}
|
|
69
75
|
return false;
|
|
70
76
|
};
|
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;
|
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,7 +1,7 @@
|
|
|
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
7
|
if (URL.parse) return URL.parse(value, base) ?? void 0;
|
|
@@ -35,9 +35,9 @@ const isSubdomainOf = (url, domains) => {
|
|
|
35
35
|
return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${normalizeDomain(domain)}`));
|
|
36
36
|
};
|
|
37
37
|
const isHttpUrl = (url) => {
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
return
|
|
38
|
+
const scheme = parseUrl(url)?.protocol;
|
|
39
|
+
if (!scheme) return false;
|
|
40
|
+
return httpSchemes.includes(scheme);
|
|
41
41
|
};
|
|
42
42
|
const isHostOrSubdomainOf = (url, domains) => {
|
|
43
43
|
return isHostOf(url, domains) || isSubdomainOf(url, domains);
|
|
@@ -70,13 +70,13 @@ const ipv4Regex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
|
70
70
|
const ipv6Regex = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i;
|
|
71
71
|
const wwwPrefixRegex = /^www\./;
|
|
72
72
|
const ipv6BracketsRegex = /^\[|\]$/g;
|
|
73
|
-
const
|
|
73
|
+
const unreservedCharsRegex = /[a-zA-Z0-9._~-]/;
|
|
74
74
|
const httpsLetterRegex = /s/i;
|
|
75
|
-
const
|
|
75
|
+
const schemePrefixRegex = /^https?:\/\//;
|
|
76
76
|
const percentEscapeOrLettersRegex = /%[0-9A-Fa-f]{2}|[A-Z]+/g;
|
|
77
77
|
const plusRegex = /\+/g;
|
|
78
|
-
const
|
|
79
|
-
const
|
|
78
|
+
const httpSchemeRegex = /^http:\/\//i;
|
|
79
|
+
const httpsSchemeRegex = /^https:\/\//i;
|
|
80
80
|
const leadingWhitespaceChars = [
|
|
81
81
|
" ",
|
|
82
82
|
" ",
|
|
@@ -89,30 +89,36 @@ const relativeLeadChars = [
|
|
|
89
89
|
"#"
|
|
90
90
|
];
|
|
91
91
|
const localhostRegex = /^localhost(?:[:/?#]|$)/i;
|
|
92
|
+
const schemeColonRegex = /^[^/]*:/;
|
|
93
|
+
const missingColonSchemeRegex = /^https?[\s=.\\]*\/\//i;
|
|
92
94
|
const validUrlRegex = /^https?:\/\/(?:www\.|[a-vx-z0-9])/i;
|
|
93
95
|
const schemeSeparator = String.raw`\.*[:\s=\\/][:\s=.\\/]*`;
|
|
94
96
|
const leadingSchemeSeparator = String.raw`\.*(?::[:\s=.\\/]*|[\s=\\/][:\s=.\\/]+)`;
|
|
95
|
-
const
|
|
96
|
-
const singleMalformedRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?(
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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])) {
|
|
101
107
|
const inner = doubledMatch[1];
|
|
102
|
-
const www = doubledMatch[
|
|
108
|
+
const www = doubledMatch[3];
|
|
103
109
|
const rest = url.slice(doubledMatch[0].length);
|
|
104
|
-
return
|
|
110
|
+
return `${httpsLetterRegex.test(inner) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
|
|
105
111
|
}
|
|
106
112
|
const singleMatch = singleMalformedRegex.exec(url);
|
|
107
|
-
if (singleMatch) {
|
|
113
|
+
if (singleMatch && isSchemeToken(singleMatch[1], singleMatch[2])) {
|
|
108
114
|
const fullMatch = singleMatch[0];
|
|
109
|
-
const www = singleMatch[
|
|
115
|
+
const www = singleMatch[3];
|
|
110
116
|
const rest = url.slice(fullMatch.length);
|
|
111
|
-
return
|
|
117
|
+
return `${httpsLetterRegex.test(fullMatch) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
|
|
112
118
|
}
|
|
113
119
|
return url;
|
|
114
120
|
};
|
|
115
|
-
const
|
|
121
|
+
const feedSchemes = [
|
|
116
122
|
"feed:",
|
|
117
123
|
"rss:",
|
|
118
124
|
"podcast:",
|
|
@@ -125,26 +131,26 @@ const feedProtocols = [
|
|
|
125
131
|
"itms-podcast:",
|
|
126
132
|
"itms-podcasts:"
|
|
127
133
|
];
|
|
128
|
-
const
|
|
134
|
+
const resolveFeedScheme = (url, scheme = "https") => {
|
|
129
135
|
const firstCharCode = url.charCodeAt(0) | 32;
|
|
130
136
|
if (firstCharCode !== 102 && firstCharCode !== 114 && firstCharCode !== 112 && firstCharCode !== 105) return url;
|
|
131
137
|
const urlLower = url.toLowerCase();
|
|
132
|
-
for (const
|
|
133
|
-
if (!urlLower.startsWith(
|
|
134
|
-
if (urlLower.startsWith(`${
|
|
135
|
-
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)}`;
|
|
136
142
|
}
|
|
137
143
|
return url;
|
|
138
144
|
};
|
|
139
|
-
const
|
|
140
|
-
if (url.startsWith("[")) return parseUrl(`${
|
|
145
|
+
const addMissingScheme = (url, scheme = "https") => {
|
|
146
|
+
if (url.startsWith("[")) return parseUrl(`${scheme}://${url}`) ? `${scheme}://${url}` : url;
|
|
141
147
|
const colonIndex = url.indexOf(":");
|
|
142
148
|
if (colonIndex > 0) {
|
|
143
149
|
const beforeColon = url.slice(0, colonIndex);
|
|
144
150
|
if (!beforeColon.includes(".") && !beforeColon.includes("/") && !localhostRegex.test(url)) return url;
|
|
145
151
|
}
|
|
146
152
|
if (url.startsWith("//") && !url.startsWith("///")) {
|
|
147
|
-
const parsed = parseUrl(`${
|
|
153
|
+
const parsed = parseUrl(`${scheme}:${url}`);
|
|
148
154
|
if (!parsed) return url;
|
|
149
155
|
const hostname = parsed.hostname;
|
|
150
156
|
if (hostname.includes(".") || hostname === "localhost" || isIpAddress(hostname)) return parsed.href;
|
|
@@ -158,11 +164,15 @@ const addMissingProtocol = (url, protocol = "https") => {
|
|
|
158
164
|
}
|
|
159
165
|
const firstChar = url.charAt(0);
|
|
160
166
|
if (leadingWhitespaceChars.includes(firstChar)) return url;
|
|
161
|
-
return `${
|
|
167
|
+
return `${scheme}://${url}`;
|
|
162
168
|
};
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
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;
|
|
166
176
|
};
|
|
167
177
|
const resolveUrl = (url, base) => {
|
|
168
178
|
const trimmedUrl = url.trim();
|
|
@@ -170,15 +180,15 @@ const resolveUrl = (url, base) => {
|
|
|
170
180
|
if (trimmedUrl.startsWith("#") && !base) return;
|
|
171
181
|
let resolvedUrl;
|
|
172
182
|
resolvedUrl = trimmedUrl.includes("&") ? decodeHTMLStrict(trimmedUrl) : trimmedUrl;
|
|
173
|
-
resolvedUrl =
|
|
174
|
-
if (!base ||
|
|
183
|
+
resolvedUrl = resolveFeedScheme(resolvedUrl);
|
|
184
|
+
if (!base || schemeColonRegex.test(resolvedUrl) || missingColonSchemeRegex.test(resolvedUrl)) resolvedUrl = fixMalformedScheme(resolvedUrl);
|
|
175
185
|
if (base) {
|
|
176
186
|
const resolved = parseUrl(resolvedUrl, base) ?? parseUrl(resolvedUrl);
|
|
177
187
|
if (!resolved) return;
|
|
178
188
|
if (isHttpUrl(resolved)) return resolved.href;
|
|
179
189
|
resolvedUrl = resolved.href;
|
|
180
190
|
}
|
|
181
|
-
resolvedUrl =
|
|
191
|
+
resolvedUrl = addMissingScheme(resolvedUrl);
|
|
182
192
|
const parsed = parseUrl(resolvedUrl);
|
|
183
193
|
if (!parsed || !isHttpUrl(parsed)) return;
|
|
184
194
|
return parsed.href;
|
|
@@ -188,7 +198,7 @@ const decodeAndNormalizeEncoding = (value) => {
|
|
|
188
198
|
return value.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex) => {
|
|
189
199
|
const charCode = Number.parseInt(hex, 16);
|
|
190
200
|
const char = String.fromCharCode(charCode);
|
|
191
|
-
if (
|
|
201
|
+
if (unreservedCharsRegex.test(char)) return char;
|
|
192
202
|
return `%${hex.toUpperCase()}`;
|
|
193
203
|
});
|
|
194
204
|
};
|
|
@@ -225,14 +235,15 @@ const normalizeUrl = (url, options) => {
|
|
|
225
235
|
if (options.normalizeEncoding) pathname = decodeAndNormalizeEncoding(pathname);
|
|
226
236
|
if (options.collapseSlashes) pathname = pathname.replace(/\/+/g, "/");
|
|
227
237
|
if (options.stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
228
|
-
if (options.stripRootSlash && pathname === "/") pathname = "";
|
|
229
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);
|
|
230
241
|
if (options.stripQuery) parsed.search = "";
|
|
231
242
|
if (parsed.search && (options.stripQueryParams || options.lowercaseQuery)) {
|
|
232
243
|
let pairs = parsed.search.slice(1).split("&");
|
|
233
244
|
if (options.stripQueryParams) {
|
|
234
245
|
const strippedSet = getStrippedParamsSet(options.stripQueryParams);
|
|
235
|
-
pairs = pairs.filter((pair) => !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
|
|
246
|
+
pairs = pairs.filter((pair) => pair && !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
|
|
236
247
|
}
|
|
237
248
|
if (options.lowercaseQuery) pairs = pairs.map(lowercaseQueryPair);
|
|
238
249
|
parsed.search = pairs.join("&");
|
|
@@ -241,7 +252,7 @@ const normalizeUrl = (url, options) => {
|
|
|
241
252
|
if (options.stripEmptyQuery && parsed.search === "") parsed.search = "";
|
|
242
253
|
let result = parsed.href;
|
|
243
254
|
if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
|
|
244
|
-
if (options.stripProtocol) result = result.replace(
|
|
255
|
+
if (options.stripScheme ?? options.stripProtocol) result = result.replace(schemePrefixRegex, "");
|
|
245
256
|
return result;
|
|
246
257
|
} catch {
|
|
247
258
|
return url;
|
|
@@ -268,15 +279,59 @@ const genericSecondLevels = [
|
|
|
268
279
|
"or",
|
|
269
280
|
"org"
|
|
270
281
|
];
|
|
271
|
-
const
|
|
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) => {
|
|
272
315
|
const hostname = getHostname(url)?.toLowerCase();
|
|
273
316
|
if (!hostname) return;
|
|
274
317
|
if (isIpAddress(hostname)) return hostname;
|
|
275
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
|
+
}
|
|
276
323
|
if (parts.length <= 2) return hostname;
|
|
277
324
|
const [secondLevel, topLevel] = parts.slice(-2);
|
|
278
325
|
const labels = topLevel.length === 2 && genericSecondLevels.includes(secondLevel) ? 3 : 2;
|
|
279
326
|
return parts.slice(-labels).join(".");
|
|
280
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;
|
|
281
336
|
//#endregion
|
|
282
|
-
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