Plate Autoformat 运行时对齐与扩展:从 insertText 规则引擎到 feature 插件 inputRules 的架构演进
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
Autoformat(自动格式化)一直是富文本编辑器中最“好用但也最容易被误解”的能力:用户敲下#、**、->时希望立即得到结构化结果,而编辑器工程团队则需要在“哪一行代码拥有这条规则”上达成共识。本文以 docs/plans/2026-04-10-autoformat-runtime-alignment-and-extension-plan.md 为核心骨架,讲解 Plate 项目如何把混杂在应用层(app kit)里的 autoformat 规则重新对齐到共享包与 feature 插件的边界上:谁拥有 block shorthand、谁拥有 mark closure、谁拥有文本替换、谁负责 Enter 耦合的“后续键”流程,以及最终落地为inputRules声明式运行时。读完本文,你将掌握这套所有权划分模型、五个工作流与五个积压切片的内容,并能对照 packages/autoformat/CHANGELOG.md 的迁移映射完成旧规则到新运行时的手工迁移。
一、问题框架:运行时里混着的三种形态
计划文档开门见山地指出:editor-behavior 规范(下文简称“law”)已经变得诚实,但运行时仍然把三种不同形状的东西混在同一个引擎里:
- 干净的单词触发 shorthand:例如
#转标题、**闭合加粗,这类行为@platejs/autoformat包原本就建模得很好; - 当前 app-kit 的怪癖:例如依赖规则数组顺序、依赖自定义
format回调才能工作的行为; - Typora 式 follow-up-key 流程:例如输入
$$之后再按 Enter 才触发块级数学,这类行为本质上根本不是“单词触发的 autoformat”。
如果不先把边界画清楚就把所有东西硬塞进@platejs/autoformat包,结果只会“重新发明一个更大的应用局部规则堆”(原文:re-invent a bigger pile of app-local rules)。这正是这份计划存在的理由:在动手改代码之前,先完成所有权与运行时的对齐。
二、需求摘要与明确非目标
计划对本次工作定义了 7 条硬性要求:
- 运行时与最新编写的 autoformat law 对齐;
- block、mark、text-substitution 三个家族在代码与测试中保持彼此独立;
- 在共享包或 helper 能干净持有行为的地方,减少 app-kit 中的“民俗代码”(folklore);
- 不假装所有 Typora 式触发都属于当前仅支持
insertText的 autoformat 引擎; - 保留现有的 invalid-match、转义(escaping)与代码块 gating 行为;
- 在触发重叠处显式声明优先级(precedence);
- 增加足够测试,使未来新增规则不会在无意中重排或破坏既有行为。
同时,计划明确列出 5 条非目标,防止范围膨胀:
- 不从头重开整个 editor-behavior law;
- 不把每一个可选输入辅助面强行塞进 core-major 发布门禁;
- 不在本计划中解决搜索/查找替换、工具栏命令或斜杠菜单插入;
- 不指望一次性在所有地方做到 Typora 级完美;
- 不把 Enter 耦合的提升(promotion)当作又一个文本触发规则。
三、关键决策:不是所有“autoformat”都属于@platejs/autoformat
整份计划的核心结论只有一句话:所有权必须显式划分。划分结果如下。
A.@platejs/autoformat拥有的
- 单词触发(single-keystroke)的
insertText驱动的 block shorthand; - 单词触发的 markdown 定界符 mark 闭合;
- 单词触发的文本替换;
- 可逆文本替换(原
enableUndoOnDelete,见下文落地后的内置化说明); - 显式的规则优先级与上下文 gating。
B. 邻近共享 input-rule / key 行为拥有的
$$后按 Enter 的块级数学提升;- Typora 式代码围栏或主题分隔线随 Enter 触发(如果决定改为这种形态而不是保留当前的即时转换);
- 任何依赖“超过当前插入字符与局部文本匹配”的流程。
这类行为可以放在@platejs/autoformat下的共享 input-rule 基础设施里、@platejs/core的 key 行为里,或归属的功能包(如 code-block、math 包)里,但绝不能放进普通AutoformatPlugin的派发循环。
四、计划落地后的仓库现状(Status: Executed)
计划文档开头标注的状态是Executed(已执行),并给出了关闭后的所有权归属清单:
- 共享包拥有:heading shorthand、inline mark autoformat、text substitution;
- 显式应用层拥有(current-kit 行为):blockquote wrap、list 与 condensed todo shorthand、code-block gating、immediate code-fence promotion、immediate HR insertion;
- 不在普通 autoformat 家族内:link automd;
- 后续邻近 input-rule 跟进:Enter 拥有的 code-fence 或 HR 提升。
在当前的 packages/autoformat/src/plugin.ts 中可以看到这一落地的直接证据:AutoformatPlugin现在是一个惰性兼容插件,源码注释明确写道“故意不活跃(intentionally inert)”,并指引开发者把规则注册到拥有该功能的 feature 插件上:
@platejs/basic-nodes:HeadingRules、BlockquoteRules、mark 规则;@platejs/code-block:CodeBlockRules;@platejs/list/@platejs/list-classic:列表规则工厂;- 局部文本替换:
createTextSubstitutionInputRule。
4.1 从旧AutoformatRule到新inputRules的迁移映射
packages/autoformat/CHANGELOG.md 的 53.0.0 版本条目完整记录了旧规则到新规则工厂的一一映射,这是把计划落成代码的“对照表”,摘录核心几组:
基础块(@platejs/basic-nodes)
| 旧规则 | 新规则 |
|---|---|
{ match: '# '..'###### ', mode: 'block', type: KEYS.h1..h6 } | HxPlugin.configure({ inputRules: [HeadingRules.markdown()] }) |
{ match: '> ', mode: 'block', type: KEYS.blockquote } | BlockquotePlugin.configure({ inputRules: [BlockquoteRules.markdown()] }) |
{ match: ['---', '—-', '___ '], mode: 'block', type: KEYS.hr } | HorizontalRulePlugin.configure({ inputRules: [HorizontalRuleRules.markdown({ variant: '-' }), ...] }) |
基础 mark(@platejs/basic-nodes)
| 旧规则 | 新规则 | 拥有插件 |
|---|---|---|
{ match: '**', mode: 'mark', type: KEYS.bold } | BoldRules.markdown({ variant: '*' }) | BoldPlugin |
{ match: '__', mode: 'mark', type: KEYS.underline } | UnderlineRules.markdown() | UnderlinePlugin |
{ match: '`', mode: 'mark', type: KEYS.code } | CodeRules.markdown() | CodePlugin |
{ match: '~~', mode: 'mark', type: KEYS.strikethrough } | StrikethroughRules.markdown() | StrikethroughPlugin |
{ match: '==', mode: 'mark', type: KEYS.highlight } | HighlightRules.markdown({ variant: '==' }) | HighlightPlugin |
{ match: '***', mode: 'mark', type: [bold, italic] } | MarkComboRules.markdown({ variant: 'boldItalic' }) | BoldPlugin |
代码块(@platejs/code-block):{ match: '```', mode: 'block', type: KEYS.codeBlock, ... }→CodeBlockPlugin.configure({ inputRules: [CodeBlockRules.markdown({ on: 'match' })] })。
列表(@platejs/list/@platejs/list-classic)
| 旧规则 | 新规则 |
|---|---|
{ match: ['- ', '* '], mode: 'block', format: toggleList(...) } | BulletedListRules.markdown({ variant: '-' })、BulletedListRules.markdown({ variant: '*' }) |
{ match: /^\d+\.$|^\d+\)$/, matchByRegex: true, ... } | OrderedListRules.markdown({ variant: '.' })、OrderedListRules.markdown({ variant: ')' }) |
{ match: '[] ', mode: 'block', ... } | TaskListRules.markdown({ checked: false }) |
{ match: '[x] ', mode: 'block', ... } | TaskListRules.markdown({ checked: true }) |
数学(@platejs/math):行内$…$→MathRules.markdown({ variant: '$' });块级$$…$$→MathRules.markdown({ on: 'break', variant: '$$' })——注意这里on: 'break'正是计划中“Enter 耦合提升不属于普通 autoformat 家族”论断的代码体现。
链接(@platejs/link):textmarkdown →LinkRules.markdown();粘贴/空格/回车自动链接 →LinkRules.autolink({ variant: 'paste' | 'space' | 'break' })。这与计划 Slice 4“把 link automd 留在链接/源录入车道”一致。
自定义规则:旧AutoformatRule对象没有直接替代品,改用createRuleFactory自建规则家族:
import { createRuleFactory } from "platejs"; const MyRules = { markdown: createRuleFactory({ type: "blockMatch", match: "!! ", format: "my-block", }), }; MyPlugin.configure({ inputRules: [MyRules.markdown()] });4.2 文本替换的新写法
旧的符号替换表(箭头、分数、法律符号、数学运算符、智能引号等)迁移为本地createSlatePlugin上的createTextSubstitutionInputRule:
import { createSlatePlugin, createTextSubstitutionInputRule, KEYS, } from "platejs"; const isTextSubstitutionBlocked = (editor) => editor.api.some({ match: { type: [editor.getType(KEYS.codeBlock)] } }); const ShortcutsPlugin = createSlatePlugin({ key: "shortcuts", inputRules: [ createTextSubstitutionInputRule({ enabled: ({ editor }) => !isTextSubstitutionBlocked(editor), patterns: [ { format: "→", match: "->" }, { format: "⇒", match: "=>" }, { format: "½", match: "1/2" }, { format: "™", match: ["(tm)", "(TM)"] }, { format: ["“", "”"], match: '"' }, ], }), ], });注意enabled回调中通过editor.api.some({ match: { type: [codeBlock] } })实现的代码块 gating——这正是计划反复强调“保留 code-block gating 行为”的落地形态;旧选项enableUndoOnDelete被移除,因为在规则插入的节点上按 Backspace 恢复源文本已成为内置默认行为。
在应用注册表侧,apps/www/src/registry/components/editor/plugins/autoformat-kit.tsx 中的AutoformatKit把 13 组文本替换族打包成一个插件autoformatShortcuts:箭头(arrows)、比较符(comparisons)、等号(equality)、分数(fractions)、法律符号(legal / legalHtml)、运算符(operators)、标点(punctuation)、智能引号(smartQuotes)、下标数字/符号(subscriptNumbers / subscriptSymbols)、上标数字/符号(superscriptNumbers / superscriptSymbols),全部通过同一个createAutoformatTextSubstitutionRulehelper 统一注入isTextSubstitutionBlockedgating。
五、五个推荐工作流
计划把执行拆成五个并行/串行的工作流(Workstream),每个都有明确目标、实现单元与主文件。
Workstream 1:运行时真值表(Runtime Truth Table)
目标:停止抽象争论“什么是 autoformat、什么是当前 kit 行为”,用测试把当前实际行为钉死。覆盖的行包括:标题、含嵌套引号进入的 blockquote、无序/有序列表 shorthand、condensed todo shorthand、代码围栏触发、HR 触发、mark 闭合、invalid mark 用例、智能引号/标点/符号、代码块 gating。同时要求测试命名与 markdown-editing-spec.md 中的 spec ID 对齐。
计划列出的主文件(当前仓库中@platejs/autoformat已重构,原lib/下实现已迁移到@platejs/core的 input-rules 运行时,测试文件相应落在各自 feature 包内,如packages/basic-nodes/src/lib/BaseMarkInputRules.spec.tsx、packages/basic-nodes/src/lib/BaseHeadingInputRules.spec.tsx、packages/basic-nodes/src/lib/BaseBlockquoteInputRules.spec.tsx、packages/code-block/src/lib/BaseCodeBlockPlugin.inputRules.spec.tsx、packages/list/src/lib/inputRules.spec.tsx、packages/math/src/lib/inputRules.spec.tsx,以及计划指定的应用集成测试apps/www/src/__tests__/package-integration/autoformat/blockquote.slow.tsx、apps/www/src/__tests__/package-integration/autoformat/list.slow.tsx)。
为什么放第一位:先锁行为再动所有权,让后续规范化是“显式的”而不是“意外的”。
Workstream 2:共享包边界(Shared Package Boundaries)
目标:把明显共享的行为从应用民俗中搬进共享包,但只搬“共享包 API 能干净持有”的部分。
- 提升为共享规则族:headings、list shorthand、常见 mark 闭合、智能引号/标点/法律/箭头/数学符号表;
- 保持显式应用级:blockquote wrap 语义、condensed todo shorthand、immediate code-fence promotion、immediate HR insertion。
主文件:packages/autoformat/src/lib/rules/**、apps/www/src/registry/components/editor/plugins/autoformat-kit.tsx。交付物是“包拥有的规则表”与“应用拥有的产品决策”之间的干净拆分。
Workstream 3:Autoformat 引擎扩展
目标:只在“当前引擎形状确实是阻塞点”的地方扩展@platejs/autoformat。列出的候选扩展点:
- 显式规则家族元数据(block shorthand / mark autoformat / text substitution / 可选产品专属家族);
- 显式优先级——停止依赖数组静默顺序解决
==这类冲突; - 给
query/format回调传递更丰富的上下文——当前块 owner、最近容器、是否命中块起始匹配、可选的 feature flags/profile 选项; - block shorthand 的共享 helper 路径——retag block、wrap container、build list item / restart number、insert new owned block;
- 包表面更好的当前契约文档——包保证什么、刻意不建模什么。
同时强调约束:不要一次性膨胀成完整的 profile 引擎重写,只落地让所有权与优先级显式化的最小扩展集。从当前仓库形态看,这一工作流的最终走向是“把引擎能力下沉到@platejs/core的 input-rules 运行时、并在各 feature 包以*Rules工厂形态暴露”,而不是在@platejs/autoformat里继续堆配置。
Workstream 4:邻近输入规则车道(Neighboring Input-Rule Lane)
目标:处理那些“本质上不是单词触发 autoformat”的用例,候选面包括:$$+ Enter 的块级数学提升、```+ Enter 的代码围栏提升(若未来选择 Typora 风格)、Enter 触发的主题分隔线(若未来选择该形态)。
推荐做法:除非出现极小的共享接缝,否则不要放进@platejs/autoformat的第一轮扩展;而是作为所属车道(@platejs/autoformat的共享 input-rule 运行时、core key 行为、code-block 包、math 包)的邻近 key/input 规则来设计与实现,复用同一套 spec ID 与家族命名,但不伪装包所有权。原因很简单:当前AutoformatPlugin围绕“一个插入字符 + 局部文本匹配”构建,而 Enter 耦合的提升是另一类交互。
Workstream 5:公开文档与面向用户的契约
目标:让公开的/docs/autoformat故事与新 law/runtime 同步。需要更新的内容包括:三个家族、可选的 profile-adjacent 性质、当前 app-kit 行为中哪些是包级、哪些是应用级;在“Plate 自有 shorthand(非通用 markdown 惯例)”处添加重叠警告;包 API 变化时同步包文档与示例。
落地后的公开文档位于 content/docs/(plugins)/(functionality)/autoformat.mdx/(functionality)/autoformat.mdx),页面标题即“Markdown shortcuts and text substitutions powered by input rules”,其特性清单明确写着“由理解它们的 feature 插件拥有 markdown 快捷方式”“无隐藏默认快捷行为”,与计划的“包级/应用级边界诚实化”目标一一对应。
六、即时积压切片(Immediate Backlog Slices)
计划强调这些是“按顺序执行的切片”,不是松散的后续事项:
- Slice 1:运行时真值表锁定——为所有已规范的 block/mark/text 行写 truth-table 测试、为
==这类重叠触发写优先级测试、提供 code-block gating 证明;理由:在测试锁死当前行为之前,不做任何规范化或包抽取。 - Slice 2:共享 Autoformat 边界清理——把明显共享的规则表/helper 移入稳定的包级导出,目标行是 block shorthand 标题/列表、常见 mark 闭合、智能引号/标点/符号表。
- Slice 3:文本替换权威性收尾——智能引号、em dash、省略号、角引号保持在“主流输入规范 + 当前测试”的强地基上;箭头、法律符号、分数、运算符替换、unicode 上下标 shorthand 则显式标注为“较薄的本地契约”(除非出现更强外部权威);目标行:
EDIT-PROFILE-AUTOFMT-TEXT-001至004。 - Slice 4:Link Automd 车道——把
text留在普通 autoformat 家族之外;要么作为 link/source-entry 面的产品化功能上线,要么保持延期,但包接缝与文档边界都要诚实,不冒充当前 autoformat 支持;目标行:EDIT-INTERACT-LINK-AUTOMD-001。 - Slice 5:当前 kit 规范化——
[]/[x]保持为 Plate 自有的 condensed todo 便利(除非产品主动收窄);immediate code-fence promotion 与 immediate HR insertion 作为当前 kit 偏差,向 Enter 拥有的邻近 input-rule 车道规范化;目标行:EDIT-PROFILE-AUTOFMT-BLOCK-004至006。
七、验收标准与测试计划
验收标准
- 每一条已规范的 autoformat 行都能用“共享包当前契约 / 显式应用产品选择 / 邻近 input-rule 车道”之一描述;
- 重叠触发优先级在代码与测试中显式;
- invalid-match 护栏保持覆盖;
- code-block gating 保持覆盖;
- 公开 autoformat 文档不再暗示所有 shorthand 行为是一个通用包特性;
- 如果保留 condensed todo shorthand、immediate code-fence promotion 等当前偏差,计划/结果必须显式说明,而不是把它们当成 markdown 标准。
测试计划
- 包测试:
packages/autoformat/src/lib/AutoformatPlugin.spec.tsx、packages/autoformat/src/lib/transforms/*.spec.ts、packages/autoformat/src/lib/__tests__/withAutoformat/**/*.spec.tsx(对应到当前仓库,即各 feature 包的*InputRules.spec.tsx与@platejs/core的createTextSubstitutionInputRule.spec.ts、createRuleFactory.spec.ts等)。新增或扩展:优先级冲突测试、家族元数据/优先级测试(若引入)、更丰富的 query/context 测试(若引入)、代表性文本替换的 undo-on-delete 测试。 - 应用集成测试:
apps/www/src/__tests__/package-integration/autoformat/blockquote.slow.tsx、list.slow.tsx,并新增三条:当前 kit 代码围栏行为、当前 kit HR 行为、当前 kit 代码块 gating。 - 文档验证:代码形态定型后重读 standards/spec/protocol/parity/audit 文档,并按真实运行时重读
/docs/autoformat与示例。
八、风险与缓解
| 风险 | 缓解 |
|---|---|
| 过度设计包:架构文档已想要 profile-aware 重写,但现在全做会爆炸范围 | 把 profile-aware 重写当作方向而非即时范围;只落地当前家族所有权与优先级所需的最小扩展集 |
| “规范化”时破坏现有 app-kit 行为:app kit 已发布真实行为,即使有怪癖 | 先锁 truth-table 测试,再逐行显式决策地规范化 |
| 把 autoformat 与 Enter 拥有的提升混为一谈:最容易犯的概念性错误 | insertText驱动与Enter驱动分工作流;要求每一行显式声明所有权 |
| 符号替换的权威性薄弱:部分符号表目前主要是本地契约 | 显式标注为较薄权威,不当作通用 markdown/编辑器真理来过度推销 |
九、推荐执行排序
- 锁定运行时 truth-table 测试;
- 把明显共享的规则表/helper 从应用民俗中搬出;
- 若测试证明确实需要,再在
@platejs/autoformat增加显式优先级与更丰富的 query/context 支持; - Enter 耦合提升作为后续车道(除非自然出现微小共享接缝);
- 最后同步公开文档——等运行时边界真实落定后再写。
计划还提供了两条可直接执行的命令(面向仓库内部工作流):
$ralplan --consensus --direct docs/plans/2026-04-10-autoformat-runtime-alignment-and-extension-plan.md $ralph "Execute docs/plans/2026-04-10-autoformat-runtime-alignment-and-extension-plan.md"十、从源码看新运行时:input-rules 函数族
计划执行后,运行时核心落在 packages/core/src/lib/plugins/input-rules/createInputRules.ts,它按“目标事件 + 匹配 + 应用”三段式建模,与计划的三家族划分一一对应:
createBlockStartInputRule(block shorthand):通过matchBlockStart取块起始区间与文本,支持字符串精确匹配与正则匹配,apply阶段按wrap/toggle/setNodes三种模式处理——其中wrap模式对应计划强调的“blockquote 必须包裹嵌套引用”这类容器规则,区别于普通的 flat block retag。createMarkInputRule(mark closure):通过getMarkMatch在选区前寻找起始定界符与可选的结束定界符,skipInvalid: true跳过无效匹配,并通过isPreviousCharacterEmpty要求定界符前为空字符;apply阶段删除定界符、addMark施加格式并removeMarks收尾——这就是计划中“invalid-mark guardrails 保持覆盖”的代码级实现。createBlockFenceInputRule(代码围栏):on: 'match'走insertText目标、以围栏最后一个字符为 trigger;on: 'break'走insertBreak目标——后者的存在直接印证了计划“Enter 耦合提升属于邻近车道”的结论,代码块与数学包的on: 'break'配置正是该车道的落地。createTextSubstitutionInputRule(文本替换):compilePatternsByTrigger预先按 trigger 字符建索引,resolveTextSubstitution依次尝试同 trigger 的候选 pattern(这正是“重叠触发优先级显式化”的实现位置),applyTextSubstitution支持format为字符串或[起始, 结束]配对(智能引号["“", "”"]即此形态);matchDelimitedInline则提供定界符内联匹配的独立路径。
从packages/autoformat/src/plugin.ts的注释、packages/autoformat/CHANGELOG.md的 53.0.0 迁移说明、以及 apps/www/src/registry/components/editor/plugins/autoformat-kit.tsx 的AutoformatKit实现可以看出:这份计划不是停留在纸面的蓝图,而是已经闭环执行的架构决策。旧的AutoformatPlugin规则引擎被 feature 插件自带的inputRules声明式运行时取代,@platejs/autoformat退化为惰性兼容导出,而文本替换、代码块 gating、undo-on-delete 等能力以更小、更内聚的工厂函数沉淀在@platejs/core与各 feature 包中——这正是“运行时对齐 + 扩展”计划的最终交付物。
延伸阅读
- markdown-editing-spec.md:把 autoformat 视为三个 profile-adjacent 家族的规范依据
- editor-protocol-matrix.md:block shorthand、inline mark、text substitution、undo-on-delete、code-block gating 的协议行
- markdown-parity-matrix.md:这些表面作为可选 current-kit 行为、不进入 core-major 门禁的奇偶校验
- markdown-shorthand-and-inline-autoformat.md 与 input-autoformat-lanes.md:Typora 与 Milkdown 的权威性研究
- autoformat-families-are-profile-adjacent-input-assist-surfaces.md 与 text-substitution-autoformat-authority.md:所有权拆分与文本替换权威性
- blockquote-autoformat-must-wrap-nested-quotes.md:wrapper/container 规则不能当作 flat block retag 的最强应用侧教训
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考