release: v0.6.0 — 内置解析器迁移 / Toast 全面接入 / Sqlark 深度集成

- 渲染管线迁移至 MetonaEditor 内置解析器,移除 unified/rehype 全家桶(9 个依赖)
- 修复相对路径图片修复的目录前缀碰撞与路径解析 bug
- ConfirmDialog/useConfirm/LoadingSpinner/useDocStats 移除,改用 MeToast.confirm/loading/promise
- 新增状态栏(字数/行数/阅读时间/光标位置)、Zen 模式、数据备份导出导入
- Sqlark: 版本化迁移(addMigration/migrateTo)、subscribe 表变更、备份 exportAll/importTable
- 数据库损坏自愈:异常退出残留残缺 SSTable 导致打开失败时自动重建
- 大纲导航改用 scrollToLine 官方 API
- 修复 rollup 平台包互删(postinstall 自动补齐)+ 集成测试(fake-indexeddb)
This commit is contained in:
thzxx
2026-08-09 16:50:06 +08:00
parent 20eb39efc5
commit ee5f35177e
71 changed files with 4837 additions and 4345 deletions
+93 -19
View File
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'
import { renderMarkdown } from '../markdown'
import { renderMarkdown, renderMarkdownSync } from '../markdown'
describe('renderMarkdown', () => {
describe('renderMarkdown (MetonaEditor 内置解析器)', () => {
it('should render a simple heading', async () => {
const result = await renderMarkdown('# Hello World')
expect(result).toContain('<h1')
@@ -26,7 +26,7 @@ describe('renderMarkdown', () => {
expect(result).toContain('code')
})
it('should render code blocks with syntax highlighting', async () => {
it('should render code blocks', async () => {
const md = '```javascript\nconst x = 1;\n```'
const result = await renderMarkdown(md)
expect(result).toContain('<pre')
@@ -75,32 +75,106 @@ describe('renderMarkdown', () => {
expect(result).toContain('deleted')
})
it('should render task list', async () => {
const result = await renderMarkdown('- [x] done\n- [ ] todo')
expect(result).toContain('me-task-item')
expect(result).toContain('checked')
})
it('should render mermaid as .me-mermaid container', async () => {
const md = '```mermaid\ngraph TD\nA-->B\n```'
const result = await renderMarkdown(md)
expect(result).toContain('me-mermaid')
expect(result).toContain('class="mermaid"')
expect(result).toContain('graph TD')
})
it('should handle empty content', async () => {
const result = await renderMarkdown('')
expect(result).toBe('')
})
it('should return error HTML on rendering failure with invalid input', async () => {
// unified should still handle gracefully, but verify error path works
const result = await renderMarkdown('normal text')
expect(result).not.toContain('渲染错误')
it('should handle null and undefined filePath', async () => {
expect(await renderMarkdown('# No File', null)).toContain('No File')
expect(await renderMarkdown('# No File')).toContain('No File')
})
it('should cache processors for same filePath', async () => {
// Call twice with same filePath to test caching
const result1 = await renderMarkdown('# Test', '/test/file.md')
const result2 = await renderMarkdown('# Test 2', '/test/file.md')
expect(result1).toContain('<h1')
expect(result2).toContain('Test 2')
it('should be sync-callable via renderMarkdownSync', () => {
const result = renderMarkdownSync('# Sync')
expect(result).toContain('<h1')
})
})
it('should handle null filePath', async () => {
const result = await renderMarkdown('# No File', null)
expect(result).toContain('No File')
describe('fixImageSrcs — 相对路径图片修复', () => {
const FILE = '/home/user/docs/note.md'
it('should fix relative image paths to file://', async () => {
const result = await renderMarkdown('![img](./pic.png)', FILE)
expect(result).toContain('src="file:///home/user/docs/pic.png"')
})
it('should fix nested relative paths', async () => {
const result = await renderMarkdown('![img](assets/a.png)', FILE)
expect(result).toContain('src="file:///home/user/docs/assets/a.png"')
})
it('should fix unix absolute paths', async () => {
const result = await renderMarkdown('![img](/abs/pic.png)', FILE)
expect(result).toContain('src="file:///abs/pic.png"')
})
it('should not touch http/https/data:/file: URLs', async () => {
const md = '![a](https://x.com/a.png) ![b](data:image/png;base64,xx) ![c](file:///c.png)'
const result = await renderMarkdown(md, FILE)
expect(result).toContain('https://x.com/a.png')
expect(result).toContain('data:image/png;base64,xx')
expect(result).toContain('file:///c.png')
})
it('should skip out-of-directory traversal (../ and ../../)', async () => {
const md = '![a](../outside.png) ![b](../../outside.png)'
const result = await renderMarkdown(md, FILE)
// 越界路径保持原样(不注入 file://)
expect(result).not.toContain('file:')
expect(result).toContain('../outside.png')
expect(result).toContain('../../outside.png')
})
it('should not treat sibling directory with same prefix as inside', async () => {
// /home/user/docs-other/ 与 /home/user/docs/ 前缀相似但目录不同
const result = await renderMarkdown('![img](../docs-other/pic.png)', FILE)
expect(result).not.toContain('file:')
expect(result).toContain('../docs-other/pic.png')
})
it('should not touch images when filePath is null', async () => {
const result = await renderMarkdown('![img](./pic.png)')
expect(result).toContain('./pic.png')
expect(result).not.toContain('file:')
})
it('should fix relative image paths from root-level file', async () => {
const result = await renderMarkdown('![img](./pic.png)', '/note.md')
expect(result).toContain('src="file:///pic.png"')
})
it('should escape special chars in fixed path', async () => {
const result = await renderMarkdown('![img](./a&b.png)', FILE)
expect(result).toContain('src="file:///home/user/docs/a&amp;b.png"')
})
})
describe('XSS 防护(内置 safeUrl', () => {
it('should not emit anchor for javascript: URLs', async () => {
const result = await renderMarkdown('[x](javascript:alert(1))')
// 内置解析器将危险 URL 转义原样输出(不生成 <a>),文本不构成可执行链接
expect(result).not.toContain('<a')
expect(result).not.toContain('href=')
})
it('should handle undefined filePath', async () => {
const result = await renderMarkdown('# No File')
expect(result).toContain('No File')
it('should not emit img for javascript: image src', async () => {
const result = await renderMarkdown('![x](javascript:alert(1))')
expect(result).not.toContain('<img')
expect(result).not.toContain('src=')
})
})