trousse 2.6.1 → 2.8.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.
@@ -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 (isNonEmptyString(value)) {
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 testRegex(pattern, parsedValue);
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 testRegex(pattern, parsedValue);
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 testRegex(pattern, lowerValue);
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 testRegex(pattern, lowerValue);
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.toLowerCase().split(whitespaceRegex);
69
+ const words = value.split(whitespaceRegex);
65
70
  for (const word of words) {
66
- for (const stringPattern of stringPatterns) if (word === stringPattern) return true;
67
- for (const regexPattern of regexPatterns) if (testRegex(regexPattern, word)) return true;
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
@@ -1,7 +1,8 @@
1
1
  //#region src/timers.ts
2
+ const maxDelay = 2 ** 31 - 1;
2
3
  const sleep = (milliseconds) => {
3
4
  return new Promise((resolve) => {
4
- setTimeout(resolve, milliseconds);
5
+ setTimeout(resolve, Math.min(milliseconds, maxDelay));
5
6
  });
6
7
  };
7
8
  //#endregion
package/dist/types.d.ts CHANGED
@@ -6,8 +6,14 @@ 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;
16
+ stripHostTrailingDot?: boolean;
11
17
  stripAuthentication?: boolean;
12
18
  stripWww?: boolean;
13
19
  stripTrailingSlash?: 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 fixMalformedProtocol: (url: string) => string;
12
- export declare const resolveFeedProtocol: (url: string, protocol?: 'http' | 'https') => string;
13
- export declare const addMissingProtocol: (url: string, protocol?: 'http' | 'https') => string;
14
- export declare const upgradeProtocol: (url: string, protocol?: 'http' | 'https') => string;
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 httpProtocols = ["http:", "https:"];
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 protocol = parseUrl(url)?.protocol;
39
- if (!protocol) return false;
40
- return httpProtocols.includes(protocol);
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);
@@ -68,15 +68,15 @@ const getStrippedParamsSet = (params) => {
68
68
  };
69
69
  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
- const wwwPrefixRegex = /^www\./;
71
+ const wwwPrefixRegex = /^(?:www\.)+(?=.*\.)/;
72
72
  const ipv6BracketsRegex = /^\[|\]$/g;
73
- const safePathCharsRegex = /[a-zA-Z0-9._~!$&'()*+,;=:@-]/;
73
+ const unreservedCharsRegex = /[a-zA-Z0-9._~-]/;
74
74
  const httpsLetterRegex = /s/i;
75
- const protocolPrefixRegex = /^https?:\/\//;
75
+ const schemePrefixRegex = /^https?:\/\//;
76
76
  const percentEscapeOrLettersRegex = /%[0-9A-Fa-f]{2}|[A-Z]+/g;
77
77
  const plusRegex = /\+/g;
78
- const httpProtocolRegex = /^http:\/\//i;
79
- const httpsProtocolRegex = /^https:\/\//i;
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 doubledProtocolRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?[htps]{2,7}${leadingSchemeSeparator}([htps]{2,7})(?!:\d)${schemeSeparator}[.,:/]*(www[./]+)?`, "i");
96
- const singleMalformedRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?(?:h[htps():]{1,10}|t{1,2}ps?)${leadingSchemeSeparator}[.,:/]*(www[./]+)?`, "i");
97
- const fixMalformedProtocol = (url) => {
98
- if (validUrlRegex.test(url) && !doubledProtocolRegex.test(url)) return url;
99
- const doubledMatch = doubledProtocolRegex.exec(url);
100
- if (doubledMatch) {
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[2];
108
+ const www = doubledMatch[3];
103
109
  const rest = url.slice(doubledMatch[0].length);
104
- return (httpsLetterRegex.test(inner) ? "https://" : "http://") + (www ? "www." : "") + rest;
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[1];
115
+ const www = singleMatch[3];
110
116
  const rest = url.slice(fullMatch.length);
111
- return (httpsLetterRegex.test(fullMatch) ? "https://" : "http://") + (www ? "www." : "") + rest;
117
+ return `${httpsLetterRegex.test(fullMatch) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
112
118
  }
113
119
  return url;
114
120
  };
115
- const feedProtocols = [
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 resolveFeedProtocol = (url, protocol = "https") => {
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 scheme of feedProtocols) {
133
- if (!urlLower.startsWith(scheme)) continue;
134
- if (urlLower.startsWith(`${scheme}http://`) || urlLower.startsWith(`${scheme}https://`)) return url.slice(scheme.length);
135
- if (urlLower.startsWith(`${scheme}//`)) return `${protocol}:${url.slice(scheme.length)}`;
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 addMissingProtocol = (url, protocol = "https") => {
140
- if (url.startsWith("[")) return parseUrl(`${protocol}://${url}`) ? `${protocol}://${url}` : url;
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(`${protocol}:${url}`);
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 `${protocol}://${url}`;
167
+ return `${scheme}://${url}`;
162
168
  };
163
- const upgradeProtocol = (url, protocol = "https") => {
164
- if (protocol === "https") return url.replace(httpProtocolRegex, "https://");
165
- return url.replace(httpsProtocolRegex, "http://");
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 = resolveFeedProtocol(resolvedUrl);
174
- if (!base || !resolvedUrl.startsWith("/")) resolvedUrl = fixMalformedProtocol(resolvedUrl);
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 = addMissingProtocol(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 (safePathCharsRegex.test(char)) return char;
201
+ if (unreservedCharsRegex.test(char)) return char;
192
202
  return `%${hex.toUpperCase()}`;
193
203
  });
194
204
  };
@@ -215,6 +225,7 @@ const lowercaseQueryPair = (pair) => {
215
225
  const normalizeUrl = (url, options) => {
216
226
  try {
217
227
  const parsed = new URL(options.normalizeUnicode ? url.normalize("NFC") : url);
228
+ if (options.stripHostTrailingDot && parsed.hostname.endsWith(".")) parsed.hostname = parsed.hostname.slice(0, -1);
218
229
  if (options.stripAuthentication) {
219
230
  parsed.username = "";
220
231
  parsed.password = "";
@@ -225,14 +236,15 @@ const normalizeUrl = (url, options) => {
225
236
  if (options.normalizeEncoding) pathname = decodeAndNormalizeEncoding(pathname);
226
237
  if (options.collapseSlashes) pathname = pathname.replace(/\/+/g, "/");
227
238
  if (options.stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
228
- if (options.stripRootSlash && pathname === "/") pathname = "";
229
239
  parsed.pathname = pathname;
240
+ if (options.normalizeEncoding && parsed.search.includes("%")) parsed.search = decodeAndNormalizeEncoding(parsed.search);
241
+ if (options.normalizeEncoding && parsed.hash.includes("%")) parsed.hash = decodeAndNormalizeEncoding(parsed.hash);
230
242
  if (options.stripQuery) parsed.search = "";
231
243
  if (parsed.search && (options.stripQueryParams || options.lowercaseQuery)) {
232
244
  let pairs = parsed.search.slice(1).split("&");
233
245
  if (options.stripQueryParams) {
234
246
  const strippedSet = getStrippedParamsSet(options.stripQueryParams);
235
- pairs = pairs.filter((pair) => !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
247
+ pairs = pairs.filter((pair) => pair && !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
236
248
  }
237
249
  if (options.lowercaseQuery) pairs = pairs.map(lowercaseQueryPair);
238
250
  parsed.search = pairs.join("&");
@@ -241,15 +253,14 @@ const normalizeUrl = (url, options) => {
241
253
  if (options.stripEmptyQuery && parsed.search === "") parsed.search = "";
242
254
  let result = parsed.href;
243
255
  if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
244
- if (options.stripProtocol) result = result.replace(protocolPrefixRegex, "");
256
+ if (options.stripScheme ?? options.stripProtocol) result = result.replace(schemePrefixRegex, "");
245
257
  return result;
246
258
  } catch {
247
259
  return url;
248
260
  }
249
261
  };
250
262
  const stripWww = (hostname) => {
251
- const stripped = hostname.replace(wwwPrefixRegex, "");
252
- return stripped.includes(".") ? stripped : hostname;
263
+ return hostname.replace(wwwPrefixRegex, "");
253
264
  };
254
265
  const isIpAddress = (hostname) => {
255
266
  return ipv4Regex.test(hostname) || ipv6Regex.test(hostname.replace(ipv6BracketsRegex, ""));
@@ -268,15 +279,59 @@ const genericSecondLevels = [
268
279
  "or",
269
280
  "org"
270
281
  ];
271
- const getRegistrableDomain = (url) => {
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
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "^1.4.2",
43
- "kvalita": "^1.24.0",
43
+ "kvalita": "^1.24.3",
44
44
  "tsdown": "^0.23.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "entities": "^8.1.0"
48
48
  },
49
- "version": "2.6.1"
49
+ "version": "2.8.0"
50
50
  }