diff options
Diffstat (limited to 'webui/src/components/Content/index.tsx')
-rw-r--r-- | webui/src/components/Content/index.tsx | 98 |
1 files changed, 75 insertions, 23 deletions
diff --git a/webui/src/components/Content/index.tsx b/webui/src/components/Content/index.tsx index 9bf9ff7a6..3cfa9eb89 100644 --- a/webui/src/components/Content/index.tsx +++ b/webui/src/components/Content/index.tsx @@ -1,11 +1,24 @@ -import { createElement, Fragment, useEffect, useState } from 'react'; +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 gemoji from 'remark-gemoji'; -import html from 'remark-html'; -import parse from 'remark-parse'; +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'; @@ -13,32 +26,71 @@ 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<RemarkRehypeOptions[], MdRoot, HastRoot>; +type RehypePlugin<Options extends unknown[] = []> = 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<RehypeHighlightOpts[]>, { + detect: true, + subset: ['text'], + }) + .use(rehypeReact, { + ...production, + components: { + a: AnchorTag, + blockquote: BlockQuoteTag, + img: ImageTag, + pre: PreTag, + }, + }); + const Content: React.FC<Props> = ({ markdown }: Props) => { - const [Content, setContent] = useState(<></>); + const theme = React.useContext(ThemeContext); + const [content, setContent] = useState(<></>); useEffect(() => { - unified() - .use(parse) - .use(gemoji) - .use(html) - .use(remarkRehype) - .use(rehypeReact, { - createElement, - Fragment, - components: { - img: ImageTag, - pre: PreTag, - a: AnchorTag, - blockquote: BlockQuoteTag, - }, - }) + markdownPipeline .process(markdown) - .then((file) => { - setContent(file.result); + .then((file) => setContent(file.result)) + .catch((err: any) => { + setContent( + <> + <span className="error">{err}</span> + <pre>{markdown}</pre> + </> + ); }); }, [markdown]); - return Content; + return ( + <div className={'highlight-theme'} data-theme={theme.mode}> + {content} + </div> + ); }; export default Content; |