- 渲染管线迁移至 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)
181 lines
6.4 KiB
TypeScript
181 lines
6.4 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import { renderMarkdown, renderMarkdownSync } from '../markdown'
|
||
|
||
describe('renderMarkdown (MetonaEditor 内置解析器)', () => {
|
||
it('should render a simple heading', async () => {
|
||
const result = await renderMarkdown('# Hello World')
|
||
expect(result).toContain('<h1')
|
||
expect(result).toContain('Hello World')
|
||
})
|
||
|
||
it('should render bold text', async () => {
|
||
const result = await renderMarkdown('**bold**')
|
||
expect(result).toContain('<strong')
|
||
expect(result).toContain('bold')
|
||
})
|
||
|
||
it('should render italic text', async () => {
|
||
const result = await renderMarkdown('*italic*')
|
||
expect(result).toContain('<em')
|
||
expect(result).toContain('italic')
|
||
})
|
||
|
||
it('should render inline code', async () => {
|
||
const result = await renderMarkdown('`code`')
|
||
expect(result).toContain('<code')
|
||
expect(result).toContain('code')
|
||
})
|
||
|
||
it('should render code blocks', async () => {
|
||
const md = '```javascript\nconst x = 1;\n```'
|
||
const result = await renderMarkdown(md)
|
||
expect(result).toContain('<pre')
|
||
expect(result).toContain('<code')
|
||
expect(result).toContain('const')
|
||
})
|
||
|
||
it('should render links', async () => {
|
||
const result = await renderMarkdown('[link](https://example.com)')
|
||
expect(result).toContain('<a')
|
||
expect(result).toContain('https://example.com')
|
||
expect(result).toContain('link')
|
||
})
|
||
|
||
it('should render unordered lists', async () => {
|
||
const result = await renderMarkdown('- item 1\n- item 2')
|
||
expect(result).toContain('<ul')
|
||
expect(result).toContain('<li')
|
||
expect(result).toContain('item 1')
|
||
expect(result).toContain('item 2')
|
||
})
|
||
|
||
it('should render ordered lists', async () => {
|
||
const result = await renderMarkdown('1. first\n2. second')
|
||
expect(result).toContain('<ol')
|
||
expect(result).toContain('first')
|
||
expect(result).toContain('second')
|
||
})
|
||
|
||
it('should render blockquotes', async () => {
|
||
const result = await renderMarkdown('> quote')
|
||
expect(result).toContain('<blockquote')
|
||
expect(result).toContain('quote')
|
||
})
|
||
|
||
it('should render tables via GFM', async () => {
|
||
const md = '| A | B |\n|---|---|\n| 1 | 2 |'
|
||
const result = await renderMarkdown(md)
|
||
expect(result).toContain('<table')
|
||
expect(result).toContain('<td')
|
||
})
|
||
|
||
it('should render strikethrough via GFM', async () => {
|
||
const result = await renderMarkdown('~~deleted~~')
|
||
expect(result).toContain('<del')
|
||
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 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 be sync-callable via renderMarkdownSync', () => {
|
||
const result = renderMarkdownSync('# Sync')
|
||
expect(result).toContain('<h1')
|
||
})
|
||
})
|
||
|
||
describe('fixImageSrcs — 相对路径图片修复', () => {
|
||
const FILE = '/home/user/docs/note.md'
|
||
|
||
it('should fix relative image paths to file://', async () => {
|
||
const result = await renderMarkdown('', FILE)
|
||
expect(result).toContain('src="file:///home/user/docs/pic.png"')
|
||
})
|
||
|
||
it('should fix nested relative paths', async () => {
|
||
const result = await renderMarkdown('', FILE)
|
||
expect(result).toContain('src="file:///home/user/docs/assets/a.png"')
|
||
})
|
||
|
||
it('should fix unix absolute paths', async () => {
|
||
const result = await renderMarkdown('', FILE)
|
||
expect(result).toContain('src="file:///abs/pic.png"')
|
||
})
|
||
|
||
it('should not touch http/https/data:/file: URLs', async () => {
|
||
const md = '  '
|
||
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 = ' '
|
||
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('', 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('')
|
||
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('', '/note.md')
|
||
expect(result).toContain('src="file:///pic.png"')
|
||
})
|
||
|
||
it('should escape special chars in fixed path', async () => {
|
||
const result = await renderMarkdown('', FILE)
|
||
expect(result).toContain('src="file:///home/user/docs/a&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 not emit img for javascript: image src', async () => {
|
||
const result = await renderMarkdown(')')
|
||
expect(result).not.toContain('<img')
|
||
expect(result).not.toContain('src=')
|
||
})
|
||
})
|