diff --git a/package.json b/package.json index 40fb41f..e86af3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marklite", - "version": "0.4.4", + "version": "0.4.5", "description": "Lightweight Markdown Editor for Windows", "main": "./dist/main/index.js", "scripts": { @@ -31,6 +31,7 @@ "@metona-team/metona-editor": "0.2.4", "@metona-team/metona-toast": "0.2.1", "dexie": "^4.0.11", + "mermaid": "^10.9.6", "nanoid": "^5.1.5", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 081c1ba..8dbec23 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef } from 'react' import MeEditor from '@metona-team/metona-editor' import type { MarkdownEditor } from '@metona-team/metona-editor' +import mermaid from 'mermaid' import { useTabStore } from '../../stores/tabStore' import { setMetonaEditorGetter, useEditorStore } from '../../stores/editorStore' import { settingsRepository } from '../../db/settingsRepository' @@ -11,6 +12,9 @@ import type { ThemeMode } from '../../types/settings' type EditMode = 'edit' | 'split' | 'preview' type ThemeName = 'light' | 'dark' | 'auto' | 'warm' | string +// v0.4.5: Mermaid 初始化(全局一次性配置) +mermaid.initialize({ startOnLoad: false, theme: 'default' }) + interface EditorProps { themeMode: ThemeMode onAppSave?: () => void @@ -147,6 +151,14 @@ export const Editor = React.memo(function Editor({ themeMode, onAppSave }: Edito setMetonaEditorGetter(() => editor) currentContentRef.current = activeTab?.content ?? '' + // v0.4.5: 绑定 afterRender → 触发 Mermaid 图表渲染 + const renderMermaid = () => { + try { mermaid.run({ querySelector: '.me-mermaid .mermaid' }) } catch { /* 容错 */ } + } + editor.on('afterRender', renderMermaid) + // 首次渲染后延迟触发一次 + setTimeout(renderMermaid, 300) + return () => { editor.destroy() editorRef.current = null diff --git a/src/renderer/lib/markdown.ts b/src/renderer/lib/markdown.ts index 4b09aec..86d7ab2 100644 --- a/src/renderer/lib/markdown.ts +++ b/src/renderer/lib/markdown.ts @@ -6,6 +6,7 @@ import rehypeRaw from 'rehype-raw' import rehypeSanitize, { defaultSchema } from 'rehype-sanitize' import rehypeStringify from 'rehype-stringify' import rehypeHighlight from 'rehype-highlight' +import { visit } from 'unist-util-visit' import type { Element, Root } from 'hast' // 简单的路径解析(Electron renderer 中没有 path 模块) @@ -71,6 +72,47 @@ function rehypeFixImages(filePath: string | null): Plugin<[], Root> { } } +// 自定义 rehype 插件:将 mermaid 代码块转为 .me-mermaid 容器 +// 在 rehypeHighlight 之后运行,此时 code 元素已有 language-mermaid 类名。 +// 输出结构需与 MetonaEditor 内置解析器一致,以便 mermaid.run() 统一查询。 +function rehypeMermaid(): Plugin<[], Root> { + return () => (tree: Root) => { + visit(tree, 'element', (node: Element, index, parent) => { + if (node.tagName !== 'pre') return + if (parent === null || parent === undefined || index === undefined) return + + const code = node.children.find( + (child): child is Element => child.type === 'element' && child.tagName === 'code' + ) + if (!code) return + + const className: string[] = (code.properties?.className as string[] | undefined) ?? [] + if (!className.some(c => c === 'language-mermaid' || c === 'mermaid')) return + + // 提取代码文本内容 + const text = (code.children ?? []) + .filter(c => c.type === 'text') + .map(c => (c as { type: 'text'; value: string }).value) + .join('') + + // 替换为 mermaid 渲染容器(与 MetonaEditor 内置输出一致) + const mermaidContainer: Element = { + type: 'element', + tagName: 'div', + properties: { className: ['me-mermaid'] }, + children: [{ + type: 'element', + tagName: 'div', + properties: { className: ['mermaid'] }, + children: [{ type: 'text', value: text }] + }] + } + + parent.children[index] = mermaidContainer + }) + } +} + // PF-01: Markdown处理器LRU缓存 const MAX_CACHE_SIZE = 20 const processorCache = new Map>() @@ -102,6 +144,7 @@ function buildProcessor(filePath: string | null) { strip: ['script', 'on*', 'javascript:'], }) .use(rehypeHighlight) + .use(rehypeMermaid()) .use(rehypeStringify) } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 9c88767..913dd18 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,5 +1,5 @@ // 共享常量 — 主进程和渲染进程共用 -export const APP_VERSION = 'v0.4.4' +export const APP_VERSION = 'v0.4.5' export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB export const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const export const SKIP_DIRS = new Set([