index.tsx

 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;