2024-07-10 16:44:39 +00:00
|
|
|
import { memo, useEffect, useState } from 'react';
|
2024-07-17 18:54:46 +00:00
|
|
|
import { bundledLanguages, codeToHtml, isSpecialLang, type BundledLanguage, type SpecialLanguage } from 'shiki';
|
2024-07-24 15:43:32 +00:00
|
|
|
import { classNames } from '~/utils/classNames';
|
|
|
|
import { createScopedLogger } from '~/utils/logger';
|
2024-07-10 16:44:39 +00:00
|
|
|
import styles from './CodeBlock.module.scss';
|
|
|
|
|
|
|
|
const logger = createScopedLogger('CodeBlock');
|
|
|
|
|
|
|
|
interface CodeBlockProps {
|
2024-07-17 18:54:46 +00:00
|
|
|
className?: string;
|
2024-07-10 16:44:39 +00:00
|
|
|
code: string;
|
2024-07-17 18:54:46 +00:00
|
|
|
language?: BundledLanguage | SpecialLanguage;
|
|
|
|
theme?: 'light-plus' | 'dark-plus';
|
|
|
|
disableCopy?: boolean;
|
2024-07-10 16:44:39 +00:00
|
|
|
}
|
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
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);
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
const copyToClipboard = () => {
|
|
|
|
if (copied) {
|
|
|
|
return;
|
|
|
|
}
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
navigator.clipboard.writeText(code);
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
setCopied(true);
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
setCopied(false);
|
|
|
|
}, 2000);
|
|
|
|
};
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (language && !isSpecialLang(language) && !(language in bundledLanguages)) {
|
|
|
|
logger.warn(`Unsupported language '${language}'`);
|
|
|
|
}
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
logger.trace(`Language = ${language}`);
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
const processCode = async () => {
|
|
|
|
setHTML(await codeToHtml(code, { lang: language, theme }));
|
|
|
|
};
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
processCode();
|
|
|
|
}, [code]);
|
2024-07-10 16:44:39 +00:00
|
|
|
|
2024-07-17 18:54:46 +00:00
|
|
|
return (
|
|
|
|
<div className={classNames('relative group text-left', className)}>
|
|
|
|
<div
|
2024-07-10 16:44:39 +00:00
|
|
|
className={classNames(
|
2024-07-17 18:54:46 +00:00
|
|
|
styles.CopyButtonContainer,
|
|
|
|
'bg-white absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100',
|
2024-07-10 16:44:39 +00:00
|
|
|
{
|
2024-07-17 18:54:46 +00:00
|
|
|
'rounded-l-0 opacity-100': copied,
|
2024-07-10 16:44:39 +00:00
|
|
|
},
|
|
|
|
)}
|
|
|
|
>
|
2024-07-17 18:54:46 +00:00
|
|
|
{!disableCopy && (
|
|
|
|
<button
|
|
|
|
className={classNames(
|
|
|
|
'flex items-center bg-transparent p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
|
|
|
|
{
|
|
|
|
'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>
|
2024-07-10 16:44:39 +00:00
|
|
|
</div>
|
2024-07-17 18:54:46 +00:00
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|