CodeMirror是一个基于JavaScript的代码编辑器,CodeMirror支持大量语言的语法高亮,也包括css,html,js等的高亮显示。此外,CodeMirror还支持代码自动完成、搜索/替换、HTML预览、行号、选择/搜索结果高亮、可视化tab、代码自动格式等。
CodeMirror源码的github地址:https://github.com/marijnh/CodeMirror/。这几天除了上课之外有空我都是在啃着它的源码,在网上相关资料基本一点都没找到,发觉看起来真是很吃力,这篇总结也只是说个大概原理,具体细节我也很多不明白,虽然很多代码都读得懂,但是串联起来有很大问题,没注释,源码大部分变量都是猜它的意思,大部分函数也真是知道个大概实现什么功能。
CodeMirror之所以能够支持这么多语言的高亮,是由于在它的mode包中定义了多种语言的解析方式,然后对外提供统一的接口。源码中也把这部分内容分为一个层次。下面我主要是对CodeMirror库自带的对JS和CSS代码加亮脚本为例进行了研究。
github:https://github.com/marijnh/CodeMirror/blob/master/mode/javascript/javascript.js
这个是它定义的js解析方式,下面我用mode.js代替该js文件
mode.js中主要定义了两个函数:
CodeMirror.defineMode("javascript",function(config,parserConfig){} CodeMirror.defineMIME("text/javascript", "javascript");
这两个define的作用主要是挂靠到CodeMirror这个主体类中
mode.js 对外提供的接口主要是:
return{ startState:function(basecolumn){...} token:function(stream,state){...} indent:function(state,textAfter){...} }
现在解析这三个函数:
(1)startState:主要是定义函数解析执行的上下文环境,起始的状态,如果没有这个方法的话,相当于在解析过程中没有了语义。
startState键虽然不是必选但也十分重要,因为高亮往往涉及语境,即目前高亮的短语处于一个什么样的上下文中,通常影响语义和颜色的选取。所以需要一个startState来初始化一个状态物体,而这个状态物体具体包含什么内容完全由具体应用决定,CodeMirror没有硬性规定。
(2)token:这是最主要的解析语法函数,通过调用state.tokenize(stream,state)执行 function jsTokenBase(stream, state) {...},下面我会解析这个函数的主要内容.
(3)indent:这个是可有可无的
说下jsTokenBase这个函数,通过stream.next()读取下一个字符,并对字符进行判断,主要用到了正则匹配,返回的结果???
function jsTokenBase(stream,state){ var ch = stream.next(); if(ch == '”' || ch=”'”) return ...; //判断是否存在下个”或',return [“string”,”string”] else if(/[\[\]{}\(\),;\:\.]/.test(ch)) return .. ; //匹配[]{}()...这几个,return ch else if(ch==”0” && stream.eat(/x/i)){ stream.eatwhile(/[\da-f]/i); //0x**,解析16进制数 return ret(“number”,”number”);//返回一个自己封装好的对象function ret(tp,style,cont) } else if(/\d/.test(ch) || ch ==“”&&stream.eat(/\d/)) return ret(“number”,”number”);//匹配数字 else if (ch == "/") { //匹配注释 if(stream.eat(“*”)) return [“comment”,”comment”]; //判断“/*” else if(stream.eat(“/”)) return [“comment”,”comment”]; //判断“//” else if (state.lastType == "operator" || state.lastType == "keyword c" || /^[\[{}\(,;:]$/.test(state.lastType)) {} //?? else if(stream.eatWhile(isOperatorChar)) return ret(“operator”); //判断/之后的操作符 } else if(ch == "#") return [“error”,”error”]; //返回语句是错误的 else if(isOperatorChar.test(ch)) return ret(“operator”); //返回操作符 else { stream.eatWhile(/[\w\$_]/); return ..} //返回匹配字符串 }
上面这个只是判断每一个ch = stream.next() 是属于什么类型的字符,也就是知道现在的字符是属于符号,字符串,数字,注释还是其他的.
接着,更重点的还是后面的字符串栈,其实在代码里面是可以看到栈的影子的。就像编译原理里面的语法分析和语义分析,你需要扫描字符串中的每个字符,并判断是否进栈或者规约,这学期的编译原理没特别认真去学,还得重新复习一遍。在前面举例子时其实就已经感受到,加亮JS或CSS代码需要上下文,而JS或CSS的大括号、冒号这种层级关系从上往下从左往右读时恰好是一个压栈的过程。
For example: function pushcontext(){...} function popcontext() {...} function pushlex(type,info){..} function poplex(){...}
然后通过function statement(type){}等进行调用。
另外要说的一点是,上面判断中为什么需要标记这么多的状态?因为高亮并不是一次性完成的,当用户完输入代码后,可能会将光标移动到任意一个点,然后修改代码,这时难道要重新解析整个代码吗?不是,但是某种程度上来说也是。是,因为用户修改点之后的代码必须重新高亮,因为用户可能输入一个大括号,从而改变所有之后代码的层级(一个大括号入栈,之后的代码的栈环境均发生改变,而加亮方案要靠栈的元素决定)。也不是。因为之前的代码当然可以很安全地认为是不需要重新加亮的,所以如果重新加亮整个代码是没必要的,试想若是几千行的代码,用户每次按键都要重新加亮,岂不是非常低效。所以,当每次捕获加亮任务,程序应该从这个修改点往后进行加亮。而实际上CodeMirror也是这么做的。这个多状态物体,就是为了能很快的重新从某个点开始重新加亮。CodeMirror其实会帮你“备份”这些状态物体(copystate函数),对于源代码中的copyState函数实现细节还真是不懂....
相比JS的mode文件,CSS会感觉简单点,容易理解点..原理也差不多,就不多说一遍了,总体上是定义大堆的keyword,然后对于每个关键符号进行判断,也用到了stack.
github源码:https://github.com/marijnh/CodeMirror/blob/master/mode/css/css.js
现在转到CodeMirror的主函数,html的调用方式为:
var editor = CodeMirror.fromTextArea(document.getElementById("code"), { mode: "application/xml", styleActiveLine: true, //line选择是是否加亮 lineNumbers: true, //是否显示行数 lineWrapping: true, //是否自动换行 });
其实调用过程中还可以传递更多自定义的参数,不过这里就不是讨论重点了。总之是把自定义的属性整合到CodeMirror的defaultConfig中。
在CodeMirror里通过functionhighlightLine(cm,line,state){}调用 function runMode(cm,text,mode,state,f) 再者通过 mode.token(stream,state) 调用mode.js对外公开的接口token。
在hightlightLine()函数执行前进行了大量的配置定义和分行然后格式化对应的字符串.剩下部分前两天看了不过现在还真得再看一遍才能理清楚思路啦,几千行的代码,用最笨的方法看..
如果简单的词语高亮,而且不需要考虑到很复杂的语义,用正则表达式可以简单解决..如:
var kw1 = new RegExp("(if|while|with|else|do|try|finally|return|break|continue|new|delete|throw|var|function|catch|for|switch|case|default|typeof|instanceof|true|false|null|undefined|NaN)"), //匹配关键字 kw2 = new RegExp("(\\/\\/[^\n<]*("), //匹配注释
但是正则有时候也会出很多问题,在有语义的情况下,写正则表达式是很麻烦的.高亮一般的方式是采用编译原理里面的语法分析+语义分析,这部分是有点难度的。本来还想在CodeMirror基础上改进些东西,但是发现很难,还不如自己写个简单的,过些天有空我会自己尝试下。最近开始期末考,然后还有大把综合实验,看书时间都少了。淡定!!保持心态...
附:
CodeMirror的应用可以参考:http://codemirror.net/