import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import katex from 'katex'
import hljs from 'highlight.js'
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) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
}),
})
marked.setOptions({
breaks: true,
gfm: true,
})
export function renderMarkdown(text) {
return marked.parse(text)
}
/**
* 后处理 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 = ''
const checkSvg = ''
copyBtn.addEventListener('click', () => {
const raw = code?.textContent || ''
navigator.clipboard.writeText(raw).then(() => {
copyBtn.innerHTML = checkSvg
setTimeout(() => { copyBtn.innerHTML = '' }, 1500)
}).catch(() => {
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)
copyBtn.innerHTML = checkSvg
setTimeout(() => { copyBtn.innerHTML = '' }, 1500)
})
})
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;'
}
}
}