三线并行审计(编辑器核心/Rust 后端/服务与组件)+ App 编排层自审。 高危:分片载入未收尾时窗格卸载回写部分序列化,干净标签会被静默 截断——cleanup 补 muted 守卫,create 完成回调补取消守卫。 性能:gitSync/outline/收起回调稳定化保住 Sidebar memo;光标行列 rAF 合并;图片显示解析加缓存;导出图片并发解析;chunkLoader 偏移前缀表;围栏切点只认顶格(列表内嵌围栏等价回归实测)。 Rust:资源命令 async 化 + 32MB 上限;搜索 junction 越界校验; recovery/workspace 条数与单条上限;启动坏库改名重建 + WAL; validate 补 ext:: 黑名单与 ref 形态;init 先种 .gitignore (含 .*.tmp-* 防崩溃残留被提交);游离 HEAD 不再猜分支。 验证:vitest 122+2、tsc、构建通过;Rust 过 rustfmt 解析、 validate 纯函数抽出实跑断言、命令对账 28=28。
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
// 分片载入(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<Editor> {
|
||
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)
|
||
})
|