shikijs / shiki

A beautiful yet powerful syntax highlighter
http://shiki.style/
MIT License
10.37k stars 377 forks source link

How can I use @shikijs/rehype with react-markdown #829

Open HerbertHe opened 2 weeks ago

HerbertHe commented 2 weeks ago

Validations

Describe the bug

I tried to use @shikijs/rehype with react-markdown, but I got an error as followed:

image

import RehypeShiki from '@shikijs/rehype';

<ReactMarkdown
      remarkPlugins={[RemarkMath, RemarkGFM]}
      rehypePlugins={[
        RehypeKaTeX,
        [
          RehypeShiki,
          {
            themes: {
              light: 'vitesse-light',
              dark: 'vitesse-dark',
            },
          },
        ],
      ]}
      components={{}}
    >
      {content}
    </ReactMarkdown>

Reproduction

follow the document with @shikijs/rehype

Contributes

Serpentarius13 commented 2 weeks ago

That's probably an issue with Async plugins in react-markdown: https://github.com/remarkjs/react-markdown/issues/680

liuhq commented 2 weeks ago

@HerbertHe Yesterday, I also got the same issue when using shiki with react-markdown. But it seems that no a solution yet on the react-markdown side. So, I referred to this article, and I think we only need a simple hook and don't necessarily need react-markdown, like below:

Hook useRemark

import { useCallback, useState } from 'react'
import * as jsxRuntime from 'react/jsx-runtime'
import rehypeReact, { type Options as RehypeReactOptions } from 'rehype-react'
import remarkParse, { type Options as RemarkParseOptions } from 'remark-parse'
import remarkRehype, { type Options as RemarkRehypeOptions } from 'remark-rehype'
import { unified, type PluggableList } from 'unified'

export interface UseRemarkOptions {
    remarkParseOptions?: RemarkParseOptions
    remarkPlugins?: PluggableList
    remarkRehypeOptions?: RemarkRehypeOptions
    rehypePlugins?: PluggableList
    rehypeReactOptions?: Pick<RehypeReactOptions, 'components'>
    onError?: (err: Error) => void
}

export default function useRemark({
    remarkParseOptions,
    remarkPlugins = [],
    remarkRehypeOptions,
    rehypePlugins = [],
    rehypeReactOptions,
    onError = () => {},
}: UseRemarkOptions = {}): [React.ReactElement | null, (source: string) => void] {
    const [content, setContent] = useState<React.ReactElement | null>(null)

    const setMarkdown = useCallback((source: string) => {
        unified()
            .use(remarkParse, remarkParseOptions)        // parse markdown
            .use(remarkPlugins)
            .use(remarkRehype, remarkRehypeOptions)      // markdown to html
            .use(rehypePlugins)
            .use(rehypeReact, {                          // html to react elements
                ...rehypeReactOptions,
                Fragment: jsxRuntime.Fragment,
                jsx: jsxRuntime.jsx,
                jsxs: jsxRuntime.jsxs,
            } satisfies RehypeReactOptions)
            .process(source)
            .then(vfile => setContent(vfile.result))     // get react elements
            .catch(onError)
    }, [])

    return [content, setMarkdown]
}

you just need to add the pkg of unified, remark-parse, remark-rehype, rehype-react

Usage

const [markdown, setMarkdown] = useRemark({
    rehypePlugins: [
        [rehypeShiki, { theme: 'catppuccin-mocha' } satisfies RehypeShikiOptions]
    ],
})

if (markdown == null) {
    setMarkdown(source) // markdown source string
}

return <article>{markdown}</article>

Hopefully, this will be useful to you.

HerbertHe commented 2 weeks ago

@liuhq Thank you for the example, maybe I don't need to do that now🌚. I just need to render the markdown formatted content for AI Agent answers. It seems that I don't need to JUST highlight the code content, but to add more interactions🤣