CodeMirror中定制Mode实践
背景介绍
几个月以来“亦非书”一直使用HTML中的textarea元素实现文章编辑功能,用户体验比较差,纯粹的textarea不能对编辑内容自定义样式,并且编辑过程中的用户交互比较差。因此我需要优化编辑体验,对比Manaco和CodeMirror两款业内知名的开源项目之后,我选择基于CodeMirror打造一个针对Kude语法规则的插件。
CodeMirror的官网链接是https://codemirror.net/,Github地址是https://github.com/codemirror/codemirror。
我需要的是定制CodeMirror中的mode,因此其它功能在此不做详述。
Mode
我尝试使用一句话解释Mode,即Mode是以CodeMirror编辑器为载体,针对用户输入的文本内容,按特定语法规则解析之后,实现关键字符高亮的插件。
Mode概念
上图描述的是Mode对象可以包含的钩子函数,其中token函数是必须存在的。
CodeMirror拿到当前编辑框中的所有字符之后,按行拆分,然后逐行调用token函数,token函数返回值为type,CodeMirror自动拼接type到完整的CSS className以渲染HTML,展现给用户。(type最好选用codemirror.css中内置的一些CSS高亮规则,因为如果Mode使用了自定义CSS className,开发者使用第三方theme.css主题时不能完整覆盖)。
Mode中的token函数钩子
token有两个参数,第一个是stream,包含当前解析到的字符位置信息(stream.pos)、当前行内容(stream.string),以及CodeMirror提供的一些方便操作stream对象的函数等。
token中stream的使用
token的参数stream中提供了很多的api,最常用的可能是:
sol判断当前位置是否行首。eol判断当前位置是否行尾。peek取下一个字符。next取下一个字符并使位置+1。skipToEnd使位置跳到当前行尾。column取token开始的位置。current取token开始的位置到当前位置之间的子字符串。indentation取当前行首的空格数(缩进)。- 其它函数方法。。。
实际上,在定制Mode的时候,我并没有借助这些函数。我只是不断手动更新stream.pos,即当前解析的字符在当前行的位置,然后返回对应的type,最终实现目的。
举例分析
用上图举例:
用户输入第一行abcdef,这里只是一段普通文本,不需要高亮展示,所以token返回undefined,并更新位置到行尾。
function token(stream, state) {
// stream.pos ==> 当前位置0
// stream.string ==> 当前行内容'abcdef'
if(stream.string === 'abcdef') {
// stream.pos = stream.string.length;
stream.skipToEnd();
return undefined;
}
}
用户输入第二行abc_def_ghi,假设_是特殊控制字符,即_def_需要高亮显示,当前行最少需要使CodeMirror调用3次token函数钩子。
function token(stream, state) {
if(stream.string.substring(stream.pos).startWith('abc')) {
stream.pos = 3;
return undefined;
}
if(stream.string.substring(stream.pos).startWith('_def_')) {
stream.pos = 3 + 5;
return 'strong';
}
stream.skipToEnd();
return undefined;
}
CodeMirror在行字符位置为3 + 5时,拿到type为strong,自动拼接为cm-strong,这在codemirror.css中的定义是.cm-strong {font-weight: bold;},最终渲染的HTML节点是<span class="cm-strong">_def_</span>。
Mode中的startState函数钩子
startState仅会在整篇文本最开始出执行一次,它需要返回一个对象,包含的一些key value由开发者自定义。主要用途是Mode解析文本过程中暂存一些状态(比如解析一块表格内容,涉及跨行解析,需要借助state)。
token被调用时,state作为第二个参数传递,token函数可以修改state中的值,以传递到之后的行解析。
Mode中的blankLine函数钩子
由于只包含空白字符的行会被忽略,不经过token解析,所以如果需要实现编辑的文本内容遇到空白行修改state,可以在blankLine函数钩子中直接修改state。
注意要点
CodeMirror会暂存每行生成的state快照,以便用户在某一行编辑时,自动从当前行开始至文末之间的所有行重新解析。
如果想借助copyState实现每行重置state是不行的。虽然它在每行开始解析时都会被调用,但是CodeMirror传递到token函数解析时依然是上一行解析完生成的state快照。
总结
实际开发一个Mode的复杂度当然比上文所述高很多,但是万变不离其宗,原理就是如此。实际开发一个CodeMirror Mode可以翻阅官方文档。