import type { Root as HastRoot } from 'hast'; import type { Root as MdRoot } from 'mdast'; import { useEffect, useState } from 'react'; import * as React from 'react'; import * as production from 'react/jsx-runtime'; import rehypeHighlight, { Options as RehypeHighlightOpts, } from 'rehype-highlight'; import rehypeReact from 'rehype-react'; import rehypeSanitize from 'rehype-sanitize'; import remarkBreaks from 'remark-breaks'; import remarkGemoji from 'remark-gemoji'; import remarkGfm from 'remark-gfm'; import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; import type { Options as RemarkRehypeOptions } from 'remark-rehype'; import { unified } from 'unified'; import type { Plugin, Processor } from 'unified'; import { Node as UnistNode } from 'unified/lib'; import { ThemeContext } from '../Themer'; import AnchorTag from './AnchorTag'; import BlockQuoteTag from './BlockQuoteTag'; import ImageTag from './ImageTag'; import PreTag from './PreTag'; type Props = { markdown: string }; // @lygaret 2025/05/16 // type inference for some of this doesn't work, but the pipeline is fine // this might get better when we upgrade typescript type RemarkPlugin = Plugin<[], MdRoot, HastRoot>; type RemarkRehypePlugin = Plugin; type RehypePlugin = Plugin< Options, HastRoot, HastRoot >; const markdownPipeline: Processor< UnistNode, undefined, undefined, HastRoot, React.JSX.Element > = unified() .use(remarkParse) .use(remarkGemoji as unknown as RemarkPlugin) .use(remarkBreaks as unknown as RemarkPlugin) .use(remarkGfm) .use(remarkRehype as unknown as RemarkRehypePlugin, { allowDangerousHtml: true, }) .use(rehypeSanitize as unknown as RehypePlugin) .use(rehypeHighlight as unknown as RehypePlugin, { detect: true, subset: ['text'], }) .use(rehypeReact, { ...production, components: { a: AnchorTag, blockquote: BlockQuoteTag, img: ImageTag, pre: PreTag, }, }); const Content: React.FC = ({ markdown }: Props) => { const theme = React.useContext(ThemeContext); const [content, setContent] = useState(<>); useEffect(() => { markdownPipeline .process(markdown) .then((file) => setContent(file.result)) .catch((err: any) => { setContent( <> {err}
{markdown}
); }); }, [markdown]); return (
{content}
); }; export default Content;