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 (
rehypeHighlight({ ignoreMissing: true })]}
components={{
pre: CustomPre,
code: CustomCodeBlock,
h1: (props) => {props.children}
,
h2: (props) => {props.children}
,
a: (props) => CustomLink({ children: props.children, href: props.href }),
p: (props) => {props.children}
,
ul: (props) => (
),
ol: (props) => (
{props.children}
),
li: (props) => {props.children},
}}
>
{children}
);
};
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 (
{language.charAt(0).toUpperCase() + language.slice(1)}
{children}
);
};
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 {children}
;
}
const language = className ? className.replace("language-", "") : "plaintext";
return {children}
;
};
const CustomLink = ({ children, href }) => {
return (
{children}
);
};
const isValidCustomCodeBlock = (
element: ReactNode
): element is React.ReactElement =>
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;