feedcanon 1.0.0 → 1.1.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
@@ -36,11 +36,14 @@ The 9 URLs below all work and return identical content. None redirect to each ot
36
36
 
37
37
  ### How It Works
38
38
 
39
+ This is a simplified flow. For complete details, see [How It Works](https://feedcanon.dev/how-it-works) in the docs.
40
+
39
41
  1. Fetch the input URL and parse the feed to establish reference content.
40
- 2. Extract the feed's declared self URL and validate it serves identical content.
41
- 3. Generate URL variants ordered from cleanest to least clean.
42
- 4. Test variants in order—the first one serving identical content wins.
43
- 5. Upgrade HTTP to HTTPS if both serve identical content.
42
+ 2. Extract the feed's declared self URL (if present).
43
+ 3. Validate the self URL by fetching and comparing content.
44
+ 4. Generate URL variants ordered from cleanest to least clean.
45
+ 5. Test variants in order—the first one serving identical content wins.
46
+ 6. Upgrade HTTP to HTTPS if both serve identical content.
44
47
 
45
48
  ### Customization
46
49
 
package/dist/defaults.cjs CHANGED
@@ -185,6 +185,13 @@ const defaultFetch = async (url, options) => {
185
185
  status: response.status
186
186
  };
187
187
  };
188
+ const findSelfLink = (parsed) => {
189
+ switch (parsed.format) {
190
+ case "atom": return parsed.feed.links?.find((link) => link.rel === "self");
191
+ case "rss":
192
+ case "rdf": return parsed.feed.atom?.links?.find((link) => link.rel === "self");
193
+ }
194
+ };
188
195
  const defaultParser = {
189
196
  parse: (body) => {
190
197
  try {
@@ -192,15 +199,36 @@ const defaultParser = {
192
199
  } catch {}
193
200
  },
194
201
  getSelfUrl: (parsed) => {
195
- switch (parsed.format) {
196
- case "atom": return parsed.feed.links?.find((link) => link.rel === "self")?.href;
197
- case "rss":
198
- case "rdf": return parsed.feed.atom?.links?.find((link) => link.rel === "self")?.href;
199
- case "json": return parsed.feed.feed_url;
200
- }
202
+ return parsed.format === "json" ? parsed.feed.feed_url : findSelfLink(parsed)?.href;
201
203
  },
202
204
  getSignature: (parsed) => {
203
- return parsed.feed;
205
+ if (parsed.format === "json") {
206
+ const originalSelfUrl = parsed.feed.feed_url;
207
+ parsed.feed.feed_url = void 0;
208
+ const signature$1 = JSON.stringify(parsed.feed);
209
+ parsed.feed.feed_url = originalSelfUrl;
210
+ return signature$1;
211
+ }
212
+ let signature;
213
+ let originalBuildDate;
214
+ if (parsed.format === "rss") {
215
+ originalBuildDate = parsed.feed.lastBuildDate;
216
+ parsed.feed.lastBuildDate = void 0;
217
+ } else if (parsed.format === "atom") {
218
+ originalBuildDate = parsed.feed.updated;
219
+ parsed.feed.updated = void 0;
220
+ }
221
+ const link = findSelfLink(parsed);
222
+ if (!link) signature = JSON.stringify(parsed.feed);
223
+ else {
224
+ const originalSelfUrl = link.href;
225
+ link.href = void 0;
226
+ signature = JSON.stringify(parsed.feed);
227
+ link.href = originalSelfUrl;
228
+ }
229
+ if (parsed.format === "rss") parsed.feed.lastBuildDate = originalBuildDate;
230
+ else if (parsed.format === "atom") parsed.feed.updated = originalBuildDate;
231
+ return signature;
204
232
  }
205
233
  };
206
234
  const defaultTiers = [
@@ -1,11 +1,11 @@
1
- import { FeedsmithFeed, FetchFn, NormalizeOptions, ParserAdapter, PlatformHandler, Tier } from "./types.cjs";
1
+ import { DefaultParserResult, FetchFn, NormalizeOptions, ParserAdapter, PlatformHandler, Tier } from "./types.cjs";
2
2
 
3
3
  //#region src/defaults.d.ts
4
4
  declare const defaultPlatforms: Array<PlatformHandler>;
5
5
  declare const defaultStrippedParams: string[];
6
6
  declare const defaultNormalizeOptions: NormalizeOptions;
7
7
  declare const defaultFetch: FetchFn;
8
- declare const defaultParser: ParserAdapter<FeedsmithFeed>;
8
+ declare const defaultParser: ParserAdapter<DefaultParserResult>;
9
9
  declare const defaultTiers: Array<Tier>;
10
10
  //#endregion
11
11
  export { defaultFetch, defaultNormalizeOptions, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers };
@@ -1,11 +1,11 @@
1
- import { FeedsmithFeed, FetchFn, NormalizeOptions, ParserAdapter, PlatformHandler, Tier } from "./types.js";
1
+ import { DefaultParserResult, FetchFn, NormalizeOptions, ParserAdapter, PlatformHandler, Tier } from "./types.js";
2
2
 
3
3
  //#region src/defaults.d.ts
4
4
  declare const defaultPlatforms: Array<PlatformHandler>;
5
5
  declare const defaultStrippedParams: string[];
6
6
  declare const defaultNormalizeOptions: NormalizeOptions;
7
7
  declare const defaultFetch: FetchFn;
8
- declare const defaultParser: ParserAdapter<FeedsmithFeed>;
8
+ declare const defaultParser: ParserAdapter<DefaultParserResult>;
9
9
  declare const defaultTiers: Array<Tier>;
10
10
  //#endregion
11
11
  export { defaultFetch, defaultNormalizeOptions, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers };
package/dist/defaults.js CHANGED
@@ -185,6 +185,13 @@ const defaultFetch = async (url, options) => {
185
185
  status: response.status
186
186
  };
187
187
  };
188
+ const findSelfLink = (parsed) => {
189
+ switch (parsed.format) {
190
+ case "atom": return parsed.feed.links?.find((link) => link.rel === "self");
191
+ case "rss":
192
+ case "rdf": return parsed.feed.atom?.links?.find((link) => link.rel === "self");
193
+ }
194
+ };
188
195
  const defaultParser = {
189
196
  parse: (body) => {
190
197
  try {
@@ -192,15 +199,36 @@ const defaultParser = {
192
199
  } catch {}
193
200
  },
194
201
  getSelfUrl: (parsed) => {
195
- switch (parsed.format) {
196
- case "atom": return parsed.feed.links?.find((link) => link.rel === "self")?.href;
197
- case "rss":
198
- case "rdf": return parsed.feed.atom?.links?.find((link) => link.rel === "self")?.href;
199
- case "json": return parsed.feed.feed_url;
200
- }
202
+ return parsed.format === "json" ? parsed.feed.feed_url : findSelfLink(parsed)?.href;
201
203
  },
202
204
  getSignature: (parsed) => {
203
- return parsed.feed;
205
+ if (parsed.format === "json") {
206
+ const originalSelfUrl = parsed.feed.feed_url;
207
+ parsed.feed.feed_url = void 0;
208
+ const signature$1 = JSON.stringify(parsed.feed);
209
+ parsed.feed.feed_url = originalSelfUrl;
210
+ return signature$1;
211
+ }
212
+ let signature;
213
+ let originalBuildDate;
214
+ if (parsed.format === "rss") {
215
+ originalBuildDate = parsed.feed.lastBuildDate;
216
+ parsed.feed.lastBuildDate = void 0;
217
+ } else if (parsed.format === "atom") {
218
+ originalBuildDate = parsed.feed.updated;
219
+ parsed.feed.updated = void 0;
220
+ }
221
+ const link = findSelfLink(parsed);
222
+ if (!link) signature = JSON.stringify(parsed.feed);
223
+ else {
224
+ const originalSelfUrl = link.href;
225
+ link.href = void 0;
226
+ signature = JSON.stringify(parsed.feed);
227
+ link.href = originalSelfUrl;
228
+ }
229
+ if (parsed.format === "rss") parsed.feed.lastBuildDate = originalBuildDate;
230
+ else if (parsed.format === "atom") parsed.feed.updated = originalBuildDate;
231
+ return signature;
204
232
  }
205
233
  };
206
234
  const defaultTiers = [
package/dist/exports.cjs CHANGED
@@ -11,6 +11,7 @@ exports.defaultStrippedParams = require_defaults.defaultStrippedParams;
11
11
  exports.defaultTiers = require_defaults.defaultTiers;
12
12
  exports.feedburnerHandler = require_feedburner.feedburnerHandler;
13
13
  exports.findCanonical = require_index.findCanonical;
14
+ exports.fixMalformedProtocol = require_utils.fixMalformedProtocol;
14
15
  exports.normalizeUrl = require_utils.normalizeUrl;
15
16
  exports.resolveFeedProtocol = require_utils.resolveFeedProtocol;
16
17
  exports.resolveUrl = require_utils.resolveUrl;
@@ -1,6 +1,6 @@
1
- import { ExistsFn, FeedsmithFeed, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler } from "./types.cjs";
1
+ import { DefaultParserResult, ExistsFn, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler } from "./types.cjs";
2
2
  import { defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers } from "./defaults.cjs";
3
3
  import { findCanonical } from "./index.cjs";
4
4
  import { feedburnerHandler } from "./platforms/feedburner.cjs";
5
- import { addMissingProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.cjs";
6
- export { type ExistsFn, type FeedsmithFeed, type FetchFn, type FetchFnOptions, type FetchFnResponse, type FindCanonicalOptions, type NormalizeOptions, type OnExistsFn, type OnFetchFn, type OnMatchFn, type ParserAdapter, type PlatformHandler, addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, normalizeUrl, resolveFeedProtocol, resolveUrl };
5
+ import { addMissingProtocol, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.cjs";
6
+ export { type DefaultParserResult, type ExistsFn, type FetchFn, type FetchFnOptions, type FetchFnResponse, type FindCanonicalOptions, type NormalizeOptions, type OnExistsFn, type OnFetchFn, type OnMatchFn, type ParserAdapter, type PlatformHandler, addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/dist/exports.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { ExistsFn, FeedsmithFeed, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler } from "./types.js";
1
+ import { DefaultParserResult, ExistsFn, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler } from "./types.js";
2
2
  import { defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers } from "./defaults.js";
3
3
  import { findCanonical } from "./index.js";
4
4
  import { feedburnerHandler } from "./platforms/feedburner.js";
5
- import { addMissingProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.js";
6
- export { type ExistsFn, type FeedsmithFeed, type FetchFn, type FetchFnOptions, type FetchFnResponse, type FindCanonicalOptions, type NormalizeOptions, type OnExistsFn, type OnFetchFn, type OnMatchFn, type ParserAdapter, type PlatformHandler, addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, normalizeUrl, resolveFeedProtocol, resolveUrl };
5
+ import { addMissingProtocol, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.js";
6
+ export { type DefaultParserResult, type ExistsFn, type FetchFn, type FetchFnOptions, type FetchFnResponse, type FindCanonicalOptions, type NormalizeOptions, type OnExistsFn, type OnFetchFn, type OnMatchFn, type ParserAdapter, type PlatformHandler, addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/dist/exports.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { feedburnerHandler } from "./platforms/feedburner.js";
2
2
  import { defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers } from "./defaults.js";
3
- import { addMissingProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.js";
3
+ import { addMissingProtocol, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl } from "./utils.js";
4
4
  import { findCanonical } from "./index.js";
5
5
 
6
- export { addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, normalizeUrl, resolveFeedProtocol, resolveUrl };
6
+ export { addMissingProtocol, defaultFetch, defaultParser, defaultPlatforms, defaultStrippedParams, defaultTiers, feedburnerHandler, findCanonical, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/dist/index.cjs CHANGED
@@ -52,8 +52,8 @@ async function findCanonical(inputUrl, options) {
52
52
  if (initialResponseBody === comparedResponseBody) return true;
53
53
  const comparedResponseFeed = await parser.parse(comparedResponseBody);
54
54
  if (comparedResponseFeed) {
55
- initialResponseSignature ||= JSON.stringify(parser.getSignature(initialResponseFeed));
56
- const comparedResponseSignature = JSON.stringify(parser.getSignature(comparedResponseFeed));
55
+ initialResponseSignature ||= parser.getSignature(initialResponseFeed);
56
+ const comparedResponseSignature = parser.getSignature(comparedResponseFeed);
57
57
  return initialResponseSignature === comparedResponseSignature;
58
58
  }
59
59
  return false;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { FeedsmithFeed, FetchFnResponse, FindCanonicalOptions, ParserAdapter } from "./types.cjs";
1
+ import { DefaultParserResult, FetchFnResponse, FindCanonicalOptions, ParserAdapter } from "./types.cjs";
2
2
 
3
3
  //#region src/index.d.ts
4
- declare function findCanonical<TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options?: Omit<FindCanonicalOptions<FeedsmithFeed, TResponse, TExisting>, 'parser'>): Promise<string | undefined>;
4
+ declare function findCanonical<TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options?: Omit<FindCanonicalOptions<DefaultParserResult, TResponse, TExisting>, 'parser'>): Promise<string | undefined>;
5
5
  declare function findCanonical<TFeed, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options: FindCanonicalOptions<TFeed, TResponse, TExisting> & {
6
6
  parser: ParserAdapter<TFeed>;
7
7
  }): Promise<string | undefined>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { FeedsmithFeed, FetchFnResponse, FindCanonicalOptions, ParserAdapter } from "./types.js";
1
+ import { DefaultParserResult, FetchFnResponse, FindCanonicalOptions, ParserAdapter } from "./types.js";
2
2
 
3
3
  //#region src/index.d.ts
4
- declare function findCanonical<TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options?: Omit<FindCanonicalOptions<FeedsmithFeed, TResponse, TExisting>, 'parser'>): Promise<string | undefined>;
4
+ declare function findCanonical<TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options?: Omit<FindCanonicalOptions<DefaultParserResult, TResponse, TExisting>, 'parser'>): Promise<string | undefined>;
5
5
  declare function findCanonical<TFeed, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown>(inputUrl: string, options: FindCanonicalOptions<TFeed, TResponse, TExisting> & {
6
6
  parser: ParserAdapter<TFeed>;
7
7
  }): Promise<string | undefined>;
package/dist/index.js CHANGED
@@ -52,8 +52,8 @@ async function findCanonical(inputUrl, options) {
52
52
  if (initialResponseBody === comparedResponseBody) return true;
53
53
  const comparedResponseFeed = await parser.parse(comparedResponseBody);
54
54
  if (comparedResponseFeed) {
55
- initialResponseSignature ||= JSON.stringify(parser.getSignature(initialResponseFeed));
56
- const comparedResponseSignature = JSON.stringify(parser.getSignature(comparedResponseFeed));
55
+ initialResponseSignature ||= parser.getSignature(initialResponseFeed);
56
+ const comparedResponseSignature = parser.getSignature(comparedResponseFeed);
57
57
  return initialResponseSignature === comparedResponseSignature;
58
58
  }
59
59
  return false;
package/dist/types.d.cts CHANGED
@@ -1,11 +1,11 @@
1
1
  import * as feedsmith0 from "feedsmith";
2
2
 
3
3
  //#region src/types.d.ts
4
- type FeedsmithFeed = ReturnType<typeof feedsmith0.parseFeed>;
4
+ type DefaultParserResult = ReturnType<typeof feedsmith0.parseFeed>;
5
5
  type ParserAdapter<T> = {
6
6
  parse: (body: string) => Promise<T | undefined> | T | undefined;
7
7
  getSelfUrl: (parsed: T) => string | undefined;
8
- getSignature: (parsed: T) => object;
8
+ getSignature: (parsed: T) => string;
9
9
  };
10
10
  type PlatformHandler = {
11
11
  match: (url: URL) => boolean;
@@ -41,7 +41,7 @@ type OnExistsFn<T> = (data: {
41
41
  url: string;
42
42
  data: T;
43
43
  }) => void;
44
- type FindCanonicalOptions<TFeed = FeedsmithFeed, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown> = {
44
+ type FindCanonicalOptions<TFeed = DefaultParserResult, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown> = {
45
45
  parser?: ParserAdapter<TFeed>;
46
46
  fetchFn?: FetchFn<TResponse>;
47
47
  existsFn?: ExistsFn<TExisting>;
@@ -65,4 +65,4 @@ type FetchFnResponse = {
65
65
  };
66
66
  type FetchFn<TResponse extends FetchFnResponse = FetchFnResponse> = (url: string, options?: FetchFnOptions) => Promise<TResponse>;
67
67
  //#endregion
68
- export { ExistsFn, FeedsmithFeed, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler, Tier };
68
+ export { DefaultParserResult, ExistsFn, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler, Tier };
package/dist/types.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import * as feedsmith0 from "feedsmith";
2
2
 
3
3
  //#region src/types.d.ts
4
- type FeedsmithFeed = ReturnType<typeof feedsmith0.parseFeed>;
4
+ type DefaultParserResult = ReturnType<typeof feedsmith0.parseFeed>;
5
5
  type ParserAdapter<T> = {
6
6
  parse: (body: string) => Promise<T | undefined> | T | undefined;
7
7
  getSelfUrl: (parsed: T) => string | undefined;
8
- getSignature: (parsed: T) => object;
8
+ getSignature: (parsed: T) => string;
9
9
  };
10
10
  type PlatformHandler = {
11
11
  match: (url: URL) => boolean;
@@ -41,7 +41,7 @@ type OnExistsFn<T> = (data: {
41
41
  url: string;
42
42
  data: T;
43
43
  }) => void;
44
- type FindCanonicalOptions<TFeed = FeedsmithFeed, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown> = {
44
+ type FindCanonicalOptions<TFeed = DefaultParserResult, TResponse extends FetchFnResponse = FetchFnResponse, TExisting = unknown> = {
45
45
  parser?: ParserAdapter<TFeed>;
46
46
  fetchFn?: FetchFn<TResponse>;
47
47
  existsFn?: ExistsFn<TExisting>;
@@ -65,4 +65,4 @@ type FetchFnResponse = {
65
65
  };
66
66
  type FetchFn<TResponse extends FetchFnResponse = FetchFnResponse> = (url: string, options?: FetchFnOptions) => Promise<TResponse>;
67
67
  //#endregion
68
- export { ExistsFn, FeedsmithFeed, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler, Tier };
68
+ export { DefaultParserResult, ExistsFn, FetchFn, FetchFnOptions, FetchFnResponse, FindCanonicalOptions, NormalizeOptions, OnExistsFn, OnFetchFn, OnMatchFn, ParserAdapter, PlatformHandler, Tier };
package/dist/utils.cjs CHANGED
@@ -15,6 +15,27 @@ const getStrippedParamsSet = (params) => {
15
15
  const ipv4Pattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
16
16
  const ipv6Pattern = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i;
17
17
  const safePathChars = /[a-zA-Z0-9._~!$&'()*+,;=:@-]/;
18
+ const validUrlPattern = /^https?:\/\/(?:www\.|[a-vx-z0-9])/i;
19
+ const doubledProtocolPattern = /^\/?[htps]{2,7}[:\s=.\\/]+([htps]{2,7})[:\s=.\\/]+[.,:/]*(www[./]+)?/i;
20
+ const singleMalformedPattern = /^\/?(?:h[htps():]{1,10}|t{1,2}ps?)[:\s=.\\/]+[.,:/]*(www[./]+)?/i;
21
+ const fixMalformedProtocol = (url) => {
22
+ if (validUrlPattern.test(url) && !doubledProtocolPattern.test(url)) return url;
23
+ const doubledMatch = doubledProtocolPattern.exec(url);
24
+ if (doubledMatch) {
25
+ const inner = doubledMatch[1];
26
+ const www = doubledMatch[2];
27
+ const rest = url.slice(doubledMatch[0].length);
28
+ return (/s/i.test(inner) ? "https://" : "http://") + (www ? "www." : "") + rest;
29
+ }
30
+ const singleMatch = singleMalformedPattern.exec(url);
31
+ if (singleMatch) {
32
+ const fullMatch = singleMatch[0];
33
+ const www = singleMatch[1];
34
+ const rest = url.slice(fullMatch.length);
35
+ return (/s/i.test(fullMatch) ? "https://" : "http://") + (www ? "www." : "") + rest;
36
+ }
37
+ return url;
38
+ };
18
39
  const feedProtocols = [
19
40
  "feed:",
20
41
  "rss:",
@@ -58,6 +79,7 @@ const resolveUrl = (url, base) => {
58
79
  let resolvedUrl;
59
80
  resolvedUrl = url.includes("&") ? (0, entities.decodeHTML)(url) : url;
60
81
  resolvedUrl = resolveFeedProtocol(resolvedUrl);
82
+ resolvedUrl = fixMalformedProtocol(resolvedUrl);
61
83
  if (base) try {
62
84
  resolvedUrl = new URL(resolvedUrl, base).href;
63
85
  } catch {
@@ -114,6 +136,7 @@ const normalizeUrl = (url, options = require_defaults.defaultNormalizeOptions) =
114
136
  if (options.sortQueryParams) parsed.searchParams.sort();
115
137
  if (options.stripEmptyQuery && parsed.href.endsWith("?")) parsed.search = "";
116
138
  let result = parsed.href;
139
+ if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
117
140
  if (options.stripProtocol) result = result.replace(/^https?:\/\//, "");
118
141
  return result;
119
142
  } catch {
@@ -136,6 +159,7 @@ const applyPlatformHandlers = (url, platforms) => {
136
159
  //#endregion
137
160
  exports.addMissingProtocol = addMissingProtocol;
138
161
  exports.applyPlatformHandlers = applyPlatformHandlers;
162
+ exports.fixMalformedProtocol = fixMalformedProtocol;
139
163
  exports.normalizeUrl = normalizeUrl;
140
164
  exports.resolveFeedProtocol = resolveFeedProtocol;
141
165
  exports.resolveUrl = resolveUrl;
package/dist/utils.d.cts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { NormalizeOptions } from "./types.cjs";
2
2
 
3
3
  //#region src/utils.d.ts
4
+ declare const fixMalformedProtocol: (url: string) => string;
4
5
  declare const resolveFeedProtocol: (url: string, protocol?: "http" | "https") => string;
5
6
  declare const addMissingProtocol: (url: string, protocol?: "http" | "https") => string;
6
7
  declare const resolveUrl: (url: string, base?: string) => string | undefined;
7
8
  declare const normalizeUrl: (url: string, options?: NormalizeOptions) => string;
8
9
  //#endregion
9
- export { addMissingProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
10
+ export { addMissingProtocol, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/dist/utils.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { NormalizeOptions } from "./types.js";
2
2
 
3
3
  //#region src/utils.d.ts
4
+ declare const fixMalformedProtocol: (url: string) => string;
4
5
  declare const resolveFeedProtocol: (url: string, protocol?: "http" | "https") => string;
5
6
  declare const addMissingProtocol: (url: string, protocol?: "http" | "https") => string;
6
7
  declare const resolveUrl: (url: string, base?: string) => string | undefined;
7
8
  declare const normalizeUrl: (url: string, options?: NormalizeOptions) => string;
8
9
  //#endregion
9
- export { addMissingProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
10
+ export { addMissingProtocol, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/dist/utils.js CHANGED
@@ -15,6 +15,27 @@ const getStrippedParamsSet = (params) => {
15
15
  const ipv4Pattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
16
16
  const ipv6Pattern = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i;
17
17
  const safePathChars = /[a-zA-Z0-9._~!$&'()*+,;=:@-]/;
18
+ const validUrlPattern = /^https?:\/\/(?:www\.|[a-vx-z0-9])/i;
19
+ const doubledProtocolPattern = /^\/?[htps]{2,7}[:\s=.\\/]+([htps]{2,7})[:\s=.\\/]+[.,:/]*(www[./]+)?/i;
20
+ const singleMalformedPattern = /^\/?(?:h[htps():]{1,10}|t{1,2}ps?)[:\s=.\\/]+[.,:/]*(www[./]+)?/i;
21
+ const fixMalformedProtocol = (url) => {
22
+ if (validUrlPattern.test(url) && !doubledProtocolPattern.test(url)) return url;
23
+ const doubledMatch = doubledProtocolPattern.exec(url);
24
+ if (doubledMatch) {
25
+ const inner = doubledMatch[1];
26
+ const www = doubledMatch[2];
27
+ const rest = url.slice(doubledMatch[0].length);
28
+ return (/s/i.test(inner) ? "https://" : "http://") + (www ? "www." : "") + rest;
29
+ }
30
+ const singleMatch = singleMalformedPattern.exec(url);
31
+ if (singleMatch) {
32
+ const fullMatch = singleMatch[0];
33
+ const www = singleMatch[1];
34
+ const rest = url.slice(fullMatch.length);
35
+ return (/s/i.test(fullMatch) ? "https://" : "http://") + (www ? "www." : "") + rest;
36
+ }
37
+ return url;
38
+ };
18
39
  const feedProtocols = [
19
40
  "feed:",
20
41
  "rss:",
@@ -58,6 +79,7 @@ const resolveUrl = (url, base) => {
58
79
  let resolvedUrl;
59
80
  resolvedUrl = url.includes("&") ? decodeHTML(url) : url;
60
81
  resolvedUrl = resolveFeedProtocol(resolvedUrl);
82
+ resolvedUrl = fixMalformedProtocol(resolvedUrl);
61
83
  if (base) try {
62
84
  resolvedUrl = new URL(resolvedUrl, base).href;
63
85
  } catch {
@@ -114,6 +136,7 @@ const normalizeUrl = (url, options = defaultNormalizeOptions) => {
114
136
  if (options.sortQueryParams) parsed.searchParams.sort();
115
137
  if (options.stripEmptyQuery && parsed.href.endsWith("?")) parsed.search = "";
116
138
  let result = parsed.href;
139
+ if (options.stripRootSlash && result === `${parsed.origin}/`) result = parsed.origin;
117
140
  if (options.stripProtocol) result = result.replace(/^https?:\/\//, "");
118
141
  return result;
119
142
  } catch {
@@ -134,4 +157,4 @@ const applyPlatformHandlers = (url, platforms) => {
134
157
  };
135
158
 
136
159
  //#endregion
137
- export { addMissingProtocol, applyPlatformHandlers, normalizeUrl, resolveFeedProtocol, resolveUrl };
160
+ export { addMissingProtocol, applyPlatformHandlers, fixMalformedProtocol, normalizeUrl, resolveFeedProtocol, resolveUrl };
package/package.json CHANGED
@@ -60,8 +60,8 @@
60
60
  "devDependencies": {
61
61
  "@types/bun": "^1.3.5",
62
62
  "kvalita": "1.9.0",
63
- "tsdown": "^0.18.3",
63
+ "tsdown": "^0.18.4",
64
64
  "vitepress": "^1.6.4"
65
65
  },
66
- "version": "1.0.0"
66
+ "version": "1.1.0"
67
67
  }