Code Block
Syntax-highlighted code block with line numbers and copy button.
source cached 2026-10-19
components/infsh/code-block/code-block.tsx
'use client'
import { cn } from '@/lib/utils'
import { Check, Copy } from 'lucide-react'
import React, { memo, useMemo, useState } from 'react'
import { tokenize, type TokenizeContext } from '@/components/infsh/code-block/tokenizer'
import { tokenStyles } from '@/components/infsh/code-block/styles'
import { normalizeLanguage } from '@/components/infsh/code-block/languages'
import { getTextContent, splitLines, copyToClipboard } from '@/components/infsh/code-block/utils'
// Re-export types for consumers
export type { Token, TokenType, LanguageDefinition } from '@/components/infsh/code-block/types'
export { getLanguage, normalizeLanguage } from '@/components/infsh/code-block/languages'
/**
* Tokenize all lines with context passing between lines
* This handles multiline constructs like template literals
*/
function tokenizeLines(lines: string[], language: string) {
const tokenizedLines: { tokens: ReturnType<typeof tokenize>['tokens'] }[] = []
let context: TokenizeContext = {}
for (const line of lines) {
const result = tokenize(line, language, context)
tokenizedLines.push({ tokens: result.tokens })
context = result.context
}
return tokenizedLines
}
/**
* Renders a single line with pre-computed tokens
*/
const HighlightedLine = memo(function HighlightedLine({
tokens,
}: {
tokens: ReturnType<typeof tokenize>['tokens']
}) {
return (
<>
{tokens.map((token, i) =>
token.type ? (
<span key={i} className={tokenStyles[token.type]}>
{token.content}
</span>
) : (
<span key={i}>{token.content}</span>
)
)}
</>
)
})
export interface CodeBlockProps {
children: React.ReactNode
language?: string
className?: string
/** Show line numbers (default: true) */
showLineNumbers?: boolean
/** Show copy button (default: true) */
showCopyButton?: boolean
/** Show language header (default: true) */
showHeader?: boolean
/** Enable syntax highlighting (default: true) */
enableHighlighting?: boolean
/** Custom text size class */
textSize?: string
}
/**
* Full-featured code block with syntax highlighting, line numbers, and copy button
*/
export const CodeBlock = memo(function CodeBlock({
children,
language,
className,
showLineNumbers = true,
showCopyButton = true,
showHeader = true,
enableHighlighting = true,
textSize,
}: CodeBlockProps) {
const [copied, setCopied] = useState(false)
const text = getTextContent(children)
const lines = splitLines(text)
const normalizedLang = normalizeLanguage(language)
// Pre-tokenize all lines with context passing for multiline support
const tokenizedLines = useMemo(
() => enableHighlighting ? tokenizeLines(lines, normalizedLang) : null,
[lines, normalizedLang, enableHighlighting]
)
const handleCopy = async () => {
const success = await copyToClipboard(text)
if (success) {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
}
return (
<div
className={cn(
'relative group/codeblock my-6 rounded-lg border border-border overflow-hidden bg-muted/40 dark:bg-muted/20 min-h-0 flex flex-col',
className
)}
>
{/* Header bar */}
{showHeader && (
<div className="flex-none flex items-center justify-between px-2 py-2 border-b border-border/30 bg-muted">
<span className="text-xs text-zinc-500 font-mono">
{language || 'code'}
</span>
{showCopyButton && (
<button
onClick={handleCopy}
className="cursor-pointer flex items-center gap-1.5 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
aria-label="Copy code"
>
{copied ? (
<>
<Check className="h-3.5 w-3.5 text-emerald-400" />
<span>copied!</span>
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
<span>copy</span>
</>
)}
</button>
)}
</div>
)}
{/* Code content */}
<pre className="flex-1 min-h-0 overflow-auto p-4">
<code className={cn('grid font-mono', textSize || 'text-xs')}>
{lines.map((line, i) => (
<span
key={i}
className={cn(
'grid gap-6',
showLineNumbers ? 'grid-cols-[auto_1fr]' : 'grid-cols-1'
)}
>
{showLineNumbers && (
<span className="text-zinc-600 select-none text-right min-w-[2ch] text-xs leading-6">
{i + 1}
</span>
)}
<span className="whitespace-pre leading-6">
{tokenizedLines ? (
<HighlightedLine tokens={tokenizedLines[i]?.tokens ?? [{ type: null, content: line || ' ' }]} />
) : (
line || ' '
)}
</span>
</span>
))}
</code>
</pre>
</div>
)
})
export interface CompactCodeBlockProps {
children: React.ReactNode
className?: string
textSize?: string
/** Optional language for syntax highlighting */
language?: string
}
/**
* Simpler code block for compact/chat contexts
* Optional syntax highlighting, minimal styling
*/
export const CompactCodeBlock = memo(function CompactCodeBlock({
children,
className,
textSize = 'text-xs',
language,
}: CompactCodeBlockProps) {
const [copied, setCopied] = useState(false)
const text = getTextContent(children)
const lines = splitLines(text)
const normalizedLang = language ? normalizeLanguage(language) : null
// Pre-tokenize if language is provided
const tokenizedLines = useMemo(
() => normalizedLang ? tokenizeLines(lines, normalizedLang) : null,
[lines, normalizedLang]
)
const handleCopy = async () => {
const success = await copyToClipboard(text)
if (success) {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
}
return (
<div
className={cn(
'relative group/codeblock block bg-muted/30 rounded-lg max-w-full min-w-0',
className
)}
>
<button
onClick={handleCopy}
className="cursor-pointer absolute top-1 right-1 p-1.5 rounded bg-background/80 hover:bg-background border border-border opacity-0 group-hover/codeblock:opacity-100 transition-opacity z-10"
aria-label="Copy code"
>
{copied ? (
<Check className="h-3.5 w-3.5 text-pink-400" />
) : (
<Copy className="h-3.5 w-3.5 text-muted-foreground" />
)}
</button>
<div className="overflow-y-auto max-h-[inherit] p-2">
<pre className="overflow-x-auto">
<code className="grid">
{lines.map((line, i) => (
<span key={i} className="grid grid-cols-[auto_1fr] gap-4">
<span
className={cn(
textSize,
'text-muted-foreground/30 select-none text-right min-w-[2ch]'
)}
>
{i + 1}
</span>
<span
className={cn(
textSize,
'whitespace-pre-wrap break-all',
!tokenizedLines && 'text-muted-foreground'
)}
>
{tokenizedLines ? (
<HighlightedLine tokens={tokenizedLines[i]?.tokens ?? [{ type: null, content: line || ' ' }]} />
) : (
line || ' '
)}
</span>
</span>
))}
</code>
</pre>
</div>
</div>
)
})- Visual style
- terminal-mono