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