UNPKG

@r4ai/remark-embed

Version:

[![JSR](https://jsr.io/badges/@r4ai/remark-embed)](https://jsr.io/@r4ai/remark-embed) [![codecov](https://codecov.io/gh/r4ai/remark-embed/graph/badge.svg?token=B9EZXC0PR8)](https://codecov.io/gh/r4ai/remark-embed) [![CI](https://github.com/r4ai/remark-emb

158 lines (157 loc) 5.26 kB
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; };