import { type Node, select, selectAll } from "unist-util-select"; import { visit } from "unist-util-visit"; import type { EmbedsOption } from "."; export default function createPlugin({ embeds = [], importNamespace, }: { embeds: EmbedsOption[]; importNamespace: string; }) { /** * Get the name of the embed component for this URL * @param {string} url URL to test * @returns Component node for this URL or undefined if none matched */ function getComponentForUrl(url: string) { for (const embed of embeds) { const { componentName, urlArgument = "href", urlMatcher } = embed; if (!urlMatcher) continue; let match = urlMatcher(url); if (!match) continue; // MDX custom component node. return { type: "mdxJsxFlowElement", name: `${importNamespace}_${componentName}`, attributes: [ { type: "mdxJsxAttribute", name: urlArgument, value: match }, ], children: [], }; } return undefined; } function getComponentForDirective(node: Node) { for (const embed of embeds) { const { componentName, urlArgument = "href", directiveName } = embed; // @ts-expect-error if (!directiveName || !node.name) continue; const name = (node as any).name; const attributes = (node as any).attributes ?? {}; const children = (node as any).children ?? []; if ( directiveName === name || (Array.isArray(directiveName) && directiveName.includes(name)) ) { // turn into array const attributeArray = Object.keys(attributes).map((key) => { return { type: "mdxJsxAttribute", name: key, value: attributes[key] }; }); // if there's one child and it's either a link node with a text node child or a text node // add that as a attribute using the urlArgument if (children && children.length === 1) { const child = children[0]; if (child.type === "link") { if ( child.children.length === 1 && child.children[0].type === "text" ) { attributeArray.push({ type: "mdxJsxAttribute", name: urlArgument, value: child.url, }); } } else if (child.type === "text") { attributeArray.push({ type: "mdxJsxAttribute", name: urlArgument, value: child.value, }); } } return { type: "mdxJsxFlowElement", name: `${importNamespace}_${componentName}`, attributes: attributeArray, children: children ?? [], }; } } return undefined; } type Link = Node & { url?: string; value?: string; children?: Node[]; }; function transformer(tree: Node) { visit(tree, function (node) { if ( node.type === "containerDirective" || node.type === "leafDirective" || node.type === "textDirective" ) { let component = getComponentForDirective(node); if (component) { // @ts-expect-error We’re overriding the initial node type with arbitrary data. for (const key in component) node[key] = component[key]; } } }); const paragraphs = selectAll("paragraph", tree); paragraphs.forEach((paragraph) => { const link = select(":scope > link:only-child", paragraph) as Link | undefined; if (!link) return; const { url, children } = link; // We’re only interested in HTTP links if (!url || !url.startsWith("http")) return; // URLs should have a single child if (!children || children.length !== 1) return; // The child should be a text node with a value matching the URL const child = children[0]; if ( !child || child.type !== "text" || !("value" in child) || child.value !== url ) { return; } const component = getComponentForUrl(url); if (component) { // @ts-expect-error We’re overriding the initial node type with arbitrary data. for (const key in component) paragraph[key] = component[key]; } }); return tree; } return function attacher() { return transformer; }; }