网易首页 > 网易号 > 正文 申请入驻

ProseMirror作者花3小时修了个"假bug"

0
分享至


一个Backspace键,能让加粗标题的格式"污染"到普通段落。这个bug在Milkdown(基于ProseMirror的markdown编辑器)里藏了多久?作者花了3小时追踪,最后发现:根子不在ProseMirror,而在markdown编辑器对"格式"的理解方式上。

这不是一个代码错误,是两个bug叠在一起的建模困境。

第一步:复现那个"诡异"的行为

操作路径极其简单。打一个加粗的标题,光标移到标题最开头,按Backspace。标题会和上面的段落合并,但加粗格式会"流"进原本没有格式的段落文字里。

用户视角:我只是删了个换行,为什么下面的格式跑上面去了?

开发者第一反应:joinTextblockBackward这个命令有问题。这是ProseMirror处理"向前合并文本块"的核心命令,Backspace触发的正是它。

但跟进去之后,事情开始变味。

joinTextblockBackward的调用链是这样的:用户按Backspace → 检测到光标在文本块开头 → 触发joinTextblockBackward → 内部调用joinTextblocksAround → 执行replaceStep删除两个块之间的边界。

replaceStep用的是Slice.empty,意思是"用空切片替换掉这段边界"。真正干活的是Fitter算法,它负责把第二个块的内容"缝"进第一个块。

Fitter只关心节点结构,不碰内联标记(inline marks)。文本节点原样转移,加粗、斜体、代码格式全都跟着走。

这里有道安全阀:clearIncompatible函数会剥离目标节点类型不允许的标记。但段落允许加粗,所以什么都没发生。还有splitBlockKeepMarks处理的是回车键(Enter)的反向操作,但它管的是storedMarks——光标行为,不是内容合并。

标记就这么活下来了。而且它们本来就该活下来。

第二步:为什么"活下来"是对的,也是错的

想象两段普通段落:

She said the results were

**statistically significant** and could not be ignored.

光标在第二段开头,按Backspace。合并成一段,加粗必须保留——用户明确加粗了"statistically significant",这是有意的内容,不是误操作。


如果ProseMirror在合并时剥离所有标记,等于主动销毁用户内容。这个设计是对的。

但标题场景不同。在markdown编辑器里,"# **Bold Heading**"的加粗标记来自源码,用户看到的是"标题的视觉权重",格式被存了两个地方:节点类型(heading)和内联标记(bold)。

ProseMirror的视角:heading是个块节点,里面包着带bold标记的文本节点。合并时块类型变成paragraph,但文本节点原封不动,bold标记还在。

ProseMirror不知道这些bold是"标题的视觉属性",还是"用户明确要加粗"。它看到的只是内联标记,而段落允许加粗,所以保留。

这不是ProseMirror的bug。这是markdown编辑器特有的建模问题——你把一个视觉概念(标题加粗)拆成了两层存储,却在合并时只处理了一层。

第三步:拦截、修复、原子化

作者最终的解法是在命令到达编辑器状态之前拦截它。

具体动作:快照标题内容区域 → 在事务里追加removeMark调用,清掉原标题区域的加粗 → 同时清除storedMarks,防止光标继承格式 → 所有操作打包进一个事务,保证undo能原子回退。

代码很干净。一个独立的ProseMirror插件,只监听Backspace键,只在检测到heading块被向前合并时触发。

import { Plugin, PluginKey } from "prosemirror-state";

import { joinTextblockBackward } from "prosemirror-commands";

const headingBackspacePlugin = new Plugin({

key: new PluginKey("heading-backspace"),

props: {

handleKeyDown(view, event) {

if (event.key !== "Backspace") return false;

// ... 检测光标位置、块类型、执行修复逻辑


这个方案的关键是"原子性"。如果分两步执行(先合并、再清格式),undo会卡在中间状态。打包成一个事务,用户按Ctrl+Z时,整个操作一起回退,体验才对。

第四步:为什么这事值得写三千字

这个案例戳中了富文本编辑器的一个深层张力:用户的心智模型和编辑器的数据结构模型, rarely 对齐。

用户说"标题是加粗的",意思是"标题这个整体看起来粗"。markdown编辑器说"标题是加粗的",意思是"heading节点里包着带bold标记的text节点"。合并时用户期待的是前者(视觉属性随块类型消失),ProseMirror执行的是后者(内联标记独立存活)。

类似的裂缝无处不在。列表项按Tab缩进,用户觉得是"层级变化",编辑器可能是"换了个list_item节点、改了indent属性、或者套了层嵌套列表"。粘贴富文本时,用户期待"看起来一样",编辑器要在完全不同的schema之间做映射。

ProseMirror的设计哲学是"不猜测用户意图"。标记存了就是存了,合并时不动,除非schema明确禁止。这个保守策略避免了数据丢失,但也把"意图修复"的责任推给了上层。

Milkdown作为markdown编辑器,必须在ProseMirror的通用能力和markdown的特定语义之间搭桥。这个Backspace处理,就是搭桥的成本。

有意思的是,作者最初以为要改ProseMirror核心,最后发现只需要一个30行左右的插件。这种"问题在上层"的错位感,做过大型系统的人都懂。

另一个细节:storedMarks的处理。光标继承格式是ProseMirror的默认行为,但在这个场景下,如果不清掉,用户继续打字会默认加粗,体验更诡异。修复方案里专门处理了这一点,说明作者完整走完了用户场景。

最后看数据。这个插件只影响heading块的向前合并,不影响普通段落的正常合并,也不影响其他编辑操作。范围控制得极窄,副作用极小。

但每个markdown编辑器都要面对这个问题吗?取决于你怎么存heading的格式。如果你把heading的"粗"完全交给CSS(比如所有h1默认font-weight: bold),不在数据层存bold标记,就不会有这个bug。但那样又没法支持"# **partial bold** heading"这种混合样式。

建模没有银弹,只有取舍。

这个修复方案会进Milkdown主分支吗?作者没提。但issue页面有人追问:如果用户确实想在heading里保留部分加粗(比如"# **Warning:** Message"),这个修复会不会误伤?作者的回答是:检测逻辑会精确匹配"整个heading内容都被加粗"的情况,部分加粗不会触发清理。

边界情况永远比想象中多。一个Backspace键,牵出的是整类"块级语义 vs 内联标记"的协调难题。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
三观炸裂!翟欣欣出轨聊天记录流出,尺度大到咂舌,判12年都嫌少

三观炸裂!翟欣欣出轨聊天记录流出,尺度大到咂舌,判12年都嫌少

有范又有料
2025-09-29 14:21:11
台湾前参谋总长李喜明一席话,直接让岛内炸了锅!

台湾前参谋总长李喜明一席话,直接让岛内炸了锅!

安安说
2026-01-11 11:12:07
险胜7分!没有詹姆斯,残阵湖人,这么强的吗?有震惊到

险胜7分!没有詹姆斯,残阵湖人,这么强的吗?有震惊到

李絙在北漂
2026-04-08 15:23:48
据说:市面上90%的烤鸭都是用这种做的?你还吃吗?

据说:市面上90%的烤鸭都是用这种做的?你还吃吗?

今朝牛马
2026-01-08 18:32:41
李谷一的“女儿”曝光,原来是我们熟悉的歌手,连续多年登上春晚

李谷一的“女儿”曝光,原来是我们熟悉的歌手,连续多年登上春晚

胡一舸南游y
2026-03-30 15:50:41
刚刚,阿联酋正式参战,翼龙2炸伊朗炼油厂,特朗普这下收场难了

刚刚,阿联酋正式参战,翼龙2炸伊朗炼油厂,特朗普这下收场难了

共工之锚
2026-04-09 00:07:33
马斯克被调查,炸上热搜!

马斯克被调查,炸上热搜!

财经三分钟pro
2026-04-08 15:15:17
单依纯不简单啊

单依纯不简单啊

牛锅巴小钒
2026-03-30 10:43:11
死磕29年,耗光两代人:五代十国的乱局,是这两个家族的私人恩怨

死磕29年,耗光两代人:五代十国的乱局,是这两个家族的私人恩怨

老达子
2026-04-04 06:10:03
被假货包围的北面,终于忍无可忍

被假货包围的北面,终于忍无可忍

金错刀
2026-04-07 11:12:47
张兰生日!情绪低落发文,儿子儿媳没送祝福,只有一人记得她生日

张兰生日!情绪低落发文,儿子儿媳没送祝福,只有一人记得她生日

胡一舸南游y
2026-04-08 16:13:27
又一个户外运动品牌杀入中国!

又一个户外运动品牌杀入中国!

独角Mall
2026-04-07 15:51:43
红34师幸存一团长,55年授中将,临终遗言:要和他们葬在一起

红34师幸存一团长,55年授中将,临终遗言:要和他们葬在一起

历史龙元阁
2026-04-07 18:10:14
蒋经国查知陈诚助共谍亲属,不告蒋介石,反倒亲手销毁证据

蒋经国查知陈诚助共谍亲属,不告蒋介石,反倒亲手销毁证据

唠叨说历史
2026-03-23 16:09:55
光纤涨价只是开胃菜!空芯光纤才是AI算力真正的万亿级赛道

光纤涨价只是开胃菜!空芯光纤才是AI算力真正的万亿级赛道

林子说事
2026-04-06 19:03:45
逆转青岛!仅打两分钟得到媒体人怒赞:最后时刻盖帽和篮板立功了

逆转青岛!仅打两分钟得到媒体人怒赞:最后时刻盖帽和篮板立功了

南海浪花
2026-04-08 23:10:12
官媒对王虹的称呼变了,两字之差释放强烈信号,韦东奕说得太对

官媒对王虹的称呼变了,两字之差释放强烈信号,韦东奕说得太对

翰飞观事
2026-04-08 20:04:16
轰43分!杜锋良苦用心得到了回报,广东队收获争冠法宝

轰43分!杜锋良苦用心得到了回报,广东队收获争冠法宝

体育哲人
2026-04-08 22:51:16
伊朗为啥同意停火了?

伊朗为啥同意停火了?

静思有我
2026-04-08 22:20:55
欧冠从未夺冠球员射手榜:姆巴佩69球第1,凯恩51球第3

欧冠从未夺冠球员射手榜:姆巴佩69球第1,凯恩51球第3

懂球帝
2026-04-09 00:09:07
2026-04-09 01:15:00
Ping值焦虑
Ping值焦虑
有态度网友ytd
891文章数 21关注度
往期回顾 全部

科技要闻

造出地表最强AI,却死活不给你用!

头条要闻

央视披露:78亿变1亿 河南三地现巨额数据造假

头条要闻

央视披露:78亿变1亿 河南三地现巨额数据造假

体育要闻

40岁,但实力倒退12年

娱乐要闻

侯佩岑全家悉尼度假,一家四口幸福满溢

财经要闻

天津海河乳业回应直播间涉黄

汽车要闻

20万级满配华为全家桶 华境S是懂家庭的大六座

态度原创

本地
艺术
数码
公开课
军事航空

本地新闻

跟着歌声游安徽,听古村回响

艺术要闻

惊艳!她的私房自拍照让人无法抵挡!

数码要闻

小米多款新品本月发,看看你期待哪款?

公开课

李玫瑾:为什么性格比能力更重要?

军事要闻

文化符号当“弹药” 美伊将信息战带入新阶段

无障碍浏览 进入关怀版