You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

98 lines
3.1 KiB

import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import DOMPurify from 'isomorphic-dompurify'
import { stripFrontMatter } from './markdown-front-matter'
const escapeHtml = (value: string): string =>
value
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
const toSafeLanguageClass = (lang: string): string => {
const normalized = lang.trim().toLowerCase().replaceAll(/[^a-z0-9_-]/g, '')
return normalized || 'text'
}
const getHighlightedCode = (source: string, lang: string): string => {
const normalizedLang = lang.trim().toLowerCase()
if (normalizedLang && hljs.getLanguage(normalizedLang)) {
return hljs.highlight(source, { language: normalizedLang, ignoreIllegals: true }).value
}
if (normalizedLang) {
return hljs.highlightAuto(source).value
}
return escapeHtml(source)
}
const md = new MarkdownIt({
html: false,
linkify: true,
typographer: false,
breaks: true,
// 开启代码块语言识别
highlight: function (str: string, lang: string) {
// 处理语言标签(如果没有指定语言,默认 text)
const language = toSafeLanguageClass(lang || 'text')
const highlightedCode = getHighlightedCode(str, lang || '')
// 生成带语言标识 + 复制按钮的代码块外壳
return (
`<div class="code-block-wrapper"><div class="code-header"><span class="code-lang">${language}</span>` +
`<button class="copy-btn" type="button" data-copy-code>复制</button></div>` +
`<pre><code class="hljs language-${language}">${highlightedCode}</code></pre></div>`
)
},
})
/** 全局复制代码函数与事件绑定标记 */
declare global {
interface Window {
copyCode: (btn: HTMLButtonElement) => void
__markdownCopyBound?: boolean
}
}
// 只在客户端环境注册复制函数
if (typeof window !== 'undefined') {
window.copyCode = function (btn: HTMLButtonElement) {
const codeBlock = btn.closest('.code-block-wrapper')!.querySelector('code')!
const text = codeBlock.textContent || ''
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.textContent
btn.textContent = '已复制'
btn.classList.add('is-copied')
// 2秒后恢复按钮文字
setTimeout(() => {
btn.textContent = originalText
btn.classList.remove('is-copied')
}, 2000)
})
}
if (!window.__markdownCopyBound) {
document.addEventListener('click', (event) => {
const target = event.target
if (!(target instanceof Element)) {
return
}
const btn = target.closest<HTMLButtonElement>('button[data-copy-code]')
if (!btn) {
return
}
window.copyCode(btn)
})
window.__markdownCopyBound = true
}
}
/** 将 Markdown 转为可安全用于 `v-html` 的 HTML(禁用源码中的原始 HTML)。 */
export function renderSafeMarkdown(src: string): string {
if (!src.trim()) {
return ''
}
return DOMPurify.sanitize(md.render(stripFrontMatter(src)), { USE_PROFILES: { html: true } })
}