| import { memo, useEffect, useState } from 'react'; | |
| import { bundledLanguages, codeToHtml, isSpecialLang, type BundledLanguage, type SpecialLanguage } from 'shiki'; | |
| import { classNames } from '~/utils/classNames'; | |
| import { createScopedLogger } from '~/utils/logger'; | |
| import styles from './CodeBlock.module.scss'; | |
| const logger = createScopedLogger('CodeBlock'); | |
| interface CodeBlockProps { | |
| className?: string; | |
| code: string; | |
| language?: BundledLanguage | SpecialLanguage; | |
| theme?: 'light-plus' | 'dark-plus'; | |
| disableCopy?: boolean; | |
| } | |
| export const CodeBlock = memo( | |
| ({ className, code, language = 'plaintext', theme = 'dark-plus', disableCopy = false }: CodeBlockProps) => { | |
| const [html, setHTML] = useState<string | undefined>(undefined); | |
| const [copied, setCopied] = useState(false); | |
| const copyToClipboard = () => { | |
| if (copied) { | |
| return; | |
| } | |
| navigator.clipboard.writeText(code); | |
| setCopied(true); | |
| setTimeout(() => { | |
| setCopied(false); | |
| }, 2000); | |
| }; | |
| useEffect(() => { | |
| if (language && !isSpecialLang(language) && !(language in bundledLanguages)) { | |
| logger.warn(`Unsupported language '${language}'`); | |
| } | |
| logger.trace(`Language = ${language}`); | |
| const processCode = async () => { | |
| setHTML(await codeToHtml(code, { lang: language, theme })); | |
| }; | |
| processCode(); | |
| }, [code]); | |
| return ( | |
| <div className={classNames('relative group text-left', className)}> | |
| <div | |
| className={classNames( | |
| styles.CopyButtonContainer, | |
| 'bg-transparant absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100', | |
| { | |
| 'rounded-l-0 opacity-100': copied, | |
| }, | |
| )} | |
| > | |
| {!disableCopy && ( | |
| <button | |
| className={classNames( | |
| 'flex items-center bg-accent-500 p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300 rounded-md transition-theme', | |
| { | |
| 'before:opacity-0': !copied, | |
| 'before:opacity-100': copied, | |
| }, | |
| )} | |
| title="Copy Code" | |
| onClick={() => copyToClipboard()} | |
| > | |
| <div className="i-ph:clipboard-text-duotone"></div> | |
| </button> | |
| )} | |
| </div> | |
| <div dangerouslySetInnerHTML={{ __html: html ?? '' }}></div> | |
| </div> | |
| ); | |
| }, | |
| ); | |