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) }