三种调用方式
以下调用假设实例安装了 StarterKit。命令的 TypeScript 类型可见,不代表运行时功能已经安装。
const applied = editor.commands.toggleBold()
const enabled = editor.can().toggleBold()
const committed = editor.chain().insertText('A').insertText('B').run()
if (!committed) console.warn(editor.lastCommandResult)
commands 返回布尔结果;can() 只试算,不发布事务,也不更新 lastCommandResult。命令链在一个草稿上顺序执行,最后 run() 一次提交;任何一步失败都会放弃整条链。它不能包含异步步骤。
写一个会参与命令链的业务命令
import { Extension } from '@quirekit/msfe-core'
declare module '@quirekit/msfe-core' {
interface Commands<ReturnType> {
/** Commands installed by the signature feature. */
signature: {
/** @param name - Literal author name. @returns Command outcome. */
insertSignature(name: string): ReturnType
}
}
}
export const Signature = Extension.create({
name: 'signature',
addCommands() {
return {
insertSignature: (name: string) => ({ tr }) => {
if (!name.trim() || /[\r\n]/.test(name)) return false
return tr.insertText(`— ${name.trim()}`)
},
}
},
})
安装 Signature 后即可调用 editor.can().insertSignature('Ada')。若业务需要 Markdown 字面值,应使用正确的字段编码或限制允许字符,不要误把用户文本当作可信语法模板。
草稿状态和嵌套命令
命令工厂接收业务参数,返回同步纯函数。读取 props.state 或 tr.state,它们包含链中前面步骤的结果。读 props.editor.state 会读到尚未提交的旧状态。
使用 props.commands、props.chain()、tr.replace() 等继续同一草稿。命令内部不能调用 editor.commands 提交另一笔修改,不能手动调用 dispatch,不能发请求、打开弹窗或修改 storage。试算同样会调用你的代码。
tr.replace(from, to, insert) 的坐标属于当前草稿;前一步改变长度后,后续步骤需要重新计算或映射坐标。tr.apply() 是高级 CodeMirror 入口,同样必须遵守该坐标规则。
已有规划函数
import { sourceCommand } from '@quirekit/msfe-core'
editor.chain().command(sourceCommand(state => ({
changes: { from: state.selection.main.from, to: state.selection.main.to, insert: 'approved' },
}))).run()
直接运行纯规划函数可用 editor.run(planner, target?),它返回 MsfeResult。返回 null 表示不适用;不要直接改写 DOM 来代替事务。
失败需要处理
| 原因 | 含义 | 宿主动作 |
|---|---|---|
not-installed | 类型存在但功能没安装 | 检查预设是否禁用或漏装 |
not-applicable | 当前语法或选区不接受此动作 | 禁用按钮或保留原内容 |
readonly | 显式只读或 locked | 关闭写操作 |
stale-target | 目标的实例、文档或版本已变 | 重新确认意图,不能直接重放旧偏移 |
invalid-change | 修改范围或事务无效 | 检查坐标与字段编码 |
composing | 组合输入正在拥有编辑过程 | 等待本次输入结束,不抢焦点 |
destroyed / not-mounted | 生命周期无效 | 结束动作并释放资源 |
快捷键、撤销与测试
结构性回车使用扩展的 addBlockInput(),由块声明 enter(Enter)、lineBreak(Shift-Enter)和 exit(Mod-Enter)。普通业务快捷键继续使用 addKeyboardShortcuts()。
处理器根据当前源码、选区和块边界决定行为,不累计按键次数。返回一个 TransactionSpec 表示整笔处理,'parent' 表示委托外层块,'native' 表示回到既有快捷键链;后两者也能直接作为规则值。未声明的意图委托父级,未注册的语法 Block 阻止外层默认规则接管。块内普通段落应显式委托,让列表、引用或自定义容器获得处理机会。
公共层先检查模式、只读、组合输入和已解析范围,再沿每个选区端点的祖先查找。所有端点必须解析到同一个处理函数才调用一次,否则整笔回退。处理器接收的 targets 是端点及其所有者,同一非空选区有两个端点;实际修改仍使用 state.selection。处理器必须纯粹,不得调用 dispatch、editor.commands 或修改 storage,返回的编辑由公共层一次提交和撤销。
每个 parser 节点只允许一个声明所有者,通过原扩展的 .extend() 替换规则,不要安装两个扩展争用节点。下面的最小示例让顶层二级标题末尾回车进入新段落;在 StarterKit.configure({ heading: SectionHeading }) 中安装它。产品 MD Editor 已为段落、标题、列表、引用、容器、代码和表格配置完整默认规则。
import { Heading } from '@quirekit/msfe-heading'
export const SectionHeading = Heading.extend({
/** @returns A pure, source-owned heading boundary behavior. */
addBlockInput() {
return [{
nodes: ['ATXHeading2'],
enter: ({ state, targets }) => {
const range = state.selection.main
if (state.selection.ranges.length !== 1 || !range.empty
|| !targets[0]!.node.parent?.type.isTop
|| range.head !== targets[0]!.node.to) return 'native'
return {
changes: { from: range.head, insert: '\n\n' },
selection: { anchor: range.head + 2 },
scrollIntoView: true,
userEvent: 'input',
}
},
lineBreak: 'native',
}]
},
})
在扩展的 addKeyboardShortcuts() 返回 { 'Mod-Shift-s': () => this.editor.commands.insertSignature('Ada') }。false 允许其他绑定继续处理;避免覆盖浏览器或输入法行为而不给出替代入口。
用户可感知的一次动作尽量通过一条命令链提交;连续原生输入仍遵循编辑历史的分组规则。测试至少覆盖:单次和链中调用、链中失败不留半截修改、can() 不改变状态、只读拒绝、撤销恢复原始拼写,以及多选区的处理策略。