// 分片载入(docs/20):切分安全性 + 分片解析拼接与整篇解析的序列化等价。 import { describe, it, expect, afterEach } from 'vitest' import type { Editor } from '@milkdown/kit/core' import { createMarkdownEditor, getMarkdown } from '../src/editor/editor' import { splitMarkdownChunks, prepareChunkLoad } from '../src/editor/chunkLoader' let editors: Editor[] = [] afterEach(async () => { for (const e of editors) { try { await e.destroy() } catch { /* ignore */ } } editors = [] document.body.innerHTML = '' }) async function load(md: string): Promise { const root = document.createElement('div') document.body.appendChild(root) const editor = createMarkdownEditor(root, md) editors.push(editor) await editor.create() return editor } /** 生成超过阈值的富结构文档:多标题、宽松列表、任务、表格、公式、Mermaid、含空行围栏 */ function bigDoc(): string { const parts: string[] = [] for (let s = 1; s <= 20; s++) { parts.push(`## 第 ${s} 节`) parts.push('') parts.push(`第 ${s} 节的正文段落,包含**加粗**、*斜体*与行内公式 $a_${s}+b$,以及足够长的中文填充文本。`.repeat(60)) parts.push('') parts.push('- 条目甲') parts.push('') parts.push('- [ ] 任务乙') parts.push('') parts.push('| 列一 | 列二 |') parts.push('| --- | --- |') parts.push(`| 值 ${s} | ${s} |`) parts.push('') parts.push('```text') parts.push(`code ${s} 第一行`) parts.push('') parts.push('# 这行在围栏里不是标题') parts.push('') parts.push('```') parts.push('') parts.push('> 引用第一段') parts.push('>') parts.push('> 引用第二段') parts.push('') parts.push('```mermaid') parts.push('graph TD') parts.push(' A-->B') parts.push('```') parts.push('') parts.push(`$$\nE_${s} = mc^2\n$$`) parts.push('') parts.push('---') parts.push('') } return parts.join('\n') } describe('splitMarkdownChunks', () => { it('拼接回去与原文逐字相同,片数 > 1', () => { const md = bigDoc() expect(md.length).toBeGreaterThan(60_000) const chunks = splitMarkdownChunks(md) expect(chunks.length).toBeGreaterThan(1) expect(chunks.join('')).toBe(md) }) it('围栏内的空行与 # 行不触发切分', () => { const md = `# 顶\n\n\`\`\`text\n第一行\n\n# 假标题\n\n\`\`\`\n\n正文 ${'填充'.repeat(40)}` const chunks = splitMarkdownChunks(md) expect(chunks.join('')).toBe(md) // 没有合法切点时整篇一片(小文档本来也一片) expect(chunks.length).toBe(1) }) it('含链接引用定义时整篇一片(定义可能在文末被前文引用)', () => { const md = `${bigDoc()}\n\n[ref1]: https://example.com\n` expect(splitMarkdownChunks(md).length).toBe(1) expect(prepareChunkLoad(md)).toBeNull() }) it('小文档不走分片', () => { expect(prepareChunkLoad('# 小\n\n正文')).toBeNull() }) }) describe('分片载入与整篇解析等价', () => { it('逐片追加后的序列化 == 整篇解析的序列化', async () => { const md = bigDoc() const whole = getMarkdown(await load(md)) const handle = prepareChunkLoad(md) expect(handle).not.toBeNull() if (!handle) return const editor = await load(handle.firstChunk) const progress: number[] = [] await handle.appendRest(editor, (done) => { progress.push(done) }, () => false) expect(progress[progress.length - 1]).toBe(handle.totalChunks) const chunked = getMarkdown(editor) expect(chunked).toBe(whole) }, 60_000) }) describe('列表内嵌围栏不被切开(docs/29)', () => { it('分片序列化与整篇逐字相等', async () => { const parts: string[] = [] for (let s = 1; s <= 30; s++) { parts.push(`## 小节 ${s}`) parts.push('') parts.push('正文填充。'.repeat(420)) parts.push('') parts.push('- 列表项,内含缩进围栏代码:') parts.push('') parts.push(' ```js') parts.push(` const v${s} = ${s}`) parts.push(' ```') parts.push('') parts.push('- 下一项') parts.push('') } const md = parts.join('\n') expect(md.length).toBeGreaterThan(60_000) const whole = getMarkdown(await load(md)) const handle = prepareChunkLoad(md) expect(handle).not.toBeNull() const editor = await load(handle!.firstChunk) await handle!.appendRest(editor, () => undefined, () => false) expect(getMarkdown(editor)).toBe(whole) }, 60000) })