@r4ai/remark-embed
Version:
[](https://jsr.io/@r4ai/remark-embed) [](https://codecov.io/gh/r4ai/remark-embed) [ • 5.26 kB
JavaScript
import { defu } from "defu";
import { fromHtmlIsomorphic } from "hast-util-from-html-isomorphic";
import { unfurl } from "unfurl.js";
import * as v from "valibot";
import { OEmbedSchema } from "./schemas.js";
export const defaultTransformerOEmbedOptions = {
providers: {
// https://developer.x.com/en/docs/x-for-websites/oembed-api
"twitter.com": {
match: (url) => url.hostname === "twitter.com",
response: (url) => fetch(`https://publish.twitter.com/oembed?${new URLSearchParams({ url: url.href })}`),
},
},
postProcess: (html) => html,
photo: (_, oEmbed) => ({
tagName: "img",
properties: {
src: oEmbed.url,
width: oEmbed.width,
height: oEmbed.height,
alt: oEmbed.title,
className: "oembed oembed-photo",
href: null,
},
children: [],
}),
video: (_, oEmbed, options) => ({
tagName: "div",
properties: {
className: "oembed oembed-video",
href: null,
},
children: html2hast(options.postProcess(oEmbed.html)),
}),
rich: (_, oEmbed, options) => ({
tagName: "div",
properties: {
className: "oembed oembed-rich",
href: null,
},
children: html2hast(options.postProcess(oEmbed.html)),
}),
link: (url) => ({
tagName: "a",
properties: {
href: url.href,
className: "oembed oembed-link",
},
children: [{ type: "text", value: url.href }],
}),
};
/**
* A transformer for oEmbed.
* Embeds the content of the URL using the oEmbed metadata.
* @see {@link https://oembed.com/ | oembed.com}
*
* @example
* ```ts
* const html = (
* await unified()
* .use(remarkParse)
* .use(remarkRehype)
* .use(remarkEmbed, {
* transformers: [transformerOEmbed()],
* })
* .use(rehypeStringify)
* .process(md)
* ).toString()
* ```
*/
export const transformerOEmbed = (_options) => {
const options = defu(_options, defaultTransformerOEmbedOptions);
const cache = new Map();
return {
name: "oembed",
tagName: (url) => {
const oEmbed = cache.get(url.href);
switch (oEmbed?.type) {
case "photo":
return options.photo(url, oEmbed, options).tagName;
case "video":
return options.video(url, oEmbed, options).tagName;
case "rich":
return options.rich(url, oEmbed, options).tagName;
case "link":
return options.link(url, oEmbed, options).tagName;
default:
return "div";
}
},
properties: async (url) => {
const oEmbed = cache.get(url.href);
switch (oEmbed?.type) {
case "photo":
return options.photo(url, oEmbed, options).properties;
case "video":
return options.video(url, oEmbed, options).properties;
case "rich":
return options.rich(url, oEmbed, options).properties;
case "link":
return options.link(url, oEmbed, options).properties;
default:
return {};
}
},
children: async (url) => {
const oEmbed = cache.get(url.href);
switch (oEmbed?.type) {
case "photo":
return options.photo(url, oEmbed, options).children;
case "video":
return options.video(url, oEmbed, options).children;
case "rich":
return options.rich(url, oEmbed, options).children;
case "link":
return options.link(url, oEmbed, options).children;
default:
return [];
}
},
match: async (url) => {
const cached = cache.get(url.href);
if (cached)
return !!cached.type;
const provider = Object.values(options.providers).find((provider) => provider.match(url));
if (provider) {
try {
const response = await provider.response(url);
if (!response.ok) {
cache.set(url.href, {});
return false;
}
const oEmbed = v.parse(OEmbedSchema, await response.json());
cache.set(url.href, oEmbed);
return true;
}
catch (error) {
cache.set(url.href, {});
throw error;
}
}
const metadata = await unfurl(url.href);
if (!metadata.oEmbed) {
cache.set(url.href, {});
return false;
}
cache.set(url.href, metadata.oEmbed);
return true;
},
};
};
const html2hast = (html) => {
const hast = fromHtmlIsomorphic(html, {
fragment: true,
}).children;
return hast;
};