import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import katex from 'katex'
import { highlightCode } from './highlight'
import { COPY_BUTTON_RESET_MS } from '../constants'
function renderMath(text, displayMode) {
try {
return katex.renderToString(text, {
displayMode,
throwOnError: false,
strict: false,
})
} catch {
return text
}
}
// marked extension for inline math $...$
const mathExtension = {
name: 'math',
level: 'inline',
start(src) {
const idx = src.search(/(?${renderMath(token.text, true)}`
},
}
marked.use({
extensions: [blockMathExtension, mathExtension],
...markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
return highlightCode(code, lang)
},
}),
})
marked.setOptions({
breaks: true,
gfm: true,
})
export function renderMarkdown(text) {
return marked.parse(text)
}
const COPY_SVG = ''
const CHECK_SVG = ''
/**
* 后处理 HTML:为所有代码块包裹 .code-block 容器,
* 添加语言标签和复制按钮。在组件 onMounted / updated 中调用。
*/
export function enhanceCodeBlocks(container) {
if (!container) return
const pres = container.querySelectorAll('pre')
for (const pre of pres) {
// 跳过已处理过的
if (pre.parentElement.classList.contains('code-block')) continue
const code = pre.querySelector('code')
const langClass = code?.className || ''
const lang = langClass.replace(/hljs\s+language-/, '').trim() || 'code'
const wrapper = document.createElement('div')
wrapper.className = 'code-block'
const header = document.createElement('div')
header.className = 'code-header'
const langSpan = document.createElement('span')
langSpan.className = 'code-lang'
langSpan.textContent = lang
const copyBtn = document.createElement('button')
copyBtn.className = 'code-copy-btn'
copyBtn.title = '复制'
copyBtn.innerHTML = COPY_SVG
copyBtn.addEventListener('click', () => {
const raw = code?.textContent || ''
const copy = () => { copyBtn.innerHTML = CHECK_SVG; setTimeout(() => { copyBtn.innerHTML = COPY_SVG }, COPY_BUTTON_RESET_MS) }
if (navigator.clipboard) {
navigator.clipboard.writeText(raw).then(copy)
} else {
const ta = document.createElement('textarea')
ta.value = raw
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
copy()
}
})
header.appendChild(langSpan)
header.appendChild(copyBtn)
pre.parentNode.insertBefore(wrapper, pre)
wrapper.appendChild(header)
wrapper.appendChild(pre)
// 重置 pre 的内联样式,确保由 .code-block 系列样式控制
pre.style.cssText = 'margin:0;padding:0;border:none;border-radius:0;background:transparent;'
if (code) {
code.style.cssText = 'display:block;padding:12px 12px 12px 16px;overflow-x:auto;font-family:JetBrains Mono,Fira Code,monospace;font-size:13px;line-height:1.5;'
}
}
}