CodeMirror代码加亮原理

CodeMirror是一个很好的高亮编辑js库, 为了用CodeMirror编写代码来加亮我自定义的T2Math语言,我必须先研究明白CodeMirror的代码加亮原理。于是我以mode/css/css.js,即CodeMirror库自带的对CSS代码加亮脚本为例进行了研究。

读CodeMirror的用户手册可知,从大的方向看,一个代码加亮脚本的执行其实就是调用两个函数,一个是CodeMirror.defineMode,另一个是CodeMirror.defineMIME。顾名思义这两个函数是用来向总的CodeMirror物体注射加亮逻辑的。CodeMirror.defineMode的第二个参数是一个函数,为了不污染命名空间,CodeMirror手册推荐我们在这个参数里完成所有高亮逻辑。按照手册的介绍以及内置示例代码,我可以总结出,一个基本的高亮脚本的骨架是这样的:

1.CodeMirror.defineMode("t2ku", (config, parserConfig)->
2.  startState:(base)->
3.    baseIndent: base || 0
4.    stack: []
5.  token: (stream, state)->
6.    ch = stream.next()
7.)
8.CodeMirror.defineMIME("text/x-t2ku", "t2ku")

其中defineMode的第二个参数的函数的主要目的是要返回一个Hash,这个Hash必须要有一个token键,其他的键是可选的,可以有startState键、indent键、blankLine键、copyState键、compareStates键、electricChars键。

token键的值是接受stream和state两个参数的函数,这个函数负责主要的逻辑。必须注意的是,token必须通过调用stream.next()等函数来实现高亮过程的前进,试验表明,如果这个token什么都不做(定义为空函数),那么高亮过程会成为一个死循环。原因就在于stream.next()的效果是读取并消费下一个字符,如果定义空的token函数那么脚本将始终不消耗字符,所以进入死循环。

startState键虽然不是必选但也十分重要,因为高亮往往涉及语境,即目前高亮的短语处于一个什么样的上下文中,通常影响语义和颜色的选取。所以需要一个startState来初始化一个状态物体,而这个状态物体具体包含什么内容完全由具体应用决定,CodeMirror没有硬性规定。

例如在以下简单的css语句中,大括号以前的#nav是一个层次的颜色,进入大括号后的float和width又是另外一个层次的颜色,冒号之后的具体值又是另外一种,更不用提注释了。所以只靠token里的stream.next()是不够的,必须保存一个上下文的状态。而startState就是要初始化这个状态。

01.#nav {
02.  float: left;
03.  width: 217px;
04.}
05./* line 374 */
06.#chrome {
07.  margin-left: 217px;
08.  border: 1px solid #F0C3C3;
09.}

下面继续研究css.js,以下假定CodeMirror在页面加载时要加亮上述10行CSS代码。首先一个重要的问题是,在高亮时要维护几个状态物体?是每行一个状态物体?还是整个TextArea整个一个状态物体?还是每个字都关联一个状态物体?

首先,startState执行几次?我在css.js的startState定义中加入计数器window.startStateCnt+=1; 最后发现页面加载完成后显示2次,之后再修改TextArea的值这个数目不再改变。

可是只有两个状态物体吗?不是。我在codemirror.js的 highlightWorker()函数 (位于约1300行)中加入如下一句

1.window.debug_lines = lines;

以方便在Firebug的DOM观察框内查看CodeMirror对用户代码每行的处理。

如图可见CodeMirror将用户的每行输入都包装为一个object,这个object的styles,stateAfter和text三个字段通常非空,其中stateAfter很重要,它表明每行都会关联一个状态物体(第0行除外)。

css.js的状态物体初始化如下。

1.startState: function(base) {
2.  return {tokenize: tokenBase,
3.          baseIndent: base || 0,
4.          stack: []};

这个状态物体带一个栈数据结构。为什么需要栈?在前面举例子时其实就已经感受到,加亮CSS代码需要上下文,而CSS的大括号、冒号这种层级关系从上往下从左往右读时恰好是一个压栈的过程。所以lines[1],lines[2]的stateAfter物体的栈中都有一个'{',而相对比lines[3],lines[4]就没有。

为什么需要这么多状态物体?因为高亮并不是一次性完成的,当用户完输入代码后,他/她会将光标移动到任意一个点,然后修改代码,这时难道要重新解析整个代码吗?是也不是。是是因为,用户修改点之后的代码必须重新高亮,因为用户可能输入一个大括号,从而改变所有之后代码的层级(一个大括号入栈,之后的代码的栈环境均发生改变,而加亮方案要靠栈的元素决定)。不是是因为之前的代码当然可以很安全地认为是不需要重新加亮的,所以如果重新加亮整个代码是没必要的,试想若是几千行的代码,用户每次按键都要重新加亮,岂不是非常低效。

所以,当每次捕获加亮任务,程序应该从这个修改点往后进行加亮。而实际上CodeMirror也是这么做的。这个多状态物体,就是为了能很快的重新从某个点开始重新加亮。

但是,这么多状态物体,初始化却只调用2次,其余的是从那里生成的?CodeMirror其实会帮你“备份”这些状态物体。请见CodeMirror源代码对copyState函数的定义。

posted @ 2011-10-25 10:23  dodo-yufan  阅读(19536)  评论(0)    收藏  举报