feat: Add CodeBlock component for syntax highlighting in Markdown
This commit is contained in:
parent
3161367a90
commit
024d0506f3
66
src/components/Common/CodeBlock.tsx
Normal file
66
src/components/Common/CodeBlock.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user