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;' } } }