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.
@@ -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/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.constructor === Object;
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, (_, key) => String(params[key]));
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, (_, key) => escapeHtml(String(params[key])));
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 regexMetaCharsRegex = /[.*+?^${}()|[\]\\-]/g;
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(regexMetaCharsRegex, "\\$&");
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.test(parsedValue);
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) => parsedValue === pattern.toLowerCase().trim());
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.test(parsedValue);
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.test(lowerValue);
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.test(lowerValue);
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) if (pattern instanceof RegExp) regexPatterns.push(pattern);
47
- else stringPatterns.push(pattern.toLowerCase().trim());
48
- const words = value.toLowerCase().split(whitespaceRegex);
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
- for (const stringPattern of stringPatterns) if (word === stringPattern) return true;
51
- for (const regexPattern of regexPatterns) if (regexPattern.test(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;
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[key] = object[key];
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[key] = value;
38
+ if (keep(value)) setKey(result, key, value);
27
39
  }
28
40
  return result;
29
41
  };
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,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?: Array<string>;
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 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,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 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
+ 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 isHostOf = (url, hosts) => {
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 = parseUrl(url)?.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 protocol = parseUrl(url)?.protocol;
26
- if (!protocol) return false;
27
- return httpProtocols.includes(protocol);
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 = parseUrl(url)?.hostname.toLowerCase();
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 safePathCharsRegex = /[a-zA-Z0-9._~!$&'()*+,;=:@-]/;
73
+ const unreservedCharsRegex = /[a-zA-Z0-9._~-]/;
61
74
  const httpsLetterRegex = /s/i;
62
- const protocolPrefixRegex = /^https?:\/\//;
75
+ const schemePrefixRegex = /^https?:\/\//;
63
76
  const percentEscapeOrLettersRegex = /%[0-9A-Fa-f]{2}|[A-Z]+/g;
64
77
  const plusRegex = /\+/g;
65
- const httpProtocolRegex = /^http:\/\//i;
66
- const httpsProtocolRegex = /^https:\/\//i;
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 doubledProtocolRegex = new RegExp(String.raw`^\/?[htps]{2,7}${schemeSeparator}([htps]{2,7})${schemeSeparator}[.,:/]*(www[./]+)?`, "i");
75
- const singleMalformedRegex = new RegExp(String.raw`^\/?(?:h[htps():]{1,10}|t{1,2}ps?)${schemeSeparator}[.,:/]*(www[./]+)?`, "i");
76
- const fixMalformedProtocol = (url) => {
77
- if (validUrlRegex.test(url) && !doubledProtocolRegex.test(url)) return url;
78
- const doubledMatch = doubledProtocolRegex.exec(url);
79
- if (doubledMatch) {
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[2];
108
+ const www = doubledMatch[3];
82
109
  const rest = url.slice(doubledMatch[0].length);
83
- return (httpsLetterRegex.test(inner) ? "https://" : "http://") + (www ? "www." : "") + rest;
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[1];
115
+ const www = singleMatch[3];
89
116
  const rest = url.slice(fullMatch.length);
90
- return (httpsLetterRegex.test(fullMatch) ? "https://" : "http://") + (www ? "www." : "") + rest;
117
+ return `${httpsLetterRegex.test(fullMatch) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
91
118
  }
92
119
  return url;
93
120
  };
94
- const feedProtocols = [
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 resolveFeedProtocol = (url, protocol = "https") => {
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 scheme of feedProtocols) {
112
- if (!urlLower.startsWith(scheme)) continue;
113
- if (urlLower.startsWith(`${scheme}http://`) || urlLower.startsWith(`${scheme}https://`)) return url.slice(scheme.length);
114
- 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)}`;
115
142
  }
116
143
  return url;
117
144
  };
118
- const addMissingProtocol = (url, protocol = "https") => {
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("/") && beforeColon !== "localhost") return url;
150
+ if (!beforeColon.includes(".") && !beforeColon.includes("/") && !localhostRegex.test(url)) return url;
123
151
  }
124
152
  if (url.startsWith("//") && !url.startsWith("///")) {
125
- const parsed = parseUrl(`${protocol}:${url}`);
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 (url.startsWith("/") || url.startsWith(".")) return url;
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 (!url.startsWith("localhost")) return url;
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 `${protocol}://${url}`;
167
+ return `${scheme}://${url}`;
140
168
  };
141
- const upgradeProtocol = (url, protocol = "https") => {
142
- if (protocol === "https") return url.replace(httpProtocolRegex, "https://");
143
- 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;
144
176
  };
145
177
  const resolveUrl = (url, base) => {
146
- if (url.startsWith("#") && !base) return;
178
+ const trimmedUrl = url.trim();
179
+ if (!trimmedUrl) return;
180
+ if (trimmedUrl.startsWith("#") && !base) return;
147
181
  let resolvedUrl;
148
- resolvedUrl = url.includes("&") ? decodeHTMLStrict(url) : url;
149
- resolvedUrl = resolveFeedProtocol(resolvedUrl);
150
- resolvedUrl = fixMalformedProtocol(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 = addMissingProtocol(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 (safePathCharsRegex.test(char)) return char;
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.href.endsWith("?")) parsed.search = "";
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(protocolPrefixRegex, "");
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
- return hostname.replace(wwwPrefixRegex, "");
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 getRegistrableDomain = (url) => {
251
- const hostname = parseUrl(url)?.hostname;
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
@@ -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.0"
49
+ "version": "2.7.0"
50
50
  }