Spaces:
Runtime error
Runtime error
import clsx from "clsx"; | |
import type { ReactNode } from "react"; | |
import React, { useCallback, useState } from "react"; | |
import { FiClipboard } from "react-icons/fi"; | |
import ReactMarkdown from "react-markdown"; | |
import rehypeHighlight from "rehype-highlight"; | |
import remarkGfm from "remark-gfm"; | |
import "highlight.js/styles/default.css"; | |
interface MarkdownRendererProps { | |
children: string; | |
className?: string; | |
} | |
const MarkdownRenderer = ({ children, className }: MarkdownRendererProps) => { | |
return ( | |
<ReactMarkdown | |
remarkPlugins={[remarkGfm]} | |
rehypePlugins={[() => rehypeHighlight({ ignoreMissing: true })]} | |
components={{ | |
pre: CustomPre, | |
code: CustomCodeBlock, | |
h1: (props) => <h1 className="text-md mb-2 font-black sm:text-xl">{props.children}</h1>, | |
h2: (props) => <h1 className="sm:text-md mb-2 text-sm font-bold">{props.children}</h1>, | |
a: (props) => CustomLink({ children: props.children, href: props.href }), | |
p: (props) => <p className="mb-4">{props.children}</p>, | |
ul: (props) => ( | |
<ul className={clsx("mb-4 list-disc marker:text-neutral-400", className)}> | |
{props.children} | |
</ul> | |
), | |
ol: (props) => ( | |
<ol className="mb-4 ml-8 list-decimal marker:text-neutral-400">{props.children}</ol> | |
), | |
li: (props) => <li className="mb-1 ml-8">{props.children}</li>, | |
}} | |
> | |
{children} | |
</ReactMarkdown> | |
); | |
}; | |
const CustomPre = ({ children }: { children: ReactNode }) => { | |
const [isCopied, setIsCopied] = useState(false); | |
const code = React.Children.toArray(children).find(isValidCustomCodeBlock); | |
const language: string = | |
code && code.props.className | |
? extractLanguageName(code.props.className.replace("hljs ", "")) | |
: ""; | |
const handleCopyClick = useCallback(() => { | |
if (code && React.isValidElement(code)) { | |
const codeString = extractTextFromNode(code.props.children); | |
void navigator.clipboard.writeText(codeString); | |
setIsCopied(true); | |
setTimeout(() => { | |
setIsCopied(false); | |
}, 2000); | |
} | |
}, [code]); | |
return ( | |
<div className="mb-4 flex flex-col "> | |
<div className="flex w-full items-center justify-between rounded-t-lg bg-slate-10 p-1 px-4 text-white"> | |
<div>{language.charAt(0).toUpperCase() + language.slice(1)}</div> | |
<button | |
onClick={handleCopyClick} | |
className="flex items-center gap-2 rounded px-2 py-1 hover:bg-slate-9 focus:outline-none" | |
> | |
<FiClipboard /> | |
{isCopied ? "Copied!" : "Copy Code"} | |
</button> | |
</div> | |
<pre className="rounded-t-[0]">{children}</pre> | |
</div> | |
); | |
}; | |
interface CustomCodeBlockProps { | |
inline?: boolean; | |
className?: string; | |
children: ReactNode; | |
} | |
const CustomCodeBlock = ({ inline, className, children }: CustomCodeBlockProps) => { | |
// Inline code blocks will be placed directly within a paragraph | |
if (inline) { | |
return <code className="rounded bg-slate-2 px-1 py-[1px] text-black">{children}</code>; | |
} | |
const language = className ? className.replace("language-", "") : "plaintext"; | |
return <code className={`hljs ${language}`}>{children}</code>; | |
}; | |
const CustomLink = ({ children, href }) => { | |
return ( | |
<a | |
className={clsx( | |
"mx-0.5 rounded-full bg-sky-600 px-1.5 py-0.5 align-top text-[0.6rem] text-white", | |
"transition-colors duration-300 hover:bg-sky-500 hover:text-white" | |
)} | |
href={href as string} | |
target="_blank" | |
rel="noopener noreferrer" | |
> | |
{children} | |
</a> | |
); | |
}; | |
const isValidCustomCodeBlock = ( | |
element: ReactNode | |
): element is React.ReactElement<CustomCodeBlockProps> => | |
React.isValidElement(element) && element.type === CustomCodeBlock; | |
const extractLanguageName = (languageString: string): string => { | |
// The provided language will be "language-{PROGRAMMING_LANGUAGE}" | |
const parts = languageString.split("-"); | |
if (parts.length > 1) { | |
return parts[1] || ""; | |
} | |
return ""; | |
}; | |
const extractTextFromNode = (node: React.ReactNode): string => { | |
if (typeof node === "string") { | |
return node; | |
} | |
if (Array.isArray(node)) { | |
return node.map(extractTextFromNode).join(""); | |
} | |
if (React.isValidElement(node)) { | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access | |
return extractTextFromNode(node.props.children); | |
} | |
return ""; | |
}; | |
export default MarkdownRenderer; | |