import { marked } from 'marked' import hljs from 'highlight.js' import 'highlight.js/lib/common' // 按需导入 common 包不包含的语言 import 'highlight.js/lib/languages/powershell' import 'highlight.js/lib/languages/dos' import 'highlight.js/lib/languages/autohotkey' import 'highlight.js/lib/languages/latex' import 'highlight.js/lib/languages/dockerfile' import 'highlight.js/lib/languages/cmake' import 'highlight.js/lib/languages/scala' import 'highlight.js/lib/languages/dart' import { getHljsLanguage } from './languageMap' let mermaidInstance: typeof import('mermaid').default | null = null let mermaidTheme: string | null = null // 检测当前是否为暗色主题 function isDarkTheme(): boolean { if (typeof document === 'undefined') return false return document.body.getAttribute('arco-theme')?.includes('dark') ?? false } async function loadMermaid() { const currentTheme = isDarkTheme() ? 'dark' : 'default' if (mermaidInstance && mermaidTheme === currentTheme) { return mermaidInstance } if (!mermaidInstance) { const m = await import('mermaid') mermaidInstance = m.default } mermaidInstance.initialize({ startOnLoad: false, theme: currentTheme, securityLevel: 'strict', themeVariables: Object.assign({ primaryColor: '#165DFF', primaryTextColor: '#ffffff', primaryBorderColor: '#4080FF' }, currentTheme === 'dark' ? { lineColor: '#4E5969', secondaryColor: '#0E42D2', tertiaryColor: '#0FC6C2', mainBkg: '#17171A', nodeBorder: '#165DFF', clusterBkg: '#232324', titleColor: '#FFFFFF', edgeLabelBackground: '#232324' } : { lineColor: '#86909C', secondaryColor: '#E8F3FF', tertiaryColor: '#722ED1', mainBkg: '#F2F3F5', nodeBorder: '#165DFF', clusterBkg: '#F7F8FA', titleColor: '#1D2129', edgeLabelBackground: '#F2F3F5' }) }) mermaidTheme = currentTheme return mermaidInstance } const renderer = new marked.Renderer() renderer.code = function(token: any) { if (token.lang === 'mermaid') { return `
${token.text}
` } const lang = getHljsLanguage(token.lang) let highlighted: string try { highlighted = hljs.highlight(token.text, { language: lang }).value } catch { highlighted = token.text.replace(/&/g, '&').replace(//g, '>') } return `
${highlighted}
` } 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 ` ${text} ` } // ========== 图片相对路径转换支持 ========== // 当前 Markdown 文件所在目录(由调用方在渲染前设置) let _currentFileDir: string = '' // 文件服务器 Base URL(由调用方在渲染前设置) let _fileServerBase: string = 'http://localhost:2652/localfs' /** * 设置当前 Markdown 文件所在目录(用于图片相对路径→文件服务器 URL 转换) * @param dir 文件所在目录的绝对路径,如 "D:/docs" 或 "/"(根目录) */ export function setCurrentFileDir(dir: string): void { _currentFileDir = dir } /** 获取当前设置的文件目录 */ export function getCurrentFileDir(): string { return _currentFileDir } /** * 设置文件服务器 Base URL(用于图片相对路径转换) * @param base 完整的 base URL 前缀,如 "http://localhost:2652/localfs" 或 "https://host:port/api/v1/proxy/localfs" */ export function setFileServerBase(base: string): void { _fileServerBase = base } /** * 将相对路径图片 src 解析为文件服务器 URL * - 绝对路径(Windows: D:/...、Unix: /usr/...)、网络URL、data URI → 不转换 * - 相对路径 → 基于当前文件目录解析为绝对路径,再编码为文件服务器 URL */ function resolveImageUrl(src: string, fileServerBase: string): string { if (!src) return src // 不转换:绝对路径(Windows 盘符)、网络协议、锚点、data URI if (/^(?:[a-zA-Z]:[/\\]|\/(?:[^/]|$)|https?:|ftp:|data:|#)/i.test(src)) return src // 解析相对路径(处理 ../ 和 ./) const dir = _currentFileDir || '/' const sep = dir.includes('\\') ? '\\' : '/' let resolved = normalizeRelativePath(dir, src, sep) // 编码路径(保留 / 分隔符) const encoded = encodeURIComponent(resolved).replace(/%2F/gi, '/').replace(/%5C/gi, '\\') return `${fileServerBase}/${encoded}` } /** * 规范化相对路径,处理 .. 和 . 段 */ function normalizeRelativePath(base: string, relative: string, sep: string): string { // 确保基础路径不以分隔符结尾 let baseNormalized = base.replace(/[\\/]+$/, '') if (!baseNormalized) baseNormalized = sep === '/' ? '/' : 'C:\\' const baseParts = baseNormalized.split(sep).filter(Boolean) const relParts = relative.split(/[\\/]/).filter(Boolean) for (const part of relParts) { if (part === '..') { baseParts.pop() // 向上一级 } else if (part !== '.') { baseParts.push(part) } } // 重建路径:Windows 绝对路径保留盘符前缀 if (/^[a-zA-Z]:$/i.test(baseNormalized.split(sep)[0] || '')) { return baseParts.join(sep) } // Unix 风格:以 / 开头 return sep + baseParts.join(sep) } // 判断是否为本地文件链接(相对路径或本地绝对路径) const isLocalFileLink = (href: string): boolean => { if (!href) return false if (/^(https?|ftp|mailto|tel|data):/i.test(href)) return false if (href.startsWith('#')) return false return true } // 自定义图片渲染器 - 转换相对路径为文件服务器 URL renderer.image = function(token: any) { const src = token.href || '' const title = token.title || '' const alt = token.text || '' const titleAttr = title ? ` title="${title}"` : '' // 判断是否需要转换(仅处理相对路径,且当前目录已设置) if (_currentFileDir && !/^(?:[a-zA-Z]:[/\\]|\/(?:[^/]|$)|https?:|ftp:|data:|#)/i.test(src)) { const resolvedSrc = resolveImageUrl(src, _fileServerBase) return `${alt}` } // 默认渲染(绝对路径 / 网络 URL / data URI / 未设置目录时原样输出) return `${alt}` } // 自定义链接渲染器 - 支持本地文件链接 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 `${text}` } if (isLocalFileLink(href)) { return `${text}` } return `${text}` } marked.use({ renderer, breaks: true, gfm: true, async: false }) export { marked } export async function renderMermaidDiagrams() { const mermaid = await loadMermaid() if (mermaid) { // 渲染前保存原始源码(textContent 在 SVG 渲染后会变成 CSS 垃圾) document.querySelectorAll('.mermaid:not([data-mermaid-src])').forEach(pre => { ;(pre as HTMLElement).setAttribute('data-mermaid-src', pre.textContent || '') }) await mermaid.run() } } /** 清除已渲染内容并重新渲染(用于主题切换后刷新) */ export async function rerenderMermaidDiagrams(container?: HTMLElement | null) { // 强制重新加载(清除缓存,让下次 loadMermaid 重新初始化新主题) mermaidInstance = null mermaidTheme = null const target = container || document target.querySelectorAll('.mermaid').forEach(pre => { const el = pre as HTMLElement if (el.getAttribute('data-processed')) { // 从保存的原始源码恢复,而非 textContent(SVG 的 textContent 是 CSS 垃圾) el.innerHTML = el.getAttribute('data-mermaid-src') || '' el.removeAttribute('data-processed') } }) await renderMermaidDiagrams() }