import { cn } from "@/utils/ui"; import rehypeParse from "rehype-parse"; import { unified } from "unified"; import { createElement } from "react"; import type React from "react"; type ProseProps = React.HTMLAttributes & { as?: "article"; html: string; }; type HastTextNode = { type: "text"; value: string; }; type HastElementNode = { type: "element"; tagName: string; properties?: Record; children?: HastNode[]; }; type HastRootNode = { type: "root"; children?: HastNode[]; }; type HastNode = HastTextNode | HastElementNode | HastRootNode; function toReactProps({ properties, }: { properties: HastElementNode["properties"]; }): Record { if (!properties) { return {}; } const props: Record = {}; for (const [propertyName, propertyValue] of Object.entries(properties)) { if (propertyValue === null || propertyValue === undefined) { continue; } if (propertyName.startsWith("on")) { continue; } if (propertyName === "class") { props.className = Array.isArray(propertyValue) ? propertyValue.join(" ") : propertyValue; continue; } if (propertyName === "for") { props.htmlFor = propertyValue; continue; } props[propertyName] = propertyValue; } return props; } function renderHastNode({ node, key, }: { node: HastNode; key: string; }): React.ReactNode { if (node.type === "text") { return node.value; } if (node.type !== "element") { return null; } const children = (node.children ?? []).map((childNode, index) => renderHastNode({ node: childNode, key: `${key}-${index}` }), ); return createElement(node.tagName, { ...toReactProps({ properties: node.properties }), key }, children); } function renderHtmlNodes({ html }: { html: string }): React.ReactNode { const rootNode = unified().use(rehypeParse, { fragment: true }).parse(html) as HastRootNode; return (rootNode.children ?? []).map((childNode, index) => renderHastNode({ node: childNode, key: `prose-node-${index}` }), ); } function Prose({ children, html, className }: ProseProps) { return (
{html ? renderHtmlNodes({ html }) : children}
); } export default Prose;