本文深入解析 JavaScript 中正则表达式实现语法高亮的两种主流思路,对比分析了 Barret Lee 与次碳酸钴的实现方案。重点介绍了次碳酸钴代码中字符串、数字匹配及分组优化的细节,并提供了经过修正的完整正则表达式与 HTML 高亮演示代码,旨在帮助开发者理解原理并提升匹配性能。
正则表达式语法高亮实现概述
经过几天的正则表达式学习,是时候总结整理一下成果了。之前一直想尝试实现语法高亮匹配,但由于水平有限,连示例都难以理解,因此迟迟没有动手。
接下来,我们将深入分析两位开发者次碳酸钴和 Barret Lee 关于语法高亮的实现方案。
首先讨论 Barret Lee 的文章《几个小例子教你如何实现正则表达式highlight高亮》。
初看时只觉得神奇,尤其是那个分步匹配的例子,逻辑清晰且强大。作者也指出,分步匹配仅为了演示方便,能直观展示每一步的匹配结果;若一步到位,用户将难以察觉处理过程。
具体代码如下:
(/^\s+|\s+$/) // 匹配首尾空格
(/(["'])(?:\\.|[^\\\n])*?\1/) // 匹配字符串
(/\/(?!\*|span).+\/(?!span)[gim]*/) // 匹配正则,其中 span 是作者前次处理添加的,笔者认为此处不应出现。
(/(\/\/.*|\/\*[\S\s]+?\*\/)\/) // 匹配注释
(/(\*\s*)(@\w+)(?=\s*)/) // 匹配注释中的标记
(/\b(break|continue|do|for|in|function|if|else|return|switch|throw|try|catch|finally|var|while|with|case|new|typeof|instance|delete|void|Object|Array|String|Number|Boolean|Function|RegExp|Date|Math|window|document|navigator|location|true|false|null|undefined|NaN)\b/) // 匹配关键词
小胡子哥(Barret Lee)似乎并不想重复造轮子,而是希望通过此例弄清造轮子的原理。因此,他的处理点到为止,未做深入细致的处理,显得较为粗糙。当然,这并非批评,只是简单评论。毕竟优秀的语法高亮插件众多,无需重复开发,学习其原理即可。
次碳酸钴源码分析与优化
接下来分析次碳酸钴的文章《如何实现正则表达式的JavaScript的代码高亮》。这篇文章分析得非常详细,笔者仅做补充说明。次碳酸钴的思维一向严谨,笔者初读此文章耗时一个多小时,仅得大概;此次重新分析并自行实现,竟花费半天时间,但收获颇丰,真心学到了很多。
首先来看大体的逻辑结构。
代码如下:
(\/\/.*|\/\*[\S\s]+?\*\/) // 匹配注释
((["'])(?:\\.|[^\\\n])*?\3) // 匹配字符串
\b(break|continue|do|for|in|function|if|else|return|switch|this|throw|try|catch|finally|var|while|with|case|new|typeof|instance|delete|void)\b // 匹配关键词
\b(Object|Array|String|Number|Boolean|Function|RegExp|Date|Math|window|document|navigator|location)\b // 匹配内置对象
\b(true|false)\b // 匹配布尔值
\b(null|undefined|NaN)\b // 匹配各种空值,笔者认为这组与布尔值归为一类较为合适。
(?:[^\W\d]|\$)[\$\w]* // 匹配普通的变量名
(0[xX][0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE]\d+)?) // 匹配数字(前者不占用,这里就会有问题)
(?:[^\)\]\}]|^)(\/(?!\*)(?:\\.|[^\\\/\n])+?\/[gim]*) // 匹配正则
[\S\s] // 其他不能匹配的任意值
原文对最后一个 [\S\s] 的描述指出:我们必须匹配到每一个字符,因为它们都需要做一次 HTML 转义。随后附有详细代码。
这是一篇非常优秀的文章,笔者前前后后至少阅读了不下 10 次,直到前两天才基本完全理解。
不过,该代码仍存在一些微小瑕疵,例如字符串无法匹配折行情况,以及字符串匹配逻辑有待优化。
此外,数字匹配不够全面,仅能匹配 0xff、12.34、1e3 等格式,如 .123、12.3e+3 等格式无法匹配。同时,笔者认为关键词顺序可稍作优化。由于传统型 NFA 引擎仅从左往右匹配,匹配到即停止后续分支操作,因此将最常出现的关键词置于前端,可提升部分性能。最后,建议使用 new RegExp 构造函数,对于代码量大的场景,性能会有所提升。
下面给出笔者优化后的正则表达式及简单 Demo(实际上仅是对次碳酸钴源码的优化)。先来看正则部分:
代码如下:
(\/\/.*|\/\*[\s\S]*?\*\/) // 匹配注释,未修改
("(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*') // 匹配字符串,已优化
\b(true|false|null|undefined|NaN)\b // 匹配布尔和空值,这几项比较常用,分组提前
\b(var|for|if|else|return|this|while|new|function|switch|case|typeof|do|in|throw|try|catch|finally|with|instance|delete|void|break|continue)\b // 匹配关键词,关键词顺序已调整
\b(document|Date|Math|window|Object|location|navigator|Array|String|Number|Boolean|Function|RegExp)\b // 内置对象,单词顺序已调整
(?:[^\W\d]|\$)[\$\w]* // 匹配普通的变量名,未修改
(0[xX][0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?) // 匹配数字,修复了匹配问题
(?:^|[^\)\]\}])(\/(?!\*)(?:\\.|[^\\\/\n])+?\/[gim]*) // 匹配正则,此部分最复杂,情况很多,笔者暂时无力修改
[\s\S] // 匹配其他
笔者合并了布尔和空值至一个分组,并优化了正则分组,因此比原代码减少了 2 个分组。原代码中 2、3 是字符串分组,因为 (["']) 捕获了前面的引号,而笔者的正则未做此处理。关于 (true|false|null|undefined|NaN) 是否放在一个分组,若不喜欢合并,分开亦可。是否为同一分组仅用于区分着色,Sublime Text 下 true|false|null|undefined|NaN 显示同一颜色,而 Notepad++ 仅着色 true|false,对此笔者表示无奈。
完整示例与代码演示
好了,差不多该给出示例了。相信不少人在此之前可能已关闭页面,或只是拉了下滚动条便离开了。但笔者撰写此文旨在给那些认真看完的朋友,只要有一人阅读,便不算白写。示例如下:
代码如下:
// 单行注释
/**
* 多行注释
* @date 2014-05-12 22:24:37
* @name 测试一下
*/
var str1 = "123\"456";
var str2 = '123\'456';
var str3 = "123\
456";
var num = 123;
var arr = [12, 12.34, .12, 1e3, 1e+3, 1e-3, 12.34e3, 12.34e+3, 12.34e-3, .1234e3];
var arr = ["12", "12.34", '.12, 1e3', '1e+3, 1e-3', '12.34e3, 12.34e+3, 12.34e-3', ".1234e3"];
var arr = [/12", "12.34/, /"12\/34"/];
for (var i=0; i<1e3; i++) {
var node = document.getElementById("a"+i);
arr.push(node);
}
function test () {
return true;
}
test();
(function(window, undefined) {
var _re_js = new RegExp('(\\/\\/.*|\\/\\*[\\s\\S]*?\\*\\/)|("(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\')|\\b(true|false|null|undefined|NaN)\\b|\\b(var|for|if|else|return|this|while|new|function|switch|case|typeof|do|in|throw|try|catch|finally|with|instance|delete|void|break|continue)\\b|\\b(document|Date|Math|window|Object|location|navigator|Array|String|Number|Boolean|Function|RegExp)\\b|(?:[^\\W\\d]|\\$)[\\$\\w]*|(0[xX][0-9a-fA-F]+|\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|\\.\\d+(?:[eE][+-]?\\d+)?)|(?:^|[^\\)\\]\\}])(\\/(?!\\*)(?:\\\\.|[^\\\\\\/\\n])+?\\/[gim]*)|[\\s\\S]', 'g');
function prettify(node) {
var code = node.innerHTML.replace(/\r\n|[\r\n]/g, "\n").replace(/^\s+|\s+$/g, "");
code = code.replace(_re_js, function() {
var s, a = arguments;
for (var i = 1; i <= 7; i++) {
if (s = a[i]) {
s = htmlEncode(s);
switch (i) {
case 1: //注释 com
return '<span class="com">' + s + '</span>';
case 2: //字符串 str
return '<span class="str">' + s + '</span>';
case 3: //true|false|null|undefined|NaN val
return '<span class="val">' + s + '</span>';
case 4: //关键词 kwd
return '<span class="kwd">' + s + '</span>';
case 5: //内置对象 obj
return '<span class="obj">' + s + '</span>';
case 6: //数字 num
return '<span class="num">' + s + '</span>';
case 7: //正则 reg
return htmlEncode(a[0]).replace(s, '<span class="reg">' + s + '</span>');
}
}
}
return htmlEncode(a[0]);
});
code = code.replace(/(?:\s*\*\s*|(?: )*\*(?: )*)(@\w+)\b/g, ' * <span class="comkey">$1</span>') // 匹配注释中的标记
.replace(/(\w+)(\s*\(|(?: )*\()|(\w+)(\s*=\s*function|(?: )*=(?: )*function)/g, '<span class="func">$1</span>$2') // 匹配函数
return code;
}
function htmlEncode(str) {
var i, s = {
//"&": /&/g,
""": /"/g,
"'": /'/g,
"<": //g,
"<br>": /\n/g,
" ": / /g,
" ": /\t/g
};
for (i in s) {
str = str.replace(s[i], i);
}
return str;
}
window.prettify = prettify;
})(window);
你们可以使用下面的代码进行测试。
代码:
代码如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<style>
/* 高亮样式 */
*{font-size:12px;}
code{word-break:break-all;}
.com {color:#008000;} /* 注释 */
.comkey {color:#FFA500;} /* 注释标记 */
.str {color:#808080;} /* 字符串 */
.val {color:#000080;} /* true|false|null|undefined|NaN */
.kwd {color:#000080;font:bold 12px 'comic sans ms', sans-serif;} /* 关键词 */
.obj {color:#000080;} /* 内置对象 */
.num {color:#FF0000;} /* 数字 */
.reg {color:#8000FF;} /* 正则 */
.func {color:#A355B9;} /* 函数 */
</style>
</head>
<body>
<code id="regdemon">
// 单行注释
/**
* 多行注释
* @date 2014-05-12 22:24:37
* @name 测试一下
*/
var str1 = "123\"456";
var str2 = '123\'456';
var str3 = "123\
456";
var num = 123;
var arr = [12, 12.34, .12, 1e3, 1e+3, 1e-3, 12.34e3, 12.34e+3, 12.34e-3, .1234e3];
var arr = ["12", "12.34", '.12, 1e3', '1e+3, 1e-3', '12.34e3, 12.34e+3, 12.34e-3', ".1234e3"];
var arr = [/12", "12.34/, /"12\/34"/];
for (var i=0; i<1e3; i++) {
var node = document.getElementById("a"+i);
arr.push(node);
}
function test () {
return true;
}
test();
(function(window, undefined) {
var _re_js = new RegExp('(\\/\\/.*|\\/\\*[\\s\\S]*?\\*\\/)|("(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\')|\\b(true|false|null|undefined|NaN)\\b|\\b(var|for|if|else|return|this|while|new|function|switch|case|typeof|do|in|throw|try|catch|finally|with|instance|delete|void|break|continue)\\b|\\b(document|Date|Math|window|Object|location|navigator|Array|String|Number|Boolean|Function|RegExp)\\b|(?:[^\\W\\d]|\\$)[\\$\\w]*|(0[xX][0-9a-fA-F]+|\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|\\.\\d+(?:[eE][+-]?\\d+)?)|(?:^|[^\\)\\]\\}])(\\/(?!\\*)(?:\\\\.|[^\\\\\\/\\n])+?\\/[gim]*)|[\\s\\S]', 'g');
function prettify(node) {
var code = node.innerHTML.replace(/\r\n|[\r\n]/g, "\n").replace(/^\s+|\s+$/g, "");
code = code.replace(_re_js, function() {
var s, a = arguments;
for (var i = 1; i <= 7; i++) {
if (s = a[i]) {
s = htmlEncode(s);
switch (i) {
case 1: //注释 com
return '<span class="com">' + s + '</span>';
case 2: //字符串 str
return '<span class="str">' + s + '</span>';
case 3: //true|false|null|undefined|NaN val
return '<span class="val">' + s + '</span>';
case 4: //关键词 kwd
return '<span class="kwd">' + s + '</span>';
case 5: //内置对象 obj
return '<span class="obj">' + s + '</span>';
case 6: //数字 num
return '<span class="num">' + s + '</span>';
case 7: //正则 reg
return htmlEncode(a[0]).replace(s, '<span class="reg">' + s + '</span>');
}
}
}
return htmlEncode(a[0]);
});
code = code.replace(/(?:\s*\*\s*|(?: )*\*(?: )*)(@\w+)\b/g, ' * <span class="comkey">$1</span>') // 匹配注释中的标记
.replace(/(\w+)(\s*\(|(?: )*\()|(\w+)(\s*=\s*function|(?: )*=(?: )*function)/g, '<span class="func">$1</span>$2') // 匹配函数
return code;
}
function htmlEncode(str) {
var i, s = {
//"&": /&/g,
""": /"/g,
"'": /'/g,
"<": /</g,
">": />/g,
"<br>": /\n/g,
" ": / /g,
" ": /\t/g
};
for (i in s) {
str = str.replace(s[i], i);
}
return str;
}
window.prettify = prettify;
})(window);
</code>
<script>
(function(window, undefined) {
var _re_js = new RegExp('(\\/\\/.*|\\/\\*[\\s\\S]*?\\*\\/)|("(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\')|\\b(true|false|null|undefined|NaN)\\b|\\b(var|for|if|else|return|this|while|new|function|switch|case|typeof|do|in|throw|try|catch|finally|with|instance|delete|void|break|continue)\\b|\\b(document|Date|Math|window|Object|location|navigator|Array|String|Number|Boolean|Function|RegExp)\\b|(?:[^\\W\\d]|\\$)[\\$\\w]*|(0[xX][0-9a-fA-F]+|\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|\\.\\d+(?:[eE][+-]?\\d+)?)|(?:^|[^\\)\\]\\}])(\\/(?!\\*)(?:\\\\.|[^\\\\\\/\\n])+?\\/[gim]*)|[\\s\\S]', 'g');
function prettify(node) {
var code = node.innerHTML.replace(/\r\n|[\r\n]/g, "\n").replace(/^\s+|\s+$/g, "");
code = code.replace(_re_js, function() {
var s, a = arguments;
for (var i = 1; i <= 7; i++) {
if (s = a[i]) {
s = htmlEncode(s);
switch (i) {
case 1: //注释 com
return '<span class="com">' + s + '</span>';
case 2: //字符串 str
return '<span class="str">' + s + '</span>';
case 3: //true|false|null|undefined|NaN val
return '<span class="val">' + s + '</span>';
case 4: //关键词 kwd
return '<span class="kwd">' + s + '</span>';
case 5: //内置对象 obj
return '<span class="obj">' + s + '</span>';
case 6: //数字 num
return '<span class="num">' + s + '</span>';
case 7: //正则 reg
return htmlEncode(a[0]).replace(s, '<span class="reg">' + s + '</span>');
}
}
}
return htmlEncode(a[0]);
});
code = code.replace(/(?:\s*\*\s*|(?: )*\*(?: )*)(@\w+)\b/g, ' * <span class="comkey">$1</span>') // 匹配注释中的标记
.replace(/(\w+)(\s*\(|(?: )*\()|(\w+)(\s*=\s*function|(?: )*=(?: )*function)/g, '<span class="func">$1</span>$2') // 匹配函数
return code;
}
function htmlEncode(str) {
var i, s = {
//"&": /&/g,
""": /"/g,
"'": /'/g,
"<": /</g,
">": />/g,
"<br>": /\n/g,
" ": / /g,
" ": /\t/g
};
for (i in s) {
str = str.replace(s[i], i);
}
return str;
}
window.prettify = prettify;
})(window);
var code = document.getElementById("regdemon");
code.innerHTML = prettify(code);
</script>
</body>
</html>
这大致结合了小胡子哥和次碳酸钴两种思路的结果,目前看来较为完善。至于兼容性,尚未测试,也无需测试,因为笔者并未打算自行编写各种语法的高亮插件,那实在太累。