@mh-gg/base-plugins 0.1.1-alpha.20260613T085325975Z
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 +247 -0
- package/package.json +48 -0
- package/src/approvals/index.cjs +65 -0
- package/src/attachments/index.cjs +75 -0
- package/src/calendar/index.cjs +48 -0
- package/src/checklists/index.cjs +58 -0
- package/src/comments/index.cjs +15 -0
- package/src/comments/plugin.cjs +66 -0
- package/src/comments/reducer.cjs +81 -0
- package/src/comments/schemas.cjs +60 -0
- package/src/comments/state.cjs +17 -0
- package/src/comments/threads.cjs +44 -0
- package/src/comments/views.cjs +27 -0
- package/src/composer/capabilities.cjs +19 -0
- package/src/composer/compose.cjs +37 -0
- package/src/composer/index.cjs +15 -0
- package/src/composer/operations.cjs +42 -0
- package/src/composer/registry.cjs +155 -0
- package/src/composer/selection.cjs +39 -0
- package/src/composer/suite.cjs +32 -0
- package/src/crdt/client.mjs +207 -0
- package/src/crdt/index.cjs +258 -0
- package/src/embeds/index.cjs +90 -0
- package/src/files/index.cjs +133 -0
- package/src/index.cjs +19 -0
- package/src/labels/index.cjs +46 -0
- package/src/location-pins/index.cjs +142 -0
- package/src/markdown/documents/index.cjs +128 -0
- package/src/markdown/index.cjs +8 -0
- package/src/markdown/parser/index.cjs +127 -0
- package/src/markdown/providers/audio.cjs +77 -0
- package/src/markdown/providers/cloud.cjs +72 -0
- package/src/markdown/providers/developer.cjs +45 -0
- package/src/markdown/providers/direct.cjs +49 -0
- package/src/markdown/providers/games.cjs +26 -0
- package/src/markdown/providers/images.cjs +88 -0
- package/src/markdown/providers/index.cjs +97 -0
- package/src/markdown/providers/maps.cjs +24 -0
- package/src/markdown/providers/productivity.cjs +30 -0
- package/src/markdown/providers/res-inspired.cjs +11 -0
- package/src/markdown/providers/social.cjs +33 -0
- package/src/markdown/providers/video.cjs +139 -0
- package/src/markdown/resolve.cjs +87 -0
- package/src/media-rooms/index.cjs +244 -0
- package/src/presence/index.cjs +193 -0
- package/src/reactions/index.cjs +47 -0
- package/src/screen-share/index.cjs +84 -0
- package/src/shared/constants.cjs +87 -0
- package/src/shared/embed.cjs +82 -0
- package/src/shared/index.cjs +20 -0
- package/src/shared/roles.cjs +5 -0
- package/src/shared/scopes.cjs +15 -0
- package/src/shared/url.cjs +32 -0
- package/src/shared/validation.cjs +31 -0
- package/test/composable-plugins.test.cjs +170 -0
- package/test/crdt-plugin.test.cjs +168 -0
- package/test/embed-autodetect-providers.test.cjs +138 -0
- package/test/markdown-media-workflow-plugins.test.cjs +201 -0
- package/test/markdown-parser-edge-cases.test.cjs +86 -0
- package/test/plugin-structure.test.cjs +69 -0
- package/test/shared-plugin-edges.test.cjs +207 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
function directFilePlugin({ id, provider, aliases, extensions, kind, title, renderMode }) {
|
|
17
|
+
const pattern = new RegExp(`\\.(?:${extensions.join("|")})$`, "i");
|
|
18
|
+
return defineMarkdownPlugin({
|
|
19
|
+
id,
|
|
20
|
+
provider,
|
|
21
|
+
aliases,
|
|
22
|
+
matchUrl(url) { return pattern.test(new URL(url).pathname); },
|
|
23
|
+
toEmbed(url, ctx = {}) { return createEmbedRecord({ provider, kind, title: ctx.title || title, url, embedUrl: url, renderMode, metadata: { extension: new URL(url).pathname.split(".").pop().toLowerCase() } }); }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const directImageMarkdownPlugin = directFilePlugin({ id: "markdown.embed.direct-image", provider: "image", aliases: ["image", "img"], extensions: ["png", "jpe?g", "webp", "gif", "avif", "svg"], kind: "image", title: "Image", renderMode: "image" });
|
|
27
|
+
const directVideoMarkdownPlugin = directFilePlugin({ id: "markdown.embed.direct-video", provider: "video", aliases: ["video", "mp4"], extensions: ["mp4", "webm", "mov", "m4v"], kind: "video", title: "Video", renderMode: "video" });
|
|
28
|
+
const directAudioMarkdownPlugin = directFilePlugin({ id: "markdown.embed.direct-audio", provider: "audio", aliases: ["audio"], extensions: ["mp3", "wav", "ogg", "m4a", "flac"], kind: "audio", title: "Audio", renderMode: "audio" });
|
|
29
|
+
const directDocumentMarkdownPlugin = directFilePlugin({ id: "markdown.embed.direct-document", provider: "document", aliases: ["pdf", "doc"], extensions: ["pdf"], kind: "document", title: "Document", renderMode: "file-preview" });
|
|
30
|
+
|
|
31
|
+
const genericLinkMarkdownPlugin = defineMarkdownPlugin({
|
|
32
|
+
id: "markdown.embed.generic-link",
|
|
33
|
+
provider: "link",
|
|
34
|
+
aliases: ["embed", "link", "url"],
|
|
35
|
+
matchUrl() { return true; },
|
|
36
|
+
toEmbed(url, ctx = {}) {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
return createEmbedRecord({ provider: "link", kind: "link", title: ctx.title || parsed.hostname.replace(/^www\./, ""), url, renderMode: "card", metadata: { host: parsed.hostname.toLowerCase() } });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
directAudioMarkdownPlugin,
|
|
45
|
+
directDocumentMarkdownPlugin,
|
|
46
|
+
directImageMarkdownPlugin,
|
|
47
|
+
directVideoMarkdownPlugin,
|
|
48
|
+
genericLinkMarkdownPlugin
|
|
49
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const steamMarkdownPlugin = simpleProvider({ id: "markdown.embed.steam", provider: "steam", aliases: ["steam", "steampowered", "steamcommunity"], hosts: ["store.steampowered.com", "steamcommunity.com"], kind: "game", title: "Steam item", renderMode: "external-link" });
|
|
17
|
+
const xboxDvrMarkdownPlugin = simpleProvider({ id: "markdown.embed.xboxdvr", provider: "xboxdvr", aliases: ["xboxdvr"], hostSuffixes: ["xboxdvr.com", "gamerdvr.com"], kind: "game-clip", title: "Game clip", renderMode: "external-link" });
|
|
18
|
+
const xkcdMarkdownPlugin = simpleProvider({ id: "markdown.embed.xkcd", provider: "xkcd", aliases: ["xkcd"], hosts: ["xkcd.com", "www.xkcd.com"], kind: "comic", title: (parsed) => `xkcd ${pathParts(parsed.toString())[0] || "comic"}`, renderMode: "external-link" });
|
|
19
|
+
const strawpollMarkdownPlugin = simpleProvider({ id: "markdown.embed.strawpoll", provider: "strawpoll", aliases: ["strawpoll", "poll"], hostSuffixes: ["strawpoll.com", "strawpoll.me"], kind: "poll", title: "Strawpoll", renderMode: "external-link" });
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
steamMarkdownPlugin,
|
|
23
|
+
strawpollMarkdownPlugin,
|
|
24
|
+
xboxDvrMarkdownPlugin,
|
|
25
|
+
xkcdMarkdownPlugin
|
|
26
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const imgurMarkdownPlugin = defineMarkdownPlugin({
|
|
17
|
+
id: "markdown.embed.imgur",
|
|
18
|
+
provider: "imgur",
|
|
19
|
+
aliases: ["imgur"],
|
|
20
|
+
matchUrl(url) { return hostEndsWith(url, ["imgur.com"]); },
|
|
21
|
+
toEmbed(url, ctx = {}) {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
const host = safeHostname(url);
|
|
24
|
+
const parts = pathParts(url);
|
|
25
|
+
const direct = host === "i.imgur.com" || /\.(?:png|jpe?g|gifv?|mp4|webm)$/i.test(parsed.pathname);
|
|
26
|
+
const albumId = parts[0] === "a" || parts[0] === "gallery" ? parts[1] : null;
|
|
27
|
+
const rawId = albumId || (direct ? parts.at(-1)?.replace(/\.(?:png|jpe?g|gifv?|mp4|webm)$/i, "") : parts[0]);
|
|
28
|
+
if (!rawId) throw new Error("Imgur URL is missing an id");
|
|
29
|
+
let embedUrl;
|
|
30
|
+
let renderMode = "external-link";
|
|
31
|
+
let kind = albumId ? "gallery" : mediaKindFromPath(parsed.pathname);
|
|
32
|
+
if (direct && kind === "image") { embedUrl = host === "i.imgur.com" ? url : `https://i.imgur.com/${cleanId(rawId)}.jpg`; renderMode = "image"; }
|
|
33
|
+
else if (direct && (kind === "video" || parsed.pathname.endsWith(".gifv"))) { embedUrl = `https://i.imgur.com/${cleanId(rawId)}.mp4`; renderMode = "video"; kind = "video"; }
|
|
34
|
+
return createEmbedRecord({ provider: "imgur", kind, title: ctx.title || (albumId ? "Imgur album" : "Imgur media"), url, ...(embedUrl ? { embedUrl } : {}), renderMode, metadata: { imgurId: rawId, albumId: albumId || null } });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const giphyMarkdownPlugin = defineMarkdownPlugin({
|
|
39
|
+
id: "markdown.embed.giphy",
|
|
40
|
+
provider: "giphy",
|
|
41
|
+
aliases: ["giphy"],
|
|
42
|
+
matchUrl(url) { return hostEndsWith(url, ["giphy.com"]); },
|
|
43
|
+
toEmbed(url, ctx = {}) {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
const id = firstMatch([/\/gifs\/(?:[^/]+-)?([A-Za-z0-9]+)$/, /\/media\/([^/]+)\//], parsed.pathname)?.[1] || pathParts(url).at(-1);
|
|
46
|
+
return createEmbedRecord({ provider: "giphy", kind: "gif", title: ctx.title || "Giphy GIF", url, embedUrl: `https://media.giphy.com/media/${cleanId(id)}/giphy.gif`, renderMode: "image", metadata: { gifId: id } });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const tenorMarkdownPlugin = defineMarkdownPlugin({
|
|
51
|
+
id: "markdown.embed.tenor",
|
|
52
|
+
provider: "tenor",
|
|
53
|
+
aliases: ["tenor"],
|
|
54
|
+
matchUrl(url) { return hostEndsWith(url, ["tenor.com", "tenor.co", "media.tenor.com"]); },
|
|
55
|
+
toEmbed(url, ctx = {}) {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
const kind = safeHostname(url).startsWith("media.tenor") ? mediaKindFromPath(parsed.pathname) : "gif";
|
|
58
|
+
return createEmbedRecord({ provider: "tenor", kind, title: ctx.title || "Tenor GIF", url, ...(kind === "image" || kind === "video" ? { embedUrl: url } : {}), renderMode: kind === "image" ? "image" : kind === "video" ? "video" : "external-link", metadata: { host: safeHostname(url) } });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const redgifsMarkdownPlugin = simpleProvider({ id: "markdown.embed.redgifs", provider: "redgifs", aliases: ["redgifs"], hosts: ["redgifs.com", "www.redgifs.com"], kind: "video", title: "Redgifs video", renderMode: "external-link", metadata: (parsed) => ({ slug: pathParts(parsed.toString()).at(-1) || null }) });
|
|
63
|
+
const gfycatMarkdownPlugin = simpleProvider({ id: "markdown.embed.gfycat", provider: "gfycat", aliases: ["gfycat"], hosts: ["gfycat.com", "www.gfycat.com"], kind: "video", title: "Gfycat video", renderMode: "external-link", metadata: (parsed) => ({ slug: pathParts(parsed.toString()).at(-1) || null }) });
|
|
64
|
+
const gyazoMarkdownPlugin = simpleProvider({ id: "markdown.embed.gyazo", provider: "gyazo", aliases: ["gyazo"], hosts: ["gyazo.com", "i.gyazo.com"], kind: (parsed) => mediaKindFromPath(parsed.pathname) === "file" ? "image" : mediaKindFromPath(parsed.pathname), title: "Gyazo capture", renderMode: (_parsed, kind) => directRenderMode(kind) });
|
|
65
|
+
|
|
66
|
+
const flickrMarkdownPlugin = simpleProvider({ id: "markdown.embed.flickr", provider: "flickr", aliases: ["flickr"], hostSuffixes: ["flickr.com", "staticflickr.com"], kind: "photo", title: "Flickr photo", renderMode: "external-link" });
|
|
67
|
+
const fiveHundredPxMarkdownPlugin = simpleProvider({ id: "markdown.embed.500px", provider: "500px", aliases: ["500px", "fivehundredpx"], hosts: ["500px.com"], kind: "photo", title: "500px photo", renderMode: "external-link" });
|
|
68
|
+
const deviantArtMarkdownPlugin = simpleProvider({ id: "markdown.embed.deviantart", provider: "deviantart", aliases: ["deviantart"], hostSuffixes: ["deviantart.com", "deviantart.net"], kind: "artwork", title: "DeviantArt artwork", renderMode: "external-link" });
|
|
69
|
+
const photobucketMarkdownPlugin = simpleProvider({ id: "markdown.embed.photobucket", provider: "photobucket", aliases: ["photobucket"], hostSuffixes: ["photobucket.com"], kind: "photo", title: "Photobucket image", renderMode: "external-link" });
|
|
70
|
+
const pixivMarkdownPlugin = simpleProvider({ id: "markdown.embed.pixiv", provider: "pixiv", aliases: ["pixiv"], hosts: ["pixiv.net", "www.pixiv.net"], kind: "artwork", title: "Pixiv artwork", renderMode: "external-link" });
|
|
71
|
+
const tumblrMarkdownPlugin = simpleProvider({ id: "markdown.embed.tumblr", provider: "tumblr", aliases: ["tumblr"], hostSuffixes: ["tumblr.com"], kind: "post", title: "Tumblr post", renderMode: "external-link" });
|
|
72
|
+
const twimgMarkdownPlugin = simpleProvider({ id: "markdown.embed.twimg", provider: "twimg", aliases: ["twimg"], hostSuffixes: ["twimg.com"], kind: (parsed) => mediaKindFromPath(parsed.pathname), title: "Twitter media", renderMode: (_parsed, kind) => directRenderMode(kind) });
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
deviantArtMarkdownPlugin,
|
|
76
|
+
fiveHundredPxMarkdownPlugin,
|
|
77
|
+
flickrMarkdownPlugin,
|
|
78
|
+
gfycatMarkdownPlugin,
|
|
79
|
+
giphyMarkdownPlugin,
|
|
80
|
+
gyazoMarkdownPlugin,
|
|
81
|
+
imgurMarkdownPlugin,
|
|
82
|
+
photobucketMarkdownPlugin,
|
|
83
|
+
pixivMarkdownPlugin,
|
|
84
|
+
redgifsMarkdownPlugin,
|
|
85
|
+
tenorMarkdownPlugin,
|
|
86
|
+
tumblrMarkdownPlugin,
|
|
87
|
+
twimgMarkdownPlugin
|
|
88
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { RES_INSPIRED_EMBED_PROVIDERS } = require("./res-inspired.cjs");
|
|
2
|
+
const video = require("./video.cjs");
|
|
3
|
+
const audio = require("./audio.cjs");
|
|
4
|
+
const cloud = require("./cloud.cjs");
|
|
5
|
+
const images = require("./images.cjs");
|
|
6
|
+
const social = require("./social.cjs");
|
|
7
|
+
const developer = require("./developer.cjs");
|
|
8
|
+
const maps = require("./maps.cjs");
|
|
9
|
+
const productivity = require("./productivity.cjs");
|
|
10
|
+
const games = require("./games.cjs");
|
|
11
|
+
const direct = require("./direct.cjs");
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MARKDOWN_PLUGINS = Object.freeze([
|
|
14
|
+
video.youtubeMarkdownPlugin,
|
|
15
|
+
audio.spotifyMarkdownPlugin,
|
|
16
|
+
cloud.googleDriveMarkdownPlugin,
|
|
17
|
+
cloud.googlePhotosMarkdownPlugin,
|
|
18
|
+
cloud.dropboxMarkdownPlugin,
|
|
19
|
+
video.vimeoMarkdownPlugin,
|
|
20
|
+
audio.soundcloudMarkdownPlugin,
|
|
21
|
+
audio.bandcampMarkdownPlugin,
|
|
22
|
+
audio.appleMusicMarkdownPlugin,
|
|
23
|
+
video.twitchMarkdownPlugin,
|
|
24
|
+
video.dailymotionMarkdownPlugin,
|
|
25
|
+
video.streamableMarkdownPlugin,
|
|
26
|
+
video.streamjaMarkdownPlugin,
|
|
27
|
+
video.coubMarkdownPlugin,
|
|
28
|
+
images.imgurMarkdownPlugin,
|
|
29
|
+
images.giphyMarkdownPlugin,
|
|
30
|
+
images.tenorMarkdownPlugin,
|
|
31
|
+
images.redgifsMarkdownPlugin,
|
|
32
|
+
images.gfycatMarkdownPlugin,
|
|
33
|
+
images.gyazoMarkdownPlugin,
|
|
34
|
+
images.flickrMarkdownPlugin,
|
|
35
|
+
images.fiveHundredPxMarkdownPlugin,
|
|
36
|
+
images.deviantArtMarkdownPlugin,
|
|
37
|
+
images.photobucketMarkdownPlugin,
|
|
38
|
+
images.pixivMarkdownPlugin,
|
|
39
|
+
images.tumblrMarkdownPlugin,
|
|
40
|
+
images.twimgMarkdownPlugin,
|
|
41
|
+
social.instagramMarkdownPlugin,
|
|
42
|
+
social.facebookVideoMarkdownPlugin,
|
|
43
|
+
social.tiktokMarkdownPlugin,
|
|
44
|
+
social.twitterMarkdownPlugin,
|
|
45
|
+
social.blueskyMarkdownPlugin,
|
|
46
|
+
social.redditMarkdownPlugin,
|
|
47
|
+
social.imgflipMarkdownPlugin,
|
|
48
|
+
developer.codepenMarkdownPlugin,
|
|
49
|
+
developer.jsfiddleMarkdownPlugin,
|
|
50
|
+
developer.githubMarkdownPlugin,
|
|
51
|
+
developer.gistMarkdownPlugin,
|
|
52
|
+
developer.pastebinMarkdownPlugin,
|
|
53
|
+
developer.hastebinMarkdownPlugin,
|
|
54
|
+
developer.wikipediaMarkdownPlugin,
|
|
55
|
+
maps.googleMapsMarkdownPlugin,
|
|
56
|
+
maps.openStreetMapMarkdownPlugin,
|
|
57
|
+
maps.rideWithGpsMarkdownPlugin,
|
|
58
|
+
cloud.onedriveMarkdownPlugin,
|
|
59
|
+
productivity.figmaMarkdownPlugin,
|
|
60
|
+
productivity.notionMarkdownPlugin,
|
|
61
|
+
productivity.loomMarkdownPlugin,
|
|
62
|
+
productivity.canvaMarkdownPlugin,
|
|
63
|
+
productivity.miroMarkdownPlugin,
|
|
64
|
+
productivity.excalidrawMarkdownPlugin,
|
|
65
|
+
games.steamMarkdownPlugin,
|
|
66
|
+
games.xboxDvrMarkdownPlugin,
|
|
67
|
+
games.xkcdMarkdownPlugin,
|
|
68
|
+
games.strawpollMarkdownPlugin,
|
|
69
|
+
video.peertubeMarkdownPlugin,
|
|
70
|
+
audio.clypMarkdownPlugin,
|
|
71
|
+
video.getyarnMarkdownPlugin,
|
|
72
|
+
direct.directImageMarkdownPlugin,
|
|
73
|
+
direct.directVideoMarkdownPlugin,
|
|
74
|
+
direct.directAudioMarkdownPlugin,
|
|
75
|
+
direct.directDocumentMarkdownPlugin,
|
|
76
|
+
direct.genericLinkMarkdownPlugin
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const EMBED_PROVIDER_KEYS = Object.freeze(DEFAULT_MARKDOWN_PLUGINS.map((plugin) => plugin.provider));
|
|
80
|
+
const EMBED_PROVIDER_CAPABILITIES = Object.freeze(EMBED_PROVIDER_KEYS.map((provider) => `embed.${provider}`));
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
...video,
|
|
84
|
+
...audio,
|
|
85
|
+
...cloud,
|
|
86
|
+
...images,
|
|
87
|
+
...social,
|
|
88
|
+
...developer,
|
|
89
|
+
...maps,
|
|
90
|
+
...productivity,
|
|
91
|
+
...games,
|
|
92
|
+
...direct,
|
|
93
|
+
DEFAULT_MARKDOWN_PLUGINS,
|
|
94
|
+
EMBED_PROVIDER_CAPABILITIES,
|
|
95
|
+
EMBED_PROVIDER_KEYS,
|
|
96
|
+
RES_INSPIRED_EMBED_PROVIDERS
|
|
97
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const googleMapsMarkdownPlugin = simpleProvider({ id: "markdown.embed.google-maps", provider: "google-maps", aliases: ["maps", "googlemaps", "map"], hosts: ["maps.google.com", "google.com", "goo.gl"], kind: "map", title: "Google Maps location", renderMode: "external-link", matchPath: (parsed) => parsed.hostname === "maps.google.com" || parsed.pathname.startsWith("/maps") || parsed.hostname === "goo.gl" });
|
|
17
|
+
const openStreetMapMarkdownPlugin = simpleProvider({ id: "markdown.embed.openstreetmap", provider: "openstreetmap", aliases: ["osm", "openstreetmap"], hosts: ["openstreetmap.org", "www.openstreetmap.org"], kind: "map", title: "OpenStreetMap location", renderMode: "external-link" });
|
|
18
|
+
const rideWithGpsMarkdownPlugin = simpleProvider({ id: "markdown.embed.ridewithgps", provider: "ridewithgps", aliases: ["ridewithgps"], hosts: ["ridewithgps.com", "www.ridewithgps.com"], kind: "route", title: "Ride with GPS route", renderMode: "external-link" });
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
googleMapsMarkdownPlugin,
|
|
22
|
+
openStreetMapMarkdownPlugin,
|
|
23
|
+
rideWithGpsMarkdownPlugin
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const figmaMarkdownPlugin = simpleProvider({ id: "markdown.embed.figma", provider: "figma", aliases: ["figma"], hosts: ["figma.com", "www.figma.com"], kind: "design", title: "Figma file", renderMode: "external-link" });
|
|
17
|
+
const notionMarkdownPlugin = simpleProvider({ id: "markdown.embed.notion", provider: "notion", aliases: ["notion"], hostSuffixes: ["notion.site", "notion.so"], kind: "document", title: "Notion page", renderMode: "external-link" });
|
|
18
|
+
const loomMarkdownPlugin = simpleProvider({ id: "markdown.embed.loom", provider: "loom", aliases: ["loom"], hosts: ["loom.com", "www.loom.com"], kind: "video", title: "Loom video", renderMode: "external-link" });
|
|
19
|
+
const canvaMarkdownPlugin = simpleProvider({ id: "markdown.embed.canva", provider: "canva", aliases: ["canva"], hosts: ["canva.com", "www.canva.com"], kind: "design", title: "Canva design", renderMode: "external-link" });
|
|
20
|
+
const miroMarkdownPlugin = simpleProvider({ id: "markdown.embed.miro", provider: "miro", aliases: ["miro"], hostSuffixes: ["miro.com"], kind: "whiteboard", title: "Miro board", renderMode: "external-link" });
|
|
21
|
+
const excalidrawMarkdownPlugin = simpleProvider({ id: "markdown.embed.excalidraw", provider: "excalidraw", aliases: ["excalidraw"], hosts: ["excalidraw.com"], kind: "whiteboard", title: "Excalidraw drawing", renderMode: "external-link" });
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
canvaMarkdownPlugin,
|
|
25
|
+
excalidrawMarkdownPlugin,
|
|
26
|
+
figmaMarkdownPlugin,
|
|
27
|
+
loomMarkdownPlugin,
|
|
28
|
+
miroMarkdownPlugin,
|
|
29
|
+
notionMarkdownPlugin
|
|
30
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const RES_INSPIRED_EMBED_PROVIDERS = Object.freeze([
|
|
2
|
+
"aarli", "adultswim", "archilogic", "archiveis", "bime", "bluesky", "clyp", "codepen", "coub", "dailymotion",
|
|
3
|
+
"defaultImage", "defaultAudio", "defaultVideo", "deviantart", "dropbox", "facebookvideo", "fiveHundredPx", "flickr",
|
|
4
|
+
"gamerdvr", "getyarn", "gfycat", "giphy", "github", "googlemaps", "gyazo", "hastebin", "imgflip", "imgur",
|
|
5
|
+
"instagram", "ireddit", "jsfiddle", "onedrive", "pastebin", "peertube", "photobucket", "pixiv", "redditgallery",
|
|
6
|
+
"redditmedia", "redditpoll", "redgifs", "ridewithgps", "soundcloud", "spotify", "steamcommunity", "steampowered",
|
|
7
|
+
"strawpoll", "streamable", "streamja", "tenor", "tumblr", "twimg", "twitch", "twitchclips", "twitter", "vimeo",
|
|
8
|
+
"wikipedia", "xboxdvr", "xkcd", "youtube"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
module.exports = { RES_INSPIRED_EMBED_PROVIDERS };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const instagramMarkdownPlugin = simpleProvider({ id: "markdown.embed.instagram", provider: "instagram", aliases: ["instagram", "ig"], hosts: ["instagram.com"], kind: "social-post", title: "Instagram post", renderMode: "external-link", matchPath: (parsed) => ["p", "reel", "tv"].includes(pathParts(parsed.toString())[0]) });
|
|
17
|
+
const facebookVideoMarkdownPlugin = simpleProvider({ id: "markdown.embed.facebook-video", provider: "facebook-video", aliases: ["facebook", "fb"], hostSuffixes: ["facebook.com", "fb.watch"], kind: "video", title: "Facebook video", renderMode: "external-link" });
|
|
18
|
+
const tiktokMarkdownPlugin = simpleProvider({ id: "markdown.embed.tiktok", provider: "tiktok", aliases: ["tiktok"], hostSuffixes: ["tiktok.com"], kind: "video", title: "TikTok video", renderMode: "external-link" });
|
|
19
|
+
const twitterMarkdownPlugin = simpleProvider({ id: "markdown.embed.twitter", provider: "twitter", aliases: ["twitter", "x", "tweet"], hosts: ["twitter.com", "x.com", "mobile.twitter.com"], kind: "social-post", title: "Social post", renderMode: "external-link", matchPath: (parsed) => pathParts(parsed.toString()).includes("status") });
|
|
20
|
+
const blueskyMarkdownPlugin = simpleProvider({ id: "markdown.embed.bluesky", provider: "bluesky", aliases: ["bluesky", "bsky"], hosts: ["bsky.app"], kind: "social-post", title: "Bluesky post", renderMode: "external-link", matchPath: (parsed) => pathParts(parsed.toString()).includes("post") });
|
|
21
|
+
|
|
22
|
+
const redditMarkdownPlugin = simpleProvider({ id: "markdown.embed.reddit", provider: "reddit", aliases: ["reddit", "ireddit", "vreddit", "redditgallery", "redditpoll"], hostSuffixes: ["reddit.com", "redd.it", "i.redd.it", "v.redd.it", "redditmedia.com", "redditstatic.com"], kind: (parsed) => parsed.hostname.includes("i.redd.it") ? mediaKindFromPath(parsed.pathname) : parsed.hostname.includes("v.redd.it") ? "video" : parsed.pathname.startsWith("/gallery/") ? "gallery" : "reddit-post", title: "Reddit item", renderMode: (_parsed, kind) => directRenderMode(kind) === "card" ? "external-link" : directRenderMode(kind) });
|
|
23
|
+
const imgflipMarkdownPlugin = simpleProvider({ id: "markdown.embed.imgflip", provider: "imgflip", aliases: ["imgflip"], hosts: ["imgflip.com", "i.imgflip.com"], kind: "meme", title: "Imgflip meme", renderMode: "external-link" });
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
blueskyMarkdownPlugin,
|
|
27
|
+
facebookVideoMarkdownPlugin,
|
|
28
|
+
imgflipMarkdownPlugin,
|
|
29
|
+
instagramMarkdownPlugin,
|
|
30
|
+
redditMarkdownPlugin,
|
|
31
|
+
tiktokMarkdownPlugin,
|
|
32
|
+
twitterMarkdownPlugin
|
|
33
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const youtubeMarkdownPlugin = defineMarkdownPlugin({
|
|
17
|
+
id: "markdown.embed.youtube",
|
|
18
|
+
provider: "youtube",
|
|
19
|
+
aliases: ["youtube", "yt", "youtu"],
|
|
20
|
+
matchUrl(url) {
|
|
21
|
+
const parsed = new URL(url);
|
|
22
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
23
|
+
return ["youtube.com", "m.youtube.com", "youtu.be", "youtube-nocookie.com", "music.youtube.com"].includes(host);
|
|
24
|
+
},
|
|
25
|
+
toEmbed(url, ctx = {}) {
|
|
26
|
+
const parsed = new URL(url);
|
|
27
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
28
|
+
const id = host === "youtu.be"
|
|
29
|
+
? parsed.pathname.split("/").filter(Boolean)[0]
|
|
30
|
+
: parsed.searchParams.get("v") || firstMatch([/^\/shorts\/([^/?#]+)/, /^\/embed\/([^/?#]+)/, /^\/live\/([^/?#]+)/], parsed.pathname)?.[1];
|
|
31
|
+
const playlistId = parsed.searchParams.get("list");
|
|
32
|
+
if (!id && playlistId) {
|
|
33
|
+
return createEmbedRecord({
|
|
34
|
+
provider: "youtube",
|
|
35
|
+
kind: "video-playlist",
|
|
36
|
+
title: ctx.title || "YouTube playlist",
|
|
37
|
+
url,
|
|
38
|
+
embedUrl: `https://www.youtube-nocookie.com/embed/videoseries?list=${encodeURIComponent(playlistId)}`,
|
|
39
|
+
renderMode: "iframe",
|
|
40
|
+
metadata: { playlistId }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!id || !/^[A-Za-z0-9_-]{6,}$/.test(id)) throw new Error("YouTube URL is missing a video id");
|
|
44
|
+
return createEmbedRecord({
|
|
45
|
+
provider: "youtube",
|
|
46
|
+
kind: "video",
|
|
47
|
+
title: ctx.title || "YouTube video",
|
|
48
|
+
url,
|
|
49
|
+
embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
|
|
50
|
+
thumbnailUrl: `https://i.ytimg.com/vi/${id}/hqdefault.jpg`,
|
|
51
|
+
renderMode: "iframe",
|
|
52
|
+
metadata: { videoId: id, playlistId: playlistId || null }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const vimeoMarkdownPlugin = defineMarkdownPlugin({
|
|
58
|
+
id: "markdown.embed.vimeo",
|
|
59
|
+
provider: "vimeo",
|
|
60
|
+
aliases: ["vimeo"],
|
|
61
|
+
matchUrl(url) { return hostIs(url, ["vimeo.com", "player.vimeo.com"]) && Boolean(firstMatch([/^\/(?:video\/)?([0-9]+)/], new URL(url).pathname)); },
|
|
62
|
+
toEmbed(url, ctx = {}) {
|
|
63
|
+
const id = firstMatch([/^\/(?:video\/)?([0-9]+)/], new URL(url).pathname)?.[1];
|
|
64
|
+
return createEmbedRecord({ provider: "vimeo", kind: "video", title: ctx.title || "Vimeo video", url, embedUrl: `https://player.vimeo.com/video/${id}`, renderMode: "iframe", metadata: { videoId: id } });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const twitchMarkdownPlugin = defineMarkdownPlugin({
|
|
69
|
+
id: "markdown.embed.twitch",
|
|
70
|
+
provider: "twitch",
|
|
71
|
+
aliases: ["twitch", "twitchclips", "clip"],
|
|
72
|
+
matchUrl(url) { return hostIs(url, ["twitch.tv", "m.twitch.tv", "clips.twitch.tv"]); },
|
|
73
|
+
toEmbed(url, ctx = {}) {
|
|
74
|
+
const parsed = new URL(url);
|
|
75
|
+
const parts = pathParts(url);
|
|
76
|
+
const host = safeHostname(url);
|
|
77
|
+
let embedUrl;
|
|
78
|
+
let kind = "livestream";
|
|
79
|
+
const clipSlug = host === "clips.twitch.tv" ? parts[0] : (parts[1] === "clip" ? parts[2] : null);
|
|
80
|
+
if (parts[0] === "videos" && parts[1]) { kind = "video"; embedUrl = `https://player.twitch.tv/?video=${cleanId(parts[1])}&parent=matterhorn.gg&autoplay=false`; }
|
|
81
|
+
else if (clipSlug) { kind = "clip"; embedUrl = `https://clips.twitch.tv/embed?clip=${cleanId(clipSlug)}&parent=matterhorn.gg&autoplay=false`; }
|
|
82
|
+
else if (parts[0]) { embedUrl = `https://player.twitch.tv/?channel=${cleanId(parts[0])}&parent=matterhorn.gg&autoplay=false`; }
|
|
83
|
+
else throw new Error("Twitch URL is missing a channel, video, or clip id");
|
|
84
|
+
return createEmbedRecord({ provider: "twitch", kind, title: ctx.title || "Twitch stream", url, embedUrl, renderMode: "iframe", metadata: { kind, channel: parts[0] || null, clipSlug: clipSlug || null } });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const dailymotionMarkdownPlugin = defineMarkdownPlugin({
|
|
89
|
+
id: "markdown.embed.dailymotion",
|
|
90
|
+
provider: "dailymotion",
|
|
91
|
+
aliases: ["dailymotion", "daily"],
|
|
92
|
+
matchUrl(url) { return hostIs(url, ["dailymotion.com", "dai.ly"]); },
|
|
93
|
+
toEmbed(url, ctx = {}) {
|
|
94
|
+
const parsed = new URL(url);
|
|
95
|
+
const id = safeHostname(url) === "dai.ly" ? pathParts(url)[0] : firstMatch([/\/video\/([^_/?#]+)/], parsed.pathname)?.[1];
|
|
96
|
+
if (!id) throw new Error("Dailymotion URL is missing a video id");
|
|
97
|
+
return createEmbedRecord({ provider: "dailymotion", kind: "video", title: ctx.title || "Dailymotion video", url, embedUrl: `https://www.dailymotion.com/embed/video/${cleanId(id)}`, renderMode: "iframe", metadata: { videoId: id } });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const streamableMarkdownPlugin = defineMarkdownPlugin({
|
|
102
|
+
id: "markdown.embed.streamable",
|
|
103
|
+
provider: "streamable",
|
|
104
|
+
aliases: ["streamable"],
|
|
105
|
+
matchUrl(url) { return hostIs(url, ["streamable.com"]) && pathParts(url)[0]; },
|
|
106
|
+
toEmbed(url, ctx = {}) { const id = pathParts(url)[0]; return createEmbedRecord({ provider: "streamable", kind: "video", title: ctx.title || "Streamable video", url, embedUrl: `https://streamable.com/e/${cleanId(id)}`, renderMode: "iframe", metadata: { videoId: id } }); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const streamjaMarkdownPlugin = defineMarkdownPlugin({
|
|
110
|
+
id: "markdown.embed.streamja",
|
|
111
|
+
provider: "streamja",
|
|
112
|
+
aliases: ["streamja"],
|
|
113
|
+
matchUrl(url) { return hostIs(url, ["streamja.com"]) && pathParts(url)[0]; },
|
|
114
|
+
toEmbed(url, ctx = {}) { const id = pathParts(url)[0]; return createEmbedRecord({ provider: "streamja", kind: "video", title: ctx.title || "Streamja video", url, embedUrl: `https://streamja.com/embed/${cleanId(id)}`, renderMode: "iframe", metadata: { videoId: id } }); }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const coubMarkdownPlugin = defineMarkdownPlugin({
|
|
118
|
+
id: "markdown.embed.coub",
|
|
119
|
+
provider: "coub",
|
|
120
|
+
aliases: ["coub"],
|
|
121
|
+
matchUrl(url) { return hostIs(url, ["coub.com"]) && pathParts(url)[0] === "view" && pathParts(url)[1]; },
|
|
122
|
+
toEmbed(url, ctx = {}) { const id = pathParts(url)[1]; return createEmbedRecord({ provider: "coub", kind: "video-loop", title: ctx.title || "Coub", url, embedUrl: `https://coub.com/embed/${cleanId(id)}`, renderMode: "iframe", metadata: { coubId: id } }); }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const peertubeMarkdownPlugin = simpleProvider({ id: "markdown.embed.peertube", provider: "peertube", aliases: ["peertube"], hostSuffixes: ["joinpeertube.org"], kind: "video", title: "PeerTube video", renderMode: "external-link", matchPath: (parsed) => /\/(w|videos\/watch)\//.test(parsed.pathname) });
|
|
126
|
+
const clypMarkdownPlugin = simpleProvider({ id: "markdown.embed.clyp", provider: "clyp", aliases: ["clyp"], hostSuffixes: ["clyp.it"], kind: "audio", title: "Clyp audio", renderMode: "external-link" });
|
|
127
|
+
const getyarnMarkdownPlugin = simpleProvider({ id: "markdown.embed.getyarn", provider: "getyarn", aliases: ["getyarn", "yarn"], hosts: ["getyarn.io", "yarn.co"], kind: "video-clip", title: "Yarn clip", renderMode: "external-link" });
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
coubMarkdownPlugin,
|
|
131
|
+
dailymotionMarkdownPlugin,
|
|
132
|
+
getyarnMarkdownPlugin,
|
|
133
|
+
peertubeMarkdownPlugin,
|
|
134
|
+
streamableMarkdownPlugin,
|
|
135
|
+
streamjaMarkdownPlugin,
|
|
136
|
+
twitchMarkdownPlugin,
|
|
137
|
+
vimeoMarkdownPlugin,
|
|
138
|
+
youtubeMarkdownPlugin
|
|
139
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { normalizeUrl } = require("../shared/index.cjs");
|
|
2
|
+
const {
|
|
3
|
+
DEFAULT_MARKDOWN_PLUGINS,
|
|
4
|
+
genericLinkMarkdownPlugin
|
|
5
|
+
} = require("./providers/index.cjs");
|
|
6
|
+
|
|
7
|
+
function pluginForUrl(url, plugins = DEFAULT_MARKDOWN_PLUGINS) {
|
|
8
|
+
const clean = normalizeUrl(url);
|
|
9
|
+
for (const plugin of plugins) {
|
|
10
|
+
try {
|
|
11
|
+
if (plugin.matchUrl(clean)) return plugin;
|
|
12
|
+
} catch {
|
|
13
|
+
// Keep trying other resolvers.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return genericLinkMarkdownPlugin;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pluginForDirective(name, plugins = DEFAULT_MARKDOWN_PLUGINS) {
|
|
20
|
+
const key = String(name || "embed").toLowerCase();
|
|
21
|
+
const found = plugins.find((plugin) => plugin.provider === key || plugin.id === key || (plugin.aliases || []).includes(key));
|
|
22
|
+
return found || (key === "embed" ? genericLinkMarkdownPlugin : null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveEmbed(url, options = {}) {
|
|
26
|
+
const clean = normalizeUrl(url);
|
|
27
|
+
const plugins = options.plugins || DEFAULT_MARKDOWN_PLUGINS;
|
|
28
|
+
const requested = options.provider ? pluginForDirective(options.provider, plugins) : null;
|
|
29
|
+
const plugin = requested || pluginForUrl(clean, plugins);
|
|
30
|
+
if (!plugin) throw new Error(`No markdown embed plugin for ${options.provider}`);
|
|
31
|
+
const embed = plugin.toEmbed(clean, { title: options.title, directive: options.provider });
|
|
32
|
+
return { ...embed, pluginId: plugin.id };
|
|
33
|
+
}
|
|
34
|
+
function cleanUrlToken(raw) {
|
|
35
|
+
return String(raw || "")
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/[\]}>.,!?;:'"]+$/g, "")
|
|
38
|
+
.replace(/\)+$/g, (suffix, offset, full) => {
|
|
39
|
+
const before = full.slice(0, offset);
|
|
40
|
+
const opens = (before.match(/\(/g) || []).length;
|
|
41
|
+
const closes = (before.match(/\)/g) || []).length;
|
|
42
|
+
const keep = Math.max(0, opens - closes);
|
|
43
|
+
return suffix.slice(0, keep);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractUrlsFromText(text) {
|
|
48
|
+
if (typeof text !== "string") throw new Error("text must be a string");
|
|
49
|
+
const urls = [];
|
|
50
|
+
const pattern = /https?:\/\/[^\s<>'"`]+/g;
|
|
51
|
+
for (const match of text.matchAll(pattern)) {
|
|
52
|
+
const raw = cleanUrlToken(match[0]);
|
|
53
|
+
if (!raw) continue;
|
|
54
|
+
try {
|
|
55
|
+
urls.push({ url: normalizeUrl(raw), index: match.index });
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore malformed URLs while keeping detection resilient for chat text.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return urls;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function autoDetectEmbeds(text, options = {}) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const detected = [];
|
|
66
|
+
for (const candidate of extractUrlsFromText(text)) {
|
|
67
|
+
if (seen.has(candidate.url)) continue;
|
|
68
|
+
seen.add(candidate.url);
|
|
69
|
+
try {
|
|
70
|
+
const embed = resolveEmbed(candidate.url, options);
|
|
71
|
+
if (options.includeGeneric === false && embed.provider === "link") continue;
|
|
72
|
+
detected.push({ ...embed, sourceIndex: candidate.index, autoDetected: true });
|
|
73
|
+
} catch {
|
|
74
|
+
// Detection should not make message parsing fail.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return detected;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
autoDetectEmbeds,
|
|
82
|
+
cleanUrlToken,
|
|
83
|
+
extractUrlsFromText,
|
|
84
|
+
pluginForDirective,
|
|
85
|
+
pluginForUrl,
|
|
86
|
+
resolveEmbed
|
|
87
|
+
};
|