1import type { Root as HastRoot } from 'hast';
2import type { Root as MdRoot } from 'mdast';
3import { useEffect, useState } from 'react';
4import * as React from 'react';
5import * as production from 'react/jsx-runtime';
6import rehypeHighlight, {
7 Options as RehypeHighlightOpts,
8} from 'rehype-highlight';
9import rehypeReact from 'rehype-react';
10import rehypeSanitize from 'rehype-sanitize';
11import remarkBreaks from 'remark-breaks';
12import remarkGemoji from 'remark-gemoji';
13import remarkGfm from 'remark-gfm';
14import remarkParse from 'remark-parse';
15import remarkRehype from 'remark-rehype';
16import type { Options as RemarkRehypeOptions } from 'remark-rehype';
17import { unified } from 'unified';
18import type { Plugin, Processor } from 'unified';
19import { Node as UnistNode } from 'unified/lib';
20
21import { ThemeContext } from '../Themer';
22
23import AnchorTag from './AnchorTag';
24import BlockQuoteTag from './BlockQuoteTag';
25import ImageTag from './ImageTag';
26import PreTag from './PreTag';
27
28type Props = { markdown: string };
29
30// @lygaret 2025/05/16
31// type inference for some of this doesn't work, but the pipeline is fine
32// this might get better when we upgrade typescript
33
34type RemarkPlugin = Plugin<[], MdRoot, HastRoot>;
35type RemarkRehypePlugin = Plugin<RemarkRehypeOptions[], MdRoot, HastRoot>;
36type RehypePlugin<Options extends unknown[] = []> = Plugin<
37 Options,
38 HastRoot,
39 HastRoot
40>;
41
42const markdownPipeline: Processor<
43 UnistNode,
44 undefined,
45 undefined,
46 HastRoot,
47 React.JSX.Element
48> = unified()
49 .use(remarkParse)
50 .use(remarkGemoji as unknown as RemarkPlugin)
51 .use(remarkBreaks as unknown as RemarkPlugin)
52 .use(remarkGfm)
53 .use(remarkRehype as unknown as RemarkRehypePlugin, {
54 allowDangerousHtml: true,
55 })
56 .use(rehypeSanitize as unknown as RehypePlugin)
57 .use(rehypeHighlight as unknown as RehypePlugin<RehypeHighlightOpts[]>, {
58 detect: true,
59 subset: ['text'],
60 })
61 .use(rehypeReact, {
62 ...production,
63 components: {
64 a: AnchorTag,
65 blockquote: BlockQuoteTag,
66 img: ImageTag,
67 pre: PreTag,
68 },
69 });
70
71const Content: React.FC<Props> = ({ markdown }: Props) => {
72 const theme = React.useContext(ThemeContext);
73 const [content, setContent] = useState(<></>);
74
75 useEffect(() => {
76 markdownPipeline
77 .process(markdown)
78 .then((file) => setContent(file.result))
79 .catch((err: any) => {
80 setContent(
81 <>
82 <span className="error">{err}</span>
83 <pre>{markdown}</pre>
84 </>
85 );
86 });
87 }, [markdown]);
88
89 return (
90 <div className={'highlight-theme'} data-theme={theme.mode}>
91 {content}
92 </div>
93 );
94};
95
96export default Content;