mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
feat: rewrite CodeViewer with prism-react-renderer — pure React rendering, GitHub theme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
941d8f0314
commit
b9a387406a
@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import hljs from 'highlight.js'
|
||||
import { useState } from 'react'
|
||||
import { Highlight, themes } from 'prism-react-renderer'
|
||||
import { CopyButton } from '../shared/CopyButton'
|
||||
|
||||
type Props = {
|
||||
@ -12,37 +12,14 @@ type Props = {
|
||||
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = true }: Props) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const lines = code.split('\n')
|
||||
const isTruncated = !expanded && lines.length > maxLines
|
||||
const visibleCode = isTruncated ? lines.slice(0, maxLines).join('\n') : code
|
||||
|
||||
// Only highlight when language is explicitly known.
|
||||
// Do NOT use highlightAuto — it misdetects plain text as code.
|
||||
const hasLanguage = !!language && !!hljs.getLanguage(language)
|
||||
const effectiveShowLineNumbers = showLineNumbers && hasLanguage
|
||||
|
||||
const highlightedLines = useMemo(() => {
|
||||
return visibleCode.split('\n').map((line) => {
|
||||
try {
|
||||
if (hasLanguage) {
|
||||
return hljs.highlight(line || ' ', { language: language!, ignoreIllegals: true }).value
|
||||
}
|
||||
return escapeHtml(line || ' ')
|
||||
} catch {
|
||||
return escapeHtml(line)
|
||||
}
|
||||
})
|
||||
}, [hasLanguage, language, visibleCode])
|
||||
|
||||
const startLine = 1
|
||||
const allLines = code.split('\n')
|
||||
const isTruncated = !expanded && allLines.length > maxLines
|
||||
const visibleCode = isTruncated ? allLines.slice(0, maxLines).join('\n') : code
|
||||
|
||||
const effectiveShowLineNumbers = showLineNumbers && !!language && language !== 'text'
|
||||
const languageLabel = language || 'code'
|
||||
|
||||
const visibleLines = visibleCode.split('\n')
|
||||
|
||||
const lineCountLabel = `${lines.length} ${lines.length === 1 ? 'line' : 'lines'}`
|
||||
|
||||
const showExpandToggle = lines.length > maxLines
|
||||
const lineCountLabel = `${allLines.length} ${allLines.length === 1 ? 'line' : 'lines'}`
|
||||
const showExpandToggle = allLines.length > maxLines
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-[#d0d7de] bg-[#f6f8fa] text-[#24292f]">
|
||||
@ -58,34 +35,45 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr
|
||||
</div>
|
||||
|
||||
<div className="max-h-[420px] overflow-auto">
|
||||
<div className="min-w-full font-[var(--font-mono)] text-[12px] leading-[1.3]">
|
||||
{visibleLines.map((line, index) => (
|
||||
effectiveShowLineNumbers ? (
|
||||
<div
|
||||
key={`${startLine + index}-${line}`}
|
||||
className="grid grid-cols-[3rem,minmax(0,1fr)] gap-0 hover:bg-[#f6f8fa]/50"
|
||||
>
|
||||
<span className="select-none border-r border-[#eaeef2] bg-[#fafbfc] px-2 py-px text-right text-[11px] text-[#8b949e]">
|
||||
{startLine + index}
|
||||
</span>
|
||||
<span
|
||||
className="overflow-hidden bg-white px-3 py-px whitespace-pre-wrap break-words text-[#24292f]"
|
||||
dangerouslySetInnerHTML={{ __html: highlightedLines[index] ?? escapeHtml(line) }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={`${startLine + index}-${line}`}
|
||||
className="hover:bg-[#f6f8fa]/50"
|
||||
>
|
||||
<span
|
||||
className="block bg-white px-3 py-px whitespace-pre-wrap break-words text-[#24292f]"
|
||||
dangerouslySetInnerHTML={{ __html: highlightedLines[index] ?? escapeHtml(line) }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
<Highlight theme={themes.github} code={visibleCode} language={language || 'text'}>
|
||||
{({ tokens, getLineProps, getTokenProps }) => (
|
||||
<pre className="m-0 bg-white p-0 font-[var(--font-mono)] text-[12px] leading-[1.3]">
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({ line })
|
||||
return effectiveShowLineNumbers ? (
|
||||
<div
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className="grid grid-cols-[3rem,minmax(0,1fr)] gap-0 hover:bg-[#f6f8fa]/50"
|
||||
style={undefined}
|
||||
>
|
||||
<span className="select-none border-r border-[#eaeef2] bg-[#fafbfc] px-2 py-px text-right text-[11px] text-[#8b949e]">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="overflow-hidden px-3 py-px whitespace-pre-wrap break-words">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className="hover:bg-[#f6f8fa]/50"
|
||||
style={undefined}
|
||||
>
|
||||
<span className="block px-3 py-px whitespace-pre-wrap break-words">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
|
||||
{showExpandToggle && (
|
||||
@ -93,17 +81,9 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="w-full border-t border-[#d0d7de] bg-[#f6f8fa] py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-[#57606a] transition-colors hover:bg-[#eaeef2] hover:text-[#24292f]"
|
||||
>
|
||||
{expanded ? 'Collapse' : `Show ${lines.length - maxLines} more lines`}
|
||||
{expanded ? 'Collapse' : `Show ${allLines.length - maxLines} more lines`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user