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 +7 -4
- package/dist/defaults.cjs +35 -7
- package/dist/defaults.d.cts +2 -2
- package/dist/defaults.d.ts +2 -2
- package/dist/defaults.js +35 -7
- package/dist/exports.cjs +1 -0
- package/dist/exports.d.cts +3 -3
- package/dist/exports.d.ts +3 -3
- package/dist/exports.js +2 -2
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/types.d.cts +4 -4
- package/dist/types.d.ts +4 -4
- package/dist/utils.cjs +24 -0
- package/dist/utils.d.cts +2 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +24 -1
- package/package.json +2 -2
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
|
|
41
|
-
3.
|
|
42
|
-
4.
|
|
43
|
-
5.
|
|
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
|
-
|
|
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
|
-
|
|
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/defaults.d.cts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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<
|
|
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.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/exports.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
|
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 ||=
|
|
56
|
-
const comparedResponseSignature =
|
|
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 {
|
|
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<
|
|
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 {
|
|
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<
|
|
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 ||=
|
|
56
|
-
const comparedResponseSignature =
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 {
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 {
|
|
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 };
|