| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- 'use client';
- import { Button } from '@/components/ui/button';
- import { cn } from '@/lib/utils';
- import { CheckIcon, CopyIcon } from 'lucide-react';
- import {
- type ComponentProps,
- createContext,
- type HTMLAttributes,
- useContext,
- useEffect,
- useRef,
- useState,
- } from 'react';
- import { type BundledLanguage, codeToHtml, type ShikiTransformer } from 'shiki';
- type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
- code: string;
- language: BundledLanguage;
- showLineNumbers?: boolean;
- };
- type CodeBlockContextType = {
- code: string;
- };
- const CodeBlockContext = createContext<CodeBlockContextType>({
- code: '',
- });
- const lineNumberTransformer: ShikiTransformer = {
- name: 'line-numbers',
- line(node, line) {
- node.children.unshift({
- type: 'element',
- tagName: 'span',
- properties: {
- className: [
- 'inline-block',
- 'min-w-10',
- 'mr-4',
- 'text-right',
- 'select-none',
- 'text-muted-foreground',
- ],
- },
- children: [{ type: 'text', value: String(line) }],
- });
- },
- };
- export async function highlightCode(
- code: string,
- language: BundledLanguage,
- showLineNumbers = false,
- ) {
- const transformers: ShikiTransformer[] = showLineNumbers ? [lineNumberTransformer] : [];
- return await Promise.all([
- codeToHtml(code, {
- lang: language,
- theme: 'one-light',
- transformers,
- }),
- codeToHtml(code, {
- lang: language,
- theme: 'one-dark-pro',
- transformers,
- }),
- ]);
- }
- export const CodeBlock = ({
- code,
- language,
- showLineNumbers = false,
- className,
- children,
- ...props
- }: CodeBlockProps) => {
- const [html, setHtml] = useState<string>('');
- const [darkHtml, setDarkHtml] = useState<string>('');
- const mounted = useRef(false);
- useEffect(() => {
- highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
- if (!mounted.current) {
- setHtml(light);
- setDarkHtml(dark);
- mounted.current = true;
- }
- });
- return () => {
- mounted.current = false;
- };
- }, [code, language, showLineNumbers]);
- return (
- <CodeBlockContext.Provider value={{ code }}>
- <div
- className={cn(
- 'group relative w-full overflow-hidden rounded-md border bg-background text-foreground',
- className,
- )}
- {...props}
- >
- <div className="relative">
- <div
- className="overflow-auto dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
- // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
- dangerouslySetInnerHTML={{ __html: html }}
- />
- <div
- className="hidden overflow-auto dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
- // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
- dangerouslySetInnerHTML={{ __html: darkHtml }}
- />
- {children && (
- <div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>
- )}
- </div>
- </div>
- </CodeBlockContext.Provider>
- );
- };
- export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
- onCopy?: () => void;
- onError?: (error: Error) => void;
- timeout?: number;
- };
- export const CodeBlockCopyButton = ({
- onCopy,
- onError,
- timeout = 2000,
- children,
- className,
- ...props
- }: CodeBlockCopyButtonProps) => {
- const [isCopied, setIsCopied] = useState(false);
- const { code } = useContext(CodeBlockContext);
- const copyToClipboard = async () => {
- if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
- onError?.(new Error('Clipboard API not available'));
- return;
- }
- try {
- await navigator.clipboard.writeText(code);
- setIsCopied(true);
- onCopy?.();
- setTimeout(() => setIsCopied(false), timeout);
- } catch (error) {
- onError?.(error as Error);
- }
- };
- const Icon = isCopied ? CheckIcon : CopyIcon;
- return (
- <Button
- className={cn('shrink-0', className)}
- onClick={copyToClipboard}
- size="icon"
- variant="ghost"
- {...props}
- >
- {children ?? <Icon size={14} />}
- </Button>
- );
- };
|