feedscout 1.6.2 → 1.7.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.
Files changed (92) hide show
  1. package/README.md +14 -0
  2. package/dist/blogrolls/extractors.cjs +1 -1
  3. package/dist/blogrolls/extractors.js +1 -1
  4. package/dist/blogrolls/index.cjs +3 -2
  5. package/dist/blogrolls/index.js +5 -4
  6. package/dist/common/discover/index.cjs +21 -9
  7. package/dist/common/discover/index.js +21 -9
  8. package/dist/common/discover/utils.cjs +44 -18
  9. package/dist/common/discover/utils.d.cts +8 -0
  10. package/dist/common/discover/utils.d.ts +8 -0
  11. package/dist/common/discover/utils.js +43 -19
  12. package/dist/common/locales.cjs +3 -1
  13. package/dist/common/locales.js +3 -1
  14. package/dist/common/types.d.cts +6 -4
  15. package/dist/common/types.d.ts +6 -4
  16. package/dist/common/uris/guess/utils.cjs +3 -2
  17. package/dist/common/uris/guess/utils.js +3 -2
  18. package/dist/common/uris/headers/index.cjs +2 -1
  19. package/dist/common/uris/headers/index.js +2 -1
  20. package/dist/common/utils.cjs +15 -4
  21. package/dist/common/utils.js +14 -4
  22. package/dist/favicons/extractors.cjs +16 -4
  23. package/dist/favicons/extractors.js +16 -4
  24. package/dist/favicons/index.cjs +3 -2
  25. package/dist/favicons/index.js +5 -4
  26. package/dist/favicons/platform/handlers/bluesky.cjs +3 -3
  27. package/dist/favicons/platform/handlers/bluesky.js +3 -3
  28. package/dist/favicons/platform/handlers/mastodon.cjs +7 -5
  29. package/dist/favicons/platform/handlers/mastodon.js +7 -5
  30. package/dist/favicons/platform/handlers/reddit.cjs +5 -6
  31. package/dist/favicons/platform/handlers/reddit.js +5 -6
  32. package/dist/favicons/utils.cjs +10 -0
  33. package/dist/favicons/utils.js +9 -0
  34. package/dist/feeds/defaults.cjs +2 -0
  35. package/dist/feeds/defaults.js +2 -0
  36. package/dist/feeds/extractors.cjs +10 -8
  37. package/dist/feeds/extractors.js +10 -8
  38. package/dist/feeds/index.cjs +2 -2
  39. package/dist/feeds/index.js +3 -3
  40. package/dist/feeds/platform/handlers/blogspot.cjs +2 -1
  41. package/dist/feeds/platform/handlers/blogspot.js +2 -1
  42. package/dist/feeds/platform/handlers/bluesky.cjs +2 -1
  43. package/dist/feeds/platform/handlers/bluesky.js +2 -1
  44. package/dist/feeds/platform/handlers/csdn.cjs +2 -1
  45. package/dist/feeds/platform/handlers/csdn.js +2 -1
  46. package/dist/feeds/platform/handlers/deviantart.cjs +8 -4
  47. package/dist/feeds/platform/handlers/deviantart.js +8 -4
  48. package/dist/feeds/platform/handlers/douban.cjs +4 -2
  49. package/dist/feeds/platform/handlers/douban.js +4 -2
  50. package/dist/feeds/platform/handlers/github.cjs +12 -6
  51. package/dist/feeds/platform/handlers/github.js +12 -6
  52. package/dist/feeds/platform/handlers/githubGist.cjs +6 -3
  53. package/dist/feeds/platform/handlers/githubGist.js +6 -3
  54. package/dist/feeds/platform/handlers/gitlab.cjs +15 -2
  55. package/dist/feeds/platform/handlers/gitlab.js +16 -3
  56. package/dist/feeds/platform/handlers/lemmy.cjs +46 -0
  57. package/dist/feeds/platform/handlers/lemmy.js +46 -0
  58. package/dist/feeds/platform/handlers/mastodon.cjs +5 -3
  59. package/dist/feeds/platform/handlers/mastodon.js +5 -3
  60. package/dist/feeds/platform/handlers/medium.cjs +10 -5
  61. package/dist/feeds/platform/handlers/medium.js +10 -5
  62. package/dist/feeds/platform/handlers/reddit.cjs +10 -5
  63. package/dist/feeds/platform/handlers/reddit.js +10 -5
  64. package/dist/feeds/platform/handlers/soundcloud.cjs +3 -2
  65. package/dist/feeds/platform/handlers/soundcloud.js +3 -2
  66. package/dist/feeds/platform/handlers/stackExchange.cjs +6 -3
  67. package/dist/feeds/platform/handlers/stackExchange.js +6 -3
  68. package/dist/feeds/platform/handlers/steam.cjs +4 -2
  69. package/dist/feeds/platform/handlers/steam.js +4 -2
  70. package/dist/feeds/platform/handlers/v2ex.cjs +4 -2
  71. package/dist/feeds/platform/handlers/v2ex.js +4 -2
  72. package/dist/feeds/platform/handlers/vimeo.cjs +3 -2
  73. package/dist/feeds/platform/handlers/vimeo.js +3 -2
  74. package/dist/feeds/platform/handlers/ximalaya.cjs +2 -1
  75. package/dist/feeds/platform/handlers/ximalaya.js +2 -1
  76. package/dist/feeds/platform/handlers/youtube.cjs +10 -9
  77. package/dist/feeds/platform/handlers/youtube.js +10 -9
  78. package/dist/hubs/discover/index.cjs +4 -4
  79. package/dist/hubs/discover/index.js +5 -5
  80. package/dist/hubs/discover/types.d.cts +2 -2
  81. package/dist/hubs/discover/types.d.ts +2 -2
  82. package/dist/hubs/feed/index.cjs +7 -5
  83. package/dist/hubs/feed/index.js +7 -5
  84. package/dist/hubs/headers/index.cjs +3 -3
  85. package/dist/hubs/headers/index.js +4 -4
  86. package/dist/hubs/html/index.cjs +3 -3
  87. package/dist/hubs/html/index.js +4 -4
  88. package/dist/index.cjs +3 -0
  89. package/dist/index.d.cts +3 -2
  90. package/dist/index.d.ts +3 -2
  91. package/dist/index.js +2 -1
  92. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  const require_locales = require("./locales.cjs");
2
2
  //#region src/common/utils.ts
3
+ const whitespaceRegex = /\s+/;
3
4
  const composeHint = (key) => ({
4
5
  key,
5
6
  label: require_locales.hints[key]
@@ -30,7 +31,7 @@ const isAnyOf = (value, patterns, parser) => {
30
31
  return patterns.some((pattern) => parsedValue === pattern.toLowerCase().trim());
31
32
  };
32
33
  const anyWordMatchesAnyOf = (value, patterns) => {
33
- return value.toLowerCase().split(/\s+/).some((word) => isAnyOf(word, patterns));
34
+ return value.toLowerCase().split(whitespaceRegex).some((word) => isAnyOf(word, patterns));
34
35
  };
35
36
  const endsWithAnyOf = (value, patterns) => {
36
37
  const lowerValue = value.toLowerCase();
@@ -41,13 +42,22 @@ const isOfAllowedMimeType = (type, allowedTypes) => {
41
42
  if (!type) return false;
42
43
  return isAnyOf(type, allowedTypes, normalizeMimeType);
43
44
  };
45
+ const hasMetaContent = (content, name, value) => {
46
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48
+ return new RegExp(`<meta(?=[^>]*(?:name|property)=["']${escapedName}["'])(?=[^>]*content=["']${escapedValue})`, "i").test(content);
49
+ };
44
50
  const omitEmpty = (array) => {
45
51
  const result = [];
46
52
  for (const item of array) if (item != null && item !== "") result.push(item);
47
53
  return result;
48
54
  };
49
- const normalizeUrl = (url, baseUrl) => {
50
- return baseUrl ? new URL(url, baseUrl).href : url;
55
+ const resolveUrl = (url, baseUrl) => {
56
+ try {
57
+ return new URL(url, baseUrl).href;
58
+ } catch {
59
+ return;
60
+ }
51
61
  };
52
62
  const matchesAnyOfLinkSelectors = (rel, type, selectors) => {
53
63
  return selectors.some((selector) => {
@@ -76,11 +86,12 @@ const processConcurrently = async (items, processFn, options) => {
76
86
  exports.anyWordMatchesAnyOf = anyWordMatchesAnyOf;
77
87
  exports.composeHint = composeHint;
78
88
  exports.endsWithAnyOf = endsWithAnyOf;
89
+ exports.hasMetaContent = hasMetaContent;
79
90
  exports.includesAnyOf = includesAnyOf;
80
91
  exports.isAnyOf = isAnyOf;
81
92
  exports.isHostOf = isHostOf;
82
93
  exports.isSubdomainOf = isSubdomainOf;
83
94
  exports.matchesAnyOfLinkSelectors = matchesAnyOfLinkSelectors;
84
- exports.normalizeUrl = normalizeUrl;
85
95
  exports.omitEmpty = omitEmpty;
86
96
  exports.processConcurrently = processConcurrently;
97
+ exports.resolveUrl = resolveUrl;
@@ -1,5 +1,6 @@
1
1
  import { hints } from "./locales.js";
2
2
  //#region src/common/utils.ts
3
+ const whitespaceRegex = /\s+/;
3
4
  const composeHint = (key) => ({
4
5
  key,
5
6
  label: hints[key]
@@ -30,7 +31,7 @@ const isAnyOf = (value, patterns, parser) => {
30
31
  return patterns.some((pattern) => parsedValue === pattern.toLowerCase().trim());
31
32
  };
32
33
  const anyWordMatchesAnyOf = (value, patterns) => {
33
- return value.toLowerCase().split(/\s+/).some((word) => isAnyOf(word, patterns));
34
+ return value.toLowerCase().split(whitespaceRegex).some((word) => isAnyOf(word, patterns));
34
35
  };
35
36
  const endsWithAnyOf = (value, patterns) => {
36
37
  const lowerValue = value.toLowerCase();
@@ -41,13 +42,22 @@ const isOfAllowedMimeType = (type, allowedTypes) => {
41
42
  if (!type) return false;
42
43
  return isAnyOf(type, allowedTypes, normalizeMimeType);
43
44
  };
45
+ const hasMetaContent = (content, name, value) => {
46
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48
+ return new RegExp(`<meta(?=[^>]*(?:name|property)=["']${escapedName}["'])(?=[^>]*content=["']${escapedValue})`, "i").test(content);
49
+ };
44
50
  const omitEmpty = (array) => {
45
51
  const result = [];
46
52
  for (const item of array) if (item != null && item !== "") result.push(item);
47
53
  return result;
48
54
  };
49
- const normalizeUrl = (url, baseUrl) => {
50
- return baseUrl ? new URL(url, baseUrl).href : url;
55
+ const resolveUrl = (url, baseUrl) => {
56
+ try {
57
+ return new URL(url, baseUrl).href;
58
+ } catch {
59
+ return;
60
+ }
51
61
  };
52
62
  const matchesAnyOfLinkSelectors = (rel, type, selectors) => {
53
63
  return selectors.some((selector) => {
@@ -73,4 +83,4 @@ const processConcurrently = async (items, processFn, options) => {
73
83
  }
74
84
  };
75
85
  //#endregion
76
- export { anyWordMatchesAnyOf, composeHint, endsWithAnyOf, includesAnyOf, isAnyOf, isHostOf, isSubdomainOf, matchesAnyOfLinkSelectors, normalizeUrl, omitEmpty, processConcurrently };
86
+ export { anyWordMatchesAnyOf, composeHint, endsWithAnyOf, hasMetaContent, includesAnyOf, isAnyOf, isHostOf, isSubdomainOf, matchesAnyOfLinkSelectors, omitEmpty, processConcurrently, resolveUrl };
@@ -1,11 +1,23 @@
1
1
  //#region src/favicons/extractors.ts
2
- const defaultExtractor = async ({ url, status }) => {
3
- if (status !== void 0 && status >= 200 && status < 400) return {
4
- url,
2
+ const isImageContentType = (headers) => {
3
+ return headers?.get("content-type")?.startsWith("image/") ?? false;
4
+ };
5
+ const isImageContent = (content) => {
6
+ if (content.includes("<html")) return false;
7
+ const trimmed = content.trimStart();
8
+ const head = trimmed.slice(0, 200);
9
+ return trimmed.startsWith("<svg") || trimmed.startsWith("<?xml") && head.includes("<svg") || content.slice(1, 4) === "PNG" || content.startsWith("GIF8") || content.startsWith("RIFF") && content.includes("WEBP");
10
+ };
11
+ const isSuccessStatus = (status) => {
12
+ return status !== void 0 && status >= 200 && status < 400;
13
+ };
14
+ const defaultExtractor = (input) => {
15
+ if (isImageContentType(input.headers) || isImageContent(input.content) || isSuccessStatus(input.status)) return {
16
+ url: input.url,
5
17
  isValid: true
6
18
  };
7
19
  return {
8
- url,
20
+ url: input.url,
9
21
  isValid: false
10
22
  };
11
23
  };
@@ -1,11 +1,23 @@
1
1
  //#region src/favicons/extractors.ts
2
- const defaultExtractor = async ({ url, status }) => {
3
- if (status !== void 0 && status >= 200 && status < 400) return {
4
- url,
2
+ const isImageContentType = (headers) => {
3
+ return headers?.get("content-type")?.startsWith("image/") ?? false;
4
+ };
5
+ const isImageContent = (content) => {
6
+ if (content.includes("<html")) return false;
7
+ const trimmed = content.trimStart();
8
+ const head = trimmed.slice(0, 200);
9
+ return trimmed.startsWith("<svg") || trimmed.startsWith("<?xml") && head.includes("<svg") || content.slice(1, 4) === "PNG" || content.startsWith("GIF8") || content.startsWith("RIFF") && content.includes("WEBP");
10
+ };
11
+ const isSuccessStatus = (status) => {
12
+ return status !== void 0 && status >= 200 && status < 400;
13
+ };
14
+ const defaultExtractor = (input) => {
15
+ if (isImageContentType(input.headers) || isImageContent(input.content) || isSuccessStatus(input.status)) return {
16
+ url: input.url,
5
17
  isValid: true
6
18
  };
7
19
  return {
8
- url,
20
+ url: input.url,
9
21
  isValid: false
10
22
  };
11
23
  };
@@ -4,7 +4,7 @@ const require_index = require("../common/discover/index.cjs");
4
4
  const require_defaults = require("./defaults.cjs");
5
5
  const require_extractors = require("./extractors.cjs");
6
6
  //#region src/favicons/index.ts
7
- const discoverFavicons = async (input, options = {}) => {
7
+ const discoverFavicons = (input, options = {}) => {
8
8
  return require_index.discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -16,7 +16,8 @@ const discoverFavicons = async (input, options = {}) => {
16
16
  ],
17
17
  fetchFn: options.fetchFn ?? require_utils$1.defaultFetchFn,
18
18
  extractFn: options.extractFn ?? require_extractors.defaultExtractor,
19
- normalizeUrlFn: options.normalizeUrlFn ?? require_utils.normalizeUrl
19
+ resolveUrlFn: options.resolveUrlFn ?? require_utils.resolveUrl,
20
+ resolveSiteUrlFn: options.resolveSiteUrlFn ?? require_utils$1.defaultResolveSiteUrlFn
20
21
  }, {
21
22
  platform: require_defaults.defaultPlatformOptions,
22
23
  feed: require_defaults.defaultFeedOptions,
@@ -1,10 +1,10 @@
1
- import { normalizeUrl } from "../common/utils.js";
2
- import { defaultFetchFn } from "../common/discover/utils.js";
1
+ import { resolveUrl } from "../common/utils.js";
2
+ import { defaultFetchFn, defaultResolveSiteUrlFn } from "../common/discover/utils.js";
3
3
  import { discover } from "../common/discover/index.js";
4
4
  import { defaultFeedOptions, defaultGuessOptions, defaultHeadersOptions, defaultHtmlOptions, defaultPlatformOptions } from "./defaults.js";
5
5
  import { defaultExtractor } from "./extractors.js";
6
6
  //#region src/favicons/index.ts
7
- const discoverFavicons = async (input, options = {}) => {
7
+ const discoverFavicons = (input, options = {}) => {
8
8
  return discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -16,7 +16,8 @@ const discoverFavicons = async (input, options = {}) => {
16
16
  ],
17
17
  fetchFn: options.fetchFn ?? defaultFetchFn,
18
18
  extractFn: options.extractFn ?? defaultExtractor,
19
- normalizeUrlFn: options.normalizeUrlFn ?? normalizeUrl
19
+ resolveUrlFn: options.resolveUrlFn ?? resolveUrl,
20
+ resolveSiteUrlFn: options.resolveSiteUrlFn ?? defaultResolveSiteUrlFn
20
21
  }, {
21
22
  platform: defaultPlatformOptions,
22
23
  feed: defaultFeedOptions,
@@ -1,4 +1,5 @@
1
1
  const require_utils = require("../../../common/utils.cjs");
2
+ const require_utils$1 = require("../../utils.cjs");
2
3
  //#region src/favicons/platform/handlers/bluesky.ts
3
4
  const hosts = ["bsky.app", "www.bsky.app"];
4
5
  const isProfilePath = (pathname) => {
@@ -16,9 +17,8 @@ const blueskyHandler = {
16
17
  if (!fetchFn) return [];
17
18
  try {
18
19
  const { pathname } = new URL(url);
19
- const response = await fetchFn(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${pathname.split("/").filter(Boolean)[1]}`);
20
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
21
- if (typeof data.avatar === "string" && data.avatar.length > 0) return [{ uri: data.avatar }];
20
+ const data = require_utils$1.parseBodyJson((await fetchFn(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${pathname.split("/").filter(Boolean)[1]}`)).body);
21
+ if (require_utils$1.isNonEmptyString(data.avatar)) return [{ uri: data.avatar }];
22
22
  } catch {}
23
23
  return [];
24
24
  }
@@ -1,4 +1,5 @@
1
1
  import { isHostOf } from "../../../common/utils.js";
2
+ import { isNonEmptyString, parseBodyJson } from "../../utils.js";
2
3
  //#region src/favicons/platform/handlers/bluesky.ts
3
4
  const hosts = ["bsky.app", "www.bsky.app"];
4
5
  const isProfilePath = (pathname) => {
@@ -16,9 +17,8 @@ const blueskyHandler = {
16
17
  if (!fetchFn) return [];
17
18
  try {
18
19
  const { pathname } = new URL(url);
19
- const response = await fetchFn(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${pathname.split("/").filter(Boolean)[1]}`);
20
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
21
- if (typeof data.avatar === "string" && data.avatar.length > 0) return [{ uri: data.avatar }];
20
+ const data = parseBodyJson((await fetchFn(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${pathname.split("/").filter(Boolean)[1]}`)).body);
21
+ if (isNonEmptyString(data.avatar)) return [{ uri: data.avatar }];
22
22
  } catch {}
23
23
  return [];
24
24
  }
@@ -1,13 +1,16 @@
1
+ const require_utils = require("../../../common/utils.cjs");
2
+ const require_utils$1 = require("../../utils.cjs");
1
3
  //#region src/favicons/platform/handlers/mastodon.ts
4
+ const mastodonRegex = /mastodon/i;
2
5
  const isProfilePath = (pathname) => {
3
6
  const segments = pathname.split("/").filter(Boolean);
4
7
  return segments.length === 1 && segments[0].startsWith("@");
5
8
  };
6
9
  const isMastodonHtml = (content) => {
7
- return /<meta[^>]+name=["']generator["'][^>]+content=["']Mastodon/i.test(content);
10
+ return require_utils.hasMetaContent(content, "generator", "Mastodon");
8
11
  };
9
12
  const isMastodonHeaders = (headers) => {
10
- return /mastodon/i.test(headers.get("server") ?? "");
13
+ return mastodonRegex.test(headers.get("server") ?? "");
11
14
  };
12
15
  const mastodonHandler = {
13
16
  match: (url, content, headers) => {
@@ -23,9 +26,8 @@ const mastodonHandler = {
23
26
  if (!fetchFn) return [];
24
27
  try {
25
28
  const { hostname, pathname } = new URL(url);
26
- const response = await fetchFn(`https://${hostname}/api/v1/accounts/lookup?acct=${pathname.split("/").filter(Boolean)[0].replace("@", "")}`);
27
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
28
- if (typeof data.avatar === "string" && data.avatar.length > 0) return [{ uri: data.avatar }];
29
+ const data = require_utils$1.parseBodyJson((await fetchFn(`https://${hostname}/api/v1/accounts/lookup?acct=${pathname.split("/").filter(Boolean)[0].replace("@", "")}`)).body);
30
+ if (require_utils$1.isNonEmptyString(data.avatar)) return [{ uri: data.avatar }];
29
31
  } catch {}
30
32
  return [];
31
33
  }
@@ -1,13 +1,16 @@
1
+ import { hasMetaContent } from "../../../common/utils.js";
2
+ import { isNonEmptyString, parseBodyJson } from "../../utils.js";
1
3
  //#region src/favicons/platform/handlers/mastodon.ts
4
+ const mastodonRegex = /mastodon/i;
2
5
  const isProfilePath = (pathname) => {
3
6
  const segments = pathname.split("/").filter(Boolean);
4
7
  return segments.length === 1 && segments[0].startsWith("@");
5
8
  };
6
9
  const isMastodonHtml = (content) => {
7
- return /<meta[^>]+name=["']generator["'][^>]+content=["']Mastodon/i.test(content);
10
+ return hasMetaContent(content, "generator", "Mastodon");
8
11
  };
9
12
  const isMastodonHeaders = (headers) => {
10
- return /mastodon/i.test(headers.get("server") ?? "");
13
+ return mastodonRegex.test(headers.get("server") ?? "");
11
14
  };
12
15
  const mastodonHandler = {
13
16
  match: (url, content, headers) => {
@@ -23,9 +26,8 @@ const mastodonHandler = {
23
26
  if (!fetchFn) return [];
24
27
  try {
25
28
  const { hostname, pathname } = new URL(url);
26
- const response = await fetchFn(`https://${hostname}/api/v1/accounts/lookup?acct=${pathname.split("/").filter(Boolean)[0].replace("@", "")}`);
27
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
28
- if (typeof data.avatar === "string" && data.avatar.length > 0) return [{ uri: data.avatar }];
29
+ const data = parseBodyJson((await fetchFn(`https://${hostname}/api/v1/accounts/lookup?acct=${pathname.split("/").filter(Boolean)[0].replace("@", "")}`)).body);
30
+ if (isNonEmptyString(data.avatar)) return [{ uri: data.avatar }];
29
31
  } catch {}
30
32
  return [];
31
33
  }
@@ -1,4 +1,5 @@
1
1
  const require_utils = require("../../../common/utils.cjs");
2
+ const require_utils$1 = require("../../utils.cjs");
2
3
  const require_reddit = require("../../../feeds/platform/handlers/reddit.cjs");
3
4
  //#region src/favicons/platform/handlers/reddit.ts
4
5
  const isSubredditPath = (pathname) => {
@@ -22,16 +23,14 @@ const redditHandler = {
22
23
  try {
23
24
  const { pathname } = new URL(url);
24
25
  if (isSubredditPath(pathname)) {
25
- const response = await fetchFn(`https://www.reddit.com/r/${pathname.split("/").filter(Boolean)[1]}/about.json`);
26
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
26
+ const data = require_utils$1.parseBodyJson((await fetchFn(`https://www.reddit.com/r/${pathname.split("/").filter(Boolean)[1]}/about.json`)).body);
27
27
  const icon = data?.data?.community_icon?.split("?")[0] || data?.data?.icon_img;
28
- if (typeof icon === "string" && icon.length > 0) return [{ uri: icon }];
28
+ if (require_utils$1.isNonEmptyString(icon)) return [{ uri: icon }];
29
29
  }
30
30
  if (isUserPath(pathname)) {
31
- const response = await fetchFn(`https://www.reddit.com/user/${pathname.split("/").filter(Boolean)[1]}/about.json`);
32
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
31
+ const data = require_utils$1.parseBodyJson((await fetchFn(`https://www.reddit.com/user/${pathname.split("/").filter(Boolean)[1]}/about.json`)).body);
33
32
  const icon = data?.data?.icon_img || data?.data?.snoovatar_img;
34
- if (typeof icon === "string" && icon.length > 0) return [{ uri: icon }];
33
+ if (require_utils$1.isNonEmptyString(icon)) return [{ uri: icon }];
35
34
  }
36
35
  } catch {}
37
36
  return [];
@@ -1,4 +1,5 @@
1
1
  import { isHostOf } from "../../../common/utils.js";
2
+ import { isNonEmptyString, parseBodyJson } from "../../utils.js";
2
3
  import { hosts } from "../../../feeds/platform/handlers/reddit.js";
3
4
  //#region src/favicons/platform/handlers/reddit.ts
4
5
  const isSubredditPath = (pathname) => {
@@ -22,16 +23,14 @@ const redditHandler = {
22
23
  try {
23
24
  const { pathname } = new URL(url);
24
25
  if (isSubredditPath(pathname)) {
25
- const response = await fetchFn(`https://www.reddit.com/r/${pathname.split("/").filter(Boolean)[1]}/about.json`);
26
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
26
+ const data = parseBodyJson((await fetchFn(`https://www.reddit.com/r/${pathname.split("/").filter(Boolean)[1]}/about.json`)).body);
27
27
  const icon = data?.data?.community_icon?.split("?")[0] || data?.data?.icon_img;
28
- if (typeof icon === "string" && icon.length > 0) return [{ uri: icon }];
28
+ if (isNonEmptyString(icon)) return [{ uri: icon }];
29
29
  }
30
30
  if (isUserPath(pathname)) {
31
- const response = await fetchFn(`https://www.reddit.com/user/${pathname.split("/").filter(Boolean)[1]}/about.json`);
32
- const data = JSON.parse(typeof response.body === "string" ? response.body : "");
31
+ const data = parseBodyJson((await fetchFn(`https://www.reddit.com/user/${pathname.split("/").filter(Boolean)[1]}/about.json`)).body);
33
32
  const icon = data?.data?.icon_img || data?.data?.snoovatar_img;
34
- if (typeof icon === "string" && icon.length > 0) return [{ uri: icon }];
33
+ if (isNonEmptyString(icon)) return [{ uri: icon }];
35
34
  }
36
35
  } catch {}
37
36
  return [];
@@ -0,0 +1,10 @@
1
+ //#region src/favicons/utils.ts
2
+ const parseBodyJson = (body) => {
3
+ return JSON.parse(typeof body === "string" ? body : "");
4
+ };
5
+ const isNonEmptyString = (value) => {
6
+ return typeof value === "string" && value.length > 0;
7
+ };
8
+ //#endregion
9
+ exports.isNonEmptyString = isNonEmptyString;
10
+ exports.parseBodyJson = parseBodyJson;
@@ -0,0 +1,9 @@
1
+ //#region src/favicons/utils.ts
2
+ const parseBodyJson = (body) => {
3
+ return JSON.parse(typeof body === "string" ? body : "");
4
+ };
5
+ const isNonEmptyString = (value) => {
6
+ return typeof value === "string" && value.length > 0;
7
+ };
8
+ //#endregion
9
+ export { isNonEmptyString, parseBodyJson };
@@ -19,6 +19,7 @@ const require_hashnode = require("./platform/handlers/hashnode.cjs");
19
19
  const require_hatenablog = require("./platform/handlers/hatenablog.cjs");
20
20
  const require_itchio = require("./platform/handlers/itchio.cjs");
21
21
  const require_kickstarter = require("./platform/handlers/kickstarter.cjs");
22
+ const require_lemmy = require("./platform/handlers/lemmy.cjs");
22
23
  const require_letterboxd = require("./platform/handlers/letterboxd.cjs");
23
24
  const require_mastodon = require("./platform/handlers/mastodon.cjs");
24
25
  const require_medium = require("./platform/handlers/medium.cjs");
@@ -139,6 +140,7 @@ const defaultPlatformOptions = { handlers: [
139
140
  require_gitlab.gitlabHandler,
140
141
  require_itchio.itchioHandler,
141
142
  require_kickstarter.kickstarterHandler,
143
+ require_lemmy.lemmyHandler,
142
144
  require_letterboxd.letterboxdHandler,
143
145
  require_lobsters.lobstersHandler,
144
146
  require_mastodon.mastodonHandler,
@@ -19,6 +19,7 @@ import { hashnodeHandler } from "./platform/handlers/hashnode.js";
19
19
  import { hatenablogHandler } from "./platform/handlers/hatenablog.js";
20
20
  import { itchioHandler } from "./platform/handlers/itchio.js";
21
21
  import { kickstarterHandler } from "./platform/handlers/kickstarter.js";
22
+ import { lemmyHandler } from "./platform/handlers/lemmy.js";
22
23
  import { letterboxdHandler } from "./platform/handlers/letterboxd.js";
23
24
  import { mastodonHandler } from "./platform/handlers/mastodon.js";
24
25
  import { mediumHandler } from "./platform/handlers/medium.js";
@@ -139,6 +140,7 @@ const defaultPlatformOptions = { handlers: [
139
140
  gitlabHandler,
140
141
  itchioHandler,
141
142
  kickstarterHandler,
143
+ lemmyHandler,
142
144
  letterboxdHandler,
143
145
  lobstersHandler,
144
146
  mastodonHandler,
@@ -1,22 +1,24 @@
1
+ const require_utils = require("../common/utils.cjs");
2
+ const require_utils$1 = require("../common/discover/utils.cjs");
1
3
  let feedsmith = require("feedsmith");
2
4
  //#region src/feeds/extractors.ts
3
- const getLinkOfType = (links, rel) => {
4
- return links?.find((link) => link.rel === rel);
5
- };
6
- const defaultExtractor = async ({ content, url }) => {
5
+ const defaultExtractor = ({ content, url }) => {
7
6
  if (!content) return {
8
7
  url,
9
8
  isValid: false
10
9
  };
11
10
  try {
12
- const { format, feed } = (0, feedsmith.parseFeed)(content);
11
+ const parsed = (0, feedsmith.parseFeed)(content);
12
+ const { format, feed } = parsed;
13
+ const rawSiteUrl = require_utils$1.getFeedSiteUrl(parsed);
14
+ const siteUrl = rawSiteUrl ? require_utils.resolveUrl(rawSiteUrl, url) : void 0;
13
15
  if (format === "rss" || format === "rdf") return {
14
16
  url,
15
17
  isValid: true,
16
18
  format,
17
19
  title: feed.title,
18
20
  description: feed.description,
19
- siteUrl: getLinkOfType(feed.atom?.links, "alternate")?.href || feed.link
21
+ siteUrl
20
22
  };
21
23
  if (format === "atom") return {
22
24
  url,
@@ -24,7 +26,7 @@ const defaultExtractor = async ({ content, url }) => {
24
26
  format,
25
27
  title: feed.title,
26
28
  description: feed.subtitle,
27
- siteUrl: getLinkOfType(feed.links, "alternate")?.href
29
+ siteUrl
28
30
  };
29
31
  if (format === "json") return {
30
32
  url,
@@ -32,7 +34,7 @@ const defaultExtractor = async ({ content, url }) => {
32
34
  format,
33
35
  title: feed.title,
34
36
  description: feed.description,
35
- siteUrl: feed.home_page_url
37
+ siteUrl
36
38
  };
37
39
  } catch {}
38
40
  return {
@@ -1,22 +1,24 @@
1
+ import { resolveUrl } from "../common/utils.js";
2
+ import { getFeedSiteUrl } from "../common/discover/utils.js";
1
3
  import { parseFeed } from "feedsmith";
2
4
  //#region src/feeds/extractors.ts
3
- const getLinkOfType = (links, rel) => {
4
- return links?.find((link) => link.rel === rel);
5
- };
6
- const defaultExtractor = async ({ content, url }) => {
5
+ const defaultExtractor = ({ content, url }) => {
7
6
  if (!content) return {
8
7
  url,
9
8
  isValid: false
10
9
  };
11
10
  try {
12
- const { format, feed } = parseFeed(content);
11
+ const parsed = parseFeed(content);
12
+ const { format, feed } = parsed;
13
+ const rawSiteUrl = getFeedSiteUrl(parsed);
14
+ const siteUrl = rawSiteUrl ? resolveUrl(rawSiteUrl, url) : void 0;
13
15
  if (format === "rss" || format === "rdf") return {
14
16
  url,
15
17
  isValid: true,
16
18
  format,
17
19
  title: feed.title,
18
20
  description: feed.description,
19
- siteUrl: getLinkOfType(feed.atom?.links, "alternate")?.href || feed.link
21
+ siteUrl
20
22
  };
21
23
  if (format === "atom") return {
22
24
  url,
@@ -24,7 +26,7 @@ const defaultExtractor = async ({ content, url }) => {
24
26
  format,
25
27
  title: feed.title,
26
28
  description: feed.subtitle,
27
- siteUrl: getLinkOfType(feed.links, "alternate")?.href
29
+ siteUrl
28
30
  };
29
31
  if (format === "json") return {
30
32
  url,
@@ -32,7 +34,7 @@ const defaultExtractor = async ({ content, url }) => {
32
34
  format,
33
35
  title: feed.title,
34
36
  description: feed.description,
35
- siteUrl: feed.home_page_url
37
+ siteUrl
36
38
  };
37
39
  } catch {}
38
40
  return {
@@ -4,7 +4,7 @@ const require_index = require("../common/discover/index.cjs");
4
4
  const require_defaults = require("./defaults.cjs");
5
5
  const require_extractors = require("./extractors.cjs");
6
6
  //#region src/feeds/index.ts
7
- const discoverFeeds = async (input, options = {}) => {
7
+ const discoverFeeds = (input, options = {}) => {
8
8
  return require_index.discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -15,7 +15,7 @@ const discoverFeeds = async (input, options = {}) => {
15
15
  ],
16
16
  fetchFn: options.fetchFn ?? require_utils$1.defaultFetchFn,
17
17
  extractFn: options.extractFn ?? require_extractors.defaultExtractor,
18
- normalizeUrlFn: options.normalizeUrlFn ?? require_utils.normalizeUrl
18
+ resolveUrlFn: options.resolveUrlFn ?? require_utils.resolveUrl
19
19
  }, {
20
20
  platform: require_defaults.defaultPlatformOptions,
21
21
  html: require_defaults.defaultHtmlOptions,
@@ -1,10 +1,10 @@
1
- import { normalizeUrl } from "../common/utils.js";
1
+ import { resolveUrl } from "../common/utils.js";
2
2
  import { defaultFetchFn } from "../common/discover/utils.js";
3
3
  import { discover } from "../common/discover/index.js";
4
4
  import { defaultGuessOptions, defaultHeadersOptions, defaultHtmlOptions, defaultPlatformOptions } from "./defaults.js";
5
5
  import { defaultExtractor } from "./extractors.js";
6
6
  //#region src/feeds/index.ts
7
- const discoverFeeds = async (input, options = {}) => {
7
+ const discoverFeeds = (input, options = {}) => {
8
8
  return discover(input, {
9
9
  ...options,
10
10
  methods: options.methods ?? [
@@ -15,7 +15,7 @@ const discoverFeeds = async (input, options = {}) => {
15
15
  ],
16
16
  fetchFn: options.fetchFn ?? defaultFetchFn,
17
17
  extractFn: options.extractFn ?? defaultExtractor,
18
- normalizeUrlFn: options.normalizeUrlFn ?? normalizeUrl
18
+ resolveUrlFn: options.resolveUrlFn ?? resolveUrl
19
19
  }, {
20
20
  platform: defaultPlatformOptions,
21
21
  html: defaultHtmlOptions,
@@ -1,6 +1,7 @@
1
1
  const require_utils = require("../../../common/utils.cjs");
2
2
  //#region src/feeds/platform/handlers/blogspot.ts
3
3
  const blogspotDomainRegex = /^.+\.blogspot\.(?:com|co\.[a-z]{2}|com\.[a-z]{2}|[a-z]{2,3})$/;
4
+ const labelRegex = /^\/search\/label\/([^/]+)/;
4
5
  const blogspotHandler = {
5
6
  match: (url) => {
6
7
  try {
@@ -12,7 +13,7 @@ const blogspotHandler = {
12
13
  resolve: (url) => {
13
14
  const { origin, pathname } = new URL(url);
14
15
  const uris = [];
15
- const labelMatch = pathname.match(/^\/search\/label\/([^/]+)/);
16
+ const labelMatch = pathname.match(labelRegex);
16
17
  if (labelMatch?.[1]) {
17
18
  const label = labelMatch[1];
18
19
  uris.push({
@@ -1,6 +1,7 @@
1
1
  import { composeHint } from "../../../common/utils.js";
2
2
  //#region src/feeds/platform/handlers/blogspot.ts
3
3
  const blogspotDomainRegex = /^.+\.blogspot\.(?:com|co\.[a-z]{2}|com\.[a-z]{2}|[a-z]{2,3})$/;
4
+ const labelRegex = /^\/search\/label\/([^/]+)/;
4
5
  const blogspotHandler = {
5
6
  match: (url) => {
6
7
  try {
@@ -12,7 +13,7 @@ const blogspotHandler = {
12
13
  resolve: (url) => {
13
14
  const { origin, pathname } = new URL(url);
14
15
  const uris = [];
15
- const labelMatch = pathname.match(/^\/search\/label\/([^/]+)/);
16
+ const labelMatch = pathname.match(labelRegex);
16
17
  if (labelMatch?.[1]) {
17
18
  const label = labelMatch[1];
18
19
  uris.push({
@@ -1,5 +1,6 @@
1
1
  const require_utils = require("../../../common/utils.cjs");
2
2
  //#region src/feeds/platform/handlers/bluesky.ts
3
+ const profileRegex = /^\/profile\/([^/]+)/;
3
4
  const hosts = ["bsky.app", "www.bsky.app"];
4
5
  const blueskyHandler = {
5
6
  match: (url) => {
@@ -7,7 +8,7 @@ const blueskyHandler = {
7
8
  },
8
9
  resolve: (url) => {
9
10
  const { pathname } = new URL(url);
10
- const handle = pathname.match(/^\/profile\/([^/]+)/)?.[1];
11
+ const handle = pathname.match(profileRegex)?.[1];
11
12
  if (!handle) return [];
12
13
  return [{
13
14
  uri: `https://bsky.app/profile/${handle}/rss`,
@@ -1,5 +1,6 @@
1
1
  import { composeHint, isHostOf } from "../../../common/utils.js";
2
2
  //#region src/feeds/platform/handlers/bluesky.ts
3
+ const profileRegex = /^\/profile\/([^/]+)/;
3
4
  const hosts = ["bsky.app", "www.bsky.app"];
4
5
  const blueskyHandler = {
5
6
  match: (url) => {
@@ -7,7 +8,7 @@ const blueskyHandler = {
7
8
  },
8
9
  resolve: (url) => {
9
10
  const { pathname } = new URL(url);
10
- const handle = pathname.match(/^\/profile\/([^/]+)/)?.[1];
11
+ const handle = pathname.match(profileRegex)?.[1];
11
12
  if (!handle) return [];
12
13
  return [{
13
14
  uri: `https://bsky.app/profile/${handle}/rss`,
@@ -1,5 +1,6 @@
1
1
  const require_utils = require("../../../common/utils.cjs");
2
2
  //#region src/feeds/platform/handlers/csdn.ts
3
+ const userRegex = /^\/([^/]+)/;
3
4
  const hosts = ["blog.csdn.net"];
4
5
  const csdnHandler = {
5
6
  match: (url) => {
@@ -7,7 +8,7 @@ const csdnHandler = {
7
8
  },
8
9
  resolve: (url) => {
9
10
  const { pathname } = new URL(url);
10
- const username = pathname.match(/^\/([^/]+)/)?.[1];
11
+ const username = pathname.match(userRegex)?.[1];
11
12
  if (!username) return [];
12
13
  return [{
13
14
  uri: [`https://rss.csdn.net/${username}/rss/map`, `https://blog.csdn.net/${username}/rss/list`],