// 阶段 5 验收:主题注册表完整性、导出 HTML 管线(结构/表格/公式/代码)、HTML 导入转换。 import { describe, it, expect, vi } from 'vitest' import { DOC_THEMES, docThemeById } from '../src/themes' import { markdownToExportHtml, htmlToMarkdown, exportCss } from '../src/services/exportHtml' // jsdom 里 mermaid.render 依赖真实 SVG 布局 API 会失败;用桩验证导出管线的 // 替换与兜底逻辑(真机栅格化 PNG 只能在 Windows 复验) vi.mock('mermaid', () => ({ default: { initialize: () => undefined, render: async () => ({ svg: '图' }), }, })) describe('文档主题注册表', () => { it('7 套主题 id 唯一且可查', () => { expect(DOC_THEMES).toHaveLength(7) expect(new Set(DOC_THEMES.map((t) => t.id)).size).toBe(7) expect(docThemeById('paper').name).toBe('经典纸张') expect(docThemeById('不存在').id).toBe('default') for (const t of DOC_THEMES) expect(exportCss(t.id)).toContain('--f:') }) }) describe('导出 HTML 管线', () => { it('标题/表格/行内公式/代码块都转成对应 HTML(关高亮与 Mermaid 时走纯管线)', async () => { const md = '# 导出标题\n\n正文含 $E = mc^2$ 公式。\n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\n```ts\nconst v = 1;\n```\n' const html = await markdownToExportHtml(md, { title: '导出标题', themeId: 'github', imageMode: 'relative', highlight: false, renderMermaid: false, }) expect(html).toContain('') expect(html).toContain('

导出标题

') expect(html).toContain('') expect(html).toContain('katex') // 公式经 rehype-katex 渲染(含 MathML) expect(html).toContain('const v = 1;') expect(html).toContain('导出标题') }) it('Shiki 高亮开启时代码块带着色 span', async () => { const html = await markdownToExportHtml('```ts\nconst v: number = 42;\n```\n', { title: 't', themeId: 'default', imageMode: 'relative', renderMermaid: false, }) expect(html).toContain('style="color:') expect(html).toContain('42') }) it('图片内嵌模式用 resolveImage 替换为 data URL', async () => { const html = await markdownToExportHtml('![示意图](文章.assets/a.png)\n', { title: 't', themeId: 'default', imageMode: 'embed', highlight: false, renderMermaid: false, resolveImage: async (src) => (src.endsWith('a.png') ? 'data:image/png;base64,QUJD' : null), }) expect(html).toContain('src="data:image/png;base64,QUJD"') }) }) describe('导入转换', () => { it('HTML 转 Markdown 保留标题与列表', async () => { const md = await htmlToMarkdown('

标题

正文加粗

') expect(md).toContain('# 标题') expect(md).toMatch(/-\s+第一项/) expect(md).toContain('**加粗**') }) }) describe('二轮验收:导出与导入', () => { it('相对路径按导出文件位置重写(E3)', async () => { const html = await markdownToExportHtml('# 图\n\n![示意图](doc.assets/pic%20one.png)\n', { title: 't', themeId: 'default', imageMode: 'relative', docDir: 'C:/ws/docs', exportDir: 'C:/out', highlight: false, renderMermaid: false, }) expect(html).toContain('src="../ws/docs/doc.assets/pic%20one.png"') const sameDir = await markdownToExportHtml('![a](img.png)', { title: 't', themeId: 'default', imageMode: 'relative', docDir: 'C:/ws', exportDir: 'C:/ws', highlight: false, renderMermaid: false, }) expect(sameDir).toContain('src="img.png"') }) it('剪贴板版公式只剩 MathML(E4)', async () => { const html = await markdownToExportHtml('公式 $x=1$ 在此\n', { title: 't', themeId: 'default', imageMode: 'embed', forClipboard: true, highlight: false, renderMermaid: false, }) expect(html).toContain(' { const md = await htmlToMarkdown('标题' + '

正文題

第一段

' + '

第二段

') expect(md).toContain('# 正文題') expect(md).toContain('第一段') expect(md).toContain('第二段') expect(md).not.toContain('alert') expect(md).not.toContain('color:red') }) }) describe('二轮验收:Mermaid 导出替换', () => { it('剪贴板版 Mermaid 经桩渲染后不再是代码块(PNG 栅格化失败退回 SVG)', async () => { const html = await markdownToExportHtml('```mermaid\ngraph TD\n A-->B\n```\n', { title: 't', themeId: 'default', imageMode: 'embed', forClipboard: true, mermaidPng: true, highlight: false, }) expect(html).toContain('data-stub="mermaid"') expect(html).not.toContain('language-mermaid') }) })