文章截图 - 更好的排版
源代码下载
dp.SyntaxHighlighter代码分析
dp.SyntaxHighlighter作为一个最常用的JavaScript代码高亮工具受到广泛的欢迎。
那么你有没有想知道它的内部实现机制是什么,本文在对其分析后,抽取核心代码以重现其功能。
注:本文中的代码高亮使用的就是手工打造的dp.SyntaxHighlighter的简化版。
核心思想
1. 首先定义了一系列的正则表达式用来匹配所有的高亮代码,包括关键字、单行注释、多行注释、字符串, 如果你仔细观察高亮后的JavaScript代码,你会发现也只有这些代码被高亮显示了,是不是很简单。
var keywords = 'abstract boolean break byte case catch char class const continue debugger ' + 'default delete do double else enum export extends false final finally float ' + 'for function goto if implements import in instanceof int interface long native ' + 'new null package private protected public return short static super switch ' + 'synchronized this throw throws transient true try typeof var void volatile while with'; var regexList = [{ regex: new RegExp('/\\*[\\s\\S]*?\\*/', 'gm'), css: 'comment' }, { regex: new RegExp('//.*$', 'gm'), css: 'comment' }, { regex: new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"', 'g'), css: 'string' }, { regex: new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'", 'g'), css: 'string' }, { regex: new RegExp(getKeywords(keywords), 'gm'), css: 'keyword'}]; function getKeywords(str) { return '\\b' + str.replace(/ /g, '\\b|\\b') + '\\b'; }
2. 接下来我们需要通过JavaScript中的正则匹配来找到所有的匹配项。
for (var i = 0; i < regexList.length; i++) { var match = null; while ((match = regexList[i].regex.exec(source)) != null) { matchs.push({ value: match[0], index: match.index, length: match[0].length, css: regexList[i].css }); } }
3. 然后对匹配项进行排序(按照其在原始代码中的顺序)
function sortCallback(m1, m2) { if (m1.index < m2.index) return -1; else if (m1.index > m2.index) return 1; else { return 0; } } matchs = matchs.sort(sortCallback);
4. 最后拿着这些排好顺序的匹配项,和原始代码进行整合就好了。
注意事项
在拿到排序后的匹配项列表后,还不能和原始代码整合,因为其中有重复项,比如:
var keywords = 'abstract boolean break';会找到如下匹配项:var,'abstract boolean break',abstract,boolean,break这五个匹配项,其中一个字符串,四个关键词。
对于这样的情况,我们还必须起初重复的匹配项。
function isInlcude(matchs, index) { var match = matchs[index]; for (var i = index - 1; i >= 0; i--) { if (matchs[i]) { if (match.index > matchs[i].index && match.index < matchs[i].index + matchs[i].length) { return true; } } } return false; } for (var i = 0; i < matchs.length; i++) { if (isInlcude(matchs, i)) { matchs[i] = null; } }
修改后的源代码
这里只处理JavaScript的情况,对于其他语言的代码,只需要定义不同的正则匹配表达式就行了。
function hightLight(source) { var keywords = 'abstract boolean break byte case catch char class const continue debugger ' + 'default delete do double else enum export extends false final finally float ' + 'for function goto if implements import in instanceof int interface long native ' + 'new null package private protected public return short static super switch ' + 'synchronized this throw throws transient true try typeof var void volatile while with'; var regexList = [{ regex: new RegExp('/\\*[\\s\\S]*?\\*/', 'gm'), css: 'comment' }, { regex: new RegExp('//.*$', 'gm'), css: 'comment' }, { regex: new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"', 'g'), css: 'string' }, { regex: new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'", 'g'), css: 'string' }, { regex: new RegExp(getKeywords(keywords), 'gm'), css: 'keyword'}]; var matchs = [], result = []; function getKeywords(str) { return '\\b' + str.replace(/ /g, '\\b|\\b') + '\\b'; } function sortCallback(m1, m2) { if (m1.index < m2.index) return -1; else if (m1.index > m2.index) return 1; else { return 0; } } function isInlcude(matchs, index) { var match = matchs[index]; for (var i = index - 1; i >= 0; i--) { if (matchs[i]) { if (match.index > matchs[i].index && match.index < matchs[i].index + matchs[i].length) { return true; } } } return false; } function convertSpace(str) { return str.replace(/\t/g, " ").replace(/ /g, " "); } for (var i = 0; i < regexList.length; i++) { var match = null; while ((match = regexList[i].regex.exec(source)) != null) { matchs.push({ value: match[0], index: match.index, length: match[0].length, css: regexList[i].css }); } } if (matchs.length) { matchs = matchs.sort(sortCallback); // 去除重复 for (var i = 0; i < matchs.length; i++) { if (isInlcude(matchs, i)) { matchs[i] = null; } } var i = 0; for (var j = 0; j < matchs.length; j++) { var match = matchs[j]; if (match) { if (match.index > i) { result.push('<span>' + convertSpace(source.substr(i, match.index - i)) + '</span>'); } result.push('<span class="' + match.css + '">' + convertSpace(match.value) + '</span>'); i = match.index + match.length; } } if (i != source.length - 1) { result.push(convertSpace(source.substr(i))); } } var resultLines = result.join("").split("\n"); var out = [], alt = false; out.push('<div class="dp-highlighter"><ol start="1" class="dp-c">'); for (var i = 0; i < resultLines.length; i++) { out.push(alt ? '<li class="alt">' : '<li>'); out.push('<span>'); out.push(resultLines[i] === "" ? " " : resultLines[i]); out.push('</span>'); out.push('</li>'); alt = !alt; } out.push('</ol></div>'); return out.join(""); } $(function() { $("pre.js").each(function() { var $this = $(this); $this.hide().after(hightLight($this.html())); }); });