核心概念
理解 Incremark 的工作原理,有助于更好地使用和调试。
增量解析流程
┌──────────────────────────────────────────────────────────────┐
│ 输入流 │
│ "# 标题" → "\n\n内" → "容\n" → "\n## 二" → "级标题" │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 缓冲区 (Buffer) │
│ "# 标题\n\n内容\n\n## 二级标题" │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 边界检测 (Boundary Detection) │
│ 逐行扫描,识别块边界: │
│ - 空行分隔段落 │
│ - 标题行独立成块 │
│ - 代码围栏 ``` 需要配对 │
└──────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ 已完成块 (Completed) │ │ 待处理块 (Pending) │
│ • 不再重新解析 │ │ • 每次 append 重新解析 │
│ • 节点可被复用 │ │ • 可能不完整 │
└─────────────────────────┘ └─────────────────────────┘块状态
每个解析出的块有三种状态:
| 状态 | 说明 | 处理方式 |
|---|---|---|
pending | 正在接收,可能不完整 | 每次 append 重新解析 |
stable | 可能完整,但后续 chunk 可能改变 | 缓存但不确认 |
completed | 确认完成,不会再变 | 永久缓存,不再处理 |
边界检测规则
Incremark 使用启发式规则检测块边界:
简单块
- 空行 - 分隔段落
- 标题 (
#) - 独立成块 - 分隔线 (
---) - 独立成块
需要闭合的块
- 代码块 (
```) - 必须等待闭合围栏 - 容器 (
:::) - 必须等待闭合标记
嵌套块
- 列表 - 跟踪缩进级别
- 引用 (
>) - 跟踪引用深度 - 表格 - 检测分隔符行
上下文跟踪
为了正确处理嵌套结构,解析器维护上下文状态:
ts
interface BlockContext {
inFencedCode: boolean // 是否在代码块中
fenceChar?: string // 代码块围栏字符
fenceLength?: number // 围栏长度
listDepth: number // 列表嵌套深度
blockquoteDepth: number // 引用嵌套深度
inContainer: boolean // 是否在容器中
containerDepth: number // 容器嵌套深度
}AST 结构
Incremark 生成标准的 MDAST 格式:
ts
interface Root {
type: 'root'
children: RootContent[]
}
// 块级节点
type RootContent =
| Heading
| Paragraph
| Code
| List
| Blockquote
| Table
| ThematicBreak
| ...性能优化
为什么快?
- 跳过已完成块 - O(1) 而非 O(n)
- 增量行更新 - 只处理新增行
- 前缀和优化 - O(1) 计算行偏移
复杂度对比
| 操作 | 传统方式 | Incremark |
|---|---|---|
| 追加 chunk | O(n) | O(k) |
| 总解析量 | O(n²) | O(n) |
| 内存占用 | 反复创建 | 增量复用 |
n = 总字符数,k = 新增字符数