Spaces:
Paused
Paused
File size: 5,166 Bytes
9853396 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 'use client';
import { type ComponentProps, createContext, type HTMLAttributes, useContext, useEffect, useRef, useState } from 'react';
import type { Element } from 'hast';
import { CheckIcon, CopyIcon } from 'lucide-react';
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from 'shiki';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
type MaskedCodeBlockProps = HTMLAttributes<HTMLDivElement> & {
displayCode: string;
realCode: string;
language: BundledLanguage;
showLineNumbers?: boolean;
preRenderedHtml?: {
light: string;
dark: string;
};
};
type MaskedCodeBlockContextType = {
displayCode: string;
realCode: string;
};
const MaskedCodeBlockContext = createContext<MaskedCodeBlockContextType>({
displayCode: '',
realCode: '',
});
const lineNumberTransformer: ShikiTransformer = {
name: 'line-numbers',
line(node: Element, line: number) {
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 highlightMaskedCode(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 MaskedCodeBlock = ({ displayCode, realCode, language, showLineNumbers = false, preRenderedHtml, className, children, ...props }: MaskedCodeBlockProps) => {
const [html, setHtml] = useState<string>(preRenderedHtml?.light || '');
const [darkHtml, setDarkHtml] = useState<string>(preRenderedHtml?.dark || '');
const [isLoading, setIsLoading] = useState(!preRenderedHtml);
const mounted = useRef(false);
useEffect(() => {
if (preRenderedHtml) {
setHtml(preRenderedHtml.light);
setDarkHtml(preRenderedHtml.dark);
setIsLoading(false);
return;
}
setIsLoading(true);
highlightMaskedCode(displayCode, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) {
setHtml(light);
setDarkHtml(dark);
setIsLoading(false);
mounted.current = true;
}
});
return () => {
mounted.current = false;
};
}, [displayCode, language, showLineNumbers, preRenderedHtml]);
return (
<MaskedCodeBlockContext.Provider value={{ displayCode, realCode }}>
<div className={cn('group bg-background text-foreground relative w-full overflow-hidden rounded-md border', className)} {...props}>
<div className='relative'>
{isLoading ? (
<div className='flex items-center justify-center p-4'>
<div className='h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent' />
</div>
) : (
<>
<div
className='[&>pre]:bg-background! [&>pre]:text-foreground! overflow-hidden dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:p-4 [&>pre]:text-sm'
dangerouslySetInnerHTML={{ __html: html }}
/>
<div
className='[&>pre]:bg-background! [&>pre]:text-foreground! hidden overflow-hidden dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:p-4 [&>pre]:text-sm'
dangerouslySetInnerHTML={{ __html: darkHtml }}
/>
</>
)}
{children && <div className='absolute top-2 right-2 flex items-center gap-2'>{children}</div>}
</div>
</div>
</MaskedCodeBlockContext.Provider>
);
};
export type MaskedCodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const MaskedCodeBlockCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props }: MaskedCodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const { realCode } = useContext(MaskedCodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
onError?.(new Error('Clipboard API not available'));
return;
}
try {
await navigator.clipboard.writeText(realCode);
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>
);
};
|