CodeMirror是一个基于JavaScript的代码编辑器,CodeMirror支持大量语言的语法高亮,也包括css,html,js等的高亮显示。此外,CodeMirror还支持代码自动完成、搜索/替换、HTML预览、行号、选择/搜索结果高亮、可视化tab、代码自动格式等。
CodeMirror源码的github地址:https://github.com/marijnh/CodeMirror/。这几天除了上课之外有空我都是在啃着它的源码,在网上相关资料基本一点都没找到,发觉看起来真是很吃力,这篇总结也只是说个大概原理,具体细节我也很多不明白,虽然很多代码都读得懂,但是串联起来有很大问题,没注释,源码大部分变量都是猜它的意思,大部分函数也真是知道个大概实现什么功能。
CodeMirror之所以能够支持这么多语言的高亮,是由于在它的mode包中定义了多种语言的解析方式,然后对外提供统一的接口。源码中也把这部分内容分为一个层次。下面我主要是对CodeMirror库自带的对JS和CSS代码加亮脚本为例进行了研究。
github:
这个是它定义的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 == "https://www.jb51.net/") { //匹配注释 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函数实现细节还真是不懂....