feat: Add CodeBlock component for syntax highlighting in Markdown

This commit is contained in:
n4ze3m 2024-05-30 23:49:18 +05:30
parent 3161367a90
commit 024d0506f3
2 changed files with 85 additions and 49 deletions

View File

@ -0,0 +1,66 @@
import { Tooltip } from "antd"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { FC, memo, useState } from "react"
import { useTranslation } from "react-i18next"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
interface Props {
language: string
value: string
}
export const CodeBlock: FC<Props> = memo(({ language, value }) => {
const [isBtnPressed, setIsBtnPressed] = useState(false)
const { t } = useTranslation("common")
return (
<>
<div className="code relative text-base font-sans codeblock bg-zinc-950 rounded-md overflow-hidden">
<div className="flex bg-gray-800 items-center justify-between py-1.5 px-4">
<span className="text-xs lowercase text-gray-200">{language}</span>
<div className="flex items-center">
<Tooltip title={t("copyToClipboard")}>
<button
onClick={() => {
navigator.clipboard.writeText(value)
setIsBtnPressed(true)
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}}
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100">
{!isBtnPressed ? (
<ClipboardIcon className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4 text-green-400" />
)}
</button>
</Tooltip>
</div>
</div>
<SyntaxHighlighter
language={language}
style={coldarkDark}
PreTag="div"
customStyle={{
margin: 0,
width: "100%",
background: "transparent",
padding: "1.5rem 1rem"
}}
lineNumberStyle={{
userSelect: "none"
}}
codeTagProps={{
style: {
fontSize: "0.9rem",
fontFamily: "var(--font-mono)"
}
}}>
{value}
</SyntaxHighlighter>
</div>
</>
)
})

View File

@ -1,68 +1,38 @@
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import remarkGfm from "remark-gfm" import remarkGfm from "remark-gfm"
import { nightOwl } from "react-syntax-highlighter/dist/cjs/styles/prism"
import remarkMath from "remark-math" import remarkMath from "remark-math"
import ReactMarkdown from "react-markdown" import ReactMarkdown, { Options } from "react-markdown"
import "property-information" import "property-information"
import React from "react" import React from "react"
import { Tooltip } from "antd" import { Tooltip } from "antd"
import { CheckIcon, ClipboardIcon } from "lucide-react" import { CheckIcon, ClipboardIcon } from "lucide-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { FC, memo } from "react"
import { CodeBlock } from "./CodeBlock"
export const MemoizedReactMarkdown: FC<Options> = memo(
ReactMarkdown,
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.className === nextProps.className
)
export default function Markdown({ message }: { message: string }) { export default function Markdown({ message }: { message: string }) {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const { t } = useTranslation("common")
return ( return (
<React.Fragment> <React.Fragment>
<ReactMarkdown <MemoizedReactMarkdown
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark" className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
components={{ components={{
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "") const match = /language-(\w+)/.exec(className || "")
return !inline ? ( return !inline ? (
<div className="code relative text-base bg-gray-800 rounded-md overflow-hidden"> <CodeBlock
<div className="flex items-center justify-between py-1.5 px-4"> language={match ? match[1] : ""}
<span className="text-xs lowercase text-gray-200"> value={String(children).replace(/\n$/, "")}
{className && className.replace("language-", "")}
</span>
<div className="flex items-center">
<Tooltip title={t("copyToClipboard")}>
<button
onClick={() => {
navigator.clipboard.writeText(children[0] as string)
setIsBtnPressed(true)
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}}
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100">
{!isBtnPressed ? (
<ClipboardIcon className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4 text-green-400" />
)}
</button>
</Tooltip>
</div>
</div>
<SyntaxHighlighter
{...props}
children={String(children).replace(/\n$/, "")}
style={nightOwl}
key={Math.random()}
customStyle={{
margin: 0,
fontSize: "1rem",
lineHeight: "1.5rem"
}}
language={(match && match[1]) || ""}
codeTagProps={{
className: "text-sm"
}}
/> />
</div>
) : ( ) : (
<code className={`${className} font-semibold`} {...props}> <code className={`${className} font-semibold`} {...props}>
{children} {children}
@ -85,7 +55,7 @@ export default function Markdown({ message }: { message: string }) {
} }
}}> }}>
{message} {message}
</ReactMarkdown> </MemoizedReactMarkdown>
</React.Fragment> </React.Fragment>
) )
} }