feat: show language label on fenced code blocks
Display a header bar above code blocks with the detected language name (e.g. TypeScript, Python, Shell). Pretty-prints common language identifiers. The copy button remains in the top-right corner.
This commit is contained in:
@@ -1,16 +1,54 @@
|
||||
import { useState, useCallback, type HTMLAttributes } from 'react';
|
||||
import { useState, useCallback, type HTMLAttributes, type ReactElement } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
|
||||
/** Extract the language from the nested <code> element's className (e.g. "language-ts"). */
|
||||
function extractLanguage(children: React.ReactNode): string | null {
|
||||
const codeEl = children as ReactElement<{ className?: string }> | undefined;
|
||||
const className = codeEl?.props?.className;
|
||||
if (typeof className !== 'string') return null;
|
||||
const match = className.match(/language-(\S+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/** Pretty-print common language identifiers. */
|
||||
const LANGUAGE_LABELS: Record<string, string> = {
|
||||
js: 'JavaScript',
|
||||
jsx: 'JSX',
|
||||
ts: 'TypeScript',
|
||||
tsx: 'TSX',
|
||||
py: 'Python',
|
||||
rb: 'Ruby',
|
||||
rs: 'Rust',
|
||||
go: 'Go',
|
||||
sh: 'Shell',
|
||||
bash: 'Bash',
|
||||
zsh: 'Zsh',
|
||||
yml: 'YAML',
|
||||
yaml: 'YAML',
|
||||
md: 'Markdown',
|
||||
json: 'JSON',
|
||||
html: 'HTML',
|
||||
css: 'CSS',
|
||||
sql: 'SQL',
|
||||
dockerfile: 'Dockerfile',
|
||||
toml: 'TOML',
|
||||
};
|
||||
|
||||
function formatLanguage(lang: string): string {
|
||||
return LANGUAGE_LABELS[lang] || lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom <pre> renderer for ReactMarkdown.
|
||||
* Wraps code blocks with a floating copy button.
|
||||
* Wraps code blocks with a language label and a floating copy button.
|
||||
*/
|
||||
export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const language = extractLanguage(props.children);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
// Extract text from the nested <code> element
|
||||
const code = (props.children as React.ReactElement<{ children?: string }> | undefined)?.props?.children;
|
||||
const code = (props.children as ReactElement<{ children?: string }> | undefined)?.props?.children;
|
||||
if (typeof code === 'string') {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopied(true);
|
||||
@@ -21,7 +59,12 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
||||
|
||||
return (
|
||||
<div className="group/code relative">
|
||||
<pre {...props} />
|
||||
{language && (
|
||||
<div className="flex items-center justify-between px-4 py-1.5 bg-zinc-800/80 border-b border-white/5 rounded-t-lg text-[11px] text-zinc-500 font-mono select-none">
|
||||
{formatLanguage(language)}
|
||||
</div>
|
||||
)}
|
||||
<pre {...props} className={`${props.className || ''} ${language ? '!rounded-t-none !mt-0' : ''}`} />
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-lg bg-zinc-700/60 hover:bg-zinc-600/80 border border-white/10 text-zinc-400 hover:text-zinc-200 opacity-0 group-hover/code:opacity-100 transition-opacity duration-150"
|
||||
|
||||
Reference in New Issue
Block a user