urlpurify 1.4.1 → 1.5.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/README.md CHANGED
@@ -53,6 +53,8 @@ Removes matching query parameters and returns the cleaned URL. The input is retu
53
53
 
54
54
  Both `cleanUrl` and `stripTrackingParams` also drop a `ref` parameter when its value is the URL's own host (Ghost's self-referral `?ref=example.com` on `example.com`), regardless of the tracking list passed. With any other value `ref` is left alone, since it is often a real referral target.
55
55
 
56
+ A tracking list is compiled the first time it's used, and later calls with the same array reuse that result. To change the list, pass a new array. Entries pushed onto an array that's already been used are ignored.
57
+
56
58
  ### `createParamExtractor(config)`
57
59
 
58
60
  Builds an unwrapper for the common case where the target URL sits in a query parameter:
package/dist/clean.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { parseUrl } from "./node_modules/trousse/dist/urls.mjs";
1
+ import { isHttpUrl, 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 trackingMatcherCache = /* @__PURE__ */ new WeakMap();
@@ -20,28 +20,24 @@ const getTrackingMatcher = (params) => {
20
20
  const deleteTrackingParams = (url, trackingParams) => {
21
21
  if (!url.search) return false;
22
22
  const matcher = getTrackingMatcher(trackingParams);
23
- const keysToDelete = [];
24
- for (const key of url.searchParams.keys()) {
23
+ const host = stripWww(url.hostname);
24
+ const pairs = url.search.slice(1).split("&");
25
+ const keptPairs = pairs.filter((pair) => {
26
+ const entry = new URLSearchParams(pair).entries().next().value;
27
+ if (!entry) return true;
28
+ const [key, value] = entry;
25
29
  const name = key.toLowerCase();
26
- if (matcher.literals.has(name) || matcher.patterns.some((pattern) => pattern.test(name))) keysToDelete.push(key);
27
- }
28
- for (const key of keysToDelete) url.searchParams.delete(key);
29
- return keysToDelete.length > 0;
30
- };
31
- const wwwPrefixRegex = /^www\./;
32
- const stripHostPrefix = (host) => {
33
- return host.replace(wwwPrefixRegex, "").toLowerCase();
34
- };
35
- const deleteSelfReferentialParams = (url) => {
36
- const value = url.searchParams.get("ref");
37
- if (!value || stripHostPrefix(value) !== stripHostPrefix(url.hostname)) return false;
38
- url.searchParams.delete("ref");
30
+ if (matcher.literals.has(name) || matcher.patterns.some((pattern) => name.search(pattern) !== -1)) return false;
31
+ return key !== "ref" || stripWww(value.toLowerCase()) !== host;
32
+ });
33
+ if (keptPairs.length === pairs.length) return false;
34
+ url.search = keptPairs.join("&");
39
35
  return true;
40
36
  };
41
37
  const applyUnwrappers = (url, unwrappers) => {
42
38
  for (const unwrap of unwrappers) {
43
39
  const target = unwrap(url);
44
- if (target) return target;
40
+ if (target && isHttpUrl(target)) return target;
45
41
  }
46
42
  };
47
43
  const unwrapUrl = (url, unwrappers = defaultUnwrappers) => {
@@ -52,9 +48,7 @@ const unwrapUrl = (url, unwrappers = defaultUnwrappers) => {
52
48
  const stripTrackingParams = (url, trackingParams = defaultTrackingParams) => {
53
49
  const parsed = parseUrl(url);
54
50
  if (!parsed) return url;
55
- const trackingRemoved = deleteTrackingParams(parsed, trackingParams);
56
- const selfReferentialRemoved = deleteSelfReferentialParams(parsed);
57
- if (trackingRemoved || selfReferentialRemoved) return parsed.toString();
51
+ if (deleteTrackingParams(parsed, trackingParams)) return parsed.toString();
58
52
  return url;
59
53
  };
60
54
  const cleanUrl = (url, options) => {
@@ -73,9 +67,7 @@ const cleanUrl = (url, options) => {
73
67
  currentUrl = target;
74
68
  currentParsed = targetParsed;
75
69
  }
76
- const trackingRemoved = deleteTrackingParams(currentParsed, trackingParams);
77
- const selfReferentialRemoved = deleteSelfReferentialParams(currentParsed);
78
- if (trackingRemoved || selfReferentialRemoved) return currentParsed.toString();
70
+ if (deleteTrackingParams(currentParsed, trackingParams)) return currentParsed.toString();
79
71
  return currentUrl;
80
72
  };
81
73
  //#endregion
package/dist/index.d.mts CHANGED
@@ -81,8 +81,9 @@ import { unwrapViglink } from "./unwraps/viglink.mjs";
81
81
  import { unwrapVkAway } from "./unwraps/vkAway.mjs";
82
82
  import { unwrapWebArchive } from "./unwraps/webArchive.mjs";
83
83
  import { unwrapYahooSearch } from "./unwraps/yahooSearch.mjs";
84
+ import { unwrapYandexTranslate } from "./unwraps/yandexTranslate.mjs";
84
85
  import { unwrapYandexTurbo } from "./unwraps/yandexTurbo.mjs";
85
86
  import { unwrapYouTube } from "./unwraps/youtube.mjs";
86
87
  import { unwrapZhihu } from "./unwraps/zhihu.mjs";
87
88
  import { ParamExtractorConfig, createParamExtractor } from "./utils.mjs";
88
- export { type CleanUrlOptions, type ParamExtractorConfig, type TrackingParam, type UrlUnwrapper, cleanUrl, createParamExtractor, defaultTrackingParams, defaultUnwrappers, stripTrackingParams, trackingParamsLiterals, trackingParamsPatterns, unwrapAceml, unwrapAdjust, unwrapAmazonAffiliate, unwrapAmpCache, unwrapAnonymTo, unwrapAwin, unwrapBing, unwrapBlueskyRedirect, unwrapCjNetwork, unwrapCsdn, unwrapDeviantartOutgoing, unwrapDigidip, unwrapDisqus, unwrapDouban, unwrapDuckduckgo, unwrapEbayRover, unwrapEffiliation, unwrapEmbedly, unwrapFacebookShim, unwrapFeedsportal, unwrapFirebaseDynamicLinks, unwrapFlipboard, unwrapGateSc, unwrapGeoriot, unwrapGitee, unwrapGoogle, unwrapGoogleAmpViewer, unwrapGoogleNews, unwrapGoogleNewsModern, unwrapGoogleScholar, unwrapGoogleTranslate, unwrapHashnode, unwrapHrefLi, unwrapIcptrack, unwrapIdealoPartner, unwrapInstagramShim, unwrapJianshuGo, unwrapJuejin, unwrapLeverAnalytics, unwrapLinksynergy, unwrapMailchimp, unwrapMailpanion, unwrapMailpgn, unwrapMailtrack, unwrapMedium, unwrapMimecast, unwrapMozillaOutgoing, unwrapNarrativ, unwrapNaverOutgoing, unwrapNicoMs, unwrapOutlookSafelinks, unwrapPartnerAds, unwrapPocket, unwrapPostmark, unwrapProofpointV1, unwrapProofpointV2, unwrapProofpointV3, unwrapPxf, unwrapRecruitics, unwrapRedditOut, unwrapRedirectingat, unwrapSegmentfault, unwrapShareasale, unwrapSjv, unwrapSkimlinks, unwrapSlack, unwrapSmartredirect, unwrapSspai, unwrapSteamLinkfilter, unwrapTelegramIv, unwrapThreadsShim, unwrapTradedoubler, unwrapTumblr, unwrapUrl, unwrapValuecommerce, unwrapViglink, unwrapVkAway, unwrapWebArchive, unwrapYahooSearch, unwrapYandexTurbo, unwrapYouTube, unwrapZhihu };
89
+ export { type CleanUrlOptions, type ParamExtractorConfig, type TrackingParam, type UrlUnwrapper, cleanUrl, createParamExtractor, defaultTrackingParams, defaultUnwrappers, stripTrackingParams, trackingParamsLiterals, trackingParamsPatterns, unwrapAceml, unwrapAdjust, unwrapAmazonAffiliate, unwrapAmpCache, unwrapAnonymTo, unwrapAwin, unwrapBing, unwrapBlueskyRedirect, unwrapCjNetwork, unwrapCsdn, unwrapDeviantartOutgoing, unwrapDigidip, unwrapDisqus, unwrapDouban, unwrapDuckduckgo, unwrapEbayRover, unwrapEffiliation, unwrapEmbedly, unwrapFacebookShim, unwrapFeedsportal, unwrapFirebaseDynamicLinks, unwrapFlipboard, unwrapGateSc, unwrapGeoriot, unwrapGitee, unwrapGoogle, unwrapGoogleAmpViewer, unwrapGoogleNews, unwrapGoogleNewsModern, unwrapGoogleScholar, unwrapGoogleTranslate, unwrapHashnode, unwrapHrefLi, unwrapIcptrack, unwrapIdealoPartner, unwrapInstagramShim, unwrapJianshuGo, unwrapJuejin, unwrapLeverAnalytics, unwrapLinksynergy, unwrapMailchimp, unwrapMailpanion, unwrapMailpgn, unwrapMailtrack, unwrapMedium, unwrapMimecast, unwrapMozillaOutgoing, unwrapNarrativ, unwrapNaverOutgoing, unwrapNicoMs, unwrapOutlookSafelinks, unwrapPartnerAds, unwrapPocket, unwrapPostmark, unwrapProofpointV1, unwrapProofpointV2, unwrapProofpointV3, unwrapPxf, unwrapRecruitics, unwrapRedditOut, unwrapRedirectingat, unwrapSegmentfault, unwrapShareasale, unwrapSjv, unwrapSkimlinks, unwrapSlack, unwrapSmartredirect, unwrapSspai, unwrapSteamLinkfilter, unwrapTelegramIv, unwrapThreadsShim, unwrapTradedoubler, unwrapTumblr, unwrapUrl, unwrapValuecommerce, unwrapViglink, unwrapVkAway, unwrapWebArchive, unwrapYahooSearch, unwrapYandexTranslate, unwrapYandexTurbo, unwrapYouTube, unwrapZhihu };
package/dist/index.mjs CHANGED
@@ -83,5 +83,6 @@ import { unwrapTradedoubler } from "./unwraps/tradedoubler.mjs";
83
83
  import { unwrapValuecommerce } from "./unwraps/valuecommerce.mjs";
84
84
  import { unwrapViglink } from "./unwraps/viglink.mjs";
85
85
  import { unwrapWebArchive } from "./unwraps/webArchive.mjs";
86
+ import { unwrapYandexTranslate } from "./unwraps/yandexTranslate.mjs";
86
87
  import { unwrapYandexTurbo } from "./unwraps/yandexTurbo.mjs";
87
- export { cleanUrl, createParamExtractor, defaultTrackingParams, defaultUnwrappers, stripTrackingParams, trackingParamsLiterals, trackingParamsPatterns, unwrapAceml, unwrapAdjust, unwrapAmazonAffiliate, unwrapAmpCache, unwrapAnonymTo, unwrapAwin, unwrapBing, unwrapBlueskyRedirect, unwrapCjNetwork, unwrapCsdn, unwrapDeviantartOutgoing, unwrapDigidip, unwrapDisqus, unwrapDouban, unwrapDuckduckgo, unwrapEbayRover, unwrapEffiliation, unwrapEmbedly, unwrapFacebookShim, unwrapFeedsportal, unwrapFirebaseDynamicLinks, unwrapFlipboard, unwrapGateSc, unwrapGeoriot, unwrapGitee, unwrapGoogle, unwrapGoogleAmpViewer, unwrapGoogleNews, unwrapGoogleNewsModern, unwrapGoogleScholar, unwrapGoogleTranslate, unwrapHashnode, unwrapHrefLi, unwrapIcptrack, unwrapIdealoPartner, unwrapInstagramShim, unwrapJianshuGo, unwrapJuejin, unwrapLeverAnalytics, unwrapLinksynergy, unwrapMailchimp, unwrapMailpanion, unwrapMailpgn, unwrapMailtrack, unwrapMedium, unwrapMimecast, unwrapMozillaOutgoing, unwrapNarrativ, unwrapNaverOutgoing, unwrapNicoMs, unwrapOutlookSafelinks, unwrapPartnerAds, unwrapPocket, unwrapPostmark, unwrapProofpointV1, unwrapProofpointV2, unwrapProofpointV3, unwrapPxf, unwrapRecruitics, unwrapRedditOut, unwrapRedirectingat, unwrapSegmentfault, unwrapShareasale, unwrapSjv, unwrapSkimlinks, unwrapSlack, unwrapSmartredirect, unwrapSspai, unwrapSteamLinkfilter, unwrapTelegramIv, unwrapThreadsShim, unwrapTradedoubler, unwrapTumblr, unwrapUrl, unwrapValuecommerce, unwrapViglink, unwrapVkAway, unwrapWebArchive, unwrapYahooSearch, unwrapYandexTurbo, unwrapYouTube, unwrapZhihu };
88
+ export { cleanUrl, createParamExtractor, defaultTrackingParams, defaultUnwrappers, stripTrackingParams, trackingParamsLiterals, trackingParamsPatterns, unwrapAceml, unwrapAdjust, unwrapAmazonAffiliate, unwrapAmpCache, unwrapAnonymTo, unwrapAwin, unwrapBing, unwrapBlueskyRedirect, unwrapCjNetwork, unwrapCsdn, unwrapDeviantartOutgoing, unwrapDigidip, unwrapDisqus, unwrapDouban, unwrapDuckduckgo, unwrapEbayRover, unwrapEffiliation, unwrapEmbedly, unwrapFacebookShim, unwrapFeedsportal, unwrapFirebaseDynamicLinks, unwrapFlipboard, unwrapGateSc, unwrapGeoriot, unwrapGitee, unwrapGoogle, unwrapGoogleAmpViewer, unwrapGoogleNews, unwrapGoogleNewsModern, unwrapGoogleScholar, unwrapGoogleTranslate, unwrapHashnode, unwrapHrefLi, unwrapIcptrack, unwrapIdealoPartner, unwrapInstagramShim, unwrapJianshuGo, unwrapJuejin, unwrapLeverAnalytics, unwrapLinksynergy, unwrapMailchimp, unwrapMailpanion, unwrapMailpgn, unwrapMailtrack, unwrapMedium, unwrapMimecast, unwrapMozillaOutgoing, unwrapNarrativ, unwrapNaverOutgoing, unwrapNicoMs, unwrapOutlookSafelinks, unwrapPartnerAds, unwrapPocket, unwrapPostmark, unwrapProofpointV1, unwrapProofpointV2, unwrapProofpointV3, unwrapPxf, unwrapRecruitics, unwrapRedditOut, unwrapRedirectingat, unwrapSegmentfault, unwrapShareasale, unwrapSjv, unwrapSkimlinks, unwrapSlack, unwrapSmartredirect, unwrapSspai, unwrapSteamLinkfilter, unwrapTelegramIv, unwrapThreadsShim, unwrapTradedoubler, unwrapTumblr, unwrapUrl, unwrapValuecommerce, unwrapViglink, unwrapVkAway, unwrapWebArchive, unwrapYahooSearch, unwrapYandexTranslate, unwrapYandexTurbo, unwrapYouTube, unwrapZhihu };
@@ -0,0 +1,6 @@
1
+ //#region node_modules/trousse/dist/is.js
2
+ const isNullish = (value) => {
3
+ return value == null;
4
+ };
5
+ //#endregion
6
+ export { isNullish };
@@ -1,15 +1,21 @@
1
+ import { isNullish } from "./is.mjs";
1
2
  //#region node_modules/trousse/dist/matching.js
3
+ const testRegex = (regex, value) => {
4
+ regex.lastIndex = 0;
5
+ return regex.test(value);
6
+ };
2
7
  const isAnyOf = (value, patterns, parser) => {
3
- const parsedValue = parser ? parser(value) : value?.toLowerCase()?.trim();
4
- return patterns.some((pattern) => {
5
- if (pattern instanceof RegExp) return pattern.test(parsedValue);
6
- return parsedValue === pattern.toLowerCase().trim();
8
+ if (isNullish(value)) return false;
9
+ const parsedValue = parser ? parser(value) : value.toLowerCase().trim();
10
+ return (typeof patterns === "string" || patterns instanceof RegExp ? [patterns] : patterns).some((pattern) => {
11
+ if (pattern instanceof RegExp) return testRegex(pattern, parsedValue);
12
+ return parsedValue === (parser ? pattern : pattern.toLowerCase().trim());
7
13
  });
8
14
  };
9
15
  const endsWithAnyOf = (value, patterns) => {
10
16
  const lowerValue = value.toLowerCase();
11
17
  return patterns.some((pattern) => {
12
- if (pattern instanceof RegExp) return pattern.test(lowerValue);
18
+ if (pattern instanceof RegExp) return testRegex(pattern, lowerValue);
13
19
  return pattern && lowerValue.endsWith(pattern.toLowerCase());
14
20
  });
15
21
  };
@@ -1,20 +1,57 @@
1
1
  import { endsWithAnyOf, isAnyOf } from "./matching.mjs";
2
2
  //#region node_modules/trousse/dist/urls.js
3
+ const httpProtocols = ["http:", "https:"];
3
4
  const parseUrl = (value, base) => {
4
5
  if (value instanceof URL && base === void 0) return value;
6
+ if (URL.parse) return URL.parse(value, base) ?? void 0;
5
7
  try {
6
8
  return new URL(value, base);
7
9
  } catch {}
8
10
  };
9
- const isHostOf = (url, hosts) => {
11
+ const getHostname = (url) => {
10
12
  const hostname = parseUrl(url)?.hostname;
13
+ if (!hostname) return;
14
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
15
+ };
16
+ const nonAsciiRegex = /[\u0080-\uffff]/;
17
+ const normalizeDomain = (domain) => {
18
+ const trimmed = domain.trim();
19
+ const bare = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
20
+ if (!nonAsciiRegex.test(bare)) return bare;
21
+ return parseUrl(`http://${bare}`)?.hostname ?? bare;
22
+ };
23
+ const isHostOf = (url, hosts) => {
24
+ const hostname = getHostname(url);
11
25
  if (!hostname) return false;
12
- return isAnyOf(hostname, typeof hosts === "string" ? [hosts] : hosts);
26
+ return isAnyOf(hostname, (typeof hosts === "string" ? [hosts] : hosts).map(normalizeDomain));
13
27
  };
14
28
  const isSubdomainOf = (url, domains) => {
15
- const hostname = parseUrl(url)?.hostname;
29
+ const hostname = getHostname(url);
16
30
  if (!hostname) return false;
17
- return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${domain}`));
31
+ return endsWithAnyOf(hostname, (typeof domains === "string" ? [domains] : domains).map((domain) => `.${normalizeDomain(domain)}`));
32
+ };
33
+ const isHttpUrl = (url) => {
34
+ const protocol = parseUrl(url)?.protocol;
35
+ if (!protocol) return false;
36
+ return httpProtocols.includes(protocol);
37
+ };
38
+ const isHostOrSubdomainOf = (url, domains) => {
39
+ return isHostOf(url, domains) || isSubdomainOf(url, domains);
40
+ };
41
+ const decodeSegment = (segment) => {
42
+ if (segment === void 0) return;
43
+ try {
44
+ return decodeURIComponent(segment);
45
+ } catch {}
46
+ };
47
+ const wwwPrefixRegex = /^www\./;
48
+ const schemeSeparator = String.raw`\.*[:\s=\\/][:\s=.\\/]*`;
49
+ const leadingSchemeSeparator = String.raw`\.*(?::[:\s=.\\/]*|[\s=\\/][:\s=.\\/]+)`;
50
+ String.raw`^\/?[htps]{2,7}${leadingSchemeSeparator}([htps]{2,7})(?!:\d)${schemeSeparator}[.,:/]*(www[./]+)?`;
51
+ String.raw`^\/?(?:h[htps():]{1,10}|t{1,2}ps?)${leadingSchemeSeparator}[.,:/]*(www[./]+)?`;
52
+ const stripWww = (hostname) => {
53
+ const stripped = hostname.replace(wwwPrefixRegex, "");
54
+ return stripped.includes(".") ? stripped : hostname;
18
55
  };
19
56
  //#endregion
20
- export { isHostOf, isSubdomainOf, parseUrl };
57
+ export { decodeSegment, isHostOf, isHostOrSubdomainOf, isHttpUrl, isSubdomainOf, parseUrl, stripWww };
@@ -73,7 +73,6 @@ const trackingParamsLiterals = [
73
73
  "__hstc",
74
74
  "__hsfp",
75
75
  "__hssc",
76
- "cid",
77
76
  "s_kwcid",
78
77
  "sc_cid",
79
78
  "ef_id",
@@ -1,3 +1,4 @@
1
+ import { isHttpUrl } from "../node_modules/trousse/dist/urls.mjs";
1
2
  import { createParamExtractor, decodeBase64 } from "../utils.mjs";
2
3
  //#region src/unwraps/aceml.ts
3
4
  const baseExtractor = createParamExtractor({
@@ -9,7 +10,7 @@ const unwrapAceml = (url) => {
9
10
  const raw = baseExtractor(url);
10
11
  if (!raw) return;
11
12
  const decoded = decodeBase64(raw);
12
- if (decoded && (decoded.startsWith("http://") || decoded.startsWith("https://"))) return decoded;
13
+ if (decoded && isHttpUrl(decoded)) return decoded;
13
14
  };
14
15
  //#endregion
15
16
  export { unwrapAceml };
@@ -1,8 +1,9 @@
1
+ import { isHostOf, isHttpUrl } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/adjust.ts
2
3
  const unwrapAdjust = (url) => {
3
- if (url.hostname !== "app.adjust.com") return;
4
+ if (!isHostOf(url, "app.adjust.com")) return;
4
5
  const target = url.searchParams.get("redirect");
5
- if (target?.startsWith("https://") !== true && target?.startsWith("http://") !== true) return;
6
+ if (!target || !isHttpUrl(target)) return;
6
7
  return target;
7
8
  };
8
9
  //#endregion
@@ -3,7 +3,9 @@ const amazonHostRegex = /\.amazon-adsystem\.com$/;
3
3
  const amazonPathRegex = /^\/x\/c\/[^/]+\/(https?:\/\/.+)$/;
4
4
  const unwrapAmazonAffiliate = (url) => {
5
5
  if (!amazonHostRegex.test(url.hostname)) return;
6
- return url.pathname.match(amazonPathRegex)?.[1];
6
+ const match = url.pathname.match(amazonPathRegex);
7
+ if (!match) return;
8
+ return `${match[1]}${url.search}${url.hash}`;
7
9
  };
8
10
  //#endregion
9
11
  export { unwrapAmazonAffiliate };
@@ -1,13 +1,13 @@
1
- import { isHostOf, isSubdomainOf } from "../node_modules/trousse/dist/urls.mjs";
1
+ import { isHostOrSubdomainOf } from "../node_modules/trousse/dist/urls.mjs";
2
2
  //#region src/unwraps/ampCache.ts
3
3
  const httpsPathRegex = /^\/c\/s\/(.+)$/;
4
4
  const httpPathRegex = /^\/c\/(?!s\/)(.+)$/;
5
5
  const unwrapAmpCache = (url) => {
6
- if (!isHostOf(url, "cdn.ampproject.org") && !isSubdomainOf(url, "cdn.ampproject.org")) return;
6
+ if (!isHostOrSubdomainOf(url, "cdn.ampproject.org")) return;
7
7
  const httpsMatch = url.pathname.match(httpsPathRegex);
8
- if (httpsMatch) return `https://${httpsMatch[1]}`;
8
+ if (httpsMatch) return `https://${httpsMatch[1]}${url.search}${url.hash}`;
9
9
  const httpMatch = url.pathname.match(httpPathRegex);
10
- if (httpMatch) return `http://${httpMatch[1]}`;
10
+ if (httpMatch) return `http://${httpMatch[1]}${url.search}${url.hash}`;
11
11
  };
12
12
  //#endregion
13
13
  export { unwrapAmpCache };
@@ -1,7 +1,10 @@
1
+ import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/anonymTo.ts
2
3
  const unwrapAnonymTo = (url) => {
3
- if (url.hostname !== "anonym.to" && url.hostname !== "www.anonym.to") return;
4
- return url.search.slice(1) || void 0;
4
+ if (!isHostOf(url, ["anonym.to", "www.anonym.to"])) return;
5
+ const target = url.search.slice(1);
6
+ if (!target) return;
7
+ return `${target}${url.hash}`;
5
8
  };
6
9
  //#endregion
7
10
  export { unwrapAnonymTo };
@@ -1,3 +1,4 @@
1
+ import { isHttpUrl } from "../node_modules/trousse/dist/urls.mjs";
1
2
  import { decodeBase64Url } from "../utils.mjs";
2
3
  //#region src/unwraps/bing.ts
3
4
  const bingHostRegex = /^(?:www\.|cn\.)?bing\.com$/;
@@ -7,7 +8,7 @@ const unwrapBing = (url) => {
7
8
  const value = url.searchParams.get("u");
8
9
  if (!value || !bingPrefixRegex.test(value)) return;
9
10
  const decoded = decodeBase64Url(value.slice(2));
10
- if (!decoded || !decoded.startsWith("https://") && !decoded.startsWith("http://")) return;
11
+ if (!decoded || !isHttpUrl(decoded)) return;
11
12
  return decoded;
12
13
  };
13
14
  //#endregion
@@ -1,7 +1,10 @@
1
+ import { isHostOrSubdomainOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/deviantartOutgoing.ts
2
3
  const unwrapDeviantartOutgoing = (url) => {
3
- if (!url.hostname.endsWith("deviantart.com") || !url.pathname.endsWith("/outgoing")) return;
4
- return url.search.slice(1) || void 0;
4
+ if (!isHostOrSubdomainOf(url, "deviantart.com") || !url.pathname.endsWith("/outgoing")) return;
5
+ const target = url.search.slice(1);
6
+ if (!target) return;
7
+ return `${target}${url.hash}`;
5
8
  };
6
9
  //#endregion
7
10
  export { unwrapDeviantartOutgoing };
@@ -1,3 +1,4 @@
1
+ import { isHttpUrl } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/feedsportal.ts
2
3
  const feedsportalPathRegex = /\/([0-9A-Za-z]{20,})\/story01\.htm$/;
3
4
  const feedsportalAlphabet = {
@@ -37,7 +38,7 @@ const unwrapFeedsportal = (url) => {
37
38
  const substitution = head ? feedsportalAlphabet[head] : void 0;
38
39
  return substitution === void 0 ? `0${chunk}` : substitution + chunk.slice(1);
39
40
  }).join("");
40
- if (!decoded.startsWith("http://") && !decoded.startsWith("https://")) return;
41
+ if (!isHttpUrl(decoded)) return;
41
42
  return decoded;
42
43
  };
43
44
  //#endregion
@@ -5,9 +5,9 @@ const httpPathRegex = /^\/amp\/(?!s\/)(.+)$/;
5
5
  const unwrapGoogleAmpViewer = (url) => {
6
6
  if (!googleAmpHostRegex.test(url.hostname)) return;
7
7
  const httpsMatch = url.pathname.match(httpsPathRegex);
8
- if (httpsMatch) return `https://${httpsMatch[1]}`;
8
+ if (httpsMatch) return `https://${httpsMatch[1]}${url.search}`;
9
9
  const httpMatch = url.pathname.match(httpPathRegex);
10
- if (httpMatch) return `http://${httpMatch[1]}`;
10
+ if (httpMatch) return `http://${httpMatch[1]}${url.search}`;
11
11
  };
12
12
  //#endregion
13
13
  export { unwrapGoogleAmpViewer };
@@ -1,16 +1,13 @@
1
1
  import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
2
- import { decodeBase64Binary } from "../utils.mjs";
2
+ import { decodeBase64UrlBinary } from "../utils.mjs";
3
3
  //#region src/unwraps/googleNewsModern.ts
4
4
  const articleIdRegex = /^\/(?:rss\/)?articles\/([\w-]+)/;
5
- const base64UrlMinusRegex = /-/g;
6
- const base64UrlUnderscoreRegex = /_/g;
7
5
  const protobufFramingRegex = /\x08\x13".+?(https?:\/\/[^\xd2]+)\xd2\x01/;
8
6
  const unwrapGoogleNewsModern = (url) => {
9
7
  if (!isHostOf(url, "news.google.com")) return;
10
8
  const match = url.pathname.match(articleIdRegex);
11
9
  if (!match) return;
12
- const padded = match[1].replace(base64UrlMinusRegex, "+").replace(base64UrlUnderscoreRegex, "/");
13
- return (decodeBase64Binary(padded)?.match(protobufFramingRegex))?.[1];
10
+ return (decodeBase64UrlBinary(match[1])?.match(protobufFramingRegex))?.[1];
14
11
  };
15
12
  //#endregion
16
13
  export { unwrapGoogleNewsModern };
@@ -1,7 +1,10 @@
1
+ import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/hrefLi.ts
2
3
  const unwrapHrefLi = (url) => {
3
- if (url.hostname !== "href.li" && url.hostname !== "www.href.li") return;
4
- return url.search.slice(1) || void 0;
4
+ if (!isHostOf(url, ["href.li", "www.href.li"])) return;
5
+ const target = url.search.slice(1);
6
+ if (!target) return;
7
+ return `${target}${url.hash}`;
5
8
  };
6
9
  //#endregion
7
10
  export { unwrapHrefLi };
@@ -1,13 +1,12 @@
1
+ import { decodeSegment, isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/mozillaOutgoing.ts
2
3
  const pathRegex = /^\/v1\/[0-9a-f]{64}\/(.+)$/;
3
- const mozillaOutgoingHosts = /* @__PURE__ */ new Set(["outgoing.prod.mozaws.net", "prod.outgoing.prod.webservices.mozgcp.net"]);
4
+ const mozillaOutgoingHosts = ["outgoing.prod.mozaws.net", "prod.outgoing.prod.webservices.mozgcp.net"];
4
5
  const unwrapMozillaOutgoing = (url) => {
5
- if (!mozillaOutgoingHosts.has(url.hostname)) return;
6
+ if (!isHostOf(url, mozillaOutgoingHosts)) return;
6
7
  const match = url.pathname.match(pathRegex);
7
8
  if (!match) return;
8
- try {
9
- return decodeURIComponent(match[1]);
10
- } catch {}
9
+ return decodeSegment(match[1]);
11
10
  };
12
11
  //#endregion
13
12
  export { unwrapMozillaOutgoing };
@@ -1,7 +1,8 @@
1
+ import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/nicoMs.ts
2
3
  const nicoMsRegex = /^\/((?:sm|nm|so|im)\w+)$/;
3
4
  const unwrapNicoMs = (url) => {
4
- if (url.hostname !== "nico.ms") return;
5
+ if (!isHostOf(url, "nico.ms")) return;
5
6
  const match = url.pathname.match(nicoMsRegex);
6
7
  if (!match) return;
7
8
  const id = match[1];
@@ -1,12 +1,11 @@
1
+ import { decodeSegment, isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/postmark.ts
2
3
  const postmarkPathRegex = /^\/[23][st]\/([^/]+)\//;
3
4
  const unwrapPostmark = (url) => {
4
- if (url.hostname !== "click.pstmrk.it") return;
5
+ if (!isHostOf(url, "click.pstmrk.it")) return;
5
6
  const match = url.pathname.match(postmarkPathRegex);
6
7
  if (!match) return;
7
- try {
8
- return decodeURIComponent(match[1]);
9
- } catch {}
8
+ return decodeSegment(match[1]);
10
9
  };
11
10
  //#endregion
12
11
  export { unwrapPostmark };
@@ -1,16 +1,18 @@
1
+ import { decodeSegment } from "../node_modules/trousse/dist/urls.mjs";
1
2
  import { createParamExtractor } from "../utils.mjs";
2
3
  //#region src/unwraps/proofpointV1.ts
3
- const baseExtractor = createParamExtractor({
4
- hosts: "urldefense.proofpoint.com",
5
- path: "/v1/url",
6
- params: ["u"]
7
- });
8
- const unwrapProofpointV1 = (url) => {
9
- const raw = baseExtractor(url);
10
- if (!raw) return;
11
- try {
12
- return decodeURIComponent(raw.replace(/-/g, "%").replace(/_/g, "/"));
13
- } catch {}
4
+ const createProofpointUnwrapper = (path) => {
5
+ const baseExtractor = createParamExtractor({
6
+ hosts: "urldefense.proofpoint.com",
7
+ path,
8
+ params: ["u"]
9
+ });
10
+ return (url) => {
11
+ const raw = baseExtractor(url);
12
+ if (!raw) return;
13
+ return decodeSegment(raw.replace(/-/g, "%").replace(/_/g, "/"));
14
+ };
14
15
  };
16
+ const unwrapProofpointV1 = createProofpointUnwrapper("/v1/url");
15
17
  //#endregion
16
- export { unwrapProofpointV1 };
18
+ export { createProofpointUnwrapper, unwrapProofpointV1 };
@@ -1,16 +1,5 @@
1
- import { createParamExtractor } from "../utils.mjs";
1
+ import { createProofpointUnwrapper } from "./proofpointV1.mjs";
2
2
  //#region src/unwraps/proofpointV2.ts
3
- const baseExtractor = createParamExtractor({
4
- hosts: "urldefense.proofpoint.com",
5
- path: "/v2/url",
6
- params: ["u"]
7
- });
8
- const unwrapProofpointV2 = (url) => {
9
- const raw = baseExtractor(url);
10
- if (!raw) return;
11
- try {
12
- return decodeURIComponent(raw.replace(/-/g, "%").replace(/_/g, "/"));
13
- } catch {}
14
- };
3
+ const unwrapProofpointV2 = createProofpointUnwrapper("/v2/url");
15
4
  //#endregion
16
5
  export { unwrapProofpointV2 };
@@ -1,17 +1,13 @@
1
+ import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  import { decodeBase64Url, getUtf8ByteLength } from "../utils.mjs";
2
3
  //#region src/unwraps/proofpointV3.ts
3
4
  const v3PathRegex = /^\/v3\/__(.+)__;([^!]*)!/;
4
- const v3HostSet = /* @__PURE__ */ new Set([
5
+ const v3Hosts = [
5
6
  "urldefense.com",
6
7
  "urldefense.proofpoint.com",
7
8
  "urldefense.us"
8
- ]);
9
- const runLengthMap = (() => {
10
- const map = {};
11
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
12
- for (let index = 0; index < 64; index += 1) map[alphabet[index]] = index + 2;
13
- return map;
14
- })();
9
+ ];
10
+ const runLengthAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
15
11
  const decodeReplacements = (mangled, replacementBase64) => {
16
12
  if (replacementBase64.length === 0) return mangled;
17
13
  const replacement = decodeBase64Url(replacementBase64);
@@ -19,11 +15,11 @@ const decodeReplacements = (mangled, replacementBase64) => {
19
15
  const replacementChars = Array.from(replacement);
20
16
  const urlChars = Array.from(mangled);
21
17
  const result = [];
22
- const matcher = /(?<!\*)\*(?!\*)|\*{2}[A-Za-z0-9\-_]/g;
18
+ const markerRegex = /(?<!\*)\*(?!\*)|\*{2}[A-Za-z0-9\-_]/g;
23
19
  let lastIndex = 0;
24
20
  let savedBytes = 0;
25
21
  let match;
26
- match = matcher.exec(mangled);
22
+ match = markerRegex.exec(mangled);
27
23
  while (match !== null) {
28
24
  const startChar = positionInChars(mangled, match.index);
29
25
  const endChar = positionInChars(mangled, match.index + match[0].length);
@@ -33,7 +29,7 @@ const decodeReplacements = (mangled, replacementBase64) => {
33
29
  if (next === void 0) return;
34
30
  result.push(next);
35
31
  } else {
36
- let bytesToReplace = runLengthMap[match[0][2]];
32
+ let bytesToReplace = runLengthAlphabet.indexOf(match[0][2]) + 2;
37
33
  if (savedBytes !== 0) {
38
34
  bytesToReplace += savedBytes;
39
35
  savedBytes = 0;
@@ -53,7 +49,7 @@ const decodeReplacements = (mangled, replacementBase64) => {
53
49
  }
54
50
  }
55
51
  lastIndex = endChar;
56
- match = matcher.exec(mangled);
52
+ match = markerRegex.exec(mangled);
57
53
  }
58
54
  for (let charIndex = lastIndex; charIndex < urlChars.length; charIndex += 1) result.push(urlChars[charIndex]);
59
55
  return result.join("");
@@ -69,7 +65,7 @@ const positionInChars = (source, codeUnitIndex) => {
69
65
  return charIndex;
70
66
  };
71
67
  const unwrapProofpointV3 = (url) => {
72
- if (!v3HostSet.has(url.hostname)) return;
68
+ if (!isHostOf(url, v3Hosts)) return;
73
69
  const match = `${url.pathname}${url.search}`.match(v3PathRegex);
74
70
  if (!match) return;
75
71
  return decodeReplacements(match[1], match[2]);
@@ -1,3 +1,4 @@
1
+ import { isHttpUrl } from "../node_modules/trousse/dist/urls.mjs";
1
2
  import { createParamExtractor, decodeBase64 } from "../utils.mjs";
2
3
  //#region src/unwraps/segmentfault.ts
3
4
  const baseExtractor = createParamExtractor({
@@ -8,7 +9,7 @@ const unwrapSegmentfault = (url) => {
8
9
  const raw = baseExtractor(url);
9
10
  if (!raw) return;
10
11
  const decoded = decodeBase64(raw);
11
- if (decoded && (decoded.startsWith("http://") || decoded.startsWith("https://"))) return decoded;
12
+ if (decoded && isHttpUrl(decoded)) return decoded;
12
13
  };
13
14
  //#endregion
14
15
  export { unwrapSegmentfault };
@@ -1,12 +1,13 @@
1
+ import { decodeSegment, isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/webArchive.ts
2
3
  const pathRegex = /^\/web\/\d{14}\*?\/(.+)$/;
3
4
  const unwrapWebArchive = (url) => {
4
- if (url.hostname !== "web.archive.org") return;
5
+ if (!isHostOf(url, "web.archive.org")) return;
5
6
  const match = url.pathname.match(pathRegex);
6
7
  if (!match) return;
7
- try {
8
- return decodeURIComponent(match[1]);
9
- } catch {}
8
+ const target = decodeSegment(match[1]);
9
+ if (!target) return;
10
+ return `${target}${url.search}${url.hash}`;
10
11
  };
11
12
  //#endregion
12
13
  export { unwrapWebArchive };
@@ -1,12 +1,11 @@
1
+ import { decodeSegment, isHostOf } from "../node_modules/trousse/dist/urls.mjs";
1
2
  //#region src/unwraps/yahooSearch.ts
2
3
  const yahooPathRegex = /\/RU=([^/]+)\/RK=/;
3
4
  const unwrapYahooSearch = (url) => {
4
- if (url.hostname !== "r.search.yahoo.com") return;
5
+ if (!isHostOf(url, "r.search.yahoo.com")) return;
5
6
  const match = url.pathname.match(yahooPathRegex);
6
7
  if (!match) return;
7
- try {
8
- return decodeURIComponent(match[1]);
9
- } catch {}
8
+ return decodeSegment(match[1]);
10
9
  };
11
10
  //#endregion
12
11
  export { unwrapYahooSearch };
@@ -0,0 +1,4 @@
1
+ import { UrlUnwrapper } from "../types.mjs";
2
+ //#region src/unwraps/yandexTranslate.d.ts
3
+ export declare const unwrapYandexTranslate: UrlUnwrapper;
4
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { isHostOf } from "../node_modules/trousse/dist/urls.mjs";
2
+ //#region src/unwraps/yandexTranslate.ts
3
+ const proxyPathRegex = /^\/proxy_u\/[^/]+\/(?:(https?)\/)?(.+)$/;
4
+ const unwrapYandexTranslate = (url) => {
5
+ if (!isHostOf(url, "translated.turbopages.org")) return;
6
+ const match = url.pathname.match(proxyPathRegex);
7
+ if (!match) return;
8
+ return `${match[1] ?? "http"}://${match[2]}${url.search}${url.hash}`;
9
+ };
10
+ //#endregion
11
+ export { unwrapYandexTranslate };
package/dist/utils.mjs CHANGED
@@ -1,13 +1,8 @@
1
+ import { isAnyOf } from "./node_modules/trousse/dist/matching.mjs";
1
2
  //#region src/utils.ts
2
- const createHostMatcher = (hosts) => {
3
- if (hosts instanceof RegExp) return (host) => hosts.test(host);
4
- const hostSet = new Set(Array.isArray(hosts) ? hosts : [hosts]);
5
- return (host) => hostSet.has(host);
6
- };
7
3
  const createParamExtractor = (config) => {
8
- const matchesHost = createHostMatcher(config.hosts);
9
4
  return (url) => {
10
- if (!matchesHost(url.hostname)) return;
5
+ if (!isAnyOf(url.hostname, config.hosts)) return;
11
6
  if (config.path && url.pathname !== config.path) return;
12
7
  for (const param of config.params) {
13
8
  const value = url.searchParams.get(param);
@@ -28,11 +23,18 @@ const decodeBase64 = (value) => {
28
23
  for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
29
24
  return utf8Decoder.decode(bytes);
30
25
  };
26
+ const toBase64 = (base64Url) => {
27
+ return base64Url.replace(/-/g, "+").replace(/_/g, "/");
28
+ };
31
29
  const decodeBase64Url = (value) => {
32
- return decodeBase64(value.replace(/-/g, "+").replace(/_/g, "/"));
30
+ return decodeBase64(toBase64(value));
31
+ };
32
+ const decodeBase64UrlBinary = (value) => {
33
+ return decodeBase64Binary(toBase64(value));
33
34
  };
35
+ const utf8Encoder = new TextEncoder();
34
36
  const getUtf8ByteLength = (value) => {
35
- return new TextEncoder().encode(value).length;
37
+ return utf8Encoder.encode(value).length;
36
38
  };
37
39
  //#endregion
38
- export { createParamExtractor, decodeBase64, decodeBase64Binary, decodeBase64Url, getUtf8ByteLength };
40
+ export { createParamExtractor, decodeBase64, decodeBase64Binary, decodeBase64Url, decodeBase64UrlBinary, getUtf8ByteLength };
package/package.json CHANGED
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/bun": "^1.4.2",
44
- "kvalita": "^1.23.0",
45
- "trousse": "^2.2.1",
44
+ "kvalita": "^1.24.3",
45
+ "trousse": "^2.6.1",
46
46
  "tsdown": "^0.23.0"
47
47
  },
48
- "version": "1.4.1"
48
+ "version": "1.5.0"
49
49
  }