news 2026/9/17 4:11:08

Plate 代码块围栏回归测试实战:用 `CodeBlockKit` 锁定三反引号自动格式化契约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Plate 代码块围栏回归测试实战:用 `CodeBlockKit` 锁定三反引号自动格式化契约

Plate 代码块围栏回归测试实战:用CodeBlockKit锁定三反引号自动格式化契约

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

导读

三反引号(```````)在行首被完整输入时,是否应该自动把段落“升级”成代码块?这个看似微小的交互,恰恰是 Markdown 编辑体验中最容易产生回归的边界。本文以docs/plans/2026-04-17-codeblock-fence-test.md这份回归测试计划为骨架,完整还原 Plate 项目中如何定位测试缝、如何在核心包与 app 集成层分别锁定三反引号自动格式化行为,并结合createBlockFenceInputRule的源码剖析围栏匹配的底层原理。读完你将掌握:block fence 输入规则的两种触发模式、CodeBlockRules.markdown({ on })的正确用法,以及如何在 shipped app kit 层面编写可防回归的集成测试。

背景:一个关于``````` 不回退成代码块的疑问

问题的起点是一次用户提问:当输入三反引号时,编辑器不应把内容错误地转换为代码块。这句话听起来像是在描述 bug,实际上是在要求为“正确的自动格式化行为”补上回归测试——即锁定“三反引号确实会触发代码块提升”这一既有契约,防止未来改动悄悄破坏它。

从 计划文档 可以看到当时的定位结论:

  • 该行为的真正所有者是code-block 插件的输入规则(input-rule)路径,而不是文档 UI 或示例页面;
  • 已有的经验记录指向block-fence 输入规则才是正确的测试缝(seam);
  • 核心包@platejs/code-block其实早已存在直接的输入规则测试,真正缺失的覆盖是shipped app kit(CodeBlockKit)这一集成层

也就是说,这次任务不是“发现并修复 bug”,而是“在回归最可能悄悄溜进来的地方把契约锁死”。

相关经验:两条支撑本次决策的最佳实践

计划文档明确引用了两份解决方案文档,它们共同决定了测试怎么写、规则怎么配。

围栏匹配与功能应用应当分离

block-fence-input-rules-should-split-fence-matching-from-feature-apply.md 指出:块级数学公式($$)与围栏代码块(```````)本质上是同一类块围栏规则——在块首匹配一个围栏、要求光标位于块尾,然后在围栏补全或按下 Enter 时提交。过去这套共享逻辑被埋在各自包内,导致重复的“折叠选区、段落类型、块尾、块首围栏”检查散落各处。

解决方案是抽出核心原语createBlockFenceInputRule,并用一个on选项表达真正的产品语义:

createBlockFenceInputRule({ fence: '```', on: 'match', // 最后一个定界符使围栏完整时触发 block: KEYS.p, // 仅在段落内生效 isBlocked, apply, });
  • on: 'match':当最后一个字符使围栏完整(如输入第三个反引号)时触发;
  • on: 'break':当已完整的围栏后按下 Enter 时触发。

核心负责匹配器(折叠选区、当前块查找、块类型门控、光标位于块尾门控、块首围栏文本匹配),而各包继续保有语义(插入代码块、插入公式等),互不越权。

显式注册规则实例,而非布尔键开关

input-rules-should-register-explicit-rule-instances-while-packages-export-markdown-families.md 则解释了为什么测试与配置中看到的是CodeBlockRules.markdown({ on: 'match' })而不是inputRules: { fence: true }。旧的布尔键配置把“包拥有的规范规则语义”与“kit 层的布尔激活”混为一谈;新设计下:

  • 包导出语义化 markdown 家族:如CodeBlockRules.markdown(...)BoldRules.markdown({ variant: '*' })MathRules.markdown({ variant: '$' })
  • kit 显式注册具体规则实例CodeBlockPlugin.configure({ inputRules: [CodeBlockRules.markdown({ on: 'match' })] })
  • 运行时只存具体规则,不再需要“可用规则注册表”或字符串键激活层。

标点符号变成variant/on之类的选项值,而不是顶层配置键,公开 API 因此大幅简化。

核心机制:createBlockFenceInputRule的两种触发路径

从源码看,核心原语位于 packages/core/src/lib/plugins/input-rules/createInputRules.ts,其内部按on取值分派到两种defineInputRule

on: 'break'分支(createInputRules.ts#L315-L328):

if (config.on === 'break') { return defineInputRule({ priority: config.priority, target: 'insertBreak', // 挂到插入换行事件 enabled: config.enabled, resolve: (context) => matchBlockFence(context, { block: config.block, fence: config.fence, resolveMatch: config.resolveMatch, }), apply: config.apply, }); }

它把target设为'insertBreak',即监听“回车”。此时matchBlockFence直接用完整围栏fence去匹配块首文本。

on: 'match'分支(createInputRules.ts#L330-L351):

const trigger = config.fence.at(-1); // 取围栏最后一个字符:'`' return defineInputRule({ priority: config.priority, target: 'insertText', // 挂到文本插入事件 enabled: config.enabled, trigger, resolve: (context) => { if (context.text !== trigger) return; return matchBlockFence(context, { block: config.block, fence: config.fence.slice(0, -trigger.length), // 匹配剩余部分 '``' resolveMatch: config.resolveMatch, }); }, apply: config.apply, });

这里有两个值得注意的实现细节:

  1. 触发字符是围栏的最后一个字符trigger = config.fence.at(-1),即第三个反引号。当用户依次输入```,前两个字符不满足触发条件,第三个反引号插入时才进入matchBlockFence
  2. 匹配时用“去掉触发字符后的前缀”config.fence.slice(0, -trigger.length)得到'``',因为此刻第三个反引号已经被insertText事件消费,块首文本恰好是 ``,需要拿前缀去比对,否则永远匹配不上。

matchBlockFence(createInputRules.ts#L275-L310)就是那套被抽出来的共享匹配逻辑,逐条验证:

if (!context.isCollapsed || !selection) return; // 1. 折叠选区 const blockEntry = context.getBlockEntry(); if (!blockEntry) return; const [blockNode, path] = blockEntry; const endPoint = editor.api.end(path); if (config.block && blockNode.type !== editor.getType(config.block)) return; // 2. 块类型门控 if (!endPoint || !editor.api.isEnd(selection.focus, path)) return; // 3. 光标在块尾 const range = context.getBlockStartRange(); const blockText = context.getBlockStartText(); if (!range || blockText === undefined || blockText !== config.fence) return; // 4. 块首文本 == 围栏

四个条件缺一不可:光标必须折叠、必须在段落内、必须在块尾、块首全文必须正好等于围栏。例如输入 `` 后再输入x而不是第三个反引号,就永远无法命中;这也解释了为什么“x位于围栏之后”会天然阻止误转换——匹配要求块首文本严格等于围栏本身。

包侧实现:CodeBlockRules.markdown的围栏配置

代码块包通过 packages/code-block/src/lib/CodeBlockRules.ts 把核心原语封装成语义化规则工厂:

export const CodeBlockRules = { markdown: createRuleFactory< { on: 'break' | 'match' }, { block: string; fence: string }, BlockFenceInputRuleMatch >({ type: 'blockFence', fence: '```', block: KEYS.p, enabled: ({ editor }) => !isCodeBlockInputBlocked(editor), priority: 100, apply: ({ editor, on }, match) => { insertCodeBlockAtPath(editor, match.path); return true; }, }), };

要点:

  • fence: '```'block: KEYS.p声明“三反引号 + 段落”这一组合;
  • enabled通过editor.api.some({ match: { type: codeBlock } })判断文档中是否已存在代码块,若存在则禁用规则,避免嵌套场景下的误触发(见 CodeBlockRules.ts#L7-L12);
  • apply调用insertCodeBlockAtPath:先removeNodes删除围栏段落,再insertNodes插入code_block结构(内含一个空的code_line),最后把选区移动到新代码行的开头(CodeBlockRules.ts#L14-L37)。这一步保证了围栏段落被整体替换,而不是留下前两个反引号残骸
  • createRuleFactory在 packages/core/src/lib/plugins/input-rules/createRuleFactory.ts 中把工厂配置转译为createBlockFenceInputRule,将on: 'break' | 'match'透传为两条不同的输入规则。

核心包的直接测试:三用例锁定三种行为

BaseCodeBlockPlugin的输入规则测试位于 packages/code-block/src/lib/BaseCodeBlockPlugin.inputRules.spec.tsx,它使用@platejs/test-utils的 JSX 语法构造文档,再通过createSlateEditor搭建编辑器实例。

用例一:on: 'match'下三反引号提升为代码块

初始文档是一个只含 `` 的段落,光标位于末尾(偏移 2)。配置inputRules: [CodeBlockRules.markdown({ on: 'match' })]后:

editor.tf.insertText('`'); // 输入第三个反引号,触发规则 editor.tf.insertText('code'); // 继续输入正文 expect(input.children).toEqual( <fragment> <hcodeblock> <hcodeline>code</hcodeline> </hcodeblock> </fragment> );

断言的核心是:围栏段落被替换为code_block -> code_line -> "code",用户随后键入的code直接落在代码块的第一行里。

用例二:围栏段落整体替换,不留残骸

第二个用例专门回归“替换围栏段落而非把前两个反引号留在后面”:

// 选区锚定在偏移 2,即 `` `` `` 之后 editor.tf.insertText('`'); expect(editor.children).toMatchObject([ { children: [{ children: [{ text: '' }], type: 'code_line' }], type: 'code_block', }, ]);

这个断言对应insertCodeBlockAtPathremoveNodes + insertNodes实现:如果某次重构改成“原地改写节点”,前两个反引号很可能残留为段落文本,此测试会立即失败。

用例三:on: 'break'下回车触发提升

第三个用例验证另一种 DX:围栏已经完整(段落文本为 ```),光标在块尾,此时触发的是 Enter。配置改为{ on: 'break' },然后:

editor.tf.select({ anchor: { offset: 3, path: [0, 0] }, focus: { offset: 3, path: [0, 0] } }); editor.tf.insertBreak(); // 模拟回车

断言结果同样是文档被替换为空的code_block。它证明同一个CodeBlockRules.markdown只需要切换一个选项,就能从“打完三个反引号立即生效”切换为“回车后生效”,这正是最佳实践文档所说的“一个选项切换两种完成方式”。

新增覆盖:在 shippedCodeBlockKit表面锁定契约

计划文档的关键结论是:核心包已经有直接测试,真正缺的是 app 集成层CodeBlockKit是正确的主人车道,因为它在 apps/www/src/registry/components/editor/plugins/code-block-kit.tsx 中把CodeBlockRules.markdown({ on: 'match' })真正接线进了 registry 编辑器表面:

export const CodeBlockKit = [ CodeBlockPlugin.configure({ inputRules: [CodeBlockRules.markdown({ on: 'match' })], node: { component: CodeBlockElement }, options: { lowlight }, shortcuts: { toggle: { keys: 'mod+alt+8' } }, }), CodeLinePlugin.withComponent(CodeLineElement), CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf), ];

注意CodeBlockKit同时携带了lowlight语法高亮与mod+alt+8切换快捷键,而基础版BaseCodeBlockKit(code-block-base-kit.tsx)不注册任何输入规则——这进一步说明输入规则是 kit 层显式选择的,而不是插件的隐含行为

于是本次计划新增的回归测试落在 apps/www/src/tests/package-integration/code-block/current-kit.slow.tsx,文件后缀.slow表明它属于慢速集成测试通道:

const createEditor = (text: string, offset = text.length) => createSlateEditor({ plugins: [BaseParagraphPlugin, ...CodeBlockKit], selection: { anchor: { offset, path: [0, 0] }, focus: { offset, path: [0, 0] } }, value: [{ children: [{ text }], type: 'p' }], } as any); describe('CodeBlockKit current contract', () => { it('promotes triple backticks into a code block in the shipped kit surface', () => { const editor = createEditor('``', 2); editor.tf.insertText('`'); // 补全围栏 editor.tf.insertText('code'); // 键入正文 expect(editor.children).toMatchObject([ { children: [{ children: [{ text: 'code' }], type: 'code_line' }], type: 'code_block', }, ]); expect(editor.selection).toEqual({ anchor: { offset: 4, path: [0, 0, 0] }, focus: { offset: 4, path: [0, 0, 0] }, }); }); });

与核心包测试相比,这个用例有两个额外价值:

  1. 测试的是真实出货配置:插件不是手工拼装的BaseCodeBlockPlugin.configure(...),而是用户实际会安装的CodeBlockKit整体,任何 kit 层配置漂移(例如误删inputRules、改错on值、破坏lowlight初始化)都会暴露;
  2. 额外断言了选区位置selection必须落在path [0, 0, 0](即新code_line内)偏移 4 处,锁定了insertCodeBlockAtPath中“提升后光标进入代码行开头”的交互细节。

运行与验证

计划文档的验证步骤是“运行定向测试”。该项目使用 Bun 作为包管理器与测试运行器(见根目录 bun.lock 与 bunfig.toml),因此可在仓库根目录执行:

bun test packages/code-block/src/lib/BaseCodeBlockPlugin.inputRules.spec.tsx bun test apps/www/src/__tests__/package-integration/code-block/current-kit.slow.tsx

第一行验证核心包三个直接用例,第二行验证新增的 shipped kit 回归测试。计划文档的结论是:新回归测试直接通过,本轮无需携带任何修复——因为这次请求的本质是“把契约锁在回归最可能发生的地方”,而不是修一个已经存在的错误。换言之,CodeBlockRules.markdown({ on: 'match' })CodeBlockKit中的接线被证明与预期行为一致。

小结:本次测试计划沉淀的三条可复用经验

  1. 测试缝要选在行为真正的所有者处:三反引号自动格式化属于 code-block 输入规则路径,而非文档 UI;在CodeBlockKit接线层补测试,比再写一个核心包用例更能防止实际出货配置的回归。
  2. 覆盖要同时包含“行为”与“副作用”:核心包三个用例分别锁定了on: 'match'提升、围栏段落整体替换(不留残骸)、on: 'break'回车提升;集成层用例还额外断言了提升后的选区位置,形成纵深防御。
  3. 规则配置的语义化是关键on: 'match' | 'break'这种选项让同一个CodeBlockRules.markdown通过一个参数切换触发时机,而createBlockFenceInputRule在核心处统一处理折叠选区、块类型、块尾与块首文本四个门控——测试与实现共享同一套心智模型,回归自然更早暴露。

如果你需要在应用中接入代码块自动格式化,请记住:在插件配置中显式注册CodeBlockRules.markdown({ on: 'match' })(或{ on: 'break' })而非依赖布尔键开关;若希望直接使用已出货的 kit,则只需把CodeBlockKit加入插件列表,行为即与本文所述的回归测试完全一致。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 4:10:02

C语言核心概念实战解析:从指针到内存管理

这些年不管是带新人还是看论坛里的提问&#xff0c;我发现一个特别普遍的现象&#xff1a;C语言这门课人人都学过&#xff0c;语法书也翻过好几遍&#xff0c;但真正问到“指针到底是什么”“字符串为什么总出问题”“结构体什么时候该用指针”这类问题的时候&#xff0c;很多人…

作者头像 李华
网站建设 2026/9/17 4:09:32

CentOS 7挂载U盘指南:FAT32与exFAT格式从原理到实践

插上U盘却挂载不了&#xff0c;这大概是Linux新手和老手都会遇到的场景。我在CentOS 7上折腾移动硬盘和U盘挂载时&#xff0c;踩过不少坑&#xff0c;尤其是exFAT格式的盘&#xff0c;系统默认根本不认&#xff0c;更别提直接mount了。这篇就把FAT32和exFAT两种格式的挂载方法、…

作者头像 李华
网站建设 2026/9/17 4:08:25

Windows笔记本电池循环计数与健康度:powercfg报告与换电池判断

1. 先搞清循环计数到底在数什么我见过太多人拿着笔记本电池报告截图来问&#xff1a;健康度才 78%&#xff0c;是不是该换电池了&#xff1f;结果一看循环计数&#xff0c;才 120 次。这两组数字如果在你的认知里是同一件事&#xff0c;后面的判断基本都会偏。Windows 笔记本电…

作者头像 李华
网站建设 2026/9/17 4:06:16

YOLO v11 针对 SAR 图像飞机检测的物理建模优化

简介&#xff1a;本资源是一套基于YOLO v11实现SAR图像飞机目标检测的完整开源项目&#xff0c;面向计算机视觉初学者、遥感图像处理研究者及AI工程实践者&#xff0c;解决合成孔径雷达图像中低对比度、弱纹理目标识别难的问题。压缩包共26个文件&#xff08;731KB&#xff09;…

作者头像 李华