代码高亮实现:不用 highlight.js 也能着色
代码高亮看起来神奇,原理其实很简单:按优先级识别几种 token,套上带颜色的 span。
一、先认识 token 类型
一段代码里需要区分的东西就这几类:
| token | 例子 | 处理优先级 |
|---|---|---|
| 注释 | # xxx // xxx | 最高(注释里的内容不该被识别为关键字) |
| 字符串 | "xxx" 'xxx'` | 高 | |
| 数字 | 123 0x1f | 中 |
| 变量 | $var ${var} | 中 |
| 关键字 | if for server | 中 |
| 标志 | --flag -v | 低 |
二、逐字符扫描
不要试图用正则一把梭——嵌套和转义会让你崩溃。正确做法是从左到右扫描,遇到什么处理什么:
function highlight(code, lang) {
var out = "";
var i = 0;
while (i < code.length) {
var ch = code[i];
// 1. 注释:从注释符到行尾,整段吞掉
if (ch === "#" || code.substr(i, 2) === "//") {
var end = code.indexOf("\n", i);
if (end === -1) end = code.length;
out += span("comment", code.slice(i, end));
i = end;
continue;
}
// 2. 字符串:找到配对的结束引号
if (ch === "\"" || ch === "'") {
var j = i + 1;
while (j < code.length && code[j] !== ch) {
if (code[j] === "\\") j++; // 跳过转义字符
j++;
}
out += span("string", code.slice(i, j + 1));
i = j + 1;
continue;
}
// 3. 数字
if (/[0-9]/.test(ch) && !/\w/.test(code[i - 1] || "")) {
var m = code.slice(i).match(/^\d+(\.\d+)?/);
out += span("number", m[0]);
i += m[0].length;
continue;
}
// 4. 标识符:可能是关键字
var w = code.slice(i).match(/^[A-Za-z_$][\w$]*/);
if (w) {
var word = w[0];
out += keywords.indexOf(word) >= 0
? span("keyword", word)
: escapeHtml(word);
i += word.length;
continue;
}
// 5. 其他字符原样输出
out += escapeHtml(ch);
i++;
}
return out;
}
function span(cls, text) {
return "<span class=\"tok-" + cls + "\">" + escapeHtml(text) + "</span>";
}三、关键字表按语言分
var KEYWORDS = {
bash: ["if","then","else","fi","for","while","do","done","case","esac",
"function","return","export","local","echo","cd","sudo"],
nginx: ["server","location","listen","server_name","root","index",
"proxy_pass","upstream","include","ssl_certificate","return",
"if","set","add_header","try_files"],
js: ["const","let","var","function","return","if","else","for","while",
"class","new","await","async","import","export","try","catch"],
json: ["true","false","null"],
yaml: ["true","false","null","yes","no"],
};不用追求完整的关键字表。把常用的一二十个加上,视觉上就已经「像高亮」了。
四、YAML/TOML 这类配置文件的键名
配置文件里最有辨识度的是键名,可以单独着色:
// 行首的 key: 形式
var m = line.match(/^(\s*)([A-Za-z_][\w.-]*)(\s*:)/);
if (m) {
out += m[1] + span("variable", m[2]) + m[3];
}五、与 Markdown 解析器的配合
关键点:高亮在转义之后。
function codeBlock(code, lang) {
var body = highlight(code, lang); // 内部会 escapeHtml
return "<div class=\"code-block\">"
+ "<div class=\"code-bar\">"
+ "<span class=\"code-lang\">" + (lang || "text") + "</span>"
+ "<button class=\"copy-btn\" data-copy>复制</button>"
+ "</div>"
+ "<pre><code>" + body + "</code></pre>"
+ "</div>";
}这样代码块里的内容不会被二次解析,也不会有 XSS 风险。
六、配色:用 CSS 变量
:root {
--tok-keyword: #cf222e;
--tok-string: #0a3069;
--tok-comment: #6e7781;
--tok-number: #0550ae;
--tok-variable:#953800;
--code-bg: #f6f8fa;
}
[data-theme="dark"] {
--tok-keyword: #ff7b72;
--tok-string: #a5d6ff;
--tok-comment: #8b949e;
--tok-number: #79c0ff;
--tok-variable:#ffa657;
--code-bg: #0f151d;
}
.tok-keyword { color: var(--tok-keyword); }
.tok-comment { color: var(--tok-comment); font-style: italic; }好处是主题切换时代码配色自动跟着变,不用维护两套 CSS 规则。
七、复制到剪贴板
btn.addEventListener("click", function () {
var code = btn.closest(".code-block").querySelector("code").innerText;
function done() {
btn.textContent = "已复制";
setTimeout(() => { btn.textContent = "复制"; }, 1600);
}
function fallback() {
var ta = document.createElement("textarea");
ta.value = code;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
done();
}
// Clipboard API 只在安全上下文(https / localhost)可用
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(code).then(done, fallback);
} else {
fallback();
}
});
file://下navigator.clipboard不可用,所以必须有execCommand兜底。这是本地可用的站点特别要注意的。
八、要不要用现成方案
| 方案 | 体积 | 适用 |
|---|---|---|
| 自写 | ~5KB | 只支持 5~6 种语言,够用 |
| Prism.js | ~50KB(含语言包) | 语言多,主题丰富 |
| highlight.js | ~100KB | 自动识别语言 |
| Shiki | 较大,但质量最高 | 用 VS Code 的配色 |
本站选自写的理由:
- 只写笔记,涉及的语言就 bash/nginx/yaml/json/js 几种;
- 零依赖是本站的核心原则;
- 100KB 的 JS 对一个小站来说太重了。
如果你的博客涉及十几种语言,直接上 Prism 更明智。
九、检查清单
- 注释、字符串内部不被误识别
- 转义字符处理正确(
"a\"b") - 输出经过 HTML 转义
- 深浅色各有配色
- 复制按钮在非 https 环境也能用
代码高亮是那种「看起来复杂,做起来两小时」的功能。自己写一遍,对文本处理的理解会深一层。
