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
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('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''')
|
|
|
|
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 } })
|
|
}
|
|
|