80 lines
2.0 KiB
TypeScript
80 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { evaluate } from "@mdx-js/mdx";
|
|
import { MDXProvider, useMDXComponents } from "@mdx-js/react";
|
|
import type { MDXComponents } from "mdx/types";
|
|
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
import * as runtime from "react/jsx-runtime";
|
|
import rehypeHighlight from "rehype-highlight";
|
|
import remarkGfm from "remark-gfm";
|
|
import { mdxComponents } from "~/components/mdx-components";
|
|
|
|
type MdxModule = {
|
|
default: ComponentType<{ components?: MDXComponents }>;
|
|
};
|
|
|
|
type ClientMdxProps = {
|
|
source: string;
|
|
components?: MDXComponents;
|
|
format?: "md" | "mdx";
|
|
fallback?: ReactNode;
|
|
errorFallback?: (error: Error) => ReactNode;
|
|
};
|
|
|
|
export function ClientMdx({
|
|
source,
|
|
components = mdxComponents,
|
|
format = "md",
|
|
fallback = null,
|
|
errorFallback,
|
|
}: ClientMdxProps) {
|
|
const [Content, setContent] = useState<MdxModule["default"] | null>(null);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const trimmed = source.trim();
|
|
|
|
if (!trimmed) {
|
|
setContent(null);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
void evaluate(trimmed, {
|
|
...runtime,
|
|
baseUrl: import.meta.url,
|
|
format,
|
|
useMDXComponents,
|
|
rehypePlugins: [rehypeHighlight],
|
|
remarkPlugins: [remarkGfm],
|
|
})
|
|
.then((mod) => {
|
|
if (cancelled) return;
|
|
setContent(() => (mod as MdxModule).default);
|
|
setError(null);
|
|
})
|
|
.catch((nextError: unknown) => {
|
|
if (cancelled) return;
|
|
setContent(null);
|
|
setError(nextError instanceof Error ? nextError : new Error("Failed to render MDX"));
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [format, source]);
|
|
|
|
if (error) {
|
|
return errorFallback ? errorFallback(error) : <p>{source}</p>;
|
|
}
|
|
|
|
if (!Content) return <>{fallback}</>;
|
|
|
|
return (
|
|
<MDXProvider components={components}>
|
|
<Content components={components} />
|
|
</MDXProvider>
|
|
);
|
|
}
|