import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype'
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
import {
Children,
type ComponentProps,
type ReactElement,
type ReactNode,
Suspense,
use,
useDeferredValue
} from 'react'
import { Fragment, jsx, jsxs } from 'react/jsx-runtime'
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'
import defaultMdxComponents from 'fumadocs-ui/mdx'
import { visit } from 'unist-util-visit'
import type { ElementContent, Root, RootContent } from 'hast'
export interface Processor {
process: (content: string) => Promise
}
export function rehypeWrapWords() {
return (tree: Root) => {
visit(tree, ['text', 'element'], (node, index, parent) => {
if (node.type === 'element' && node.tagName === 'pre') return 'skip'
if (node.type !== 'text' || !parent || index === undefined) return
const words = node.value.split(/(?=\s)/)
// Create new span nodes for each word and whitespace
const newNodes: ElementContent[] = words.flatMap((word) => {
if (word.length === 0) return []
return {
type: 'element',
tagName: 'span',
properties: {
class: 'animate-fd-fade-in'
},
children: [{ type: 'text', value: word }]
}
})
Object.assign(node, {
type: 'element',
tagName: 'span',
properties: {},
children: newNodes
} satisfies RootContent)
return 'skip'
})
}
}
function createProcessor(): Processor {
const processor = remark().use(remarkGfm).use(remarkRehype).use(rehypeWrapWords)
return {
async process(content) {
const nodes = processor.parse({ value: content })
const hast = await processor.run(nodes)
return toJsxRuntime(hast, {
development: false,
jsx,
jsxs,
Fragment,
components: {
...defaultMdxComponents,
pre: Pre,
img: undefined // use JSX
}
})
}
}
}
function Pre(props: ComponentProps<'pre'>) {
const code = Children.only(props.children) as ReactElement
const codeProps = code.props as ComponentProps<'code'>
const content = codeProps.children
if (typeof content !== 'string') return null
let lang =
codeProps.className
?.split(' ')
.find((v) => v.startsWith('language-'))
?.slice('language-'.length) ?? 'text'
if (lang === 'mdx') lang = 'md'
return
}
const processor = createProcessor()
export function Markdown({ text }: { text: string }) {
const deferredText = useDeferredValue(text)
return (
{text}
}>
)
}
const cache = new Map>()
function Renderer({ text }: { text: string }) {
const result = cache.get(text) ?? processor.process(text)
cache.set(text, result)
return use(result)
}