cc-haha/desktop/src/components/markdown/MarkdownRenderer.tsx
程序员阿江(Relakkes) 3b83c9bd34 fix: disable hljs.highlightAuto for unknown languages, hide line numbers for plain text
- highlightAuto was misdetecting plain text (file trees, command output) as
  code and applying wrong syntax colors (red keywords, green tags, etc.)
- Now only highlights when language is explicitly specified and known to hljs
- Code blocks without a specified language render as clean plain text
- Line numbers hidden for plain text blocks (only shown for actual code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 21:55:00 +08:00

108 lines
4.0 KiB
TypeScript

import { useMemo } from 'react'
import { marked, type Tokens } from 'marked'
import { escapeHtml, highlightCodeLines, isHighlightable } from '../chat/highlightCode'
type Props = {
content: string
}
const renderer = new marked.Renderer()
renderer.code = function ({ text, lang }: Tokens.Code) {
const languageLabel = escapeHtml(lang || 'code')
const lines = text.split('\n')
const hasLanguage = isHighlightable(lang)
const highlightedLines = highlightCodeLines(text, lang)
// Show line numbers only when language is known (actual code).
// Plain text, file trees, command output etc. look better without them.
const body = hasLanguage
? highlightedLines
.map((line, index) => `
<div class="grid grid-cols-[3rem,minmax(0,1fr)] gap-0 hover:bg-[#f6f8fa]/50">
<span class="select-none border-r border-[#eaeef2] bg-[#fafbfc] px-2 py-px text-right text-[11px] text-[#8b949e]">${index + 1}</span>
<span class="overflow-hidden bg-white px-3 py-px whitespace-pre-wrap break-words text-[#24292f]">${line || '&nbsp;'}</span>
</div>
`)
.join('')
: highlightedLines
.map((line) => `
<div class="hover:bg-[#f6f8fa]/50">
<span class="block bg-white px-3 py-px whitespace-pre-wrap break-words text-[#24292f]">${line || '&nbsp;'}</span>
</div>
`)
.join('')
return `
<div class="my-4 overflow-hidden rounded-lg border border-[#d0d7de] bg-[#f6f8fa] text-[#24292f]">
<div class="flex items-center justify-between border-b border-[#d0d7de] bg-white px-3 py-1.5 text-[11px] text-[#57606a]">
<div class="flex items-center gap-3">
<span class="font-semibold uppercase tracking-[0.14em] text-[#57606a]">${languageLabel}</span>
<span>${lines.length} ${lines.length === 1 ? 'line' : 'lines'}</span>
</div>
<button class="rounded-md border border-[#d0d7de] bg-white px-2 py-0.5 text-[11px] text-[#57606a] transition-colors hover:bg-[#f3f4f6] hover:text-[#24292f]" data-copy-code="${escapeHtml(text)}">
Copy
</button>
</div>
<div class="max-h-[420px] overflow-auto">
<div class="min-w-full font-[var(--font-mono)] text-[12px] leading-[1.3]">${body}</div>
</div>
</div>
`
}
marked.setOptions({
breaks: true,
gfm: true,
})
marked.use({ renderer })
export function MarkdownRenderer({ content }: Props) {
const html = useMemo(() => {
try {
return marked.parse(content) as string
} catch {
return content
}
}, [content])
const handleClick = async (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.target as HTMLElement | null
const button = target?.closest<HTMLButtonElement>('[data-copy-code]')
if (!button) return
const text = button.getAttribute('data-copy-code')
if (!text) return
try {
await navigator.clipboard.writeText(text)
const original = button.textContent
button.textContent = 'Copied'
window.setTimeout(() => {
button.textContent = original
}, 1500)
} catch {
// Ignore clipboard errors and keep the original label.
}
}
return (
<div
className="prose prose-sm max-w-none text-[var(--color-text-primary)]
prose-headings:text-[var(--color-text-primary)] prose-headings:font-semibold
prose-p:my-2 prose-p:leading-relaxed
prose-code:text-[13px] prose-code:font-[var(--font-mono)] prose-code:bg-[var(--color-surface-info)] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded
prose-pre:!bg-transparent prose-pre:!p-0 prose-pre:!shadow-none
prose-a:text-[var(--color-text-accent)] prose-a:no-underline hover:prose-a:underline
prose-strong:text-[var(--color-text-primary)]
prose-ul:my-2 prose-ol:my-2
prose-li:my-0.5
prose-table:text-sm
prose-th:bg-[var(--color-surface-info)] prose-th:px-3 prose-th:py-2
prose-td:px-3 prose-td:py-2 prose-td:border-[var(--color-border)]"
dangerouslySetInnerHTML={{ __html: html }}
onClick={handleClick}
/>
)
}