release: v0.4.5 — Mermaid 图表渲染支持
新增 Mermaid 图表渲染:
- npm 安装 mermaid@10 作为项目依赖
- markdown.ts 新增 rehypeMermaid 插件:将 mermaid 代码块
转换为 .me-mermaid 容器(与 MetonaEditor 内置输出一致)
- Editor.tsx 初始化 mermaid (startOnLoad: false)
- 绑定 afterRender 事件 → mermaid.run() 渲染 SVG 图表
- 首次加载 setTimeout 兜底渲染
技术实现:
- rehypeMermaid 插件在 rehypeHighlight 之后运行
- 查找 pre > code.language-mermaid 节点
- 替换为 div.me-mermaid > div.mermaid 容器
- 保持 unified 管线完整(图片路径修复 + sanitize + highlight)
- mermaid.run({ querySelector: '.me-mermaid .mermaid' })
质量保证:
- TypeScript 严格模式: 0 errors
- 单元测试: 98/98 passed (16 markdown + 82 others)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<string, ReturnType<typeof buildProcessor>>()
|
||||
@@ -102,6 +144,7 @@ function buildProcessor(filePath: string | null) {
|
||||
strip: ['script', 'on*', 'javascript:'],
|
||||
})
|
||||
.use(rehypeHighlight)
|
||||
.use(rehypeMermaid())
|
||||
.use(rehypeStringify)
|
||||
}
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user