使用背景:
最近需要开发一款线上velocity编辑器,用于配置模板的管理。为了能够使用户觉得好用,编辑器需要提供velocity语法高亮,并且能够提供自动联想功能。需求是比较清晰的,但是完全自己写编译器,难度太高,是不可能的。而且,软件业不鼓励重复造轮子。所以,首先搜索现成的插件,主要以javascript为主。这方面的资料并不算多,有的介绍也是比较简单,最近Google被封死,部分软件也抵不过新墙的坚硬,外文资料基本为空。查找到主要是对文本显示提供高亮,并不对文本提供编辑功能。有个codepress,新版本似乎只兼容IE,而且textarea获取焦点的时候,所有文本的换行符都消失,很不便于真正的操作。只能再找,花了近半天时间,一开始试验了几款,都不算接近需求。终于,众里寻他,在下文 的文末介绍到了CodeMirror,博文中以“灰常推荐”说明,进入官网一看,哈哈,太满意了。所以,就有了接下去一天半的整合开发。用的很愉快,特此记录下来。
CodeMirror简介:
CodeMirror是一个利用JavaScript实现代码编辑器。它为使用者提供了几乎覆盖全部流行编程语言的代码高亮和自动缩进功能,在浏览器之上构建了一个简易的IDE。
在textarea中我们希望能够实现代码高亮,以便可以在线上做编辑,并提交到后台。很多editor web编辑器都有类似的功能,比如highlight.js/rainbow/SyntaxHighlighter等,但需要我们手动去修改插件的代码,对于前端不太熟悉,希望拿来即用的开发者,显然不是很好的方案。众里寻他,codemirror正是完美的选择,它是个javascript插件,可以帮助我们实现代码高亮显示,并且在编辑时就可以看到高亮效果。具体的实现原理可以参见http://www.cnblogs.com/zgqys1980/archive/2011/10/25/2223492.html和开发文档。其中,CodeMirror可以编辑的语言高达90多种,包括主流的C/C++/Java/Javascript/Fortran等,还包括比较特殊的velocity/Z80/Jinja2等,而且还支持自定义扩展。更牛犇的是,还具备自动联想提示功能——可见,称其为线上简易IDE是当之无愧的。
好的,介绍完背景,回归到我们的正题,如何实现在线velocity语法高亮和自动联想功能。非常高兴的是,CodeMirror已经提供了velocity语法高亮功能,但是并没有提供velocity的自动联想提示功能,需要我们自己做开发。
自定义autoCompletion
官方说明http://codemirror.net/doc/manual.html#addon_show-hint
主要有如下一段:
Provides a frameworkfor showing autocompletion hints. Defines editor.showHint, whichtakes an optional options object, and pops up a widget that allows the user toselect a completion. Finding hints is done with a hinting functions(the hint option), which is a function that take an editorinstance and options object, and return a{list, from, to} object,where list is an array of strings or objects(thecompletions), and from and to give the start and end of thetoken that is being completed as {line, ch} objects.
通过上面一段说明,可以知道showHint的数据来源是一个hint函数,该函数返回值是一个{list, from, to}对象——这样我们查看addon/hind文件下的实例就有把握了。比如我改造的javascript-hint.js文件,搜索函数返回值list,可以发现文件中有一个list,作为函数scriptHint()的返回值,该函数被javascriptHint函数直接调用,而该函数又被如下注册到CodeMirror对象中:
CodeMirror.registerHelper("hint","javascript", javascriptHint);
再找sql-hint.js文件,同样有
CodeMirror.registerHelper("hint","sql", sqlHint);
可以推测,registerHelper函数的第二参数是auto-completion的对象——当且把它认为是hint的名称,应该和mode的名称一致。这样,我们自定义的velocity也即改为
CodeMirror.registerHelper("hint","velocity", velocityHint);
根据文档解释,如果这么注册,那么在指定mode的情况下,hind就会去找相应的已注册的同名hint,然后调用相应的方法进行解析。即删除如下代码:
function getCoffeeScriptToken(editor, cur) {
// This getToken, it is for coffeescript, imitates the behavior of
// getTokenAt method in javascript.js, that is, returning"property"
// type and treat "." as indepenent token.
var token = editor.getTokenAt(cur);
if (cur.ch == token.start + 1 &&token.string.charAt(0) == '.') {
token.end = token.start;
token.string = '.';
token.type = "property";
}
else if (/^\.[\w$_]*$/.test(token.string)) {
token.type = "property";
token.start++;
token.string = token.string.replace(/\./, '');
}
return token;
}
functioncoffeescriptHint(editor, options) {
return scriptHint(editor, coffeescriptKeywords,getCoffeeScriptToken, options);
}
CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
Javascript-hint.js中注册了两个hint,其中一个是CoffeeScript,这个不是我们需要的,可以直接注解删除。
继续查看最核心的代码scriptHint(),为了查看各参数含义,使用chrome调试工具
1、editor.getCursor()返回当前光标所在的行数和列数
2、token=getToken()返回当前光标指向的字符串
跳过正则式匹配,直接查看getCompletions(token, context,keywords, options)方法,根据调用方
functionjavascriptHint(editor, options) {
return scriptHint(editor, javascriptKeywords,
function (e, cur) {return e.getTokenAt(cur);},
options);
};
可知传入的keyworks仅为javascriptKeywords这个数组,其它三个数组stringPops/arrayPops/funcPops则是在maybeAdd()函数中做匹配,从字面和内容上看,这三个数组分别对应javascript的String方法、Array方法和函数方法,这个velocity不关心,可以直接干掉,就留下javascriptKeywords,改名为velocityCustomizedKeywords。
functiongetCompletions(token, context, keywords, options) {
var found = [], start = token.string; //found为匹配的数组,start即为查询的字段
function maybeAdd(str) { //判断start是否匹配str,并且未加入到found数组中
if (str.lastIndexOf(start, 0) == 0 &&!arrayContains(found, str))
found.push(str);
}
function gatherCompletions(obj) {
//遍历obj,obj应该是一个数组,注意,这里不是遍历数组,而是遍历数组的元素,所以也需要修改
for (var name in obj)
maybeAdd(name);
}
if (context && context.length) {
这样,开始有把握删除掉其他判断是否是java对象或属性的代码,然后开始自定义解析velocity语法。
搞到这里,我一开始还想着使用字符串转数组的方式处理,但是搞着就发现越来越麻烦,而且代码块很大。旁边的高手提示,使用正则表达式,效率会高很多。以前稍微接触过正则表达式,觉得实在太繁琐,并没有仔细查看,基本上是拿来主义。看来,又一次学习的契机到了。我花了不到一天时间学习,并将学到的整理到文章《正则表达式快速入手,用集合的方式理解正则表达式》。由于我们要做匹配的velocity语法其实是比较简单的,所以匹配写法也不会太复杂。为了避免太多说明性的文字导致读者兴趣寥寥,也不方便自己后续查阅,直接上代码吧!代码中会对重要的细节做注释。
最终结果:
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"),
require("../../mode/velocity/velocity")); // 1
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../../mode/velocity/velocity"], mod);
else
// Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var Pos = CodeMirror.Pos; // A Pos instance represents a position within the text.
function arrayContains(arr, item) { // 判断元素item是否存在数组arr中
if (!Array.prototype.indexOf) {
var i = arr.length;
while (i--) {
if (arr[i] === item) {
return true;
}
}
return false;
}
return arr.indexOf(item) != -1;
}
function scriptHint(editor, context, keywords, getToken, options) { // 处理hint的核心函数,改名为velocityHint(也可以不做修改)
// Find the token at the cursor,获取当前光标指定的字符串
var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token;
return {
list : getCompletions(token, context, keywords, options),
from : Pos(cur.line, fetchStartPoint(token)), // 字符串拼接的初始位置,这个很重要
to : Pos(cur.line, token.end)
};
}
function fetchStartPoint(token) {
var index = token.string.lastIndexOf("\.");
if (index < 0) {
return token.start + 1;
} else {
return token.start + index + 1;
}
}
function velocityHint(editor, options) {
return scriptHint(editor, CodeMirror.velocityContext, CodeMirror.velocityCustomizedKeywords, function(e, cur) {
return e.getTokenAt(cur);
}, options);
};
CodeMirror.registerHelper("hint", "velocity", velocityHint);
function getCompletions(token, context, keywords, options) {
var found = [], start, pointCount, content = token.string; // found为匹配的数组
if (content && content.length) {
start = token.string.charAt(0);
content = content.substring(1, content.lenght);
pointCount = content.split('.').length - 1; // 查看字符串中有多少个.
}
var result = null;
if (start == '$') { // 必须以$开头,这里暂时不解析${}
var regexp = new RegExp("\\b" + content + "\\w+\\.?\\b", "gi");
if (pointCount == 0) {
result = context.match(regexp);
} else {
result = keywords.match(regexp);
}
}
if (result && result.length) {
for (var i = 0; i < result.length; i++) {
if (!arrayContains(found, result[i]) && pointCount > 0) {
found.push(result[i].substring(content.lastIndexOf("\.") + 1, result[i].length));
} else {
found.push(result[i]);
}
}
}
return found;
}
});
调用方式:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
codeMirror测试
Autocomplete Demo
其中,codeMirror提供了通过on外部绑定事件,一般插件都有这个扩展功能。通过这个事件绑定,我们就可以每次输入一个字符,就触发自动联想功能,这样用户的使用就又提升了一层。
界面效果如下,
输入关键字$,则会联想提示出context的内容,可以使用上下键选择,完全摆脱鼠标;选中server后,输入关键字.,又会有联想提示。如果不输入关键字$,则不会有提示。为了满足可以有自定义参数$ser,我们把自动填满字符串的功能禁用掉,即另completeSingle: false。
小结:
刚开始开发时,觉得这个难度不小,第一天花了大半天尝试各种可能的工具。直到找到有velocity高亮并且可以文本编辑的CodeMirror,以为可以偷懒(呵呵,我对前端不是很熟悉,所以多少还是有点畏惧感)。但是没有办法,为了满足需求,还是只能硬着头皮看,感谢旁边的哥们给了一些重要性的提示。我就一步步的查看源码,然后如上揣摩代码的意思——这一点我还是可以的,因为如我非科班出身的,一开始学习,经常是使用这种方式。逐渐地,多看了几遍,理解了大概意思,就开始动手开发,感觉这过程还是很美妙的,自己动手二次开发的信心也大增,也因此知道知道自己对javascript知识的缺漏,知道查阅哪些书籍了。呵呵,总之,受益颇多。
对本文有帮助的博客列表如下
http://blog.csdn.net/zxyzhuzong/article/details/12101353