dia-gov's picture
Upload 569 files
cd6f98e verified
raw
history blame
4.52 kB
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;