第4章:基于正则的语法高亮器
说实话,正则表达式这东西,我刚入行时觉得又爱又恨。爱的是它一行代码能顶别人几十行,恨的是调试起来简直要命。但做语法高亮,正则绝对是最趁手的工具。
这一章,我们就手写一个基于正则的语法高亮器。不依赖任何第三方库,纯原生JavaScript搞定。
4.1 高亮函数的核心设计
先想清楚我们要做什么。输入是一段代码字符串,输出是带标签的HTML。每个标签包裹着不同类型的语法单元,比如关键字、字符串、注释。
我个人的习惯是,先搭一个骨架函数:
function highlight(code) {
// 1. 转义HTML特殊字符
// 2. 按规则匹配并替换
// 3. 返回带span的HTML
}
为什么要先转义?你想想看,如果代码里有个<script>,直接塞进HTML里,浏览器会把它当成标签解析。这坑我踩过,输出页面直接乱掉。
转义函数很简单:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
4.2 匹配关键字
关键字匹配是最直观的。比如JavaScript里的function、var、if、else这些。
正则怎么写?
const KEYWORDS = /\b(function|var|let|const|if|else|return|for|while)\b/g;
注意那个\b,表示单词边界。没有它,functionality里的function也会被匹配到。我曾经在项目里漏掉这个,结果高亮出来的代码花花绿绿的,完全没法看。
替换的时候,我们给匹配到的关键字包上:
code = code.replace(KEYWORDS, '<span class="keyword">$1</span>');
4.3 匹配字符串
字符串匹配稍微复杂点。单引号、双引号、模板字符串都得考虑。
先处理双引号字符串:
const STRING_DOUBLE = /"([^"\\]*(\\.[^"\\]*)*)"/g;
这个正则看着有点吓人,其实逻辑很简单:
"匹配开头引号[^"\\]*匹配非引号、非反斜杠的字符(\\.[^"\\]*)*匹配转义序列,比如\"或\\- 最后
"匹配结尾引号
单引号同理,把双引号换成单引号就行:
const STRING_SINGLE = /'([^'\\]*(\\.[^'\\]*)*)'/g;
替换时注意顺序。我建议先处理字符串,再处理关键字。为什么?因为字符串里的关键字不应该被高亮。比如:
var str = "function is a keyword";
这里的function是字符串内容,不是关键字。如果先匹配关键字,它就会被错误高亮。
4.4 匹配注释
注释分单行和多行两种。
单行注释:
const LINE_COMMENT = /\/\/.*/g;
多行注释:
const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
这里用了[\s\S]来匹配任意字符,包括换行。问号表示非贪婪匹配,避免匹配到两个注释之间的内容。
嗯,这里要注意:多行注释的正则如果写错了,可能导致整个文件高亮失败。我遇到过最离谱的一次,是正则写成了贪婪模式,结果一个文件里两个注释之间的所有代码都被当成注释了。
4.5 组装完整的高亮函数
现在我们把所有部分拼起来:
function highlight(code) {
// 先转义
let html = escapeHtml(code);
// 匹配顺序:注释 > 字符串 > 关键字
html = html
.replace(BLOCK_COMMENT, '<span class="comment">$&</span>')
.replace(LINE_COMMENT, '<span class="comment">$&</span>')
.replace(STRING_DOUBLE, '<span class="string">$&</span>')
.replace(STRING_SINGLE, '<span class="string">$&</span>')
.replace(KEYWORDS, '<span class="keyword">$1</span>');
return html;
}
注意替换顺序。注释优先,然后是字符串,最后是关键字。这样能最大程度避免误匹配。
4.6 生成带span标签的HTML
有了高亮函数,生成HTML就简单了。把代码放进<pre><code>里,再调用highlight:
const code = `function hello() {
// 打印问候语
var msg = "Hello, World!";
console.log(msg);
}`;
document.getElementById('output').innerHTML =
'<pre><code>' + highlight(code) + '</code></pre>';
配上CSS样式,效果就出来了:
.keyword { color: #d73a49; font-weight: bold; }
.string { color: #032f62; }
.comment { color: #6a737d; font-style: italic; }
4.7 知识体系总览
下面这张图,帮你理清整个高亮器的核心逻辑:
4.8 实际项目中的坑
讲几个我踩过的坑:
- 正则回溯问题: 复杂的正则可能导致性能问题。比如匹配多行注释时,如果文件很大,非贪婪匹配也可能很慢。我建议对超大文件做分段处理。
- 嵌套注释: 有些语言支持嵌套注释,比如
/* /* */ */。正则很难处理这种情况,需要写解析器。对于简单高亮器,我一般忽略嵌套。 - 模板字符串: JavaScript的模板字符串里可以嵌套表达式,比如
`Hello ${name}`。这超出了正则的能力范围,需要更复杂的解析。
好了,这一章的内容就到这里。代码示例都在上面,你可以直接复制运行。下一章我们会聊更高级的解析方式,敬请期待。