| 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> |
| ); |
| }, |
| ); |
|
|