Spaces:
Runtime error
Runtime error
File size: 4,515 Bytes
cd6f98e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
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;
|