Markdown 预览增强: - 支持点击本地文件链接(相对路径)打开对应文件 - 支持链接文本中的加粗/斜体等内联语法 - 锚点链接保持页面内跳转,外部链接新窗口打开 代码高亮增强: - 添加 sh/bash/shell 语言别名映射 - 安装 @codemirror/legacy-modes 支持 .sh 文件语法高亮
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
import { marked } from 'marked'
|
||
import hljs from 'highlight.js'
|
||
import 'highlight.js/lib/common'
|
||
// 额外导入 common 包不包含的语言
|
||
import 'highlight.js/lib/languages/bash'
|
||
import 'highlight.js/lib/languages/go'
|
||
import 'highlight.js/styles/github-dark.css'
|
||
import 'highlight.js/styles/github.css'
|
||
|
||
// 语言别名映射(sh -> bash 等)
|
||
const languageAliases: Record<string, string> = {
|
||
'sh': 'bash',
|
||
'shell': 'bash',
|
||
'zsh': 'bash',
|
||
'ksh': 'bash',
|
||
'ts': 'typescript',
|
||
'js': 'javascript',
|
||
'py': 'python',
|
||
'rb': 'ruby',
|
||
'yml': 'yaml',
|
||
'md': 'markdown'
|
||
}
|
||
|
||
let mermaidInstance: typeof import('mermaid').default | null = null
|
||
|
||
async function loadMermaid() {
|
||
if (mermaidInstance) return mermaidInstance
|
||
|
||
try {
|
||
const mermaid = await import('mermaid')
|
||
mermaid.default.initialize({
|
||
startOnLoad: false,
|
||
theme: 'default',
|
||
securityLevel: 'loose'
|
||
})
|
||
mermaidInstance = mermaid.default
|
||
return mermaidInstance
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
const renderer = new marked.Renderer()
|
||
|
||
renderer.code = function(token: any) {
|
||
if (token.lang === 'mermaid') {
|
||
return `<pre class="mermaid">${token.text}</pre>`
|
||
}
|
||
|
||
// 获取语言,支持别名
|
||
let lang = token.lang || 'plaintext'
|
||
lang = languageAliases[lang] || lang
|
||
|
||
// 检查语言是否支持
|
||
if (!hljs.getLanguage(lang)) {
|
||
lang = 'plaintext'
|
||
}
|
||
|
||
const highlighted = hljs.highlight(token.text, { language: lang }).value
|
||
return `<pre><code class="hljs language-${lang}" data-theme="auto">${highlighted}</code></pre>`
|
||
}
|
||
|
||
renderer.heading = function(token: any) {
|
||
const raw = token.raw || ''
|
||
const depth = token.depth || 1
|
||
const text = token.text || ''
|
||
|
||
const id = raw
|
||
.toLowerCase()
|
||
.replace(/[^\u4e00-\u9fa5a-z0-9\s-]/g, '')
|
||
.trim()
|
||
.replace(/\s+/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-+|-+$/g, '') || `heading-${Math.random().toString(36).slice(2, 11)}`
|
||
|
||
return `<h${depth} id="${id}" class="heading">
|
||
${text}<a href="#${id}" class="heading-anchor" aria-hidden="true" title="跳转到此标题">#</a>
|
||
</h${depth}>`
|
||
}
|
||
|
||
// 判断是否为本地文件链接(相对路径或本地绝对路径)
|
||
const isLocalFileLink = (href: string): boolean => {
|
||
if (!href) return false
|
||
// 排除 http/https/ftp/mailto 等外部链接
|
||
if (/^(https?|ftp|mailto|tel|data):/i.test(href)) return false
|
||
// 排除锚点链接
|
||
if (href.startsWith('#')) return false
|
||
// 相对路径或本地路径(如 ./file.md, ../file.md, /path/to/file, C:\path\file)
|
||
return true
|
||
}
|
||
|
||
// 自定义链接渲染器 - 支持本地文件链接
|
||
renderer.link = function(token: any) {
|
||
const href = token.href || ''
|
||
// 解析链接文本中的内联元素(如加粗、斜体等)
|
||
const text = this.parser.parseInline(token.tokens) || token.text || ''
|
||
const title = token.title || ''
|
||
|
||
const titleAttr = title ? ` title="${title}"` : ''
|
||
|
||
// 锚点链接 - 保持原样,页面内跳转
|
||
if (href.startsWith('#')) {
|
||
return `<a href="${href}"${titleAttr}>${text}</a>`
|
||
}
|
||
|
||
// 判断是否为本地文件链接
|
||
if (isLocalFileLink(href)) {
|
||
return `<a href="javascript:void(0)" data-local-link="${href}" class="local-file-link"${titleAttr}>${text}</a>`
|
||
}
|
||
|
||
// 外部链接使用默认行为
|
||
return `<a href="${href}" target="_blank" rel="noopener noreferrer"${titleAttr}>${text}</a>`
|
||
}
|
||
|
||
marked.use({ renderer, breaks: true, gfm: true })
|
||
|
||
export { marked }
|
||
|
||
export async function renderMermaidDiagrams() {
|
||
const mermaid = await loadMermaid()
|
||
if (mermaid) {
|
||
await mermaid.run()
|
||
}
|
||
}
|
||
|
||
|