跳到正文
QuireKit Markdown
开发者文档
预览版 · 未发布

与 Incremark 比较

为什么我们重新设计了 Incremark,以及新引擎怎样处理流式 Markdown。

Incremark 是我们开发的第一代流式 Markdown 工具。 QuireKit Markdown 延续了同一个目标:让 AI 一边生成文字,页面一边显示排版好的内容。

这次重新设计,主要解决一个容易被低估的问题:后面收到的内容,仍然可能改变前面那句话应该怎样显示。 新引擎在减少重复解析和渲染的同时,让这些后到的定义能够正确更新前文。

看一棵树怎样更新

“中间插入”会连续修改同一份原文三次:先在第二段加入加粗,再在第一段加入行内代码, 最后在中间插入两项列表。每次上一轮的结果都成为下一轮的起点。

点击播放,跟随移动片段进入原文落点,再找到橙色的受影响分支、换入蓝色的新节点。 绿色节点保留原树对象。可以暂停、单步、回退,或选择某次插入与某个阶段。 视图固定,不支持拖动或缩放。

第三次插在段落边界:第三段原文虽然没改,解析器仍重建了它的语法分支。 这也说明更新范围要看实际语法与上下文,不能只按改了几个字判断。 “末尾追加”和“后置引用”保留对照;后置定义到达后,前面变成链接的是 bar。 树和阅读结果来自真实引擎;移动轨迹与慢放是教学编排,不是底层执行日志。

这段树变化也由我们的流程图库绘制与播放:节点、连线、片段移动及树的替换,都使用同一个公共播放器。

正在准备示例…

对照旧版的流程

切换“旧 Incremark”和“当前算法”,沿着箭头看一份新片段到达后会经历哪些步骤。图使用我们现有的流程图编辑器渲染,支持缩放和查看大图。

正在准备示例…

为什么要重新设计

旧 Incremark 把正在生成的回答分成两部分:已经结束的段落,和还在继续写的部分。新文字到来时,尽量保留前面的解析结果,只重新解析末尾。这个思路能减少重复计算。

但 Markdown 不总是写完一段就能确定这一段的意思。例如,AI 可以先写正文,最后才给出链接地址:

md

在最后一行到达之前,[文档][api] 还没有可用的地址;地址到达以后,前面的“文档”应当变成链接。脚注也是如此:正文先出现,解释可能在整篇回答的末尾才到达。

旧版已经为这些情况做了处理:先记录引用,等定义到达后再补上链接。困难在于,有些后置定义还会改变哪几个方括号组成一个链接。这时,只给已有节点补地址就不够了。我们需要重新检查前面的语法,同时让页面更新正确的内容。

旧实现的两个具体问题

同一个链接地址定义了两次

假设下面的正文和两个地址依次到达:

md

按照 Markdown 的规则,同名定义应使用第一次出现的地址,所以“文档”应当指向 /first。旧版在合并定义时覆盖了原来的值,导致前面的链接改为 /second。当前引擎保留首次定义。

链接落在了错误的文字上

再看一个短例子。先收到第一行,随后收到 baz 的定义:

md

这段文字应该被理解为:保留普通文字 [foo],将 [bar][baz] 解释为链接,也就是让 bar 可以点击。

旧版提前把 [foo][bar] 识别成了一个引用,把 [baz] 留作另一个引用。等地址到达后,可点击的变成了 baz。前面的分组已经确定,补上地址也不能纠正它。当前引擎会依据实际到达的定义重新判断分组,因此链接落在 bar 上。

我们用旧版的两种引擎重新跑了这两个例子,结果如下:

情况旧 Marked旧 Micromark当前 QuireKit Markdown
api 定义了两次链接到 /second链接到 /second链接到 /first
相邻的三个方括号baz 可以点击baz 可以点击bar 可以点击

新算法怎样处理流式更新

1. 保留实际收到的原文

每次追加都写入同一份 Markdown。停止生成时保留已经收到的文字,不为了显示效果补写围栏、括号或 HTML 结束标签。切换到另一条消息时,再建立新的解析会话。

2. 让 Lezer 找出可以继续使用的解析结果

Lezer 把标题、段落、列表和链接等结构记录在语法树里。新文字到来后,它结合语法上下文判断哪些旧结果还能使用,哪些需要重新解析。我们复用这套机制来处理流式追加。

例如,在末尾开始新段落时,前面未受影响的段落可以继续使用。列表、围栏等结构发生变化时,解析器也会按相应的语法处理,而不是仅凭一个空行就断定前面的内容已经结束。

3. 后到的定义可以回头修正引用

新引擎先收集当前文档中的链接定义,再检查哪些段落的引用判断受到影响。像上面的 [foo][bar][baz],需要重新判断哪部分是链接;同名定义则按首次出现的值处理。

它也会记录段落查询过哪些引用,包括当时还不存在的引用。有足够依据确认一个新定义没有影响该段落时,就继续使用旧结果。

下面这张白板把依赖画出来:api 的定义到达后,需要重新检查用过它的段落 B。普通介绍和只查询 faq 的段落,满足复用条件时可以保留。

正在准备示例…

4. 显示内容也要检查,才能复用

语法结构没变,链接地址或脚注编号仍可能改变。因此,渲染缓存还会检查这些依赖,以及组件和 HTML 配置。条件满足时保留已有内容;受影响或无法确认的块重新生成。

这样,AI 继续往下写时,前面可以复用的内容继续保留;后面的定义改变了前文含义时,前面的链接和脚注仍会更新。

这些步骤由谁负责

解析、引用判断和缓存证明在公共内核里。Vue 和 React 负责各自的组件与提交生命周期;网络流和可选图表渲染由应用接入。

正在准备示例…

这三张图都保留了原始图稿。自己的文档也可以接入同样的流程图、思维导图与白板渲染。

看一次流式更新

点击“开始流式演示”,观察正文先出现、链接和脚注定义随后到达的过程。同一个脚注被引用两次,脚注正文里也有链接。点击“停止”可以保留当前已经收到的内容。

正在准备示例…

也可以把上面的两个短例子粘到 Playground,查看新引擎的输出。

速度有什么变化

旧版的 Marked 本身很快。换用 Lezer,并不意味着所有 Markdown 都会解析得更快。当前优化主要减少两种重复工作:重新解析没有受影响的文字,以及重新生成可以复用的显示内容。

此前原 demo 的解析测试中,Lezer/MSFE 快于旧 Micromark、慢于旧 Marked。这批数据早于当前渲染缓存,不能当作当前版本的速度排名。

大型列表、表格或很长的单段落,仍可能作为一个整体重新处理。判断流式体验时,需要同时看解析、渲染和浏览器更新,不能只看解析器耗时。测量方法见性能说明。

使用与迁移

QuireKit Markdown 支持 Vue 和 React,两者共用同一套解析规则。基本用法仍然是传入 Markdown文字;链接、代码块等可以换成自己的组件,也支持自定义语法和显式开启 HTML。

新包没有沿用旧 Incremark 的 API,旧插件需要适配。可以从 Vue 快速开始或 React 接入开始,按自定义语法与节点迁移扩展。当前仍为未发布的预览版;旧 Incremark 与当前的核心、Vue、React 包均使用 MIT 许可。

继续了解实现

如果你准备扩展解析器或研究缓存,下面这些细节会更有用:

  • 语法复用: CodeMirror 用文本事务和 TreeFragment 映射变化。我们在 Lezer 层提供 顶层块复用信息,保持原生语法和复用判断;引用处理放在 MSFE 的 parser 包装层。
  • 引用判断: 完整段落记录语法实际查询过的标签及其是否存在。只有这些查询结果未变, 才复用相应判断;没有完整记录的范围保守检查。地址和 title 的变化另由渲染层检查。
  • 渲染缓存: 检查原生 Tree 身份、源码范围、引用目标、脚注导航和渲染配置。 自定义节点可以用 cacheKey(document) 声明额外依赖;未声明时重新生成。 具体条件与缓存限额见性能说明。
  • 框架与资源: Vue 和 React 使用各自的组件。React 在提交后接受候选缓存,资源也在 客户端提交后加载;切换、卸载和迟到结果按取消与释放规则处理,见 React 接入。

文中两例于 2026-10-11 验证,旧版为 @incremark/core 1.0.2、commit 98f6b95。旧结果来自两引擎的 AST 与定义表,当前结果来自 React 服务端渲染,并以独立完整 GFM 解析和CommonMark 引用规则核对。这两个例子说明了具体问题,版本范围不延伸到后续 Incremark。

对应源码:旧解析会话、旧引用识别、旧定义合并。

QuireKit Markdown · QuireKit流式 Markdown · @quirekit/incremark · MIT