import { describe, it, expect } from 'vitest'; import { promises as fs } from 'node:fs'; import nodePath from 'node:path'; import { fileURLToPath } from 'node:url'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkStringify from 'remark-stringify'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; const __dirname = nodePath.dirname(fileURLToPath(import.meta.url)); const fxDir = nodePath.join(__dirname, '..', 'fixtures', 'roundtrip'); const parse = (md: string) => unified().use(remarkParse).use(remarkGfm).use(remarkMath).parse(md); const roundtrip = (md: string) => unified().use(remarkParse).use(remarkGfm).use(remarkMath) .use(remarkStringify).use(remarkGfm).use(remarkMath).processSync(md).toString(); function normalize(node: any): any { if (node === null || typeof node !== 'object') return node; if (Array.isArray(node)) return node.map(normalize); const { position, ...rest } = node; for (const k of Object.keys(rest)) rest[k] = normalize(rest[k]); return rest; } const CASES = ['emphasis-nested','escaping','tables','codeblock','tasklist','headings','thematic-break','blockquote','lists-nested','whitespace','breaks','links','images','math']; describe('Markdown 往返 fixture 集(mdast 等价)', () => { it('fixture 数量足够', async () => { const names = (await fs.readdir(fxDir)).filter(f => f.endsWith('.md') && f !== 'README.md'); expect(names.length).toBeGreaterThanOrEqual(15); }); for (const name of CASES) { it(name + ' 往返 mdast 等价', async () => { const md = await fs.readFile(nodePath.join(fxDir, name + '.md'), 'utf8'); const mdPrime = roundtrip(md); expect(normalize(parse(mdPrime))).toEqual(normalize(parse(md))); }); } });