urlpurify 1.6.0 → 1.6.1

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/clean.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { fixMalformedProtocol, isHttpUrl, isIpAddress, parseUrl, stripWww } from "./node_modules/trousse/dist/urls.mjs";
1
+ import { fixMalformedProtocol, isHttpUrl, isIpAddress, normalizeUrl, parseUrl, stripWww } from "./node_modules/trousse/dist/urls.mjs";
2
2
  import { defaultTrackingParams, defaultUnwrappers } from "./defaults.mjs";
3
3
  //#region src/clean.ts
4
4
  const replacementCharacter = "�";
@@ -73,6 +73,9 @@ const applyUnwrappers = (url, unwrappers) => {
73
73
  const target = cleanTarget(raw);
74
74
  if (target) return target;
75
75
  }
76
+ const collapsed = parseUrl(normalizeUrl(url.href, { collapseSlashes: true }));
77
+ if (!collapsed || collapsed.href === url.href) return;
78
+ return applyUnwrappers(collapsed, unwrappers);
76
79
  };
77
80
  const unwrapUrl = (url, unwrappers = defaultUnwrappers) => {
78
81
  const parsed = parseUrl(url);
@@ -4,18 +4,22 @@ const testRegex = (regex, value) => {
4
4
  regex.lastIndex = 0;
5
5
  return regex.test(value);
6
6
  };
7
+ const testRegexAnyCase = (regex, lowerValue, value) => {
8
+ return testRegex(regex, lowerValue) || lowerValue !== value && testRegex(regex, value);
9
+ };
7
10
  const isAnyOf = (value, patterns, parser) => {
8
11
  if (isNullish(value)) return false;
9
12
  const parsedValue = parser ? parser(value) : value.toLowerCase().trim();
13
+ const originalValue = parser ? parsedValue : value.trim();
10
14
  return (typeof patterns === "string" || patterns instanceof RegExp ? [patterns] : patterns).some((pattern) => {
11
- if (pattern instanceof RegExp) return testRegex(pattern, parsedValue);
15
+ if (pattern instanceof RegExp) return testRegexAnyCase(pattern, parsedValue, originalValue);
12
16
  return parsedValue === (parser ? pattern : pattern.toLowerCase().trim());
13
17
  });
14
18
  };
15
19
  const endsWithAnyOf = (value, patterns) => {
16
20
  const lowerValue = value.toLowerCase();
17
21
  return patterns.some((pattern) => {
18
- if (pattern instanceof RegExp) return testRegex(pattern, lowerValue);
22
+ if (pattern instanceof RegExp) return testRegexAnyCase(pattern, lowerValue, value);
19
23
  return pattern && lowerValue.endsWith(pattern.toLowerCase());
20
24
  });
21
25
  };
@@ -1,6 +1,6 @@
1
1
  import { endsWithAnyOf, isAnyOf } from "./matching.mjs";
2
2
  //#region node_modules/trousse/dist/urls.js
3
- const httpProtocols = ["http:", "https:"];
3
+ const httpSchemes = ["http:", "https:"];
4
4
  const parseUrl = (value, base) => {
5
5
  if (value instanceof URL && base === void 0) return value;
6
6
  if (URL.parse) return URL.parse(value, base) ?? void 0;
@@ -31,9 +31,9 @@ const isSubdomainOf = (url, domains) => {
31
31
  return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${normalizeDomain(domain)}`));
32
32
  };
33
33
  const isHttpUrl = (url) => {
34
- const protocol = parseUrl(url)?.protocol;
35
- if (!protocol) return false;
36
- return httpProtocols.includes(protocol);
34
+ const scheme = parseUrl(url)?.protocol;
35
+ if (!scheme) return false;
36
+ return httpSchemes.includes(scheme);
37
37
  };
38
38
  const isHostOrSubdomainOf = (url, domains) => {
39
39
  return isHostOf(url, domains) || isSubdomainOf(url, domains);
@@ -44,11 +44,25 @@ const decodeSegment = (segment) => {
44
44
  return decodeURIComponent(segment);
45
45
  } catch {}
46
46
  };
47
+ const strippedParamsCache = /* @__PURE__ */ new WeakMap();
48
+ const getStrippedParamsSet = (params) => {
49
+ let cached = strippedParamsCache.get(params);
50
+ if (!cached) {
51
+ cached = new Set(params.map((param) => param.toLowerCase()));
52
+ strippedParamsCache.set(params, cached);
53
+ }
54
+ return cached;
55
+ };
47
56
  const ipv4Regex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
48
57
  const ipv6Regex = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i;
49
- const wwwPrefixRegex = /^www\./;
58
+ const wwwPrefixRegex = /^(?:www\.)+(?=.*\.)/;
50
59
  const ipv6BracketsRegex = /^\[|\]$/g;
60
+ const unreservedCharsRegex = /[a-zA-Z0-9._~-]/;
51
61
  const httpsLetterRegex = /s/i;
62
+ const schemePrefixRegex = /^https?:\/\//;
63
+ const percentEscapeOrLettersRegex = /%[0-9A-Fa-f]{2}|[A-Z]+/g;
64
+ const plusRegex = /\+/g;
65
+ const repeatedSlashesRegex = /(?<!:|%3a)\/{2,}/gi;
52
66
  const leadingWhitespaceChars = [
53
67
  " ",
54
68
  " ",
@@ -64,35 +78,39 @@ const localhostRegex = /^localhost(?:[:/?#]|$)/i;
64
78
  const validUrlRegex = /^https?:\/\/(?:www\.|[a-vx-z0-9])/i;
65
79
  const schemeSeparator = String.raw`\.*[:\s=\\/][:\s=.\\/]*`;
66
80
  const leadingSchemeSeparator = String.raw`\.*(?::[:\s=.\\/]*|[\s=\\/][:\s=.\\/]+)`;
67
- const doubledProtocolRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?[htps]{2,7}${leadingSchemeSeparator}([htps]{2,7})(?!:\d)${schemeSeparator}[.,:/]*(www[./]+)?`, "i");
68
- const singleMalformedRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?(?:h[htps():]{1,10}|t{1,2}ps?)${leadingSchemeSeparator}[.,:/]*(www[./]+)?`, "i");
69
- const fixMalformedProtocol = (url) => {
70
- if (validUrlRegex.test(url) && !doubledProtocolRegex.test(url)) return url;
71
- const doubledMatch = doubledProtocolRegex.exec(url);
72
- if (doubledMatch) {
81
+ const doubledSchemeRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?[htps]{2,7}${leadingSchemeSeparator}([htps]{2,7})(?!:\d|[^/?#\\]*@)(${schemeSeparator})[.,:/]*(www[./]+)?`, "i");
82
+ const singleMalformedRegex = /* @__PURE__ */ new RegExp(String.raw`^\/?(h[htps():]{1,10}|t{1,2}ps?)(${leadingSchemeSeparator})[.,:/]*(www[./]+)?`, "i");
83
+ const isSchemeToken = (token, separator) => {
84
+ const lowered = token.toLowerCase();
85
+ return `${token}${separator}`.includes(":") || lowered.includes("t") && lowered.includes("p");
86
+ };
87
+ const fixMalformedScheme = (url) => {
88
+ if (validUrlRegex.test(url) && !doubledSchemeRegex.test(url)) return url;
89
+ const doubledMatch = doubledSchemeRegex.exec(url);
90
+ if (doubledMatch && isSchemeToken(doubledMatch[1], doubledMatch[2])) {
73
91
  const inner = doubledMatch[1];
74
- const www = doubledMatch[2];
92
+ const www = doubledMatch[3];
75
93
  const rest = url.slice(doubledMatch[0].length);
76
- return (httpsLetterRegex.test(inner) ? "https://" : "http://") + (www ? "www." : "") + rest;
94
+ return `${httpsLetterRegex.test(inner) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
77
95
  }
78
96
  const singleMatch = singleMalformedRegex.exec(url);
79
- if (singleMatch) {
97
+ if (singleMatch && isSchemeToken(singleMatch[1], singleMatch[2])) {
80
98
  const fullMatch = singleMatch[0];
81
- const www = singleMatch[1];
99
+ const www = singleMatch[3];
82
100
  const rest = url.slice(fullMatch.length);
83
- return (httpsLetterRegex.test(fullMatch) ? "https://" : "http://") + (www ? "www." : "") + rest;
101
+ return `${httpsLetterRegex.test(fullMatch) ? "https://" : "http://"}${www ? "www." : ""}${rest}`;
84
102
  }
85
103
  return url;
86
104
  };
87
- const addMissingProtocol = (url, protocol = "https") => {
88
- if (url.startsWith("[")) return parseUrl(`${protocol}://${url}`) ? `${protocol}://${url}` : url;
105
+ const addMissingScheme = (url, scheme = "https") => {
106
+ if (url.startsWith("[")) return parseUrl(`${scheme}://${url}`) ? `${scheme}://${url}` : url;
89
107
  const colonIndex = url.indexOf(":");
90
108
  if (colonIndex > 0) {
91
109
  const beforeColon = url.slice(0, colonIndex);
92
110
  if (!beforeColon.includes(".") && !beforeColon.includes("/") && !localhostRegex.test(url)) return url;
93
111
  }
94
112
  if (url.startsWith("//") && !url.startsWith("///")) {
95
- const parsed = parseUrl(`${protocol}:${url}`);
113
+ const parsed = parseUrl(`${scheme}:${url}`);
96
114
  if (!parsed) return url;
97
115
  const hostname = parsed.hostname;
98
116
  if (hostname.includes(".") || hostname === "localhost" || isIpAddress(hostname)) return parsed.href;
@@ -106,14 +124,83 @@ const addMissingProtocol = (url, protocol = "https") => {
106
124
  }
107
125
  const firstChar = url.charAt(0);
108
126
  if (leadingWhitespaceChars.includes(firstChar)) return url;
109
- return `${protocol}://${url}`;
127
+ return `${scheme}://${url}`;
128
+ };
129
+ const decodeAndNormalizeEncoding = (value) => {
130
+ if (!value.includes("%")) return value;
131
+ return value.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex) => {
132
+ const charCode = Number.parseInt(hex, 16);
133
+ const char = String.fromCharCode(charCode);
134
+ if (unreservedCharsRegex.test(char)) return char;
135
+ return `%${hex.toUpperCase()}`;
136
+ });
137
+ };
138
+ const decodeQueryKey = (pair) => {
139
+ const key = pair.split("=")[0].replace(plusRegex, " ");
140
+ try {
141
+ return decodeURIComponent(key);
142
+ } catch {
143
+ return key;
144
+ }
145
+ };
146
+ const compareQueryPairs = (a, b) => {
147
+ const keyA = decodeQueryKey(a);
148
+ const keyB = decodeQueryKey(b);
149
+ if (keyA < keyB) return -1;
150
+ if (keyA > keyB) return 1;
151
+ return 0;
152
+ };
153
+ const lowercaseQueryPair = (pair) => {
154
+ return pair.replace(percentEscapeOrLettersRegex, (match) => {
155
+ return match.startsWith("%") ? match : match.toLowerCase();
156
+ });
157
+ };
158
+ const normalizeUrl = (url, options) => {
159
+ try {
160
+ const parsed = new URL(options.normalizeUnicode ? url.normalize("NFC") : url);
161
+ if (options.stripHostTrailingDot && parsed.hostname.endsWith(".")) parsed.hostname = parsed.hostname.slice(0, -1);
162
+ if (options.stripAuthentication) {
163
+ parsed.username = "";
164
+ parsed.password = "";
165
+ }
166
+ if (options.stripWww) parsed.hostname = stripWww(parsed.hostname);
167
+ if (options.stripHash) parsed.hash = "";
168
+ let pathname = parsed.pathname;
169
+ if (options.normalizeEncoding) pathname = decodeAndNormalizeEncoding(pathname);
170
+ if (options.collapseSlashes) pathname = pathname.replace(repeatedSlashesRegex, "/");
171
+ if (options.stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
172
+ parsed.pathname = pathname;
173
+ if (options.normalizeEncoding && parsed.search.includes("%")) parsed.search = decodeAndNormalizeEncoding(parsed.search);
174
+ if (options.normalizeEncoding && parsed.hash.includes("%")) parsed.hash = decodeAndNormalizeEncoding(parsed.hash);
175
+ if (options.stripQuery) parsed.search = "";
176
+ if (parsed.search && (options.stripQueryParams || options.lowercaseQuery)) {
177
+ let pairs = parsed.search.slice(1).split("&");
178
+ if (options.stripQueryParams) {
179
+ const strippedSet = getStrippedParamsSet(options.stripQueryParams);
180
+ pairs = pairs.filter((pair) => pair && !strippedSet.has(decodeQueryKey(pair).toLowerCase()));
181
+ }
182
+ if (options.lowercaseQuery) pairs = pairs.map(lowercaseQueryPair);
183
+ parsed.search = pairs.join("&");
184
+ }
185
+ if (options.sortQueryParams && parsed.search.includes("&")) parsed.search = parsed.search.slice(1).split("&").filter(Boolean).sort(compareQueryPairs).join("&");
186
+ if (options.stripEmptyQuery && parsed.search === "") parsed.search = "";
187
+ let result = parsed.href;
188
+ if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
189
+ if (options.stripScheme ?? options.stripProtocol) result = result.replace(schemePrefixRegex, "");
190
+ return result;
191
+ } catch {
192
+ return url;
193
+ }
110
194
  };
111
195
  const stripWww = (hostname) => {
112
- const stripped = hostname.replace(wwwPrefixRegex, "");
113
- return stripped.includes(".") ? stripped : hostname;
196
+ return hostname.replace(wwwPrefixRegex, "");
114
197
  };
115
198
  const isIpAddress = (hostname) => {
116
199
  return ipv4Regex.test(hostname) || ipv6Regex.test(hostname.replace(ipv6BracketsRegex, ""));
117
200
  };
201
+ /** @deprecated Use `fixMalformedScheme` instead. */
202
+ const fixMalformedProtocol = fixMalformedScheme;
203
+ /** @deprecated Use `addMissingScheme` instead. */
204
+ const addMissingProtocol = addMissingScheme;
118
205
  //#endregion
119
- export { addMissingProtocol, decodeSegment, fixMalformedProtocol, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, parseUrl, stripWww };
206
+ export { addMissingProtocol, addMissingScheme, decodeSegment, fixMalformedProtocol, fixMalformedScheme, isHostOf, isHostOrSubdomainOf, isHttpUrl, isIpAddress, isSubdomainOf, normalizeUrl, parseUrl, stripWww };
package/package.json CHANGED
@@ -42,8 +42,8 @@
42
42
  "devDependencies": {
43
43
  "@types/bun": "^1.4.2",
44
44
  "kvalita": "^1.24.3",
45
- "trousse": "^2.6.1",
45
+ "trousse": "^2.10.1",
46
46
  "tsdown": "^0.23.0"
47
47
  },
48
- "version": "1.6.0"
48
+ "version": "1.6.1"
49
49
  }