// 阶段 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('\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\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('', {
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('