ProseMirror Input Rules 模块
prosemirror-inputrules 模块用于将一些输入规则 input rules 与编辑器绑定,当用户输入特定的内容时编辑器会作出响应,例如对文字内容进行转换
提示
该模块可以用于实现富文本编辑器对 Markdown 语法的兼容性支持,例如在段首输入 ## 并按下空格键就将该段落节点转变为标题节点
InputRule 类
InputRule 类用正则表达式 regular expression 来描述一段文本,当用户输入的内容与之相匹配,就会触发相应的处理函数执行
通过方法 new Input(match, handler, options?) 进行实例化,得到一个 inputrule 对象,以下称作「输入规则」
相关参数的具体说明如下:
- 第一个参数
match是一个 RegExp 正则表达式对象,用于匹配文本提示
由于输入规则 input rule 的目的是响应用户输入,所以正则表达式会应用于正处于光标前面的文本内容,相应地正则表达式一般以锚点
$结束,以约束匹配的内容是位于文本段落的最后(即由用户输入的) - 第二个参数
handler可以是一个字符串,用于替换匹配的内容(如果正则表达式设置有捕获组 matched group,则替换第一个捕获组的内容);也可以是一个函数fn对匹配的内容进行自由度更高的处理ts// 该函数接受四个参数 fn( state: EditorState, // 编辑器状态对象 match: RegExpMatchArray, // 正则表达式方法 RegExp.exec() 的结果数组 // 匹配的开始位置(一般是光标所在的文本节点的开头,遵循 index schema 规则) start: number, // 匹配的结束位置(光标所在的位置相对于文本节点的开头的偏移量,遵循 index schema 规则) end: number ): Transaction | null // 该处理函数返回一个事务对象 transaction 以表示对编辑器进行转换操作,或返回 null 表示不进行操作 - 第三个(可选)参数
options是一个对象{undoable?: boolean, inCode?: boolean | "only"}用于定制输入规则的一些行为。- (可选)属性
undoable是一个布尔值,表示该输入规则触发的转换是否可以(通过分发 command 指令undoInputRule)撤销 - (可选)属性
inCode是一个布尔值或字符串"only",表示该输入规则是否应用于带有code标识的节点中(默认值为false,即输入规则不会应用于代码类型的节点中)。可以设置为true以扩大该输入规则的适用范围到代码类型的节点中;也可以设置为"only"表示该输入规则只针对代码类型的节点
- (可选)属性
输入规则 inputrule 对象实例具有属性 inCode 以表示该输入规则的适用性(是否可应用于带有 code 标识的节点中,或只针对代码类型的节点)
创建插件
使用该模块所提供的方法 inputRules({rules}) 根据给定的配置参数(一个对象,具有属性 rules 其值为一个数组 InputRule[] 即每个元素都是一个输入规则实例)创建一个插件,将插件注册到编辑器上就可以应用特定的一系列输入规则
该方法最后返回的插件带有自定义的 state {transform: Transaction, from: number, to: number, text: string} | null 其中 transform 是输入规则所触发的转换操作,from 和 to 记录规则所匹配的文本范围,text 表示进行匹配的原始内容
提示
该插件只是监听了输入单个字符(「合成输入」则可能是一次性输入多个字符)相关的操作,对于回车键的操作并不会响应
如果需要输入规则能够匹配 /n 换行符,则需要在插件的配置对象中设置属性 props,在其中监听所按下的 enter 键,在回调函数中执行正则表达式对输入内容的匹配
可以参考官方的插件源码创建一个自己的插件以添加所需功能,也可以参考这个帖子对官方插件实例对象进行扩展
例如以下代码是基于官方的插件进行扩展,为插件添加自定义 pluginKey
import { Plugin } from 'prosemirror-state';
import type { EditorState, PluginKey, TextSelection, Transaction } from 'prosemirror-state';
import type { EditorView } from 'prosemirror-view';
// refer to https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/inputrules.ts
type PluginState = { transform: Transaction; from: number; to: number; text: string } | null;
declare class InputRule {
match: RegExp;
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction | null;
undoable: boolean;
inCode: boolean | 'only';
}
export function inputRulePlugin({ rules, key }: { rules: readonly InputRule[]; key: PluginKey }) {
const plugin: Plugin<PluginState> = new Plugin<PluginState>({
// 该插件的 key
key,
// 该插件自定义的状态
// 其值可能是 null 或是 {transform: tr, from, to, text}
// 用于记录最近的一次由于输入规则成功匹配而触发的 transform 操作及相关信息,便于实现 undoInputRule 指令(撤销由于满足输入规则导致的文档更新)
state: {
// 初始状态是 null
init() { return null; },
// 更新状态(将给定的事务 tr 应用到插件的状态,以生成一个新的状态)
// prev 是插件的当前状态值(即旧的状态值,还没有根据 tr 进行更新)
apply(tr, prev) {
// 获取存储在当前事务 tr 中的元信息,键名为 plugin
const stored = tr.getMeta(plugin);
// 如果存在相应的元信息,将其返回作为该插件的新状态
// 该值的类型是 {transform: tr, from, to, text} 它是在用户输入文本时,如果与(通过该插件所有添加的)任何一个输入规则相匹配,就会执行预设的操作,并分发事务 tr 来将更改应用到编辑器,而在该事务 tr 中就添加了上述元信息
// 具体逻辑可以查看后面的函数 run
if (stored) return stored;
// 如果当前事务 tr 中没有找到相应的元信息,而且该事务导致文档选区或内容的改变,则返回 null(以清空存储在插件的状态中的,最近一次由于输入规则所导致的文档变更信息)
return tr.selectionSet || tr.docChanged ? null : prev;
},
},
// 监听相关输入操作,以对输入的内容进行模式匹配,具体操作封装在 run 函数中
props: {
handleTextInput(view, from, to, text) {
return run(view, from, to, text, rules, plugin);
},
handleDOMEvents: {
compositionend: (view) => {
setTimeout(() => {
const { $cursor } = view.state.selection as TextSelection;
if ($cursor) run(view, $cursor.pos, $cursor.pos, '', rules, plugin);
});
},
},
},
// 为该插件所自定义的属性,以表示该插件是为编辑器添加输入规则的
isInputRules: true,
});
return plugin;
};
const MAX_MATCH = 500; // 设置所需匹配的最大字符数量
// 当用户输入内容时会运行以下函数,对输入的文字(及其前方的部分内容)进行正则表达式的匹配
// 在用户每次敲击键盘输入时,handleTextInput 都会触发 run 运行,所传入的参数会根据输入的情况而不同:
// * 若采用的是「合成输入」法,则输入过程中,参数 from 和 to 是相同的,都是输入字符后光标所在的位置,参数 text 是键盘当前所按下的单个字符;而输入结束时,参数 from 和 to 是这一次「合成输入」所累计键入的字符片段的开头和结尾的位置,参数 text 则是最终所「合成」的文字片段(所以 from 和 to 并不一定是最终显示在页面上的内容的开始和结束位置)
// * 若采用用普通的输入法,则参数 from 和 to 总是相同的,都是输入字符后光标所在的位置,参数 text 是键盘当前所按下的单个字符
// 而 compositionend 触发 run 运行只会发生在「合成输入」结束时,但是参数 from 和 to 是相同的,都是「合成输入」最后键入的字符的右侧(该位置并不一定是最终显示在页面上的内容的右侧位置)
function run(view: EditorView, from: number, to: number, text: string, rules: readonly InputRule[], plugin: Plugin) {
if (view.composing) return false; // 如果当前处于「合成输入」,则不进行处理
const state = view.state;
const $from = state.doc.resolve(from);
// 获取所需匹配的字符串,包括当前输入的内容 text,以及从它的前方所截取的部分内容
// 从输入开始位置的前面截取内容:
// * 起始位置:$from.parentOffset(输入开始时相对于父节点内容开头的位置)向前 MAX_MATCH 个字符的位置,但是最小值是 0(即该父节点内容的开始位置)
// * 结束位置:输入开始时的位置
// 如果截取内容中包含非文本的叶子节点(例如图片),则使用 '\ufffc'(对象替代字符)将其转译为字符
const textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, null, '\ufffc') + text;
// 遍历该插件所添加的输入规则 rules,对光标前方的 textBefore 字符串进行匹配
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
// 需要考虑所需匹配的字符串是否包含 code 代码,以及当前所遍历的输入规则 rule 是否适用于对代码进行匹配
if ($from.parent.type.spec.code) {
if (!rule.inCode) continue;
}
else if (rule.inCode === 'only') {
continue;
}
// 使用当前所遍历的输入规则 rule 的正则表达式 match 对字符串进行匹配
const match = rule.match.exec(textBefore);
// 如果匹配成功,则调用当前所遍历的输入规则 rule 所预设的处理器 handler
// 如果返回一个事务 tr 则后续可以通过编辑器视图进行分发,来触发编辑器更新
const tr = match && rule.handler(state, match, from - (match[0].length - text.length), to);
if (!tr) continue;
// 如果该输入规则对文档的处理是可撤销 undoable
// 则为即将分发的事务 tr 添加元信息(将该操作存储到该插件的自定义状态中)
if (rule.undoable) tr.setMeta(plugin, { transform: tr, from, to, text });
// 分发事务
view.dispatch(tr);
return true;
}
return false;
}
撤销操作
该模块导出了一个 command 指令 undoInputRule 用于撤销由输入规则所触发的转换操作(如果该操作是编辑器的最后一次执行的转换操作)
预设输入规则
该模块导出了一些输入规则(一般针对英文输入环境),可以直接使用官方内置的这些输入规则,也可以参考学习其源码以创建适合自己场景的输入规则
emDash变量:一个输入规则实例,将两个短斜杠 double dash 转换为一个长破折号 emdashellipsis变量:一个输入规则实例,将三个点 three dots 转换为一个省略号 ellipsisopenDoubleQuote变量:一个输入规则实例,将输入的一系列符号(例如"普通双引号)「智能」地转换为“左弯(开放)双引号 opening double quote
openDoubleQuote closeDoubleQuote变量:一个输入规则的实例,将输入的"(普通双引号)符号「智能」地转换为右弯(关闭)双引号 closing double quoteopenSingleQuote变量:一个输入规则的实例,将输入的一系列符号(例如'普通单引号)「智能」地转换为左弯(开放)单引号 opening single quote
openSingleQuote closeSingleQuote变量:一个输入规则的实例,将输入的'(普通单引号)符号「智能」地转换为右弯(关闭)单引号 closing single quotesmartQuotes变量:一个数组(每个元素都是一个输入规则的实例),包含是上述与引号相关的 4 种输入规则ts// 源码 export const smartQuotes: readonly InputRule[] = [openDoubleQuote, closeDoubleQuote, openSingleQuote, closeSingleQuote]
其他
该模块还提供了一些辅助函数,用于更快速地构建输入规则实例
wrappingInputRule(regexp, nodeType, getAttrs?, joinPredicate?)方法:根据传入的参数创建一个输入规则实例,(符合匹配条件时)它将光标所在的 textblock 文本块包裹在给定类型的节点中(作为该节点的内容)
各参数的具体说明如下:- 第一个参数
regexp是一个 RegExp 正则表达式对象,用于匹配文本。在该方法的内部这个参数会直接传递给 classInputRule的构造函数,以用作实例化。如果希望将整个 textblock 文本节点都被包裹在给定的节点中,可以在正则表达式中使用锚点^作为开始,以表示要在文本的开头就开始匹配 - 第二个参数
nodeType是节点类型对象,以表示使用该类型的节点来包裹匹配的文本 - 第三个(可选)参数
getAttrs用于设置节点的 attributes 相关属性(默认值是null表示不为节点设置 attribute)。它可以是一个对象包含节点 attributes 相关属性;也可以是一个函数fn(match: RegExpMatchArray)(其返回值是一个对象或null)以动态的形式设置节点的 attributes 相关属性,它接受正则表达式(通过方法RegExp.exec())的匹配结果数组作为入参 - 第四个(可选)参数
joinPredicate是一个函数fn(match, node)它返回一个布尔值,以表示如果新生成的节点与前一个相邻的节点类型相同时,是否将两者进行合并(默认值为true,即进行合并)。通过该方法可以对同类型节点合并进行灵活度更高的定制操作。
该函数接受的参数具体说明如下:- 第一个参数
match是正则表达式(通过方法RegExp.exec())的匹配结果数组 - 第二个参数
node是包裹节点的前一个(同类型)节点实例
- 第一个参数
- 第一个参数
textblockTypeInputRule(regexp, nodeType, getAttrs?)方法:根据传入的参数创建一个输入规则实例,(符合匹配条件时)它将光标所在的 textblock 文本块的类型转换为给定类型的节点
各参数的具体说明如下:- 第一个参数
regexp是一个 RegExp 正则表达式对象,用于匹配文本。如果希望将整个 textblock 文本节点都被包裹在给定的节点中,可以在正则表达式中使用锚点^作为开始,以表示要在文本的开头就开始匹配 - 第二个参数
nodeType是节点类型对象,以表示将使用该类型的节点实例来替换匹配的文本 - 第三个(可选)参数
getAttrs用于设置节点的 attributes 相关属性(默认值是null表示不为节点设置 attribute)。它可以是一个对象包含节点 attributes 相关属性;也可以是一个函数fn(match: RegExpMatchArray)(其返回值是一个对象或null)以动态的形式设置节点的 attributes 相关属性,它接受正则表达式(通过方法RegExp.exec())的匹配结果数组作为入参
- 第一个参数
注意
在 ProseMirror 预设 handler 处理函数的 input rules 中,都会将匹配到的特殊符号删掉,因为它们一般仅起到 hint 提示/触发执行转换的作用
- 实例化输入规则
new Input(match, handler, options?)如果处理函数handler使用字符串,则使用给出的字符串替换匹配的内容(也相当于删掉匹配的符号)
而如果在实例化输入规则new Input(match, handler, options?)参数handler是函数,则可以自定义如何处理匹配的内容 - 使用辅助函数
wrappingInputRule(regexp, nodeType, getAttrs?, joinPredicate?)匹配的内容会被删掉,然后将所光标所在的 textblock 文本块包裹在指定类型的节点里 - 使用辅助函数
textblockTypeInputRule(regexp, nodeType, getAttrs?)匹配的内容会被删掉,它将光标所在的 textblock 文本块的类型转换为给定类型的节点